@el4cteo/rbx-studio-mcp 0.8.4 → 0.8.6
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 +20 -3
- package/dist/bridge/rpc.js +1 -1
- package/dist/bridge/rpc.js.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/apidump.js +75 -0
- package/dist/lib/apidump.js.map +1 -1
- package/dist/lib/errors.js +5 -4
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/format.js +92 -25
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/notices.js +29 -0
- package/dist/lib/notices.js.map +1 -0
- package/dist/lib/protocol.js.map +1 -1
- package/dist/lib/sync.js +1141 -0
- package/dist/lib/sync.js.map +1 -0
- package/dist/lib/syncplan.js +338 -0
- package/dist/lib/syncplan.js.map +1 -0
- package/dist/lib/tool.js +9 -2
- package/dist/lib/tool.js.map +1 -1
- package/dist/tools/discover.js +6 -1
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/exec.js +5 -0
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/instances.js +2 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +28 -3
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/screenshot.js +7 -3
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/scripts.js +110 -28
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/sync.js +169 -0
- package/dist/tools/sync.js.map +1 -0
- package/package.json +4 -4
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Dispatch.luau +131 -90
- package/plugin/src/ExecRuntime.luau +190 -168
- package/plugin/src/LogBuffer.luau +38 -9
- package/plugin/src/Paths.luau +42 -0
- package/plugin/src/Phrase.luau +41 -0
- package/plugin/src/ScriptEdit.luau +94 -8
- package/plugin/src/Serialize.luau +16 -1
- package/plugin/src/Transport.luau +5 -2
- package/plugin/src/Undo.luau +74 -10
- package/plugin/src/handlers/Capture.luau +818 -809
- package/plugin/src/handlers/Debug.luau +19 -16
- package/plugin/src/handlers/Discover.luau +31 -1
- package/plugin/src/handlers/Perf.luau +100 -21
- package/plugin/src/handlers/Scripts.luau +274 -90
- package/plugin/src/handlers/Sync.luau +968 -0
- package/plugin/src/init.server.luau +33 -2
- package/scripts/build.mjs +14 -0
- package/scripts/sync-fake.mjs +191 -0
- package/scripts/test-live-sync-scale.mjs +150 -0
- package/scripts/test-live-sync.mjs +232 -0
- package/scripts/test-live-tools.mjs +6 -1
- package/scripts/test-plugin.mjs +16 -0
- package/scripts/test-results.mjs +41 -0
- package/scripts/test-sync-more.mjs +228 -0
- package/scripts/test-sync.mjs +245 -0
- package/dist/tools/spatial.js +0 -135
- package/dist/tools/spatial.js.map +0 -1
- package/dist/tools/upload.js +0 -294
- package/dist/tools/upload.js.map +0 -1
|
@@ -441,7 +441,7 @@ function Debug.clear(params: { [string]: any }): { [string]: any }
|
|
|
441
441
|
local ok, removed = pcall(function()
|
|
442
442
|
return service:RemoveBreakpoint(target, line)
|
|
443
443
|
end)
|
|
444
|
-
untrack(target, line)
|
|
444
|
+
if ok and removed == true then untrack(target, line) end
|
|
445
445
|
return { removed = ok and removed == true, path = Paths.of(target), line = line }
|
|
446
446
|
end
|
|
447
447
|
|
|
@@ -458,27 +458,29 @@ function Debug.clear(params: { [string]: any }): { [string]: any }
|
|
|
458
458
|
local removedLines: { number } = {}
|
|
459
459
|
if lines then
|
|
460
460
|
for lineNumber in lines do
|
|
461
|
-
local ok = pcall(function()
|
|
462
|
-
service:RemoveBreakpoint(target, lineNumber)
|
|
461
|
+
local ok, removed = pcall(function()
|
|
462
|
+
return service:RemoveBreakpoint(target, lineNumber)
|
|
463
463
|
end)
|
|
464
|
-
if ok then
|
|
464
|
+
if ok and removed == true then
|
|
465
465
|
table.insert(removedLines, lineNumber)
|
|
466
|
+
lines[lineNumber] = nil
|
|
466
467
|
end
|
|
467
468
|
end
|
|
468
|
-
trackedBreakpoints[target] = nil
|
|
469
|
+
if next(lines) == nil then trackedBreakpoints[target] = nil end
|
|
469
470
|
end
|
|
470
471
|
table.sort(removedLines)
|
|
471
472
|
return { removed = #removedLines > 0, path = Paths.of(target), lines = removedLines }
|
|
472
473
|
end
|
|
473
474
|
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
475
|
+
-- Only remove MCP-owned breakpoints, preserving those set in Studio by users.
|
|
476
|
+
for target, lines in trackedBreakpoints do
|
|
477
|
+
for lineNumber in lines do
|
|
478
|
+
local ok, removed = pcall(function() return service:RemoveBreakpoint(target, lineNumber) end)
|
|
479
|
+
if ok and removed then lines[lineNumber] = nil end
|
|
480
|
+
end
|
|
481
|
+
if next(lines) == nil then trackedBreakpoints[target] = nil end
|
|
479
482
|
end
|
|
480
|
-
|
|
481
|
-
return { cleared = true }
|
|
483
|
+
return { cleared = next(trackedBreakpoints) == nil }
|
|
482
484
|
end
|
|
483
485
|
|
|
484
486
|
function Debug.snapshots(params: { [string]: any }): { [string]: any }
|
|
@@ -490,15 +492,16 @@ function Debug.snapshots(params: { [string]: any }): { [string]: any }
|
|
|
490
492
|
table.insert(out, snapshots[index])
|
|
491
493
|
end
|
|
492
494
|
|
|
495
|
+
local total, lost = #snapshots, overflow
|
|
493
496
|
if params.clear == true then
|
|
494
|
-
|
|
495
|
-
overflow = 0
|
|
497
|
+
for index = #snapshots, first, -1 do table.remove(snapshots, index) end
|
|
498
|
+
if #snapshots == 0 then overflow = 0 end
|
|
496
499
|
end
|
|
497
500
|
|
|
498
501
|
return {
|
|
499
502
|
items = out,
|
|
500
|
-
total =
|
|
501
|
-
overflow = if
|
|
503
|
+
total = total,
|
|
504
|
+
overflow = if lost > 0 then lost else nil,
|
|
502
505
|
installed = installed,
|
|
503
506
|
}
|
|
504
507
|
end
|
|
@@ -660,10 +660,40 @@ function Discover.find(params: { [string]: any }): { [string]: any }
|
|
|
660
660
|
|
|
661
661
|
stableOrder(matched)
|
|
662
662
|
|
|
663
|
+
--[[
|
|
664
|
+
`properties` reads named properties onto each row returned.
|
|
665
|
+
|
|
666
|
+
The usual follow-up to a find is an inspect of every match for the one or
|
|
667
|
+
two values that decide what to do with it -- a call per row, or one wide
|
|
668
|
+
call returning every property. Projecting just those names here makes it
|
|
669
|
+
one call returning what was asked. Names that cannot be read on a row are
|
|
670
|
+
listed on that row, since a class that lacks the property is an answer too.
|
|
671
|
+
]]
|
|
672
|
+
local projected = params.properties
|
|
673
|
+
if projected ~= nil and (typeof(projected) ~= "table" or #projected > 16) then
|
|
674
|
+
Dispatch.fail("BAD_PARAMS", "`properties` must be a list of at most 16 property names.")
|
|
675
|
+
end
|
|
676
|
+
|
|
663
677
|
local items: { { [string]: any } } = {}
|
|
664
678
|
local memo: Paths.NameIndex = {}
|
|
665
679
|
for index = offset + 1, math.min(offset + limit, #matched) do
|
|
666
|
-
|
|
680
|
+
local instance = matched[index] :: Instance
|
|
681
|
+
local entry = summarise(instance, memo)
|
|
682
|
+
if projected ~= nil then
|
|
683
|
+
local values: { [string]: any } = {}
|
|
684
|
+
local unreadable: { string } = {}
|
|
685
|
+
for _, name in projected do
|
|
686
|
+
local ok, value = Serialize.readProperty(instance, tostring(name))
|
|
687
|
+
if ok then
|
|
688
|
+
values[tostring(name)] = value
|
|
689
|
+
else
|
|
690
|
+
table.insert(unreadable, tostring(name))
|
|
691
|
+
end
|
|
692
|
+
end
|
|
693
|
+
entry.properties = values
|
|
694
|
+
entry.unreadable = if #unreadable > 0 then unreadable else nil
|
|
695
|
+
end
|
|
696
|
+
table.insert(items, entry)
|
|
667
697
|
end
|
|
668
698
|
|
|
669
699
|
return {
|
|
@@ -28,6 +28,8 @@ local Scope = require(script.Parent.Parent.Scope)
|
|
|
28
28
|
-- swamp any context window.
|
|
29
29
|
local MAX_LOG_ENTRIES = 500
|
|
30
30
|
local DEFAULT_LOG_ENTRIES = 100
|
|
31
|
+
-- Characters of log rows one console read picks. See `take` in Perf.console.
|
|
32
|
+
local CONSOLE_BUDGET = 16_000
|
|
31
33
|
|
|
32
34
|
-- Profiling is a blocking wait, so the ceiling is what a caller will sit through
|
|
33
35
|
-- rather than what the profiler can manage.
|
|
@@ -116,21 +118,21 @@ function Perf.console(params: { [string]: any }): { [string]: any }
|
|
|
116
118
|
local oldest = if #history > 0 then history[1].sequence or 1 else latest + 1
|
|
117
119
|
local missed = if since ~= nil then math.max(0, oldest - since - 1) else 0
|
|
118
120
|
|
|
119
|
-
local
|
|
120
|
-
|
|
121
|
+
local mode = params.mode or "tail"
|
|
122
|
+
if mode ~= "tail" and mode ~= "drain" then
|
|
123
|
+
Dispatch.fail("BAD_PARAMS", string.format("%q is not a console mode.", tostring(mode)), "Use tail or drain.")
|
|
124
|
+
end
|
|
121
125
|
|
|
126
|
+
local matches: { LogBuffer.Entry } = {}
|
|
122
127
|
for _, item in history do
|
|
123
128
|
if since ~= nil and (item.sequence or 0) <= since then continue end
|
|
124
|
-
|
|
125
|
-
if level and kind ~= level then
|
|
129
|
+
if level and item.level ~= level then
|
|
126
130
|
continue
|
|
127
131
|
end
|
|
128
|
-
|
|
129
|
-
local message = item.message
|
|
130
132
|
if pattern then
|
|
131
133
|
-- An invalid pattern raises rather than failing to match, so it is
|
|
132
134
|
-- reported as a pattern problem instead of an empty result.
|
|
133
|
-
local valid, position = pcall(string.find, message, pattern)
|
|
135
|
+
local valid, position = pcall(string.find, item.message, pattern)
|
|
134
136
|
if not valid then
|
|
135
137
|
Dispatch.fail(
|
|
136
138
|
"BAD_PATTERN",
|
|
@@ -142,34 +144,111 @@ function Perf.console(params: { [string]: any }): { [string]: any }
|
|
|
142
144
|
continue
|
|
143
145
|
end
|
|
144
146
|
end
|
|
147
|
+
table.insert(matches, item)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
--[[
|
|
151
|
+
Rows are picked against a size budget as well as `limit`.
|
|
145
152
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
153
|
+
One noisy script printing long lines used to fill a read with a few
|
|
154
|
+
hundred of them, which the server then cut down to fit its response
|
|
155
|
+
limit -- dropping lines this side had already reported as delivered. A
|
|
156
|
+
budget here means what is picked is what is shown. The first row is always
|
|
157
|
+
taken, whatever its size, so a single huge entry cannot stall a drain.
|
|
158
|
+
]]
|
|
159
|
+
local selected: { { [string]: any } } = {}
|
|
160
|
+
local budget = CONSOLE_BUDGET
|
|
161
|
+
local function take(item: LogBuffer.Entry): boolean
|
|
162
|
+
local stack = item.stack
|
|
163
|
+
local cost = #item.message + 300
|
|
164
|
+
+ (if stack then #stack + #string.split(stack, "\n") * 8 else 0)
|
|
165
|
+
+ (if item.source then #item.source else 0)
|
|
166
|
+
+ (if item.context then #item.context else 0)
|
|
167
|
+
if #selected >= limit or (cost > budget and #selected > 0) then
|
|
168
|
+
return false
|
|
169
|
+
end
|
|
170
|
+
budget -= cost
|
|
171
|
+
table.insert(selected, {
|
|
172
|
+
sequence = item.sequence,
|
|
173
|
+
updated = item.updated,
|
|
174
|
+
level = item.level,
|
|
175
|
+
message = item.message,
|
|
150
176
|
timestamp = item.timestamp,
|
|
151
|
-
stack =
|
|
177
|
+
stack = stack,
|
|
152
178
|
source = item.source,
|
|
153
179
|
context = item.context,
|
|
154
180
|
})
|
|
181
|
+
return true
|
|
155
182
|
end
|
|
156
183
|
|
|
157
|
-
--
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
184
|
+
--[[
|
|
185
|
+
`tail` answers "what just happened": the newest matches, and a cursor at
|
|
186
|
+
the end of the log, so a following read sees only what comes after.
|
|
187
|
+
|
|
188
|
+
`drain` answers "what happened, all of it": the oldest unread matches,
|
|
189
|
+
and a cursor just before the first one not shown, so reading on in a loop
|
|
190
|
+
delivers every match exactly once however busy the log is. That only
|
|
191
|
+
holds with the same filters each time -- the cursor is a position, and it
|
|
192
|
+
does not remember what the filter skipped.
|
|
193
|
+
]]
|
|
194
|
+
local watermark = latest
|
|
195
|
+
if mode == "drain" then
|
|
196
|
+
for _, item in matches do
|
|
197
|
+
if not take(item) then
|
|
198
|
+
watermark = (item.sequence or latest + 1) - 1
|
|
199
|
+
break
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
for index = #matches, 1, -1 do
|
|
204
|
+
if not take(matches[index]) then
|
|
205
|
+
break
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
-- Picked newest first; read oldest first, the way the Output window does.
|
|
209
|
+
local ordered: { { [string]: any } } = {}
|
|
210
|
+
for index = #selected, 1, -1 do
|
|
211
|
+
table.insert(ordered, selected[index])
|
|
212
|
+
end
|
|
213
|
+
selected = ordered
|
|
214
|
+
end
|
|
215
|
+
local consumed = #selected
|
|
216
|
+
|
|
217
|
+
-- Identical lines collapse into one row with a count, after the cursor is
|
|
218
|
+
-- settled: grouping changes how rows are shown, never which were consumed.
|
|
219
|
+
if params.group == true then
|
|
220
|
+
local grouped: { { [string]: any } } = {}
|
|
221
|
+
local byKey: { [string]: { [string]: any } } = {}
|
|
222
|
+
for _, item in selected do
|
|
223
|
+
local key = table.concat({
|
|
224
|
+
item.level, item.message, item.source or "", item.stack or "", item.context or "",
|
|
225
|
+
if item.updated then "u" else "",
|
|
226
|
+
}, "\0")
|
|
227
|
+
local group = byKey[key]
|
|
228
|
+
if group then
|
|
229
|
+
group.count += 1
|
|
230
|
+
group.lastTimestamp = item.timestamp
|
|
231
|
+
else
|
|
232
|
+
item.count = 1
|
|
233
|
+
item.firstTimestamp = item.timestamp
|
|
234
|
+
item.lastTimestamp = item.timestamp
|
|
235
|
+
byKey[key] = item
|
|
236
|
+
table.insert(grouped, item)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
selected = grouped
|
|
162
240
|
end
|
|
163
241
|
|
|
164
242
|
return {
|
|
165
|
-
items =
|
|
166
|
-
total =
|
|
167
|
-
dropped =
|
|
243
|
+
items = selected,
|
|
244
|
+
total = #matches,
|
|
245
|
+
dropped = if mode == "tail" then #matches - consumed else 0,
|
|
246
|
+
hasMore = mode == "drain" and consumed < #matches,
|
|
168
247
|
-- Lines lost to the buffer's own capacity, as opposed to ones trimmed to
|
|
169
248
|
-- satisfy this call's limit. Only the first is a reason to worry.
|
|
170
249
|
evicted = if since ~= nil then (if missed > 0 then missed else nil) else (if evicted > 0 then evicted else nil),
|
|
171
250
|
recordingSeconds = recordingSeconds,
|
|
172
|
-
nextCursor = token .. ":" .. tostring(
|
|
251
|
+
nextCursor = token .. ":" .. tostring(watermark),
|
|
173
252
|
player = if player then player.Name else nil,
|
|
174
253
|
capturing = if player then LogBuffer.clientReady(player) else true,
|
|
175
254
|
}
|