dominus-cli 0.6.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 +1576 -1346
- 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 -725
- 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 -5796
- package/dist/index.js.map +0 -1
- package/plugin/src/Executor.lua +0 -117
|
@@ -1,412 +1,225 @@
|
|
|
1
|
-
|
|
2
|
-
Dominus - Roblox Studio Plugin
|
|
3
|
-
AI-powered development agent bridge via WebSocket
|
|
1
|
+
local HttpService = game:GetService("HttpService")
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
enabling real-time bidirectional communication for AI-assisted development.
|
|
7
|
-
]]
|
|
8
|
-
|
|
9
|
-
local toolbar = plugin:CreateToolbar("Dominus")
|
|
10
|
-
local connectButton = toolbar:CreateButton("Connect", "Connect to Dominus CLI agent", "rbxassetid://12974519994")
|
|
11
|
-
|
|
12
|
-
local WsClient = require(script.WsClient)
|
|
3
|
+
local CommandRouter = require(script.CommandRouter)
|
|
13
4
|
local Protocol = require(script.Protocol)
|
|
14
|
-
local Explorer = require(script.Explorer)
|
|
15
|
-
local Executor = require(script.Executor)
|
|
16
5
|
local Watcher = require(script.Watcher)
|
|
17
|
-
local
|
|
18
|
-
|
|
19
|
-
local
|
|
20
|
-
local
|
|
6
|
+
local WsClient = require(script.WsClient)
|
|
7
|
+
|
|
8
|
+
local PROTOCOL_VERSION = 2
|
|
9
|
+
local DEFAULT_PORT = 18088
|
|
10
|
+
local MAX_MESSAGE_BYTES = 1024 * 1024
|
|
11
|
+
|
|
12
|
+
local toolbar = plugin:CreateToolbar("Dominus 2")
|
|
13
|
+
local connectButton = toolbar:CreateButton(
|
|
14
|
+
"Dominus 2",
|
|
15
|
+
"Connect this Studio session to the Dominus 2 MCP server",
|
|
16
|
+
"rbxassetid://12974519994"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
local authenticated = false
|
|
20
|
+
local transportOpen = false
|
|
21
|
+
local shouldReconnect = true
|
|
22
|
+
local reconnectScheduled = false
|
|
23
|
+
local reconnectDelay = 1
|
|
24
|
+
local shuttingDown = false
|
|
25
|
+
|
|
26
|
+
local function getSetting(name, default)
|
|
27
|
+
local ok, value = pcall(function()
|
|
28
|
+
return plugin:GetSetting(name)
|
|
29
|
+
end)
|
|
30
|
+
if ok and value ~= nil then
|
|
31
|
+
return value
|
|
32
|
+
end
|
|
33
|
+
return default
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
local clientId = getSetting("Dominus2ClientId", nil)
|
|
37
|
+
if type(clientId) ~= "string" then
|
|
38
|
+
clientId = HttpService:GenerateGUID(false)
|
|
39
|
+
pcall(function()
|
|
40
|
+
plugin:SetSetting("Dominus2ClientId", clientId)
|
|
41
|
+
end)
|
|
42
|
+
end
|
|
43
|
+
local bridgeToken = getSetting("Dominus2BridgeToken", nil)
|
|
44
|
+
local port = tonumber(getSetting("Dominus2Port", DEFAULT_PORT)) or DEFAULT_PORT
|
|
21
45
|
|
|
22
|
-
local
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
local
|
|
46
|
+
local function log(message)
|
|
47
|
+
print("[Dominus 2] " .. message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
local function sendEnvelope(message)
|
|
51
|
+
local encodedOk, encoded = pcall(Protocol.encode, message)
|
|
52
|
+
if not encodedOk then
|
|
53
|
+
return false, tostring(encoded)
|
|
54
|
+
end
|
|
55
|
+
if #encoded > MAX_MESSAGE_BYTES then
|
|
56
|
+
return false, "Message exceeds one MiB"
|
|
57
|
+
end
|
|
58
|
+
return WsClient.send(encoded)
|
|
59
|
+
end
|
|
27
60
|
|
|
28
|
-
local function
|
|
29
|
-
|
|
61
|
+
local function sendResponse(id, payload)
|
|
62
|
+
return sendEnvelope({ id = id, type = "studio:response", payload = payload, ts = os.time() })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
local scheduleReconnect
|
|
66
|
+
|
|
67
|
+
local function stopSession()
|
|
68
|
+
authenticated = false
|
|
69
|
+
transportOpen = false
|
|
70
|
+
Watcher.stop()
|
|
71
|
+
connectButton:SetActive(false)
|
|
30
72
|
end
|
|
31
73
|
|
|
32
74
|
local function handleMessage(rawMessage)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
75
|
+
if #rawMessage > MAX_MESSAGE_BYTES then
|
|
76
|
+
warn("[Dominus 2] Rejected an oversized server message")
|
|
77
|
+
WsClient.disconnect()
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
local decodedOk, message = pcall(Protocol.decode, rawMessage)
|
|
81
|
+
if not decodedOk or type(message) ~= "table" then
|
|
82
|
+
warn("[Dominus 2] Rejected an invalid server message")
|
|
36
83
|
return
|
|
37
84
|
end
|
|
38
85
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
86
|
+
if message.type == "server:pairing_required" then
|
|
87
|
+
local payload = message.payload or {}
|
|
88
|
+
log(
|
|
89
|
+
"Pairing required. In your MCP client call dominus_pair_studio with connectionId "
|
|
90
|
+
.. tostring(payload.connectionId)
|
|
91
|
+
.. " and code "
|
|
92
|
+
.. tostring(payload.pairingCode)
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
end
|
|
42
96
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
payload = { tree = tree },
|
|
50
|
-
ts = os.clock(),
|
|
51
|
-
}))
|
|
52
|
-
elseif msgType == "cli:get:script" then
|
|
53
|
-
local result = Explorer.getScriptSource(payload.path)
|
|
54
|
-
WsClient.send(Protocol.encode({
|
|
55
|
-
id = msgId,
|
|
56
|
-
type = "studio:response",
|
|
57
|
-
payload = result,
|
|
58
|
-
ts = os.clock(),
|
|
59
|
-
}))
|
|
60
|
-
elseif msgType == "cli:set:script" then
|
|
61
|
-
local result = Explorer.setScriptSource(payload.path, payload.source)
|
|
62
|
-
WsClient.send(Protocol.encode({
|
|
63
|
-
id = msgId,
|
|
64
|
-
type = "studio:response",
|
|
65
|
-
payload = result,
|
|
66
|
-
ts = os.clock(),
|
|
67
|
-
}))
|
|
68
|
-
elseif msgType == "cli:run" then
|
|
69
|
-
local result = Executor.run(payload.code)
|
|
70
|
-
WsClient.send(Protocol.encode({
|
|
71
|
-
id = msgId,
|
|
72
|
-
type = "studio:response",
|
|
73
|
-
payload = result,
|
|
74
|
-
ts = os.clock(),
|
|
75
|
-
}))
|
|
76
|
-
elseif msgType == "cli:get:properties" then
|
|
77
|
-
local compact = payload.compact
|
|
78
|
-
if compact == nil then
|
|
79
|
-
compact = true
|
|
97
|
+
if message.type == "server:authenticated" then
|
|
98
|
+
local payload = message.payload or {}
|
|
99
|
+
if payload.protocolVersion ~= PROTOCOL_VERSION or type(payload.connectionId) ~= "string" then
|
|
100
|
+
warn("[Dominus 2] Server returned an invalid authentication response")
|
|
101
|
+
WsClient.disconnect()
|
|
102
|
+
return
|
|
80
103
|
end
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
ts = os.clock(),
|
|
87
|
-
}))
|
|
88
|
-
elseif msgType == "cli:get:descendants:properties" then
|
|
89
|
-
local compact = payload.compact
|
|
90
|
-
if compact == nil then
|
|
91
|
-
compact = true
|
|
92
|
-
end -- default to compact
|
|
93
|
-
local result = Properties.getDescendants(payload.path, compact)
|
|
94
|
-
WsClient.send(Protocol.encode({
|
|
95
|
-
id = msgId,
|
|
96
|
-
type = "studio:response",
|
|
97
|
-
payload = result,
|
|
98
|
-
ts = os.clock(),
|
|
99
|
-
}))
|
|
100
|
-
elseif msgType == "cli:set:properties" then
|
|
101
|
-
local result = Properties.set(payload.path, payload.properties)
|
|
102
|
-
WsClient.send(Protocol.encode({
|
|
103
|
-
id = msgId,
|
|
104
|
-
type = "studio:response",
|
|
105
|
-
payload = result,
|
|
106
|
-
ts = os.clock(),
|
|
107
|
-
}))
|
|
108
|
-
elseif msgType == "cli:insert" then
|
|
109
|
-
local result = Explorer.insertInstance(payload.className, payload.parentPath, payload.name, payload.properties)
|
|
110
|
-
WsClient.send(Protocol.encode({
|
|
111
|
-
id = msgId,
|
|
112
|
-
type = "studio:response",
|
|
113
|
-
payload = result,
|
|
114
|
-
ts = os.clock(),
|
|
115
|
-
}))
|
|
116
|
-
elseif msgType == "cli:delete" then
|
|
117
|
-
local result = Explorer.deleteInstance(payload.path)
|
|
118
|
-
WsClient.send(Protocol.encode({
|
|
119
|
-
id = msgId,
|
|
120
|
-
type = "studio:response",
|
|
121
|
-
payload = result,
|
|
122
|
-
ts = os.clock(),
|
|
123
|
-
}))
|
|
124
|
-
elseif msgType == "cli:get:selection" then
|
|
125
|
-
local Selection = game:GetService("Selection")
|
|
126
|
-
local selected = Selection:Get()
|
|
127
|
-
local paths = {}
|
|
128
|
-
local classNames = {}
|
|
129
|
-
for _, obj in selected do
|
|
130
|
-
table.insert(paths, Explorer.getPath(obj))
|
|
131
|
-
table.insert(classNames, obj.ClassName)
|
|
104
|
+
if type(payload.token) == "string" then
|
|
105
|
+
bridgeToken = payload.token
|
|
106
|
+
pcall(function()
|
|
107
|
+
plugin:SetSetting("Dominus2BridgeToken", bridgeToken)
|
|
108
|
+
end)
|
|
132
109
|
end
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
elseif msgType == "cli:search:scripts" then
|
|
140
|
-
local result = Explorer.searchScripts(payload.query, payload.maxResults or 20)
|
|
141
|
-
WsClient.send(Protocol.encode({
|
|
142
|
-
id = msgId,
|
|
143
|
-
type = "studio:response",
|
|
144
|
-
payload = result,
|
|
145
|
-
ts = os.clock(),
|
|
146
|
-
}))
|
|
147
|
-
elseif msgType == "cli:get:output" then
|
|
148
|
-
local result = Watcher.getRecentOutput(payload.limit or 50, payload.level)
|
|
149
|
-
WsClient.send(Protocol.encode({
|
|
150
|
-
id = msgId,
|
|
151
|
-
type = "studio:response",
|
|
152
|
-
payload = result,
|
|
153
|
-
ts = os.clock(),
|
|
154
|
-
}))
|
|
155
|
-
elseif msgType == "cli:run:tests" then
|
|
156
|
-
local result = TestRunner.run(payload.testName)
|
|
157
|
-
WsClient.send(Protocol.encode({
|
|
158
|
-
id = msgId,
|
|
159
|
-
type = "studio:response",
|
|
160
|
-
payload = result,
|
|
161
|
-
ts = os.clock(),
|
|
162
|
-
}))
|
|
163
|
-
elseif msgType == "cli:test:run" then
|
|
164
|
-
-- ExecuteRunModeAsync yields until EndTest is called, so run in a thread
|
|
165
|
-
task.spawn(function()
|
|
166
|
-
local result = TestRunner.executeRunMode(payload.args)
|
|
167
|
-
WsClient.send(Protocol.encode({
|
|
168
|
-
id = msgId,
|
|
169
|
-
type = "studio:response",
|
|
170
|
-
payload = result,
|
|
171
|
-
ts = os.clock(),
|
|
172
|
-
}))
|
|
173
|
-
end)
|
|
174
|
-
elseif msgType == "cli:test:play" then
|
|
175
|
-
-- ExecutePlayModeAsync yields until EndTest is called, so run in a thread
|
|
176
|
-
task.spawn(function()
|
|
177
|
-
local result = TestRunner.executePlayMode(payload.args)
|
|
178
|
-
WsClient.send(Protocol.encode({
|
|
179
|
-
id = msgId,
|
|
180
|
-
type = "studio:response",
|
|
181
|
-
payload = result,
|
|
182
|
-
ts = os.clock(),
|
|
183
|
-
}))
|
|
184
|
-
end)
|
|
185
|
-
elseif msgType == "cli:test:end" then
|
|
186
|
-
local result = TestRunner.endTest(payload.value)
|
|
187
|
-
WsClient.send(Protocol.encode({
|
|
188
|
-
id = msgId,
|
|
189
|
-
type = "studio:response",
|
|
190
|
-
payload = result,
|
|
191
|
-
ts = os.clock(),
|
|
192
|
-
}))
|
|
193
|
-
elseif msgType == "cli:build:ui" then
|
|
194
|
-
local result = UIBuilder.build(payload)
|
|
195
|
-
WsClient.send(Protocol.encode({
|
|
196
|
-
id = msgId,
|
|
197
|
-
type = "studio:response",
|
|
198
|
-
payload = result,
|
|
199
|
-
ts = os.clock(),
|
|
200
|
-
}))
|
|
201
|
-
elseif msgType == "cli:get:reflection" then
|
|
202
|
-
local result = Reflection.getClassInfo(payload.className)
|
|
203
|
-
WsClient.send(Protocol.encode({
|
|
204
|
-
id = msgId,
|
|
205
|
-
type = "studio:response",
|
|
206
|
-
payload = result,
|
|
207
|
-
ts = os.clock(),
|
|
208
|
-
}))
|
|
209
|
-
elseif msgType == "cli:serialize:ui" then
|
|
210
|
-
local result = UIBuilder.serialize(payload)
|
|
211
|
-
WsClient.send(Protocol.encode({
|
|
212
|
-
id = msgId,
|
|
213
|
-
type = "studio:response",
|
|
214
|
-
payload = result,
|
|
215
|
-
ts = os.clock(),
|
|
216
|
-
}))
|
|
217
|
-
elseif msgType == "cli:move:instance" then
|
|
218
|
-
local instance = Explorer.resolveInstance(payload.path)
|
|
219
|
-
if not instance then
|
|
220
|
-
WsClient.send(Protocol.encode({
|
|
221
|
-
id = msgId,
|
|
222
|
-
type = "studio:response",
|
|
223
|
-
payload = { success = false, error = "Instance not found: " .. payload.path },
|
|
224
|
-
ts = os.clock(),
|
|
225
|
-
}))
|
|
226
|
-
else
|
|
227
|
-
local newParent = Explorer.resolveInstance(payload.newParent)
|
|
228
|
-
if not newParent then
|
|
229
|
-
local svcOk, svc = pcall(function()
|
|
230
|
-
return game:GetService(payload.newParent)
|
|
231
|
-
end)
|
|
232
|
-
if svcOk then
|
|
233
|
-
newParent = svc
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
if not newParent then
|
|
237
|
-
WsClient.send(Protocol.encode({
|
|
238
|
-
id = msgId,
|
|
239
|
-
type = "studio:response",
|
|
240
|
-
payload = { success = false, error = "New parent not found: " .. payload.newParent },
|
|
241
|
-
ts = os.clock(),
|
|
242
|
-
}))
|
|
243
|
-
else
|
|
244
|
-
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
245
|
-
local recording = ChangeHistoryService:TryBeginRecording("Dominus: Move " .. instance.Name)
|
|
246
|
-
local ok, err = pcall(function()
|
|
247
|
-
instance.Parent = newParent
|
|
248
|
-
end)
|
|
249
|
-
if recording then
|
|
250
|
-
if ok then
|
|
251
|
-
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Commit)
|
|
252
|
-
else
|
|
253
|
-
ChangeHistoryService:FinishRecording(recording, Enum.FinishRecordingOperation.Cancel)
|
|
254
|
-
end
|
|
255
|
-
end
|
|
256
|
-
if ok then
|
|
257
|
-
WsClient.send(Protocol.encode({
|
|
258
|
-
id = msgId,
|
|
259
|
-
type = "studio:response",
|
|
260
|
-
payload = { success = true, newPath = Explorer.getPath(instance) },
|
|
261
|
-
ts = os.clock(),
|
|
262
|
-
}))
|
|
263
|
-
else
|
|
264
|
-
WsClient.send(Protocol.encode({
|
|
265
|
-
id = msgId,
|
|
266
|
-
type = "studio:response",
|
|
267
|
-
payload = { success = false, error = "Move failed: " .. tostring(err) },
|
|
268
|
-
ts = os.clock(),
|
|
269
|
-
}))
|
|
270
|
-
end
|
|
110
|
+
authenticated = true
|
|
111
|
+
connectButton:SetActive(true)
|
|
112
|
+
log("Authenticated as Studio session " .. payload.connectionId)
|
|
113
|
+
Watcher.start(function(entry)
|
|
114
|
+
if authenticated then
|
|
115
|
+
sendEnvelope({ id = Protocol.generateId(), type = "studio:output", payload = entry, ts = os.time() })
|
|
271
116
|
end
|
|
272
|
-
end
|
|
273
|
-
elseif msgType == "cli:undo" then
|
|
274
|
-
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
275
|
-
local ok, err = pcall(function()
|
|
276
|
-
ChangeHistoryService:Undo()
|
|
277
|
-
end)
|
|
278
|
-
WsClient.send(Protocol.encode({
|
|
279
|
-
id = msgId,
|
|
280
|
-
type = "studio:response",
|
|
281
|
-
payload = ok and { success = true } or { success = false, error = tostring(err) },
|
|
282
|
-
ts = os.clock(),
|
|
283
|
-
}))
|
|
284
|
-
elseif msgType == "cli:redo" then
|
|
285
|
-
local ChangeHistoryService = game:GetService("ChangeHistoryService")
|
|
286
|
-
local ok, err = pcall(function()
|
|
287
|
-
ChangeHistoryService:Redo()
|
|
288
117
|
end)
|
|
289
|
-
|
|
290
|
-
id = msgId,
|
|
291
|
-
type = "studio:response",
|
|
292
|
-
payload = ok and { success = true } or { success = false, error = tostring(err) },
|
|
293
|
-
ts = os.clock(),
|
|
294
|
-
}))
|
|
295
|
-
elseif msgType == "cli:bulk:set:properties" then
|
|
296
|
-
local result = Properties.bulkSet(payload)
|
|
297
|
-
WsClient.send(Protocol.encode({
|
|
298
|
-
id = msgId,
|
|
299
|
-
type = "studio:response",
|
|
300
|
-
payload = result,
|
|
301
|
-
ts = os.clock(),
|
|
302
|
-
}))
|
|
303
|
-
elseif msgType == "cli:find:replace:scripts" then
|
|
304
|
-
local result = Explorer.findReplaceScripts(payload)
|
|
305
|
-
WsClient.send(Protocol.encode({
|
|
306
|
-
id = msgId,
|
|
307
|
-
type = "studio:response",
|
|
308
|
-
payload = result,
|
|
309
|
-
ts = os.clock(),
|
|
310
|
-
}))
|
|
311
|
-
else
|
|
312
|
-
warn("[Dominus] Unknown message type:", msgType)
|
|
118
|
+
return
|
|
313
119
|
end
|
|
314
|
-
end
|
|
315
120
|
|
|
316
|
-
|
|
317
|
-
|
|
121
|
+
if message.type == "server:auth_rejected" then
|
|
122
|
+
warn("[Dominus 2] Authentication rejected: " .. tostring(message.payload and message.payload.error))
|
|
123
|
+
WsClient.disconnect()
|
|
124
|
+
return
|
|
125
|
+
end
|
|
318
126
|
|
|
319
|
-
|
|
127
|
+
if not authenticated then
|
|
128
|
+
warn("[Dominus 2] Ignored a command before authentication")
|
|
129
|
+
return
|
|
130
|
+
end
|
|
131
|
+
CommandRouter.handle(message, sendResponse)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
local function connect()
|
|
135
|
+
if shuttingDown or transportOpen then
|
|
136
|
+
return
|
|
137
|
+
end
|
|
138
|
+
reconnectScheduled = false
|
|
139
|
+
local success = WsClient.connect("ws://127.0.0.1:" .. port, {
|
|
320
140
|
onOpen = function()
|
|
321
|
-
|
|
322
|
-
|
|
141
|
+
transportOpen = true
|
|
142
|
+
reconnectDelay = 1
|
|
323
143
|
connectButton:SetActive(true)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
local studioVersion = game:GetService("RobloxPluginGuiService") and "Studio" or "Unknown"
|
|
327
|
-
local placeName = "Unknown"
|
|
328
|
-
pcall(function()
|
|
329
|
-
placeName = game:GetService("MarketplaceService"):GetProductInfo(game.PlaceId).Name
|
|
330
|
-
end)
|
|
331
|
-
|
|
332
|
-
WsClient.send(Protocol.encode({
|
|
144
|
+
local sent, sendErr = sendEnvelope({
|
|
333
145
|
id = Protocol.generateId(),
|
|
334
|
-
type = "studio:
|
|
146
|
+
type = "studio:hello",
|
|
335
147
|
payload = {
|
|
336
|
-
|
|
148
|
+
protocolVersion = PROTOCOL_VERSION,
|
|
149
|
+
clientId = clientId,
|
|
150
|
+
token = bridgeToken,
|
|
151
|
+
studioVersion = version(),
|
|
337
152
|
placeId = game.PlaceId,
|
|
338
|
-
placeName =
|
|
153
|
+
placeName = game.Name,
|
|
339
154
|
},
|
|
340
|
-
ts = os.
|
|
341
|
-
})
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
WsClient.send(Protocol.encode({
|
|
346
|
-
id = Protocol.generateId(),
|
|
347
|
-
type = "studio:output",
|
|
348
|
-
payload = entry,
|
|
349
|
-
ts = os.clock(),
|
|
350
|
-
}))
|
|
351
|
-
end
|
|
352
|
-
end)
|
|
155
|
+
ts = os.time(),
|
|
156
|
+
})
|
|
157
|
+
if not sent then
|
|
158
|
+
warn("[Dominus 2] Could not send handshake: " .. tostring(sendErr))
|
|
159
|
+
end
|
|
353
160
|
end,
|
|
354
|
-
|
|
355
161
|
onMessage = function(message)
|
|
356
162
|
task.spawn(handleMessage, message)
|
|
357
163
|
end,
|
|
358
|
-
|
|
359
164
|
onClose = function()
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
165
|
+
local wasOpen = transportOpen
|
|
166
|
+
stopSession()
|
|
167
|
+
if wasOpen and not shuttingDown then
|
|
168
|
+
log("Studio bridge disconnected")
|
|
169
|
+
end
|
|
170
|
+
if shouldReconnect then
|
|
171
|
+
scheduleReconnect()
|
|
172
|
+
end
|
|
364
173
|
end,
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
174
|
+
onError = function(statusCode, errorMessage)
|
|
175
|
+
if transportOpen then
|
|
176
|
+
warn("[Dominus 2] Bridge error " .. tostring(statusCode) .. ": " .. tostring(errorMessage))
|
|
177
|
+
end
|
|
368
178
|
end,
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
|
|
179
|
+
}, 4)
|
|
180
|
+
if not success then
|
|
181
|
+
stopSession()
|
|
182
|
+
if shouldReconnect then
|
|
183
|
+
scheduleReconnect()
|
|
184
|
+
end
|
|
185
|
+
end
|
|
372
186
|
end
|
|
373
187
|
|
|
374
|
-
|
|
375
|
-
if
|
|
376
|
-
|
|
377
|
-
WsClient.disconnect()
|
|
378
|
-
connected = false
|
|
188
|
+
scheduleReconnect = function()
|
|
189
|
+
if reconnectScheduled or shuttingDown or not shouldReconnect then
|
|
190
|
+
return
|
|
379
191
|
end
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return
|
|
192
|
+
reconnectScheduled = true
|
|
193
|
+
local delaySeconds = reconnectDelay
|
|
194
|
+
reconnectDelay = math.min(reconnectDelay * 2, 30)
|
|
195
|
+
task.delay(delaySeconds, function()
|
|
196
|
+
reconnectScheduled = false
|
|
197
|
+
if shouldReconnect and not shuttingDown then
|
|
198
|
+
task.spawn(connect)
|
|
388
199
|
end
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
warn("[Dominus] No Dominus server found. Is `dominus` or `dominus mcp` running?")
|
|
200
|
+
end)
|
|
392
201
|
end
|
|
393
202
|
|
|
394
203
|
connectButton.Click:Connect(function()
|
|
395
|
-
if
|
|
204
|
+
if transportOpen or WsClient.isConnected() then
|
|
205
|
+
shouldReconnect = false
|
|
396
206
|
WsClient.disconnect()
|
|
397
|
-
|
|
398
|
-
|
|
207
|
+
stopSession()
|
|
208
|
+
log("Automatic connection disabled; click Dominus 2 to reconnect")
|
|
399
209
|
else
|
|
400
|
-
|
|
210
|
+
shouldReconnect = true
|
|
211
|
+
reconnectDelay = 1
|
|
212
|
+
task.spawn(connect)
|
|
401
213
|
end
|
|
402
214
|
end)
|
|
403
215
|
|
|
404
|
-
-- Auto-connect on plugin load
|
|
405
216
|
task.delay(1, function()
|
|
406
|
-
connect
|
|
217
|
+
task.spawn(connect)
|
|
407
218
|
end)
|
|
408
219
|
|
|
409
220
|
plugin.Unloading:Connect(function()
|
|
410
|
-
|
|
221
|
+
shuttingDown = true
|
|
222
|
+
shouldReconnect = false
|
|
223
|
+
stopSession()
|
|
411
224
|
WsClient.disconnect()
|
|
412
225
|
end)
|