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 +24 -4
- package/README.md +12 -14
- package/dist/install-plugin.js +58 -13
- package/dist/install-plugin.js.map +1 -1
- package/dist/mcp.js +13 -121
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +4 -4
- package/package.json +5 -3
- package/plugin/Dominus.rbxmx +3674 -0
- package/plugin/src/BridgeConfig.lua +2 -0
- package/plugin/src/ValueCodec.lua +5 -4
- package/plugin/src/WsClient.lua +13 -0
- package/plugin/src/init.server.lua +35 -22
- package/plugin/Dominus.rbxm +0 -0
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
76
|
-
2.
|
|
77
|
-
3.
|
|
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.
|
|
135
|
-
|
|
136
|
-
command allowlist and contains no
|
|
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
|
package/dist/install-plugin.js
CHANGED
|
@@ -1,30 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
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
|
|
47
|
+
return path3.join(localAppData, "Roblox", "Plugins");
|
|
12
48
|
}
|
|
13
49
|
if (process.platform === "darwin") {
|
|
14
|
-
return
|
|
50
|
+
return path3.join(os2.homedir(), "Documents", "Roblox", "Plugins");
|
|
15
51
|
}
|
|
16
|
-
return
|
|
52
|
+
return path3.join(os2.homedir(), ".local", "share", "Roblox", "Plugins");
|
|
17
53
|
}
|
|
18
|
-
var distDirectory =
|
|
19
|
-
var source =
|
|
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 =
|
|
22
|
-
if (!
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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":"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","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
|
|
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.
|
|
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
|
|
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.
|
|
1610
|
+
var VERSION = "2.1.1";
|
|
1719
1611
|
function createDominusMcpServer(bridge) {
|
|
1720
1612
|
const server = new McpServer({
|
|
1721
1613
|
name: "dominus-2",
|