dominus-cli 0.5.9 → 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 +3010 -631
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +4286 -639
- 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",
|
|
@@ -252,7 +252,12 @@ var init_ws_server = __esm({
|
|
|
252
252
|
init_protocol();
|
|
253
253
|
DominusServer = class extends EventEmitter {
|
|
254
254
|
wss = null;
|
|
255
|
-
|
|
255
|
+
/** All connected Studio instances, keyed by placeId (0 for unknown) */
|
|
256
|
+
studioClients = /* @__PURE__ */ new Map();
|
|
257
|
+
/** Reverse lookup: ws → placeId */
|
|
258
|
+
wsToPlaceId = /* @__PURE__ */ new Map();
|
|
259
|
+
/** Which placeId to target. 0 = auto-select if only one is connected. */
|
|
260
|
+
activePlaceId = 0;
|
|
256
261
|
controllers = /* @__PURE__ */ new Set();
|
|
257
262
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
258
263
|
relayRequests = /* @__PURE__ */ new Map();
|
|
@@ -301,7 +306,9 @@ var init_ws_server = __esm({
|
|
|
301
306
|
this.addController(ws);
|
|
302
307
|
ws.send(serializeMessage(createMessage("controller:welcome", {
|
|
303
308
|
studioConnected: this.isConnected(),
|
|
304
|
-
port: this.port
|
|
309
|
+
port: this.port,
|
|
310
|
+
connections: this.listConnections(),
|
|
311
|
+
activePlaceId: this.activePlaceId
|
|
305
312
|
})));
|
|
306
313
|
return;
|
|
307
314
|
}
|
|
@@ -309,7 +316,7 @@ var init_ws_server = __esm({
|
|
|
309
316
|
if (this.controllers.has(ws)) {
|
|
310
317
|
this.handleControllerMessage(ws, msg);
|
|
311
318
|
} else {
|
|
312
|
-
this.handleStudioMessage(msg);
|
|
319
|
+
this.handleStudioMessage(ws, msg);
|
|
313
320
|
}
|
|
314
321
|
} catch (err) {
|
|
315
322
|
this.emit("server:error", err);
|
|
@@ -318,17 +325,41 @@ var init_ws_server = __esm({
|
|
|
318
325
|
ws.on("close", () => {
|
|
319
326
|
if (this.controllers.has(ws)) {
|
|
320
327
|
this.controllers.delete(ws);
|
|
321
|
-
for (const [id,
|
|
322
|
-
if (controllerWs === ws) this.relayRequests.delete(id);
|
|
328
|
+
for (const [id, relay] of this.relayRequests) {
|
|
329
|
+
if (relay.controllerWs === ws) this.relayRequests.delete(id);
|
|
323
330
|
}
|
|
324
|
-
} else
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.
|
|
331
|
+
} else {
|
|
332
|
+
const placeId = this.wsToPlaceId.get(ws);
|
|
333
|
+
if (placeId !== void 0) {
|
|
334
|
+
const conn = this.studioClients.get(placeId);
|
|
335
|
+
this.studioClients.delete(placeId);
|
|
336
|
+
this.wsToPlaceId.delete(ws);
|
|
337
|
+
this.emit("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
338
|
+
this.broadcastToControllers("studio:disconnected", { placeId, placeName: conn?.placeName });
|
|
339
|
+
for (const [id, pending] of this.pendingRequests) {
|
|
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
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (this.activePlaceId === placeId) {
|
|
360
|
+
this.activePlaceId = 0;
|
|
361
|
+
this.broadcastTargetState();
|
|
362
|
+
}
|
|
332
363
|
}
|
|
333
364
|
}
|
|
334
365
|
});
|
|
@@ -337,19 +368,29 @@ var init_ws_server = __esm({
|
|
|
337
368
|
});
|
|
338
369
|
}
|
|
339
370
|
setStudioClient(ws, msg) {
|
|
340
|
-
if (this.studioClient) {
|
|
341
|
-
this.studioClient.ws.close();
|
|
342
|
-
}
|
|
343
371
|
const payload = msg.payload;
|
|
344
|
-
|
|
372
|
+
const placeId = payload.placeId ?? 0;
|
|
373
|
+
const existing = this.studioClients.get(placeId);
|
|
374
|
+
if (existing && existing.ws !== ws) {
|
|
375
|
+
existing.ws.close();
|
|
376
|
+
this.wsToPlaceId.delete(existing.ws);
|
|
377
|
+
}
|
|
378
|
+
const conn = {
|
|
345
379
|
ws,
|
|
380
|
+
connectionId: `${placeId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,
|
|
346
381
|
connectedAt: Date.now(),
|
|
347
382
|
studioVersion: payload.studioVersion,
|
|
348
|
-
placeId
|
|
383
|
+
placeId,
|
|
349
384
|
placeName: payload.placeName
|
|
350
385
|
};
|
|
351
|
-
this.
|
|
352
|
-
this.
|
|
386
|
+
this.studioClients.set(placeId, conn);
|
|
387
|
+
this.wsToPlaceId.set(ws, placeId);
|
|
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();
|
|
353
394
|
}
|
|
354
395
|
addController(ws) {
|
|
355
396
|
this.controllers.add(ws);
|
|
@@ -364,39 +405,80 @@ var init_ws_server = __esm({
|
|
|
364
405
|
}
|
|
365
406
|
}
|
|
366
407
|
handleControllerMessage(controllerWs, msg) {
|
|
367
|
-
if (
|
|
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
|
+
}
|
|
429
|
+
const target = this.resolveTarget();
|
|
430
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
368
431
|
controllerWs.send(serializeMessage({
|
|
369
432
|
id: msg.id,
|
|
370
433
|
type: StudioMsg.RESPONSE,
|
|
371
|
-
payload: { error:
|
|
434
|
+
payload: { error: this.getTargetError() },
|
|
372
435
|
ts: Date.now()
|
|
373
436
|
}));
|
|
374
437
|
return;
|
|
375
438
|
}
|
|
376
|
-
this.relayRequests.set(msg.id,
|
|
377
|
-
|
|
439
|
+
this.relayRequests.set(msg.id, {
|
|
440
|
+
controllerWs,
|
|
441
|
+
targetWs: target.ws,
|
|
442
|
+
targetPlaceId: target.placeId ?? 0
|
|
443
|
+
});
|
|
444
|
+
target.ws.send(serializeMessage(msg));
|
|
378
445
|
}
|
|
379
|
-
handleStudioMessage(msg) {
|
|
446
|
+
handleStudioMessage(ws, msg) {
|
|
380
447
|
if (msg.type === StudioMsg.CONNECTED) {
|
|
381
448
|
const payload = msg.payload;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
449
|
+
const placeId = this.wsToPlaceId.get(ws);
|
|
450
|
+
if (placeId !== void 0) {
|
|
451
|
+
const conn = this.studioClients.get(placeId);
|
|
452
|
+
if (conn) {
|
|
453
|
+
conn.studioVersion = payload.studioVersion;
|
|
454
|
+
conn.placeId = payload.placeId;
|
|
455
|
+
conn.placeName = payload.placeName;
|
|
456
|
+
}
|
|
386
457
|
}
|
|
387
458
|
this.emit("studio:connected", payload);
|
|
388
459
|
this.broadcastToControllers("studio:connected", payload);
|
|
389
460
|
return;
|
|
390
461
|
}
|
|
391
462
|
if (msg.type === StudioMsg.RESPONSE) {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|
|
395
473
|
this.relayRequests.delete(msg.id);
|
|
396
474
|
return;
|
|
397
475
|
}
|
|
398
476
|
const pending = this.pendingRequests.get(msg.id);
|
|
399
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
|
+
}
|
|
400
482
|
clearTimeout(pending.timer);
|
|
401
483
|
pending.resolve(msg);
|
|
402
484
|
this.pendingRequests.delete(msg.id);
|
|
@@ -407,10 +489,35 @@ var init_ws_server = __esm({
|
|
|
407
489
|
this.emit("message", msg);
|
|
408
490
|
this.broadcastToControllers(msg.type, msg.payload);
|
|
409
491
|
}
|
|
492
|
+
/** Resolve which Studio connection to target */
|
|
493
|
+
resolveTarget() {
|
|
494
|
+
if (this.studioClients.size === 0) return null;
|
|
495
|
+
if (this.activePlaceId !== 0) {
|
|
496
|
+
const conn = this.studioClients.get(this.activePlaceId);
|
|
497
|
+
if (conn && conn.ws.readyState === WebSocket.OPEN) return conn;
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
if (this.studioClients.size === 1) {
|
|
501
|
+
const conn = this.studioClients.values().next().value;
|
|
502
|
+
if (conn.ws.readyState === WebSocket.OPEN) return conn;
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
/** Generate an appropriate error message when no target found */
|
|
508
|
+
getTargetError() {
|
|
509
|
+
if (this.studioClients.size === 0) return "No Studio connection";
|
|
510
|
+
if (this.activePlaceId !== 0) {
|
|
511
|
+
return `Active place ${this.activePlaceId} is not connected. Use set_active_place or list_connections.`;
|
|
512
|
+
}
|
|
513
|
+
const places = [...this.studioClients.values()].map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`);
|
|
514
|
+
return `Multiple Studio instances connected: ${places.join(", ")}. Use set_active_place to choose one.`;
|
|
515
|
+
}
|
|
410
516
|
sendRequest(type, payload, timeoutMs = 1e4) {
|
|
411
517
|
return new Promise((resolve, reject) => {
|
|
412
|
-
|
|
413
|
-
|
|
518
|
+
const target = this.resolveTarget();
|
|
519
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) {
|
|
520
|
+
reject(new Error(this.getTargetError()));
|
|
414
521
|
return;
|
|
415
522
|
}
|
|
416
523
|
const msg = createMessage(type, payload);
|
|
@@ -421,33 +528,61 @@ var init_ws_server = __esm({
|
|
|
421
528
|
this.pendingRequests.set(msg.id, {
|
|
422
529
|
resolve,
|
|
423
530
|
reject,
|
|
424
|
-
timer
|
|
531
|
+
timer,
|
|
532
|
+
targetWs: target.ws,
|
|
533
|
+
targetPlaceId: target.placeId ?? 0
|
|
425
534
|
});
|
|
426
|
-
|
|
535
|
+
target.ws.send(serializeMessage(msg));
|
|
427
536
|
});
|
|
428
537
|
}
|
|
429
538
|
sendNotification(type, payload) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
}
|
|
539
|
+
const target = this.resolveTarget();
|
|
540
|
+
if (!target || target.ws.readyState !== WebSocket.OPEN) return;
|
|
433
541
|
const msg = createMessage(type, payload);
|
|
434
|
-
|
|
542
|
+
target.ws.send(serializeMessage(msg));
|
|
435
543
|
}
|
|
436
544
|
isConnected() {
|
|
437
|
-
return this.
|
|
545
|
+
return this.resolveTarget() !== null;
|
|
438
546
|
}
|
|
439
547
|
getConnectionInfo() {
|
|
440
|
-
return this.
|
|
548
|
+
return this.resolveTarget();
|
|
549
|
+
}
|
|
550
|
+
listConnections() {
|
|
551
|
+
return [...this.studioClients.values()].filter((c) => c.ws.readyState === WebSocket.OPEN).map((c) => ({
|
|
552
|
+
placeId: c.placeId ?? 0,
|
|
553
|
+
connectionId: c.connectionId,
|
|
554
|
+
placeName: c.placeName,
|
|
555
|
+
studioVersion: c.studioVersion,
|
|
556
|
+
connectedAt: c.connectedAt,
|
|
557
|
+
active: this.activePlaceId === 0 && this.studioClients.size === 1 || c.placeId === this.activePlaceId
|
|
558
|
+
}));
|
|
559
|
+
}
|
|
560
|
+
getActivePlaceId() {
|
|
561
|
+
return this.activePlaceId;
|
|
562
|
+
}
|
|
563
|
+
setActivePlaceId(placeId) {
|
|
564
|
+
if (placeId !== 0 && !this.studioClients.has(placeId)) {
|
|
565
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
566
|
+
}
|
|
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
|
+
});
|
|
441
575
|
}
|
|
442
576
|
getPort() {
|
|
443
577
|
return this.port;
|
|
444
578
|
}
|
|
445
579
|
stop() {
|
|
446
580
|
return new Promise((resolve) => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
this.studioClient = null;
|
|
581
|
+
for (const conn of this.studioClients.values()) {
|
|
582
|
+
conn.ws.close();
|
|
450
583
|
}
|
|
584
|
+
this.studioClients.clear();
|
|
585
|
+
this.wsToPlaceId.clear();
|
|
451
586
|
for (const ws of this.controllers) {
|
|
452
587
|
ws.close();
|
|
453
588
|
}
|
|
@@ -468,6 +603,15 @@ var init_ws_server = __esm({
|
|
|
468
603
|
};
|
|
469
604
|
}
|
|
470
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
|
+
}
|
|
471
615
|
async function isPortInUse(port) {
|
|
472
616
|
return new Promise((resolve) => {
|
|
473
617
|
const ws = new WebSocket(`ws://localhost:${port}`);
|
|
@@ -494,7 +638,8 @@ var init_ws_client = __esm({
|
|
|
494
638
|
ws = null;
|
|
495
639
|
port;
|
|
496
640
|
studioConnected = false;
|
|
497
|
-
|
|
641
|
+
connections = /* @__PURE__ */ new Map();
|
|
642
|
+
activePlaceId = 0;
|
|
498
643
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
499
644
|
constructor(port = 18088) {
|
|
500
645
|
super();
|
|
@@ -538,27 +683,52 @@ var init_ws_client = __esm({
|
|
|
538
683
|
if (msg.type === "controller:welcome") {
|
|
539
684
|
const p = msg.payload;
|
|
540
685
|
this.studioConnected = p.studioConnected;
|
|
686
|
+
if (p.connections) {
|
|
687
|
+
this.connections.clear();
|
|
688
|
+
for (const c of p.connections) {
|
|
689
|
+
this.connections.set(c.placeId, toConnectionInfo(c));
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
if (p.activePlaceId !== void 0) {
|
|
693
|
+
this.activePlaceId = p.activePlaceId;
|
|
694
|
+
}
|
|
541
695
|
if (this.studioConnected) {
|
|
542
696
|
this.emit("studio:connected", {});
|
|
543
697
|
}
|
|
544
698
|
return;
|
|
545
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
|
+
}
|
|
546
706
|
if (msg.type === "studio:connected") {
|
|
547
707
|
this.studioConnected = true;
|
|
548
708
|
const payload = msg.payload;
|
|
549
|
-
|
|
709
|
+
const placeId = payload.placeId ?? 0;
|
|
710
|
+
this.connections.set(placeId, {
|
|
550
711
|
connectedAt: Date.now(),
|
|
712
|
+
connectionId: payload.connectionId ?? `${placeId}`,
|
|
551
713
|
studioVersion: payload.studioVersion,
|
|
552
|
-
placeId
|
|
714
|
+
placeId,
|
|
553
715
|
placeName: payload.placeName
|
|
554
|
-
};
|
|
716
|
+
});
|
|
555
717
|
this.emit("studio:connected", payload);
|
|
556
718
|
return;
|
|
557
719
|
}
|
|
558
720
|
if (msg.type === "studio:disconnected") {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
this.
|
|
721
|
+
const payload = msg.payload;
|
|
722
|
+
const placeId = payload.placeId ?? 0;
|
|
723
|
+
this.connections.delete(placeId);
|
|
724
|
+
if (this.connections.size === 0) {
|
|
725
|
+
this.studioConnected = false;
|
|
726
|
+
}
|
|
727
|
+
if (this.activePlaceId === placeId) {
|
|
728
|
+
this.activePlaceId = 0;
|
|
729
|
+
}
|
|
730
|
+
this.studioConnected = this.resolveTargetInfo() !== null;
|
|
731
|
+
this.emit("studio:disconnected", payload);
|
|
562
732
|
return;
|
|
563
733
|
}
|
|
564
734
|
if (msg.type === StudioMsg.RESPONSE) {
|
|
@@ -602,11 +772,57 @@ var init_ws_client = __esm({
|
|
|
602
772
|
this.ws.send(serializeMessage(msg));
|
|
603
773
|
}
|
|
604
774
|
isConnected() {
|
|
605
|
-
return this.
|
|
775
|
+
return this.resolveTargetInfo() !== null;
|
|
606
776
|
}
|
|
607
777
|
getConnectionInfo() {
|
|
608
|
-
|
|
609
|
-
|
|
778
|
+
const target = this.resolveTargetInfo();
|
|
779
|
+
if (!target) return null;
|
|
780
|
+
return { ...target, ws: this.ws };
|
|
781
|
+
}
|
|
782
|
+
listConnections() {
|
|
783
|
+
return [...this.connections.values()].map((c) => ({
|
|
784
|
+
placeId: c.placeId ?? 0,
|
|
785
|
+
connectionId: c.connectionId,
|
|
786
|
+
placeName: c.placeName,
|
|
787
|
+
studioVersion: c.studioVersion,
|
|
788
|
+
connectedAt: c.connectedAt,
|
|
789
|
+
active: this.activePlaceId === 0 && this.connections.size === 1 || c.placeId === this.activePlaceId
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
getActivePlaceId() {
|
|
793
|
+
return this.activePlaceId;
|
|
794
|
+
}
|
|
795
|
+
setActivePlaceId(placeId) {
|
|
796
|
+
if (placeId !== 0 && !this.connections.has(placeId)) {
|
|
797
|
+
throw new Error(`Place ${placeId} is not connected`);
|
|
798
|
+
}
|
|
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;
|
|
610
826
|
}
|
|
611
827
|
getPort() {
|
|
612
828
|
return this.port;
|
|
@@ -628,6 +844,148 @@ var init_ws_client = __esm({
|
|
|
628
844
|
};
|
|
629
845
|
}
|
|
630
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
|
+
});
|
|
631
989
|
|
|
632
990
|
// src/agent/internal-memory.ts
|
|
633
991
|
var INTERNAL_MEMORY;
|
|
@@ -683,6 +1041,7 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
683
1041
|
- **Inspecting user's selection \u2192 \`get_selection\`, then \`get_properties\` or \`serialize_ui\`**
|
|
684
1042
|
- **Reading properties \u2192 \`get_properties\` (NEVER \`run_code\` with dump scripts)**
|
|
685
1043
|
- **Browsing the tree \u2192 \`get_explorer\`**
|
|
1044
|
+
- **Multiple Studios open \u2192 \`list_connections\` to see all, \`set_active_place\` to target one**
|
|
686
1045
|
- Anything exotic or procedural \u2192 \`run_code\` as last resort
|
|
687
1046
|
|
|
688
1047
|
### NEVER use run_code for:
|
|
@@ -692,6 +1051,8 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
692
1051
|
- Converting UI to Roact/React \u2192 use \`convert_ui_to_code\`
|
|
693
1052
|
- Creating instances \u2192 use \`create_part\`, \`create_ui\`, \`insert_instance\`, \`build_multiple\`
|
|
694
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)
|
|
695
1056
|
- Reading scripts \u2192 use \`read_script\`
|
|
696
1057
|
- Browsing the explorer \u2192 use \`get_explorer\`
|
|
697
1058
|
\`run_code\` is ONLY for procedural logic, terrain generation, raycasts, physics queries, or custom algorithms with no tool equivalent.
|
|
@@ -701,6 +1062,19 @@ These are built-in lessons that ship with Dominus. Follow them exactly.
|
|
|
701
1062
|
- \`inspect_instance\` \u2014 Returns compact properties + children tree for a single instance in ONE call. Combines get_properties + get_explorer.
|
|
702
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".
|
|
703
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
|
+
|
|
704
1078
|
### Verification habits
|
|
705
1079
|
- After a build, call \`get_explorer\` or \`get_selection\` to confirm the tree looks right.
|
|
706
1080
|
- After editing a script, read it back.
|
|
@@ -747,14 +1121,14 @@ __export(store_exports, {
|
|
|
747
1121
|
function persist() {
|
|
748
1122
|
if (!db) return;
|
|
749
1123
|
const data = db.export();
|
|
750
|
-
|
|
1124
|
+
fs6.writeFileSync(dbPath, Buffer.from(data));
|
|
751
1125
|
}
|
|
752
1126
|
async function initMemoryStore() {
|
|
753
1127
|
if (db) return;
|
|
754
1128
|
dbPath = getDbPath();
|
|
755
1129
|
const SQL = await initSqlJs();
|
|
756
|
-
if (
|
|
757
|
-
const buffer =
|
|
1130
|
+
if (fs6.existsSync(dbPath)) {
|
|
1131
|
+
const buffer = fs6.readFileSync(dbPath);
|
|
758
1132
|
db = new SQL.Database(buffer);
|
|
759
1133
|
} else {
|
|
760
1134
|
db = new SQL.Database();
|
|
@@ -1019,131 +1393,980 @@ CREATE INDEX IF NOT EXISTS idx_summaries_project ON summaries(project_id);
|
|
|
1019
1393
|
}
|
|
1020
1394
|
});
|
|
1021
1395
|
|
|
1022
|
-
// src/
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
path: response.payload.path,
|
|
1057
|
-
className: response.payload.className,
|
|
1058
|
-
source: response.payload.source,
|
|
1059
|
-
lineCount: response.payload.lineCount
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
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;
|
|
1062
1430
|
}
|
|
1063
|
-
|
|
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"() {
|
|
1064
1452
|
}
|
|
1065
1453
|
});
|
|
1066
1454
|
|
|
1067
|
-
// src/
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
required: ["path", "source"]
|
|
1092
|
-
},
|
|
1093
|
-
async execute(params, ctx) {
|
|
1094
|
-
if (!ctx.isStudioConnected()) {
|
|
1095
|
-
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
|
|
1096
1479
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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)
|
|
1103
1499
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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 [];
|
|
1107
1508
|
}
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
description: "Luau code to execute in Studio"
|
|
1128
|
-
}
|
|
1129
|
-
},
|
|
1130
|
-
required: ["code"]
|
|
1131
|
-
},
|
|
1132
|
-
async execute(params, ctx) {
|
|
1133
|
-
if (!ctx.isStudioConnected()) {
|
|
1134
|
-
return { success: false, error: "Studio is not connected" };
|
|
1135
|
-
}
|
|
1136
|
-
const code = params.code;
|
|
1137
|
-
const response = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
1138
|
-
const payload = response.payload;
|
|
1139
|
-
if (payload.error) {
|
|
1140
|
-
return { success: false, error: payload.error, data: { output: payload.output } };
|
|
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)
|
|
1141
1528
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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}`);
|
|
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)}`
|
|
1621
|
+
};
|
|
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
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
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: {
|
|
2348
|
+
code: {
|
|
2349
|
+
type: "string",
|
|
2350
|
+
description: "Luau code to execute in Studio"
|
|
2351
|
+
}
|
|
2352
|
+
},
|
|
2353
|
+
required: ["code"]
|
|
2354
|
+
},
|
|
2355
|
+
async execute(params, ctx) {
|
|
2356
|
+
if (!ctx.isStudioConnected()) {
|
|
2357
|
+
return { success: false, error: "Studio is not connected" };
|
|
2358
|
+
}
|
|
2359
|
+
const code = params.code;
|
|
2360
|
+
const response = await ctx.sendToStudio(CliMsg.RUN_CODE, { code });
|
|
2361
|
+
const payload = response.payload;
|
|
2362
|
+
if (payload.error) {
|
|
2363
|
+
return { success: false, error: payload.error, data: { output: payload.output } };
|
|
2364
|
+
}
|
|
2365
|
+
return {
|
|
2366
|
+
success: true,
|
|
2367
|
+
data: { output: payload.output, duration: payload.duration }
|
|
2368
|
+
};
|
|
2369
|
+
}
|
|
1147
2370
|
};
|
|
1148
2371
|
}
|
|
1149
2372
|
});
|
|
@@ -1271,10 +2494,10 @@ var init_get_descendants_properties = __esm({
|
|
|
1271
2494
|
// 5 minute timeout since getting ALL descendants can take a very long time
|
|
1272
2495
|
);
|
|
1273
2496
|
if (params.outputFile && typeof params.outputFile === "string") {
|
|
1274
|
-
const
|
|
1275
|
-
const
|
|
1276
|
-
const outputPath =
|
|
1277
|
-
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");
|
|
1278
2501
|
return { success: true, data: `Successfully saved descendants to ${outputPath}` };
|
|
1279
2502
|
}
|
|
1280
2503
|
return { success: true, data: response.payload };
|
|
@@ -1851,8 +3074,8 @@ __export(clone_instance_exports, {
|
|
|
1851
3074
|
tool: () => tool19
|
|
1852
3075
|
});
|
|
1853
3076
|
function buildCloneCode(params) {
|
|
1854
|
-
const
|
|
1855
|
-
const parts =
|
|
3077
|
+
const path6 = params.path;
|
|
3078
|
+
const parts = path6.split(".");
|
|
1856
3079
|
let resolve = "game";
|
|
1857
3080
|
for (const p of parts) {
|
|
1858
3081
|
resolve += `["${p}"]`;
|
|
@@ -1993,8 +3216,8 @@ __export(build_multiple_exports, {
|
|
|
1993
3216
|
});
|
|
1994
3217
|
function generateBatchCode(parts, groupName, groupParent) {
|
|
1995
3218
|
const lines = [];
|
|
1996
|
-
function resolveParent(
|
|
1997
|
-
const segs =
|
|
3219
|
+
function resolveParent(path6) {
|
|
3220
|
+
const segs = path6.split(".");
|
|
1998
3221
|
let r = "game";
|
|
1999
3222
|
for (const s of segs) r += `["${s}"]`;
|
|
2000
3223
|
return r;
|
|
@@ -2220,7 +3443,7 @@ var init_bulk_set_properties = __esm({
|
|
|
2220
3443
|
init_protocol();
|
|
2221
3444
|
tool24 = {
|
|
2222
3445
|
name: "bulk_set_properties",
|
|
2223
|
-
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}]',
|
|
2224
3447
|
parameters: {
|
|
2225
3448
|
type: "object",
|
|
2226
3449
|
properties: {
|
|
@@ -2243,6 +3466,14 @@ var init_bulk_set_properties = __esm({
|
|
|
2243
3466
|
properties: {
|
|
2244
3467
|
type: "object",
|
|
2245
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
|
+
}
|
|
2246
3477
|
}
|
|
2247
3478
|
}
|
|
2248
3479
|
},
|
|
@@ -2254,7 +3485,8 @@ var init_bulk_set_properties = __esm({
|
|
|
2254
3485
|
targets: params.targets,
|
|
2255
3486
|
root: params.root,
|
|
2256
3487
|
className: params.className,
|
|
2257
|
-
properties: params.properties
|
|
3488
|
+
properties: params.properties,
|
|
3489
|
+
addChildren: params.addChildren
|
|
2258
3490
|
}, 1e3 * 60);
|
|
2259
3491
|
const payload = response.payload;
|
|
2260
3492
|
if (!payload.success) {
|
|
@@ -2287,53 +3519,571 @@ var init_find_replace_scripts = __esm({
|
|
|
2287
3519
|
parameters: {
|
|
2288
3520
|
type: "object",
|
|
2289
3521
|
properties: {
|
|
2290
|
-
find: {
|
|
2291
|
-
type: "string",
|
|
2292
|
-
description: "Text or Lua pattern to search for"
|
|
2293
|
-
},
|
|
2294
|
-
replace: {
|
|
3522
|
+
find: {
|
|
3523
|
+
type: "string",
|
|
3524
|
+
description: "Text or Lua pattern to search for"
|
|
3525
|
+
},
|
|
3526
|
+
replace: {
|
|
3527
|
+
type: "string",
|
|
3528
|
+
description: "Replacement text (supports Lua pattern captures like %1, %2)"
|
|
3529
|
+
},
|
|
3530
|
+
root: {
|
|
3531
|
+
type: "string",
|
|
3532
|
+
description: "Root path to limit search scope (optional, searches entire game if omitted)"
|
|
3533
|
+
},
|
|
3534
|
+
usePattern: {
|
|
3535
|
+
type: "boolean",
|
|
3536
|
+
description: "If true, treat find/replace as Lua patterns. Default: false (plain text)"
|
|
3537
|
+
},
|
|
3538
|
+
dryRun: {
|
|
3539
|
+
type: "boolean",
|
|
3540
|
+
description: "If true, report matches but do not modify scripts. Default: false"
|
|
3541
|
+
}
|
|
3542
|
+
},
|
|
3543
|
+
required: ["find", "replace"]
|
|
3544
|
+
},
|
|
3545
|
+
async execute(params, ctx) {
|
|
3546
|
+
if (!ctx.isStudioConnected()) {
|
|
3547
|
+
return { success: false, error: "Studio is not connected" };
|
|
3548
|
+
}
|
|
3549
|
+
const response = await ctx.sendToStudio(CliMsg.FIND_REPLACE_SCRIPTS, {
|
|
3550
|
+
find: params.find,
|
|
3551
|
+
replace: params.replace,
|
|
3552
|
+
root: params.root,
|
|
3553
|
+
usePattern: params.usePattern ?? false,
|
|
3554
|
+
dryRun: params.dryRun ?? false
|
|
3555
|
+
}, 1e3 * 60 * 5);
|
|
3556
|
+
const payload = response.payload;
|
|
3557
|
+
if (!payload.success) {
|
|
3558
|
+
return { success: false, error: payload.error ?? "Find/replace failed" };
|
|
3559
|
+
}
|
|
3560
|
+
return {
|
|
3561
|
+
success: true,
|
|
3562
|
+
data: {
|
|
3563
|
+
totalFiles: payload.totalFiles,
|
|
3564
|
+
totalMatches: payload.totalMatches,
|
|
3565
|
+
modified: payload.modified,
|
|
3566
|
+
dryRun: params.dryRun ?? false
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
}
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
});
|
|
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: {
|
|
2295
4060
|
type: "string",
|
|
2296
|
-
description:
|
|
4061
|
+
description: 'API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'
|
|
2297
4062
|
},
|
|
2298
|
-
|
|
4063
|
+
kind: {
|
|
2299
4064
|
type: "string",
|
|
2300
|
-
description:
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
type: "boolean",
|
|
2304
|
-
description: "If true, treat find/replace as Lua patterns. Default: false (plain text)"
|
|
4065
|
+
description: 'Optional reference kind. Use "any" to search all reference kinds.',
|
|
4066
|
+
enum: ["any", "class", "datatype", "enum", "global", "library"],
|
|
4067
|
+
default: "any"
|
|
2305
4068
|
},
|
|
2306
|
-
|
|
4069
|
+
includeInherited: {
|
|
2307
4070
|
type: "boolean",
|
|
2308
|
-
description: "
|
|
4071
|
+
description: "For classes, include members from inherited base classes (default: true).",
|
|
4072
|
+
default: true
|
|
2309
4073
|
}
|
|
2310
4074
|
},
|
|
2311
|
-
required: ["
|
|
4075
|
+
required: ["name"]
|
|
2312
4076
|
},
|
|
2313
|
-
async execute(params
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
dryRun: params.dryRun ?? false
|
|
2323
|
-
}, 1e3 * 60 * 5);
|
|
2324
|
-
const payload = response.payload;
|
|
2325
|
-
if (!payload.success) {
|
|
2326
|
-
return { success: false, error: payload.error ?? "Find/replace failed" };
|
|
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) };
|
|
2327
4086
|
}
|
|
2328
|
-
return {
|
|
2329
|
-
success: true,
|
|
2330
|
-
data: {
|
|
2331
|
-
totalFiles: payload.totalFiles,
|
|
2332
|
-
totalMatches: payload.totalMatches,
|
|
2333
|
-
modified: payload.modified,
|
|
2334
|
-
dryRun: params.dryRun ?? false
|
|
2335
|
-
}
|
|
2336
|
-
};
|
|
2337
4087
|
}
|
|
2338
4088
|
};
|
|
2339
4089
|
}
|
|
@@ -2342,13 +4092,13 @@ var init_find_replace_scripts = __esm({
|
|
|
2342
4092
|
// src/tools/memory/recall.ts
|
|
2343
4093
|
var recall_exports = {};
|
|
2344
4094
|
__export(recall_exports, {
|
|
2345
|
-
tool: () =>
|
|
4095
|
+
tool: () => tool28
|
|
2346
4096
|
});
|
|
2347
|
-
var
|
|
4097
|
+
var tool28;
|
|
2348
4098
|
var init_recall = __esm({
|
|
2349
4099
|
"src/tools/memory/recall.ts"() {
|
|
2350
4100
|
init_store();
|
|
2351
|
-
|
|
4101
|
+
tool28 = {
|
|
2352
4102
|
name: "recall_memory",
|
|
2353
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.",
|
|
2354
4104
|
parameters: {
|
|
@@ -2387,13 +4137,13 @@ var init_recall = __esm({
|
|
|
2387
4137
|
// src/tools/memory/remember.ts
|
|
2388
4138
|
var remember_exports = {};
|
|
2389
4139
|
__export(remember_exports, {
|
|
2390
|
-
tool: () =>
|
|
4140
|
+
tool: () => tool29
|
|
2391
4141
|
});
|
|
2392
|
-
var
|
|
4142
|
+
var tool29;
|
|
2393
4143
|
var init_remember = __esm({
|
|
2394
4144
|
"src/tools/memory/remember.ts"() {
|
|
2395
4145
|
init_store();
|
|
2396
|
-
|
|
4146
|
+
tool29 = {
|
|
2397
4147
|
name: "remember",
|
|
2398
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.",
|
|
2399
4149
|
parameters: {
|
|
@@ -2433,12 +4183,12 @@ var init_remember = __esm({
|
|
|
2433
4183
|
// src/tools/cloud/upload-asset.ts
|
|
2434
4184
|
var upload_asset_exports = {};
|
|
2435
4185
|
__export(upload_asset_exports, {
|
|
2436
|
-
tool: () =>
|
|
4186
|
+
tool: () => tool30
|
|
2437
4187
|
});
|
|
2438
|
-
var
|
|
4188
|
+
var tool30;
|
|
2439
4189
|
var init_upload_asset = __esm({
|
|
2440
4190
|
"src/tools/cloud/upload-asset.ts"() {
|
|
2441
|
-
|
|
4191
|
+
tool30 = {
|
|
2442
4192
|
name: "upload_asset",
|
|
2443
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.",
|
|
2444
4194
|
parameters: {
|
|
@@ -2476,11 +4226,11 @@ var init_upload_asset = __esm({
|
|
|
2476
4226
|
const assetType = params.assetType;
|
|
2477
4227
|
const name = params.name;
|
|
2478
4228
|
const description = params.description ?? "";
|
|
2479
|
-
if (!
|
|
4229
|
+
if (!fs6.existsSync(filePath)) {
|
|
2480
4230
|
return { success: false, error: `File not found: ${filePath}` };
|
|
2481
4231
|
}
|
|
2482
|
-
const fileBuffer =
|
|
2483
|
-
const fileName =
|
|
4232
|
+
const fileBuffer = fs6.readFileSync(filePath);
|
|
4233
|
+
const fileName = path5.basename(filePath);
|
|
2484
4234
|
const typeMapping = {
|
|
2485
4235
|
Image: "Decal",
|
|
2486
4236
|
Audio: "Audio",
|
|
@@ -2585,6 +4335,99 @@ var init_undo_redo = __esm({
|
|
|
2585
4335
|
}
|
|
2586
4336
|
});
|
|
2587
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
|
+
|
|
2588
4431
|
// src/mcp/server.ts
|
|
2589
4432
|
var server_exports = {};
|
|
2590
4433
|
__export(server_exports, {
|
|
@@ -2605,7 +4448,7 @@ async function startMcpServer() {
|
|
|
2605
4448
|
}
|
|
2606
4449
|
const mcp = new McpServer({
|
|
2607
4450
|
name: "dominus",
|
|
2608
|
-
version: "0.
|
|
4451
|
+
version: "2.0.0"
|
|
2609
4452
|
}, {
|
|
2610
4453
|
instructions: INTERNAL_MEMORY
|
|
2611
4454
|
});
|
|
@@ -2717,9 +4560,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2717
4560
|
"read_script",
|
|
2718
4561
|
"Read the source code of a script in Roblox Studio",
|
|
2719
4562
|
{ path: z.string().describe('Full instance path, e.g. "ServerScriptService.GameManager"') },
|
|
2720
|
-
async ({ path:
|
|
4563
|
+
async ({ path: path6 }) => {
|
|
2721
4564
|
assertConnected();
|
|
2722
|
-
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path:
|
|
4565
|
+
const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path6 });
|
|
2723
4566
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2724
4567
|
}
|
|
2725
4568
|
);
|
|
@@ -2730,9 +4573,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2730
4573
|
path: z.string().describe("Full instance path"),
|
|
2731
4574
|
source: z.string().describe("New complete source code")
|
|
2732
4575
|
},
|
|
2733
|
-
async ({ path:
|
|
4576
|
+
async ({ path: path6, source }) => {
|
|
2734
4577
|
assertConnected();
|
|
2735
|
-
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path:
|
|
4578
|
+
const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path6, source });
|
|
2736
4579
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2737
4580
|
}
|
|
2738
4581
|
);
|
|
@@ -2769,9 +4612,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2769
4612
|
path: z.string().describe('Full instance path (e.g. "Workspace.Part", "StarterGui.MyUI.Frame")'),
|
|
2770
4613
|
compact: z.boolean().optional().default(true).describe("Strip read-only, nil, deprecated, and default-valued properties (default true). Set false for full dump.")
|
|
2771
4614
|
},
|
|
2772
|
-
async ({ path:
|
|
4615
|
+
async ({ path: path6, compact }) => {
|
|
2773
4616
|
assertConnected();
|
|
2774
|
-
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path:
|
|
4617
|
+
const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path6, compact });
|
|
2775
4618
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2776
4619
|
}
|
|
2777
4620
|
);
|
|
@@ -2783,9 +4626,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2783
4626
|
compact: z.boolean().optional().default(true).describe("Strip noisy/default properties (default true). Set false for full dump."),
|
|
2784
4627
|
outputFile: z.string().optional().describe("Optional file path to write to prevent limit errors/timeouts")
|
|
2785
4628
|
},
|
|
2786
|
-
async ({ path:
|
|
4629
|
+
async ({ path: path6, compact, outputFile }) => {
|
|
2787
4630
|
assertConnected();
|
|
2788
|
-
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);
|
|
2789
4632
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2790
4633
|
}
|
|
2791
4634
|
);
|
|
@@ -2796,9 +4639,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2796
4639
|
path: z.string().describe("Full instance path"),
|
|
2797
4640
|
properties: z.record(z.unknown()).describe("Key-value pairs of properties to set")
|
|
2798
4641
|
},
|
|
2799
|
-
async ({ path:
|
|
4642
|
+
async ({ path: path6, properties }) => {
|
|
2800
4643
|
assertConnected();
|
|
2801
|
-
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path:
|
|
4644
|
+
const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path6, properties });
|
|
2802
4645
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2803
4646
|
}
|
|
2804
4647
|
);
|
|
@@ -2826,9 +4669,9 @@ ${" ".repeat(_depth)}}`;
|
|
|
2826
4669
|
"delete_instance",
|
|
2827
4670
|
"Delete an instance from the Roblox Studio DataModel",
|
|
2828
4671
|
{ path: z.string().describe("Full instance path to delete") },
|
|
2829
|
-
async ({ path:
|
|
4672
|
+
async ({ path: path6 }) => {
|
|
2830
4673
|
assertConnected();
|
|
2831
|
-
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path:
|
|
4674
|
+
const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path6 });
|
|
2832
4675
|
return { content: [{ type: "text", text: JSON.stringify(res.payload) }] };
|
|
2833
4676
|
}
|
|
2834
4677
|
);
|
|
@@ -2881,6 +4724,61 @@ ${" ".repeat(_depth)}}`;
|
|
|
2881
4724
|
return { content: [{ type: "text", text: JSON.stringify(res.payload, null, 2) }] };
|
|
2882
4725
|
}
|
|
2883
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
|
+
);
|
|
2884
4782
|
mcp.tool(
|
|
2885
4783
|
"create_ui",
|
|
2886
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.',
|
|
@@ -3096,11 +4994,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
3096
4994
|
path: z.string().describe('Instance path to serialize (e.g. "StarterGui.MyScreenGui")'),
|
|
3097
4995
|
maxDepth: z.number().optional().default(50).describe("Maximum tree depth (default 50)")
|
|
3098
4996
|
},
|
|
3099
|
-
async ({ path:
|
|
4997
|
+
async ({ path: path6, maxDepth }) => {
|
|
3100
4998
|
assertConnected();
|
|
3101
4999
|
const res = await bridge.sendRequest(
|
|
3102
5000
|
CliMsg.SERIALIZE_UI,
|
|
3103
|
-
{ path:
|
|
5001
|
+
{ path: path6, maxDepth },
|
|
3104
5002
|
1e3 * 60 * 2
|
|
3105
5003
|
);
|
|
3106
5004
|
if (!res.payload.success) {
|
|
@@ -3145,16 +5043,16 @@ ${" ".repeat(_depth)}}`;
|
|
|
3145
5043
|
path: z.string().describe('Full instance path (e.g. "Workspace.MyModel")'),
|
|
3146
5044
|
childDepth: z.number().optional().default(2).describe("How deep to show the children tree (default 2)")
|
|
3147
5045
|
},
|
|
3148
|
-
async ({ path:
|
|
5046
|
+
async ({ path: path6, childDepth }) => {
|
|
3149
5047
|
assertConnected();
|
|
3150
5048
|
const [propRes, treeRes] = await Promise.all([
|
|
3151
5049
|
bridge.sendRequest(
|
|
3152
5050
|
CliMsg.GET_PROPERTIES,
|
|
3153
|
-
{ path:
|
|
5051
|
+
{ path: path6, compact: true }
|
|
3154
5052
|
),
|
|
3155
5053
|
bridge.sendRequest(
|
|
3156
5054
|
CliMsg.GET_EXPLORER,
|
|
3157
|
-
{ rootPath:
|
|
5055
|
+
{ rootPath: path6, maxDepth: childDepth ?? 2 }
|
|
3158
5056
|
)
|
|
3159
5057
|
]);
|
|
3160
5058
|
return {
|
|
@@ -3176,11 +5074,11 @@ ${" ".repeat(_depth)}}`;
|
|
|
3176
5074
|
framework: z.enum(["roact", "react"]).optional().default("react").describe('Target framework: "roact" (legacy) or "react" (react-lua, default)'),
|
|
3177
5075
|
componentName: z.string().optional().describe("Name for the generated component (defaults to instance Name)")
|
|
3178
5076
|
},
|
|
3179
|
-
async ({ path:
|
|
5077
|
+
async ({ path: path6, framework, componentName }) => {
|
|
3180
5078
|
assertConnected();
|
|
3181
5079
|
const res = await bridge.sendRequest(
|
|
3182
5080
|
CliMsg.SERIALIZE_UI,
|
|
3183
|
-
{ path:
|
|
5081
|
+
{ path: path6, maxDepth: 50 },
|
|
3184
5082
|
1e3 * 60 * 2
|
|
3185
5083
|
);
|
|
3186
5084
|
if (!res.payload.success || !res.payload.tree) {
|
|
@@ -3193,6 +5091,66 @@ ${" ".repeat(_depth)}}`;
|
|
|
3193
5091
|
return { content: [{ type: "text", text: code }] };
|
|
3194
5092
|
}
|
|
3195
5093
|
);
|
|
5094
|
+
mcp.tool(
|
|
5095
|
+
"list_connections",
|
|
5096
|
+
"List all Roblox Studio instances currently connected to Dominus. Shows placeId, placeName, and connection time for each. Use this when working with multiple Studio windows.",
|
|
5097
|
+
{},
|
|
5098
|
+
async () => {
|
|
5099
|
+
const connections = bridge.listConnections();
|
|
5100
|
+
const activePlaceId = bridge.getActivePlaceId();
|
|
5101
|
+
return {
|
|
5102
|
+
content: [{
|
|
5103
|
+
type: "text",
|
|
5104
|
+
text: JSON.stringify({
|
|
5105
|
+
activePlaceId: activePlaceId || "auto",
|
|
5106
|
+
connections: connections.map((c) => ({
|
|
5107
|
+
...c,
|
|
5108
|
+
active: activePlaceId === 0 && connections.length === 1 ? true : c.placeId === activePlaceId
|
|
5109
|
+
}))
|
|
5110
|
+
}, null, 2)
|
|
5111
|
+
}]
|
|
5112
|
+
};
|
|
5113
|
+
}
|
|
5114
|
+
);
|
|
5115
|
+
mcp.tool(
|
|
5116
|
+
"set_active_place",
|
|
5117
|
+
"Set which Roblox Studio instance to target when multiple are connected. Pass placeId=0 to reset to auto mode (works when only one is connected). Call list_connections first to see available placeIds.",
|
|
5118
|
+
{
|
|
5119
|
+
placeId: z.number().describe("The placeId to target. Use 0 to reset to auto-select.")
|
|
5120
|
+
},
|
|
5121
|
+
async ({ placeId }) => {
|
|
5122
|
+
const connections = bridge.listConnections();
|
|
5123
|
+
if (placeId !== 0 && !connections.some((c) => c.placeId === placeId)) {
|
|
5124
|
+
const available = connections.map((c) => `${c.placeName ?? "Unknown"} (${c.placeId})`).join(", ");
|
|
5125
|
+
return {
|
|
5126
|
+
content: [{
|
|
5127
|
+
type: "text",
|
|
5128
|
+
text: `Place ${placeId} is not connected. Available: ${available || "none"}`
|
|
5129
|
+
}]
|
|
5130
|
+
};
|
|
5131
|
+
}
|
|
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
|
+
}
|
|
5142
|
+
if (placeId === 0) {
|
|
5143
|
+
return { content: [{ type: "text", text: "Active place reset to auto-select." }] };
|
|
5144
|
+
}
|
|
5145
|
+
const target = connections.find((c) => c.placeId === placeId);
|
|
5146
|
+
return {
|
|
5147
|
+
content: [{
|
|
5148
|
+
type: "text",
|
|
5149
|
+
text: `Active place set to ${target?.placeName ?? "Unknown"} (${placeId})`
|
|
5150
|
+
}]
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
);
|
|
3196
5154
|
mcp.tool(
|
|
3197
5155
|
"recall_memory",
|
|
3198
5156
|
"Search persistent memory for facts about the current Roblox project",
|
|
@@ -3246,11 +5204,50 @@ ${" ".repeat(_depth)}}`;
|
|
|
3246
5204
|
}
|
|
3247
5205
|
function assertConnected() {
|
|
3248
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
|
+
}
|
|
3249
5216
|
throw new Error(
|
|
3250
5217
|
"Roblox Studio is not connected. Open Studio and click the Dominus plugin Connect button."
|
|
3251
5218
|
);
|
|
3252
5219
|
}
|
|
3253
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
|
+
}
|
|
3254
5251
|
var bridge;
|
|
3255
5252
|
var init_server = __esm({
|
|
3256
5253
|
"src/mcp/server.ts"() {
|
|
@@ -3260,6 +5257,10 @@ var init_server = __esm({
|
|
|
3260
5257
|
init_store();
|
|
3261
5258
|
init_config();
|
|
3262
5259
|
init_internal_memory();
|
|
5260
|
+
init_roblox_api();
|
|
5261
|
+
init_provider();
|
|
5262
|
+
init_coordinator();
|
|
5263
|
+
init_registry();
|
|
3263
5264
|
}
|
|
3264
5265
|
});
|
|
3265
5266
|
var CROWN_ART = ` \u25C6 \u25C6 \u25C6
|
|
@@ -3487,11 +5488,20 @@ var Setup = ({ onComplete }) => {
|
|
|
3487
5488
|
var StatusBar = React6.memo(({
|
|
3488
5489
|
connected,
|
|
3489
5490
|
placeName,
|
|
5491
|
+
connectionCount,
|
|
3490
5492
|
model,
|
|
3491
5493
|
sessionTokens
|
|
3492
5494
|
}) => {
|
|
3493
5495
|
const statusColor = connected ? "green" : "red";
|
|
3494
|
-
const
|
|
5496
|
+
const count = connectionCount ?? (connected ? 1 : 0);
|
|
5497
|
+
let statusText;
|
|
5498
|
+
if (!connected) {
|
|
5499
|
+
statusText = "\u25CB Disconnected";
|
|
5500
|
+
} else if (count > 1) {
|
|
5501
|
+
statusText = `\u25CF ${count} Studios${placeName ? ` \u2014 active: ${placeName}` : ""}`;
|
|
5502
|
+
} else {
|
|
5503
|
+
statusText = `\u25CF Connected${placeName ? ` \u2014 ${placeName}` : ""}`;
|
|
5504
|
+
}
|
|
3495
5505
|
const shortModel = model.split("/").pop() ?? model;
|
|
3496
5506
|
return /* @__PURE__ */ jsxs(
|
|
3497
5507
|
Box,
|
|
@@ -3909,6 +5919,7 @@ var ToolCallView = React6.memo(({ calls }) => {
|
|
|
3909
5919
|
var Layout = React6.memo(({
|
|
3910
5920
|
connected,
|
|
3911
5921
|
placeName,
|
|
5922
|
+
connectionCount,
|
|
3912
5923
|
model,
|
|
3913
5924
|
chatEntries,
|
|
3914
5925
|
outputEntries,
|
|
@@ -3922,7 +5933,7 @@ var Layout = React6.memo(({
|
|
|
3922
5933
|
onClearImage
|
|
3923
5934
|
}) => {
|
|
3924
5935
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: "100%", children: [
|
|
3925
|
-
/* @__PURE__ */ jsx(StatusBar, { connected, placeName, model }),
|
|
5936
|
+
/* @__PURE__ */ jsx(StatusBar, { connected, placeName, connectionCount, model }),
|
|
3926
5937
|
/* @__PURE__ */ jsxs(Box, { flexGrow: 1, children: [
|
|
3927
5938
|
/* @__PURE__ */ jsx(ExplorerPane, { tree: explorerTree }),
|
|
3928
5939
|
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", flexGrow: 1, children: [
|
|
@@ -3948,10 +5959,12 @@ var Layout = React6.memo(({
|
|
|
3948
5959
|
init_protocol();
|
|
3949
5960
|
function useStudio(server) {
|
|
3950
5961
|
const connInfo = server.getConnectionInfo();
|
|
5962
|
+
const connections = server.listConnections();
|
|
3951
5963
|
const [state, setState] = useState({
|
|
3952
5964
|
connected: server.isConnected(),
|
|
3953
5965
|
placeName: connInfo?.placeName,
|
|
3954
5966
|
placeId: connInfo?.placeId,
|
|
5967
|
+
connectionCount: connections.length,
|
|
3955
5968
|
explorerTree: [],
|
|
3956
5969
|
outputLog: []
|
|
3957
5970
|
});
|
|
@@ -3960,27 +5973,36 @@ function useStudio(server) {
|
|
|
3960
5973
|
useEffect(() => {
|
|
3961
5974
|
if (server.isConnected() && !stateRef.current.connected) {
|
|
3962
5975
|
const info = server.getConnectionInfo();
|
|
5976
|
+
const conns = server.listConnections();
|
|
3963
5977
|
setState((prev) => ({
|
|
3964
5978
|
...prev,
|
|
3965
5979
|
connected: true,
|
|
3966
5980
|
placeName: info?.placeName,
|
|
3967
|
-
placeId: info?.placeId
|
|
5981
|
+
placeId: info?.placeId,
|
|
5982
|
+
connectionCount: conns.length
|
|
3968
5983
|
}));
|
|
3969
5984
|
}
|
|
3970
5985
|
const onConnected = (payload) => {
|
|
5986
|
+
const conns = server.listConnections();
|
|
5987
|
+
const info = server.getConnectionInfo();
|
|
3971
5988
|
setState((prev) => ({
|
|
3972
5989
|
...prev,
|
|
3973
5990
|
connected: true,
|
|
3974
|
-
placeName: payload.placeName ?? prev.placeName,
|
|
3975
|
-
placeId: payload.placeId ?? prev.placeId
|
|
5991
|
+
placeName: info?.placeName ?? payload.placeName ?? prev.placeName,
|
|
5992
|
+
placeId: info?.placeId ?? payload.placeId ?? prev.placeId,
|
|
5993
|
+
connectionCount: conns.length
|
|
3976
5994
|
}));
|
|
3977
5995
|
};
|
|
3978
5996
|
const onDisconnected = () => {
|
|
5997
|
+
const conns = server.listConnections();
|
|
5998
|
+
const info = server.getConnectionInfo();
|
|
5999
|
+
const stillConnected = conns.length > 0;
|
|
3979
6000
|
setState((prev) => ({
|
|
3980
6001
|
...prev,
|
|
3981
|
-
connected:
|
|
3982
|
-
placeName: void 0,
|
|
3983
|
-
placeId: void 0
|
|
6002
|
+
connected: stillConnected,
|
|
6003
|
+
placeName: stillConnected ? info?.placeName : void 0,
|
|
6004
|
+
placeId: stillConnected ? info?.placeId : void 0,
|
|
6005
|
+
connectionCount: conns.length
|
|
3984
6006
|
}));
|
|
3985
6007
|
};
|
|
3986
6008
|
const onExplorer = (payload) => {
|
|
@@ -4263,182 +6285,47 @@ var App = ({ server, agent: initialAgent, version }) => {
|
|
|
4263
6285
|
connected: studio.connected,
|
|
4264
6286
|
cwd: process.cwd()
|
|
4265
6287
|
}
|
|
4266
|
-
),
|
|
4267
|
-
/* @__PURE__ */ jsx(
|
|
4268
|
-
InputBar,
|
|
4269
|
-
{
|
|
4270
|
-
onSubmit: handleSubmit,
|
|
4271
|
-
onImagePaste: handleImagePaste,
|
|
4272
|
-
isProcessing: false,
|
|
4273
|
-
imageAttached: !!pendingImage,
|
|
4274
|
-
onClearImage: handleClearImage,
|
|
4275
|
-
placeholder: hasApiKey ? "Type a message to begin, or Ctrl+V to paste an image..." : "Set up your API key to get started, or type /help..."
|
|
4276
|
-
}
|
|
4277
|
-
)
|
|
4278
|
-
] });
|
|
4279
|
-
}
|
|
4280
|
-
return /* @__PURE__ */ jsx(
|
|
4281
|
-
Layout,
|
|
4282
|
-
{
|
|
4283
|
-
connected: studio.connected,
|
|
4284
|
-
placeName: studio.placeName,
|
|
4285
|
-
model: agent?.getModel() ?? resolveProviderSettings(config).model,
|
|
4286
|
-
chatEntries,
|
|
4287
|
-
outputEntries: studio.outputLog,
|
|
4288
|
-
explorerTree: studio.explorerTree,
|
|
4289
|
-
toolCalls,
|
|
4290
|
-
streaming: streaming || void 0,
|
|
4291
|
-
isProcessing,
|
|
4292
|
-
onSubmit: handleSubmit,
|
|
4293
|
-
onImagePaste: handleImagePaste,
|
|
4294
|
-
imageAttached: !!pendingImage,
|
|
4295
|
-
onClearImage: handleClearImage
|
|
4296
|
-
}
|
|
4297
|
-
);
|
|
4298
|
-
};
|
|
4299
|
-
|
|
4300
|
-
// src/index.tsx
|
|
4301
|
-
init_ws_server();
|
|
4302
|
-
init_ws_client();
|
|
4303
|
-
|
|
4304
|
-
// src/ai/provider.ts
|
|
4305
|
-
init_config();
|
|
4306
|
-
var AIProvider = class {
|
|
4307
|
-
client;
|
|
4308
|
-
model;
|
|
4309
|
-
providerName;
|
|
4310
|
-
constructor(config) {
|
|
4311
|
-
if (!config.apiKey) {
|
|
4312
|
-
throw new Error("API key not configured. Run: dominus config set apiKey <your-key>");
|
|
4313
|
-
}
|
|
4314
|
-
const { baseUrl, model } = resolveProviderSettings(config);
|
|
4315
|
-
this.providerName = PROVIDERS[config.provider]?.name ?? config.provider;
|
|
4316
|
-
this.client = new OpenAI({
|
|
4317
|
-
apiKey: config.apiKey,
|
|
4318
|
-
baseURL: baseUrl
|
|
4319
|
-
});
|
|
4320
|
-
this.model = model;
|
|
4321
|
-
}
|
|
4322
|
-
setModel(model) {
|
|
4323
|
-
this.model = model;
|
|
4324
|
-
}
|
|
4325
|
-
getModel() {
|
|
4326
|
-
return this.model;
|
|
4327
|
-
}
|
|
4328
|
-
getProviderName() {
|
|
4329
|
-
return this.providerName;
|
|
4330
|
-
}
|
|
4331
|
-
async listModels() {
|
|
4332
|
-
try {
|
|
4333
|
-
const list = await this.client.models.list();
|
|
4334
|
-
const models = [];
|
|
4335
|
-
for await (const m of list) {
|
|
4336
|
-
models.push({ id: m.id, owned_by: m.owned_by });
|
|
4337
|
-
}
|
|
4338
|
-
models.sort((a, b) => a.id.localeCompare(b.id));
|
|
4339
|
-
return models;
|
|
4340
|
-
} catch {
|
|
4341
|
-
return [];
|
|
4342
|
-
}
|
|
4343
|
-
}
|
|
4344
|
-
async chat(messages, tools) {
|
|
4345
|
-
const openAITools = tools?.map(toolToOpenAI);
|
|
4346
|
-
const response = await this.client.chat.completions.create({
|
|
4347
|
-
model: this.model,
|
|
4348
|
-
messages,
|
|
4349
|
-
tools: openAITools?.length ? openAITools : void 0,
|
|
4350
|
-
temperature: 0.3
|
|
4351
|
-
});
|
|
4352
|
-
const choice = response.choices[0];
|
|
4353
|
-
if (!choice) throw new Error("No response from AI");
|
|
4354
|
-
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
4355
|
-
id: tc.id,
|
|
4356
|
-
name: tc.function.name,
|
|
4357
|
-
arguments: JSON.parse(tc.function.arguments)
|
|
4358
|
-
}));
|
|
4359
|
-
return {
|
|
4360
|
-
content: choice.message.content,
|
|
4361
|
-
toolCalls,
|
|
4362
|
-
finishReason: choice.finish_reason ?? "stop",
|
|
4363
|
-
usage: response.usage ? {
|
|
4364
|
-
promptTokens: response.usage.prompt_tokens,
|
|
4365
|
-
completionTokens: response.usage.completion_tokens,
|
|
4366
|
-
totalTokens: response.usage.total_tokens
|
|
4367
|
-
} : void 0
|
|
4368
|
-
};
|
|
4369
|
-
}
|
|
4370
|
-
async *streamChat(messages, tools) {
|
|
4371
|
-
const openAITools = tools?.map(toolToOpenAI);
|
|
4372
|
-
const stream = await this.client.chat.completions.create({
|
|
4373
|
-
model: this.model,
|
|
4374
|
-
messages,
|
|
4375
|
-
tools: openAITools?.length ? openAITools : void 0,
|
|
4376
|
-
temperature: 0.3,
|
|
4377
|
-
stream: true
|
|
4378
|
-
});
|
|
4379
|
-
const toolCallBuffers = /* @__PURE__ */ new Map();
|
|
4380
|
-
for await (const chunk of stream) {
|
|
4381
|
-
const delta = chunk.choices[0]?.delta;
|
|
4382
|
-
if (!delta) continue;
|
|
4383
|
-
if (delta.content) {
|
|
4384
|
-
yield { type: "text", content: delta.content };
|
|
4385
|
-
}
|
|
4386
|
-
if (delta.tool_calls) {
|
|
4387
|
-
for (const tc of delta.tool_calls) {
|
|
4388
|
-
if (!toolCallBuffers.has(tc.index)) {
|
|
4389
|
-
toolCallBuffers.set(tc.index, {
|
|
4390
|
-
id: tc.id ?? "",
|
|
4391
|
-
name: tc.function?.name ?? "",
|
|
4392
|
-
args: ""
|
|
4393
|
-
});
|
|
4394
|
-
if (tc.function?.name) {
|
|
4395
|
-
yield {
|
|
4396
|
-
type: "tool_call_start",
|
|
4397
|
-
toolCall: { id: tc.id, name: tc.function.name }
|
|
4398
|
-
};
|
|
4399
|
-
}
|
|
4400
|
-
}
|
|
4401
|
-
const buffer = toolCallBuffers.get(tc.index);
|
|
4402
|
-
if (tc.id) buffer.id = tc.id;
|
|
4403
|
-
if (tc.function?.name) buffer.name = tc.function.name;
|
|
4404
|
-
if (tc.function?.arguments) {
|
|
4405
|
-
buffer.args += tc.function.arguments;
|
|
4406
|
-
yield {
|
|
4407
|
-
type: "tool_call_delta",
|
|
4408
|
-
content: tc.function.arguments,
|
|
4409
|
-
toolCall: { id: buffer.id, name: buffer.name }
|
|
4410
|
-
};
|
|
4411
|
-
}
|
|
4412
|
-
}
|
|
4413
|
-
}
|
|
4414
|
-
if (chunk.choices[0]?.finish_reason === "tool_calls") {
|
|
4415
|
-
for (const [, buffer] of toolCallBuffers) {
|
|
4416
|
-
let args = {};
|
|
4417
|
-
try {
|
|
4418
|
-
args = JSON.parse(buffer.args);
|
|
4419
|
-
} catch {
|
|
4420
|
-
}
|
|
4421
|
-
yield {
|
|
4422
|
-
type: "tool_call_end",
|
|
4423
|
-
toolCall: { id: buffer.id, name: buffer.name, arguments: args }
|
|
4424
|
-
};
|
|
4425
|
-
}
|
|
4426
|
-
toolCallBuffers.clear();
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
yield { type: "done" };
|
|
6288
|
+
),
|
|
6289
|
+
/* @__PURE__ */ jsx(
|
|
6290
|
+
InputBar,
|
|
6291
|
+
{
|
|
6292
|
+
onSubmit: handleSubmit,
|
|
6293
|
+
onImagePaste: handleImagePaste,
|
|
6294
|
+
isProcessing: false,
|
|
6295
|
+
imageAttached: !!pendingImage,
|
|
6296
|
+
onClearImage: handleClearImage,
|
|
6297
|
+
placeholder: hasApiKey ? "Type a message to begin, or Ctrl+V to paste an image..." : "Set up your API key to get started, or type /help..."
|
|
6298
|
+
}
|
|
6299
|
+
)
|
|
6300
|
+
] });
|
|
4430
6301
|
}
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
6302
|
+
return /* @__PURE__ */ jsx(
|
|
6303
|
+
Layout,
|
|
6304
|
+
{
|
|
6305
|
+
connected: studio.connected,
|
|
6306
|
+
placeName: studio.placeName,
|
|
6307
|
+
connectionCount: studio.connectionCount,
|
|
6308
|
+
model: agent?.getModel() ?? resolveProviderSettings(config).model,
|
|
6309
|
+
chatEntries,
|
|
6310
|
+
outputEntries: studio.outputLog,
|
|
6311
|
+
explorerTree: studio.explorerTree,
|
|
6312
|
+
toolCalls,
|
|
6313
|
+
streaming: streaming || void 0,
|
|
6314
|
+
isProcessing,
|
|
6315
|
+
onSubmit: handleSubmit,
|
|
6316
|
+
onImagePaste: handleImagePaste,
|
|
6317
|
+
imageAttached: !!pendingImage,
|
|
6318
|
+
onClearImage: handleClearImage
|
|
4439
6319
|
}
|
|
4440
|
-
|
|
4441
|
-
}
|
|
6320
|
+
);
|
|
6321
|
+
};
|
|
6322
|
+
|
|
6323
|
+
// src/index.tsx
|
|
6324
|
+
init_ws_server();
|
|
6325
|
+
init_ws_client();
|
|
6326
|
+
|
|
6327
|
+
// src/agent/loop.ts
|
|
6328
|
+
init_provider();
|
|
4442
6329
|
|
|
4443
6330
|
// src/agent/system-prompt.ts
|
|
4444
6331
|
init_internal_memory();
|
|
@@ -4568,6 +6455,23 @@ When the user asks you to build or create objects (parts, trees, houses, terrain
|
|
|
4568
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.
|
|
4569
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.
|
|
4570
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
|
+
|
|
4571
6475
|
\`create_ui\` takes a declarative JSON tree and builds everything in a single Studio round-trip:
|
|
4572
6476
|
\`\`\`json
|
|
4573
6477
|
{
|
|
@@ -4676,6 +6580,20 @@ The \`get_class_info\` tool queries Roblox's ReflectionService to return the com
|
|
|
4676
6580
|
|
|
4677
6581
|
Use it whenever you're unsure about property names or types. It's fast and cached.
|
|
4678
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
|
+
|
|
4679
6597
|
${INTERNAL_MEMORY}`);
|
|
4680
6598
|
return parts.join("\n");
|
|
4681
6599
|
}
|
|
@@ -4782,6 +6700,343 @@ ${summaryText}`
|
|
|
4782
6700
|
init_config();
|
|
4783
6701
|
init_config();
|
|
4784
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();
|
|
4785
7040
|
var AgentLoop = class {
|
|
4786
7041
|
ai;
|
|
4787
7042
|
server;
|
|
@@ -4816,10 +7071,11 @@ var AgentLoop = class {
|
|
|
4816
7071
|
this.projectId = projectId;
|
|
4817
7072
|
}
|
|
4818
7073
|
async *run(userMessage, imageAttachment) {
|
|
7074
|
+
const storedUserMessage = userMessage + (imageAttachment ? " [image attached]" : "");
|
|
4819
7075
|
storeMessage({
|
|
4820
7076
|
sessionId: this.sessionId,
|
|
4821
7077
|
role: "user",
|
|
4822
|
-
content:
|
|
7078
|
+
content: storedUserMessage,
|
|
4823
7079
|
createdAt: Date.now()
|
|
4824
7080
|
});
|
|
4825
7081
|
const { messages } = buildContext({
|
|
@@ -4829,6 +7085,7 @@ var AgentLoop = class {
|
|
|
4829
7085
|
projectId: this.projectId,
|
|
4830
7086
|
sessionId: this.sessionId
|
|
4831
7087
|
});
|
|
7088
|
+
this.conversationHistory.push({ role: "user", content: storedUserMessage });
|
|
4832
7089
|
if (imageAttachment) {
|
|
4833
7090
|
const lastMsg = messages[messages.length - 1];
|
|
4834
7091
|
if (lastMsg && lastMsg.role === "user") {
|
|
@@ -4845,76 +7102,167 @@ var AgentLoop = class {
|
|
|
4845
7102
|
}
|
|
4846
7103
|
}
|
|
4847
7104
|
const toolCtx = this.createToolContext();
|
|
4848
|
-
|
|
4849
|
-
|
|
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
|
+
];
|
|
4850
7148
|
let iteration = 0;
|
|
4851
|
-
while (iteration < this.maxIterations) {
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
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 = {
|
|
4859
7166
|
role: "assistant",
|
|
4860
|
-
content: response.content,
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
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
|
|
4912
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;
|
|
4913
7265
|
}
|
|
4914
|
-
} catch (err) {
|
|
4915
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4916
|
-
yield { type: "error", message };
|
|
4917
|
-
break;
|
|
4918
7266
|
}
|
|
4919
7267
|
}
|
|
4920
7268
|
yield { type: "done" };
|
|
@@ -4959,102 +7307,133 @@ var AgentLoop = class {
|
|
|
4959
7307
|
config: createConfigAccess()
|
|
4960
7308
|
};
|
|
4961
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
|
+
}
|
|
4962
7335
|
clearHistory() {
|
|
4963
7336
|
this.conversationHistory = [];
|
|
4964
7337
|
this.sessionId = nanoid();
|
|
4965
7338
|
}
|
|
4966
7339
|
};
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
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}`);
|
|
4976
7364
|
}
|
|
4977
|
-
|
|
4978
|
-
|
|
7365
|
+
for (const rejected of result.decision.rejected) {
|
|
7366
|
+
lines.push(`- Rejected ${rejected.workerId}: ${rejected.reason}`);
|
|
4979
7367
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
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}`));
|
|
4982
7392
|
}
|
|
4983
|
-
|
|
4984
|
-
|
|
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"}`);
|
|
4985
7410
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
} catch (err) {
|
|
4994
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4995
|
-
return { success: false, error: message };
|
|
4996
|
-
}
|
|
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}`);
|
|
4997
7418
|
}
|
|
4998
|
-
|
|
4999
|
-
|
|
7419
|
+
if (review.nextAction) {
|
|
7420
|
+
lines.push(`Next action: ${review.nextAction}`);
|
|
5000
7421
|
}
|
|
5001
|
-
|
|
5002
|
-
function createDefaultRegistry() {
|
|
5003
|
-
const registry = new ToolRegistry();
|
|
5004
|
-
const toolModules = [
|
|
5005
|
-
Promise.resolve().then(() => (init_read_script(), read_script_exports)),
|
|
5006
|
-
Promise.resolve().then(() => (init_edit_script(), edit_script_exports)),
|
|
5007
|
-
Promise.resolve().then(() => (init_run_code(), run_code_exports)),
|
|
5008
|
-
Promise.resolve().then(() => (init_get_explorer(), get_explorer_exports)),
|
|
5009
|
-
Promise.resolve().then(() => (init_get_properties(), get_properties_exports)),
|
|
5010
|
-
Promise.resolve().then(() => (init_get_descendants_properties(), get_descendants_properties_exports)),
|
|
5011
|
-
Promise.resolve().then(() => (init_set_properties(), set_properties_exports)),
|
|
5012
|
-
Promise.resolve().then(() => (init_insert_instance(), insert_instance_exports)),
|
|
5013
|
-
Promise.resolve().then(() => (init_delete_instance(), delete_instance_exports)),
|
|
5014
|
-
Promise.resolve().then(() => (init_get_output(), get_output_exports)),
|
|
5015
|
-
Promise.resolve().then(() => (init_get_selection(), get_selection_exports)),
|
|
5016
|
-
Promise.resolve().then(() => (init_search_scripts(), search_scripts_exports)),
|
|
5017
|
-
Promise.resolve().then(() => (init_run_tests(), run_tests_exports)),
|
|
5018
|
-
Promise.resolve().then(() => (init_get_class_info(), get_class_info_exports)),
|
|
5019
|
-
Promise.resolve().then(() => (init_create_ui(), create_ui_exports)),
|
|
5020
|
-
Promise.resolve().then(() => (init_execute_run_test(), execute_run_test_exports)),
|
|
5021
|
-
Promise.resolve().then(() => (init_execute_play_test(), execute_play_test_exports)),
|
|
5022
|
-
Promise.resolve().then(() => (init_create_part(), create_part_exports)),
|
|
5023
|
-
Promise.resolve().then(() => (init_clone_instance(), clone_instance_exports)),
|
|
5024
|
-
Promise.resolve().then(() => (init_group_instances(), group_instances_exports)),
|
|
5025
|
-
Promise.resolve().then(() => (init_build_multiple(), build_multiple_exports)),
|
|
5026
|
-
Promise.resolve().then(() => (init_serialize_ui(), serialize_ui_exports)),
|
|
5027
|
-
Promise.resolve().then(() => (init_move_instance(), move_instance_exports)),
|
|
5028
|
-
Promise.resolve().then(() => (init_bulk_set_properties(), bulk_set_properties_exports)),
|
|
5029
|
-
Promise.resolve().then(() => (init_find_replace_scripts(), find_replace_scripts_exports)),
|
|
5030
|
-
Promise.resolve().then(() => (init_recall(), recall_exports)),
|
|
5031
|
-
Promise.resolve().then(() => (init_remember(), remember_exports)),
|
|
5032
|
-
Promise.resolve().then(() => (init_upload_asset(), upload_asset_exports))
|
|
5033
|
-
];
|
|
5034
|
-
void Promise.all(
|
|
5035
|
-
toolModules.map(async (mod) => {
|
|
5036
|
-
const m = await mod;
|
|
5037
|
-
if (m.tool) registry.register(m.tool);
|
|
5038
|
-
})
|
|
5039
|
-
);
|
|
5040
|
-
void Promise.resolve().then(() => (init_undo_redo(), undo_redo_exports)).then((m) => {
|
|
5041
|
-
registry.register(m.undoTool);
|
|
5042
|
-
registry.register(m.redoTool);
|
|
5043
|
-
});
|
|
5044
|
-
return registry;
|
|
7422
|
+
return lines.join("\n");
|
|
5045
7423
|
}
|
|
5046
7424
|
|
|
5047
7425
|
// src/index.tsx
|
|
7426
|
+
init_registry();
|
|
5048
7427
|
init_store();
|
|
5049
7428
|
init_config();
|
|
5050
|
-
var CACHE_DIR =
|
|
5051
|
-
var CACHE_FILE =
|
|
7429
|
+
var CACHE_DIR = path5.join(os3.homedir(), ".dominus");
|
|
7430
|
+
var CACHE_FILE = path5.join(CACHE_DIR, "update-check.json");
|
|
5052
7431
|
var CHECK_INTERVAL_MS = 4 * 60 * 60 * 1e3;
|
|
5053
7432
|
var NPM_PACKAGE = "dominus-cli";
|
|
5054
7433
|
function readCache() {
|
|
5055
7434
|
try {
|
|
5056
|
-
if (
|
|
5057
|
-
return JSON.parse(
|
|
7435
|
+
if (fs6.existsSync(CACHE_FILE)) {
|
|
7436
|
+
return JSON.parse(fs6.readFileSync(CACHE_FILE, "utf-8"));
|
|
5058
7437
|
}
|
|
5059
7438
|
} catch {
|
|
5060
7439
|
}
|
|
@@ -5062,8 +7441,8 @@ function readCache() {
|
|
|
5062
7441
|
}
|
|
5063
7442
|
function writeCache(cache) {
|
|
5064
7443
|
try {
|
|
5065
|
-
if (!
|
|
5066
|
-
|
|
7444
|
+
if (!fs6.existsSync(CACHE_DIR)) fs6.mkdirSync(CACHE_DIR, { recursive: true });
|
|
7445
|
+
fs6.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
|
|
5067
7446
|
} catch {
|
|
5068
7447
|
}
|
|
5069
7448
|
}
|
|
@@ -5163,8 +7542,8 @@ async function autoUpdate(currentVersion) {
|
|
|
5163
7542
|
try {
|
|
5164
7543
|
const out = execSync("pnpm list -g dominus-cli", { stdio: "pipe", timeout: 1e4 }).toString();
|
|
5165
7544
|
if (out.includes("link:")) {
|
|
5166
|
-
const pkgPath =
|
|
5167
|
-
if (
|
|
7545
|
+
const pkgPath = path5.resolve(__dirname$1, "..");
|
|
7546
|
+
if (fs6.existsSync(path5.join(pkgPath, ".git"))) {
|
|
5168
7547
|
try {
|
|
5169
7548
|
execSync("git pull", { cwd: pkgPath, stdio: "pipe", timeout: 3e4 });
|
|
5170
7549
|
execSync("pnpm install", { cwd: pkgPath, stdio: "pipe", timeout: 6e4 });
|
|
@@ -5191,9 +7570,9 @@ async function autoUpdate(currentVersion) {
|
|
|
5191
7570
|
return { success: false, from: currentVersion, to: info.latestVersion, error: `Update command failed: ${err}` };
|
|
5192
7571
|
}
|
|
5193
7572
|
}
|
|
5194
|
-
var __dirname$1 =
|
|
5195
|
-
var __dirname2 =
|
|
5196
|
-
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;
|
|
5197
7576
|
var program = new Command();
|
|
5198
7577
|
program.name("dominus").description("AI-powered autonomous agent for Roblox Studio development").version(VERSION);
|
|
5199
7578
|
program.command("start", { isDefault: true }).description("Launch the Dominus TUI agent").option("-p, --port <port>", "WebSocket server port", "18088").action(async (opts) => {
|
|
@@ -5322,24 +7701,24 @@ program.command("update").description("Auto-update Dominus to the latest version
|
|
|
5322
7701
|
});
|
|
5323
7702
|
program.command("install-plugin").description("Build & install the Dominus plugin into Roblox Studio").action(async () => {
|
|
5324
7703
|
const { execSync: execSync3 } = await import('child_process');
|
|
5325
|
-
const packageRoot =
|
|
5326
|
-
const pluginDir =
|
|
5327
|
-
const pluginProject =
|
|
5328
|
-
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(
|
|
5329
7708
|
os3.homedir(),
|
|
5330
7709
|
"AppData",
|
|
5331
7710
|
"Local",
|
|
5332
7711
|
"Roblox",
|
|
5333
7712
|
"Plugins"
|
|
5334
7713
|
);
|
|
5335
|
-
const destFile =
|
|
5336
|
-
if (!
|
|
7714
|
+
const destFile = path5.join(studioPlugins, "Dominus.rbxm");
|
|
7715
|
+
if (!fs6.existsSync(pluginProject)) {
|
|
5337
7716
|
console.log(" \u2716 Plugin source not found.");
|
|
5338
7717
|
console.log(` Expected at: ${pluginDir}`);
|
|
5339
7718
|
return;
|
|
5340
7719
|
}
|
|
5341
|
-
if (!
|
|
5342
|
-
|
|
7720
|
+
if (!fs6.existsSync(studioPlugins)) {
|
|
7721
|
+
fs6.mkdirSync(studioPlugins, { recursive: true });
|
|
5343
7722
|
}
|
|
5344
7723
|
let rojoAvailable = false;
|
|
5345
7724
|
try {
|
|
@@ -5367,48 +7746,48 @@ program.command("mcp").description("Start the Dominus MCP server (for Cursor, Cl
|
|
|
5367
7746
|
});
|
|
5368
7747
|
function getMcpTargets() {
|
|
5369
7748
|
const home = os3.homedir();
|
|
5370
|
-
const appData = process.env.APPDATA ||
|
|
5371
|
-
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");
|
|
5372
7751
|
const cwd = process.cwd();
|
|
5373
7752
|
return [
|
|
5374
7753
|
{
|
|
5375
7754
|
name: "Cursor (project)",
|
|
5376
|
-
configPath:
|
|
7755
|
+
configPath: path5.join(cwd, ".cursor", "mcp.json"),
|
|
5377
7756
|
keyPath: ["mcpServers"]
|
|
5378
7757
|
},
|
|
5379
7758
|
{
|
|
5380
7759
|
name: "Cursor (global)",
|
|
5381
|
-
configPath:
|
|
7760
|
+
configPath: path5.join(home, ".cursor", "mcp.json"),
|
|
5382
7761
|
keyPath: ["mcpServers"]
|
|
5383
7762
|
},
|
|
5384
7763
|
{
|
|
5385
7764
|
name: "Claude Desktop",
|
|
5386
|
-
configPath:
|
|
7765
|
+
configPath: path5.join(appData, "Claude", "claude_desktop_config.json"),
|
|
5387
7766
|
keyPath: ["mcpServers"]
|
|
5388
7767
|
},
|
|
5389
7768
|
{
|
|
5390
7769
|
name: "Claude Code",
|
|
5391
|
-
configPath:
|
|
7770
|
+
configPath: path5.join(home, ".claude.json"),
|
|
5392
7771
|
keyPath: ["mcpServers"]
|
|
5393
7772
|
},
|
|
5394
7773
|
{
|
|
5395
7774
|
name: "Windsurf (project)",
|
|
5396
|
-
configPath:
|
|
7775
|
+
configPath: path5.join(cwd, ".windsurf", "mcp.json"),
|
|
5397
7776
|
keyPath: ["mcpServers"]
|
|
5398
7777
|
},
|
|
5399
7778
|
{
|
|
5400
7779
|
name: "Windsurf (global)",
|
|
5401
|
-
configPath:
|
|
7780
|
+
configPath: path5.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
5402
7781
|
keyPath: ["mcpServers"]
|
|
5403
7782
|
},
|
|
5404
7783
|
{
|
|
5405
7784
|
name: "VS Code (project)",
|
|
5406
|
-
configPath:
|
|
7785
|
+
configPath: path5.join(cwd, ".vscode", "mcp.json"),
|
|
5407
7786
|
keyPath: ["servers"]
|
|
5408
7787
|
},
|
|
5409
7788
|
{
|
|
5410
7789
|
name: "VS Code (global)",
|
|
5411
|
-
configPath:
|
|
7790
|
+
configPath: path5.join(localAppData, "Code", "User", "settings.json"),
|
|
5412
7791
|
keyPath: ["mcp", "servers"]
|
|
5413
7792
|
}
|
|
5414
7793
|
];
|
|
@@ -5420,9 +7799,9 @@ var dominusEntry = {
|
|
|
5420
7799
|
};
|
|
5421
7800
|
function injectMcpConfig(target) {
|
|
5422
7801
|
let config = {};
|
|
5423
|
-
if (
|
|
7802
|
+
if (fs6.existsSync(target.configPath)) {
|
|
5424
7803
|
try {
|
|
5425
|
-
config = JSON.parse(
|
|
7804
|
+
config = JSON.parse(fs6.readFileSync(target.configPath, "utf-8"));
|
|
5426
7805
|
} catch {
|
|
5427
7806
|
config = {};
|
|
5428
7807
|
}
|
|
@@ -5438,12 +7817,12 @@ function injectMcpConfig(target) {
|
|
|
5438
7817
|
return { status: "already" };
|
|
5439
7818
|
}
|
|
5440
7819
|
obj["dominus"] = { ...dominusEntry };
|
|
5441
|
-
const dir =
|
|
5442
|
-
if (!
|
|
5443
|
-
|
|
7820
|
+
const dir = path5.dirname(target.configPath);
|
|
7821
|
+
if (!fs6.existsSync(dir)) {
|
|
7822
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
5444
7823
|
}
|
|
5445
|
-
|
|
5446
|
-
return { status:
|
|
7824
|
+
fs6.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
7825
|
+
return { status: fs6.existsSync(target.configPath) ? "installed" : "created" };
|
|
5447
7826
|
}
|
|
5448
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) => {
|
|
5449
7828
|
const targets = getMcpTargets();
|
|
@@ -5465,7 +7844,7 @@ program.command("mcp-install").description("Auto-install Dominus MCP into Cursor
|
|
|
5465
7844
|
}
|
|
5466
7845
|
const detected = targets.map((t) => ({
|
|
5467
7846
|
target: t,
|
|
5468
|
-
exists:
|
|
7847
|
+
exists: fs6.existsSync(path5.dirname(t.configPath))
|
|
5469
7848
|
}));
|
|
5470
7849
|
const toInstall = detected.filter((d) => d.exists);
|
|
5471
7850
|
if (toInstall.length === 0) {
|
|
@@ -5538,23 +7917,23 @@ program.command("mcp-setup").description("Print MCP configuration for manual set
|
|
|
5538
7917
|
console.log("");
|
|
5539
7918
|
});
|
|
5540
7919
|
function buildPluginXml(pluginDir, destFile) {
|
|
5541
|
-
const srcDir =
|
|
5542
|
-
if (!
|
|
7920
|
+
const srcDir = path5.join(pluginDir, "src");
|
|
7921
|
+
if (!fs6.existsSync(srcDir)) {
|
|
5543
7922
|
console.log(` \u2716 Plugin source directory not found: ${srcDir}`);
|
|
5544
7923
|
return;
|
|
5545
7924
|
}
|
|
5546
|
-
const files =
|
|
7925
|
+
const files = fs6.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
|
|
5547
7926
|
const mainFile = files.find((f) => f === "init.server.lua");
|
|
5548
7927
|
const moduleFiles = files.filter((f) => f !== "init.server.lua");
|
|
5549
7928
|
if (!mainFile) {
|
|
5550
7929
|
console.log(" \u2716 init.server.lua not found in plugin source");
|
|
5551
7930
|
return;
|
|
5552
7931
|
}
|
|
5553
|
-
const mainSource =
|
|
7932
|
+
const mainSource = fs6.readFileSync(path5.join(srcDir, mainFile), "utf-8");
|
|
5554
7933
|
let moduleItems = "";
|
|
5555
7934
|
for (const mf of moduleFiles) {
|
|
5556
7935
|
const modName = mf.replace(".lua", "");
|
|
5557
|
-
const modSource =
|
|
7936
|
+
const modSource = fs6.readFileSync(path5.join(srcDir, mf), "utf-8");
|
|
5558
7937
|
moduleItems += `
|
|
5559
7938
|
<Item class="ModuleScript" referent="${modName}">
|
|
5560
7939
|
<Properties>
|
|
@@ -5572,7 +7951,7 @@ function buildPluginXml(pluginDir, destFile) {
|
|
|
5572
7951
|
</Properties>${moduleItems}
|
|
5573
7952
|
</Item>
|
|
5574
7953
|
</roblox>`;
|
|
5575
|
-
|
|
7954
|
+
fs6.writeFileSync(destFile, rbxmx, "utf-8");
|
|
5576
7955
|
console.log(` \u2714 Plugin installed to: ${destFile}`);
|
|
5577
7956
|
console.log(" Restart Roblox Studio to load the plugin.");
|
|
5578
7957
|
}
|