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 CHANGED
@@ -1,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import fs5, { existsSync, readFileSync, unlinkSync } from 'fs';
3
- import path4, { join } from 'path';
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 (!fs5.existsSync(DOMINUS_DIR)) {
61
- fs5.mkdirSync(DOMINUS_DIR, { recursive: true });
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 (fs5.existsSync(filePath)) {
67
- const raw = fs5.readFileSync(filePath, "utf-8");
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
- fs5.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
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 = path4.join(os3.homedir(), ".dominus");
129
- CONFIG_PATH = path4.join(DOMINUS_DIR, "config.json");
130
- DB_PATH = path4.join(DOMINUS_DIR, "dominus.db");
131
- RULES_PATH = path4.join(DOMINUS_DIR, "rules.json");
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
- studioClient = null;
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, controllerWs] of this.relayRequests) {
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 if (this.studioClient?.ws === ws) {
325
- this.studioClient = null;
326
- this.emit("studio:disconnected");
327
- this.broadcastToControllers("studio:disconnected", {});
328
- for (const [id, pending] of this.pendingRequests) {
329
- clearTimeout(pending.timer);
330
- pending.reject(new Error("Studio disconnected"));
331
- this.pendingRequests.delete(id);
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
- this.studioClient = {
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: payload.placeId,
383
+ placeId,
349
384
  placeName: payload.placeName
350
385
  };
351
- this.emit("studio:connected", payload);
352
- this.broadcastToControllers("studio:connected", payload);
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 (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
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: "Studio not connected" },
434
+ payload: { error: this.getTargetError() },
372
435
  ts: Date.now()
373
436
  }));
374
437
  return;
375
438
  }
376
- this.relayRequests.set(msg.id, controllerWs);
377
- this.studioClient.ws.send(serializeMessage(msg));
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
- if (this.studioClient) {
383
- this.studioClient.studioVersion = payload.studioVersion;
384
- this.studioClient.placeId = payload.placeId;
385
- this.studioClient.placeName = payload.placeName;
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
- const controllerWs = this.relayRequests.get(msg.id);
393
- if (controllerWs && controllerWs.readyState === WebSocket.OPEN) {
394
- controllerWs.send(serializeMessage(msg));
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
- if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
413
- reject(new Error("No Studio connection"));
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
- this.studioClient.ws.send(serializeMessage(msg));
535
+ target.ws.send(serializeMessage(msg));
427
536
  });
428
537
  }
429
538
  sendNotification(type, payload) {
430
- if (!this.studioClient || this.studioClient.ws.readyState !== WebSocket.OPEN) {
431
- return;
432
- }
539
+ const target = this.resolveTarget();
540
+ if (!target || target.ws.readyState !== WebSocket.OPEN) return;
433
541
  const msg = createMessage(type, payload);
434
- this.studioClient.ws.send(serializeMessage(msg));
542
+ target.ws.send(serializeMessage(msg));
435
543
  }
436
544
  isConnected() {
437
- return this.studioClient !== null && this.studioClient.ws.readyState === WebSocket.OPEN;
545
+ return this.resolveTarget() !== null;
438
546
  }
439
547
  getConnectionInfo() {
440
- return this.studioClient;
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
- if (this.studioClient) {
448
- this.studioClient.ws.close();
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
- connectionInfo = null;
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
- this.connectionInfo = {
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: payload.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
- this.studioConnected = false;
560
- this.connectionInfo = null;
561
- this.emit("studio:disconnected");
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.studioConnected;
775
+ return this.resolveTargetInfo() !== null;
606
776
  }
607
777
  getConnectionInfo() {
608
- if (!this.connectionInfo) return null;
609
- return { ...this.connectionInfo, ws: this.ws };
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
- fs5.writeFileSync(dbPath, Buffer.from(data));
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 (fs5.existsSync(dbPath)) {
757
- const buffer = fs5.readFileSync(dbPath);
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/tools/studio/read-script.ts
1023
- var read_script_exports = {};
1024
- __export(read_script_exports, {
1025
- tool: () => tool
1026
- });
1027
- var tool;
1028
- var init_read_script = __esm({
1029
- "src/tools/studio/read-script.ts"() {
1030
- init_protocol();
1031
- tool = {
1032
- name: "read_script",
1033
- description: 'Read the source code of a script in Roblox Studio. Provide the full instance path (e.g., "ServerScriptService.GameManager").',
1034
- parameters: {
1035
- type: "object",
1036
- properties: {
1037
- path: {
1038
- type: "string",
1039
- description: 'Full instance path of the script (e.g., "ServerScriptService.GameManager")'
1040
- }
1041
- },
1042
- required: ["path"]
1043
- },
1044
- async execute(params, ctx) {
1045
- if (!ctx.isStudioConnected()) {
1046
- return { success: false, error: "Studio is not connected" };
1047
- }
1048
- const path5 = params.path;
1049
- const response = await ctx.sendToStudio(CliMsg.GET_SCRIPT, { path: path5 });
1050
- if (!response.payload) {
1051
- return { success: false, error: `Script not found: ${path5}` };
1052
- }
1053
- return {
1054
- success: true,
1055
- data: {
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/tools/studio/edit-script.ts
1068
- var edit_script_exports = {};
1069
- __export(edit_script_exports, {
1070
- tool: () => tool2
1071
- });
1072
- var tool2;
1073
- var init_edit_script = __esm({
1074
- "src/tools/studio/edit-script.ts"() {
1075
- init_protocol();
1076
- tool2 = {
1077
- name: "edit_script",
1078
- description: "Replace the entire source code of a script in Roblox Studio. Provide the full path and the new source code.",
1079
- parameters: {
1080
- type: "object",
1081
- properties: {
1082
- path: {
1083
- type: "string",
1084
- description: "Full instance path of the script"
1085
- },
1086
- source: {
1087
- type: "string",
1088
- description: "The new complete source code for the script"
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
- const path5 = params.path;
1098
- const source = params.source;
1099
- const response = await ctx.sendToStudio(CliMsg.SET_SCRIPT, { path: path5, source });
1100
- const payload = response.payload;
1101
- if (!payload.success) {
1102
- return { success: false, error: payload.error ?? "Failed to edit script" };
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
- return { success: true, data: { path: path5, linesWritten: source.split("\n").length } };
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
- // src/tools/studio/run-code.ts
1111
- var run_code_exports = {};
1112
- __export(run_code_exports, {
1113
- tool: () => tool3
1114
- });
1115
- var tool3;
1116
- var init_run_code = __esm({
1117
- "src/tools/studio/run-code.ts"() {
1118
- init_protocol();
1119
- tool3 = {
1120
- name: "run_code",
1121
- description: "Execute arbitrary Luau code in Roblox Studio and return the output. The code runs in the Studio plugin context with full API access.",
1122
- parameters: {
1123
- type: "object",
1124
- properties: {
1125
- code: {
1126
- type: "string",
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
- return {
1143
- success: true,
1144
- data: { output: payload.output, duration: payload.duration }
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 fs6 = await import('fs/promises');
1275
- const path5 = await import('path');
1276
- const outputPath = path5.resolve(process.cwd(), params.outputFile);
1277
- await fs6.writeFile(outputPath, JSON.stringify(response.payload, null, 2), "utf-8");
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 path5 = params.path;
1855
- const parts = path5.split(".");
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(path5) {
1997
- const segs = path5.split(".");
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"}}, ...]\nExample (filter): root="StarterGui.HUD", className="TextLabel", properties={"Font": "GothamBold"}',
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: "Replacement text (supports Lua pattern captures like %1, %2)"
4061
+ description: 'API reference name, e.g. "Frame", "RunService", "UDim2", "FrameStyle", "task"'
2297
4062
  },
2298
- root: {
4063
+ kind: {
2299
4064
  type: "string",
2300
- description: "Root path to limit search scope (optional, searches entire game if omitted)"
2301
- },
2302
- usePattern: {
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
- dryRun: {
4069
+ includeInherited: {
2307
4070
  type: "boolean",
2308
- description: "If true, report matches but do not modify scripts. Default: false"
4071
+ description: "For classes, include members from inherited base classes (default: true).",
4072
+ default: true
2309
4073
  }
2310
4074
  },
2311
- required: ["find", "replace"]
4075
+ required: ["name"]
2312
4076
  },
2313
- async execute(params, ctx) {
2314
- if (!ctx.isStudioConnected()) {
2315
- return { success: false, error: "Studio is not connected" };
2316
- }
2317
- const response = await ctx.sendToStudio(CliMsg.FIND_REPLACE_SCRIPTS, {
2318
- find: params.find,
2319
- replace: params.replace,
2320
- root: params.root,
2321
- usePattern: params.usePattern ?? false,
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: () => tool26
4095
+ tool: () => tool28
2346
4096
  });
2347
- var tool26;
4097
+ var tool28;
2348
4098
  var init_recall = __esm({
2349
4099
  "src/tools/memory/recall.ts"() {
2350
4100
  init_store();
2351
- tool26 = {
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: () => tool27
4140
+ tool: () => tool29
2391
4141
  });
2392
- var tool27;
4142
+ var tool29;
2393
4143
  var init_remember = __esm({
2394
4144
  "src/tools/memory/remember.ts"() {
2395
4145
  init_store();
2396
- tool27 = {
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: () => tool28
4186
+ tool: () => tool30
2437
4187
  });
2438
- var tool28;
4188
+ var tool30;
2439
4189
  var init_upload_asset = __esm({
2440
4190
  "src/tools/cloud/upload-asset.ts"() {
2441
- tool28 = {
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 (!fs5.existsSync(filePath)) {
4229
+ if (!fs6.existsSync(filePath)) {
2480
4230
  return { success: false, error: `File not found: ${filePath}` };
2481
4231
  }
2482
- const fileBuffer = fs5.readFileSync(filePath);
2483
- const fileName = path4.basename(filePath);
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.5.8"
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: path5 }) => {
4563
+ async ({ path: path6 }) => {
2721
4564
  assertConnected();
2722
- const res = await bridge.sendRequest(CliMsg.GET_SCRIPT, { path: path5 });
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: path5, source }) => {
4576
+ async ({ path: path6, source }) => {
2734
4577
  assertConnected();
2735
- const res = await bridge.sendRequest(CliMsg.SET_SCRIPT, { path: path5, source });
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: path5, compact }) => {
4615
+ async ({ path: path6, compact }) => {
2773
4616
  assertConnected();
2774
- const res = await bridge.sendRequest(CliMsg.GET_PROPERTIES, { path: path5, compact });
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: path5, compact, outputFile }) => {
4629
+ async ({ path: path6, compact, outputFile }) => {
2787
4630
  assertConnected();
2788
- const res = await bridge.sendRequest(CliMsg.GET_DESCENDANTS_PROPERTIES, { path: path5, compact, outputFile }, 1e3 * 60 * 5);
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: path5, properties }) => {
4642
+ async ({ path: path6, properties }) => {
2800
4643
  assertConnected();
2801
- const res = await bridge.sendRequest(CliMsg.SET_PROPERTIES, { path: path5, properties });
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: path5 }) => {
4672
+ async ({ path: path6 }) => {
2830
4673
  assertConnected();
2831
- const res = await bridge.sendRequest(CliMsg.DELETE_INSTANCE, { path: path5 });
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: path5, maxDepth }) => {
4997
+ async ({ path: path6, maxDepth }) => {
3100
4998
  assertConnected();
3101
4999
  const res = await bridge.sendRequest(
3102
5000
  CliMsg.SERIALIZE_UI,
3103
- { path: path5, maxDepth },
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: path5, childDepth }) => {
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: path5, compact: true }
5051
+ { path: path6, compact: true }
3154
5052
  ),
3155
5053
  bridge.sendRequest(
3156
5054
  CliMsg.GET_EXPLORER,
3157
- { rootPath: path5, maxDepth: childDepth ?? 2 }
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: path5, framework, componentName }) => {
5077
+ async ({ path: path6, framework, componentName }) => {
3180
5078
  assertConnected();
3181
5079
  const res = await bridge.sendRequest(
3182
5080
  CliMsg.SERIALIZE_UI,
3183
- { path: path5, maxDepth: 50 },
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 statusText = connected ? `\u25CF Connected${placeName ? ` \u2014 ${placeName}` : ""}` : "\u25CB Disconnected";
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: false,
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
- function toolToOpenAI(tool29) {
4433
- return {
4434
- type: "function",
4435
- function: {
4436
- name: tool29.name,
4437
- description: tool29.description,
4438
- parameters: tool29.parameters
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: userMessage + (imageAttachment ? " [image attached]" : ""),
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
- const allTools = this.tools.getAll();
4849
- let currentMessages = [...messages];
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
- iteration++;
4853
- try {
4854
- const response = await this.ai.chat(currentMessages, allTools);
4855
- if (response.content) {
4856
- yield { type: "text", content: response.content };
4857
- storeMessage({
4858
- sessionId: this.sessionId,
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
- createdAt: Date.now()
4862
- });
4863
- this.conversationHistory.push({ role: "assistant", content: response.content });
4864
- }
4865
- if (response.toolCalls.length === 0) {
4866
- break;
4867
- }
4868
- const toolResults = [];
4869
- for (const call of response.toolCalls) {
4870
- yield { type: "tool_call", tool: call.name, args: call.arguments };
4871
- const result = await this.tools.execute(call, toolCtx);
4872
- toolResults.push({ call, result });
4873
- yield { type: "tool_result", tool: call.name, result };
4874
- storeMessage({
4875
- sessionId: this.sessionId,
4876
- role: "tool_call",
4877
- content: JSON.stringify({ name: call.name, args: call.arguments }),
4878
- toolName: call.name,
4879
- createdAt: Date.now()
4880
- });
4881
- storeMessage({
4882
- sessionId: this.sessionId,
4883
- role: "tool_result",
4884
- content: JSON.stringify(result),
4885
- toolName: call.name,
4886
- createdAt: Date.now()
4887
- });
4888
- }
4889
- const assistantMsg = {
4890
- role: "assistant",
4891
- content: response.content ?? null,
4892
- tool_calls: response.toolCalls.map((tc) => ({
4893
- id: tc.id,
4894
- type: "function",
4895
- function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
4896
- }))
4897
- };
4898
- currentMessages.push(assistantMsg);
4899
- for (const { call, result } of toolResults) {
4900
- currentMessages.push({
4901
- role: "tool",
4902
- content: JSON.stringify(result),
4903
- tool_call_id: call.id
4904
- });
4905
- }
4906
- this.conversationHistory.push(assistantMsg);
4907
- for (const { call, result } of toolResults) {
4908
- this.conversationHistory.push({
4909
- role: "tool",
4910
- content: JSON.stringify(result),
4911
- tool_call_id: call.id
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
- // src/tools/registry.ts
4969
- var ToolRegistry = class {
4970
- tools = /* @__PURE__ */ new Map();
4971
- register(tool29) {
4972
- if (this.tools.has(tool29.name)) {
4973
- throw new Error(`Tool already registered: ${tool29.name}`);
4974
- }
4975
- this.tools.set(tool29.name, tool29);
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
- get(name) {
4978
- return this.tools.get(name);
7365
+ for (const rejected of result.decision.rejected) {
7366
+ lines.push(`- Rejected ${rejected.workerId}: ${rejected.reason}`);
4979
7367
  }
4980
- getAll() {
4981
- return Array.from(this.tools.values());
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
- getSchemas() {
4984
- return this.getAll();
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
- async execute(call, ctx) {
4987
- const tool29 = this.tools.get(call.name);
4988
- if (!tool29) {
4989
- return { success: false, error: `Unknown tool: ${call.name}` };
4990
- }
4991
- try {
4992
- return await tool29.execute(call.arguments, ctx);
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
- listNames() {
4999
- return Array.from(this.tools.keys());
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 = path4.join(os3.homedir(), ".dominus");
5051
- var CACHE_FILE = path4.join(CACHE_DIR, "update-check.json");
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 (fs5.existsSync(CACHE_FILE)) {
5057
- return JSON.parse(fs5.readFileSync(CACHE_FILE, "utf-8"));
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 (!fs5.existsSync(CACHE_DIR)) fs5.mkdirSync(CACHE_DIR, { recursive: true });
5066
- fs5.writeFileSync(CACHE_FILE, JSON.stringify(cache), "utf-8");
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 = path4.resolve(__dirname$1, "..");
5167
- if (fs5.existsSync(path4.join(pkgPath, ".git"))) {
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 = path4.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
5195
- var __dirname2 = path4.dirname(fileURLToPath(import.meta.url));
5196
- var VERSION = JSON.parse(fs5.readFileSync(path4.join(__dirname2, "..", "package.json"), "utf-8")).version;
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 = path4.resolve(import.meta.dirname ?? __dirname2, "..");
5326
- const pluginDir = path4.join(packageRoot, "plugin");
5327
- const pluginProject = path4.join(pluginDir, "default.project.json");
5328
- const studioPlugins = path4.join(
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 = path4.join(studioPlugins, "Dominus.rbxm");
5336
- if (!fs5.existsSync(pluginProject)) {
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 (!fs5.existsSync(studioPlugins)) {
5342
- fs5.mkdirSync(studioPlugins, { recursive: true });
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 || path4.join(home, "AppData", "Roaming");
5371
- const localAppData = process.env.LOCALAPPDATA || path4.join(home, "AppData", "Local");
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: path4.join(cwd, ".cursor", "mcp.json"),
7755
+ configPath: path5.join(cwd, ".cursor", "mcp.json"),
5377
7756
  keyPath: ["mcpServers"]
5378
7757
  },
5379
7758
  {
5380
7759
  name: "Cursor (global)",
5381
- configPath: path4.join(home, ".cursor", "mcp.json"),
7760
+ configPath: path5.join(home, ".cursor", "mcp.json"),
5382
7761
  keyPath: ["mcpServers"]
5383
7762
  },
5384
7763
  {
5385
7764
  name: "Claude Desktop",
5386
- configPath: path4.join(appData, "Claude", "claude_desktop_config.json"),
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: path4.join(home, ".claude.json"),
7770
+ configPath: path5.join(home, ".claude.json"),
5392
7771
  keyPath: ["mcpServers"]
5393
7772
  },
5394
7773
  {
5395
7774
  name: "Windsurf (project)",
5396
- configPath: path4.join(cwd, ".windsurf", "mcp.json"),
7775
+ configPath: path5.join(cwd, ".windsurf", "mcp.json"),
5397
7776
  keyPath: ["mcpServers"]
5398
7777
  },
5399
7778
  {
5400
7779
  name: "Windsurf (global)",
5401
- configPath: path4.join(home, ".codeium", "windsurf", "mcp_config.json"),
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: path4.join(cwd, ".vscode", "mcp.json"),
7785
+ configPath: path5.join(cwd, ".vscode", "mcp.json"),
5407
7786
  keyPath: ["servers"]
5408
7787
  },
5409
7788
  {
5410
7789
  name: "VS Code (global)",
5411
- configPath: path4.join(localAppData, "Code", "User", "settings.json"),
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 (fs5.existsSync(target.configPath)) {
7802
+ if (fs6.existsSync(target.configPath)) {
5424
7803
  try {
5425
- config = JSON.parse(fs5.readFileSync(target.configPath, "utf-8"));
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 = path4.dirname(target.configPath);
5442
- if (!fs5.existsSync(dir)) {
5443
- fs5.mkdirSync(dir, { recursive: true });
7820
+ const dir = path5.dirname(target.configPath);
7821
+ if (!fs6.existsSync(dir)) {
7822
+ fs6.mkdirSync(dir, { recursive: true });
5444
7823
  }
5445
- fs5.writeFileSync(target.configPath, JSON.stringify(config, null, 2), "utf-8");
5446
- return { status: fs5.existsSync(target.configPath) ? "installed" : "created" };
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: fs5.existsSync(path4.dirname(t.configPath))
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 = path4.join(pluginDir, "src");
5542
- if (!fs5.existsSync(srcDir)) {
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 = fs5.readdirSync(srcDir).filter((f) => f.endsWith(".lua"));
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 = fs5.readFileSync(path4.join(srcDir, mainFile), "utf-8");
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 = fs5.readFileSync(path4.join(srcDir, mf), "utf-8");
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
- fs5.writeFileSync(destFile, rbxmx, "utf-8");
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
  }