dominus-cli 2.1.0 → 2.1.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.1 (2026-07-13)
4
+
5
+ ### Studio connection reliability
6
+
7
+ - Made pending WebStream connection attempts cancellable so overlapping retries
8
+ cannot strand a waiting coroutine or deadlock future reconnects.
9
+ - Serialized reconnect attempts and reset failed open transports explicitly.
10
+ - Changed the Dominus 2 toolbar button to always force an immediate reconnect
11
+ instead of toggling automatic reconnection off.
12
+
13
+ ### Typed mutations and packaging
14
+
15
+ - Fixed valid Enum values such as `Slate`, `Wood`, `Glass`, and `Ball` being
16
+ rejected by resolving names against the live Enum type's items.
17
+ - Removed the stale binary `.rbxm` build artifact. The package and installer now
18
+ use the single canonical `.rbxmx` plugin build.
19
+ - Corrected documentation to describe the 16-tool MCP catalog and optional Studio
20
+ restart behavior.
21
+
3
22
  ## 2.1.0 (2026-07-12)
4
23
 
5
24
  Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
@@ -8,7 +27,7 @@ Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
8
27
 
9
28
  - Removed the Ink/TUI command from the published binaries. Dominus now relies on
10
29
  the MCP host's model, context, consent UI, and agent runtime.
11
- - Replaced the monolithic legacy catalog with 17 focused tools for connection
30
+ - Replaced the monolithic legacy catalog with 16 focused tools for connection
12
31
  management, inspection, transactional changes, UI, scripts, tests, and docs.
13
32
  - Added strict input schemas, output schemas, `structuredContent`, text fallbacks,
14
33
  `isError`, and accurate read-only/destructive/idempotent/open-world annotations.
@@ -20,7 +39,8 @@ Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
20
39
  ### Secure Studio bridge
21
40
 
22
41
  - Bound the WebSocket bridge to `127.0.0.1` instead of every network interface.
23
- - Added a persistent 256-bit bridge token and six-digit first-use pairing flow.
42
+ - Added a persistent 256-bit bridge token that is provisioned directly into the
43
+ local plugin by `dominus-install-plugin`; users do not complete a pairing flow.
24
44
  - Authenticated relay/controller processes as well as Studio plugin connections.
25
45
  - Added one MiB payload limits, socket rate limits, handshake timeouts, bounded
26
46
  request timeouts, and strict message-envelope validation.
@@ -36,7 +56,7 @@ Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
36
56
  same place ID are now safe and independently selectable.
37
57
  - Relay clients carry their selected connection on each request, so separate MCP
38
58
  processes cannot overwrite each other's active target.
39
- - Added `dominus_status`, `dominus_pair_studio`, and `dominus_select_studio`.
59
+ - Added `dominus_status` and `dominus_select_studio`.
40
60
 
41
61
  ### Studio plugin harness
42
62
 
@@ -66,7 +86,7 @@ Dominus has been rewritten as an MCP-first Roblox Studio engineering server.
66
86
  installer implementation.
67
87
  - The published runtime dependency set is MCP-only; old CLI dependencies are
68
88
  development-only and excluded from the MCP bundle.
69
- - Added transport tests for auth, pairing, relays, duplicate places, wrong-target
89
+ - Added transport tests for auth, credential rejection, relays, duplicate places, wrong-target
70
90
  responses, and disconnect isolation.
71
91
  - Added in-memory MCP conformance tests for the tool catalog, structured outputs,
72
92
  resources, prompts, routing, and error results.
package/README.md CHANGED
@@ -8,12 +8,12 @@ Luau execution.
8
8
  ## What changed
9
9
 
10
10
  - MCP-only product: the old Ink chat CLI is no longer published.
11
- - 17 focused tools with input/output schemas, structured results, and risk hints.
11
+ - 16 focused tools with input/output schemas, structured results, and risk hints.
12
12
  - Stable instance refs survive dots in names, renames, reparenting, and duplicate
13
13
  sibling names for the life of the Studio session.
14
14
  - Script writes require a revision from `studio_read_script`.
15
15
  - Mutation batches and UI replacement are undoable and roll back on failure.
16
- - The loopback Studio bridge requires a persistent token and first-use pairing.
16
+ - The installer provisions a private bridge token into the local Studio plugin.
17
17
  - Multiple Studio windows are selected by unique connection ID, not place ID.
18
18
  - Roblox Creator Docs API lookup is built in.
19
19
 
@@ -39,7 +39,8 @@ Install or update the Studio plugin:
39
39
  dominus-install-plugin
40
40
  ```
41
41
 
42
- Restart Roblox Studio after installing the plugin.
42
+ Roblox Studio normally reloads the local plugin automatically. Restart Studio
43
+ only if the Dominus 2 toolbar button does not appear or reconnect.
43
44
 
44
45
  ## MCP configuration
45
46
 
@@ -62,7 +63,7 @@ For a version-pinned configuration that does not require a global install:
62
63
  "mcpServers": {
63
64
  "dominus": {
64
65
  "command": "npx",
65
- "args": ["-y", "-p", "dominus-cli@2.1.0", "dominus-mcp"]
66
+ "args": ["-y", "-p", "dominus-cli@2.1.1", "dominus-mcp"]
66
67
  }
67
68
  }
68
69
  }
@@ -72,12 +73,9 @@ Restart the MCP client after changing its configuration.
72
73
 
73
74
  ## First connection
74
75
 
75
- 1. Start the MCP client so `dominus-mcp` is running.
76
- 2. Open Roblox Studio. The Dominus 2 plugin connects to `127.0.0.1:18088`.
77
- 3. On first use, Studio Output prints a connection ID and six-digit pairing code.
78
- 4. Ask the MCP agent to call `dominus_status`, then `dominus_pair_studio` with
79
- that connection ID and code.
80
- 5. Pairing is saved. Future Studio sessions authenticate automatically.
76
+ 1. Run `dominus-install-plugin`; restart Roblox Studio only if it does not reload.
77
+ 2. Start the MCP client so `dominus-mcp` is running.
78
+ 3. Open Roblox Studio. The plugin connects and authenticates automatically.
81
79
 
82
80
  If several Studio windows are open, call `dominus_select_studio` with the exact
83
81
  connection ID before editing. Two windows on the same place ID remain distinct.
@@ -101,7 +99,6 @@ successful write response alone.
101
99
  Connection tools:
102
100
 
103
101
  - `dominus_status`
104
- - `dominus_pair_studio`
105
102
  - `dominus_select_studio`
106
103
 
107
104
  Studio inspection:
@@ -131,9 +128,10 @@ Roblox documentation:
131
128
 
132
129
  The MCP transport is stdio. The separate Studio WebSocket bridge binds only to
133
130
  `127.0.0.1`, limits messages to one MiB, rate-limits sockets, times out incomplete
134
- handshakes, and authenticates both Studio plugins and relay controllers. First-use
135
- pairing requires a code visible in Studio. The plugin only accepts an explicit
136
- command allowlist and contains no `loadstring` executor.
131
+ handshakes, and authenticates both Studio plugins and relay controllers. The local
132
+ installer injects a user-specific token into the plugin, so no manual pairing flow
133
+ is needed. The plugin only accepts an explicit command allowlist and contains no
134
+ `loadstring` executor.
137
135
 
138
136
  The bridge token protects against accidental or opportunistic access by unrelated
139
137
  local processes. Like other user-level credentials, it cannot protect against a
@@ -1,30 +1,75 @@
1
1
  #!/usr/bin/env node
2
- import fs from 'fs';
3
- import os from 'os';
4
- import path from 'path';
2
+ import fs3 from 'fs';
3
+ import os2 from 'os';
4
+ import path3 from 'path';
5
5
  import { fileURLToPath } from 'url';
6
+ import * as crypto from 'crypto';
6
7
 
8
+ var DOMINUS_DIR = path3.join(os2.homedir(), ".dominus");
9
+ path3.join(DOMINUS_DIR, "config.json");
10
+ path3.join(DOMINUS_DIR, "dominus.db");
11
+ path3.join(DOMINUS_DIR, "rules.json");
12
+ function ensureDir() {
13
+ if (!fs3.existsSync(DOMINUS_DIR)) {
14
+ fs3.mkdirSync(DOMINUS_DIR, { recursive: true });
15
+ }
16
+ }
17
+ function getDominusDir() {
18
+ ensureDir();
19
+ return DOMINUS_DIR;
20
+ }
21
+
22
+ // src/transport/auth.ts
23
+ var TOKEN_FILE = "bridge-token";
24
+ function loadOrCreateBridgeToken() {
25
+ const tokenPath = path3.join(getDominusDir(), TOKEN_FILE);
26
+ try {
27
+ const existing = fs3.readFileSync(tokenPath, "utf8").trim();
28
+ if (existing.length >= 32) return existing;
29
+ } catch {
30
+ }
31
+ const token = crypto.randomBytes(32).toString("base64url");
32
+ fs3.writeFileSync(tokenPath, `${token}
33
+ `, { encoding: "utf8", mode: 384 });
34
+ try {
35
+ fs3.chmodSync(tokenPath, 384);
36
+ } catch {
37
+ }
38
+ return token;
39
+ }
40
+
41
+ // src/install-plugin.ts
42
+ var TOKEN_PLACEHOLDER = "__DOMINUS_BRIDGE_TOKEN__";
7
43
  function getPluginDirectory() {
8
44
  if (process.platform === "win32") {
9
45
  const localAppData = process.env.LOCALAPPDATA;
10
46
  if (!localAppData) throw new Error("LOCALAPPDATA is not set");
11
- return path.join(localAppData, "Roblox", "Plugins");
47
+ return path3.join(localAppData, "Roblox", "Plugins");
12
48
  }
13
49
  if (process.platform === "darwin") {
14
- return path.join(os.homedir(), "Documents", "Roblox", "Plugins");
50
+ return path3.join(os2.homedir(), "Documents", "Roblox", "Plugins");
15
51
  }
16
- return path.join(os.homedir(), ".local", "share", "Roblox", "Plugins");
52
+ return path3.join(os2.homedir(), ".local", "share", "Roblox", "Plugins");
17
53
  }
18
- var distDirectory = path.dirname(fileURLToPath(import.meta.url));
19
- var source = path.resolve(distDirectory, "..", "plugin", "Dominus.rbxm");
54
+ var distDirectory = path3.dirname(fileURLToPath(import.meta.url));
55
+ var source = path3.resolve(distDirectory, "..", "plugin", "Dominus.rbxmx");
20
56
  var destinationDirectory = getPluginDirectory();
21
- var destination = path.join(destinationDirectory, "Dominus2.rbxm");
22
- if (!fs.existsSync(source)) {
57
+ var destination = path3.join(destinationDirectory, "Dominus2.rbxmx");
58
+ if (!fs3.existsSync(source)) {
23
59
  throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);
24
60
  }
25
- fs.mkdirSync(destinationDirectory, { recursive: true });
26
- fs.copyFileSync(source, destination);
61
+ var template = fs3.readFileSync(source, "utf8");
62
+ if (!template.includes(TOKEN_PLACEHOLDER)) {
63
+ throw new Error("Packaged plugin template is missing its bridge token placeholder");
64
+ }
65
+ var personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());
66
+ fs3.mkdirSync(destinationDirectory, { recursive: true });
67
+ fs3.writeFileSync(destination, personalizedPlugin, "utf8");
68
+ for (const obsoleteName of ["Dominus2.rbxm", "Dominus.rbxm", "Dominus.rbxmx"]) {
69
+ const obsoletePath = path3.join(destinationDirectory, obsoleteName);
70
+ if (obsoletePath !== destination) fs3.rmSync(obsoletePath, { force: true });
71
+ }
27
72
  console.log(`Installed Dominus 2 Studio plugin to ${destination}`);
28
- console.log("Restart Roblox Studio, start dominus-mcp, then approve the pairing code shown in Studio Output.");
73
+ console.log("Restart Roblox Studio. Dominus 2 will authenticate automatically when dominus-mcp starts.");
29
74
  //# sourceMappingURL=install-plugin.js.map
30
75
  //# sourceMappingURL=install-plugin.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/install-plugin.ts"],"names":[],"mappings":";;;;;;AAKA,SAAS,kBAAA,GAA6B;AACpC,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,IAAA,MAAM,YAAA,GAAe,QAAQ,GAAA,CAAI,YAAA;AACjC,IAAA,IAAI,CAAC,YAAA,EAAc,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAC5D,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,YAAA,EAAc,QAAA,EAAU,SAAS,CAAA;AAAA,EACpD;AACA,EAAA,IAAI,OAAA,CAAQ,aAAa,QAAA,EAAU;AACjC,IAAA,OAAO,KAAK,IAAA,CAAK,EAAA,CAAG,SAAQ,EAAG,WAAA,EAAa,UAAU,SAAS,CAAA;AAAA,EACjE;AACA,EAAA,OAAO,IAAA,CAAK,KAAK,EAAA,CAAG,OAAA,IAAW,QAAA,EAAU,OAAA,EAAS,UAAU,SAAS,CAAA;AACvE;AAEA,IAAM,gBAAgB,IAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AACjE,IAAM,SAAS,IAAA,CAAK,OAAA,CAAQ,aAAA,EAAe,IAAA,EAAM,UAAU,cAAc,CAAA;AACzE,IAAM,uBAAuB,kBAAA,EAAmB;AAChD,IAAM,WAAA,GAAc,IAAA,CAAK,IAAA,CAAK,oBAAA,EAAsB,eAAe,CAAA;AAEnE,IAAI,CAAC,EAAA,CAAG,UAAA,CAAW,MAAM,CAAA,EAAG;AAC1B,EAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,8BAAA,CAAgC,CAAA;AACxF;AAEA,EAAA,CAAG,SAAA,CAAU,oBAAA,EAAsB,EAAE,SAAA,EAAW,MAAM,CAAA;AACtD,EAAA,CAAG,YAAA,CAAa,QAAQ,WAAW,CAAA;AACnC,OAAA,CAAQ,GAAA,CAAI,CAAA,qCAAA,EAAwC,WAAW,CAAA,CAAE,CAAA;AACjE,OAAA,CAAQ,IAAI,iGAAiG,CAAA","file":"install-plugin.js","sourcesContent":["import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nfunction getPluginDirectory(): string {\n if (process.platform === 'win32') {\n const localAppData = process.env.LOCALAPPDATA;\n if (!localAppData) throw new Error('LOCALAPPDATA is not set');\n return path.join(localAppData, 'Roblox', 'Plugins');\n }\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Documents', 'Roblox', 'Plugins');\n }\n return path.join(os.homedir(), '.local', 'share', 'Roblox', 'Plugins');\n}\n\nconst distDirectory = path.dirname(fileURLToPath(import.meta.url));\nconst source = path.resolve(distDirectory, '..', 'plugin', 'Dominus.rbxm');\nconst destinationDirectory = getPluginDirectory();\nconst destination = path.join(destinationDirectory, 'Dominus2.rbxm');\n\nif (!fs.existsSync(source)) {\n throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);\n}\n\nfs.mkdirSync(destinationDirectory, { recursive: true });\nfs.copyFileSync(source, destination);\nconsole.log(`Installed Dominus 2 Studio plugin to ${destination}`);\nconsole.log('Restart Roblox Studio, start dominus-mcp, then approve the pairing code shown in Studio Output.');\n"]}
1
+ {"version":3,"sources":["../src/config/config.ts","../src/transport/auth.ts","../src/install-plugin.ts"],"names":["path","os","fs"],"mappings":";;;;;;;AAKA,IAAM,cAAcA,KAAA,CAAK,IAAA,CAAKC,GAAA,CAAG,OAAA,IAAW,UAAU,CAAA;AAClCD,KAAA,CAAK,IAAA,CAAK,WAAA,EAAa,aAAa;AACxCA,KAAA,CAAK,IAAA,CAAK,WAAA,EAAa,YAAY;AAChCA,KAAA,CAAK,IAAA,CAAK,WAAA,EAAa,YAAY;AA6HtD,SAAS,SAAA,GAAkB;AACzB,EAAA,IAAI,CAACE,GAAA,CAAG,UAAA,CAAW,WAAW,CAAA,EAAG;AAC/B,IAAAA,GAAA,CAAG,SAAA,CAAU,WAAA,EAAa,EAAE,SAAA,EAAW,MAAM,CAAA;AAAA,EAC/C;AACF;AAoDO,SAAS,aAAA,GAAwB;AACtC,EAAA,SAAA,EAAU;AACV,EAAA,OAAO,WAAA;AACT;;;AC3LA,IAAM,UAAA,GAAa,cAAA;AAEZ,SAAS,uBAAA,GAAkC;AAChD,EAAA,MAAM,SAAA,GAAYF,KAAAA,CAAK,IAAA,CAAK,aAAA,IAAiB,UAAU,CAAA;AACvD,EAAA,IAAI;AACF,IAAA,MAAM,WAAWE,GAAAA,CAAG,YAAA,CAAa,SAAA,EAAW,MAAM,EAAE,IAAA,EAAK;AACzD,IAAA,IAAI,QAAA,CAAS,MAAA,IAAU,EAAA,EAAI,OAAO,QAAA;AAAA,EACpC,CAAA,CAAA,MAAQ;AAAA,EAER;AAEA,EAAA,MAAM,KAAA,GAAe,MAAA,CAAA,WAAA,CAAY,EAAE,CAAA,CAAE,SAAS,WAAW,CAAA;AACzD,EAAAA,GAAAA,CAAG,aAAA,CAAc,SAAA,EAAW,CAAA,EAAG,KAAK;AAAA,CAAA,EAAM,EAAE,QAAA,EAAU,MAAA,EAAQ,IAAA,EAAM,KAAO,CAAA;AAC3E,EAAA,IAAI;AACF,IAAAA,GAAAA,CAAG,SAAA,CAAU,SAAA,EAAW,GAAK,CAAA;AAAA,EAC/B,CAAA,CAAA,MAAQ;AAAA,EAER;AACA,EAAA,OAAO,KAAA;AACT;;;AClBA,IAAM,iBAAA,GAAoB,0BAAA;AAE1B,SAAS,kBAAA,GAA6B;AACpC,EAAA,IAAI,OAAA,CAAQ,aAAa,OAAA,EAAS;AAChC,IAAA,MAAM,YAAA,GAAe,QAAQ,GAAA,CAAI,YAAA;AACjC,IAAA,IAAI,CAAC,YAAA,EAAc,MAAM,IAAI,MAAM,yBAAyB,CAAA;AAC5D,IAAA,OAAOF,KAAAA,CAAK,IAAA,CAAK,YAAA,EAAc,QAAA,EAAU,SAAS,CAAA;AAAA,EACpD;AACA,EAAA,IAAI,OAAA,CAAQ,aAAa,QAAA,EAAU;AACjC,IAAA,OAAOA,MAAK,IAAA,CAAKC,GAAAA,CAAG,SAAQ,EAAG,WAAA,EAAa,UAAU,SAAS,CAAA;AAAA,EACjE;AACA,EAAA,OAAOD,KAAAA,CAAK,KAAKC,GAAAA,CAAG,OAAA,IAAW,QAAA,EAAU,OAAA,EAAS,UAAU,SAAS,CAAA;AACvE;AAEA,IAAM,gBAAgBD,KAAAA,CAAK,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AACjE,IAAM,SAASA,KAAAA,CAAK,OAAA,CAAQ,aAAA,EAAe,IAAA,EAAM,UAAU,eAAe,CAAA;AAC1E,IAAM,uBAAuB,kBAAA,EAAmB;AAChD,IAAM,WAAA,GAAcA,KAAAA,CAAK,IAAA,CAAK,oBAAA,EAAsB,gBAAgB,CAAA;AAEpE,IAAI,CAACE,GAAAA,CAAG,UAAA,CAAW,MAAM,CAAA,EAAG;AAC1B,EAAA,MAAM,IAAI,KAAA,CAAM,CAAA,6BAAA,EAAgC,MAAM,CAAA,8BAAA,CAAgC,CAAA;AACxF;AAEA,IAAM,QAAA,GAAWA,GAAAA,CAAG,YAAA,CAAa,MAAA,EAAQ,MAAM,CAAA;AAC/C,IAAI,CAAC,QAAA,CAAS,QAAA,CAAS,iBAAiB,CAAA,EAAG;AACzC,EAAA,MAAM,IAAI,MAAM,kEAAkE,CAAA;AACpF;AACA,IAAM,kBAAA,GAAqB,QAAA,CAAS,UAAA,CAAW,iBAAA,EAAmB,yBAAyB,CAAA;AAE3FA,GAAAA,CAAG,SAAA,CAAU,oBAAA,EAAsB,EAAE,SAAA,EAAW,MAAM,CAAA;AACtDA,GAAAA,CAAG,aAAA,CAAc,WAAA,EAAa,kBAAA,EAAoB,MAAM,CAAA;AACxD,KAAA,MAAW,YAAA,IAAgB,CAAC,eAAA,EAAiB,cAAA,EAAgB,eAAe,CAAA,EAAG;AAC7E,EAAA,MAAM,YAAA,GAAeF,KAAAA,CAAK,IAAA,CAAK,oBAAA,EAAsB,YAAY,CAAA;AACjE,EAAA,IAAI,YAAA,KAAiB,aAAaE,GAAAA,CAAG,OAAO,YAAA,EAAc,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA;AAC3E;AACA,OAAA,CAAQ,GAAA,CAAI,CAAA,qCAAA,EAAwC,WAAW,CAAA,CAAE,CAAA;AACjE,OAAA,CAAQ,IAAI,2FAA2F,CAAA","file":"install-plugin.js","sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport type { ConfigAccess } from '../tools/types.js';\n\nconst DOMINUS_DIR = path.join(os.homedir(), '.dominus');\nconst CONFIG_PATH = path.join(DOMINUS_DIR, 'config.json');\nconst DB_PATH = path.join(DOMINUS_DIR, 'dominus.db');\nconst RULES_PATH = path.join(DOMINUS_DIR, 'rules.json');\n\nexport type AIProviderType =\n | 'openai'\n | 'anthropic'\n | 'gemini'\n | 'xai'\n | 'openrouter'\n | 'custom';\n\nexport interface ProviderConfig {\n name: string;\n baseUrl: string;\n envVar: string;\n defaultModel: string;\n}\n\nexport const PROVIDERS: Record<AIProviderType, ProviderConfig> = {\n openai: {\n name: 'OpenAI',\n baseUrl: 'https://api.openai.com/v1',\n envVar: 'OPENAI_API_KEY',\n defaultModel: 'gpt-5.3-codex',\n },\n anthropic: {\n name: 'Anthropic',\n baseUrl: 'https://api.anthropic.com/v1/',\n envVar: 'ANTHROPIC_API_KEY',\n defaultModel: 'claude-sonnet-4-6',\n },\n gemini: {\n name: 'Google Gemini',\n baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai',\n envVar: 'GEMINI_API_KEY',\n defaultModel: 'gemini-3.1-pro',\n },\n xai: {\n name: 'xAI (Grok)',\n baseUrl: 'https://api.x.ai/v1',\n envVar: 'XAI_API_KEY',\n defaultModel: 'grok-4.2',\n },\n openrouter: {\n name: 'OpenRouter',\n baseUrl: 'https://openrouter.ai/api/v1',\n envVar: 'OPENROUTER_API_KEY',\n defaultModel: 'anthropic/claude-sonnet-4-6',\n },\n custom: {\n name: 'Custom',\n baseUrl: '',\n envVar: '',\n defaultModel: '',\n },\n};\n\nexport interface DominusConfig {\n provider: AIProviderType;\n apiKey?: string;\n apiBaseUrl: string;\n model: string;\n port: number;\n maxToolIterations: number;\n maxVerifyRetries: number;\n tokenBudget: number;\n autoIndex: boolean;\n autoLearn: boolean;\n userName: string;\n openCloudApiKey?: string;\n}\n\nconst DEFAULTS: DominusConfig = {\n provider: 'openai',\n apiBaseUrl: '',\n model: '',\n port: 18088,\n maxToolIterations: 25,\n maxVerifyRetries: 3,\n tokenBudget: 128000,\n autoIndex: true,\n autoLearn: true,\n userName: 'Developer',\n};\n\n/**\n * Auto-detect provider from environment variables.\n * Returns the first provider whose env var is set, or null.\n */\nexport function detectProviderFromEnv(): { provider: AIProviderType; apiKey: string } | null {\n const priority: AIProviderType[] = ['openai', 'anthropic', 'gemini', 'xai', 'openrouter'];\n for (const p of priority) {\n const key = process.env[PROVIDERS[p].envVar];\n if (key) return { provider: p, apiKey: key };\n }\n return null;\n}\n\n/**\n * Resolve the effective base URL and model for a provider config.\n */\nexport function resolveProviderSettings(config: DominusConfig): {\n baseUrl: string;\n model: string;\n} {\n const providerInfo = PROVIDERS[config.provider];\n return {\n baseUrl: config.apiBaseUrl || providerInfo?.baseUrl || PROVIDERS.openrouter.baseUrl,\n model: config.model || providerInfo?.defaultModel || 'gpt-4o',\n };\n}\n\nexport interface DominusRules {\n do: string[];\n dont: string[];\n conventions: string[];\n context: string;\n}\n\nconst DEFAULT_RULES: DominusRules = {\n do: [],\n dont: [],\n conventions: [],\n context: '',\n};\n\nfunction ensureDir(): void {\n if (!fs.existsSync(DOMINUS_DIR)) {\n fs.mkdirSync(DOMINUS_DIR, { recursive: true });\n }\n}\n\nfunction readJson<T>(filePath: string, defaults: T): T {\n try {\n if (fs.existsSync(filePath)) {\n const raw = fs.readFileSync(filePath, 'utf-8');\n return { ...defaults, ...JSON.parse(raw) };\n }\n } catch {\n // Corrupted file; reset to defaults\n }\n return { ...defaults };\n}\n\nfunction writeJson(filePath: string, data: unknown): void {\n ensureDir();\n fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');\n}\n\nexport function loadConfig(): DominusConfig {\n ensureDir();\n return readJson(CONFIG_PATH, DEFAULTS);\n}\n\nexport function saveConfig(config: DominusConfig): void {\n writeJson(CONFIG_PATH, config);\n}\n\nexport function getConfigValue<K extends keyof DominusConfig>(key: K): DominusConfig[K] {\n const config = loadConfig();\n return config[key];\n}\n\nexport function setConfigValue<K extends keyof DominusConfig>(\n key: K,\n value: DominusConfig[K],\n): void {\n const config = loadConfig();\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (config as any)[key] = value;\n saveConfig(config);\n}\n\nexport function loadRules(): DominusRules {\n ensureDir();\n return readJson(RULES_PATH, DEFAULT_RULES);\n}\n\nexport function saveRules(rules: DominusRules): void {\n writeJson(RULES_PATH, rules);\n}\n\nexport function getDominusDir(): string {\n ensureDir();\n return DOMINUS_DIR;\n}\n\nexport function getDbPath(): string {\n ensureDir();\n return DB_PATH;\n}\n\nexport function createConfigAccess(): ConfigAccess {\n return {\n get<T>(key: string): T | undefined {\n const config = loadConfig() as unknown as Record<string, unknown>;\n return config[key] as T | undefined;\n },\n set(key: string, value: unknown): void {\n const config = loadConfig() as unknown as Record<string, unknown>;\n config[key] = value;\n saveConfig(config as unknown as DominusConfig);\n },\n getAll(): Record<string, unknown> {\n return loadConfig() as unknown as Record<string, unknown>;\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport * as crypto from 'node:crypto';\nimport { getDominusDir } from '../config/config.js';\n\nconst TOKEN_FILE = 'bridge-token';\n\nexport function loadOrCreateBridgeToken(): string {\n const tokenPath = path.join(getDominusDir(), TOKEN_FILE);\n try {\n const existing = fs.readFileSync(tokenPath, 'utf8').trim();\n if (existing.length >= 32) return existing;\n } catch {\n // First run or an unreadable token is repaired below.\n }\n\n const token = crypto.randomBytes(32).toString('base64url');\n fs.writeFileSync(tokenPath, `${token}\\n`, { encoding: 'utf8', mode: 0o600 });\n try {\n fs.chmodSync(tokenPath, 0o600);\n } catch {\n // Windows ACLs do not map directly to POSIX modes.\n }\n return token;\n}\n\nexport function tokensEqual(actual: unknown, expected: string): boolean {\n if (typeof actual !== 'string') return false;\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return actualBuffer.length === expectedBuffer.length\n && crypto.timingSafeEqual(actualBuffer, expectedBuffer);\n}\n","import fs from 'node:fs';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { loadOrCreateBridgeToken } from './transport/auth.js';\n\nconst TOKEN_PLACEHOLDER = '__DOMINUS_BRIDGE_TOKEN__';\n\nfunction getPluginDirectory(): string {\n if (process.platform === 'win32') {\n const localAppData = process.env.LOCALAPPDATA;\n if (!localAppData) throw new Error('LOCALAPPDATA is not set');\n return path.join(localAppData, 'Roblox', 'Plugins');\n }\n if (process.platform === 'darwin') {\n return path.join(os.homedir(), 'Documents', 'Roblox', 'Plugins');\n }\n return path.join(os.homedir(), '.local', 'share', 'Roblox', 'Plugins');\n}\n\nconst distDirectory = path.dirname(fileURLToPath(import.meta.url));\nconst source = path.resolve(distDirectory, '..', 'plugin', 'Dominus.rbxmx');\nconst destinationDirectory = getPluginDirectory();\nconst destination = path.join(destinationDirectory, 'Dominus2.rbxmx');\n\nif (!fs.existsSync(source)) {\n throw new Error(`Packaged plugin not found at ${source}. Run pnpm build:plugin first.`);\n}\n\nconst template = fs.readFileSync(source, 'utf8');\nif (!template.includes(TOKEN_PLACEHOLDER)) {\n throw new Error('Packaged plugin template is missing its bridge token placeholder');\n}\nconst personalizedPlugin = template.replaceAll(TOKEN_PLACEHOLDER, loadOrCreateBridgeToken());\n\nfs.mkdirSync(destinationDirectory, { recursive: true });\nfs.writeFileSync(destination, personalizedPlugin, 'utf8');\nfor (const obsoleteName of ['Dominus2.rbxm', 'Dominus.rbxm', 'Dominus.rbxmx']) {\n const obsoletePath = path.join(destinationDirectory, obsoleteName);\n if (obsoletePath !== destination) fs.rmSync(obsoletePath, { force: true });\n}\nconsole.log(`Installed Dominus 2 Studio plugin to ${destination}`);\nconsole.log('Restart Roblox Studio. Dominus 2 will authenticate automatically when dominus-mcp starts.');\n"]}
package/dist/mcp.js CHANGED
@@ -8,7 +8,8 @@ import { EventEmitter } from 'events';
8
8
  import { WebSocket, WebSocketServer } from 'ws';
9
9
  import { nanoid } from 'nanoid';
10
10
  import { z } from 'zod';
11
- import { randomUUID, randomBytes, timingSafeEqual, randomInt } from 'crypto';
11
+ import * as crypto from 'crypto';
12
+ import { randomUUID } from 'crypto';
12
13
 
13
14
  var DOMINUS_DIR = path3.join(os.homedir(), ".dominus");
14
15
  var CONFIG_PATH = path3.join(DOMINUS_DIR, "config.json");
@@ -64,7 +65,6 @@ var StudioMsg = {
64
65
  RESPONSE: "studio:response",
65
66
  REFLECTION: "studio:reflection",
66
67
  TEST_RESULTS: "studio:test:results",
67
- PAIRING_REQUIRED: "server:pairing_required",
68
68
  AUTHENTICATED: "server:authenticated",
69
69
  AUTH_REJECTED: "server:auth_rejected"
70
70
  };
@@ -73,9 +73,7 @@ var ControllerMsg = {
73
73
  WELCOME: "controller:welcome",
74
74
  RELAY: "controller:relay",
75
75
  TARGET_STATE: "controller:target_state",
76
- SET_ACTIVE_CONNECTION: "controller:set_active_connection",
77
- LIST_PENDING: "controller:list_pending",
78
- APPROVE_CONNECTION: "controller:approve_connection"
76
+ SET_ACTIVE_CONNECTION: "controller:set_active_connection"
79
77
  };
80
78
  var CliMsg = {
81
79
  V2_GET_TREE: "studio:v2:get_tree",
@@ -126,7 +124,7 @@ function loadOrCreateBridgeToken() {
126
124
  if (existing.length >= 32) return existing;
127
125
  } catch {
128
126
  }
129
- const token = randomBytes(32).toString("base64url");
127
+ const token = crypto.randomBytes(32).toString("base64url");
130
128
  fs3.writeFileSync(tokenPath, `${token}
131
129
  `, { encoding: "utf8", mode: 384 });
132
130
  try {
@@ -139,10 +137,7 @@ function tokensEqual(actual, expected) {
139
137
  if (typeof actual !== "string") return false;
140
138
  const actualBuffer = Buffer.from(actual);
141
139
  const expectedBuffer = Buffer.from(expected);
142
- return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
143
- }
144
- function createPairingCode() {
145
- return randomInt(1e5, 1e6).toString();
140
+ return actualBuffer.length === expectedBuffer.length && crypto.timingSafeEqual(actualBuffer, expectedBuffer);
146
141
  }
147
142
 
148
143
  // src/transport/ws-client.ts
@@ -373,20 +368,6 @@ var DominusClient = class extends EventEmitter {
373
368
  active: connection.connectionId === this.activeConnectionId
374
369
  }));
375
370
  }
376
- async listPendingConnections() {
377
- const result = await this.sendControlRequest(
378
- ControllerMsg.LIST_PENDING,
379
- {}
380
- );
381
- return result.pending;
382
- }
383
- async approveConnection(connectionId, pairingCode) {
384
- const result = await this.sendControlRequest(
385
- ControllerMsg.APPROVE_CONNECTION,
386
- { connectionId, pairingCode }
387
- );
388
- return result.connection;
389
- }
390
371
  getActiveConnectionId() {
391
372
  return this.activeConnectionId;
392
373
  }
@@ -469,14 +450,12 @@ async function isPortInUse(port) {
469
450
  });
470
451
  }
471
452
  var HANDSHAKE_TIMEOUT_MS = 8e3;
472
- var PAIRING_TTL_MS = 5 * 6e4;
473
453
  var RATE_WINDOW_MS = 1e4;
474
454
  var RATE_LIMIT = 250;
475
455
  var DominusServer = class extends EventEmitter {
476
456
  wss = null;
477
457
  studioClients = /* @__PURE__ */ new Map();
478
458
  wsToConnectionId = /* @__PURE__ */ new Map();
479
- pendingConnections = /* @__PURE__ */ new Map();
480
459
  controllers = /* @__PURE__ */ new Set();
481
460
  socketRoles = /* @__PURE__ */ new Map();
482
461
  pendingRequests = /* @__PURE__ */ new Map();
@@ -579,7 +558,10 @@ var DominusServer = class extends EventEmitter {
579
558
  if (tokensEqual(hello.token, this.token)) {
580
559
  this.authorizeStudio(ws, hello, randomUUID());
581
560
  } else {
582
- this.createPendingStudio(ws, hello);
561
+ this.sendSafe(ws, createMessage(StudioMsg.AUTH_REJECTED, {
562
+ error: "Studio plugin credentials are missing or stale. Run dominus-install-plugin and restart Studio."
563
+ }));
564
+ ws.close(1008, "Unauthorized Studio plugin");
583
565
  }
584
566
  return;
585
567
  }
@@ -605,32 +587,6 @@ var DominusServer = class extends EventEmitter {
605
587
  }));
606
588
  ws.close(1008, "Invalid Dominus handshake");
607
589
  }
608
- createPendingStudio(ws, hello) {
609
- const connectionId = randomUUID();
610
- const requestedAt = Date.now();
611
- const pending = {
612
- ws,
613
- pairingCode: createPairingCode(),
614
- hello,
615
- info: {
616
- connectionId,
617
- clientId: hello.clientId,
618
- placeId: hello.placeId ?? 0,
619
- placeName: hello.placeName,
620
- studioVersion: hello.studioVersion,
621
- requestedAt,
622
- expiresAt: requestedAt + PAIRING_TTL_MS
623
- }
624
- };
625
- this.pendingConnections.set(connectionId, pending);
626
- this.socketRoles.set(ws, "pending");
627
- this.sendSafe(ws, createMessage(StudioMsg.PAIRING_REQUIRED, {
628
- connectionId,
629
- pairingCode: pending.pairingCode,
630
- expiresAt: pending.info.expiresAt
631
- }));
632
- this.emit("studio:pairing_required", pending.info);
633
- }
634
590
  authorizeStudio(ws, hello, connectionId) {
635
591
  const connection = {
636
592
  ws,
@@ -641,7 +597,6 @@ var DominusServer = class extends EventEmitter {
641
597
  placeId: hello.placeId ?? 0,
642
598
  placeName: hello.placeName
643
599
  };
644
- this.pendingConnections.delete(connectionId);
645
600
  this.studioClients.set(connectionId, connection);
646
601
  this.wsToConnectionId.set(ws, connectionId);
647
602
  this.socketRoles.set(ws, "studio");
@@ -682,18 +637,6 @@ var DominusServer = class extends EventEmitter {
682
637
  this.sendResponse(controllerWs, msg.id, { success: true, connectionId });
683
638
  return;
684
639
  }
685
- if (msg.type === ControllerMsg.LIST_PENDING) {
686
- this.sendResponse(controllerWs, msg.id, { success: true, pending: this.getPendingInfos() });
687
- return;
688
- }
689
- if (msg.type === ControllerMsg.APPROVE_CONNECTION) {
690
- const payload = msg.payload;
691
- this.approveConnection(String(payload.connectionId ?? ""), String(payload.pairingCode ?? "")).then((connection) => this.sendResponse(controllerWs, msg.id, { success: true, connection })).catch((err) => this.sendResponse(controllerWs, msg.id, {
692
- success: false,
693
- error: err instanceof Error ? err.message : String(err)
694
- }));
695
- return;
696
- }
697
640
  this.sendResponse(controllerWs, msg.id, { error: `Unsupported controller message: ${msg.type}` });
698
641
  }
699
642
  handleStudioMessage(ws, msg) {
@@ -734,12 +677,6 @@ var DominusServer = class extends EventEmitter {
734
677
  }
735
678
  return;
736
679
  }
737
- if (role === "pending") {
738
- for (const [id, pending] of this.pendingConnections) {
739
- if (pending.ws === ws) this.pendingConnections.delete(id);
740
- }
741
- return;
742
- }
743
680
  const connectionId = this.wsToConnectionId.get(ws);
744
681
  if (!connectionId) return;
745
682
  const connection = this.studioClients.get(connectionId);
@@ -782,16 +719,6 @@ var DominusServer = class extends EventEmitter {
782
719
  if (this.activeConnectionId) return `Studio connection ${this.activeConnectionId} is unavailable`;
783
720
  return "Multiple Studio sessions are connected. Call dominus_select_studio with a connectionId.";
784
721
  }
785
- getPendingInfos() {
786
- const now = Date.now();
787
- for (const [id, pending] of this.pendingConnections) {
788
- if (pending.info.expiresAt <= now || pending.ws.readyState !== WebSocket.OPEN) {
789
- this.pendingConnections.delete(id);
790
- pending.ws.close(1008, "Pairing expired");
791
- }
792
- }
793
- return [...this.pendingConnections.values()].map(({ info }) => ({ ...info }));
794
- }
795
722
  toPlaceInfo(connection) {
796
723
  return {
797
724
  placeId: connection.placeId ?? 0,
@@ -858,20 +785,6 @@ var DominusServer = class extends EventEmitter {
858
785
  listConnections() {
859
786
  return [...this.studioClients.values()].filter(({ ws }) => ws.readyState === WebSocket.OPEN).map((connection) => this.toPlaceInfo(connection));
860
787
  }
861
- async listPendingConnections() {
862
- return this.getPendingInfos();
863
- }
864
- async approveConnection(connectionId, pairingCode) {
865
- const pending = this.pendingConnections.get(connectionId);
866
- if (!pending || pending.info.expiresAt <= Date.now()) {
867
- throw new Error("Pairing request not found or expired");
868
- }
869
- if (pending.pairingCode !== pairingCode) {
870
- throw new Error("Pairing code is incorrect");
871
- }
872
- const connection = this.authorizeStudio(pending.ws, pending.hello, connectionId);
873
- return this.toPlaceInfo(connection);
874
- }
875
788
  getActiveConnectionId() {
876
789
  return this.activeConnectionId;
877
790
  }
@@ -909,7 +822,6 @@ var DominusServer = class extends EventEmitter {
909
822
  this.relayRequests.clear();
910
823
  for (const ws of this.socketRoles.keys()) ws.close(1001, "Dominus bridge shutdown");
911
824
  this.studioClients.clear();
912
- this.pendingConnections.clear();
913
825
  this.controllers.clear();
914
826
  this.socketRoles.clear();
915
827
  this.wsToConnectionId.clear();
@@ -972,8 +884,7 @@ function registerDominusResources(server, bridge) {
972
884
  text: JSON.stringify({
973
885
  protocolVersion: 2,
974
886
  activeConnectionId: bridge.getActiveConnectionId(),
975
- connections: bridge.listConnections(),
976
- pendingConnections: await bridge.listPendingConnections()
887
+ connections: bridge.listConnections()
977
888
  }, null, 2)
978
889
  }]
979
890
  }));
@@ -1038,41 +949,22 @@ function ref(value) {
1038
949
  function registerConnectionTools(server, bridge) {
1039
950
  server.registerTool("dominus_status", {
1040
951
  title: "Dominus Studio Status",
1041
- description: "List authenticated and pending Roblox Studio sessions, the active target, and bridge health. Call this before Studio work.",
952
+ description: "List authenticated Roblox Studio sessions, the active target, and bridge health. Call this before Studio work.",
1042
953
  inputSchema: {},
1043
954
  outputSchema: toolOutputSchema,
1044
955
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
1045
956
  }, async () => {
1046
957
  try {
1047
- const pending = await bridge.listPendingConnections();
1048
958
  return success({
1049
959
  protocolVersion: 2,
1050
960
  port: bridge.getPort(),
1051
961
  activeConnectionId: bridge.getActiveConnectionId(),
1052
- connections: bridge.listConnections(),
1053
- pendingConnections: pending
962
+ connections: bridge.listConnections()
1054
963
  });
1055
964
  } catch (err) {
1056
965
  return failure(err);
1057
966
  }
1058
967
  });
1059
- server.registerTool("dominus_pair_studio", {
1060
- title: "Pair Roblox Studio",
1061
- description: "Approve a first-time Studio plugin connection using the connection ID and six-digit code printed in Roblox Studio Output.",
1062
- inputSchema: {
1063
- connectionId: z.string().uuid(),
1064
- pairingCode: z.string().regex(/^\d{6}$/)
1065
- },
1066
- outputSchema: toolOutputSchema,
1067
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }
1068
- }, async ({ connectionId, pairingCode }) => {
1069
- try {
1070
- const connection = await bridge.approveConnection(connectionId, pairingCode);
1071
- return success({ connection });
1072
- } catch (err) {
1073
- return failure(err);
1074
- }
1075
- });
1076
968
  server.registerTool("dominus_select_studio", {
1077
969
  title: "Select Studio Session",
1078
970
  description: "Choose the exact Studio connection targeted by subsequent tools. Uses connectionId, not placeId, so duplicate windows are safe.",
@@ -1715,7 +1607,7 @@ function registerStudioTools(server, bridge) {
1715
1607
  }
1716
1608
 
1717
1609
  // src/mcp/server.ts
1718
- var VERSION = "2.1.0";
1610
+ var VERSION = "2.1.1";
1719
1611
  function createDominusMcpServer(bridge) {
1720
1612
  const server = new McpServer({
1721
1613
  name: "dominus-2",