@el4cteo/rbx-studio-mcp 0.3.0 → 0.3.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 +20 -26
- package/dist/index.js +1 -1
- package/dist/lib/format.js +18 -2
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/pluginbuild.js +23 -13
- package/dist/lib/pluginbuild.js.map +1 -1
- package/dist/tools/api.js +20 -1
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/debug.js +18 -8
- package/dist/tools/debug.js.map +1 -1
- package/dist/tools/discover.js +5 -0
- package/dist/tools/discover.js.map +1 -1
- package/dist/tools/instances.js +49 -2
- package/dist/tools/instances.js.map +1 -1
- package/dist/tools/perf.js +11 -1
- package/dist/tools/perf.js.map +1 -1
- package/dist/tools/scripts.js +27 -3
- package/dist/tools/scripts.js.map +1 -1
- package/dist/tools/session.js +16 -0
- package/dist/tools/session.js.map +1 -1
- package/package.json +1 -1
- package/plugin/src/Config.luau +1 -1
- package/plugin/src/Console.luau +257 -116
- package/plugin/src/Paths.luau +138 -69
- package/plugin/src/Serialize.luau +43 -3
- package/plugin/src/ThemePicker.luau +458 -0
- package/plugin/src/Themes/Aurora.luau +147 -0
- package/plugin/src/Themes/Blueprint.luau +213 -0
- package/plugin/src/Themes/Draw.luau +177 -0
- package/plugin/src/Themes/Lattice.luau +304 -0
- package/plugin/src/Themes/Nebula.luau +182 -0
- package/plugin/src/Themes/Observatory.luau +200 -0
- package/plugin/src/Themes/Orbit.luau +268 -0
- package/plugin/src/Themes/Phosphor.luau +200 -0
- package/plugin/src/Themes/Theme.luau +121 -0
- package/plugin/src/Themes/Void.luau +230 -0
- package/plugin/src/Themes/init.luau +116 -0
- package/plugin/src/Visuals.luau +788 -907
- package/plugin/src/handlers/Discover.luau +75 -1
- package/plugin/src/handlers/Exec.luau +17 -5
- package/plugin/src/handlers/Instances.luau +35 -3
- package/plugin/src/handlers/Scripts.luau +58 -2
- package/plugin/src/init.server.luau +386 -354
package/plugin/src/Paths.luau
CHANGED
|
@@ -95,25 +95,106 @@ local function siblingIndex(instance: Instance, memo: NameIndex?): number?
|
|
|
95
95
|
return nil
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
--[[
|
|
99
|
+
Names a parent's children for an error message, collapsing repeats.
|
|
100
|
+
|
|
101
|
+
Same-named siblings are ordinary in Roblox, and listing them one by one spent
|
|
102
|
+
the whole budget saying "Part, Part, Part" 25 times -- which tells a reader
|
|
103
|
+
nothing and hides the names that would have helped. Repeats are counted
|
|
104
|
+
instead, and the count is exactly what says an index is needed.
|
|
105
|
+
]]
|
|
98
106
|
local function childNames(parent: Instance, limit: number): string
|
|
99
|
-
local
|
|
107
|
+
local order: { string } = {}
|
|
108
|
+
local counts: { [string]: number } = {}
|
|
100
109
|
for _, child in parent:GetChildren() do
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
table.insert(
|
|
104
|
-
break
|
|
110
|
+
if counts[child.Name] == nil then
|
|
111
|
+
counts[child.Name] = 0
|
|
112
|
+
table.insert(order, child.Name)
|
|
105
113
|
end
|
|
114
|
+
counts[child.Name] += 1
|
|
106
115
|
end
|
|
107
|
-
|
|
116
|
+
|
|
117
|
+
if #order == 0 then
|
|
108
118
|
return "(no children)"
|
|
109
119
|
end
|
|
120
|
+
|
|
121
|
+
local names: { string } = {}
|
|
122
|
+
for _, name in order do
|
|
123
|
+
if #names >= limit then
|
|
124
|
+
table.insert(names, string.format("... %d more", #order - limit))
|
|
125
|
+
break
|
|
126
|
+
end
|
|
127
|
+
local count = counts[name]
|
|
128
|
+
table.insert(names, if count > 1 then string.format("%s (x%d)", name, count) else name)
|
|
129
|
+
end
|
|
110
130
|
return table.concat(names, ", ")
|
|
111
131
|
end
|
|
112
132
|
|
|
133
|
+
--[[
|
|
134
|
+
Finds one child by a single segment, honouring "Name[2]" and, at the root,
|
|
135
|
+
fetching services by class name -- they are not always present as children
|
|
136
|
+
until first accessed.
|
|
137
|
+
|
|
138
|
+
The second return is a diagnostic for the one miss a caller cannot work out
|
|
139
|
+
from the sibling list: an index past the end of a name that does exist.
|
|
140
|
+
]]
|
|
141
|
+
local function childBySegment(
|
|
142
|
+
parent: Instance,
|
|
143
|
+
segment: string,
|
|
144
|
+
atRoot: boolean
|
|
145
|
+
): (Instance?, string?)
|
|
146
|
+
local name, ordinal = parseSegment(segment)
|
|
147
|
+
|
|
148
|
+
if ordinal then
|
|
149
|
+
local list = childrenByName(parent, nil)[name]
|
|
150
|
+
if list == nil then
|
|
151
|
+
return nil, nil
|
|
152
|
+
end
|
|
153
|
+
local child = list[ordinal]
|
|
154
|
+
if child then
|
|
155
|
+
return child, nil
|
|
156
|
+
end
|
|
157
|
+
return nil,
|
|
158
|
+
string.format(
|
|
159
|
+
'"%s" has %d child%s named "%s", so [%d] is past the end. Indexes are '
|
|
160
|
+
.. "1-based and shift when siblings are added or removed -- read a "
|
|
161
|
+
.. "fresh path from `find` or `tree`.",
|
|
162
|
+
parent:GetFullName(),
|
|
163
|
+
#list,
|
|
164
|
+
if #list == 1 then "" else "ren",
|
|
165
|
+
name,
|
|
166
|
+
ordinal
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
if atRoot then
|
|
171
|
+
local ok, service = pcall(function()
|
|
172
|
+
return game:GetService(name :: any)
|
|
173
|
+
end)
|
|
174
|
+
if ok and service then
|
|
175
|
+
return service, nil
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
return parent:FindFirstChild(name), nil
|
|
179
|
+
end
|
|
180
|
+
|
|
113
181
|
--[[
|
|
114
182
|
Resolves a path to an Instance, or raises a NOT_FOUND with the failing
|
|
115
183
|
segment and its siblings. `FindFirstChild` is used rather than indexing so a
|
|
116
184
|
missing child never throws a bare Luau error.
|
|
185
|
+
|
|
186
|
+
Instance names may contain dots -- "Dr. Simon" is an ordinary name, and so is
|
|
187
|
+
"v1.2 backup" -- while the separator is a dot too, so such a name arrives
|
|
188
|
+
here already split into pieces. Each step therefore tries consuming one
|
|
189
|
+
segment first and rejoins further segments only if that leads nowhere, which
|
|
190
|
+
means the search has to BACK TRACK: with both "Dr" and "Dr. Who" present,
|
|
191
|
+
"Workspace.Dr. Who.Head" matches "Dr", fails to find " Who" under it, and
|
|
192
|
+
must return to try "Dr. Who". A first pass without that backtracking left the
|
|
193
|
+
tools emitting `Workspace.Dr. Who` as a path and then refusing to read it
|
|
194
|
+
back, which is worse than either behaviour on its own.
|
|
195
|
+
|
|
196
|
+
Shortest-first keeps the old precedence: where a short name resolves the
|
|
197
|
+
whole remaining path, it still wins over a longer join.
|
|
117
198
|
]]
|
|
118
199
|
function Paths.resolve(path: string): Instance
|
|
119
200
|
if typeof(path) ~= "string" or path == "" then
|
|
@@ -130,78 +211,66 @@ function Paths.resolve(path: string): Instance
|
|
|
130
211
|
return game
|
|
131
212
|
end
|
|
132
213
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
else
|
|
153
|
-
nextInstance = current:FindFirstChild(name)
|
|
214
|
+
-- The deepest point any branch of the search reached, so a failure still
|
|
215
|
+
-- reports the segment that actually broke rather than the first one tried.
|
|
216
|
+
local deepest = 0
|
|
217
|
+
local deepestParent: Instance = game
|
|
218
|
+
local deepestNote: string? = nil
|
|
219
|
+
-- The segment text the note is about: with a rejoined name it is not
|
|
220
|
+
-- `segments[failedAt]`, and reporting that instead named "Mr" for a path
|
|
221
|
+
-- that asked for "Mr. X[3]".
|
|
222
|
+
local deepestSegment: string? = nil
|
|
223
|
+
|
|
224
|
+
local function walk(current: Instance, index: number): Instance?
|
|
225
|
+
if index > #segments then
|
|
226
|
+
return current
|
|
227
|
+
end
|
|
228
|
+
if index - 1 > deepest then
|
|
229
|
+
deepest = index - 1
|
|
230
|
+
deepestParent = current
|
|
231
|
+
deepestNote = nil
|
|
232
|
+
deepestSegment = nil
|
|
154
233
|
end
|
|
155
234
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
]]
|
|
166
|
-
if not nextInstance and index < #segments then
|
|
167
|
-
for last = #segments, index + 1, -1 do
|
|
168
|
-
local joined = table.concat(segments, ".", index, last)
|
|
169
|
-
local joinedName, joinedOrdinal = parseSegment(joined)
|
|
170
|
-
local found: Instance? = nil
|
|
171
|
-
if joinedOrdinal then
|
|
172
|
-
local list = childrenByName(current, nil)[joinedName]
|
|
173
|
-
found = if list then list[joinedOrdinal] else nil
|
|
174
|
-
else
|
|
175
|
-
found = current:FindFirstChild(joinedName)
|
|
176
|
-
end
|
|
235
|
+
for last = index, #segments do
|
|
236
|
+
local joined = table.concat(segments, ".", index, last)
|
|
237
|
+
local child, note = childBySegment(current, joined, index == 1)
|
|
238
|
+
if note and index - 1 == deepest then
|
|
239
|
+
deepestNote = note
|
|
240
|
+
deepestSegment = joined
|
|
241
|
+
end
|
|
242
|
+
if child then
|
|
243
|
+
local found = walk(child, last + 1)
|
|
177
244
|
if found then
|
|
178
|
-
|
|
179
|
-
consumed = last - index + 1
|
|
180
|
-
break
|
|
245
|
+
return found
|
|
181
246
|
end
|
|
182
247
|
end
|
|
183
248
|
end
|
|
249
|
+
return nil
|
|
250
|
+
end
|
|
184
251
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
-- the type checker so `current` stays non-optional below.
|
|
189
|
-
return Dispatch.fail(
|
|
190
|
-
"NOT_FOUND",
|
|
191
|
-
string.format('No instance at "%s".', partial),
|
|
192
|
-
string.format(
|
|
193
|
-
'"%s" has no child named "%s". It does have: %s',
|
|
194
|
-
current:GetFullName(),
|
|
195
|
-
segment,
|
|
196
|
-
childNames(current, 25)
|
|
197
|
-
)
|
|
198
|
-
)
|
|
199
|
-
end
|
|
200
|
-
current = nextInstance
|
|
201
|
-
index += consumed
|
|
252
|
+
local resolved = walk(game, 1)
|
|
253
|
+
if resolved then
|
|
254
|
+
return resolved
|
|
202
255
|
end
|
|
203
256
|
|
|
204
|
-
|
|
257
|
+
local failedAt = deepest + 1
|
|
258
|
+
local reached = if deepest > 0 then table.concat(segments, ".", 1, deepest) .. "." else ""
|
|
259
|
+
local named = if deepestSegment
|
|
260
|
+
then reached .. deepestSegment
|
|
261
|
+
else table.concat(segments, ".", 1, failedAt)
|
|
262
|
+
return Dispatch.fail(
|
|
263
|
+
"NOT_FOUND",
|
|
264
|
+
string.format('No instance at "%s".', named),
|
|
265
|
+
if deepestNote
|
|
266
|
+
then deepestNote
|
|
267
|
+
else string.format(
|
|
268
|
+
'"%s" has no child named "%s". It does have: %s',
|
|
269
|
+
deepestParent:GetFullName(),
|
|
270
|
+
segments[failedAt],
|
|
271
|
+
childNames(deepestParent, 25)
|
|
272
|
+
)
|
|
273
|
+
)
|
|
205
274
|
end
|
|
206
275
|
|
|
207
276
|
--[[
|
|
@@ -151,6 +151,31 @@ function Serialize.readProperty(instance: Instance, name: string): (boolean, any
|
|
|
151
151
|
return true, Serialize.value(value)
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
--[[
|
|
155
|
+
Pulls the numbers out of a composite value written as text.
|
|
156
|
+
|
|
157
|
+
This used to be a single `-?%d+%.?%d*` scan, which is not the grammar Luau
|
|
158
|
+
numbers actually have: "1e3, 0, .5" came back as 1, 3 and 0, so a Position
|
|
159
|
+
written in scientific notation was silently set to a different point in the
|
|
160
|
+
world and reported as applied. Splitting on the separators and handing each
|
|
161
|
+
token to `tonumber` means this agrees with the scalar path, which always
|
|
162
|
+
used `tonumber` -- the two disagreeing about what counts as a number was the
|
|
163
|
+
whole bug.
|
|
164
|
+
|
|
165
|
+
Non-numeric tokens are dropped, so "Vector3.new(1, 2, 3)" and "{1, 2, 3}"
|
|
166
|
+
still read as three numbers.
|
|
167
|
+
]]
|
|
168
|
+
local function scanTokens(text: string): { number }
|
|
169
|
+
local numbers: { number } = {}
|
|
170
|
+
for token in string.gmatch(text, "[^,%s%(%)%[%]{}<>]+") do
|
|
171
|
+
local value = tonumber(token)
|
|
172
|
+
if value then
|
|
173
|
+
table.insert(numbers, value)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
return numbers
|
|
177
|
+
end
|
|
178
|
+
|
|
154
179
|
--[[
|
|
155
180
|
Parses a serialized string back into a Roblox value, given the target type
|
|
156
181
|
from the API dump. Returns ok=false with a reason the agent can act on
|
|
@@ -176,7 +201,22 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
176
201
|
if kind == "boolean" then
|
|
177
202
|
return true, text, nil
|
|
178
203
|
end
|
|
179
|
-
|
|
204
|
+
if kind == "number" then
|
|
205
|
+
return true, text ~= 0, nil
|
|
206
|
+
end
|
|
207
|
+
-- This used to be `text == "true"`, which quietly turned "True", "TRUE"
|
|
208
|
+
-- and "1" into false and reported the write as applied. A bool that is
|
|
209
|
+
-- silently the opposite of what was asked is the worst failure this
|
|
210
|
+
-- module can produce, so near misses are accepted and anything else is
|
|
211
|
+
-- refused rather than guessed at.
|
|
212
|
+
local lowered = string.lower(tostring(text))
|
|
213
|
+
if lowered == "true" or lowered == "1" or lowered == "yes" then
|
|
214
|
+
return true, true, nil
|
|
215
|
+
end
|
|
216
|
+
if lowered == "false" or lowered == "0" or lowered == "no" then
|
|
217
|
+
return true, false, nil
|
|
218
|
+
end
|
|
219
|
+
return false, nil, string.format('expected true or false, got "%s"', tostring(text))
|
|
180
220
|
end
|
|
181
221
|
if NUMERIC_TYPES[valueType] then
|
|
182
222
|
local parsed = tonumber(text)
|
|
@@ -188,8 +228,8 @@ function Serialize.parse(text: any, valueType: string): (boolean, any, string?)
|
|
|
188
228
|
|
|
189
229
|
local numbers: { number } = {}
|
|
190
230
|
if kind == "string" then
|
|
191
|
-
for
|
|
192
|
-
table.insert(numbers,
|
|
231
|
+
for _, token in scanTokens(text) do
|
|
232
|
+
table.insert(numbers, token)
|
|
193
233
|
end
|
|
194
234
|
elseif kind == "table" then
|
|
195
235
|
for _, item in text :: { any } do
|