dominus-cli 0.6.0 → 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/index.js +2714 -512
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +3786 -294
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/Properties.lua +69 -9
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs6, { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
3
|
+
import path5, { join } from 'path';
|
|
4
4
|
import os3, { tmpdir } from 'os';
|
|
5
5
|
import { nanoid } from 'nanoid';
|
|
6
6
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
7
7
|
import { EventEmitter } from 'events';
|
|
8
|
+
import OpenAI from 'openai';
|
|
8
9
|
import initSqlJs from 'sql.js';
|
|
9
10
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
10
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -17,7 +18,6 @@ import TextInput from 'ink-text-input';
|
|
|
17
18
|
import { execSync } from 'child_process';
|
|
18
19
|
import { randomBytes } from 'crypto';
|
|
19
20
|
import Spinner from 'ink-spinner';
|
|
20
|
-
import OpenAI from 'openai';
|
|
21
21
|
import https from 'https';
|
|
22
22
|
import { fileURLToPath } from 'url';
|
|
23
23
|
|
|
@@ -57,14 +57,14 @@ function resolveProviderSettings(config) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
function ensureDir() {
|
|
60
|
-
if (!
|
|
61
|
-
|
|
60
|
+
if (!fs6.existsSync(DOMINUS_DIR)) {
|
|
61
|
+
fs6.mkdirSync(DOMINUS_DIR, { recursive: true });
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
function readJson(filePath, defaults) {
|
|
65
65
|
try {
|
|
66
|
-
if (
|
|
67
|
-
const raw =
|
|
66
|
+
if (fs6.existsSync(filePath)) {
|
|
67
|
+
const raw = fs6.readFileSync(filePath, "utf-8");
|
|
68
68
|
return { ...defaults, ...JSON.parse(raw) };
|
|
69
69
|
}
|
|
70
70
|
} catch {
|
|
@@ -73,7 +73,7 @@ function readJson(filePath, defaults) {
|
|
|
73
73
|
}
|
|
74
74
|
function writeJson(filePath, data) {
|
|
75
75
|
ensureDir();
|
|
76
|
-
|
|
76
|
+
fs6.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
77
77
|
}
|
|
78
78
|
function loadConfig() {
|
|
79
79
|
ensureDir();
|
|
@@ -125,10 +125,10 @@ function createConfigAccess() {
|
|
|
125
125
|
var DOMINUS_DIR, CONFIG_PATH, DB_PATH, RULES_PATH, PROVIDERS, DEFAULTS, DEFAULT_RULES;
|
|
126
126
|
var init_config = __esm({
|
|
127
127
|
"src/config/config.ts"() {
|
|
128
|
-
DOMINUS_DIR =
|
|
129
|
-
CONFIG_PATH =
|
|
130
|
-
DB_PATH =
|
|
131
|
-
RULES_PATH =
|
|
128
|
+
DOMINUS_DIR = path5.join(os3.homedir(), ".dominus");
|
|
129
|
+
CONFIG_PATH = path5.join(DOMINUS_DIR, "config.json");
|
|
130
|
+
DB_PATH = path5.join(DOMINUS_DIR, "dominus.db");
|
|
131
|
+
RULES_PATH = path5.join(DOMINUS_DIR, "rules.json");
|
|
132
132
|
PROVIDERS = {
|
|
133
133
|
openai: {
|
|
134
134
|
name: "OpenAI",
|
|
@@ -325,8 +325,8 @@ var init_ws_server = __esm({
|
|
|
325
325
|
ws.on("close", () => {
|
|
326
326
|
if (this.controllers.has(ws)) {
|
|
327
327
|
this.controllers.delete(ws);
|
|
328
|
-
for (const [id,
|
|
329
|
-
if (controllerWs === ws) this.relayRequests.delete(id);
|
|
328
|
+
for (const [id, relay] of this.relayRequests) {
|
|
329
|
+
if (relay.controllerWs === ws) this.relayRequests.delete(id);
|
|
330
330
|
}
|
|
331
331
|
} else {
|
|
332
332
|
const placeId = this.wsToPlaceId.get(ws);
|
|
@@ -337,12 +337,28 @@ var init_ws_server = __esm({
|
|
|
337
337
|
this.emit("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
338
338
|
this.broadcastToControllers("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
339
339
|
for (const [id, pending] of this.pendingRequests) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
340
|
+
if (pending.targetWs === ws) {
|
|
341
|
+
clearTimeout(pending.timer);
|
|
342
|
+
pending.reject(new Error(`Studio disconnected (place ${placeId})`));
|
|
343
|
+
this.pendingRequests.delete(id);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
for (const [id, relay] of this.relayRequests) {
|
|
347
|
+
if (relay.targetWs === ws) {
|
|
348
|
+
if (relay.controllerWs.readyState === WebSocket.OPEN) {
|
|
349
|
+
relay.controllerWs.send(serializeMessage({
|
|
350
|
+
id,
|
|
351
|
+
type: StudioMsg.RESPONSE,
|
|
352
|
+
payload: { error: `Studio disconnected (place ${placeId})` },
|
|
353
|
+
ts: Date.now()
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
this.relayRequests.delete(id);
|
|
357
|
+
}
|
|
343
358
|
}
|
|
344
359
|
if (this.activePlaceId === placeId) {
|
|
345
360
|
this.activePlaceId = 0;
|
|
361
|
+
this.broadcastTargetState();
|
|
346
362
|
}
|
|
347
363
|
}
|
|
348
364
|
}
|
|
@@ -361,6 +377,7 @@ var init_ws_server = __esm({
|
|
|
361
377
|
}
|
|
362
378
|
const conn = {
|
|
363
379
|
ws,
|
|
380
|
+
connectionId: `${placeId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
364
381
|
connectedAt: Date.now(),
|
|
365
382
|
studioVersion: payload.studioVersion,
|
|
366
383
|
placeId,
|
|
@@ -368,8 +385,12 @@ var init_ws_server = __esm({
|
|
|
368
385
|
};
|
|
369
386
|
this.studioClients.set(placeId, conn);
|
|
370
387
|
this.wsToPlaceId.set(ws, placeId);
|
|
371
|
-
this.
|
|
372
|
-
|
|
388
|
+
if (this.studioClients.size === 1 && this.activePlaceId === 0) {
|
|
389
|
+
this.activePlaceId = placeId;
|
|
390
|
+
}
|
|
391
|
+
this.emit("studio:connected", { ...payload, placeId, connectionId: conn.connectionId });
|
|
392
|
+
this.broadcastToControllers("studio:connected", { ...payload, placeId, connectionId: conn.connectionId });
|
|
393
|
+
this.broadcastTargetState();
|
|
373
394
|
}
|
|
374
395
|
addController(ws) {
|
|
375
396
|
this.controllers.add(ws);
|
|
@@ -384,6 +405,27 @@ var init_ws_server = __esm({
|
|
|
384
405
|
}
|
|
385
406
|
}
|
|
386
407
|
handleControllerMessage(controllerWs, msg) {
|
|
408
|
+
if (msg.type === "controller:set_active_place") {
|
|
409
|
+
const payload = msg.payload;
|
|
410
|
+
try {
|
|
411
|
+
this.setActivePlaceId(Number(payload.placeId ?? 0));
|
|
412
|
+
} catch (err) {
|
|
413
|
+
controllerWs.send(serializeMessage({
|
|
414
|
+
id: msg.id,
|
|
415
|
+
type: StudioMsg.RESPONSE,
|
|
416
|
+
payload: { success: false, error: err instanceof Error ? err.message : String(err) },
|
|
417
|
+
ts: Date.now()
|
|
418
|
+
}));
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
controllerWs.send(serializeMessage({
|
|
422
|
+
id: msg.id,
|
|
423
|
+
type: StudioMsg.RESPONSE,
|
|
424
|
+
payload: { success: true, activePlaceId: this.activePlaceId, connections: this.listConnections() },
|
|
425
|
+
ts: Date.now()
|
|
426
|
+
}));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
387
429
|
const target = this.resolveTarget();
|
|
388
430
|
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
389
431
|
controllerWs.send(serializeMessage({
|
|
@@ -394,7 +436,11 @@ var init_ws_server = __esm({
|
|
|
394
436
|
}));
|
|
395
437
|
return;
|
|
396
438
|
}
|
|
397
|
-
this.relayRequests.set(msg.id,
|
|
439
|
+
this.relayRequests.set(msg.id, {
|
|
440
|
+
controllerWs,
|
|
441
|
+
targetWs: target.ws,
|
|
442
|
+
targetPlaceId: target.placeId ?? 0
|
|
443
|
+
});
|
|
398
444
|
target.ws.send(serializeMessage(msg));
|
|
399
445
|
}
|
|
400
446
|
handleStudioMessage(ws, msg) {
|
|
@@ -414,14 +460,25 @@ var init_ws_server = __esm({
|
|
|
414
460
|
return;
|
|
415
461
|
}
|
|
416
462
|
if (msg.type === StudioMsg.RESPONSE) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
463
|
+
if (!this.wsToPlaceId.has(ws)) return;
|
|
464
|
+
const relay = this.relayRequests.get(msg.id);
|
|
465
|
+
if (relay) {
|
|
466
|
+
if (relay.targetWs !== ws) {
|
|
467
|
+
this.emit("server:error", new Error(`Ignoring relayed response ${msg.id} from non-target Studio`));
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (relay.controllerWs.readyState === WebSocket.OPEN) {
|
|
471
|
+
relay.controllerWs.send(serializeMessage(msg));
|
|
472
|
+
}
|
|
420
473
|
this.relayRequests.delete(msg.id);
|
|
421
474
|
return;
|
|
422
475
|
}
|
|
423
476
|
const pending = this.pendingRequests.get(msg.id);
|
|
424
477
|
if (pending) {
|
|
478
|
+
if (pending.targetWs !== ws) {
|
|
479
|
+
this.emit("server:error", new Error(`Ignoring response ${msg.id} from non-target Studio`));
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
425
482
|
clearTimeout(pending.timer);
|
|
426
483
|
pending.resolve(msg);
|
|
427
484
|
this.pendingRequests.delete(msg.id);
|
|
@@ -471,7 +528,9 @@ var init_ws_server = __esm({
|
|
|
471
528
|
this.pendingRequests.set(msg.id, {
|
|
472
529
|
resolve,
|
|
473
530
|
reject,
|
|
474
|
-
timer
|
|
531
|
+
timer,
|
|
532
|
+
targetWs: target.ws,
|
|
533
|
+
targetPlaceId: target.placeId ?? 0
|
|
475
534
|
});
|
|
476
535
|
target.ws.send(serializeMessage(msg));
|
|
477
536
|
});
|
|
@@ -491,16 +550,28 @@ var init_ws_server = __esm({
|
|
|
491
550
|
listConnections() {
|
|
492
551
|
return [...this.studioClients.values()].filter((c) => c.ws.readyState === WebSocket.OPEN).map((c) => ({
|
|
493
552
|
placeId: c.placeId ?? 0,
|
|
553
|
+
connectionId: c.connectionId,
|
|
494
554
|
placeName: c.placeName,
|
|
495
555
|
studioVersion: c.studioVersion,
|
|
496
|
-
connectedAt: c.connectedAt
|
|
556
|
+
connectedAt: c.connectedAt,
|
|
557
|
+
active: this.activePlaceId === 0 && this.studioClients.size === 1 || c.placeId === this.activePlaceId
|
|
497
558
|
}));
|
|
498
559
|
}
|
|
499
560
|
getActivePlaceId() {
|
|
500
561
|
return this.activePlaceId;
|
|
501
562
|
}
|
|
502
563
|
setActivePlaceId(placeId) {
|
|
564
|
+
if (placeId !== 0 && !this.studioClients.has(placeId)) {
|
|
565
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
566
|
+
}
|
|
503
567
|
this.activePlaceId = placeId;
|
|
568
|
+
this.broadcastTargetState();
|
|
569
|
+
}
|
|
570
|
+
broadcastTargetState() {
|
|
571
|
+
this.broadcastToControllers("controller:target_state", {
|
|
572
|
+
activePlaceId: this.activePlaceId,
|
|
573
|
+
connections: this.listConnections()
|
|
574
|
+
});
|
|
504
575
|
}
|
|
505
576
|
getPort() {
|
|
506
577
|
return this.port;
|
|
@@ -532,6 +603,15 @@ var init_ws_server = __esm({
|
|
|
532
603
|
};
|
|
533
604
|
}
|
|
534
605
|
});
|
|
606
|
+
function toConnectionInfo(c) {
|
|
607
|
+
return {
|
|
608
|
+
connectedAt: c.connectedAt,
|
|
609
|
+
connectionId: c.connectionId,
|
|
610
|
+
studioVersion: c.studioVersion,
|
|
611
|
+
placeId: c.placeId,
|
|
612
|
+
placeName: c.placeName
|
|
613
|
+
};
|
|
614
|
+
}
|
|
535
615
|
async function isPortInUse(port) {
|
|
536
616
|
return new Promise((resolve) => {
|
|
537
617
|
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
@@ -606,12 +686,7 @@ var init_ws_client = __esm({
|
|
|
606
686
|
if (p.connections) {
|
|
607
687
|
this.connections.clear();
|
|
608
688
|
for (const c of p.connections) {
|
|
609
|
-
this.connections.set(c.placeId,
|
|
610
|
-
connectedAt: c.connectedAt,
|
|
611
|
-
studioVersion: c.studioVersion,
|
|
612
|
-
placeId: c.placeId,
|
|
613
|
-
placeName: c.placeName
|
|
614
|
-
});
|
|
689
|
+
this.connections.set(c.placeId, toConnectionInfo(c));
|
|
615
690
|
}
|
|
616
691
|
}
|
|
617
692
|
if (p.activePlaceId !== void 0) {
|
|
@@ -622,12 +697,19 @@ var init_ws_client = __esm({
|
|
|
622
697
|
}
|
|
623
698
|
return;
|
|
624
699
|
}
|
|
700
|
+
if (msg.type === "controller:target_state") {
|
|
701
|
+
const payload = msg.payload;
|
|
702
|
+
this.applyTargetState(payload);
|
|
703
|
+
this.emit("controller:target_state", payload);
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
625
706
|
if (msg.type === "studio:connected") {
|
|
626
707
|
this.studioConnected = true;
|
|
627
708
|
const payload = msg.payload;
|
|
628
709
|
const placeId = payload.placeId ?? 0;
|
|
629
710
|
this.connections.set(placeId, {
|
|
630
711
|
connectedAt: Date.now(),
|
|
712
|
+
connectionId: payload.connectionId ?? `${placeId}`,
|
|
631
713
|
studioVersion: payload.studioVersion,
|
|
632
714
|
placeId,
|
|
633
715
|
placeName: payload.placeName
|
|
@@ -645,6 +727,7 @@ var init_ws_client = __esm({
|
|
|
645
727
|
if (this.activePlaceId === placeId) {
|
|
646
728
|
this.activePlaceId = 0;
|
|
647
729
|
}
|
|
730
|
+
this.studioConnected = this.resolveTargetInfo() !== null;
|
|
648
731
|
this.emit("studio:disconnected", payload);
|
|
649
732
|
return;
|
|
650
733
|
}
|
|
@@ -689,27 +772,57 @@ var init_ws_client = __esm({
|
|
|
689
772
|
this.ws.send(serializeMessage(msg));
|
|
690
773
|
}
|
|
691
774
|
isConnected() {
|
|
692
|
-
return this.
|
|
775
|
+
return this.resolveTargetInfo() !== null;
|
|
693
776
|
}
|
|
694
777
|
getConnectionInfo() {
|
|
695
|
-
|
|
696
|
-
const target = this.activePlaceId !== 0 ? this.connections.get(this.activePlaceId) : this.connections.values().next().value;
|
|
778
|
+
const target = this.resolveTargetInfo();
|
|
697
779
|
if (!target) return null;
|
|
698
780
|
return { ...target, ws: this.ws };
|
|
699
781
|
}
|
|
700
782
|
listConnections() {
|
|
701
783
|
return [...this.connections.values()].map((c) => ({
|
|
702
784
|
placeId: c.placeId ?? 0,
|
|
785
|
+
connectionId: c.connectionId,
|
|
703
786
|
placeName: c.placeName,
|
|
704
787
|
studioVersion: c.studioVersion,
|
|
705
|
-
connectedAt: c.connectedAt
|
|
788
|
+
connectedAt: c.connectedAt,
|
|
789
|
+
active: this.activePlaceId === 0 && this.connections.size === 1 || c.placeId === this.activePlaceId
|
|
706
790
|
}));
|
|
707
791
|
}
|
|
708
792
|
getActivePlaceId() {
|
|
709
793
|
return this.activePlaceId;
|
|
710
794
|
}
|
|
711
795
|
setActivePlaceId(placeId) {
|
|
796
|
+
if (placeId !== 0 && !this.connections.has(placeId)) {
|
|
797
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
798
|
+
}
|
|
712
799
|
this.activePlaceId = placeId;
|
|
800
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
801
|
+
const msg = createMessage("controller:set_active_place", { placeId });
|
|
802
|
+
this.ws.send(serializeMessage(msg));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
applyTargetState(payload) {
|
|
806
|
+
if (payload.activePlaceId !== void 0) {
|
|
807
|
+
this.activePlaceId = payload.activePlaceId;
|
|
808
|
+
}
|
|
809
|
+
if (payload.connections) {
|
|
810
|
+
this.connections.clear();
|
|
811
|
+
for (const c of payload.connections) {
|
|
812
|
+
this.connections.set(c.placeId, toConnectionInfo(c));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
this.studioConnected = this.resolveTargetInfo() !== null;
|
|
816
|
+
}
|
|
817
|
+
resolveTargetInfo() {
|
|
818
|
+
if (this.connections.size === 0) return null;
|
|
819
|
+
if (this.activePlaceId !== 0) {
|
|
820
|
+
return this.connections.get(this.activePlaceId) ?? null;
|
|
821
|
+
}
|
|
822
|
+
if (this.connections.size === 1) {
|
|
823
|
+
return this.connections.values().next().value ?? null;
|
|
824
|
+
}
|
|
825
|
+
return null;
|
|
713
826
|
}
|
|
714
827
|
getPort() {
|
|
715
828
|
return this.port;
|
|
@@ -731,6 +844,148 @@ var init_ws_client = __esm({
|
|
|
731
844
|
};
|
|
732
845
|
}
|
|
733
846
|
});
|
|
847
|
+
function toolToOpenAI(tool31) {
|
|
848
|
+
return {
|
|
849
|
+
type: "function",
|
|
850
|
+
function: {
|
|
851
|
+
name: tool31.name,
|
|
852
|
+
description: tool31.description,
|
|
853
|
+
parameters: tool31.parameters
|
|
854
|
+
}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
var AIProvider;
|
|
858
|
+
var init_provider = __esm({
|
|
859
|
+
"src/ai/provider.ts"() {
|
|
860
|
+
init_config();
|
|
861
|
+
AIProvider = class {
|
|
862
|
+
client;
|
|
863
|
+
model;
|
|
864
|
+
providerName;
|
|
865
|
+
constructor(config) {
|
|
866
|
+
if (!config.apiKey) {
|
|
867
|
+
throw new Error("API key not configured. Run: dominus config set apiKey <your-key>");
|
|
868
|
+
}
|
|
869
|
+
const { baseUrl, model } = resolveProviderSettings(config);
|
|
870
|
+
this.providerName = PROVIDERS[config.provider]?.name ?? config.provider;
|
|
871
|
+
this.client = new OpenAI({
|
|
872
|
+
apiKey: config.apiKey,
|
|
873
|
+
baseURL: baseUrl
|
|
874
|
+
});
|
|
875
|
+
this.model = model;
|
|
876
|
+
}
|
|
877
|
+
setModel(model) {
|
|
878
|
+
this.model = model;
|
|
879
|
+
}
|
|
880
|
+
getModel() {
|
|
881
|
+
return this.model;
|
|
882
|
+
}
|
|
883
|
+
getProviderName() {
|
|
884
|
+
return this.providerName;
|
|
885
|
+
}
|
|
886
|
+
async listModels() {
|
|
887
|
+
try {
|
|
888
|
+
const list = await this.client.models.list();
|
|
889
|
+
const models = [];
|
|
890
|
+
for await (const m of list) {
|
|
891
|
+
models.push({ id: m.id, owned_by: m.owned_by });
|
|
892
|
+
}
|
|
893
|
+
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
894
|
+
return models;
|
|
895
|
+
} catch {
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async chat(messages, tools) {
|
|
900
|
+
const openAITools = tools?.map(toolToOpenAI);
|
|
901
|
+
const response = await this.client.chat.completions.create({
|
|
902
|
+
model: this.model,
|
|
903
|
+
messages,
|
|
904
|
+
tools: openAITools?.length ? openAITools : void 0,
|
|
905
|
+
temperature: 0.3
|
|
906
|
+
});
|
|
907
|
+
const choice = response.choices[0];
|
|
908
|
+
if (!choice) throw new Error("No response from AI");
|
|
909
|
+
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
910
|
+
id: tc.id,
|
|
911
|
+
name: tc.function.name,
|
|
912
|
+
arguments: JSON.parse(tc.function.arguments)
|
|
913
|
+
}));
|
|
914
|
+
return {
|
|
915
|
+
content: choice.message.content,
|
|
916
|
+
toolCalls,
|
|
917
|
+
finishReason: choice.finish_reason ?? "stop",
|
|
918
|
+
usage: response.usage ? {
|
|
919
|
+
promptTokens: response.usage.prompt_tokens,
|
|
920
|
+
completionTokens: response.usage.completion_tokens,
|
|
921
|
+
totalTokens: response.usage.total_tokens
|
|
922
|
+
} : void 0
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
async *streamChat(messages, tools) {
|
|
926
|
+
const openAITools = tools?.map(toolToOpenAI);
|
|
927
|
+
const stream = await this.client.chat.completions.create({
|
|
928
|
+
model: this.model,
|
|
929
|
+
messages,
|
|
930
|
+
tools: openAITools?.length ? openAITools : void 0,
|
|
931
|
+
temperature: 0.3,
|
|
932
|
+
stream: true
|
|
933
|
+
});
|
|
934
|
+
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
935
|
+
for await (const chunk of stream) {
|
|
936
|
+
const delta = chunk.choices[0]?.delta;
|
|
937
|
+
if (!delta) continue;
|
|
938
|
+
if (delta.content) {
|
|
939
|
+
yield { type: "text", content: delta.content };
|
|
940
|
+
}
|
|
941
|
+
if (delta.tool_calls) {
|
|
942
|
+
for (const tc of delta.tool_calls) {
|
|
943
|
+
if (!toolCallBuffers.has(tc.index)) {
|
|
944
|
+
toolCallBuffers.set(tc.index, {
|
|
945
|
+
id: tc.id ?? "",
|
|
946
|
+
name: tc.function?.name ?? "",
|
|
947
|
+
args: ""
|
|
948
|
+
});
|
|
949
|
+
if (tc.function?.name) {
|
|
950
|
+
yield {
|
|
951
|
+
type: "tool_call_start",
|
|
952
|
+
toolCall: { id: tc.id, name: tc.function.name }
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const buffer = toolCallBuffers.get(tc.index);
|
|
957
|
+
if (tc.id) buffer.id = tc.id;
|
|
958
|
+
if (tc.function?.name) buffer.name = tc.function.name;
|
|
959
|
+
if (tc.function?.arguments) {
|
|
960
|
+
buffer.args += tc.function.arguments;
|
|
961
|
+
yield {
|
|
962
|
+
type: "tool_call_delta",
|
|
963
|
+
content: tc.function.arguments,
|
|
964
|
+
toolCall: { id: buffer.id, name: buffer.name }
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (chunk.choices[0]?.finish_reason === "tool_calls") {
|
|
970
|
+
for (const [, buffer] of toolCallBuffers) {
|
|
971
|
+
let args = {};
|
|
972
|
+
try {
|
|
973
|
+
args = JSON.parse(buffer.args);
|
|
974
|
+
} catch {
|
|
975
|
+
}
|
|
976
|
+
yield {
|
|
977
|
+
type: "tool_call_end",
|
|
978
|
+
toolCall: { id: buffer.id, name: buffer.name, arguments: args }
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
toolCallBuffers.clear();
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
yield { type: "done" };
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
});
|
|
734
989
|
|
|
735
990
|
// src/agent/internal-memory.ts
|
|
736
991
|
var INTERNAL_MEMORY;
|
|
@@ -796,6 +1051,8 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
796
1051
|
- Converting UI to Roact/React \u2192 use \`convert_ui_to_code\`
|
|
797
1052
|
- Creating instances \u2192 use \`create_part\`, \`create_ui\`, \`insert_instance\`, \`build_multiple\`
|
|
798
1053
|
- Modifying properties \u2192 use \`set_properties\` or \`bulk_set_properties\`
|
|
1054
|
+
- Bulk font/style/scaling changes \u2192 use \`bulk_set_properties\` with filter mode (root + className + properties)
|
|
1055
|
+
- Adding UIStroke/UICorner to many instances \u2192 use \`bulk_set_properties\` with \`addChildren\` (idempotent)
|
|
799
1056
|
- Reading scripts \u2192 use \`read_script\`
|
|
800
1057
|
- Browsing the explorer \u2192 use \`get_explorer\`
|
|
801
1058
|
\`run_code\` is ONLY for procedural logic, terrain generation, raycasts, physics queries, or custom algorithms with no tool equivalent.
|
|
@@ -805,6 +1062,19 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
805
1062
|
- \`inspect_instance\` \u2014 Returns compact properties + children tree for a single instance in ONE call. Combines get_properties + get_explorer.
|
|
806
1063
|
- \`convert_ui_to_code\` \u2014 Serializes a UI tree and generates a Roact/React component. Use when user says "turn this into a component" or "convert to React".
|
|
807
1064
|
|
|
1065
|
+
### UI Pre-Flight \u2014 ASK BEFORE BUILDING
|
|
1066
|
+
Before creating any non-trivial UI (more than a single element), **ask the user these questions** to avoid rework:
|
|
1067
|
+
- **A) Device scaling** \u2014 "Should this UI scale for mobile/tablet, or is it PC-only?"
|
|
1068
|
+
- Mobile: use Scale-based sizing, UIAspectRatioConstraint, TextScaled or AutomaticSize, avoid large pixel offsets.
|
|
1069
|
+
- PC-only: pixel offsets are fine.
|
|
1070
|
+
- **B) ScreenGui structure** \u2014 "All panels in one ScreenGui, or separate ScreenGuis per panel?"
|
|
1071
|
+
- One: simpler, easy to toggle all at once.
|
|
1072
|
+
- Separate: independent ZIndex, modular show/hide.
|
|
1073
|
+
- **C) Font & style** \u2014 "Any preferred font, color scheme, or style reference?"
|
|
1074
|
+
- Default to Roboto if user has no preference. Use FontFace with Weight=Bold for bold.
|
|
1075
|
+
- Match screenshots closely (transparency, stroke, spacing).
|
|
1076
|
+
- Skip these only if the user already specified preferences or the task is trivially small.
|
|
1077
|
+
|
|
808
1078
|
### Verification habits
|
|
809
1079
|
- After a build, call \`get_explorer\` or \`get_selection\` to confirm the tree looks right.
|
|
810
1080
|
- After editing a script, read it back.
|
|
@@ -851,14 +1121,14 @@ __export(store_exports, {
|
|
|
851
1121
|
function persist() {
|
|
852
1122
|
if (!db) return;
|
|
853
1123
|
const data = db.export();
|
|
854
|
-
|
|
1124
|
+
fs6.writeFileSync(dbPath, Buffer.from(data));
|
|
855
1125
|
}
|
|
856
1126
|
async function initMemoryStore() {
|
|
857
1127
|
if (db) return;
|
|
858
1128
|
dbPath = getDbPath();
|
|
859
1129
|
const SQL = await initSqlJs();
|
|
860
|
-
if (
|
|
861
|
-
const buffer =
|
|
1130
|
+
if (fs6.existsSync(dbPath)) {
|
|
1131
|
+
const buffer = fs6.readFileSync(dbPath);
|
|
862
1132
|
db = new SQL.Database(buffer);
|
|
863
1133
|
} else {
|
|
864
1134
|
db = new SQL.Database();
|
|
@@ -1123,109 +1393,958 @@ CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
|
|
|
1123
1393
|
}
|
|
1124
1394
|
});
|
|
1125
1395
|
|
|
1126
|
-
// src/
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
path: response.payload.path,
|
|
1161
|
-
className: response.payload.className,
|
|
1162
|
-
source: response.payload.source,
|
|
1163
|
-
lineCount: response.payload.lineCount
|
|
1164
|
-
}
|
|
1165
|
-
};
|
|
1396
|
+
// src/agent/json-response.ts
|
|
1397
|
+
function parseJsonResponse(content) {
|
|
1398
|
+
if (!content) return null;
|
|
1399
|
+
const trimmed = content.trim();
|
|
1400
|
+
const candidates = [
|
|
1401
|
+
trimmed,
|
|
1402
|
+
...extractMarkdownJsonBlocks(trimmed),
|
|
1403
|
+
...extractBalancedJsonBlocks(trimmed)
|
|
1404
|
+
];
|
|
1405
|
+
for (const candidate of candidates) {
|
|
1406
|
+
try {
|
|
1407
|
+
return JSON.parse(candidate);
|
|
1408
|
+
} catch {
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return null;
|
|
1412
|
+
}
|
|
1413
|
+
function extractMarkdownJsonBlocks(content) {
|
|
1414
|
+
const matches = content.match(/```(?:json)?\s*([\s\S]*?)```/gi) ?? [];
|
|
1415
|
+
return matches.map((block) => block.replace(/```(?:json)?/i, "").replace(/```$/, "").trim()).filter(Boolean);
|
|
1416
|
+
}
|
|
1417
|
+
function extractBalancedJsonBlocks(content) {
|
|
1418
|
+
const blocks = [];
|
|
1419
|
+
const openers = ["{", "["];
|
|
1420
|
+
for (let start = 0; start < content.length; start++) {
|
|
1421
|
+
if (!openers.includes(content[start])) continue;
|
|
1422
|
+
let depth = 0;
|
|
1423
|
+
let inString = false;
|
|
1424
|
+
let escapeNext = false;
|
|
1425
|
+
for (let end = start; end < content.length; end++) {
|
|
1426
|
+
const ch = content[end];
|
|
1427
|
+
if (escapeNext) {
|
|
1428
|
+
escapeNext = false;
|
|
1429
|
+
continue;
|
|
1166
1430
|
}
|
|
1167
|
-
|
|
1431
|
+
if (ch === "\\") {
|
|
1432
|
+
escapeNext = true;
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (ch === '"') {
|
|
1436
|
+
inString = !inString;
|
|
1437
|
+
continue;
|
|
1438
|
+
}
|
|
1439
|
+
if (inString) continue;
|
|
1440
|
+
if (ch === "{" || ch === "[") depth++;
|
|
1441
|
+
if (ch === "}" || ch === "]") depth--;
|
|
1442
|
+
if (depth === 0) {
|
|
1443
|
+
blocks.push(content.slice(start, end + 1).trim());
|
|
1444
|
+
break;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
return blocks;
|
|
1449
|
+
}
|
|
1450
|
+
var init_json_response = __esm({
|
|
1451
|
+
"src/agent/json-response.ts"() {
|
|
1168
1452
|
}
|
|
1169
1453
|
});
|
|
1170
1454
|
|
|
1171
|
-
// src/
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
required: ["path", "source"]
|
|
1196
|
-
},
|
|
1197
|
-
async execute(params, ctx) {
|
|
1198
|
-
if (!ctx.isStudioConnected()) {
|
|
1199
|
-
return { success: false, error: "Studio is not connected" };
|
|
1455
|
+
// src/agent/verifier.ts
|
|
1456
|
+
async function verifyAction(check, ctx) {
|
|
1457
|
+
switch (check.type) {
|
|
1458
|
+
case "script_edit":
|
|
1459
|
+
return verifyScriptEdit(check.path, check.expectedContent, ctx);
|
|
1460
|
+
case "code_run":
|
|
1461
|
+
return verifyCodeRun(ctx);
|
|
1462
|
+
case "instance_create":
|
|
1463
|
+
return verifyInstanceExists(check.path, ctx);
|
|
1464
|
+
case "property_set":
|
|
1465
|
+
return verifyPropertySet(check.path, check.expectedProperties ?? {}, ctx);
|
|
1466
|
+
default:
|
|
1467
|
+
return { success: true };
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
function inferVerifyChecks(call, result) {
|
|
1471
|
+
if (!result.success) return [];
|
|
1472
|
+
switch (call.name) {
|
|
1473
|
+
case "edit_script":
|
|
1474
|
+
return [
|
|
1475
|
+
{
|
|
1476
|
+
type: "script_edit",
|
|
1477
|
+
path: typeof call.arguments.path === "string" ? call.arguments.path : void 0,
|
|
1478
|
+
expectedContent: typeof call.arguments.source === "string" ? call.arguments.source : void 0
|
|
1200
1479
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1480
|
+
];
|
|
1481
|
+
case "run_code":
|
|
1482
|
+
case "execute_run_test":
|
|
1483
|
+
case "execute_play_test":
|
|
1484
|
+
return [{ type: "code_run" }];
|
|
1485
|
+
case "create_ui":
|
|
1486
|
+
case "create_part":
|
|
1487
|
+
case "clone_instance":
|
|
1488
|
+
case "insert_instance":
|
|
1489
|
+
case "group_instances": {
|
|
1490
|
+
const createdPath = extractPath(result.data) ?? extractStringArg(call.arguments, "path");
|
|
1491
|
+
return createdPath ? [{ type: "instance_create", path: createdPath }] : [];
|
|
1492
|
+
}
|
|
1493
|
+
case "set_properties":
|
|
1494
|
+
return [
|
|
1495
|
+
{
|
|
1496
|
+
type: "property_set",
|
|
1497
|
+
path: extractStringArg(call.arguments, "path"),
|
|
1498
|
+
expectedProperties: asRecord(call.arguments.properties)
|
|
1499
|
+
}
|
|
1500
|
+
];
|
|
1501
|
+
case "bulk_set_properties": {
|
|
1502
|
+
const path6 = extractPath(result.data) ?? extractStringArg(call.arguments, "path");
|
|
1503
|
+
const properties = asRecord(call.arguments.properties);
|
|
1504
|
+
return path6 && properties ? [{ type: "property_set", path: path6, expectedProperties: properties }] : [];
|
|
1505
|
+
}
|
|
1506
|
+
default:
|
|
1507
|
+
return [];
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
async function verifyScriptEdit(scriptPath, expectedContent, ctx) {
|
|
1511
|
+
if (!ctx.isStudioConnected()) {
|
|
1512
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
1513
|
+
}
|
|
1514
|
+
try {
|
|
1515
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, {
|
|
1516
|
+
path: scriptPath
|
|
1517
|
+
});
|
|
1518
|
+
if (!response.payload?.source) {
|
|
1519
|
+
return { success: false, error: `Could not read back script: ${scriptPath}` };
|
|
1520
|
+
}
|
|
1521
|
+
if (expectedContent && response.payload.source !== expectedContent) {
|
|
1522
|
+
return {
|
|
1523
|
+
success: false,
|
|
1524
|
+
error: "Script content does not match expected output",
|
|
1525
|
+
data: {
|
|
1526
|
+
expected: expectedContent.slice(0, 200),
|
|
1527
|
+
actual: response.payload.source.slice(0, 200)
|
|
1207
1528
|
}
|
|
1208
|
-
|
|
1529
|
+
};
|
|
1530
|
+
}
|
|
1531
|
+
return { success: true, data: { verified: scriptPath } };
|
|
1532
|
+
} catch (err) {
|
|
1533
|
+
return {
|
|
1534
|
+
success: false,
|
|
1535
|
+
error: `Verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
async function verifyCodeRun(ctx) {
|
|
1540
|
+
if (!ctx.isStudioConnected()) {
|
|
1541
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
const response = await ctx.sendToStudio(
|
|
1545
|
+
CliMsg.GET_OUTPUT,
|
|
1546
|
+
{ limit: 5, level: "error" }
|
|
1547
|
+
);
|
|
1548
|
+
const errors = response.payload?.entries?.filter((e) => e.level === "error") ?? [];
|
|
1549
|
+
if (errors.length > 0) {
|
|
1550
|
+
return {
|
|
1551
|
+
success: false,
|
|
1552
|
+
error: "Errors detected after code execution",
|
|
1553
|
+
data: { errors: errors.map((e) => e.message) }
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
return { success: true };
|
|
1557
|
+
} catch {
|
|
1558
|
+
return { success: true };
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function verifyInstanceExists(instancePath, ctx) {
|
|
1562
|
+
if (!ctx.isStudioConnected()) {
|
|
1563
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
1564
|
+
}
|
|
1565
|
+
try {
|
|
1566
|
+
const response = await ctx.sendToStudio(CliMsg.RUN_CODE, {
|
|
1567
|
+
code: buildInstanceExistsCode(instancePath)
|
|
1568
|
+
});
|
|
1569
|
+
if (response.payload.error) {
|
|
1570
|
+
return { success: false, error: `Instance verification failed: ${response.payload.error}` };
|
|
1571
|
+
}
|
|
1572
|
+
const exists = String(response.payload.output ?? "").trim().toLowerCase();
|
|
1573
|
+
if (!exists.includes("true")) {
|
|
1574
|
+
return { success: false, error: `Instance not found after creation: ${instancePath}` };
|
|
1575
|
+
}
|
|
1576
|
+
return { success: true, data: { exists: instancePath } };
|
|
1577
|
+
} catch {
|
|
1578
|
+
return { success: true };
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
async function verifyPropertySet(instancePath, expectedProperties, ctx) {
|
|
1582
|
+
if (!ctx.isStudioConnected()) {
|
|
1583
|
+
return { success: false, error: "Studio disconnected during verification" };
|
|
1584
|
+
}
|
|
1585
|
+
if (!instancePath || Object.keys(expectedProperties).length === 0) {
|
|
1586
|
+
return { success: true, data: { verified: instancePath } };
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
const response = await ctx.sendToStudio(CliMsg.GET_PROPERTIES, {
|
|
1590
|
+
path: instancePath,
|
|
1591
|
+
compact: false
|
|
1592
|
+
});
|
|
1593
|
+
const entries = response.payload?.properties ?? [];
|
|
1594
|
+
const byName = new Map(entries.map((entry) => [entry.name, entry.value]));
|
|
1595
|
+
const mismatches = [];
|
|
1596
|
+
for (const [name, expected] of Object.entries(expectedProperties)) {
|
|
1597
|
+
const actual = byName.get(name);
|
|
1598
|
+
if (actual == null) {
|
|
1599
|
+
mismatches.push(`${name}: missing`);
|
|
1600
|
+
continue;
|
|
1601
|
+
}
|
|
1602
|
+
if (!matchesPropertyValue(actual, expected)) {
|
|
1603
|
+
mismatches.push(`${name}: expected ${stringifyExpected(expected)}, got ${actual}`);
|
|
1209
1604
|
}
|
|
1605
|
+
}
|
|
1606
|
+
if (mismatches.length > 0) {
|
|
1607
|
+
return {
|
|
1608
|
+
success: false,
|
|
1609
|
+
error: "Property verification failed",
|
|
1610
|
+
data: { path: instancePath, mismatches }
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
success: true,
|
|
1615
|
+
data: { verified: instancePath, properties: Object.keys(expectedProperties) }
|
|
1616
|
+
};
|
|
1617
|
+
} catch (err) {
|
|
1618
|
+
return {
|
|
1619
|
+
success: false,
|
|
1620
|
+
error: `Property verification failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1210
1621
|
};
|
|
1211
1622
|
}
|
|
1623
|
+
}
|
|
1624
|
+
function extractPath(data) {
|
|
1625
|
+
if (!data || typeof data !== "object") return void 0;
|
|
1626
|
+
const candidate = data.path;
|
|
1627
|
+
return typeof candidate === "string" ? candidate : void 0;
|
|
1628
|
+
}
|
|
1629
|
+
function extractStringArg(args, key) {
|
|
1630
|
+
const value = args[key];
|
|
1631
|
+
return typeof value === "string" ? value : void 0;
|
|
1632
|
+
}
|
|
1633
|
+
function asRecord(value) {
|
|
1634
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
1635
|
+
}
|
|
1636
|
+
function buildInstanceExistsCode(instancePath) {
|
|
1637
|
+
const parts = instancePath.split(".").filter(Boolean);
|
|
1638
|
+
const lines = ["local node = game"];
|
|
1639
|
+
for (const part of parts) {
|
|
1640
|
+
lines.push(`node = node and node:FindFirstChild(${luaString(part)})`);
|
|
1641
|
+
}
|
|
1642
|
+
lines.push("return node ~= nil");
|
|
1643
|
+
return lines.join("\n");
|
|
1644
|
+
}
|
|
1645
|
+
function luaString(value) {
|
|
1646
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1647
|
+
}
|
|
1648
|
+
function matchesPropertyValue(actual, expected) {
|
|
1649
|
+
const normalizedActual = normalizeValue(actual);
|
|
1650
|
+
if (typeof expected === "boolean" || typeof expected === "number") {
|
|
1651
|
+
return normalizedActual === normalizeValue(String(expected));
|
|
1652
|
+
}
|
|
1653
|
+
if (typeof expected === "string") {
|
|
1654
|
+
const normalizedExpected = normalizeValue(expected);
|
|
1655
|
+
return normalizedActual === normalizedExpected || normalizedActual.includes(normalizedExpected);
|
|
1656
|
+
}
|
|
1657
|
+
if (Array.isArray(expected)) {
|
|
1658
|
+
const normalizedExpected = normalizeValue(expected.join(","));
|
|
1659
|
+
return normalizedActual.includes(normalizedExpected);
|
|
1660
|
+
}
|
|
1661
|
+
if (expected && typeof expected === "object") {
|
|
1662
|
+
const compactExpected = normalizeValue(JSON.stringify(expected));
|
|
1663
|
+
return normalizedActual.includes(compactExpected);
|
|
1664
|
+
}
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
function normalizeValue(value) {
|
|
1668
|
+
return value.replace(/[\s{}\[\]"]/g, "").toLowerCase();
|
|
1669
|
+
}
|
|
1670
|
+
function stringifyExpected(value) {
|
|
1671
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1672
|
+
}
|
|
1673
|
+
var init_verifier = __esm({
|
|
1674
|
+
"src/agent/verifier.ts"() {
|
|
1675
|
+
init_protocol();
|
|
1676
|
+
}
|
|
1212
1677
|
});
|
|
1213
1678
|
|
|
1214
|
-
// src/
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1679
|
+
// src/agent/multi-agent/conflicts.ts
|
|
1680
|
+
function normalizeScopePath(pathValue) {
|
|
1681
|
+
return pathValue.trim().replace(/\[(?:"|')([^"']+)(?:"|')\]/g, ".$1").replace(/^game\./i, "").replace(/\.+/g, ".").replace(/^\./, "").replace(/\.$/, "");
|
|
1682
|
+
}
|
|
1683
|
+
function pathsOverlap(a, b) {
|
|
1684
|
+
const left = normalizeScopePath(a).toLowerCase();
|
|
1685
|
+
const right = normalizeScopePath(b).toLowerCase();
|
|
1686
|
+
if (!left || !right) return false;
|
|
1687
|
+
return left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
1688
|
+
}
|
|
1689
|
+
function isPathWithinScopes(pathValue, scopes) {
|
|
1690
|
+
const target = normalizeScopePath(pathValue).toLowerCase();
|
|
1691
|
+
if (!target) return scopes.length === 0;
|
|
1692
|
+
return scopes.some((scope) => {
|
|
1693
|
+
const normalizedScope = normalizeScopePath(scope).toLowerCase();
|
|
1694
|
+
return target === normalizedScope || target.startsWith(`${normalizedScope}.`);
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1697
|
+
function inferTargetPath(call) {
|
|
1698
|
+
if (call.targetPath) return call.targetPath;
|
|
1699
|
+
const args = call.args ?? {};
|
|
1700
|
+
for (const key of ["path", "root", "rootPath", "parent", "parentPath"]) {
|
|
1701
|
+
const value = args[key];
|
|
1702
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
1703
|
+
}
|
|
1704
|
+
const targets = args.targets;
|
|
1705
|
+
if (Array.isArray(targets)) {
|
|
1706
|
+
const first = targets.find(
|
|
1707
|
+
(target) => target && typeof target === "object" && typeof target.path === "string"
|
|
1708
|
+
);
|
|
1709
|
+
if (first?.path) return first.path;
|
|
1710
|
+
}
|
|
1711
|
+
return void 0;
|
|
1712
|
+
}
|
|
1713
|
+
function validateWorkerProposal(assignment, proposal) {
|
|
1714
|
+
const errors = [];
|
|
1715
|
+
if (proposal.workerId !== assignment.id) {
|
|
1716
|
+
errors.push(`Proposal workerId "${proposal.workerId}" does not match assignment "${assignment.id}"`);
|
|
1717
|
+
}
|
|
1718
|
+
if (!proposal.summary?.trim()) {
|
|
1719
|
+
errors.push("Proposal summary is required");
|
|
1720
|
+
}
|
|
1721
|
+
for (const call of proposal.proposedToolCalls ?? []) {
|
|
1722
|
+
if (FORBIDDEN_WORKER_WRITE_TOOL_NAMES.has(call.tool)) {
|
|
1723
|
+
errors.push(`Worker proposed forbidden write tool "${call.tool}"`);
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (!COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)) {
|
|
1727
|
+
errors.push(`Worker proposed unsupported coordinator write tool "${call.tool}"`);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const targetPath = inferTargetPath(call);
|
|
1731
|
+
if (!targetPath) {
|
|
1732
|
+
errors.push(`Proposed ${call.tool} call is missing a target path`);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (!isPathWithinScopes(targetPath, assignment.ownedScopes)) {
|
|
1736
|
+
errors.push(`Proposed ${call.tool} target "${targetPath}" is outside owned scopes`);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return errors;
|
|
1740
|
+
}
|
|
1741
|
+
function rejectConflictingAcceptedProposals(accepted) {
|
|
1742
|
+
const finalAccepted = [];
|
|
1743
|
+
const rejected = [];
|
|
1744
|
+
const claimed = [];
|
|
1745
|
+
for (const item of accepted) {
|
|
1746
|
+
const conflict = findConflict(item.proposal, claimed);
|
|
1747
|
+
if (conflict) {
|
|
1748
|
+
rejected.push({
|
|
1749
|
+
workerId: item.workerId,
|
|
1750
|
+
proposal: item.proposal,
|
|
1751
|
+
reason: `Conflicts with accepted worker ${conflict.workerId} on ${conflict.path}`
|
|
1752
|
+
});
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
finalAccepted.push(item);
|
|
1756
|
+
for (const call of item.proposal.proposedToolCalls) {
|
|
1757
|
+
const targetPath = inferTargetPath(call);
|
|
1758
|
+
if (targetPath) claimed.push({ workerId: item.workerId, path: targetPath });
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return { accepted: finalAccepted, rejected };
|
|
1762
|
+
}
|
|
1763
|
+
function findConflict(proposal, claimed) {
|
|
1764
|
+
for (const call of proposal.proposedToolCalls) {
|
|
1765
|
+
const targetPath = inferTargetPath(call);
|
|
1766
|
+
if (!targetPath) continue;
|
|
1767
|
+
for (const existing of claimed) {
|
|
1768
|
+
if (pathsOverlap(targetPath, existing.path)) return existing;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return null;
|
|
1772
|
+
}
|
|
1773
|
+
var READ_ONLY_TOOL_NAMES, COORDINATOR_WRITE_TOOL_NAMES, FORBIDDEN_WORKER_WRITE_TOOL_NAMES;
|
|
1774
|
+
var init_conflicts = __esm({
|
|
1775
|
+
"src/agent/multi-agent/conflicts.ts"() {
|
|
1776
|
+
READ_ONLY_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1777
|
+
"get_explorer",
|
|
1778
|
+
"get_properties",
|
|
1779
|
+
"get_descendants_properties",
|
|
1780
|
+
"get_selection",
|
|
1781
|
+
"search_scripts",
|
|
1782
|
+
"read_script",
|
|
1783
|
+
"get_output",
|
|
1784
|
+
"get_class_info",
|
|
1785
|
+
"serialize_ui",
|
|
1786
|
+
"recall_memory",
|
|
1787
|
+
"search_roblox_api",
|
|
1788
|
+
"get_roblox_api_reference"
|
|
1789
|
+
]);
|
|
1790
|
+
COORDINATOR_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1791
|
+
"create_ui",
|
|
1792
|
+
"set_properties",
|
|
1793
|
+
"bulk_set_properties",
|
|
1794
|
+
"insert_instance",
|
|
1795
|
+
"move_instance"
|
|
1796
|
+
]);
|
|
1797
|
+
FORBIDDEN_WORKER_WRITE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
1798
|
+
"run_code",
|
|
1799
|
+
"edit_script",
|
|
1800
|
+
"delete_instance",
|
|
1801
|
+
"create_part",
|
|
1802
|
+
"build_multiple",
|
|
1803
|
+
"clone_instance",
|
|
1804
|
+
"group_instances",
|
|
1805
|
+
"find_replace_scripts",
|
|
1806
|
+
"undo",
|
|
1807
|
+
"redo"
|
|
1808
|
+
]);
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// src/agent/multi-agent/worker.ts
|
|
1813
|
+
function buildWorkerPrompt(goal, assignment) {
|
|
1814
|
+
return [
|
|
1815
|
+
"You are a Dominus worker agent. You are not the coordinator.",
|
|
1816
|
+
"You may inspect and reason, but you must not mutate Roblox Studio.",
|
|
1817
|
+
"Use only the read tools provided to gather evidence.",
|
|
1818
|
+
"Your final answer must be JSON only with this shape:",
|
|
1819
|
+
'{"workerId":"...","summary":"...","evidence":[{"source":"...","summary":"...","data":{}}],"proposedToolCalls":[{"tool":"set_properties","args":{},"targetPath":"...","rationale":"..."}],"risks":["..."],"verificationSuggestions":["..."]}',
|
|
1820
|
+
"",
|
|
1821
|
+
`Global goal: ${goal}`,
|
|
1822
|
+
`Worker id: ${assignment.id}`,
|
|
1823
|
+
`Role: ${assignment.role}`,
|
|
1824
|
+
`Objective: ${assignment.objective}`,
|
|
1825
|
+
`Owned scopes: ${assignment.ownedScopes.join(", ") || "(none)"}`,
|
|
1826
|
+
`Allowed read tools: ${assignment.allowedReadTools.join(", ")}`,
|
|
1827
|
+
"Acceptance criteria:",
|
|
1828
|
+
...assignment.acceptanceCriteria.map((criterion) => `- ${criterion}`),
|
|
1829
|
+
"",
|
|
1830
|
+
"Proposal rules:",
|
|
1831
|
+
"- Propose only coordinator-owned write tools: create_ui, set_properties, bulk_set_properties, insert_instance, move_instance.",
|
|
1832
|
+
"- Every proposed write must include targetPath and stay inside your owned scopes.",
|
|
1833
|
+
"- If you need no changes, return proposedToolCalls as an empty array and explain why."
|
|
1834
|
+
].join("\n");
|
|
1835
|
+
}
|
|
1836
|
+
function normalizeProposal(workerId, parsed) {
|
|
1837
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1838
|
+
return {
|
|
1839
|
+
workerId: typeof parsed.workerId === "string" ? parsed.workerId : workerId,
|
|
1840
|
+
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
1841
|
+
evidence: Array.isArray(parsed.evidence) ? parsed.evidence.map((item) => ({
|
|
1842
|
+
source: String(item?.source ?? "worker"),
|
|
1843
|
+
summary: String(item?.summary ?? ""),
|
|
1844
|
+
data: item?.data
|
|
1845
|
+
})) : [],
|
|
1846
|
+
proposedToolCalls: Array.isArray(parsed.proposedToolCalls) ? parsed.proposedToolCalls.map((call) => ({
|
|
1847
|
+
tool: String(call?.tool ?? ""),
|
|
1848
|
+
args: call?.args && typeof call.args === "object" ? call.args : {},
|
|
1849
|
+
targetPath: typeof call?.targetPath === "string" ? call.targetPath : void 0,
|
|
1850
|
+
rationale: typeof call?.rationale === "string" ? call.rationale : void 0
|
|
1851
|
+
})) : [],
|
|
1852
|
+
risks: Array.isArray(parsed.risks) ? parsed.risks.map(String) : [],
|
|
1853
|
+
verificationSuggestions: Array.isArray(parsed.verificationSuggestions) ? parsed.verificationSuggestions.map(String) : []
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
var WorkerAgent;
|
|
1857
|
+
var init_worker = __esm({
|
|
1858
|
+
"src/agent/multi-agent/worker.ts"() {
|
|
1859
|
+
init_json_response();
|
|
1860
|
+
WorkerAgent = class {
|
|
1861
|
+
constructor(ai, tools, toolCtx) {
|
|
1862
|
+
this.ai = ai;
|
|
1863
|
+
this.tools = tools;
|
|
1864
|
+
this.toolCtx = toolCtx;
|
|
1865
|
+
}
|
|
1866
|
+
async run(goal, assignment) {
|
|
1867
|
+
const messages = [
|
|
1868
|
+
{
|
|
1869
|
+
role: "system",
|
|
1870
|
+
content: buildWorkerPrompt(goal, assignment)
|
|
1871
|
+
}
|
|
1872
|
+
];
|
|
1873
|
+
const toolResults = [];
|
|
1874
|
+
for (let iteration = 0; iteration < 4; iteration++) {
|
|
1875
|
+
const response = await this.ai.chat(messages, this.tools);
|
|
1876
|
+
const assistantMessage = {
|
|
1877
|
+
role: "assistant",
|
|
1878
|
+
content: response.content ?? null,
|
|
1879
|
+
...response.toolCalls.length > 0 ? {
|
|
1880
|
+
tool_calls: response.toolCalls.map((call) => ({
|
|
1881
|
+
id: call.id,
|
|
1882
|
+
type: "function",
|
|
1883
|
+
function: { name: call.name, arguments: JSON.stringify(call.arguments) }
|
|
1884
|
+
}))
|
|
1885
|
+
} : {}
|
|
1886
|
+
};
|
|
1887
|
+
messages.push(assistantMessage);
|
|
1888
|
+
if (response.toolCalls.length === 0) {
|
|
1889
|
+
const proposal = normalizeProposal(
|
|
1890
|
+
assignment.id,
|
|
1891
|
+
parseJsonResponse(response.content)
|
|
1892
|
+
);
|
|
1893
|
+
if (!proposal) {
|
|
1894
|
+
return {
|
|
1895
|
+
assignment,
|
|
1896
|
+
error: "Worker did not return a valid WorkerProposal JSON object",
|
|
1897
|
+
toolResults
|
|
1898
|
+
};
|
|
1899
|
+
}
|
|
1900
|
+
return { assignment, proposal, toolResults };
|
|
1901
|
+
}
|
|
1902
|
+
for (const call of response.toolCalls) {
|
|
1903
|
+
const tool31 = this.tools.find((candidate) => candidate.name === call.name);
|
|
1904
|
+
const result = tool31 ? await tool31.execute(call.arguments, this.toolCtx) : { success: false, error: `Read-only worker cannot use tool: ${call.name}` };
|
|
1905
|
+
toolResults.push({ call, result });
|
|
1906
|
+
messages.push({
|
|
1907
|
+
role: "tool",
|
|
1908
|
+
content: JSON.stringify(result),
|
|
1909
|
+
tool_call_id: call.id
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
return {
|
|
1914
|
+
assignment,
|
|
1915
|
+
error: "Worker exhausted its iteration budget before returning a proposal",
|
|
1916
|
+
toolResults
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
};
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
function normalizeArgs(args) {
|
|
1923
|
+
return {
|
|
1924
|
+
...args,
|
|
1925
|
+
goal: String(args.goal ?? "").trim(),
|
|
1926
|
+
maxWorkers: Math.max(1, Math.min(Number(args.maxWorkers ?? 3), 5))
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
function buildAssignmentPrompt(args, scopes, readTools) {
|
|
1930
|
+
return [
|
|
1931
|
+
"You are the Dominus multi-agent assignment planner.",
|
|
1932
|
+
"Create non-overlapping worker assignments for a proposal-only parallel task.",
|
|
1933
|
+
'Return JSON only: {"assignments":[{"id":"worker-1","role":"...","objective":"...","ownedScopes":["..."],"allowedReadTools":["..."],"acceptanceCriteria":["..."]}]}',
|
|
1934
|
+
`Goal: ${args.goal}`,
|
|
1935
|
+
`Root path: ${args.rootPath ?? "(none)"}`,
|
|
1936
|
+
`Task type: ${args.taskType ?? "general"}`,
|
|
1937
|
+
`Constraints: ${args.constraints ?? "(none)"}`,
|
|
1938
|
+
`Max workers: ${args.maxWorkers}`,
|
|
1939
|
+
`Candidate scopes: ${scopes.join(", ")}`,
|
|
1940
|
+
`Read tools: ${readTools.map((tool31) => tool31.name).join(", ")}`
|
|
1941
|
+
].join("\n");
|
|
1942
|
+
}
|
|
1943
|
+
function buildFallbackAssignments(args, scopes, readTools) {
|
|
1944
|
+
const selectedScopes = scopes.slice(0, args.maxWorkers);
|
|
1945
|
+
const allowedReadTools = readTools.map((tool31) => tool31.name);
|
|
1946
|
+
const criteria = [
|
|
1947
|
+
"Gather concrete evidence with read-only tools.",
|
|
1948
|
+
"Propose only scoped coordinator-owned write calls.",
|
|
1949
|
+
"Explain risks and verification suggestions."
|
|
1950
|
+
];
|
|
1951
|
+
return selectedScopes.map((scope, index) => ({
|
|
1952
|
+
id: `worker-${index + 1}`,
|
|
1953
|
+
role: index === 0 ? "Inspector" : index === selectedScopes.length - 1 ? "Verifier" : "Specialist",
|
|
1954
|
+
objective: `Inspect ${scope} for: ${args.goal}`,
|
|
1955
|
+
ownedScopes: [scope],
|
|
1956
|
+
allowedReadTools,
|
|
1957
|
+
acceptanceCriteria: args.constraints ? [...criteria, args.constraints] : criteria
|
|
1958
|
+
}));
|
|
1959
|
+
}
|
|
1960
|
+
function normalizeAssignments(rawAssignments, args, scopes, readTools) {
|
|
1961
|
+
if (!Array.isArray(rawAssignments)) return [];
|
|
1962
|
+
const allowedReadTools = readTools.map((tool31) => tool31.name);
|
|
1963
|
+
return rawAssignments.slice(0, args.maxWorkers).map((assignment, index) => ({
|
|
1964
|
+
id: assignment.id?.trim() || `worker-${index + 1}`,
|
|
1965
|
+
role: assignment.role?.trim() || "Specialist",
|
|
1966
|
+
objective: assignment.objective?.trim() || `Inspect assigned scope for: ${args.goal}`,
|
|
1967
|
+
ownedScopes: Array.isArray(assignment.ownedScopes) && assignment.ownedScopes.length > 0 ? assignment.ownedScopes.map(String) : [scopes[index] ?? scopes[0] ?? args.rootPath ?? "Workspace"],
|
|
1968
|
+
allowedReadTools: allowedReadTools.filter(
|
|
1969
|
+
(tool31) => assignment.allowedReadTools?.length ? assignment.allowedReadTools.includes(tool31) : true
|
|
1970
|
+
),
|
|
1971
|
+
acceptanceCriteria: Array.isArray(assignment.acceptanceCriteria) && assignment.acceptanceCriteria.length > 0 ? assignment.acceptanceCriteria.map(String) : ["Gather evidence and propose scoped changes only."]
|
|
1972
|
+
})).filter((assignment) => assignment.ownedScopes.length > 0);
|
|
1973
|
+
}
|
|
1974
|
+
function getSerializedChildScopes(rootPath, data) {
|
|
1975
|
+
const node = data && typeof data === "object" ? data : null;
|
|
1976
|
+
const children = Array.isArray(node?.Children) ? node.Children : Array.isArray(node?.children) ? node.children : [];
|
|
1977
|
+
return children.map((child) => child && typeof child === "object" ? child.Name : void 0).filter((name) => typeof name === "string" && name.length > 0).map((name) => `${rootPath}.${name}`);
|
|
1978
|
+
}
|
|
1979
|
+
function getExplorerChildScopes(rootPath, data) {
|
|
1980
|
+
const payload = data && typeof data === "object" ? data : null;
|
|
1981
|
+
const tree = Array.isArray(payload?.tree) ? payload.tree : [];
|
|
1982
|
+
const root = tree[0] && typeof tree[0] === "object" ? tree[0] : null;
|
|
1983
|
+
const children = Array.isArray(root?.children) ? root.children : [];
|
|
1984
|
+
return children.map((child) => child && typeof child === "object" ? child.path : void 0).filter((pathValue) => typeof pathValue === "string" && pathValue.length > 0).map((pathValue) => pathValue || rootPath);
|
|
1985
|
+
}
|
|
1986
|
+
function toToolCall(name, args) {
|
|
1987
|
+
return {
|
|
1988
|
+
id: `multi_${nanoid()}`,
|
|
1989
|
+
name,
|
|
1990
|
+
arguments: args
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
var MultiAgentCoordinator;
|
|
1994
|
+
var init_coordinator = __esm({
|
|
1995
|
+
"src/agent/multi-agent/coordinator.ts"() {
|
|
1996
|
+
init_verifier();
|
|
1997
|
+
init_json_response();
|
|
1998
|
+
init_conflicts();
|
|
1999
|
+
init_worker();
|
|
2000
|
+
MultiAgentCoordinator = class {
|
|
2001
|
+
constructor(ai, allTools) {
|
|
2002
|
+
this.ai = ai;
|
|
2003
|
+
this.allTools = allTools;
|
|
2004
|
+
}
|
|
2005
|
+
async execute(args, toolCtx, emit) {
|
|
2006
|
+
const normalized = normalizeArgs(args);
|
|
2007
|
+
const readTools = this.getReadOnlyTools();
|
|
2008
|
+
const assignments = await this.createAssignments(normalized, toolCtx, readTools);
|
|
2009
|
+
emit?.({ type: "multi_agent_start", goal: normalized.goal, workerCount: assignments.length });
|
|
2010
|
+
const workerResults = await Promise.allSettled(
|
|
2011
|
+
assignments.map(async (assignment) => {
|
|
2012
|
+
emit?.({
|
|
2013
|
+
type: "multi_agent_worker_start",
|
|
2014
|
+
workerId: assignment.id,
|
|
2015
|
+
role: assignment.role,
|
|
2016
|
+
scopes: assignment.ownedScopes
|
|
2017
|
+
});
|
|
2018
|
+
const worker = new WorkerAgent(this.ai, readTools, toolCtx);
|
|
2019
|
+
const result = await worker.run(normalized.goal, assignment);
|
|
2020
|
+
emit?.({
|
|
2021
|
+
type: "multi_agent_worker_done",
|
|
2022
|
+
workerId: assignment.id,
|
|
2023
|
+
summary: result.proposal?.summary ?? result.error ?? "Worker failed",
|
|
2024
|
+
proposedToolCallCount: result.proposal?.proposedToolCalls.length ?? 0
|
|
2025
|
+
});
|
|
2026
|
+
return result;
|
|
2027
|
+
})
|
|
2028
|
+
);
|
|
2029
|
+
const { accepted, rejected } = this.validateWorkerResults(assignments, workerResults, emit);
|
|
2030
|
+
const conflictChecked = rejectConflictingAcceptedProposals(accepted);
|
|
2031
|
+
for (const item of conflictChecked.rejected) {
|
|
2032
|
+
emit?.({ type: "multi_agent_proposal", workerId: item.workerId, accepted: false, reason: item.reason });
|
|
2033
|
+
}
|
|
2034
|
+
const finalAccepted = conflictChecked.accepted;
|
|
2035
|
+
const finalRejected = [...rejected, ...conflictChecked.rejected];
|
|
2036
|
+
const finalToolCalls = await this.synthesizeFinalToolCalls(normalized.goal, finalAccepted);
|
|
2037
|
+
const decision = {
|
|
2038
|
+
accepted: finalAccepted,
|
|
2039
|
+
rejected: finalRejected,
|
|
2040
|
+
finalToolCalls
|
|
2041
|
+
};
|
|
2042
|
+
emit?.({
|
|
2043
|
+
type: "multi_agent_decision",
|
|
2044
|
+
acceptedCount: decision.accepted.length,
|
|
2045
|
+
rejectedCount: decision.rejected.length,
|
|
2046
|
+
finalToolCallCount: decision.finalToolCalls.length
|
|
2047
|
+
});
|
|
2048
|
+
emit?.({ type: "multi_agent_apply_start", toolCallCount: decision.finalToolCalls.length });
|
|
2049
|
+
const applied = await this.applyFinalToolCalls(decision.finalToolCalls, toolCtx);
|
|
2050
|
+
const verificationFailures = await this.verifyAppliedCalls(applied, toolCtx);
|
|
2051
|
+
emit?.({
|
|
2052
|
+
type: "multi_agent_apply_done",
|
|
2053
|
+
appliedCount: applied.length,
|
|
2054
|
+
verificationFailureCount: verificationFailures.length
|
|
2055
|
+
});
|
|
2056
|
+
return {
|
|
2057
|
+
goal: normalized.goal,
|
|
2058
|
+
assignments,
|
|
2059
|
+
decision,
|
|
2060
|
+
applied,
|
|
2061
|
+
verificationFailures
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
getReadOnlyTools() {
|
|
2065
|
+
return this.allTools.filter((tool31) => READ_ONLY_TOOL_NAMES.has(tool31.name));
|
|
2066
|
+
}
|
|
2067
|
+
getMetaTool() {
|
|
2068
|
+
return {
|
|
2069
|
+
name: "run_parallel_task",
|
|
2070
|
+
description: "Split a broad task into coordinated read-only worker agents. Workers inspect and propose scoped changes; the coordinator validates proposals and applies safe serial writes.",
|
|
2071
|
+
parameters: {
|
|
2072
|
+
type: "object",
|
|
2073
|
+
properties: {
|
|
2074
|
+
goal: {
|
|
2075
|
+
type: "string",
|
|
2076
|
+
description: "The task to coordinate across sub-agents."
|
|
2077
|
+
},
|
|
2078
|
+
rootPath: {
|
|
2079
|
+
type: "string",
|
|
2080
|
+
description: "Optional root instance path to partition work under, e.g. StarterGui.HUD."
|
|
2081
|
+
},
|
|
2082
|
+
maxWorkers: {
|
|
2083
|
+
type: "number",
|
|
2084
|
+
description: "Number of workers to use. Defaults to 3, max 5.",
|
|
2085
|
+
default: 3
|
|
2086
|
+
},
|
|
2087
|
+
constraints: {
|
|
2088
|
+
type: "string",
|
|
2089
|
+
description: "Extra constraints the workers and coordinator must follow."
|
|
2090
|
+
},
|
|
2091
|
+
taskType: {
|
|
2092
|
+
type: "string",
|
|
2093
|
+
description: "Optional task type hint such as ui, script, build, refactor, or general."
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
required: ["goal"]
|
|
2097
|
+
},
|
|
2098
|
+
execute: async () => ({
|
|
2099
|
+
success: false,
|
|
2100
|
+
error: "run_parallel_task is a meta-tool handled by AgentLoop or MCP, not ToolRegistry."
|
|
2101
|
+
})
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
shouldAutoTrigger(userMessage) {
|
|
2105
|
+
const lower = userMessage.toLowerCase();
|
|
2106
|
+
return [
|
|
2107
|
+
"parallel",
|
|
2108
|
+
"sub-agent",
|
|
2109
|
+
"subagent",
|
|
2110
|
+
"agents",
|
|
2111
|
+
"split this",
|
|
2112
|
+
"entire ui",
|
|
2113
|
+
"whole ui",
|
|
2114
|
+
"organize all",
|
|
2115
|
+
"organize entire",
|
|
2116
|
+
"large task"
|
|
2117
|
+
].some((cue) => lower.includes(cue));
|
|
2118
|
+
}
|
|
2119
|
+
async createAssignments(args, toolCtx, readTools) {
|
|
2120
|
+
const scopes = await this.discoverScopes(args, toolCtx);
|
|
2121
|
+
const fallback = buildFallbackAssignments(args, scopes, readTools);
|
|
2122
|
+
try {
|
|
2123
|
+
const response = await this.ai.chat([
|
|
2124
|
+
{
|
|
2125
|
+
role: "system",
|
|
2126
|
+
content: buildAssignmentPrompt(args, scopes, readTools)
|
|
2127
|
+
}
|
|
2128
|
+
]);
|
|
2129
|
+
const parsed = parseJsonResponse(response.content);
|
|
2130
|
+
const assignments = normalizeAssignments(parsed?.assignments, args, scopes, readTools);
|
|
2131
|
+
return assignments.length > 0 ? assignments : fallback;
|
|
2132
|
+
} catch {
|
|
2133
|
+
return fallback;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async discoverScopes(args, toolCtx) {
|
|
2137
|
+
if (!args.rootPath || !toolCtx.isStudioConnected()) {
|
|
2138
|
+
return args.rootPath ? [args.rootPath] : ["Workspace", "StarterGui", "ReplicatedStorage"];
|
|
2139
|
+
}
|
|
2140
|
+
const serializeTool = this.allTools.find((tool31) => tool31.name === "serialize_ui");
|
|
2141
|
+
const looksUi = (args.taskType ?? "").toLowerCase().includes("ui") || args.rootPath.includes("Gui");
|
|
2142
|
+
if (serializeTool && looksUi) {
|
|
2143
|
+
const result = await serializeTool.execute({ path: args.rootPath, maxDepth: 2 }, toolCtx);
|
|
2144
|
+
const childScopes = result.success ? getSerializedChildScopes(args.rootPath, result.data) : [];
|
|
2145
|
+
if (childScopes.length > 0) return childScopes.slice(0, args.maxWorkers);
|
|
2146
|
+
}
|
|
2147
|
+
const explorerTool = this.allTools.find((tool31) => tool31.name === "get_explorer");
|
|
2148
|
+
if (explorerTool) {
|
|
2149
|
+
const result = await explorerTool.execute({ rootPath: args.rootPath, maxDepth: 2 }, toolCtx);
|
|
2150
|
+
const childScopes = result.success ? getExplorerChildScopes(args.rootPath, result.data) : [];
|
|
2151
|
+
if (childScopes.length > 0) return childScopes.slice(0, args.maxWorkers);
|
|
2152
|
+
}
|
|
2153
|
+
return [args.rootPath];
|
|
2154
|
+
}
|
|
2155
|
+
validateWorkerResults(assignments, settledResults, emit) {
|
|
2156
|
+
const accepted = [];
|
|
2157
|
+
const rejected = [];
|
|
2158
|
+
for (let i = 0; i < settledResults.length; i++) {
|
|
2159
|
+
const assignment = assignments[i];
|
|
2160
|
+
const result = settledResults[i];
|
|
2161
|
+
if (result.status === "rejected") {
|
|
2162
|
+
const reason = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
2163
|
+
rejected.push({ workerId: assignment.id, reason });
|
|
2164
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (!result.value.proposal) {
|
|
2168
|
+
const reason = result.value.error ?? "Worker returned no proposal";
|
|
2169
|
+
rejected.push({ workerId: assignment.id, reason });
|
|
2170
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
2171
|
+
continue;
|
|
2172
|
+
}
|
|
2173
|
+
const errors = validateWorkerProposal(assignment, result.value.proposal);
|
|
2174
|
+
if (errors.length > 0) {
|
|
2175
|
+
const reason = errors.join("; ");
|
|
2176
|
+
rejected.push({ workerId: assignment.id, proposal: result.value.proposal, reason });
|
|
2177
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: false, reason });
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2180
|
+
accepted.push({ workerId: assignment.id, proposal: result.value.proposal });
|
|
2181
|
+
emit?.({ type: "multi_agent_proposal", workerId: assignment.id, accepted: true });
|
|
2182
|
+
}
|
|
2183
|
+
return { accepted, rejected };
|
|
2184
|
+
}
|
|
2185
|
+
async synthesizeFinalToolCalls(goal, accepted) {
|
|
2186
|
+
const proposed = accepted.flatMap((item) => item.proposal.proposedToolCalls);
|
|
2187
|
+
const safeCalls = proposed.filter((call) => COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)).map((call) => toToolCall(call.tool, call.args));
|
|
2188
|
+
if (safeCalls.length <= 1) return safeCalls;
|
|
2189
|
+
try {
|
|
2190
|
+
const response = await this.ai.chat([
|
|
2191
|
+
{
|
|
2192
|
+
role: "system",
|
|
2193
|
+
content: [
|
|
2194
|
+
"You are the Dominus multi-agent coordinator.",
|
|
2195
|
+
"Merge accepted worker proposals into a deterministic serial write plan.",
|
|
2196
|
+
'Return JSON only: {"toolCalls":[{"tool":"set_properties","args":{}}]}',
|
|
2197
|
+
"Only use these tools: create_ui, set_properties, bulk_set_properties, insert_instance, move_instance.",
|
|
2198
|
+
"Do not include run_code, delete_instance, edit_script, undo, or redo.",
|
|
2199
|
+
`Goal: ${goal}`,
|
|
2200
|
+
"Accepted proposals:",
|
|
2201
|
+
JSON.stringify(accepted, null, 2)
|
|
2202
|
+
].join("\n")
|
|
2203
|
+
}
|
|
2204
|
+
]);
|
|
2205
|
+
const parsed = parseJsonResponse(
|
|
2206
|
+
response.content
|
|
2207
|
+
);
|
|
2208
|
+
const merged = (parsed?.toolCalls ?? []).filter((call) => call.tool && COORDINATOR_WRITE_TOOL_NAMES.has(call.tool)).map((call) => toToolCall(call.tool, call.args ?? {}));
|
|
2209
|
+
return merged.length > 0 ? merged : safeCalls;
|
|
2210
|
+
} catch {
|
|
2211
|
+
return safeCalls;
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async applyFinalToolCalls(calls, toolCtx) {
|
|
2215
|
+
const applied = [];
|
|
2216
|
+
for (const call of calls) {
|
|
2217
|
+
const targetPath = inferTargetPath({ tool: call.name, args: call.arguments });
|
|
2218
|
+
if (!targetPath) {
|
|
2219
|
+
applied.push({ call, result: { success: false, error: `Missing target path for ${call.name}` } });
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const tool31 = this.allTools.find((candidate) => candidate.name === call.name);
|
|
2223
|
+
if (!tool31) {
|
|
2224
|
+
applied.push({ call, result: { success: false, error: `Unknown coordinator tool: ${call.name}` } });
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
applied.push({ call, result: await tool31.execute(call.arguments, toolCtx) });
|
|
2228
|
+
}
|
|
2229
|
+
return applied;
|
|
2230
|
+
}
|
|
2231
|
+
async verifyAppliedCalls(applied, toolCtx) {
|
|
2232
|
+
const failures = [];
|
|
2233
|
+
for (const { call, result } of applied) {
|
|
2234
|
+
for (const check of inferVerifyChecks(call, result)) {
|
|
2235
|
+
const verified = await verifyAction(check, toolCtx);
|
|
2236
|
+
if (!verified.success) failures.push(verified);
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
return failures;
|
|
2240
|
+
}
|
|
2241
|
+
};
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
|
|
2245
|
+
// src/tools/studio/read-script.ts
|
|
2246
|
+
var read_script_exports = {};
|
|
2247
|
+
__export(read_script_exports, {
|
|
2248
|
+
tool: () => tool
|
|
2249
|
+
});
|
|
2250
|
+
var tool;
|
|
2251
|
+
var init_read_script = __esm({
|
|
2252
|
+
"src/tools/studio/read-script.ts"() {
|
|
2253
|
+
init_protocol();
|
|
2254
|
+
tool = {
|
|
2255
|
+
name: "read_script",
|
|
2256
|
+
description: 'Read the source code of a script in Roblox Studio. Provide the full instance path (e.g., "ServerScriptService.GameManager").',
|
|
2257
|
+
parameters: {
|
|
2258
|
+
type: "object",
|
|
2259
|
+
properties: {
|
|
2260
|
+
path: {
|
|
2261
|
+
type: "string",
|
|
2262
|
+
description: 'Full instance path of the script (e.g., "ServerScriptService.GameManager")'
|
|
2263
|
+
}
|
|
2264
|
+
},
|
|
2265
|
+
required: ["path"]
|
|
2266
|
+
},
|
|
2267
|
+
async execute(params, ctx) {
|
|
2268
|
+
if (!ctx.isStudioConnected()) {
|
|
2269
|
+
return { success: false, error: "Studio is not connected" };
|
|
2270
|
+
}
|
|
2271
|
+
const path6 = params.path;
|
|
2272
|
+
const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path6 });
|
|
2273
|
+
if (!response.payload) {
|
|
2274
|
+
return { success: false, error: `Script not found: ${path6}` };
|
|
2275
|
+
}
|
|
2276
|
+
return {
|
|
2277
|
+
success: true,
|
|
2278
|
+
data: {
|
|
2279
|
+
path: response.payload.path,
|
|
2280
|
+
className: response.payload.className,
|
|
2281
|
+
source: response.payload.source,
|
|
2282
|
+
lineCount: response.payload.lineCount
|
|
2283
|
+
}
|
|
2284
|
+
};
|
|
2285
|
+
}
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
// src/tools/studio/edit-script.ts
|
|
2291
|
+
var edit_script_exports = {};
|
|
2292
|
+
__export(edit_script_exports, {
|
|
2293
|
+
tool: () => tool2
|
|
2294
|
+
});
|
|
2295
|
+
var tool2;
|
|
2296
|
+
var init_edit_script = __esm({
|
|
2297
|
+
"src/tools/studio/edit-script.ts"() {
|
|
2298
|
+
init_protocol();
|
|
2299
|
+
tool2 = {
|
|
2300
|
+
name: "edit_script",
|
|
2301
|
+
description: "Replace the entire source code of a script in Roblox Studio. Provide the full path and the new source code.",
|
|
2302
|
+
parameters: {
|
|
2303
|
+
type: "object",
|
|
2304
|
+
properties: {
|
|
2305
|
+
path: {
|
|
2306
|
+
type: "string",
|
|
2307
|
+
description: "Full instance path of the script"
|
|
2308
|
+
},
|
|
2309
|
+
source: {
|
|
2310
|
+
type: "string",
|
|
2311
|
+
description: "The new complete source code for the script"
|
|
2312
|
+
}
|
|
2313
|
+
},
|
|
2314
|
+
required: ["path", "source"]
|
|
2315
|
+
},
|
|
2316
|
+
async execute(params, ctx) {
|
|
2317
|
+
if (!ctx.isStudioConnected()) {
|
|
2318
|
+
return { success: false, error: "Studio is not connected" };
|
|
2319
|
+
}
|
|
2320
|
+
const path6 = params.path;
|
|
2321
|
+
const source = params.source;
|
|
2322
|
+
const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path6, source });
|
|
2323
|
+
const payload = response.payload;
|
|
2324
|
+
if (!payload.success) {
|
|
2325
|
+
return { success: false, error: payload.error ?? "Failed to edit script" };
|
|
2326
|
+
}
|
|
2327
|
+
return { success: true, data: { path: path6, linesWritten: source.split("\n").length } };
|
|
2328
|
+
}
|
|
2329
|
+
};
|
|
2330
|
+
}
|
|
2331
|
+
});
|
|
2332
|
+
|
|
2333
|
+
// src/tools/studio/run-code.ts
|
|
2334
|
+
var run_code_exports = {};
|
|
2335
|
+
__export(run_code_exports, {
|
|
2336
|
+
tool: () => tool3
|
|
2337
|
+
});
|
|
2338
|
+
var tool3;
|
|
2339
|
+
var init_run_code = __esm({
|
|
2340
|
+
"src/tools/studio/run-code.ts"() {
|
|
2341
|
+
init_protocol();
|
|
2342
|
+
tool3 = {
|
|
2343
|
+
name: "run_code",
|
|
2344
|
+
description: "Execute arbitrary Luau code in Roblox Studio and return the output. The code runs in the Studio plugin context with full API access.",
|
|
2345
|
+
parameters: {
|
|
2346
|
+
type: "object",
|
|
2347
|
+
properties: {
|
|
1229
2348
|
code: {
|
|
1230
2349
|
type: "string",
|
|
1231
2350
|
description: "Luau code to execute in Studio"
|
|
@@ -1375,10 +2494,10 @@ var init_get_descendants_properties = __esm({
|
|
|
1375
2494
|
// 5 minute timeout since getting ALL descendants can take a very long time
|
|
1376
2495
|
);
|
|
1377
2496
|
if (params.outputFile && typeof params.outputFile === "string") {
|
|
1378
|
-
const
|
|
1379
|
-
const
|
|
1380
|
-
const outputPath =
|
|
1381
|
-
await
|
|
2497
|
+
const fs7 = await import('fs/promises');
|
|
2498
|
+
const path6 = await import('path');
|
|
2499
|
+
const outputPath = path6.resolve(process.cwd(), params.outputFile);
|
|
2500
|
+
await fs7.writeFile(outputPath, JSON.stringify(response.payload, null, 2), "utf-8");
|
|
1382
2501
|
return { success: true, data: `Successfully saved descendants to ${outputPath}` };
|
|
1383
2502
|
}
|
|
1384
2503
|
return { success: true, data: response.payload };
|
|
@@ -1955,8 +3074,8 @@ __export(clone_instance_exports, {
|
|
|
1955
3074
|
tool: () => tool19
|
|
1956
3075
|
});
|
|
1957
3076
|
function buildCloneCode(params) {
|
|
1958
|
-
const
|
|
1959
|
-
const parts =
|
|
3077
|
+
const path6 = params.path;
|
|
3078
|
+
const parts = path6.split(".");
|
|
1960
3079
|
let resolve = "game";
|
|
1961
3080
|
for (const p of parts) {
|
|
1962
3081
|
resolve += `["${p}"]`;
|
|
@@ -2097,8 +3216,8 @@ __export(build_multiple_exports, {
|
|
|
2097
3216
|
});
|
|
2098
3217
|
function generateBatchCode(parts, groupName, groupParent) {
|
|
2099
3218
|
const lines = [];
|
|
2100
|
-
function resolveParent(
|
|
2101
|
-
const segs =
|
|
3219
|
+
function resolveParent(path6) {
|
|
3220
|
+
const segs = path6.split(".");
|
|
2102
3221
|
let r = "game";
|
|
2103
3222
|
for (const s of segs) r += `["${s}"]`;
|
|
2104
3223
|
return r;
|
|
@@ -2324,7 +3443,7 @@ var init_bulk_set_properties = __esm({
|
|
|
2324
3443
|
init_protocol();
|
|
2325
3444
|
tool24 = {
|
|
2326
3445
|
name: "bulk_set_properties",
|
|
2327
|
-
description: 'Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Supports two modes:\n1. Explicit paths: provide an array of {path, properties} objects\n2. By class filter: provide a root path, className filter, and properties to apply to all matches\n\nExample (explicit): [{"path": "StarterGui.HUD.Title", "properties": {"TextColor3": "#fff"}}
|
|
3446
|
+
description: 'Set properties on multiple instances at once in a single call. Much faster than calling set_properties repeatedly. Supports two modes:\n1. Explicit paths: provide an array of {path, properties} objects\n2. By class filter: provide a root path, className filter, and properties to apply to all matches\n\nFilter mode also supports addChildren: insert child instances (e.g. UIStroke, UICorner) on every match. Idempotent \u2014 skips if a child of that class already exists.\n\nExample (explicit): targets=[{"path": "StarterGui.HUD.Title", "properties": {"TextColor3": "#fff"}}]\nExample (filter): root="StarterGui.HUD", className="TextLabel", properties={"Font": "RobotoBold"}\nExample (addChildren): root="StarterGui.HUD", className="TextLabel", addChildren=[{"ClassName": "UIStroke", "Color": "#000000", "Thickness": 1.5}]',
|
|
2328
3447
|
parameters: {
|
|
2329
3448
|
type: "object",
|
|
2330
3449
|
properties: {
|
|
@@ -2347,6 +3466,14 @@ var init_bulk_set_properties = __esm({
|
|
|
2347
3466
|
properties: {
|
|
2348
3467
|
type: "object",
|
|
2349
3468
|
description: "Properties to set on all matched instances (filter mode)"
|
|
3469
|
+
},
|
|
3470
|
+
addChildren: {
|
|
3471
|
+
type: "array",
|
|
3472
|
+
description: "Child instances to insert on every match (filter mode). Each item: {ClassName, Name?, ...props}. Idempotent \u2014 skips if child of that class already exists.",
|
|
3473
|
+
items: {
|
|
3474
|
+
type: "object",
|
|
3475
|
+
description: "Child instance spec with ClassName and properties"
|
|
3476
|
+
}
|
|
2350
3477
|
}
|
|
2351
3478
|
}
|
|
2352
3479
|
},
|
|
@@ -2358,7 +3485,8 @@ var init_bulk_set_properties = __esm({
|
|
|
2358
3485
|
targets: params.targets,
|
|
2359
3486
|
root: params.root,
|
|
2360
3487
|
className: params.className,
|
|
2361
|
-
properties: params.properties
|
|
3488
|
+
properties: params.properties,
|
|
3489
|
+
addChildren: params.addChildren
|
|
2362
3490
|
}, 1e3 * 60);
|
|
2363
3491
|
const payload = response.payload;
|
|
2364
3492
|
if (!payload.success) {
|
|
@@ -2442,28 +3570,546 @@ var init_find_replace_scripts = __esm({
|
|
|
2442
3570
|
};
|
|
2443
3571
|
}
|
|
2444
3572
|
});
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
3573
|
+
async function getRobloxApiReference(name, opts = {}) {
|
|
3574
|
+
const normalizedName = normalizeLookupName(name);
|
|
3575
|
+
const kinds = opts.kind && opts.kind !== "any" ? [opts.kind] : Object.keys(KIND_FOLDERS);
|
|
3576
|
+
for (const kind of kinds) {
|
|
3577
|
+
const doc = await readReferenceYaml(normalizedName, kind, opts);
|
|
3578
|
+
if (!doc) continue;
|
|
3579
|
+
const reference = parseReferenceYaml(doc.content, {
|
|
3580
|
+
kind,
|
|
3581
|
+
sourceUrl: doc.sourceUrl
|
|
3582
|
+
});
|
|
3583
|
+
markDefinedOn(reference, reference.name);
|
|
3584
|
+
if (opts.includeInherited && reference.kind === "class") {
|
|
3585
|
+
const inherited = await collectInheritedMembers(reference, opts, /* @__PURE__ */ new Set([reference.name]));
|
|
3586
|
+
reference.inheritedClasses = inherited.classes;
|
|
3587
|
+
reference.inheritedMembers = inherited.members;
|
|
3588
|
+
}
|
|
3589
|
+
return reference;
|
|
3590
|
+
}
|
|
3591
|
+
throw new Error(`Roblox API reference not found for "${name}"`);
|
|
3592
|
+
}
|
|
3593
|
+
async function collectInheritedMembers(reference, opts, visited) {
|
|
3594
|
+
const classes = [];
|
|
3595
|
+
const members = {};
|
|
3596
|
+
for (const parent of reference.inherits) {
|
|
3597
|
+
if (!parent || visited.has(parent)) continue;
|
|
3598
|
+
visited.add(parent);
|
|
3599
|
+
const doc = await readReferenceYaml(parent, "class", opts);
|
|
3600
|
+
if (!doc) continue;
|
|
3601
|
+
const parentReference = parseReferenceYaml(doc.content, {
|
|
3602
|
+
kind: "class",
|
|
3603
|
+
sourceUrl: doc.sourceUrl
|
|
3604
|
+
});
|
|
3605
|
+
markDefinedOn(parentReference, parentReference.name);
|
|
3606
|
+
classes.push(parentReference.name);
|
|
3607
|
+
mergeMembers(members, parentReference.members);
|
|
3608
|
+
const inherited = await collectInheritedMembers(parentReference, opts, visited);
|
|
3609
|
+
classes.push(...inherited.classes);
|
|
3610
|
+
mergeMembers(members, inherited.members);
|
|
3611
|
+
}
|
|
3612
|
+
return { classes, members };
|
|
3613
|
+
}
|
|
3614
|
+
function markDefinedOn(reference, className) {
|
|
3615
|
+
for (const section of Object.values(reference.members)) {
|
|
3616
|
+
for (const member of section) {
|
|
3617
|
+
member.definedOn = className;
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
function mergeMembers(target, source) {
|
|
3622
|
+
for (const [section, members] of Object.entries(source)) {
|
|
3623
|
+
target[section] ??= [];
|
|
3624
|
+
target[section].push(...members);
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
async function searchRobloxApi(query, opts = {}) {
|
|
3628
|
+
const limit = Math.max(1, Math.min(opts.limit ?? 10, 50));
|
|
3629
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
3630
|
+
const kinds = opts.kind && opts.kind !== "any" ? [opts.kind] : Object.keys(KIND_FOLDERS);
|
|
3631
|
+
const entries = await listReferenceEntries(opts);
|
|
3632
|
+
return entries.filter((entry) => kinds.includes(entry.kind)).map((entry) => ({
|
|
3633
|
+
...entry,
|
|
3634
|
+
score: scoreSearchResult(normalizedQuery, entry.name, entry.kind)
|
|
3635
|
+
})).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
|
|
3636
|
+
}
|
|
3637
|
+
function parseReferenceYaml(raw, opts = {}) {
|
|
3638
|
+
const kind = opts.kind ?? normalizeKind(readTopScalar(raw, "type")) ?? "class";
|
|
3639
|
+
const members = {};
|
|
3640
|
+
for (const section of MEMBER_SECTIONS) {
|
|
3641
|
+
const parsed = parseMemberSection(raw, section);
|
|
3642
|
+
if (parsed.length > 0) {
|
|
3643
|
+
members[section] = parsed;
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
return {
|
|
3647
|
+
name: readTopScalar(raw, "name") || "Unknown",
|
|
3648
|
+
kind,
|
|
3649
|
+
summary: cleanDocText(readTopScalar(raw, "summary")),
|
|
3650
|
+
description: cleanDocText(readTopScalar(raw, "description")),
|
|
3651
|
+
inherits: readTopList(raw, "inherits"),
|
|
3652
|
+
tags: readTopList(raw, "tags"),
|
|
3653
|
+
deprecationMessage: cleanDocText(readTopScalar(raw, "deprecation_message")),
|
|
3654
|
+
sourceUrl: opts.sourceUrl ?? "",
|
|
3655
|
+
members
|
|
3656
|
+
};
|
|
3657
|
+
}
|
|
3658
|
+
function normalizeKind(value) {
|
|
3659
|
+
if (value === "class" || value === "datatype" || value === "enum" || value === "global" || value === "library") {
|
|
3660
|
+
return value;
|
|
3661
|
+
}
|
|
3662
|
+
return void 0;
|
|
3663
|
+
}
|
|
3664
|
+
async function readReferenceYaml(name, kind, opts) {
|
|
3665
|
+
const localRoot = resolveDocsRoot(opts.docsRoot);
|
|
3666
|
+
const folder = KIND_FOLDERS[kind];
|
|
3667
|
+
const fileName = `${name}.yaml`;
|
|
3668
|
+
if (localRoot) {
|
|
3669
|
+
const localPath = path5.join(localRoot, folder, fileName);
|
|
3670
|
+
if (fs6.existsSync(localPath)) {
|
|
3671
|
+
return {
|
|
3672
|
+
content: fs6.readFileSync(localPath, "utf-8"),
|
|
3673
|
+
sourceUrl: toCreatorDocsUrl(kind, name)
|
|
3674
|
+
};
|
|
3675
|
+
}
|
|
3676
|
+
}
|
|
3677
|
+
const sourceUrl = `${GITHUB_RAW_BASE}/${folder}/${encodeURIComponent(fileName)}`;
|
|
3678
|
+
try {
|
|
3679
|
+
return {
|
|
3680
|
+
content: await fetchText(sourceUrl),
|
|
3681
|
+
sourceUrl: toCreatorDocsUrl(kind, name)
|
|
3682
|
+
};
|
|
3683
|
+
} catch {
|
|
3684
|
+
return null;
|
|
3685
|
+
}
|
|
3686
|
+
}
|
|
3687
|
+
async function listReferenceEntries(opts) {
|
|
3688
|
+
const localRoot = resolveDocsRoot(opts.docsRoot);
|
|
3689
|
+
if (localRoot) {
|
|
3690
|
+
const entries = [];
|
|
3691
|
+
for (const [kind, folder] of Object.entries(KIND_FOLDERS)) {
|
|
3692
|
+
const dir = path5.join(localRoot, folder);
|
|
3693
|
+
if (!fs6.existsSync(dir)) continue;
|
|
3694
|
+
for (const file of fs6.readdirSync(dir)) {
|
|
3695
|
+
if (!file.endsWith(".yaml")) continue;
|
|
3696
|
+
const name = path5.basename(file, ".yaml");
|
|
3697
|
+
entries.push({ name, kind, score: 0, sourceUrl: toCreatorDocsUrl(kind, name) });
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
return entries;
|
|
3701
|
+
}
|
|
3702
|
+
const tree = await getRemoteTree();
|
|
3703
|
+
const prefix = "content/en-us/reference/engine/";
|
|
3704
|
+
return tree.filter((entry) => entry.type === "blob").map((entry) => entry.path).filter((entryPath) => entryPath.startsWith(prefix) && entryPath.endsWith(".yaml")).flatMap((entryPath) => {
|
|
3705
|
+
const rest = entryPath.slice(prefix.length);
|
|
3706
|
+
const [folder, file] = rest.split("/");
|
|
3707
|
+
const kind = folderToKind(folder);
|
|
3708
|
+
if (!kind || !file) return [];
|
|
3709
|
+
const name = path5.basename(file, ".yaml");
|
|
3710
|
+
return [{ name, kind, score: 0, sourceUrl: toCreatorDocsUrl(kind, name) }];
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
async function getRemoteTree() {
|
|
3714
|
+
if (remoteTreeCache) return remoteTreeCache;
|
|
3715
|
+
const payload = JSON.parse(await fetchText(GITHUB_TREE_URL));
|
|
3716
|
+
remoteTreeCache = payload.tree ?? [];
|
|
3717
|
+
return remoteTreeCache;
|
|
3718
|
+
}
|
|
3719
|
+
function folderToKind(folder) {
|
|
3720
|
+
for (const [kind, value] of Object.entries(KIND_FOLDERS)) {
|
|
3721
|
+
if (value === folder) return kind;
|
|
3722
|
+
}
|
|
3723
|
+
return void 0;
|
|
3724
|
+
}
|
|
3725
|
+
function resolveDocsRoot(explicitRoot) {
|
|
3726
|
+
const candidates = [
|
|
3727
|
+
explicitRoot,
|
|
3728
|
+
process.env.ROBLOX_CREATOR_DOCS_PATH,
|
|
3729
|
+
path5.join(process.cwd(), ".tmp", "creator-docs")
|
|
3730
|
+
].filter(Boolean);
|
|
3731
|
+
for (const candidate of candidates) {
|
|
3732
|
+
const normalized = normalizeDocsRoot(candidate);
|
|
3733
|
+
if (normalized && fs6.existsSync(normalized)) return normalized;
|
|
3734
|
+
}
|
|
3735
|
+
return null;
|
|
3736
|
+
}
|
|
3737
|
+
function normalizeDocsRoot(root) {
|
|
3738
|
+
const engineRoot = path5.join(root, "content", "en-us", "reference", "engine");
|
|
3739
|
+
if (fs6.existsSync(engineRoot)) return engineRoot;
|
|
3740
|
+
const directClasses = path5.join(root, "classes");
|
|
3741
|
+
if (fs6.existsSync(directClasses)) return root;
|
|
3742
|
+
return null;
|
|
3743
|
+
}
|
|
3744
|
+
function normalizeLookupName(name) {
|
|
3745
|
+
const trimmed = name.trim();
|
|
3746
|
+
const ownerName = trimmed.split(":")[0];
|
|
3747
|
+
const lastToken = ownerName.split(".").pop() ?? ownerName;
|
|
3748
|
+
return lastToken.replace(/[^A-Za-z0-9_]/g, "");
|
|
3749
|
+
}
|
|
3750
|
+
function normalizeSearchText(value) {
|
|
3751
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9_]+/g, "");
|
|
3752
|
+
}
|
|
3753
|
+
function scoreSearchResult(query, name, kind) {
|
|
3754
|
+
if (!query) return 1;
|
|
3755
|
+
const normalizedName = normalizeSearchText(name);
|
|
3756
|
+
if (normalizedName === query) return 100;
|
|
3757
|
+
if (normalizedName.startsWith(query)) return 75;
|
|
3758
|
+
if (normalizedName.includes(query)) return 50;
|
|
3759
|
+
const kindScore = kind.includes(query) ? 10 : 0;
|
|
3760
|
+
return kindScore;
|
|
3761
|
+
}
|
|
3762
|
+
function toCreatorDocsUrl(kind, name) {
|
|
3763
|
+
return `https://create.roblox.com/docs/reference/engine/${KIND_FOLDERS[kind]}/${name}`;
|
|
3764
|
+
}
|
|
3765
|
+
async function fetchText(url) {
|
|
3766
|
+
const response = await fetch(url, {
|
|
3767
|
+
headers: { "User-Agent": "Dominus Roblox API docs lookup" }
|
|
3768
|
+
});
|
|
3769
|
+
if (!response.ok) {
|
|
3770
|
+
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
|
|
3771
|
+
}
|
|
3772
|
+
return response.text();
|
|
3773
|
+
}
|
|
3774
|
+
function readTopScalar(raw, key) {
|
|
3775
|
+
const lines = raw.split(/\r?\n/);
|
|
3776
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3777
|
+
const match = lines[i].match(new RegExp(`^${escapeRegExp(key)}:\\s*(.*)$`));
|
|
3778
|
+
if (!match) continue;
|
|
3779
|
+
return readYamlValue(lines, i, 0, match[1]);
|
|
3780
|
+
}
|
|
3781
|
+
return "";
|
|
3782
|
+
}
|
|
3783
|
+
function readTopList(raw, key) {
|
|
3784
|
+
const lines = raw.split(/\r?\n/);
|
|
3785
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3786
|
+
const match = lines[i].match(new RegExp(`^${escapeRegExp(key)}:\\s*(.*)$`));
|
|
3787
|
+
if (!match) continue;
|
|
3788
|
+
const inline = match[1].trim();
|
|
3789
|
+
if (inline === "[]" || inline === "") {
|
|
3790
|
+
if (inline === "[]") return [];
|
|
3791
|
+
return readIndentedList(lines, i + 1, 2);
|
|
3792
|
+
}
|
|
3793
|
+
return [unquote(inline)];
|
|
3794
|
+
}
|
|
3795
|
+
return [];
|
|
3796
|
+
}
|
|
3797
|
+
function parseMemberSection(raw, section) {
|
|
3798
|
+
const sectionLines = extractTopSection(raw, section);
|
|
3799
|
+
if (sectionLines.length === 0) return [];
|
|
3800
|
+
const items = splitMemberItems(sectionLines);
|
|
3801
|
+
return items.map((item) => parseMemberItem(item, section)).filter((item) => Boolean(item));
|
|
3802
|
+
}
|
|
3803
|
+
function extractTopSection(raw, section) {
|
|
3804
|
+
const lines = raw.split(/\r?\n/);
|
|
3805
|
+
const start = lines.findIndex((line) => line === `${section}:`);
|
|
3806
|
+
if (start === -1) return [];
|
|
3807
|
+
const result = [];
|
|
3808
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3809
|
+
if (/^[A-Za-z_][A-Za-z0-9_]*:/.test(lines[i])) break;
|
|
3810
|
+
result.push(lines[i]);
|
|
3811
|
+
}
|
|
3812
|
+
return result;
|
|
3813
|
+
}
|
|
3814
|
+
function splitMemberItems(lines) {
|
|
3815
|
+
const items = [];
|
|
3816
|
+
let current = [];
|
|
3817
|
+
for (const line of lines) {
|
|
3818
|
+
if (/^ - /.test(line)) {
|
|
3819
|
+
if (current.length > 0) items.push(current);
|
|
3820
|
+
current = [line];
|
|
3821
|
+
continue;
|
|
3822
|
+
}
|
|
3823
|
+
if (current.length > 0) current.push(line);
|
|
3824
|
+
}
|
|
3825
|
+
if (current.length > 0) items.push(current);
|
|
3826
|
+
return items;
|
|
3827
|
+
}
|
|
3828
|
+
function parseMemberItem(lines, section) {
|
|
3829
|
+
const name = readMemberScalar(lines, "name");
|
|
3830
|
+
if (!name) return null;
|
|
3831
|
+
const member = {
|
|
3832
|
+
name,
|
|
3833
|
+
memberType: section,
|
|
3834
|
+
type: readMemberScalar(lines, "type") || readMemberScalar(lines, "return_type"),
|
|
3835
|
+
summary: cleanDocText(readMemberScalar(lines, "summary")),
|
|
3836
|
+
description: cleanDocText(readMemberScalar(lines, "description")),
|
|
3837
|
+
parameters: readNestedObjects(lines, "parameters"),
|
|
3838
|
+
returns: readNestedObjects(lines, "returns"),
|
|
3839
|
+
tags: readMemberList(lines, "tags"),
|
|
3840
|
+
security: readSecurity(lines),
|
|
3841
|
+
threadSafety: readMemberScalar(lines, "thread_safety"),
|
|
3842
|
+
category: readMemberScalar(lines, "category"),
|
|
3843
|
+
deprecationMessage: cleanDocText(readMemberScalar(lines, "deprecation_message"))
|
|
3844
|
+
};
|
|
3845
|
+
const value = Number(readMemberScalar(lines, "value"));
|
|
3846
|
+
if (Number.isFinite(value)) member.value = value;
|
|
3847
|
+
return member;
|
|
3848
|
+
}
|
|
3849
|
+
function readMemberScalar(lines, key) {
|
|
3850
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3851
|
+
const firstLinePattern = new RegExp(`^ - ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
3852
|
+
const fieldPattern = new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
3853
|
+
const firstLineMatch = lines[i].match(firstLinePattern);
|
|
3854
|
+
const fieldMatch = lines[i].match(fieldPattern);
|
|
3855
|
+
const match = firstLineMatch ?? fieldMatch;
|
|
3856
|
+
if (!match) continue;
|
|
3857
|
+
return readYamlValue(lines, i, firstLineMatch ? 2 : 4, match[1]);
|
|
3858
|
+
}
|
|
3859
|
+
return "";
|
|
3860
|
+
}
|
|
3861
|
+
function readMemberList(lines, key) {
|
|
3862
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3863
|
+
const match = lines[i].match(new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`));
|
|
3864
|
+
if (!match) continue;
|
|
3865
|
+
const inline = match[1].trim();
|
|
3866
|
+
if (inline === "[]") return [];
|
|
3867
|
+
if (inline) return [unquote(inline)];
|
|
3868
|
+
return readIndentedList(lines, i + 1, 6);
|
|
3869
|
+
}
|
|
3870
|
+
return [];
|
|
3871
|
+
}
|
|
3872
|
+
function readNestedObjects(lines, key) {
|
|
3873
|
+
const start = lines.findIndex((line) => line === ` ${key}:`);
|
|
3874
|
+
if (start === -1) return [];
|
|
3875
|
+
const nested = [];
|
|
3876
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
3877
|
+
if (/^ [A-Za-z_][A-Za-z0-9_]*:/.test(lines[i])) break;
|
|
3878
|
+
nested.push(lines[i]);
|
|
3879
|
+
}
|
|
3880
|
+
const items = [];
|
|
3881
|
+
let current = [];
|
|
3882
|
+
for (const line of nested) {
|
|
3883
|
+
if (/^ - /.test(line)) {
|
|
3884
|
+
if (current.length > 0) items.push(current);
|
|
3885
|
+
current = [line];
|
|
3886
|
+
continue;
|
|
3887
|
+
}
|
|
3888
|
+
if (current.length > 0) current.push(line);
|
|
3889
|
+
}
|
|
3890
|
+
if (current.length > 0) items.push(current);
|
|
3891
|
+
return items.map((item) => ({
|
|
3892
|
+
name: readNestedScalar(item, "name"),
|
|
3893
|
+
type: readNestedScalar(item, "type"),
|
|
3894
|
+
summary: cleanDocText(readNestedScalar(item, "summary")),
|
|
3895
|
+
default: readNestedScalar(item, "default")
|
|
3896
|
+
}));
|
|
3897
|
+
}
|
|
3898
|
+
function readNestedScalar(lines, key) {
|
|
3899
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3900
|
+
const firstLinePattern = new RegExp(`^ - ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
3901
|
+
const fieldPattern = new RegExp(`^ ${escapeRegExp(key)}:\\s*(.*)$`);
|
|
3902
|
+
const firstLineMatch = lines[i].match(firstLinePattern);
|
|
3903
|
+
const fieldMatch = lines[i].match(fieldPattern);
|
|
3904
|
+
const match = firstLineMatch ?? fieldMatch;
|
|
3905
|
+
if (!match) continue;
|
|
3906
|
+
return readYamlValue(lines, i, firstLineMatch ? 6 : 8, match[1]);
|
|
3907
|
+
}
|
|
3908
|
+
return "";
|
|
3909
|
+
}
|
|
3910
|
+
function readSecurity(lines) {
|
|
3911
|
+
const start = lines.findIndex((line) => line === " security:");
|
|
3912
|
+
if (start === -1) return void 0;
|
|
3913
|
+
return {
|
|
3914
|
+
read: readIndentedScalar(lines, start + 1, "read", 6),
|
|
3915
|
+
write: readIndentedScalar(lines, start + 1, "write", 6)
|
|
3916
|
+
};
|
|
3917
|
+
}
|
|
3918
|
+
function readIndentedScalar(lines, start, key, indent) {
|
|
3919
|
+
const pattern = new RegExp(`^\\s{${indent}}${escapeRegExp(key)}:\\s*(.*)$`);
|
|
3920
|
+
for (let i = start; i < lines.length; i++) {
|
|
3921
|
+
if (indentOf(lines[i]) < indent) break;
|
|
3922
|
+
const match = lines[i].match(pattern);
|
|
3923
|
+
if (match) return unquote(match[1].trim());
|
|
3924
|
+
}
|
|
3925
|
+
return "";
|
|
3926
|
+
}
|
|
3927
|
+
function readYamlValue(lines, index, indent, rawValue) {
|
|
3928
|
+
const value = rawValue.trim();
|
|
3929
|
+
if (value === "|-" || value === "|") {
|
|
3930
|
+
return readBlock(lines, index + 1, indent + 2);
|
|
3931
|
+
}
|
|
3932
|
+
if (value === "''" || value === '""') return "";
|
|
3933
|
+
return unquote(value);
|
|
3934
|
+
}
|
|
3935
|
+
function readBlock(lines, start, minIndent) {
|
|
3936
|
+
const block = [];
|
|
3937
|
+
for (let i = start; i < lines.length; i++) {
|
|
3938
|
+
const line = lines[i];
|
|
3939
|
+
if (line.trim() && indentOf(line) < minIndent) break;
|
|
3940
|
+
block.push(line.slice(Math.min(minIndent, line.length)));
|
|
3941
|
+
}
|
|
3942
|
+
return block.join("\n").trim();
|
|
3943
|
+
}
|
|
3944
|
+
function readIndentedList(lines, start, indent) {
|
|
3945
|
+
const pattern = new RegExp(`^\\s{${indent}}-\\s*(.*)$`);
|
|
3946
|
+
const values = [];
|
|
3947
|
+
for (let i = start; i < lines.length; i++) {
|
|
3948
|
+
if (lines[i].trim() && indentOf(lines[i]) < indent) break;
|
|
3949
|
+
const match = lines[i].match(pattern);
|
|
3950
|
+
if (match) values.push(unquote(match[1].trim()));
|
|
3951
|
+
}
|
|
3952
|
+
return values;
|
|
3953
|
+
}
|
|
3954
|
+
function cleanDocText(value) {
|
|
3955
|
+
return value.replace(/\r/g, "").replace(/[ \t]+\n/g, "\n").trim();
|
|
3956
|
+
}
|
|
3957
|
+
function indentOf(line) {
|
|
3958
|
+
return line.match(/^ */)?.[0].length ?? 0;
|
|
3959
|
+
}
|
|
3960
|
+
function unquote(value) {
|
|
3961
|
+
if (value.startsWith("'") && value.endsWith("'") || value.startsWith('"') && value.endsWith('"')) {
|
|
3962
|
+
return value.slice(1, -1);
|
|
3963
|
+
}
|
|
3964
|
+
return value;
|
|
3965
|
+
}
|
|
3966
|
+
function escapeRegExp(value) {
|
|
3967
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3968
|
+
}
|
|
3969
|
+
var GITHUB_RAW_BASE, GITHUB_TREE_URL, KIND_FOLDERS, MEMBER_SECTIONS, remoteTreeCache;
|
|
3970
|
+
var init_roblox_api = __esm({
|
|
3971
|
+
"src/docs/roblox-api.ts"() {
|
|
3972
|
+
GITHUB_RAW_BASE = "https://raw.githubusercontent.com/Roblox/creator-docs/main/content/en-us/reference/engine";
|
|
3973
|
+
GITHUB_TREE_URL = "https://api.github.com/repos/Roblox/creator-docs/git/trees/main?recursive=1";
|
|
3974
|
+
KIND_FOLDERS = {
|
|
3975
|
+
class: "classes",
|
|
3976
|
+
datatype: "datatypes",
|
|
3977
|
+
enum: "enums",
|
|
3978
|
+
global: "globals",
|
|
3979
|
+
library: "libraries"
|
|
3980
|
+
};
|
|
3981
|
+
MEMBER_SECTIONS = [
|
|
3982
|
+
"properties",
|
|
3983
|
+
"methods",
|
|
3984
|
+
"events",
|
|
3985
|
+
"callbacks",
|
|
3986
|
+
"constructors",
|
|
3987
|
+
"functions",
|
|
3988
|
+
"items",
|
|
3989
|
+
"constants",
|
|
3990
|
+
"math_operations"
|
|
3991
|
+
];
|
|
3992
|
+
remoteTreeCache = null;
|
|
3993
|
+
}
|
|
3994
|
+
});
|
|
3995
|
+
|
|
3996
|
+
// src/tools/docs/search-roblox-api.ts
|
|
3997
|
+
var search_roblox_api_exports = {};
|
|
3998
|
+
__export(search_roblox_api_exports, {
|
|
3999
|
+
tool: () => tool26
|
|
4000
|
+
});
|
|
4001
|
+
var tool26;
|
|
4002
|
+
var init_search_roblox_api = __esm({
|
|
4003
|
+
"src/tools/docs/search-roblox-api.ts"() {
|
|
4004
|
+
init_roblox_api();
|
|
4005
|
+
tool26 = {
|
|
4006
|
+
name: "search_roblox_api",
|
|
4007
|
+
description: "Search Roblox Creator Docs engine API reference names. Use this when you are unsure of an exact class, enum, datatype, global, or library name.",
|
|
4008
|
+
parameters: {
|
|
4009
|
+
type: "object",
|
|
4010
|
+
properties: {
|
|
4011
|
+
query: {
|
|
4012
|
+
type: "string",
|
|
4013
|
+
description: 'Name fragment to search, e.g. "Text", "Run", "UDim", "FrameStyle"'
|
|
4014
|
+
},
|
|
4015
|
+
kind: {
|
|
4016
|
+
type: "string",
|
|
4017
|
+
description: "Optional reference kind filter.",
|
|
4018
|
+
enum: ["any", "class", "datatype", "enum", "global", "library"],
|
|
4019
|
+
default: "any"
|
|
4020
|
+
},
|
|
4021
|
+
limit: {
|
|
4022
|
+
type: "number",
|
|
4023
|
+
description: "Maximum results to return (default: 10, max: 50)",
|
|
4024
|
+
default: 10
|
|
4025
|
+
}
|
|
4026
|
+
},
|
|
4027
|
+
required: ["query"]
|
|
4028
|
+
},
|
|
4029
|
+
async execute(params) {
|
|
4030
|
+
try {
|
|
4031
|
+
const results = await searchRobloxApi(String(params.query ?? ""), {
|
|
4032
|
+
kind: params.kind ?? "any",
|
|
4033
|
+
limit: params.limit ?? 10
|
|
4034
|
+
});
|
|
4035
|
+
return { success: true, data: { results } };
|
|
4036
|
+
} catch (err) {
|
|
4037
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
};
|
|
4041
|
+
}
|
|
4042
|
+
});
|
|
4043
|
+
|
|
4044
|
+
// src/tools/docs/get-roblox-api-reference.ts
|
|
4045
|
+
var get_roblox_api_reference_exports = {};
|
|
4046
|
+
__export(get_roblox_api_reference_exports, {
|
|
4047
|
+
tool: () => tool27
|
|
4048
|
+
});
|
|
4049
|
+
var tool27;
|
|
4050
|
+
var init_get_roblox_api_reference = __esm({
|
|
4051
|
+
"src/tools/docs/get-roblox-api-reference.ts"() {
|
|
4052
|
+
init_roblox_api();
|
|
4053
|
+
tool27 = {
|
|
4054
|
+
name: "get_roblox_api_reference",
|
|
4055
|
+
description: "Look up Roblox Creator Docs engine API reference for a class, datatype, enum, global, or library. Works without Studio connected and includes summaries, descriptions, tags, members, parameters, returns, security, and thread safety.",
|
|
4056
|
+
parameters: {
|
|
4057
|
+
type: "object",
|
|
4058
|
+
properties: {
|
|
4059
|
+
name: {
|
|
4060
|
+
type: "string",
|
|
4061
|
+
description: 'API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'
|
|
4062
|
+
},
|
|
4063
|
+
kind: {
|
|
4064
|
+
type: "string",
|
|
4065
|
+
description: 'Optional reference kind. Use "any" to search all reference kinds.',
|
|
4066
|
+
enum: ["any", "class", "datatype", "enum", "global", "library"],
|
|
4067
|
+
default: "any"
|
|
4068
|
+
},
|
|
4069
|
+
includeInherited: {
|
|
4070
|
+
type: "boolean",
|
|
4071
|
+
description: "For classes, include members from inherited base classes (default: true).",
|
|
4072
|
+
default: true
|
|
4073
|
+
}
|
|
4074
|
+
},
|
|
4075
|
+
required: ["name"]
|
|
4076
|
+
},
|
|
4077
|
+
async execute(params) {
|
|
4078
|
+
try {
|
|
4079
|
+
const reference = await getRobloxApiReference(String(params.name ?? ""), {
|
|
4080
|
+
kind: params.kind ?? "any",
|
|
4081
|
+
includeInherited: params.includeInherited !== false
|
|
4082
|
+
});
|
|
4083
|
+
return { success: true, data: reference };
|
|
4084
|
+
} catch (err) {
|
|
4085
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
});
|
|
4091
|
+
|
|
4092
|
+
// src/tools/memory/recall.ts
|
|
4093
|
+
var recall_exports = {};
|
|
4094
|
+
__export(recall_exports, {
|
|
4095
|
+
tool: () => tool28
|
|
4096
|
+
});
|
|
4097
|
+
var tool28;
|
|
4098
|
+
var init_recall = __esm({
|
|
4099
|
+
"src/tools/memory/recall.ts"() {
|
|
4100
|
+
init_store();
|
|
4101
|
+
tool28 = {
|
|
4102
|
+
name: "recall_memory",
|
|
4103
|
+
description: "Search your memory for previously learned facts about the current project. Use this to recall architecture decisions, patterns, known bugs, or user preferences.",
|
|
4104
|
+
parameters: {
|
|
4105
|
+
type: "object",
|
|
4106
|
+
properties: {
|
|
4107
|
+
query: {
|
|
4108
|
+
type: "string",
|
|
4109
|
+
description: 'What to search for in memory (e.g., "player data storage", "combat system architecture")'
|
|
4110
|
+
},
|
|
4111
|
+
limit: {
|
|
4112
|
+
type: "number",
|
|
2467
4113
|
description: "Max number of facts to return (default: 10)",
|
|
2468
4114
|
default: 10
|
|
2469
4115
|
}
|
|
@@ -2491,13 +4137,13 @@ var init_recall = __esm({
|
|
|
2491
4137
|
// src/tools/memory/remember.ts
|
|
2492
4138
|
var remember_exports = {};
|
|
2493
4139
|
__export(remember_exports, {
|
|
2494
|
-
tool: () =>
|
|
4140
|
+
tool: () => tool29
|
|
2495
4141
|
});
|
|
2496
|
-
var
|
|
4142
|
+
var tool29;
|
|
2497
4143
|
var init_remember = __esm({
|
|
2498
4144
|
"src/tools/memory/remember.ts"() {
|
|
2499
4145
|
init_store();
|
|
2500
|
-
|
|
4146
|
+
tool29 = {
|
|
2501
4147
|
name: "remember",
|
|
2502
4148
|
description: "Store a fact about the current project in persistent memory. Use this to remember architecture decisions, patterns, bugs found, or user preferences so you can recall them in future sessions.",
|
|
2503
4149
|
parameters: {
|
|
@@ -2537,12 +4183,12 @@ var init_remember = __esm({
|
|
|
2537
4183
|
// src/tools/cloud/upload-asset.ts
|
|
2538
4184
|
var upload_asset_exports = {};
|
|
2539
4185
|
__export(upload_asset_exports, {
|
|
2540
|
-
tool: () =>
|
|
4186
|
+
tool: () => tool30
|
|
2541
4187
|
});
|
|
2542
|
-
var
|
|
4188
|
+
var tool30;
|
|
2543
4189
|
var init_upload_asset = __esm({
|
|
2544
4190
|
"src/tools/cloud/upload-asset.ts"() {
|
|
2545
|
-
|
|
4191
|
+
tool30 = {
|
|
2546
4192
|
name: "upload_asset",
|
|
2547
4193
|
description: "Upload a local file (image, model, audio) to Roblox via the Open Cloud API. Requires an Open Cloud API key configured in dominus.",
|
|
2548
4194
|
parameters: {
|
|
@@ -2580,11 +4226,11 @@ var init_upload_asset = __esm({
|
|
|
2580
4226
|
const assetType = params.assetType;
|
|
2581
4227
|
const name = params.name;
|
|
2582
4228
|
const description = params.description ?? "";
|
|
2583
|
-
if (!
|
|
4229
|
+
if (!fs6.existsSync(filePath)) {
|
|
2584
4230
|
return { success: false, error: `File not found: ${filePath}` };
|
|
2585
4231
|
}
|
|
2586
|
-
const fileBuffer =
|
|
2587
|
-
const fileName =
|
|
4232
|
+
const fileBuffer = fs6.readFileSync(filePath);
|
|
4233
|
+
const fileName = path5.basename(filePath);
|
|
2588
4234
|
const typeMapping = {
|
|
2589
4235
|
Image: "Decal",
|
|
2590
4236
|
Audio: "Audio",
|
|
@@ -2689,6 +4335,99 @@ var init_undo_redo = __esm({
|
|
|
2689
4335
|
}
|
|
2690
4336
|
});
|
|
2691
4337
|
|
|
4338
|
+
// src/tools/registry.ts
|
|
4339
|
+
function createDefaultRegistry() {
|
|
4340
|
+
const registry = new ToolRegistry();
|
|
4341
|
+
const toolModules = [
|
|
4342
|
+
Promise.resolve().then(() => (init_read_script(), read_script_exports)),
|
|
4343
|
+
Promise.resolve().then(() => (init_edit_script(), edit_script_exports)),
|
|
4344
|
+
Promise.resolve().then(() => (init_run_code(), run_code_exports)),
|
|
4345
|
+
Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
|
|
4346
|
+
Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
|
|
4347
|
+
Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
|
|
4348
|
+
Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
|
|
4349
|
+
Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
|
|
4350
|
+
Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
|
|
4351
|
+
Promise.resolve().then(() => (init_get_output(), get_output_exports)),
|
|
4352
|
+
Promise.resolve().then(() => (init_get_selection(), get_selection_exports)),
|
|
4353
|
+
Promise.resolve().then(() => (init_search_scripts(), search_scripts_exports)),
|
|
4354
|
+
Promise.resolve().then(() => (init_run_tests(), run_tests_exports)),
|
|
4355
|
+
Promise.resolve().then(() => (init_get_class_info(), get_class_info_exports)),
|
|
4356
|
+
Promise.resolve().then(() => (init_create_ui(), create_ui_exports)),
|
|
4357
|
+
Promise.resolve().then(() => (init_execute_run_test(), execute_run_test_exports)),
|
|
4358
|
+
Promise.resolve().then(() => (init_execute_play_test(), execute_play_test_exports)),
|
|
4359
|
+
Promise.resolve().then(() => (init_create_part(), create_part_exports)),
|
|
4360
|
+
Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
|
|
4361
|
+
Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
|
|
4362
|
+
Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
|
|
4363
|
+
Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
|
|
4364
|
+
Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
|
|
4365
|
+
Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
|
|
4366
|
+
Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
|
|
4367
|
+
Promise.resolve().then(() => (init_search_roblox_api(), search_roblox_api_exports)),
|
|
4368
|
+
Promise.resolve().then(() => (init_get_roblox_api_reference(), get_roblox_api_reference_exports)),
|
|
4369
|
+
Promise.resolve().then(() => (init_recall(), recall_exports)),
|
|
4370
|
+
Promise.resolve().then(() => (init_remember(), remember_exports)),
|
|
4371
|
+
Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
|
|
4372
|
+
];
|
|
4373
|
+
registry.addReadyTask(Promise.all(
|
|
4374
|
+
toolModules.map(async (mod) => {
|
|
4375
|
+
const m = await mod;
|
|
4376
|
+
if (m.tool) registry.register(m.tool);
|
|
4377
|
+
})
|
|
4378
|
+
));
|
|
4379
|
+
registry.addReadyTask(Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
|
|
4380
|
+
registry.register(m.undoTool);
|
|
4381
|
+
registry.register(m.redoTool);
|
|
4382
|
+
}));
|
|
4383
|
+
return registry;
|
|
4384
|
+
}
|
|
4385
|
+
var ToolRegistry;
|
|
4386
|
+
var init_registry = __esm({
|
|
4387
|
+
"src/tools/registry.ts"() {
|
|
4388
|
+
ToolRegistry = class {
|
|
4389
|
+
tools = /* @__PURE__ */ new Map();
|
|
4390
|
+
readyTasks = [];
|
|
4391
|
+
register(tool31) {
|
|
4392
|
+
if (this.tools.has(tool31.name)) {
|
|
4393
|
+
throw new Error(`Tool already registered: ${tool31.name}`);
|
|
4394
|
+
}
|
|
4395
|
+
this.tools.set(tool31.name, tool31);
|
|
4396
|
+
}
|
|
4397
|
+
get(name) {
|
|
4398
|
+
return this.tools.get(name);
|
|
4399
|
+
}
|
|
4400
|
+
getAll() {
|
|
4401
|
+
return Array.from(this.tools.values());
|
|
4402
|
+
}
|
|
4403
|
+
getSchemas() {
|
|
4404
|
+
return this.getAll();
|
|
4405
|
+
}
|
|
4406
|
+
async execute(call, ctx) {
|
|
4407
|
+
const tool31 = this.tools.get(call.name);
|
|
4408
|
+
if (!tool31) {
|
|
4409
|
+
return { success: false, error: `Unknown tool: ${call.name}` };
|
|
4410
|
+
}
|
|
4411
|
+
try {
|
|
4412
|
+
return await tool31.execute(call.arguments, ctx);
|
|
4413
|
+
} catch (err) {
|
|
4414
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4415
|
+
return { success: false, error: message };
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
listNames() {
|
|
4419
|
+
return Array.from(this.tools.keys());
|
|
4420
|
+
}
|
|
4421
|
+
addReadyTask(task) {
|
|
4422
|
+
this.readyTasks.push(task);
|
|
4423
|
+
}
|
|
4424
|
+
async ready() {
|
|
4425
|
+
await Promise.all(this.readyTasks);
|
|
4426
|
+
}
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
});
|
|
4430
|
+
|
|
2692
4431
|
// src/mcp/server.ts
|
|
2693
4432
|
var server_exports = {};
|
|
2694
4433
|
__export(server_exports, {
|
|
@@ -2709,7 +4448,7 @@ async function startMcpServer() {
|
|
|
2709
4448
|
}
|
|
2710
4449
|
const mcp = new McpServer({
|
|
2711
4450
|
name: "dominus",
|
|
2712
|
-
version: "0.
|
|
4451
|
+
version: "2.0.0"
|
|
2713
4452
|
}, {
|
|
2714
4453
|
instructions: INTERNAL_MEMORY
|
|
2715
4454
|
});
|
|
@@ -2821,9 +4560,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2821
4560
|
"read_script",
|
|
2822
4561
|
"Read the source code of a script in Roblox Studio",
|
|
2823
4562
|
{ path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
|
|
2824
|
-
async ({ path:
|
|
4563
|
+
async ({ path: path6 }) => {
|
|
2825
4564
|
assertConnected();
|
|
2826
|
-
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path:
|
|
4565
|
+
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path6 });
|
|
2827
4566
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2828
4567
|
}
|
|
2829
4568
|
);
|
|
@@ -2834,9 +4573,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2834
4573
|
path: z.string().describe("Full instance path"),
|
|
2835
4574
|
source: z.string().describe("New complete source code")
|
|
2836
4575
|
},
|
|
2837
|
-
async ({ path:
|
|
4576
|
+
async ({ path: path6, source }) => {
|
|
2838
4577
|
assertConnected();
|
|
2839
|
-
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path:
|
|
4578
|
+
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path6, source });
|
|
2840
4579
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2841
4580
|
}
|
|
2842
4581
|
);
|
|
@@ -2873,9 +4612,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2873
4612
|
path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
|
|
2874
4613
|
compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
|
|
2875
4614
|
},
|
|
2876
|
-
async ({ path:
|
|
4615
|
+
async ({ path: path6, compact }) => {
|
|
2877
4616
|
assertConnected();
|
|
2878
|
-
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path:
|
|
4617
|
+
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path6, compact });
|
|
2879
4618
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2880
4619
|
}
|
|
2881
4620
|
);
|
|
@@ -2887,9 +4626,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2887
4626
|
compact: z.boolean().optional().default(true).describe("Strip noisy/default properties (default true). Set false for full dump."),
|
|
2888
4627
|
outputFile: z.string().optional().describe("Optional file path to write to prevent limit errors/timeouts")
|
|
2889
4628
|
},
|
|
2890
|
-
async ({ path:
|
|
4629
|
+
async ({ path: path6, compact, outputFile }) => {
|
|
2891
4630
|
assertConnected();
|
|
2892
|
-
const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path:
|
|
4631
|
+
const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path6, compact, outputFile }, 1e3 * 60 * 5);
|
|
2893
4632
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2894
4633
|
}
|
|
2895
4634
|
);
|
|
@@ -2900,9 +4639,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2900
4639
|
path: z.string().describe("Full instance path"),
|
|
2901
4640
|
properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
|
|
2902
4641
|
},
|
|
2903
|
-
async ({ path:
|
|
4642
|
+
async ({ path: path6, properties }) => {
|
|
2904
4643
|
assertConnected();
|
|
2905
|
-
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path:
|
|
4644
|
+
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path6, properties });
|
|
2906
4645
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2907
4646
|
}
|
|
2908
4647
|
);
|
|
@@ -2930,9 +4669,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2930
4669
|
"delete_instance",
|
|
2931
4670
|
"Delete an instance from the Roblox Studio DataModel",
|
|
2932
4671
|
{ path: z.string().describe("Full instance path to delete") },
|
|
2933
|
-
async ({ path:
|
|
4672
|
+
async ({ path: path6 }) => {
|
|
2934
4673
|
assertConnected();
|
|
2935
|
-
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path:
|
|
4674
|
+
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path6 });
|
|
2936
4675
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2937
4676
|
}
|
|
2938
4677
|
);
|
|
@@ -2985,6 +4724,61 @@ ${" ".repeat(_depth)}}`;
|
|
|
2985
4724
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2986
4725
|
}
|
|
2987
4726
|
);
|
|
4727
|
+
mcp.tool(
|
|
4728
|
+
"search_roblox_api",
|
|
4729
|
+
"Search Roblox Creator Docs engine API reference names. Use when you are unsure of the exact class, enum, datatype, global, or library name. Works without Studio connected.",
|
|
4730
|
+
{
|
|
4731
|
+
query: z.string().describe('Name fragment to search, e.g. "Text", "Run", "UDim", "FrameStyle"'),
|
|
4732
|
+
kind: z.enum(["any", "class", "datatype", "enum", "global", "library"]).optional().default("any"),
|
|
4733
|
+
limit: z.number().optional().default(10).describe("Maximum results to return")
|
|
4734
|
+
},
|
|
4735
|
+
async ({ query, kind, limit }) => {
|
|
4736
|
+
const results = await searchRobloxApi(query, { kind, limit });
|
|
4737
|
+
return { content: [{ type: "text", text: JSON.stringify({ results }, null, 2) }] };
|
|
4738
|
+
}
|
|
4739
|
+
);
|
|
4740
|
+
mcp.tool(
|
|
4741
|
+
"get_roblox_api_reference",
|
|
4742
|
+
"Look up Roblox Creator Docs engine API reference for a class, datatype, enum, global, or library. Includes summaries, descriptions, members, parameters, returns, security, and thread safety. Works without Studio connected.",
|
|
4743
|
+
{
|
|
4744
|
+
name: z.string().describe('API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'),
|
|
4745
|
+
kind: z.enum(["any", "class", "datatype", "enum", "global", "library"]).optional().default("any"),
|
|
4746
|
+
includeInherited: z.boolean().optional().default(true).describe("For classes, include members from inherited base classes")
|
|
4747
|
+
},
|
|
4748
|
+
async ({ name, kind, includeInherited }) => {
|
|
4749
|
+
const reference = await getRobloxApiReference(name, { kind, includeInherited });
|
|
4750
|
+
return { content: [{ type: "text", text: JSON.stringify(reference, null, 2) }] };
|
|
4751
|
+
}
|
|
4752
|
+
);
|
|
4753
|
+
mcp.tool(
|
|
4754
|
+
"run_parallel_task",
|
|
4755
|
+
"Split a broad task into coordinated read-only worker agents. Workers inspect and propose scoped changes; the coordinator validates proposals and applies safe serial writes.",
|
|
4756
|
+
{
|
|
4757
|
+
goal: z.string().describe("The task to coordinate across sub-agents"),
|
|
4758
|
+
rootPath: z.string().optional().describe("Optional root instance path to partition work under, e.g. StarterGui.HUD"),
|
|
4759
|
+
maxWorkers: z.number().optional().default(3).describe("Number of workers to use, max 5"),
|
|
4760
|
+
constraints: z.string().optional().describe("Extra constraints workers and coordinator must follow"),
|
|
4761
|
+
taskType: z.string().optional().describe("Optional task type hint such as ui, script, build, refactor, or general")
|
|
4762
|
+
},
|
|
4763
|
+
async ({ goal, rootPath, maxWorkers, constraints, taskType }) => {
|
|
4764
|
+
const registry = createDefaultRegistry();
|
|
4765
|
+
await registry.ready();
|
|
4766
|
+
const ai = new AIProvider(loadConfig());
|
|
4767
|
+
const coordinator = new MultiAgentCoordinator(ai, registry.getAll());
|
|
4768
|
+
const events = [];
|
|
4769
|
+
const result = await coordinator.execute(
|
|
4770
|
+
{ goal, rootPath, maxWorkers, constraints, taskType },
|
|
4771
|
+
createMcpToolContext(),
|
|
4772
|
+
(event) => events.push(event)
|
|
4773
|
+
);
|
|
4774
|
+
return {
|
|
4775
|
+
content: [{
|
|
4776
|
+
type: "text",
|
|
4777
|
+
text: JSON.stringify({ ...result, events }, null, 2)
|
|
4778
|
+
}]
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
);
|
|
2988
4782
|
mcp.tool(
|
|
2989
4783
|
"create_ui",
|
|
2990
4784
|
'Create an entire UI tree in one call. Pass a declarative JSON tree \u2014 ClassName, Name, properties, Children. All types auto-coerced in Luau (UDim2, Color3, Font, enums). 10x faster than run_code. UDim2: [xScale, xOffset, yScale, yOffset]. Color3: "#hex" or "rgb(r,g,b)". Font: "GothamBold". Enums: string names.',
|
|
@@ -3200,11 +4994,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
3200
4994
|
path: z.string().describe('Instance path to serialize (e.g. "StarterGui.MyScreenGui")'),
|
|
3201
4995
|
maxDepth: z.number().optional().default(50).describe("Maximum tree depth (default 50)")
|
|
3202
4996
|
},
|
|
3203
|
-
async ({ path:
|
|
4997
|
+
async ({ path: path6, maxDepth }) => {
|
|
3204
4998
|
assertConnected();
|
|
3205
4999
|
const res = await bridge.sendRequest(
|
|
3206
5000
|
CliMsg.SERIALIZE_UI,
|
|
3207
|
-
{ path:
|
|
5001
|
+
{ path: path6, maxDepth },
|
|
3208
5002
|
1e3 * 60 * 2
|
|
3209
5003
|
);
|
|
3210
5004
|
if (!res.payload.success) {
|
|
@@ -3249,16 +5043,16 @@ ${" ".repeat(_depth)}}`;
|
|
|
3249
5043
|
path: z.string().describe('Full instance path (e.g. "Workspace.MyModel")'),
|
|
3250
5044
|
childDepth: z.number().optional().default(2).describe("How deep to show the children tree (default 2)")
|
|
3251
5045
|
},
|
|
3252
|
-
async ({ path:
|
|
5046
|
+
async ({ path: path6, childDepth }) => {
|
|
3253
5047
|
assertConnected();
|
|
3254
5048
|
const [propRes, treeRes] = await Promise.all([
|
|
3255
5049
|
bridge.sendRequest(
|
|
3256
5050
|
CliMsg.GET_PROPERTIES,
|
|
3257
|
-
{ path:
|
|
5051
|
+
{ path: path6, compact: true }
|
|
3258
5052
|
),
|
|
3259
5053
|
bridge.sendRequest(
|
|
3260
5054
|
CliMsg.GET_EXPLORER,
|
|
3261
|
-
{ rootPath:
|
|
5055
|
+
{ rootPath: path6, maxDepth: childDepth ?? 2 }
|
|
3262
5056
|
)
|
|
3263
5057
|
]);
|
|
3264
5058
|
return {
|
|
@@ -3280,11 +5074,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
3280
5074
|
framework: z.enum(["roact", "react"]).optional().default("react").describe('Target framework: "roact" (legacy) or "react" (react-lua, default)'),
|
|
3281
5075
|
componentName: z.string().optional().describe("Name for the generated component (defaults to instance Name)")
|
|
3282
5076
|
},
|
|
3283
|
-
async ({ path:
|
|
5077
|
+
async ({ path: path6, framework, componentName }) => {
|
|
3284
5078
|
assertConnected();
|
|
3285
5079
|
const res = await bridge.sendRequest(
|
|
3286
5080
|
CliMsg.SERIALIZE_UI,
|
|
3287
|
-
{ path:
|
|
5081
|
+
{ path: path6, maxDepth: 50 },
|
|
3288
5082
|
1e3 * 60 * 2
|
|
3289
5083
|
);
|
|
3290
5084
|
if (!res.payload.success || !res.payload.tree) {
|
|
@@ -3335,7 +5129,16 @@ ${" ".repeat(_depth)}}`;
|
|
|
3335
5129
|
}]
|
|
3336
5130
|
};
|
|
3337
5131
|
}
|
|
3338
|
-
|
|
5132
|
+
try {
|
|
5133
|
+
bridge.setActivePlaceId(placeId);
|
|
5134
|
+
} catch (err) {
|
|
5135
|
+
return {
|
|
5136
|
+
content: [{
|
|
5137
|
+
type: "text",
|
|
5138
|
+
text: err instanceof Error ? err.message : String(err)
|
|
5139
|
+
}]
|
|
5140
|
+
};
|
|
5141
|
+
}
|
|
3339
5142
|
if (placeId === 0) {
|
|
3340
5143
|
return { content: [{ type: "text", text: "Active place reset to auto-select." }] };
|
|
3341
5144
|
}
|
|
@@ -3401,11 +5204,50 @@ ${" ".repeat(_depth)}}`;
|
|
|
3401
5204
|
}
|
|
3402
5205
|
function assertConnected() {
|
|
3403
5206
|
if (!bridge.isConnected()) {
|
|
5207
|
+
const connections = bridge.listConnections();
|
|
5208
|
+
const activePlaceId = bridge.getActivePlaceId();
|
|
5209
|
+
if (connections.length > 1 && activePlaceId === 0) {
|
|
5210
|
+
const available = connections.map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`).join(", ");
|
|
5211
|
+
throw new Error(`Multiple Roblox Studio instances are connected: ${available}. Use list_connections and set_active_place before running Studio tools.`);
|
|
5212
|
+
}
|
|
5213
|
+
if (connections.length > 0 && activePlaceId !== 0) {
|
|
5214
|
+
throw new Error(`Active Roblox Studio place ${activePlaceId} is not connected. Use list_connections and set_active_place to choose a connected Studio instance.`);
|
|
5215
|
+
}
|
|
3404
5216
|
throw new Error(
|
|
3405
5217
|
"Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
|
|
3406
5218
|
);
|
|
3407
5219
|
}
|
|
3408
5220
|
}
|
|
5221
|
+
function createMcpToolContext() {
|
|
5222
|
+
return {
|
|
5223
|
+
sendToStudio: (type, payload, timeoutMs) => {
|
|
5224
|
+
return bridge.sendRequest(type, payload, timeoutMs);
|
|
5225
|
+
},
|
|
5226
|
+
isStudioConnected: () => bridge.isConnected(),
|
|
5227
|
+
memory: {
|
|
5228
|
+
recall: (query, projectId, limit) => {
|
|
5229
|
+
const facts = recallFacts(projectId || "current", query, limit);
|
|
5230
|
+
return facts.map((fact) => fact.content);
|
|
5231
|
+
},
|
|
5232
|
+
learn: (projectId, facts) => {
|
|
5233
|
+
const now = Date.now();
|
|
5234
|
+
for (const fact of facts) {
|
|
5235
|
+
storeFact({
|
|
5236
|
+
projectId: projectId || "current",
|
|
5237
|
+
content: fact.content,
|
|
5238
|
+
category: fact.category,
|
|
5239
|
+
source: "mcp",
|
|
5240
|
+
relevance: 1,
|
|
5241
|
+
createdAt: now,
|
|
5242
|
+
accessedAt: now
|
|
5243
|
+
});
|
|
5244
|
+
}
|
|
5245
|
+
},
|
|
5246
|
+
getScriptIndex: (projectId) => getScriptIndex(projectId || "current")
|
|
5247
|
+
},
|
|
5248
|
+
config: createConfigAccess()
|
|
5249
|
+
};
|
|
5250
|
+
}
|
|
3409
5251
|
var bridge;
|
|
3410
5252
|
var init_server = __esm({
|
|
3411
5253
|
"src/mcp/server.ts"() {
|
|
@@ -3415,6 +5257,10 @@ var init_server = __esm({
|
|
|
3415
5257
|
init_store();
|
|
3416
5258
|
init_config();
|
|
3417
5259
|
init_internal_memory();
|
|
5260
|
+
init_roblox_api();
|
|
5261
|
+
init_provider();
|
|
5262
|
+
init_coordinator();
|
|
5263
|
+
init_registry();
|
|
3418
5264
|
}
|
|
3419
5265
|
});
|
|
3420
5266
|
var CROWN_ART = ` \u25C6 \u25C6 \u25C6
|
|
@@ -4478,144 +6324,8 @@ var App = ({ server, agent: initialAgent, version }) => {
|
|
|
4478
6324
|
init_ws_server();
|
|
4479
6325
|
init_ws_client();
|
|
4480
6326
|
|
|
4481
|
-
// src/
|
|
4482
|
-
|
|
4483
|
-
var AIProvider = class {
|
|
4484
|
-
client;
|
|
4485
|
-
model;
|
|
4486
|
-
providerName;
|
|
4487
|
-
constructor(config) {
|
|
4488
|
-
if (!config.apiKey) {
|
|
4489
|
-
throw new Error("API key not configured. Run: dominus config set apiKey <your-key>");
|
|
4490
|
-
}
|
|
4491
|
-
const { baseUrl, model } = resolveProviderSettings(config);
|
|
4492
|
-
this.providerName = PROVIDERS[config.provider]?.name ?? config.provider;
|
|
4493
|
-
this.client = new OpenAI({
|
|
4494
|
-
apiKey: config.apiKey,
|
|
4495
|
-
baseURL: baseUrl
|
|
4496
|
-
});
|
|
4497
|
-
this.model = model;
|
|
4498
|
-
}
|
|
4499
|
-
setModel(model) {
|
|
4500
|
-
this.model = model;
|
|
4501
|
-
}
|
|
4502
|
-
getModel() {
|
|
4503
|
-
return this.model;
|
|
4504
|
-
}
|
|
4505
|
-
getProviderName() {
|
|
4506
|
-
return this.providerName;
|
|
4507
|
-
}
|
|
4508
|
-
async listModels() {
|
|
4509
|
-
try {
|
|
4510
|
-
const list = await this.client.models.list();
|
|
4511
|
-
const models = [];
|
|
4512
|
-
for await (const m of list) {
|
|
4513
|
-
models.push({ id: m.id, owned_by: m.owned_by });
|
|
4514
|
-
}
|
|
4515
|
-
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
4516
|
-
return models;
|
|
4517
|
-
} catch {
|
|
4518
|
-
return [];
|
|
4519
|
-
}
|
|
4520
|
-
}
|
|
4521
|
-
async chat(messages, tools) {
|
|
4522
|
-
const openAITools = tools?.map(toolToOpenAI);
|
|
4523
|
-
const response = await this.client.chat.completions.create({
|
|
4524
|
-
model: this.model,
|
|
4525
|
-
messages,
|
|
4526
|
-
tools: openAITools?.length ? openAITools : void 0,
|
|
4527
|
-
temperature: 0.3
|
|
4528
|
-
});
|
|
4529
|
-
const choice = response.choices[0];
|
|
4530
|
-
if (!choice) throw new Error("No response from AI");
|
|
4531
|
-
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
4532
|
-
id: tc.id,
|
|
4533
|
-
name: tc.function.name,
|
|
4534
|
-
arguments: JSON.parse(tc.function.arguments)
|
|
4535
|
-
}));
|
|
4536
|
-
return {
|
|
4537
|
-
content: choice.message.content,
|
|
4538
|
-
toolCalls,
|
|
4539
|
-
finishReason: choice.finish_reason ?? "stop",
|
|
4540
|
-
usage: response.usage ? {
|
|
4541
|
-
promptTokens: response.usage.prompt_tokens,
|
|
4542
|
-
completionTokens: response.usage.completion_tokens,
|
|
4543
|
-
totalTokens: response.usage.total_tokens
|
|
4544
|
-
} : void 0
|
|
4545
|
-
};
|
|
4546
|
-
}
|
|
4547
|
-
async *streamChat(messages, tools) {
|
|
4548
|
-
const openAITools = tools?.map(toolToOpenAI);
|
|
4549
|
-
const stream = await this.client.chat.completions.create({
|
|
4550
|
-
model: this.model,
|
|
4551
|
-
messages,
|
|
4552
|
-
tools: openAITools?.length ? openAITools : void 0,
|
|
4553
|
-
temperature: 0.3,
|
|
4554
|
-
stream: true
|
|
4555
|
-
});
|
|
4556
|
-
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
4557
|
-
for await (const chunk of stream) {
|
|
4558
|
-
const delta = chunk.choices[0]?.delta;
|
|
4559
|
-
if (!delta) continue;
|
|
4560
|
-
if (delta.content) {
|
|
4561
|
-
yield { type: "text", content: delta.content };
|
|
4562
|
-
}
|
|
4563
|
-
if (delta.tool_calls) {
|
|
4564
|
-
for (const tc of delta.tool_calls) {
|
|
4565
|
-
if (!toolCallBuffers.has(tc.index)) {
|
|
4566
|
-
toolCallBuffers.set(tc.index, {
|
|
4567
|
-
id: tc.id ?? "",
|
|
4568
|
-
name: tc.function?.name ?? "",
|
|
4569
|
-
args: ""
|
|
4570
|
-
});
|
|
4571
|
-
if (tc.function?.name) {
|
|
4572
|
-
yield {
|
|
4573
|
-
type: "tool_call_start",
|
|
4574
|
-
toolCall: { id: tc.id, name: tc.function.name }
|
|
4575
|
-
};
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
const buffer = toolCallBuffers.get(tc.index);
|
|
4579
|
-
if (tc.id) buffer.id = tc.id;
|
|
4580
|
-
if (tc.function?.name) buffer.name = tc.function.name;
|
|
4581
|
-
if (tc.function?.arguments) {
|
|
4582
|
-
buffer.args += tc.function.arguments;
|
|
4583
|
-
yield {
|
|
4584
|
-
type: "tool_call_delta",
|
|
4585
|
-
content: tc.function.arguments,
|
|
4586
|
-
toolCall: { id: buffer.id, name: buffer.name }
|
|
4587
|
-
};
|
|
4588
|
-
}
|
|
4589
|
-
}
|
|
4590
|
-
}
|
|
4591
|
-
if (chunk.choices[0]?.finish_reason === "tool_calls") {
|
|
4592
|
-
for (const [, buffer] of toolCallBuffers) {
|
|
4593
|
-
let args = {};
|
|
4594
|
-
try {
|
|
4595
|
-
args = JSON.parse(buffer.args);
|
|
4596
|
-
} catch {
|
|
4597
|
-
}
|
|
4598
|
-
yield {
|
|
4599
|
-
type: "tool_call_end",
|
|
4600
|
-
toolCall: { id: buffer.id, name: buffer.name, arguments: args }
|
|
4601
|
-
};
|
|
4602
|
-
}
|
|
4603
|
-
toolCallBuffers.clear();
|
|
4604
|
-
}
|
|
4605
|
-
}
|
|
4606
|
-
yield { type: "done" };
|
|
4607
|
-
}
|
|
4608
|
-
};
|
|
4609
|
-
function toolToOpenAI(tool29) {
|
|
4610
|
-
return {
|
|
4611
|
-
type: "function",
|
|
4612
|
-
function: {
|
|
4613
|
-
name: tool29.name,
|
|
4614
|
-
description: tool29.description,
|
|
4615
|
-
parameters: tool29.parameters
|
|
4616
|
-
}
|
|
4617
|
-
};
|
|
4618
|
-
}
|
|
6327
|
+
// src/agent/loop.ts
|
|
6328
|
+
init_provider();
|
|
4619
6329
|
|
|
4620
6330
|
// src/agent/system-prompt.ts
|
|
4621
6331
|
init_internal_memory();
|
|
@@ -4745,6 +6455,23 @@ When the user asks you to build or create objects (parts, trees, houses, terrain
|
|
|
4745
6455
|
**ALWAYS use \`create_ui\` for building UI.** It creates the entire UI tree in ONE call \u2014 no run_code, no get_class_info needed.
|
|
4746
6456
|
Do NOT use run_code for UI creation. Do NOT call get_class_info before create_ui \u2014 it already handles all types natively.
|
|
4747
6457
|
|
|
6458
|
+
### UI Pre-Flight \u2014 ASK BEFORE BUILDING
|
|
6459
|
+
Before creating any non-trivial UI (more than a single element), **ask the user these questions** to avoid rework:
|
|
6460
|
+
|
|
6461
|
+
**A) Device scaling** \u2014 "Should this UI scale for mobile/tablet, or is it PC-only?"
|
|
6462
|
+
- If mobile-compatible: use Scale-based sizing (e.g. \`[0.3, 0, 0.05, 0]\`), add \`UIAspectRatioConstraint\` where needed, use \`TextScaled = true\` or sensible \`AutomaticSize\`, avoid fixed pixel sizes for main containers.
|
|
6463
|
+
- If PC-only: pixel offsets are fine.
|
|
6464
|
+
|
|
6465
|
+
**B) ScreenGui structure** \u2014 "Should all panels live in one ScreenGui, or separate ScreenGuis per panel?"
|
|
6466
|
+
- One ScreenGui: simpler hierarchy, easier to show/hide everything at once.
|
|
6467
|
+
- Separate ScreenGuis: independent ZIndex control, can toggle individual panels, better for modular systems.
|
|
6468
|
+
|
|
6469
|
+
**C) Font & style** \u2014 "Any preferred font, color scheme, or style reference?"
|
|
6470
|
+
- Default to \`Roboto\` if no preference stated. Use FontFace with Weight=\`Bold\` for bold variants.
|
|
6471
|
+
- If user provides a screenshot or reference, match its style closely (transparency, stroke, colors, spacing).
|
|
6472
|
+
|
|
6473
|
+
Skip these questions only if the user has already specified preferences, or the task is trivially small (e.g. "add a TextLabel").
|
|
6474
|
+
|
|
4748
6475
|
\`create_ui\` takes a declarative JSON tree and builds everything in a single Studio round-trip:
|
|
4749
6476
|
\`\`\`json
|
|
4750
6477
|
{
|
|
@@ -4853,6 +6580,20 @@ The \`get_class_info\` tool queries Roblox's ReflectionService to return the com
|
|
|
4853
6580
|
|
|
4854
6581
|
Use it whenever you're unsure about property names or types. It's fast and cached.
|
|
4855
6582
|
|
|
6583
|
+
## Roblox Creator Docs API Reference
|
|
6584
|
+
Use \`search_roblox_api\` and \`get_roblox_api_reference\` when you need documented Roblox engine API information:
|
|
6585
|
+
- Works even when Studio is not connected.
|
|
6586
|
+
- Covers classes, datatypes, enums, globals, and libraries from Roblox/creator-docs.
|
|
6587
|
+
- Includes summaries, descriptions, tags, deprecation notes, security, thread safety, parameters, and returns.
|
|
6588
|
+
- Prefer it before guessing unfamiliar APIs, enum values, datatype constructors, service methods, or deprecated members.
|
|
6589
|
+
- When Studio is connected and you need the live engine schema for a class, use \`get_class_info\`; when you need documentation and usage guidance, use \`get_roblox_api_reference\`.
|
|
6590
|
+
|
|
6591
|
+
## Multi-Agent Coordination
|
|
6592
|
+
Use \`run_parallel_task\` for broad tasks that benefit from parallel inspection, such as organizing an entire UI, auditing a large system, or comparing several independent branches.
|
|
6593
|
+
- Worker agents are proposal-only: they inspect with read tools and propose scoped changes.
|
|
6594
|
+
- The coordinator owns all writes, checks path ownership, rejects conflicts, applies safe tool calls serially, and verifies results.
|
|
6595
|
+
- Prefer normal single-agent execution for small, direct edits.
|
|
6596
|
+
|
|
4856
6597
|
${INTERNAL_MEMORY}`);
|
|
4857
6598
|
return parts.join("\n");
|
|
4858
6599
|
}
|
|
@@ -4959,6 +6700,343 @@ ${summaryText}`
|
|
|
4959
6700
|
init_config();
|
|
4960
6701
|
init_config();
|
|
4961
6702
|
init_store();
|
|
6703
|
+
|
|
6704
|
+
// src/agent/planner.ts
|
|
6705
|
+
init_json_response();
|
|
6706
|
+
function createPlan(goal, stepDescriptions) {
|
|
6707
|
+
return {
|
|
6708
|
+
goal,
|
|
6709
|
+
steps: stepDescriptions.map((desc) => ({
|
|
6710
|
+
description: desc,
|
|
6711
|
+
status: "pending"
|
|
6712
|
+
})),
|
|
6713
|
+
createdAt: Date.now()
|
|
6714
|
+
};
|
|
6715
|
+
}
|
|
6716
|
+
function updateStepStatus(plan, index, status, result) {
|
|
6717
|
+
if (index < 0 || index >= plan.steps.length) return plan;
|
|
6718
|
+
const steps = [...plan.steps];
|
|
6719
|
+
steps[index] = { ...steps[index], status, result };
|
|
6720
|
+
return { ...plan, steps };
|
|
6721
|
+
}
|
|
6722
|
+
function getNextPendingStep(plan) {
|
|
6723
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
6724
|
+
if (plan.steps[i].status === "pending") {
|
|
6725
|
+
return { index: i, step: plan.steps[i] };
|
|
6726
|
+
}
|
|
6727
|
+
}
|
|
6728
|
+
return null;
|
|
6729
|
+
}
|
|
6730
|
+
function isPlanComplete(plan) {
|
|
6731
|
+
return plan.steps.every((s) => s.status === "done" || s.status === "failed");
|
|
6732
|
+
}
|
|
6733
|
+
async function createExecutionPlan(opts) {
|
|
6734
|
+
const prompt = [
|
|
6735
|
+
"You are the Dominus planning module.",
|
|
6736
|
+
"Turn the goal into a short execution plan with verifiable steps.",
|
|
6737
|
+
"Return JSON only in this shape:",
|
|
6738
|
+
'{"goal":"...","steps":["..."],"acceptanceCriteria":["..."],"notes":["..."]}',
|
|
6739
|
+
"",
|
|
6740
|
+
`Goal: ${opts.goal}`,
|
|
6741
|
+
`Task kind: ${opts.taskProfile.kind}`,
|
|
6742
|
+
`Fidelity: ${opts.taskProfile.fidelity}`,
|
|
6743
|
+
`Needs visual accuracy: ${opts.taskProfile.needsVisualAccuracy ? "yes" : "no"}`,
|
|
6744
|
+
"Existing acceptance criteria:",
|
|
6745
|
+
...opts.taskProfile.acceptanceCriteria.map((item) => `- ${item}`),
|
|
6746
|
+
"",
|
|
6747
|
+
"Planning rules:",
|
|
6748
|
+
"- Produce 3 to 6 steps.",
|
|
6749
|
+
"- Each step should be actionable and specific to Roblox Studio tool work.",
|
|
6750
|
+
"- Include at least one explicit verification-oriented step.",
|
|
6751
|
+
"- For UI fidelity tasks, include evidence gathering and read-back comparison.",
|
|
6752
|
+
"- Keep notes short and practical."
|
|
6753
|
+
].join("\n");
|
|
6754
|
+
try {
|
|
6755
|
+
const response = await opts.ai.chat([{ role: "system", content: prompt }]);
|
|
6756
|
+
const parsed = parseJsonResponse(response.content);
|
|
6757
|
+
if (!parsed) {
|
|
6758
|
+
return createFallbackExecutionPlan(opts.goal, opts.taskProfile);
|
|
6759
|
+
}
|
|
6760
|
+
return normalizeExecutionPlan(parsed, opts.goal, opts.taskProfile);
|
|
6761
|
+
} catch {
|
|
6762
|
+
return createFallbackExecutionPlan(opts.goal, opts.taskProfile);
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
function createFallbackExecutionPlan(goal, taskProfile) {
|
|
6766
|
+
const steps = taskProfile.kind === "ui_port" ? [
|
|
6767
|
+
"Inspect the target UI requirements and identify the exact hierarchy and visual constraints.",
|
|
6768
|
+
"Build or convert the Roblox UI using the dedicated UI tools.",
|
|
6769
|
+
"Read back the resulting UI tree and key properties to compare against the target.",
|
|
6770
|
+
"Fix any mismatches in layout, typography, stroke, spacing, or layering."
|
|
6771
|
+
] : taskProfile.kind === "ui_build" ? [
|
|
6772
|
+
"Inspect the relevant parent and any existing UI context.",
|
|
6773
|
+
"Create the requested UI with the dedicated UI tools.",
|
|
6774
|
+
"Verify the created hierarchy and important properties."
|
|
6775
|
+
] : taskProfile.kind === "script_edit" ? [
|
|
6776
|
+
"Read the relevant scripts and gather context before changing code.",
|
|
6777
|
+
"Apply the requested code changes with dedicated tools.",
|
|
6778
|
+
"Read the script back and run checks for errors or regressions."
|
|
6779
|
+
] : [
|
|
6780
|
+
"Inspect the current Roblox context relevant to the request.",
|
|
6781
|
+
"Execute the requested change with dedicated tools.",
|
|
6782
|
+
"Verify the result before marking the task complete."
|
|
6783
|
+
];
|
|
6784
|
+
return {
|
|
6785
|
+
...createPlan(goal, steps),
|
|
6786
|
+
acceptanceCriteria: [...taskProfile.acceptanceCriteria],
|
|
6787
|
+
notes: buildFallbackNotes(taskProfile),
|
|
6788
|
+
taskProfile
|
|
6789
|
+
};
|
|
6790
|
+
}
|
|
6791
|
+
function formatExecutionPlan(plan) {
|
|
6792
|
+
const lines = [`Plan for: ${plan.goal}`];
|
|
6793
|
+
for (let i = 0; i < plan.steps.length; i++) {
|
|
6794
|
+
lines.push(`${i + 1}. ${plan.steps[i].description}`);
|
|
6795
|
+
}
|
|
6796
|
+
if (plan.acceptanceCriteria.length > 0) {
|
|
6797
|
+
lines.push("Acceptance criteria:");
|
|
6798
|
+
for (const criterion of plan.acceptanceCriteria) {
|
|
6799
|
+
lines.push(`- ${criterion}`);
|
|
6800
|
+
}
|
|
6801
|
+
}
|
|
6802
|
+
return lines.join("\n");
|
|
6803
|
+
}
|
|
6804
|
+
function normalizeExecutionPlan(parsed, fallbackGoal, taskProfile) {
|
|
6805
|
+
const goal = typeof parsed.goal === "string" && parsed.goal.trim() ? parsed.goal.trim() : fallbackGoal;
|
|
6806
|
+
const stepDescriptions = Array.isArray(parsed.steps) ? parsed.steps.map((step) => sanitizeLine(step)).filter(Boolean).slice(0, 6) : [];
|
|
6807
|
+
const acceptanceCriteria = Array.isArray(parsed.acceptanceCriteria) ? parsed.acceptanceCriteria.map((item) => sanitizeLine(item)).filter(Boolean).slice(0, 8) : [];
|
|
6808
|
+
const notes = Array.isArray(parsed.notes) ? parsed.notes.map((item) => sanitizeLine(item)).filter(Boolean).slice(0, 6) : [];
|
|
6809
|
+
const fallback = createFallbackExecutionPlan(fallbackGoal, taskProfile);
|
|
6810
|
+
return {
|
|
6811
|
+
...createPlan(
|
|
6812
|
+
goal,
|
|
6813
|
+
stepDescriptions.length >= 2 ? stepDescriptions : fallback.steps.map((step) => step.description)
|
|
6814
|
+
),
|
|
6815
|
+
acceptanceCriteria: acceptanceCriteria.length > 0 ? acceptanceCriteria : fallback.acceptanceCriteria,
|
|
6816
|
+
notes: notes.length > 0 ? notes : fallback.notes,
|
|
6817
|
+
taskProfile
|
|
6818
|
+
};
|
|
6819
|
+
}
|
|
6820
|
+
function buildFallbackNotes(taskProfile) {
|
|
6821
|
+
if (taskProfile.kind === "ui_port") {
|
|
6822
|
+
return [
|
|
6823
|
+
"Do not treat visual approximation as success.",
|
|
6824
|
+
"Use read-back evidence for UI properties before finishing."
|
|
6825
|
+
];
|
|
6826
|
+
}
|
|
6827
|
+
if (taskProfile.kind === "script_edit") {
|
|
6828
|
+
return [
|
|
6829
|
+
"Prefer reading existing code before rewriting it.",
|
|
6830
|
+
"Verify script edits by reading them back."
|
|
6831
|
+
];
|
|
6832
|
+
}
|
|
6833
|
+
return ["Keep the agent aligned to the explicit user goal and verify important effects."];
|
|
6834
|
+
}
|
|
6835
|
+
function sanitizeLine(value) {
|
|
6836
|
+
return String(value ?? "").replace(/\s+/g, " ").trim();
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
// src/agent/task-profile.ts
|
|
6840
|
+
var UI_CUES = [
|
|
6841
|
+
"ui",
|
|
6842
|
+
"gui",
|
|
6843
|
+
"screen",
|
|
6844
|
+
"frame",
|
|
6845
|
+
"textlabel",
|
|
6846
|
+
"textbutton",
|
|
6847
|
+
"imagebutton",
|
|
6848
|
+
"imagelabel",
|
|
6849
|
+
"html",
|
|
6850
|
+
"css",
|
|
6851
|
+
"react roblox",
|
|
6852
|
+
"roact",
|
|
6853
|
+
"fusion"
|
|
6854
|
+
];
|
|
6855
|
+
var PORT_CUES = ["port", "convert", "copy", "recreate", "translate", "match"];
|
|
6856
|
+
var STRICT_CUES = [
|
|
6857
|
+
"1:1",
|
|
6858
|
+
"exact",
|
|
6859
|
+
"exactly",
|
|
6860
|
+
"pixel perfect",
|
|
6861
|
+
"pixel-perfect",
|
|
6862
|
+
"identical",
|
|
6863
|
+
"same sizing",
|
|
6864
|
+
"same font",
|
|
6865
|
+
"same stroke",
|
|
6866
|
+
"same spacing"
|
|
6867
|
+
];
|
|
6868
|
+
function buildTaskProfile(userMessage) {
|
|
6869
|
+
const lower = userMessage.toLowerCase();
|
|
6870
|
+
const cues = /* @__PURE__ */ new Set();
|
|
6871
|
+
for (const cue of [...UI_CUES, ...PORT_CUES, ...STRICT_CUES]) {
|
|
6872
|
+
if (lower.includes(cue)) cues.add(cue);
|
|
6873
|
+
}
|
|
6874
|
+
const isUiTask = containsAny(lower, UI_CUES);
|
|
6875
|
+
const isPortTask = containsAny(lower, PORT_CUES);
|
|
6876
|
+
const strictFidelity = containsAny(lower, STRICT_CUES);
|
|
6877
|
+
if (isUiTask && (isPortTask || strictFidelity)) {
|
|
6878
|
+
return {
|
|
6879
|
+
kind: "ui_port",
|
|
6880
|
+
fidelity: strictFidelity ? "strict" : "normal",
|
|
6881
|
+
needsVisualAccuracy: true,
|
|
6882
|
+
cues: Array.from(cues),
|
|
6883
|
+
acceptanceCriteria: [
|
|
6884
|
+
"Recreate the same UI hierarchy and element structure.",
|
|
6885
|
+
"Match sizing, position, anchors, spacing, and layout behavior.",
|
|
6886
|
+
"Match fonts, weights, text sizing, alignment, and transparency.",
|
|
6887
|
+
"Match strokes, borders, corner radii, gradients, and layering.",
|
|
6888
|
+
"Read back or serialize the result instead of assuming it is correct."
|
|
6889
|
+
]
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
if (isUiTask) {
|
|
6893
|
+
return {
|
|
6894
|
+
kind: "ui_build",
|
|
6895
|
+
fidelity: strictFidelity ? "strict" : "normal",
|
|
6896
|
+
needsVisualAccuracy: strictFidelity,
|
|
6897
|
+
cues: Array.from(cues),
|
|
6898
|
+
acceptanceCriteria: [
|
|
6899
|
+
"Create the requested UI structure in the correct parent.",
|
|
6900
|
+
"Use appropriate scaling and layout properties for the requested target.",
|
|
6901
|
+
"Verify the created UI tree and important properties after building."
|
|
6902
|
+
]
|
|
6903
|
+
};
|
|
6904
|
+
}
|
|
6905
|
+
if (containsAny(lower, ["script", "module", "refactor", "fix bug", "luau", "code"])) {
|
|
6906
|
+
return {
|
|
6907
|
+
kind: "script_edit",
|
|
6908
|
+
fidelity: "normal",
|
|
6909
|
+
needsVisualAccuracy: false,
|
|
6910
|
+
cues: Array.from(cues),
|
|
6911
|
+
acceptanceCriteria: [
|
|
6912
|
+
"Make the requested code change without drifting from the user goal.",
|
|
6913
|
+
"Read back edited scripts after writing them.",
|
|
6914
|
+
"Check for runtime or test errors when execution is involved."
|
|
6915
|
+
]
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
return {
|
|
6919
|
+
kind: "general",
|
|
6920
|
+
fidelity: strictFidelity ? "strict" : "normal",
|
|
6921
|
+
needsVisualAccuracy: false,
|
|
6922
|
+
cues: Array.from(cues),
|
|
6923
|
+
acceptanceCriteria: [
|
|
6924
|
+
"Complete the user goal directly.",
|
|
6925
|
+
"Prefer dedicated tools over generic execution.",
|
|
6926
|
+
"Verify the effects of important changes before finishing."
|
|
6927
|
+
]
|
|
6928
|
+
};
|
|
6929
|
+
}
|
|
6930
|
+
function containsAny(text, cues) {
|
|
6931
|
+
return cues.some((cue) => text.includes(cue));
|
|
6932
|
+
}
|
|
6933
|
+
|
|
6934
|
+
// src/agent/critic.ts
|
|
6935
|
+
init_json_response();
|
|
6936
|
+
async function critiqueStep(opts) {
|
|
6937
|
+
const step = opts.plan.steps[opts.stepIndex];
|
|
6938
|
+
if (!step) {
|
|
6939
|
+
return {
|
|
6940
|
+
verdict: "blocked",
|
|
6941
|
+
summary: "No active step was available for critique.",
|
|
6942
|
+
missingCriteria: []
|
|
6943
|
+
};
|
|
6944
|
+
}
|
|
6945
|
+
const prompt = [
|
|
6946
|
+
"You are the Dominus alignment critic.",
|
|
6947
|
+
"Decide whether the current plan step is actually complete, needs another attempt, or is blocked.",
|
|
6948
|
+
"Be strict about missing evidence.",
|
|
6949
|
+
"Return JSON only in this shape:",
|
|
6950
|
+
'{"verdict":"done|retry|blocked","summary":"...","missingCriteria":["..."],"nextAction":"..."}',
|
|
6951
|
+
"",
|
|
6952
|
+
`Goal: ${opts.plan.goal}`,
|
|
6953
|
+
`Current step: ${step.description}`,
|
|
6954
|
+
`Task kind: ${opts.plan.taskProfile.kind}`,
|
|
6955
|
+
`Fidelity: ${opts.plan.taskProfile.fidelity}`,
|
|
6956
|
+
"Acceptance criteria:",
|
|
6957
|
+
...opts.plan.acceptanceCriteria.map((item) => `- ${item}`),
|
|
6958
|
+
"",
|
|
6959
|
+
"Assistant summary:",
|
|
6960
|
+
opts.assistantText || "(no assistant summary)",
|
|
6961
|
+
"",
|
|
6962
|
+
"Recent tool evidence:",
|
|
6963
|
+
JSON.stringify(summarizeToolResults(opts.toolResults), null, 2),
|
|
6964
|
+
"",
|
|
6965
|
+
"Rules:",
|
|
6966
|
+
"- For UI fidelity tasks, missing evidence about sizing, fonts, strokes, spacing, or hierarchy should usually mean retry.",
|
|
6967
|
+
"- If there was a failed tool result or failed verification, prefer retry unless the task is clearly blocked.",
|
|
6968
|
+
"- Use blocked only when the step cannot continue without outside input or a missing dependency."
|
|
6969
|
+
].join("\n");
|
|
6970
|
+
try {
|
|
6971
|
+
const response = await opts.ai.chat([{ role: "system", content: prompt }]);
|
|
6972
|
+
const parsed = parseJsonResponse(response.content);
|
|
6973
|
+
if (!parsed?.verdict || !parsed.summary) {
|
|
6974
|
+
return fallbackStepReview(opts);
|
|
6975
|
+
}
|
|
6976
|
+
return {
|
|
6977
|
+
verdict: normalizeVerdict(parsed.verdict),
|
|
6978
|
+
summary: parsed.summary.trim(),
|
|
6979
|
+
missingCriteria: Array.isArray(parsed.missingCriteria) ? parsed.missingCriteria.map((item) => String(item).trim()).filter(Boolean) : [],
|
|
6980
|
+
nextAction: typeof parsed.nextAction === "string" ? parsed.nextAction.trim() : void 0
|
|
6981
|
+
};
|
|
6982
|
+
} catch {
|
|
6983
|
+
return fallbackStepReview(opts);
|
|
6984
|
+
}
|
|
6985
|
+
}
|
|
6986
|
+
function fallbackStepReview(opts) {
|
|
6987
|
+
const failedResult = opts.toolResults.find(({ result }) => !result.success);
|
|
6988
|
+
if (failedResult) {
|
|
6989
|
+
return {
|
|
6990
|
+
verdict: "retry",
|
|
6991
|
+
summary: `The ${failedResult.call.name} result failed, so the step should be retried after correction.`,
|
|
6992
|
+
missingCriteria: ["Resolve the failing tool or verification result."],
|
|
6993
|
+
nextAction: `Inspect and fix the failure from ${failedResult.call.name}.`
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
if (opts.plan.taskProfile.needsVisualAccuracy && opts.toolResults.length === 0) {
|
|
6997
|
+
return {
|
|
6998
|
+
verdict: "retry",
|
|
6999
|
+
summary: "A visual fidelity step needs concrete evidence from Studio tools before it can be considered done.",
|
|
7000
|
+
missingCriteria: ["Collect read-back evidence for the relevant UI properties or tree."],
|
|
7001
|
+
nextAction: "Inspect or serialize the UI and compare it against the requested target."
|
|
7002
|
+
};
|
|
7003
|
+
}
|
|
7004
|
+
if (!opts.assistantText.trim() && opts.toolResults.length === 0) {
|
|
7005
|
+
return {
|
|
7006
|
+
verdict: "blocked",
|
|
7007
|
+
summary: "The step produced neither explanation nor tool evidence.",
|
|
7008
|
+
missingCriteria: ["Produce a concrete action or explain the blocker."]
|
|
7009
|
+
};
|
|
7010
|
+
}
|
|
7011
|
+
return {
|
|
7012
|
+
verdict: "done",
|
|
7013
|
+
summary: "The step produced enough evidence to continue.",
|
|
7014
|
+
missingCriteria: []
|
|
7015
|
+
};
|
|
7016
|
+
}
|
|
7017
|
+
function summarizeToolResults(toolResults) {
|
|
7018
|
+
return toolResults.map(({ call, result }) => ({
|
|
7019
|
+
tool: call.name,
|
|
7020
|
+
args: trimJson(call.arguments),
|
|
7021
|
+
success: result.success,
|
|
7022
|
+
error: result.error,
|
|
7023
|
+
data: trimJson(result.data)
|
|
7024
|
+
}));
|
|
7025
|
+
}
|
|
7026
|
+
function trimJson(value) {
|
|
7027
|
+
if (value == null) return value;
|
|
7028
|
+
const raw = JSON.stringify(value);
|
|
7029
|
+
if (raw.length <= 1200) return value;
|
|
7030
|
+
return `${raw.slice(0, 1200)}...`;
|
|
7031
|
+
}
|
|
7032
|
+
function normalizeVerdict(value) {
|
|
7033
|
+
if (value === "done" || value === "retry" || value === "blocked") return value;
|
|
7034
|
+
return "retry";
|
|
7035
|
+
}
|
|
7036
|
+
|
|
7037
|
+
// src/agent/loop.ts
|
|
7038
|
+
init_verifier();
|
|
7039
|
+
init_coordinator();
|
|
4962
7040
|
var AgentLoop = class {
|
|
4963
7041
|
ai;
|
|
4964
7042
|
server;
|
|
@@ -4993,10 +7071,11 @@ var AgentLoop = class {
|
|
|
4993
7071
|
this.projectId = projectId;
|
|
4994
7072
|
}
|
|
4995
7073
|
async *run(userMessage, imageAttachment) {
|
|
7074
|
+
const storedUserMessage = userMessage + (imageAttachment ? " [image attached]" : "");
|
|
4996
7075
|
storeMessage({
|
|
4997
7076
|
sessionId: this.sessionId,
|
|
4998
7077
|
role: "user",
|
|
4999
|
-
content:
|
|
7078
|
+
content: storedUserMessage,
|
|
5000
7079
|
createdAt: Date.now()
|
|
5001
7080
|
});
|
|
5002
7081
|
const { messages } = buildContext({
|
|
@@ -5006,6 +7085,7 @@ var AgentLoop = class {
|
|
|
5006
7085
|
projectId: this.projectId,
|
|
5007
7086
|
sessionId: this.sessionId
|
|
5008
7087
|
});
|
|
7088
|
+
this.conversationHistory.push({ role: "user", content: storedUserMessage });
|
|
5009
7089
|
if (imageAttachment) {
|
|
5010
7090
|
const lastMsg = messages[messages.length - 1];
|
|
5011
7091
|
if (lastMsg && lastMsg.role === "user") {
|
|
@@ -5022,76 +7102,167 @@ var AgentLoop = class {
|
|
|
5022
7102
|
}
|
|
5023
7103
|
}
|
|
5024
7104
|
const toolCtx = this.createToolContext();
|
|
5025
|
-
|
|
5026
|
-
|
|
7105
|
+
await this.tools.ready();
|
|
7106
|
+
const baseTools = this.tools.getAll();
|
|
7107
|
+
const coordinator = new MultiAgentCoordinator(this.ai, baseTools);
|
|
7108
|
+
const allTools = [...baseTools, coordinator.getMetaTool()];
|
|
7109
|
+
const taskProfile = buildTaskProfile(userMessage);
|
|
7110
|
+
if (coordinator.shouldAutoTrigger(userMessage)) {
|
|
7111
|
+
const events = [];
|
|
7112
|
+
const result = await coordinator.execute(
|
|
7113
|
+
{
|
|
7114
|
+
goal: userMessage,
|
|
7115
|
+
rootPath: inferRootPath(userMessage),
|
|
7116
|
+
taskType: taskProfile.kind.includes("ui") ? "ui" : taskProfile.kind
|
|
7117
|
+
},
|
|
7118
|
+
toolCtx,
|
|
7119
|
+
(event) => events.push(event)
|
|
7120
|
+
);
|
|
7121
|
+
for (const event of events) yield event;
|
|
7122
|
+
const summary = formatMultiAgentResult(result);
|
|
7123
|
+
yield { type: "text", content: summary };
|
|
7124
|
+
storeMessage({
|
|
7125
|
+
sessionId: this.sessionId,
|
|
7126
|
+
role: "assistant",
|
|
7127
|
+
content: summary,
|
|
7128
|
+
createdAt: Date.now()
|
|
7129
|
+
});
|
|
7130
|
+
this.conversationHistory.push({ role: "assistant", content: summary });
|
|
7131
|
+
yield { type: "done" };
|
|
7132
|
+
return;
|
|
7133
|
+
}
|
|
7134
|
+
let plan = await createExecutionPlan({
|
|
7135
|
+
ai: this.ai,
|
|
7136
|
+
goal: userMessage,
|
|
7137
|
+
taskProfile
|
|
7138
|
+
});
|
|
7139
|
+
yield { type: "plan", steps: plan.steps };
|
|
7140
|
+
yield { type: "text", content: formatExecutionPlan(plan) };
|
|
7141
|
+
let currentMessages = [
|
|
7142
|
+
...messages,
|
|
7143
|
+
{
|
|
7144
|
+
role: "system",
|
|
7145
|
+
content: buildExecutionContext(plan)
|
|
7146
|
+
}
|
|
7147
|
+
];
|
|
5027
7148
|
let iteration = 0;
|
|
5028
|
-
while (iteration < this.maxIterations) {
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
7149
|
+
while (iteration < this.maxIterations && !isPlanComplete(plan)) {
|
|
7150
|
+
const nextStep = getNextPendingStep(plan);
|
|
7151
|
+
if (!nextStep) break;
|
|
7152
|
+
plan = updateStepStatus(plan, nextStep.index, "running");
|
|
7153
|
+
yield { type: "plan_progress", stepIndex: nextStep.index, status: "running" };
|
|
7154
|
+
let stepDone = false;
|
|
7155
|
+
let stepAttempts = 0;
|
|
7156
|
+
while (!stepDone && stepAttempts <= this.maxRetries && iteration < this.maxIterations) {
|
|
7157
|
+
stepAttempts++;
|
|
7158
|
+
iteration++;
|
|
7159
|
+
currentMessages.push({
|
|
7160
|
+
role: "system",
|
|
7161
|
+
content: buildCurrentStepPrompt(plan, nextStep.index)
|
|
7162
|
+
});
|
|
7163
|
+
try {
|
|
7164
|
+
const response = await this.ai.chat(currentMessages, allTools);
|
|
7165
|
+
const assistantMsg = {
|
|
5036
7166
|
role: "assistant",
|
|
5037
|
-
content: response.content,
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
7167
|
+
content: response.content ?? null,
|
|
7168
|
+
...response.toolCalls.length > 0 ? {
|
|
7169
|
+
tool_calls: response.toolCalls.map((tc) => ({
|
|
7170
|
+
id: tc.id,
|
|
7171
|
+
type: "function",
|
|
7172
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
7173
|
+
}))
|
|
7174
|
+
} : {}
|
|
7175
|
+
};
|
|
7176
|
+
currentMessages.push(assistantMsg);
|
|
7177
|
+
this.conversationHistory.push(assistantMsg);
|
|
7178
|
+
if (response.content) {
|
|
7179
|
+
yield { type: "text", content: response.content };
|
|
7180
|
+
storeMessage({
|
|
7181
|
+
sessionId: this.sessionId,
|
|
7182
|
+
role: "assistant",
|
|
7183
|
+
content: response.content,
|
|
7184
|
+
createdAt: Date.now()
|
|
7185
|
+
});
|
|
7186
|
+
}
|
|
7187
|
+
const toolResults = [];
|
|
7188
|
+
for (const call of response.toolCalls) {
|
|
7189
|
+
yield { type: "tool_call", tool: call.name, args: call.arguments };
|
|
7190
|
+
const result = call.name === "run_parallel_task" ? await this.executeParallelTaskMetaCall(call.arguments, coordinator, toolCtx) : await this.tools.execute(call, toolCtx);
|
|
7191
|
+
toolResults.push({ call, result });
|
|
7192
|
+
if (call.name === "run_parallel_task") {
|
|
7193
|
+
const events = extractMultiAgentEvents(result);
|
|
7194
|
+
for (const event of events) yield event;
|
|
7195
|
+
}
|
|
7196
|
+
yield { type: "tool_result", tool: call.name, result };
|
|
7197
|
+
storeMessage({
|
|
7198
|
+
sessionId: this.sessionId,
|
|
7199
|
+
role: "tool_call",
|
|
7200
|
+
content: JSON.stringify({ name: call.name, args: call.arguments }),
|
|
7201
|
+
toolName: call.name,
|
|
7202
|
+
createdAt: Date.now()
|
|
7203
|
+
});
|
|
7204
|
+
storeMessage({
|
|
7205
|
+
sessionId: this.sessionId,
|
|
7206
|
+
role: "tool_result",
|
|
7207
|
+
content: JSON.stringify(result),
|
|
7208
|
+
toolName: call.name,
|
|
7209
|
+
createdAt: Date.now()
|
|
7210
|
+
});
|
|
7211
|
+
currentMessages.push({
|
|
7212
|
+
role: "tool",
|
|
7213
|
+
content: JSON.stringify(result),
|
|
7214
|
+
tool_call_id: call.id
|
|
7215
|
+
});
|
|
7216
|
+
this.conversationHistory.push({
|
|
7217
|
+
role: "tool",
|
|
7218
|
+
content: JSON.stringify(result),
|
|
7219
|
+
tool_call_id: call.id
|
|
7220
|
+
});
|
|
7221
|
+
}
|
|
7222
|
+
const verificationFailures = await this.runVerificationChecks(toolResults, toolCtx);
|
|
7223
|
+
if (verificationFailures.length > 0) {
|
|
7224
|
+
const failureMessage = formatVerificationFailures(verificationFailures);
|
|
7225
|
+
currentMessages.push({ role: "system", content: failureMessage });
|
|
7226
|
+
yield { type: "text", content: failureMessage };
|
|
7227
|
+
if (stepAttempts > this.maxRetries) {
|
|
7228
|
+
plan = updateStepStatus(plan, nextStep.index, "failed", failureMessage);
|
|
7229
|
+
yield { type: "plan_progress", stepIndex: nextStep.index, status: "failed" };
|
|
7230
|
+
stepDone = true;
|
|
7231
|
+
}
|
|
7232
|
+
continue;
|
|
7233
|
+
}
|
|
7234
|
+
const review = await critiqueStep({
|
|
7235
|
+
ai: this.ai,
|
|
7236
|
+
plan,
|
|
7237
|
+
stepIndex: nextStep.index,
|
|
7238
|
+
assistantText: response.content ?? "",
|
|
7239
|
+
toolResults
|
|
5089
7240
|
});
|
|
7241
|
+
if (review.verdict === "done") {
|
|
7242
|
+
plan = updateStepStatus(plan, nextStep.index, "done", review.summary);
|
|
7243
|
+
currentMessages.push({
|
|
7244
|
+
role: "system",
|
|
7245
|
+
content: `Step complete: ${review.summary}`
|
|
7246
|
+
});
|
|
7247
|
+
yield { type: "plan_progress", stepIndex: nextStep.index, status: "done" };
|
|
7248
|
+
stepDone = true;
|
|
7249
|
+
continue;
|
|
7250
|
+
}
|
|
7251
|
+
const feedback = formatCriticFeedback(review);
|
|
7252
|
+
currentMessages.push({ role: "system", content: feedback });
|
|
7253
|
+
yield { type: "text", content: feedback };
|
|
7254
|
+
if (review.verdict === "blocked" || stepAttempts > this.maxRetries) {
|
|
7255
|
+
plan = updateStepStatus(plan, nextStep.index, "failed", review.summary);
|
|
7256
|
+
yield { type: "plan_progress", stepIndex: nextStep.index, status: "failed" };
|
|
7257
|
+
stepDone = true;
|
|
7258
|
+
}
|
|
7259
|
+
} catch (err) {
|
|
7260
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7261
|
+
yield { type: "error", message };
|
|
7262
|
+
plan = updateStepStatus(plan, nextStep.index, "failed", message);
|
|
7263
|
+
yield { type: "plan_progress", stepIndex: nextStep.index, status: "failed" };
|
|
7264
|
+
stepDone = true;
|
|
5090
7265
|
}
|
|
5091
|
-
} catch (err) {
|
|
5092
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5093
|
-
yield { type: "error", message };
|
|
5094
|
-
break;
|
|
5095
7266
|
}
|
|
5096
7267
|
}
|
|
5097
7268
|
yield { type: "done" };
|
|
@@ -5136,102 +7307,133 @@ var AgentLoop = class {
|
|
|
5136
7307
|
config: createConfigAccess()
|
|
5137
7308
|
};
|
|
5138
7309
|
}
|
|
7310
|
+
async runVerificationChecks(toolResults, toolCtx) {
|
|
7311
|
+
const checks = toolResults.flatMap(({ call, result }) => inferVerifyChecks(call, result));
|
|
7312
|
+
const failures = [];
|
|
7313
|
+
for (const check of checks) {
|
|
7314
|
+
const verification = await verifyAction(check, toolCtx);
|
|
7315
|
+
if (!verification.success) {
|
|
7316
|
+
failures.push(verification);
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
return failures;
|
|
7320
|
+
}
|
|
7321
|
+
async executeParallelTaskMetaCall(args, coordinator, toolCtx) {
|
|
7322
|
+
const events = [];
|
|
7323
|
+
const result = await coordinator.execute(normalizeParallelTaskArgs(args), toolCtx, (event) => {
|
|
7324
|
+
events.push(event);
|
|
7325
|
+
});
|
|
7326
|
+
return {
|
|
7327
|
+
success: result.verificationFailures.length === 0 && result.applied.every((item) => item.result.success),
|
|
7328
|
+
data: {
|
|
7329
|
+
...result,
|
|
7330
|
+
events,
|
|
7331
|
+
summary: formatMultiAgentResult(result)
|
|
7332
|
+
}
|
|
7333
|
+
};
|
|
7334
|
+
}
|
|
5139
7335
|
clearHistory() {
|
|
5140
7336
|
this.conversationHistory = [];
|
|
5141
7337
|
this.sessionId = nanoid();
|
|
5142
7338
|
}
|
|
5143
7339
|
};
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
7340
|
+
function normalizeParallelTaskArgs(args) {
|
|
7341
|
+
return {
|
|
7342
|
+
goal: String(args.goal ?? ""),
|
|
7343
|
+
rootPath: typeof args.rootPath === "string" ? args.rootPath : void 0,
|
|
7344
|
+
maxWorkers: typeof args.maxWorkers === "number" ? args.maxWorkers : void 0,
|
|
7345
|
+
constraints: typeof args.constraints === "string" ? args.constraints : void 0,
|
|
7346
|
+
taskType: typeof args.taskType === "string" ? args.taskType : void 0
|
|
7347
|
+
};
|
|
7348
|
+
}
|
|
7349
|
+
function inferRootPath(userMessage) {
|
|
7350
|
+
const match = userMessage.match(/\b(?:StarterGui|PlayerGui|Workspace|ReplicatedStorage|ServerScriptService|StarterPlayer)(?:\.[A-Za-z0-9_]+)*/);
|
|
7351
|
+
return match?.[0];
|
|
7352
|
+
}
|
|
7353
|
+
function formatMultiAgentResult(result) {
|
|
7354
|
+
const lines = [
|
|
7355
|
+
`Multi-agent task complete: ${result.goal}`,
|
|
7356
|
+
`Workers: ${result.assignments.length}`,
|
|
7357
|
+
`Accepted proposals: ${result.decision.accepted.length}`,
|
|
7358
|
+
`Rejected proposals: ${result.decision.rejected.length}`,
|
|
7359
|
+
`Applied tool calls: ${result.applied.length}`,
|
|
7360
|
+
`Verification failures: ${result.verificationFailures.length}`
|
|
7361
|
+
];
|
|
7362
|
+
for (const accepted of result.decision.accepted) {
|
|
7363
|
+
lines.push(`- ${accepted.workerId}: ${accepted.proposal.summary}`);
|
|
5153
7364
|
}
|
|
5154
|
-
|
|
5155
|
-
|
|
7365
|
+
for (const rejected of result.decision.rejected) {
|
|
7366
|
+
lines.push(`- Rejected ${rejected.workerId}: ${rejected.reason}`);
|
|
5156
7367
|
}
|
|
5157
|
-
|
|
5158
|
-
|
|
7368
|
+
return lines.join("\n");
|
|
7369
|
+
}
|
|
7370
|
+
function extractMultiAgentEvents(result) {
|
|
7371
|
+
const data = result.data;
|
|
7372
|
+
if (!data || typeof data !== "object") return [];
|
|
7373
|
+
const events = data.events;
|
|
7374
|
+
if (!Array.isArray(events)) return [];
|
|
7375
|
+
return events.filter((event) => {
|
|
7376
|
+
return event && typeof event === "object" && "type" in event;
|
|
7377
|
+
});
|
|
7378
|
+
}
|
|
7379
|
+
function buildExecutionContext(plan) {
|
|
7380
|
+
const lines = [
|
|
7381
|
+
"You are operating in structured execution mode.",
|
|
7382
|
+
"Follow the plan, stay aligned to the goal, and verify before moving on.",
|
|
7383
|
+
`Goal: ${plan.goal}`,
|
|
7384
|
+
`Task kind: ${plan.taskProfile.kind}`,
|
|
7385
|
+
`Fidelity: ${plan.taskProfile.fidelity}`,
|
|
7386
|
+
"Acceptance criteria:",
|
|
7387
|
+
...plan.acceptanceCriteria.map((item) => `- ${item}`)
|
|
7388
|
+
];
|
|
7389
|
+
if (plan.notes.length > 0) {
|
|
7390
|
+
lines.push("Execution notes:");
|
|
7391
|
+
lines.push(...plan.notes.map((note) => `- ${note}`));
|
|
5159
7392
|
}
|
|
5160
|
-
|
|
5161
|
-
|
|
7393
|
+
return lines.join("\n");
|
|
7394
|
+
}
|
|
7395
|
+
function buildCurrentStepPrompt(plan, stepIndex) {
|
|
7396
|
+
const step = plan.steps[stepIndex];
|
|
7397
|
+
return [
|
|
7398
|
+
`Current step ${stepIndex + 1}/${plan.steps.length}: ${step.description}`,
|
|
7399
|
+
"Focus on this step only.",
|
|
7400
|
+
"Use Studio tools to gather evidence and make changes.",
|
|
7401
|
+
"Prefer dedicated tools over run_code.",
|
|
7402
|
+
"Before considering the step complete, produce evidence that it satisfies the relevant acceptance criteria.",
|
|
7403
|
+
plan.taskProfile.needsVisualAccuracy ? "For UI fidelity work, do not approximate. Check exact layout, font, stroke, spacing, and hierarchy details." : "Keep the work tightly aligned to the requested outcome."
|
|
7404
|
+
].join("\n");
|
|
7405
|
+
}
|
|
7406
|
+
function formatVerificationFailures(failures) {
|
|
7407
|
+
const lines = ["Verification failed after the recent tool actions:"];
|
|
7408
|
+
for (const failure of failures) {
|
|
7409
|
+
lines.push(`- ${failure.error ?? "Unknown verification failure"}`);
|
|
5162
7410
|
}
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
} catch (err) {
|
|
5171
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
5172
|
-
return { success: false, error: message };
|
|
5173
|
-
}
|
|
7411
|
+
lines.push("Correct the mismatch before moving to the next plan step.");
|
|
7412
|
+
return lines.join("\n");
|
|
7413
|
+
}
|
|
7414
|
+
function formatCriticFeedback(review) {
|
|
7415
|
+
const lines = [`Critic review: ${review.summary}`];
|
|
7416
|
+
for (const item of review.missingCriteria) {
|
|
7417
|
+
lines.push(`- Missing: ${item}`);
|
|
5174
7418
|
}
|
|
5175
|
-
|
|
5176
|
-
|
|
7419
|
+
if (review.nextAction) {
|
|
7420
|
+
lines.push(`Next action: ${review.nextAction}`);
|
|
5177
7421
|
}
|
|
5178
|
-
|
|
5179
|
-
function createDefaultRegistry() {
|
|
5180
|
-
const registry = new ToolRegistry();
|
|
5181
|
-
const toolModules = [
|
|
5182
|
-
Promise.resolve().then(() => (init_read_script(), read_script_exports)),
|
|
5183
|
-
Promise.resolve().then(() => (init_edit_script(), edit_script_exports)),
|
|
5184
|
-
Promise.resolve().then(() => (init_run_code(), run_code_exports)),
|
|
5185
|
-
Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
|
|
5186
|
-
Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
|
|
5187
|
-
Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
|
|
5188
|
-
Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
|
|
5189
|
-
Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
|
|
5190
|
-
Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
|
|
5191
|
-
Promise.resolve().then(() => (init_get_output(), get_output_exports)),
|
|
5192
|
-
Promise.resolve().then(() => (init_get_selection(), get_selection_exports)),
|
|
5193
|
-
Promise.resolve().then(() => (init_search_scripts(), search_scripts_exports)),
|
|
5194
|
-
Promise.resolve().then(() => (init_run_tests(), run_tests_exports)),
|
|
5195
|
-
Promise.resolve().then(() => (init_get_class_info(), get_class_info_exports)),
|
|
5196
|
-
Promise.resolve().then(() => (init_create_ui(), create_ui_exports)),
|
|
5197
|
-
Promise.resolve().then(() => (init_execute_run_test(), execute_run_test_exports)),
|
|
5198
|
-
Promise.resolve().then(() => (init_execute_play_test(), execute_play_test_exports)),
|
|
5199
|
-
Promise.resolve().then(() => (init_create_part(), create_part_exports)),
|
|
5200
|
-
Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
|
|
5201
|
-
Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
|
|
5202
|
-
Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
|
|
5203
|
-
Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
|
|
5204
|
-
Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
|
|
5205
|
-
Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
|
|
5206
|
-
Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
|
|
5207
|
-
Promise.resolve().then(() => (init_recall(), recall_exports)),
|
|
5208
|
-
Promise.resolve().then(() => (init_remember(), remember_exports)),
|
|
5209
|
-
Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
|
|
5210
|
-
];
|
|
5211
|
-
void Promise.all(
|
|
5212
|
-
toolModules.map(async (mod) => {
|
|
5213
|
-
const m = await mod;
|
|
5214
|
-
if (m.tool) registry.register(m.tool);
|
|
5215
|
-
})
|
|
5216
|
-
);
|
|
5217
|
-
void Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
|
|
5218
|
-
registry.register(m.undoTool);
|
|
5219
|
-
registry.register(m.redoTool);
|
|
5220
|
-
});
|
|
5221
|
-
return registry;
|
|
7422
|
+
return lines.join("\n");
|
|
5222
7423
|
}
|
|
5223
7424
|
|
|
5224
7425
|
// src/index.tsx
|
|
7426
|
+
init_registry();
|
|
5225
7427
|
init_store();
|
|
5226
7428
|
init_config();
|
|
5227
|
-
var CACHE_DIR =
|
|
5228
|
-
var CACHE_FILE =
|
|
7429
|
+
var CACHE_DIR = path5.join(os3.homedir(), ".dominus");
|
|
7430
|
+
var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
|
|
5229
7431
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
5230
7432
|
var NPM_PACKAGE = "dominus-cli";
|
|
5231
7433
|
function readCache() {
|
|
5232
7434
|
try {
|
|
5233
|
-
if (
|
|
5234
|
-
return JSON.parse(
|
|
7435
|
+
if (fs6.existsSync(CACHE_FILE)) {
|
|
7436
|
+
return JSON.parse(fs6.readFileSync(CACHE_FILE, "utf-8"));
|
|
5235
7437
|
}
|
|
5236
7438
|
} catch {
|
|
5237
7439
|
}
|
|
@@ -5239,8 +7441,8 @@ function readCache() {
|
|
|
5239
7441
|
}
|
|
5240
7442
|
function writeCache(cache) {
|
|
5241
7443
|
try {
|
|
5242
|
-
if (!
|
|
5243
|
-
|
|
7444
|
+
if (!fs6.existsSync(CACHE_DIR)) fs6.mkdirSync(CACHE_DIR, { recursive: true });
|
|
7445
|
+
fs6.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
5244
7446
|
} catch {
|
|
5245
7447
|
}
|
|
5246
7448
|
}
|
|
@@ -5340,8 +7542,8 @@ async function autoUpdate(currentVersion) {
|
|
|
5340
7542
|
try {
|
|
5341
7543
|
const out = execSync("pnpm list -g dominus-cli", { stdio: "pipe", timeout: 1e4 }).toString();
|
|
5342
7544
|
if (out.includes("link:")) {
|
|
5343
|
-
const pkgPath =
|
|
5344
|
-
if (
|
|
7545
|
+
const pkgPath = path5.resolve(__dirname$1, "..");
|
|
7546
|
+
if (fs6.existsSync(path5.join(pkgPath, ".git"))) {
|
|
5345
7547
|
try {
|
|
5346
7548
|
execSync("git pull", { cwd: pkgPath, stdio: "pipe", timeout: 3e4 });
|
|
5347
7549
|
execSync("pnpm install", { cwd: pkgPath, stdio: "pipe", timeout: 6e4 });
|
|
@@ -5368,9 +7570,9 @@ async function autoUpdate(currentVersion) {
|
|
|
5368
7570
|
return { success: false, from: currentVersion, to: info.latestVersion, error: `Update command failed: ${err}` };
|
|
5369
7571
|
}
|
|
5370
7572
|
}
|
|
5371
|
-
var __dirname$1 =
|
|
5372
|
-
var __dirname2 =
|
|
5373
|
-
var VERSION = JSON.parse(
|
|
7573
|
+
var __dirname$1 = path5.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
7574
|
+
var __dirname2 = path5.dirname(fileURLToPath(import.meta.url));
|
|
7575
|
+
var VERSION = JSON.parse(fs6.readFileSync(path5.join(__dirname2, "..", "package.json"), "utf-8")).version;
|
|
5374
7576
|
var program = new Command();
|
|
5375
7577
|
program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
|
|
5376
7578
|
program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
|
|
@@ -5499,24 +7701,24 @@ program.command("update").description("Auto-update Dominus to the latest version
|
|
|
5499
7701
|
});
|
|
5500
7702
|
program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
|
|
5501
7703
|
const { execSync: execSync3 } = await import('child_process');
|
|
5502
|
-
const packageRoot =
|
|
5503
|
-
const pluginDir =
|
|
5504
|
-
const pluginProject =
|
|
5505
|
-
const studioPlugins =
|
|
7704
|
+
const packageRoot = path5.resolve(import.meta.dirname ?? __dirname2, "..");
|
|
7705
|
+
const pluginDir = path5.join(packageRoot, "plugin");
|
|
7706
|
+
const pluginProject = path5.join(pluginDir, "default.project.json");
|
|
7707
|
+
const studioPlugins = path5.join(
|
|
5506
7708
|
os3.homedir(),
|
|
5507
7709
|
"AppData",
|
|
5508
7710
|
"Local",
|
|
5509
7711
|
"Roblox",
|
|
5510
7712
|
"Plugins"
|
|
5511
7713
|
);
|
|
5512
|
-
const destFile =
|
|
5513
|
-
if (!
|
|
7714
|
+
const destFile = path5.join(studioPlugins, "Dominus.rbxm");
|
|
7715
|
+
if (!fs6.existsSync(pluginProject)) {
|
|
5514
7716
|
console.log(" \u2716 Plugin source not found.");
|
|
5515
7717
|
console.log(` Expected at: ${pluginDir}`);
|
|
5516
7718
|
return;
|
|
5517
7719
|
}
|
|
5518
|
-
if (!
|
|
5519
|
-
|
|
7720
|
+
if (!fs6.existsSync(studioPlugins)) {
|
|
7721
|
+
fs6.mkdirSync(studioPlugins, { recursive: true });
|
|
5520
7722
|
}
|
|
5521
7723
|
let rojoAvailable = false;
|
|
5522
7724
|
try {
|
|
@@ -5544,48 +7746,48 @@ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Cl
|
|
|
5544
7746
|
});
|
|
5545
7747
|
function getMcpTargets() {
|
|
5546
7748
|
const home = os3.homedir();
|
|
5547
|
-
const appData = process.env.APPDATA ||
|
|
5548
|
-
const localAppData = process.env.LOCALAPPDATA ||
|
|
7749
|
+
const appData = process.env.APPDATA || path5.join(home, "AppData", "Roaming");
|
|
7750
|
+
const localAppData = process.env.LOCALAPPDATA || path5.join(home, "AppData", "Local");
|
|
5549
7751
|
const cwd = process.cwd();
|
|
5550
7752
|
return [
|
|
5551
7753
|
{
|
|
5552
7754
|
name: "Cursor (project)",
|
|
5553
|
-
configPath:
|
|
7755
|
+
configPath: path5.join(cwd, ".cursor", "mcp.json"),
|
|
5554
7756
|
keyPath: ["mcpServers"]
|
|
5555
7757
|
},
|
|
5556
7758
|
{
|
|
5557
7759
|
name: "Cursor (global)",
|
|
5558
|
-
configPath:
|
|
7760
|
+
configPath: path5.join(home, ".cursor", "mcp.json"),
|
|
5559
7761
|
keyPath: ["mcpServers"]
|
|
5560
7762
|
},
|
|
5561
7763
|
{
|
|
5562
7764
|
name: "Claude Desktop",
|
|
5563
|
-
configPath:
|
|
7765
|
+
configPath: path5.join(appData, "Claude", "claude_desktop_config.json"),
|
|
5564
7766
|
keyPath: ["mcpServers"]
|
|
5565
7767
|
},
|
|
5566
7768
|
{
|
|
5567
7769
|
name: "Claude Code",
|
|
5568
|
-
configPath:
|
|
7770
|
+
configPath: path5.join(home, ".claude.json"),
|
|
5569
7771
|
keyPath: ["mcpServers"]
|
|
5570
7772
|
},
|
|
5571
7773
|
{
|
|
5572
7774
|
name: "Windsurf (project)",
|
|
5573
|
-
configPath:
|
|
7775
|
+
configPath: path5.join(cwd, ".windsurf", "mcp.json"),
|
|
5574
7776
|
keyPath: ["mcpServers"]
|
|
5575
7777
|
},
|
|
5576
7778
|
{
|
|
5577
7779
|
name: "Windsurf (global)",
|
|
5578
|
-
configPath:
|
|
7780
|
+
configPath: path5.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
5579
7781
|
keyPath: ["mcpServers"]
|
|
5580
7782
|
},
|
|
5581
7783
|
{
|
|
5582
7784
|
name: "VS Code (project)",
|
|
5583
|
-
configPath:
|
|
7785
|
+
configPath: path5.join(cwd, ".vscode", "mcp.json"),
|
|
5584
7786
|
keyPath: ["servers"]
|
|
5585
7787
|
},
|
|
5586
7788
|
{
|
|
5587
7789
|
name: "VS Code (global)",
|
|
5588
|
-
configPath:
|
|
7790
|
+
configPath: path5.join(localAppData, "Code", "User", "settings.json"),
|
|
5589
7791
|
keyPath: ["mcp", "servers"]
|
|
5590
7792
|
}
|
|
5591
7793
|
];
|
|
@@ -5597,9 +7799,9 @@ var dominusEntry = {
|
|
|
5597
7799
|
};
|
|
5598
7800
|
function injectMcpConfig(target) {
|
|
5599
7801
|
let config = {};
|
|
5600
|
-
if (
|
|
7802
|
+
if (fs6.existsSync(target.configPath)) {
|
|
5601
7803
|
try {
|
|
5602
|
-
config = JSON.parse(
|
|
7804
|
+
config = JSON.parse(fs6.readFileSync(target.configPath, "utf-8"));
|
|
5603
7805
|
} catch {
|
|
5604
7806
|
config = {};
|
|
5605
7807
|
}
|
|
@@ -5615,12 +7817,12 @@ function injectMcpConfig(target) {
|
|
|
5615
7817
|
return { status: "already" };
|
|
5616
7818
|
}
|
|
5617
7819
|
obj["dominus"] = { ...dominusEntry };
|
|
5618
|
-
const dir =
|
|
5619
|
-
if (!
|
|
5620
|
-
|
|
7820
|
+
const dir = path5.dirname(target.configPath);
|
|
7821
|
+
if (!fs6.existsSync(dir)) {
|
|
7822
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
5621
7823
|
}
|
|
5622
|
-
|
|
5623
|
-
return { status:
|
|
7824
|
+
fs6.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7825
|
+
return { status: fs6.existsSync(target.configPath) ? "installed" : "created" };
|
|
5624
7826
|
}
|
|
5625
7827
|
program.command("mcp-install").description("Auto-install Dominus MCP into Cursor, VS Code, Claude Desktop, Windsurf, etc.").option("-a, --all", "Install to all detected editors").action(async (opts) => {
|
|
5626
7828
|
const targets = getMcpTargets();
|
|
@@ -5642,7 +7844,7 @@ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor
|
|
|
5642
7844
|
}
|
|
5643
7845
|
const detected = targets.map((t) => ({
|
|
5644
7846
|
target: t,
|
|
5645
|
-
exists:
|
|
7847
|
+
exists: fs6.existsSync(path5.dirname(t.configPath))
|
|
5646
7848
|
}));
|
|
5647
7849
|
const toInstall = detected.filter((d) => d.exists);
|
|
5648
7850
|
if (toInstall.length === 0) {
|
|
@@ -5715,23 +7917,23 @@ program.command("mcp-setup").description("Print MCP configuration for manual set
|
|
|
5715
7917
|
console.log("");
|
|
5716
7918
|
});
|
|
5717
7919
|
function buildPluginXml(pluginDir, destFile) {
|
|
5718
|
-
const srcDir =
|
|
5719
|
-
if (!
|
|
7920
|
+
const srcDir = path5.join(pluginDir, "src");
|
|
7921
|
+
if (!fs6.existsSync(srcDir)) {
|
|
5720
7922
|
console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
|
|
5721
7923
|
return;
|
|
5722
7924
|
}
|
|
5723
|
-
const files =
|
|
7925
|
+
const files = fs6.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
|
|
5724
7926
|
const mainFile = files.find((f) => f === "init.server.lua");
|
|
5725
7927
|
const moduleFiles = files.filter((f) => f !== "init.server.lua");
|
|
5726
7928
|
if (!mainFile) {
|
|
5727
7929
|
console.log(" \u2716 init.server.lua not found in plugin source");
|
|
5728
7930
|
return;
|
|
5729
7931
|
}
|
|
5730
|
-
const mainSource =
|
|
7932
|
+
const mainSource = fs6.readFileSync(path5.join(srcDir, mainFile), "utf-8");
|
|
5731
7933
|
let moduleItems = "";
|
|
5732
7934
|
for (const mf of moduleFiles) {
|
|
5733
7935
|
const modName = mf.replace(".lua", "");
|
|
5734
|
-
const modSource =
|
|
7936
|
+
const modSource = fs6.readFileSync(path5.join(srcDir, mf), "utf-8");
|
|
5735
7937
|
moduleItems += `
|
|
5736
7938
|
<Item class="ModuleScript" referent="${modName}">
|
|
5737
7939
|
<Properties>
|
|
@@ -5749,7 +7951,7 @@ function buildPluginXml(pluginDir, destFile) {
|
|
|
5749
7951
|
</Properties>${moduleItems}
|
|
5750
7952
|
</Item>
|
|
5751
7953
|
</roblox>`;
|
|
5752
|
-
|
|
7954
|
+
fs6.writeFileSync(destFile, rbxmx, "utf-8");
|
|
5753
7955
|
console.log(` \u2714 Plugin installed to: ${destFile}`);
|
|
5754
7956
|
console.log(" Restart Roblox Studio to load the plugin.");
|
|
5755
7957
|
}
|