dominus-cli 0.6.0 → 2.0.0

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