dominus-cli 2.0.0 → 2.1.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/CHANGELOG.md +175 -0
- package/README.md +164 -248
- package/dist/install-plugin.js +30 -0
- package/dist/install-plugin.js.map +1 -0
- package/dist/mcp.js +1477 -4739
- package/dist/mcp.js.map +1 -1
- package/docs/DOMINUS_2_PLAN.md +75 -0
- package/package.json +24 -25
- package/plugin/Dominus.rbxm +0 -0
- package/plugin/src/CommandRouter.lua +94 -0
- package/plugin/src/Explorer.lua +530 -373
- package/plugin/src/InstanceRegistry.lua +122 -0
- package/plugin/src/Mutation.lua +135 -0
- package/plugin/src/Properties.lua +859 -785
- package/plugin/src/Protocol.lua +30 -30
- package/plugin/src/Reflection.lua +91 -59
- package/plugin/src/TestRunner.lua +69 -141
- package/plugin/src/UIBuilder.lua +857 -727
- package/plugin/src/ValueCodec.lua +230 -0
- package/plugin/src/Watcher.lua +85 -85
- package/plugin/src/WsClient.lua +153 -81
- package/plugin/src/init.server.lua +175 -362
- package/dist/index.js +0 -7998
- package/dist/index.js.map +0 -1
- package/plugin/src/Executor.lua +0 -117
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Dominus 2 Rewrite Plan
|
|
2
|
+
|
|
3
|
+
## Product direction
|
|
4
|
+
|
|
5
|
+
Dominus 2 is a Roblox Studio MCP server, not a standalone AI chat CLI. The MCP
|
|
6
|
+
host supplies the model and consent UI. Dominus supplies a small, strongly typed
|
|
7
|
+
set of Studio inspection, mutation, documentation, and verification tools.
|
|
8
|
+
|
|
9
|
+
## July 2026 compatibility target
|
|
10
|
+
|
|
11
|
+
- Target the stable MCP 2025-11-25 protocol through the official TypeScript SDK.
|
|
12
|
+
- Use `registerTool`, strict input schemas, output schemas, `structuredContent`,
|
|
13
|
+
text fallbacks, `isError`, and accurate tool risk annotations.
|
|
14
|
+
- Keep stdio as the MCP transport. It limits the MCP server itself to the process
|
|
15
|
+
that launched it and avoids exposing another localhost HTTP endpoint.
|
|
16
|
+
- Do not depend on legacy sampling, roots, or MCP logging. They are deprecated in
|
|
17
|
+
the 2026-07-28 release candidate.
|
|
18
|
+
- Do not adopt the legacy experimental Tasks API. Long Studio calls remain normal
|
|
19
|
+
cancellable tool calls until the July Tasks extension is final and broadly
|
|
20
|
+
supported by clients and the TypeScript SDK.
|
|
21
|
+
|
|
22
|
+
Official references:
|
|
23
|
+
|
|
24
|
+
- https://modelcontextprotocol.io/specification/2025-11-25/server/tools
|
|
25
|
+
- https://ts.sdk.modelcontextprotocol.io/server
|
|
26
|
+
- https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices
|
|
27
|
+
- https://blog.modelcontextprotocol.io/posts/2026-07-28-release-candidate/
|
|
28
|
+
|
|
29
|
+
## Architecture
|
|
30
|
+
|
|
31
|
+
1. MCP host starts `dominus-mcp` over stdio.
|
|
32
|
+
2. Dominus owns or securely relays to one loopback WebSocket bridge on port 18088.
|
|
33
|
+
3. The Studio plugin authenticates with a persistent random bridge token. First
|
|
34
|
+
use requires a six-digit pairing code displayed inside Studio.
|
|
35
|
+
4. Every Studio window receives a unique connection ID. MCP callers select that
|
|
36
|
+
ID, so two windows editing the same place cannot overwrite each other's target.
|
|
37
|
+
5. Studio instances are returned as session-scoped instance references with path
|
|
38
|
+
segments for display and fallback. Dot-separated names are not identifiers.
|
|
39
|
+
6. Mutations run through a command allowlist and ChangeHistoryService recordings.
|
|
40
|
+
Failed batches are cancelled and automatically undone.
|
|
41
|
+
7. Script writes require the revision hash returned by the read operation.
|
|
42
|
+
8. There is no arbitrary Luau execution tool.
|
|
43
|
+
|
|
44
|
+
## MCP tool surface
|
|
45
|
+
|
|
46
|
+
- `dominus_status`, `dominus_pair_studio`, `dominus_select_studio`
|
|
47
|
+
- `studio_get_tree`, `studio_inspect`, `studio_get_selection`
|
|
48
|
+
- `studio_read_script`, `studio_update_script`
|
|
49
|
+
- `studio_apply`, `studio_delete_instances`
|
|
50
|
+
- `studio_build_ui`, `studio_snapshot_ui`
|
|
51
|
+
- `studio_run_test`, `studio_get_output`
|
|
52
|
+
- `roblox_search_api`, `roblox_get_api`
|
|
53
|
+
|
|
54
|
+
The intended agent loop is inspect, plan, apply, read back, test. Tool descriptions
|
|
55
|
+
and the server prompt make that workflow explicit without embedding another LLM
|
|
56
|
+
inside the MCP server.
|
|
57
|
+
|
|
58
|
+
## Safety limits
|
|
59
|
+
|
|
60
|
+
- Loopback-only listener and authenticated controller/plugin handshakes.
|
|
61
|
+
- One MiB WebSocket payload limit, handshake timeout, request timeout, and bounded
|
|
62
|
+
tool arrays/depth/source lengths.
|
|
63
|
+
- Dedicated mutation tools only; no `loadstring` or generic `run_code` path.
|
|
64
|
+
- Explicit destructive tool for deletion.
|
|
65
|
+
- Atomic UI replacement: build and validate off-tree before replacing the live UI.
|
|
66
|
+
- Output watcher filters every Dominus-prefixed message to prevent feedback loops.
|
|
67
|
+
|
|
68
|
+
## Verification
|
|
69
|
+
|
|
70
|
+
- Transport tests cover authentication, pairing, wrong-target responses,
|
|
71
|
+
disconnect isolation, duplicate place sessions, and payload rejection.
|
|
72
|
+
- Protocol and MCP tests cover schema validation and structured result shapes.
|
|
73
|
+
- Static plugin checks plus `rojo build` verify the packaged plugin model.
|
|
74
|
+
- Final gates: TypeScript typecheck, targeted Vitest, full Vitest, build, and
|
|
75
|
+
`npm pack --dry-run`.
|
package/package.json
CHANGED
|
@@ -1,40 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dominus-cli",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Dominus 2: a secure, MCP-native Roblox Studio engineering agent bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dominus": "./dist/
|
|
8
|
-
"dominus-
|
|
7
|
+
"dominus-mcp": "./dist/mcp.js",
|
|
8
|
+
"dominus-install-plugin": "./dist/install-plugin.js"
|
|
9
9
|
},
|
|
10
|
-
"main": "./dist/
|
|
10
|
+
"main": "./dist/mcp.js",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"dev": "tsx src/index.
|
|
13
|
-
"dev:mcp": "
|
|
12
|
+
"dev": "tsx src/mcp/index.ts",
|
|
13
|
+
"dev:mcp": "tsx src/mcp/index.ts",
|
|
14
14
|
"build": "tsup",
|
|
15
15
|
"build:plugin": "rojo build plugin/default.project.json -o plugin/Dominus.rbxm",
|
|
16
|
-
"start": "node dist/
|
|
16
|
+
"start": "node dist/mcp.js",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:watch": "vitest",
|
|
19
19
|
"test:coverage": "vitest run --coverage",
|
|
20
20
|
"lint": "prettier --check .",
|
|
21
21
|
"format": "prettier --write .",
|
|
22
22
|
"typecheck": "tsc --noEmit",
|
|
23
|
-
"prepublishOnly": "pnpm build",
|
|
24
|
-
"install-plugin": "
|
|
23
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build && pnpm build:plugin",
|
|
24
|
+
"install-plugin": "node dist/install-plugin.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
28
|
-
"chalk": "^5.4.1",
|
|
29
|
-
"commander": "^13.1.0",
|
|
30
|
-
"figures": "^6.1.0",
|
|
31
|
-
"ink": "^5.1.0",
|
|
32
|
-
"ink-spinner": "^5.0.0",
|
|
33
|
-
"ink-text-input": "^6.0.0",
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
34
28
|
"nanoid": "^5.1.0",
|
|
35
|
-
"openai": "^4.77.0",
|
|
36
|
-
"react": "^18.3.1",
|
|
37
|
-
"sql.js": "^1.14.0",
|
|
38
29
|
"ws": "^8.18.0",
|
|
39
30
|
"zod": "^3.24.0"
|
|
40
31
|
},
|
|
@@ -43,8 +34,17 @@
|
|
|
43
34
|
"@types/react": "^18.3.0",
|
|
44
35
|
"@types/ws": "^8.5.0",
|
|
45
36
|
"@vitest/coverage-v8": "^2.1.0",
|
|
37
|
+
"chalk": "^5.4.1",
|
|
38
|
+
"commander": "^13.1.0",
|
|
39
|
+
"figures": "^6.1.0",
|
|
40
|
+
"ink": "^5.1.0",
|
|
41
|
+
"ink-spinner": "^5.0.0",
|
|
42
|
+
"ink-text-input": "^6.0.0",
|
|
46
43
|
"nodemon": "^3.1.14",
|
|
44
|
+
"openai": "^4.77.0",
|
|
47
45
|
"prettier": "^3.4.0",
|
|
46
|
+
"react": "^18.3.1",
|
|
47
|
+
"sql.js": "^1.14.0",
|
|
48
48
|
"tsup": "^8.3.0",
|
|
49
49
|
"tsx": "^4.19.0",
|
|
50
50
|
"typescript": "^5.7.0",
|
|
@@ -55,19 +55,18 @@
|
|
|
55
55
|
},
|
|
56
56
|
"files": [
|
|
57
57
|
"dist",
|
|
58
|
-
"plugin"
|
|
58
|
+
"plugin",
|
|
59
|
+
"CHANGELOG.md",
|
|
60
|
+
"docs/DOMINUS_2_PLAN.md"
|
|
59
61
|
],
|
|
60
62
|
"keywords": [
|
|
61
63
|
"roblox",
|
|
62
64
|
"roblox-studio",
|
|
63
|
-
"
|
|
64
|
-
"tui",
|
|
65
|
-
"ai",
|
|
65
|
+
"mcp-server",
|
|
66
66
|
"agent",
|
|
67
67
|
"luau",
|
|
68
68
|
"dominus",
|
|
69
69
|
"websocket",
|
|
70
|
-
"openrouter",
|
|
71
70
|
"mcp",
|
|
72
71
|
"model-context-protocol"
|
|
73
72
|
],
|
package/plugin/Dominus.rbxm
CHANGED
|
Binary file
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
local Explorer = require(script.Parent.Explorer)
|
|
2
|
+
local InstanceRegistry = require(script.Parent.InstanceRegistry)
|
|
3
|
+
local Mutation = require(script.Parent.Mutation)
|
|
4
|
+
local Properties = require(script.Parent.Properties)
|
|
5
|
+
local Reflection = require(script.Parent.Reflection)
|
|
6
|
+
local TestRunner = require(script.Parent.TestRunner)
|
|
7
|
+
local UIBuilder = require(script.Parent.UIBuilder)
|
|
8
|
+
local Watcher = require(script.Parent.Watcher)
|
|
9
|
+
|
|
10
|
+
local CommandRouter = {}
|
|
11
|
+
|
|
12
|
+
local handlers = {
|
|
13
|
+
["studio:v2:get_tree"] = function(payload)
|
|
14
|
+
return Explorer.getTreeV2(payload)
|
|
15
|
+
end,
|
|
16
|
+
["studio:v2:inspect"] = function(payload)
|
|
17
|
+
return Properties.inspectV2(payload)
|
|
18
|
+
end,
|
|
19
|
+
["studio:v2:get_selection"] = function()
|
|
20
|
+
local Selection = game:GetService("Selection")
|
|
21
|
+
local selected = {}
|
|
22
|
+
for _, instance in Selection:Get() do
|
|
23
|
+
table.insert(selected, {
|
|
24
|
+
name = instance.Name,
|
|
25
|
+
className = instance.ClassName,
|
|
26
|
+
ref = InstanceRegistry.toRef(instance),
|
|
27
|
+
})
|
|
28
|
+
end
|
|
29
|
+
return { success = true, selection = selected }
|
|
30
|
+
end,
|
|
31
|
+
["studio:v2:read_script"] = function(payload)
|
|
32
|
+
return Explorer.readScriptV2(payload)
|
|
33
|
+
end,
|
|
34
|
+
["studio:v2:update_script"] = function(payload)
|
|
35
|
+
return Explorer.updateScriptV2(payload)
|
|
36
|
+
end,
|
|
37
|
+
["studio:v2:apply"] = function(payload)
|
|
38
|
+
return Mutation.apply(payload)
|
|
39
|
+
end,
|
|
40
|
+
["studio:v2:delete"] = function(payload)
|
|
41
|
+
return Mutation.delete(payload)
|
|
42
|
+
end,
|
|
43
|
+
["studio:v2:build_ui"] = function(payload)
|
|
44
|
+
return UIBuilder.buildV2(payload)
|
|
45
|
+
end,
|
|
46
|
+
["studio:v2:snapshot_ui"] = function(payload)
|
|
47
|
+
return UIBuilder.snapshotV2(payload)
|
|
48
|
+
end,
|
|
49
|
+
["studio:v2:run_test"] = function(payload)
|
|
50
|
+
return TestRunner.executeV2(payload)
|
|
51
|
+
end,
|
|
52
|
+
["studio:v2:get_output"] = function(payload)
|
|
53
|
+
local result = Watcher.getRecentOutput(math.clamp(payload.limit or 100, 1, 500), payload.level)
|
|
54
|
+
result.success = true
|
|
55
|
+
return result
|
|
56
|
+
end,
|
|
57
|
+
["studio:v2:get_reflection"] = function(payload)
|
|
58
|
+
if type(payload.className) ~= "string" then
|
|
59
|
+
return { success = false, error = "className must be a string" }
|
|
60
|
+
end
|
|
61
|
+
return Reflection.getClassInfo(payload.className)
|
|
62
|
+
end,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function CommandRouter.handle(message, sendResponse)
|
|
66
|
+
if type(message) ~= "table" or type(message.id) ~= "string" or #message.id > 128 then
|
|
67
|
+
return false, "Invalid Dominus request envelope"
|
|
68
|
+
end
|
|
69
|
+
if type(message.type) ~= "string" or type(message.payload) ~= "table" then
|
|
70
|
+
return false, "Invalid Dominus request type or payload"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
local handler = handlers[message.type]
|
|
74
|
+
if not handler then
|
|
75
|
+
sendResponse(message.id, {
|
|
76
|
+
success = false,
|
|
77
|
+
error = "Command is not allowed by the Dominus 2 plugin: " .. message.type,
|
|
78
|
+
})
|
|
79
|
+
return true
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
task.spawn(function()
|
|
83
|
+
local ok, result = pcall(handler, message.payload)
|
|
84
|
+
if not ok then
|
|
85
|
+
result = { success = false, error = "Command failed: " .. tostring(result) }
|
|
86
|
+
elseif type(result) ~= "table" then
|
|
87
|
+
result = { success = false, error = "Command returned an invalid result" }
|
|
88
|
+
end
|
|
89
|
+
sendResponse(message.id, result)
|
|
90
|
+
end)
|
|
91
|
+
return true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return CommandRouter
|