@el4cteo/rbx-studio-mcp 0.6.0 → 0.6.5
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/README.md +4 -4
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/tools/anim.js +159 -0
- package/dist/tools/anim.js.map +1 -0
- package/dist/tools/character.js +95 -5
- package/dist/tools/character.js.map +1 -1
- package/dist/tools/data.js +173 -0
- package/dist/tools/data.js.map +1 -0
- package/dist/tools/device.js +77 -7
- package/dist/tools/device.js.map +1 -1
- package/dist/tools/discover.js +80 -4
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +48 -1
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/input.js +35 -9
- package/dist/tools/input.js.map +1 -1
- package/dist/tools/perf.js +74 -7
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +51 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/world.js +317 -44
- package/dist/tools/world.js.map +1 -1
- package/package.json +74 -74
- package/plugin/src/Commands.luau +622 -562
- package/plugin/src/Config.luau +65 -65
- package/plugin/src/Console.luau +1909 -1843
- package/plugin/src/Emulation.luau +172 -0
- package/plugin/src/Phrase.luau +164 -16
- package/plugin/src/Png.luau +8 -4
- package/plugin/src/Serialize.luau +499 -327
- package/plugin/src/Undo.luau +94 -6
- package/plugin/src/handlers/Anim.luau +897 -0
- package/plugin/src/handlers/Assets.luau +587 -352
- package/plugin/src/handlers/Capture.luau +155 -20
- package/plugin/src/handlers/Character.luau +823 -361
- package/plugin/src/handlers/Data.luau +539 -0
- package/plugin/src/handlers/Device.luau +394 -139
- package/plugin/src/handlers/Discover.luau +685 -363
- package/plugin/src/handlers/Geometry.luau +127 -0
- package/plugin/src/handlers/Perf.luau +227 -0
- package/plugin/src/handlers/Scripts.luau +673 -539
- package/plugin/src/handlers/Session.luau +3 -0
- package/plugin/src/handlers/Viewport.luau +268 -0
- package/plugin/src/handlers/World.luau +89 -15
- package/plugin/src/init.server.luau +879 -832
- package/scripts/build-plugin.mjs +20 -0
- package/scripts/check-plugin.mjs +171 -124
package/plugin/src/Commands.luau
CHANGED
|
@@ -1,562 +1,622 @@
|
|
|
1
|
-
--!strict
|
|
2
|
-
--[[
|
|
3
|
-
What the console's prompt row does with a line.
|
|
4
|
-
|
|
5
|
-
Split from `Console` on purpose. The console owns a log and a widget; half of
|
|
6
|
-
these answers are about the transport, the port setting, the place, or the
|
|
7
|
-
bridge, and wiring all of that into the console to answer `status` would make
|
|
8
|
-
the console the whole plugin.
|
|
9
|
-
|
|
10
|
-
Two kinds of line arrive here and they are told apart by one rule: the first
|
|
11
|
-
word is either a command in this table or it is not. If it is, it runs here
|
|
12
|
-
or on the bridge and prints in milliseconds. If it is not, the whole line is
|
|
13
|
-
a request for an agent, and goes to the bridge to be run by whichever coding
|
|
14
|
-
tool the user has installed. Nothing has to be marked, quoted or prefixed --
|
|
15
|
-
`doctor` is a command and "make the door open when I touch it" is not, and no
|
|
16
|
-
person needs that explained.
|
|
17
|
-
|
|
18
|
-
Commands answered on the bridge are listed here too, with `remote = true`, so
|
|
19
|
-
`help` and Tab-completion describe the whole surface rather than only the
|
|
20
|
-
half that happens to run in Luau.
|
|
21
|
-
]]
|
|
22
|
-
|
|
23
|
-
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
24
|
-
local Selection = game:GetService("Selection")
|
|
25
|
-
local ServerStorage = game:GetService("ServerStorage")
|
|
26
|
-
|
|
27
|
-
local Config = require(script.Parent.Config)
|
|
28
|
-
local Console = require(script.Parent.Console)
|
|
29
|
-
local Net = require(script.Parent.Net)
|
|
30
|
-
local ThemePicker = require(script.Parent.ThemePicker)
|
|
31
|
-
local Themes = require(script.Parent.Themes)
|
|
32
|
-
|
|
33
|
-
local Commands = {}
|
|
34
|
-
|
|
35
|
-
export type Hooks = {
|
|
36
|
-
-- Drops the connection and dials again. Owned by init.server, which is the
|
|
37
|
-
-- only thing holding the transport's lifecycle.
|
|
38
|
-
reconnect: () -> (),
|
|
39
|
-
-- `plugin:SetSetting` is not reachable from a ModuleScript, so anything that
|
|
40
|
-
-- has to outlive the session is handed back to the script that can persist it.
|
|
41
|
-
savePort: (number) -> (),
|
|
42
|
-
saveTheme: (string) -> (),
|
|
43
|
-
--
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
local
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
"dim",
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
Console.
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if
|
|
285
|
-
Console.log(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
--
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
--
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
What the console's prompt row does with a line.
|
|
4
|
+
|
|
5
|
+
Split from `Console` on purpose. The console owns a log and a widget; half of
|
|
6
|
+
these answers are about the transport, the port setting, the place, or the
|
|
7
|
+
bridge, and wiring all of that into the console to answer `status` would make
|
|
8
|
+
the console the whole plugin.
|
|
9
|
+
|
|
10
|
+
Two kinds of line arrive here and they are told apart by one rule: the first
|
|
11
|
+
word is either a command in this table or it is not. If it is, it runs here
|
|
12
|
+
or on the bridge and prints in milliseconds. If it is not, the whole line is
|
|
13
|
+
a request for an agent, and goes to the bridge to be run by whichever coding
|
|
14
|
+
tool the user has installed. Nothing has to be marked, quoted or prefixed --
|
|
15
|
+
`doctor` is a command and "make the door open when I touch it" is not, and no
|
|
16
|
+
person needs that explained.
|
|
17
|
+
|
|
18
|
+
Commands answered on the bridge are listed here too, with `remote = true`, so
|
|
19
|
+
`help` and Tab-completion describe the whole surface rather than only the
|
|
20
|
+
half that happens to run in Luau.
|
|
21
|
+
]]
|
|
22
|
+
|
|
23
|
+
local ScriptEditorService = game:GetService("ScriptEditorService")
|
|
24
|
+
local Selection = game:GetService("Selection")
|
|
25
|
+
local ServerStorage = game:GetService("ServerStorage")
|
|
26
|
+
|
|
27
|
+
local Config = require(script.Parent.Config)
|
|
28
|
+
local Console = require(script.Parent.Console)
|
|
29
|
+
local Net = require(script.Parent.Net)
|
|
30
|
+
local ThemePicker = require(script.Parent.ThemePicker)
|
|
31
|
+
local Themes = require(script.Parent.Themes)
|
|
32
|
+
|
|
33
|
+
local Commands = {}
|
|
34
|
+
|
|
35
|
+
export type Hooks = {
|
|
36
|
+
-- Drops the connection and dials again. Owned by init.server, which is the
|
|
37
|
+
-- only thing holding the transport's lifecycle.
|
|
38
|
+
reconnect: () -> (),
|
|
39
|
+
-- `plugin:SetSetting` is not reachable from a ModuleScript, so anything that
|
|
40
|
+
-- has to outlive the session is handed back to the script that can persist it.
|
|
41
|
+
savePort: (number) -> (),
|
|
42
|
+
saveTheme: (string) -> (),
|
|
43
|
+
-- Whether the panel may open itself on load, and the setter for it.
|
|
44
|
+
autoOpen: () -> boolean,
|
|
45
|
+
saveAutoOpen: (boolean) -> (),
|
|
46
|
+
-- The transport's own view of the connection, which the console only ever
|
|
47
|
+
-- receives second-hand as a string to display.
|
|
48
|
+
status: () -> string,
|
|
49
|
+
studioId: () -> string,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
local hooks: Hooks? = nil
|
|
53
|
+
|
|
54
|
+
function Commands.setup(value: Hooks)
|
|
55
|
+
hooks = value
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
type Entry = {
|
|
59
|
+
name: string,
|
|
60
|
+
usage: string,
|
|
61
|
+
summary: string,
|
|
62
|
+
-- Answered by the bridge rather than here. Listed all the same: a user
|
|
63
|
+
-- should not have to know which side of the wire a command lives on.
|
|
64
|
+
remote: boolean?,
|
|
65
|
+
run: ((args: { string }) -> ())?,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
local entries: { Entry } = {}
|
|
69
|
+
local byName: { [string]: Entry } = {}
|
|
70
|
+
|
|
71
|
+
local function define(entry: Entry)
|
|
72
|
+
table.insert(entries, entry)
|
|
73
|
+
byName[entry.name] = entry
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
--[[
|
|
77
|
+
Sends a command to the bridge and prints whatever comes back.
|
|
78
|
+
|
|
79
|
+
Answers arrive as rows rather than as text, so the bridge decides what a
|
|
80
|
+
warning IS and this decides what a warning looks like. Both sides formatting
|
|
81
|
+
is how a diagnostic ends up rendered two different ways in one log.
|
|
82
|
+
]]
|
|
83
|
+
local function remote(command: string, args: { string }, line: string)
|
|
84
|
+
local current = hooks
|
|
85
|
+
local response = Net.postJson("/console", {
|
|
86
|
+
studioId = if current ~= nil then current.studioId() else "",
|
|
87
|
+
command = command,
|
|
88
|
+
args = args,
|
|
89
|
+
line = line,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
if not response.ok then
|
|
93
|
+
Console.setPromptBusy(false)
|
|
94
|
+
Console.log("error", string.format("%s failed", command), response.error)
|
|
95
|
+
return
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
local decoded = Net.decode(response.body)
|
|
99
|
+
--[[
|
|
100
|
+
The bridge is the authority on whether an agent is running, so the caret
|
|
101
|
+
follows its answer rather than what this side assumed when it sent the
|
|
102
|
+
line. A prompt that never started -- no harness on PATH is the common one
|
|
103
|
+
-- would otherwise leave the panel looking busy for the rest of the
|
|
104
|
+
session.
|
|
105
|
+
]]
|
|
106
|
+
Console.setPromptBusy(typeof(decoded) == "table" and (decoded :: any).running == true)
|
|
107
|
+
|
|
108
|
+
local rows = if typeof(decoded) == "table" then (decoded :: any).lines else nil
|
|
109
|
+
if typeof(rows) ~= "table" then
|
|
110
|
+
Console.log("error", string.format("%s returned nothing readable", command))
|
|
111
|
+
return
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
for _, row in rows :: { any } do
|
|
115
|
+
if typeof(row) == "table" and typeof(row.message) == "string" then
|
|
116
|
+
local level = if typeof(row.level) == "string" then row.level else "dim"
|
|
117
|
+
Console.log(
|
|
118
|
+
level :: any,
|
|
119
|
+
row.message,
|
|
120
|
+
if typeof(row.detail) == "string" then row.detail else nil
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
-- Local commands ----------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
define({
|
|
129
|
+
name = "help",
|
|
130
|
+
usage = "help",
|
|
131
|
+
summary = "list every command",
|
|
132
|
+
run = function()
|
|
133
|
+
Console.log("info", "commands")
|
|
134
|
+
for _, entry in entries do
|
|
135
|
+
Console.log("dim", " " .. entry.usage, entry.summary)
|
|
136
|
+
end
|
|
137
|
+
Console.log(
|
|
138
|
+
"dim",
|
|
139
|
+
" anything else",
|
|
140
|
+
"sent to a coding agent -- run `agent` to see which"
|
|
141
|
+
)
|
|
142
|
+
end,
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
define({
|
|
146
|
+
name = "clear",
|
|
147
|
+
usage = "clear",
|
|
148
|
+
summary = "empty the log",
|
|
149
|
+
run = function()
|
|
150
|
+
Console.clear()
|
|
151
|
+
end,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
define({
|
|
155
|
+
name = "reconnect",
|
|
156
|
+
usage = "reconnect",
|
|
157
|
+
summary = "drop the connection and dial the bridge again",
|
|
158
|
+
run = function()
|
|
159
|
+
local current = hooks
|
|
160
|
+
if current then
|
|
161
|
+
current.reconnect()
|
|
162
|
+
end
|
|
163
|
+
end,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
define({
|
|
167
|
+
name = "status",
|
|
168
|
+
usage = "status",
|
|
169
|
+
summary = "connection, bridge, build and theme",
|
|
170
|
+
run = function()
|
|
171
|
+
local current = hooks
|
|
172
|
+
Console.log("info", "session")
|
|
173
|
+
Console.log(
|
|
174
|
+
"dim",
|
|
175
|
+
" connection",
|
|
176
|
+
if current ~= nil then current.status() else "unknown"
|
|
177
|
+
)
|
|
178
|
+
Console.log("dim", " bridge", Config.baseUrl())
|
|
179
|
+
Console.log(
|
|
180
|
+
"dim",
|
|
181
|
+
" plugin",
|
|
182
|
+
string.format(
|
|
183
|
+
"v%s build %s protocol %d",
|
|
184
|
+
Config.PLUGIN_VERSION,
|
|
185
|
+
Config.BUILD_ID,
|
|
186
|
+
Config.PROTOCOL_VERSION
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
Console.log("dim", " theme", Themes.activeId())
|
|
190
|
+
end,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
define({
|
|
194
|
+
name = "clients",
|
|
195
|
+
usage = "clients",
|
|
196
|
+
summary = "which MCP clients share this bridge",
|
|
197
|
+
run = function()
|
|
198
|
+
Console.reportClients()
|
|
199
|
+
end,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
define({
|
|
203
|
+
name = "theme",
|
|
204
|
+
usage = "theme [name]",
|
|
205
|
+
summary = "switch colour preset, or list them",
|
|
206
|
+
run = function(args)
|
|
207
|
+
local wanted = args[1]
|
|
208
|
+
if wanted == nil then
|
|
209
|
+
local active = Themes.activeId()
|
|
210
|
+
Console.log("info", "themes")
|
|
211
|
+
for _, theme in Themes.list() do
|
|
212
|
+
Console.log(
|
|
213
|
+
if theme.id == active then "ok" else "dim",
|
|
214
|
+
" " .. theme.id,
|
|
215
|
+
if theme.id == active then "in use" else nil
|
|
216
|
+
)
|
|
217
|
+
end
|
|
218
|
+
return
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
-- `Themes.use` answers false both for an unknown id and for re-picking
|
|
222
|
+
-- the current one, so the two are separated here rather than reported as
|
|
223
|
+
-- the same shrug.
|
|
224
|
+
if wanted == Themes.activeId() then
|
|
225
|
+
Console.log("dim", string.format("already on %s", wanted))
|
|
226
|
+
return
|
|
227
|
+
end
|
|
228
|
+
if not Themes.use(wanted) then
|
|
229
|
+
Console.log("error", string.format("no theme called %q", wanted), "run `theme` to list them")
|
|
230
|
+
return
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
Console.applyTheme()
|
|
234
|
+
ThemePicker.applyTheme()
|
|
235
|
+
local current = hooks
|
|
236
|
+
if current then
|
|
237
|
+
current.saveTheme(wanted)
|
|
238
|
+
end
|
|
239
|
+
Console.log("ok", string.format("theme: %s", wanted))
|
|
240
|
+
end,
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
define({
|
|
244
|
+
name = "visuals",
|
|
245
|
+
usage = "visuals",
|
|
246
|
+
summary = "show or hide the activity band",
|
|
247
|
+
run = function()
|
|
248
|
+
local shown = Console.toggleVisuals()
|
|
249
|
+
Console.log("dim", if shown then "visuals on" else "visuals off")
|
|
250
|
+
end,
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
define({
|
|
254
|
+
name = "autoopen",
|
|
255
|
+
usage = "autoopen [on|off]",
|
|
256
|
+
summary = "let this panel open itself, or only ever from the toolbar",
|
|
257
|
+
run = function(args)
|
|
258
|
+
local current = hooks
|
|
259
|
+
if current == nil then
|
|
260
|
+
return
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
local wanted = args[1]
|
|
264
|
+
if wanted == nil then
|
|
265
|
+
Console.log(
|
|
266
|
+
"dim",
|
|
267
|
+
if current.autoOpen()
|
|
268
|
+
then "autoopen on -- the panel opens with the place, and with Play"
|
|
269
|
+
else "autoopen off -- the panel opens only from the toolbar button"
|
|
270
|
+
)
|
|
271
|
+
return
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
local value: boolean
|
|
275
|
+
if wanted == "on" then
|
|
276
|
+
value = true
|
|
277
|
+
elseif wanted == "off" then
|
|
278
|
+
value = false
|
|
279
|
+
else
|
|
280
|
+
Console.log("error", string.format("%q is not on or off", wanted), "autoopen [on|off]")
|
|
281
|
+
return
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if value == current.autoOpen() then
|
|
285
|
+
Console.log("dim", string.format("already autoopen %s", wanted))
|
|
286
|
+
return
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
current.saveAutoOpen(value)
|
|
290
|
+
--[[
|
|
291
|
+
Said plainly, because nothing about it can be seen from here.
|
|
292
|
+
|
|
293
|
+
A dock widget's initial state is decided when Studio creates it, and
|
|
294
|
+
this one already exists -- so the change shows up at the next load,
|
|
295
|
+
not now. A setting that appears to do nothing is a setting the user
|
|
296
|
+
runs again, and then a third time, before deciding it is broken.
|
|
297
|
+
]]
|
|
298
|
+
if value then
|
|
299
|
+
Console.log("ok", "autoopen on", "the panel will come back on its own")
|
|
300
|
+
else
|
|
301
|
+
Console.log(
|
|
302
|
+
"ok",
|
|
303
|
+
"autoopen off",
|
|
304
|
+
"from the next place or playtest, open it from the toolbar"
|
|
305
|
+
)
|
|
306
|
+
end
|
|
307
|
+
end,
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
define({
|
|
311
|
+
name = "place",
|
|
312
|
+
usage = "place",
|
|
313
|
+
summary = "what this Studio window has open",
|
|
314
|
+
run = function()
|
|
315
|
+
Console.log("info", if game.Name ~= "" then game.Name else "Untitled place")
|
|
316
|
+
Console.log("dim", " placeId", tostring(game.PlaceId))
|
|
317
|
+
Console.log("dim", " gameId", tostring(game.GameId))
|
|
318
|
+
for _, name in { "Workspace", "ServerScriptService", "ServerStorage", "ReplicatedStorage", "StarterGui" } do
|
|
319
|
+
local service = game:FindFirstChild(name)
|
|
320
|
+
if service ~= nil then
|
|
321
|
+
Console.log("dim", " " .. name, string.format("%d children", #service:GetChildren()))
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
--[[
|
|
328
|
+
Every level the log can show, so `log` can be checked against a real list
|
|
329
|
+
rather than silently accepting a typo and hiding everything.
|
|
330
|
+
]]
|
|
331
|
+
local LEVELS = { "ok", "error", "warn", "info", "dim", "call", "reply" }
|
|
332
|
+
|
|
333
|
+
define({
|
|
334
|
+
name = "log",
|
|
335
|
+
usage = "log [level|all]",
|
|
336
|
+
summary = "filter the log by kind, or `all` for everything",
|
|
337
|
+
run = function(args)
|
|
338
|
+
local wanted = args[1]
|
|
339
|
+
if wanted == nil or wanted == "all" then
|
|
340
|
+
Console.setFilter(nil)
|
|
341
|
+
Console.log("dim", "showing everything")
|
|
342
|
+
return
|
|
343
|
+
end
|
|
344
|
+
if not table.find(LEVELS, wanted) then
|
|
345
|
+
Console.log(
|
|
346
|
+
"error",
|
|
347
|
+
string.format("no level called %q", wanted),
|
|
348
|
+
table.concat(LEVELS, " ") .. " all"
|
|
349
|
+
)
|
|
350
|
+
return
|
|
351
|
+
end
|
|
352
|
+
--[[
|
|
353
|
+
An error filter shows warnings too.
|
|
354
|
+
|
|
355
|
+
Someone typing `log error` is looking for what went wrong, and a
|
|
356
|
+
warning is part of that answer. Filtering to the single level would
|
|
357
|
+
hide the row that usually explains the failure below it.
|
|
358
|
+
]]
|
|
359
|
+
local levels = if wanted == "error" then { "error", "warn" } else { wanted }
|
|
360
|
+
Console.setFilter(levels)
|
|
361
|
+
Console.log("dim", string.format("showing %s only", table.concat(levels, " and ")))
|
|
362
|
+
end,
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
define({
|
|
366
|
+
name = "copy",
|
|
367
|
+
usage = "copy",
|
|
368
|
+
summary = "put the log somewhere it can be selected and copied",
|
|
369
|
+
run = function()
|
|
370
|
+
--[[
|
|
371
|
+
Studio gives plugins no clipboard, and a TextLabel cannot be selected.
|
|
372
|
+
|
|
373
|
+
So the log goes where selection and Ctrl+C already work: a script
|
|
374
|
+
editor tab. The rows are written as comments -- see
|
|
375
|
+
`Console.plainText` -- so the tab opens as something to read rather
|
|
376
|
+
than as a wall of syntax errors, and the document is opened for the
|
|
377
|
+
user rather than merely selected, which turns a two-step
|
|
378
|
+
"find it in Explorer, double-click it" into typing one word.
|
|
379
|
+
|
|
380
|
+
Parented to ServerStorage so it can never end up in a published
|
|
381
|
+
place, and replaced rather than accumulated so running it twice does
|
|
382
|
+
not litter the tree.
|
|
383
|
+
]]
|
|
384
|
+
local existing = ServerStorage:FindFirstChild("rbx-studio log")
|
|
385
|
+
if existing then
|
|
386
|
+
existing:Destroy()
|
|
387
|
+
end
|
|
388
|
+
local holder = Instance.new("Script")
|
|
389
|
+
holder.Name = "rbx-studio log"
|
|
390
|
+
holder.Source = Console.plainText()
|
|
391
|
+
holder.Enabled = false
|
|
392
|
+
holder.Parent = ServerStorage
|
|
393
|
+
Selection:Set({ holder })
|
|
394
|
+
|
|
395
|
+
--[[
|
|
396
|
+
Opening is best effort, and deliberately not fatal.
|
|
397
|
+
|
|
398
|
+
`OpenScriptDocumentAsync` yields and can fail -- the editor may
|
|
399
|
+
refuse, and it is PluginSecurity, so a future Studio could withdraw
|
|
400
|
+
it. The script exists and is selected either way, which is the part
|
|
401
|
+
that matters; all that is lost is the convenience, so the failure is
|
|
402
|
+
reported as the extra step it costs rather than as an error.
|
|
403
|
+
]]
|
|
404
|
+
local opened = pcall(function()
|
|
405
|
+
ScriptEditorService:OpenScriptDocumentAsync(holder)
|
|
406
|
+
end)
|
|
407
|
+
if opened then
|
|
408
|
+
Console.log("ok", "log opened in a script tab", "select all and copy")
|
|
409
|
+
else
|
|
410
|
+
Console.log(
|
|
411
|
+
"ok",
|
|
412
|
+
"log written to ServerStorage",
|
|
413
|
+
"selected -- open it from Explorer and copy"
|
|
414
|
+
)
|
|
415
|
+
end
|
|
416
|
+
end,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
define({
|
|
420
|
+
name = "port",
|
|
421
|
+
usage = "port [number]",
|
|
422
|
+
summary = "show, or move to, the bridge port",
|
|
423
|
+
run = function(args)
|
|
424
|
+
local wanted = args[1]
|
|
425
|
+
if wanted == nil then
|
|
426
|
+
Console.log("dim", string.format("port %d", Config.getPort()))
|
|
427
|
+
return
|
|
428
|
+
end
|
|
429
|
+
local value = tonumber(wanted)
|
|
430
|
+
if value == nil or value ~= math.floor(value) or value < 1 or value > 65535 then
|
|
431
|
+
Console.log("error", string.format("%q is not a port", wanted))
|
|
432
|
+
return
|
|
433
|
+
end
|
|
434
|
+
Config.setPort(value :: number)
|
|
435
|
+
local current = hooks
|
|
436
|
+
if current then
|
|
437
|
+
current.savePort(value :: number)
|
|
438
|
+
Console.log("ok", string.format("port %d -- reconnecting", value))
|
|
439
|
+
current.reconnect()
|
|
440
|
+
end
|
|
441
|
+
end,
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
define({
|
|
445
|
+
name = "version",
|
|
446
|
+
usage = "version",
|
|
447
|
+
summary = "version, build and protocol of this plugin",
|
|
448
|
+
run = function()
|
|
449
|
+
Console.log(
|
|
450
|
+
"info",
|
|
451
|
+
string.format("rbx-studio v%s", Config.PLUGIN_VERSION),
|
|
452
|
+
string.format("build %s protocol %d", Config.BUILD_ID, Config.PROTOCOL_VERSION)
|
|
453
|
+
)
|
|
454
|
+
Console.log("dim", "run doctor to compare it against the installed package")
|
|
455
|
+
end,
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
-- Bridge commands ---------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
define({ name = "doctor", usage = "doctor", summary = "check every part of the setup", remote = true })
|
|
461
|
+
define({ name = "studios", usage = "studios", summary = "Studio windows on this bridge", remote = true })
|
|
462
|
+
define({ name = "use", usage = "use <number|id>", summary = "point calls at one Studio", remote = true })
|
|
463
|
+
define({
|
|
464
|
+
name = "agent",
|
|
465
|
+
usage = "agent [use <id>|new]",
|
|
466
|
+
summary = "which coding agent runs your prompts",
|
|
467
|
+
remote = true,
|
|
468
|
+
})
|
|
469
|
+
define({ name = "stop", usage = "stop", summary = "cancel the running agent", remote = true })
|
|
470
|
+
|
|
471
|
+
-- Dispatch ----------------------------------------------------------------
|
|
472
|
+
|
|
473
|
+
--[[
|
|
474
|
+
What a command looks like to the menu above the prompt.
|
|
475
|
+
|
|
476
|
+
Deliberately the same three fields `help` prints. A user who learns the
|
|
477
|
+
surface from the menu and a user who learns it from `help` should be
|
|
478
|
+
learning the same thing, and two descriptions of one command are two things
|
|
479
|
+
to keep in step.
|
|
480
|
+
]]
|
|
481
|
+
export type Suggestion = {
|
|
482
|
+
name: string,
|
|
483
|
+
usage: string,
|
|
484
|
+
summary: string,
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
--[[
|
|
488
|
+
Commands starting with `prefix`, described.
|
|
489
|
+
|
|
490
|
+
An empty prefix answers with everything, which is what the menu shows the
|
|
491
|
+
moment the field is focused: the old row said "type a command" and then left
|
|
492
|
+
the reader to guess which ones existed.
|
|
493
|
+
|
|
494
|
+
Sorted so the list does not reorder itself between keystrokes, which is
|
|
495
|
+
exactly the sort of movement that makes a suggestion unreadable.
|
|
496
|
+
]]
|
|
497
|
+
function Commands.suggest(prefix: string): { Suggestion }
|
|
498
|
+
local matches: { Suggestion } = {}
|
|
499
|
+
for _, entry in entries do
|
|
500
|
+
if string.sub(entry.name, 1, #prefix) == prefix then
|
|
501
|
+
table.insert(matches, {
|
|
502
|
+
name = entry.name,
|
|
503
|
+
usage = entry.usage,
|
|
504
|
+
summary = entry.summary,
|
|
505
|
+
})
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
table.sort(matches, function(a, b)
|
|
509
|
+
return a.name < b.name
|
|
510
|
+
end)
|
|
511
|
+
return matches
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
--[[
|
|
515
|
+
Command names starting with `prefix`, for Tab and for the hint.
|
|
516
|
+
|
|
517
|
+
Built on `suggest` rather than beside it, so the two can never disagree
|
|
518
|
+
about what matches.
|
|
519
|
+
]]
|
|
520
|
+
function Commands.complete(prefix: string): { string }
|
|
521
|
+
local names: { string } = {}
|
|
522
|
+
for _, suggestion in Commands.suggest(prefix) do
|
|
523
|
+
table.insert(names, suggestion.name)
|
|
524
|
+
end
|
|
525
|
+
return names
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
--[[
|
|
529
|
+
The nearest command to something that is not one.
|
|
530
|
+
|
|
531
|
+
Only ever a hint, and only for a single edit's distance, because the whole
|
|
532
|
+
point of the free-text path is that an unrecognised word is usually a
|
|
533
|
+
sentence rather than a typo. Suggesting `doctor` for "door" would be worse
|
|
534
|
+
than saying nothing.
|
|
535
|
+
]]
|
|
536
|
+
local function nearest(word: string): string?
|
|
537
|
+
for _, entry in entries do
|
|
538
|
+
local name = entry.name
|
|
539
|
+
if math.abs(#name - #word) <= 1 then
|
|
540
|
+
-- Cheap and sufficient: a shared prefix of everything but the last
|
|
541
|
+
-- character or two catches the transpositions and dropped letters
|
|
542
|
+
-- that actually happen at a prompt.
|
|
543
|
+
local shared = 0
|
|
544
|
+
while shared < math.min(#name, #word) and string.sub(name, shared + 1, shared + 1) == string.sub(word, shared + 1, shared + 1) do
|
|
545
|
+
shared += 1
|
|
546
|
+
end
|
|
547
|
+
if shared >= #name - 1 and shared >= 3 then
|
|
548
|
+
return name
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
return nil
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
--[[
|
|
556
|
+
Runs one submitted line.
|
|
557
|
+
|
|
558
|
+
Everything here is already off the input's thread -- `Prompt` spawns it --
|
|
559
|
+
so a remote command is free to block on the bridge without freezing the
|
|
560
|
+
field the user is typing into.
|
|
561
|
+
]]
|
|
562
|
+
function Commands.run(line: string)
|
|
563
|
+
local trimmed = string.match(line, "^%s*(.-)%s*$") or ""
|
|
564
|
+
if trimmed == "" then
|
|
565
|
+
return
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
-- Echoed before anything runs, so the log shows what was asked as well as
|
|
569
|
+
-- what came back. Without it a command that prints one line looks like a
|
|
570
|
+
-- line that appeared on its own.
|
|
571
|
+
Console.log("call", trimmed, "you")
|
|
572
|
+
|
|
573
|
+
local words: { string } = {}
|
|
574
|
+
for word in string.gmatch(trimmed, "%S+") do
|
|
575
|
+
table.insert(words, word)
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
local head = string.lower(words[1])
|
|
579
|
+
-- `?` is the one alias, because it is what people type before they have
|
|
580
|
+
-- learned there is a `help`.
|
|
581
|
+
if head == "?" then
|
|
582
|
+
head = "help"
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
local entry = byName[head]
|
|
586
|
+
if entry == nil then
|
|
587
|
+
--[[
|
|
588
|
+
Not a command, so it is a request.
|
|
589
|
+
|
|
590
|
+
A single mistyped word is the one case worth querying: it is short,
|
|
591
|
+
it matches something closely, and sending it to an agent would spend
|
|
592
|
+
real money answering a typo.
|
|
593
|
+
]]
|
|
594
|
+
if #words == 1 then
|
|
595
|
+
local guess = nearest(head)
|
|
596
|
+
if guess ~= nil then
|
|
597
|
+
Console.log("dim", string.format("no command %q -- did you mean %s?", head, guess))
|
|
598
|
+
return
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
Console.setPromptBusy(true)
|
|
602
|
+
remote("prompt", words, trimmed)
|
|
603
|
+
return
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
local args = table.move(words, 2, #words, 1, {} :: { string })
|
|
607
|
+
|
|
608
|
+
if entry.remote then
|
|
609
|
+
remote(entry.name, args, trimmed)
|
|
610
|
+
return
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
local run = entry.run
|
|
614
|
+
if run ~= nil then
|
|
615
|
+
local ok, err = pcall(run, args)
|
|
616
|
+
if not ok then
|
|
617
|
+
Console.log("error", string.format("%s failed", entry.name), tostring(err))
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
return Commands
|