robloxstudio-mcp 2.7.0-next.4 → 2.7.0-next.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/dist/index.js
CHANGED
|
@@ -158,7 +158,7 @@ var TOOL_HANDLERS = {
|
|
|
158
158
|
}),
|
|
159
159
|
get_script_source: (tools, body) => tools.getScriptSource(body.instancePath, body.startLine, body.endLine),
|
|
160
160
|
set_script_source: (tools, body) => tools.setScriptSource(body.instancePath, body.source),
|
|
161
|
-
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string),
|
|
161
|
+
edit_script_lines: (tools, body) => tools.editScriptLines(body.instancePath, body.old_string, body.new_string, body.startLine),
|
|
162
162
|
insert_script_lines: (tools, body) => tools.insertScriptLines(body.instancePath, body.afterLine, body.newContent),
|
|
163
163
|
delete_script_lines: (tools, body) => tools.deleteScriptLines(body.instancePath, body.startLine, body.endLine),
|
|
164
164
|
get_attribute: (tools, body) => tools.getAttribute(body.instancePath, body.attributeName),
|
|
@@ -1629,11 +1629,14 @@ ${code}`
|
|
|
1629
1629
|
]
|
|
1630
1630
|
};
|
|
1631
1631
|
}
|
|
1632
|
-
async editScriptLines(instancePath, oldString, newString) {
|
|
1632
|
+
async editScriptLines(instancePath, oldString, newString, startLine) {
|
|
1633
1633
|
if (!instancePath || typeof oldString !== "string" || typeof newString !== "string") {
|
|
1634
1634
|
throw new Error("Instance path, old_string, and new_string are required for edit_script_lines");
|
|
1635
1635
|
}
|
|
1636
|
-
const
|
|
1636
|
+
const payload = { instancePath, old_string: oldString, new_string: newString };
|
|
1637
|
+
if (startLine !== void 0)
|
|
1638
|
+
payload.startLine = startLine;
|
|
1639
|
+
const response = await this.client.request("/api/edit-script-lines", payload);
|
|
1637
1640
|
return {
|
|
1638
1641
|
content: [
|
|
1639
1642
|
{
|
|
@@ -3031,7 +3034,7 @@ var RobloxStudioMCPServer = class {
|
|
|
3031
3034
|
case "set_script_source":
|
|
3032
3035
|
return await this.tools.setScriptSource(args?.instancePath, args?.source);
|
|
3033
3036
|
case "edit_script_lines":
|
|
3034
|
-
return await this.tools.editScriptLines(args?.instancePath, args?.old_string, args?.new_string);
|
|
3037
|
+
return await this.tools.editScriptLines(args?.instancePath, args?.old_string, args?.new_string, args?.startLine);
|
|
3035
3038
|
case "insert_script_lines":
|
|
3036
3039
|
return await this.tools.insertScriptLines(args?.instancePath, args?.afterLine, args?.newContent);
|
|
3037
3040
|
case "delete_script_lines":
|
|
@@ -3784,7 +3787,7 @@ var TOOL_DEFINITIONS = [
|
|
|
3784
3787
|
{
|
|
3785
3788
|
name: "edit_script_lines",
|
|
3786
3789
|
category: "write",
|
|
3787
|
-
description: "Replace exact text in a script. old_string must match exactly once in the script (
|
|
3790
|
+
description: "Replace exact text in a script. Without startLine, old_string must match exactly once in the script. Pass startLine (1-indexed, from get_script_source) to anchor the edit to a specific line when old_string is ambiguous (e.g. repeated closing braces).",
|
|
3788
3791
|
inputSchema: {
|
|
3789
3792
|
type: "object",
|
|
3790
3793
|
properties: {
|
|
@@ -3794,11 +3797,15 @@ var TOOL_DEFINITIONS = [
|
|
|
3794
3797
|
},
|
|
3795
3798
|
old_string: {
|
|
3796
3799
|
type: "string",
|
|
3797
|
-
description: "Exact text to find and replace
|
|
3800
|
+
description: "Exact text to find and replace. Must be unique in the script unless startLine is provided."
|
|
3798
3801
|
},
|
|
3799
3802
|
new_string: {
|
|
3800
3803
|
type: "string",
|
|
3801
3804
|
description: "Replacement text"
|
|
3805
|
+
},
|
|
3806
|
+
startLine: {
|
|
3807
|
+
type: "number",
|
|
3808
|
+
description: "Optional 1-indexed line where old_string begins. When provided, skips uniqueness check and requires old_string to match starting at that exact line."
|
|
3802
3809
|
}
|
|
3803
3810
|
},
|
|
3804
3811
|
required: ["instancePath", "old_string", "new_string"]
|
package/package.json
CHANGED
|
@@ -4429,11 +4429,11 @@ local function setScriptSource(requestData)
|
|
|
4429
4429
|
error = `Failed to set script source. UpdateSourceAsync failed: {updateResult}. Direct assignment failed: {directResult}. Replace method failed: {replaceResult}`,
|
|
4430
4430
|
}
|
|
4431
4431
|
end
|
|
4432
|
-
local escapeLuaPattern, escapeLuaReplacement
|
|
4433
4432
|
local function editScriptLines(requestData)
|
|
4434
4433
|
local instancePath = requestData.instancePath
|
|
4435
4434
|
local oldString = requestData.old_string
|
|
4436
4435
|
local newString = requestData.new_string
|
|
4436
|
+
local startLine = requestData.startLine
|
|
4437
4437
|
if not (instancePath ~= "" and instancePath) or oldString == nil or newString == nil then
|
|
4438
4438
|
return {
|
|
4439
4439
|
error = "Instance path, old_string, and new_string are required",
|
|
@@ -4455,31 +4455,55 @@ local function editScriptLines(requestData)
|
|
|
4455
4455
|
local recordingId = beginRecording(`Edit script: {instance.Name}`)
|
|
4456
4456
|
local success, result = pcall(function()
|
|
4457
4457
|
local source = readScriptSource(instance)
|
|
4458
|
-
-- Count occurrences to ensure uniqueness
|
|
4459
|
-
local count = 0
|
|
4460
|
-
local searchPos = 1
|
|
4461
4458
|
local searchLen = #oldString
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
if
|
|
4465
|
-
|
|
4459
|
+
local matchStart
|
|
4460
|
+
if startLine ~= nil then
|
|
4461
|
+
if startLine < 1 then
|
|
4462
|
+
error(`startLine must be >= 1 (got {startLine})`)
|
|
4463
|
+
end
|
|
4464
|
+
local lineStartByte = 1
|
|
4465
|
+
local currentLine = 1
|
|
4466
|
+
while currentLine < startLine do
|
|
4467
|
+
local nlPos = string.find(source, "\n", lineStartByte, true)
|
|
4468
|
+
if nlPos == nil then
|
|
4469
|
+
error(`startLine {startLine} is past end of script ({currentLine} lines)`)
|
|
4470
|
+
end
|
|
4471
|
+
lineStartByte = nlPos + 1
|
|
4472
|
+
currentLine += 1
|
|
4473
|
+
end
|
|
4474
|
+
local candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1)
|
|
4475
|
+
if candidate ~= oldString then
|
|
4476
|
+
error(`old_string does not match at line {startLine}. Use get_script_source to verify the exact text at that line.`)
|
|
4477
|
+
end
|
|
4478
|
+
matchStart = lineStartByte
|
|
4479
|
+
else
|
|
4480
|
+
local count = 0
|
|
4481
|
+
local searchPos = 1
|
|
4482
|
+
local firstMatch
|
|
4483
|
+
while true do
|
|
4484
|
+
local foundStart = string.find(source, oldString, searchPos, true)
|
|
4485
|
+
if foundStart == nil then
|
|
4486
|
+
break
|
|
4487
|
+
end
|
|
4488
|
+
if firstMatch == nil then
|
|
4489
|
+
firstMatch = foundStart
|
|
4490
|
+
end
|
|
4491
|
+
count += 1
|
|
4492
|
+
if count > 1 then
|
|
4493
|
+
break
|
|
4494
|
+
end
|
|
4495
|
+
searchPos = foundStart + searchLen
|
|
4496
|
+
end
|
|
4497
|
+
if count == 0 then
|
|
4498
|
+
error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.")
|
|
4466
4499
|
end
|
|
4467
|
-
count += 1
|
|
4468
4500
|
if count > 1 then
|
|
4469
|
-
|
|
4501
|
+
error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.")
|
|
4470
4502
|
end
|
|
4471
|
-
|
|
4472
|
-
end
|
|
4473
|
-
if count == 0 then
|
|
4474
|
-
error("old_string not found in script")
|
|
4475
|
-
end
|
|
4476
|
-
if count > 1 then
|
|
4477
|
-
error("old_string matches multiple locations. Provide more surrounding context to make it unique")
|
|
4503
|
+
matchStart = firstMatch
|
|
4478
4504
|
end
|
|
4479
|
-
--
|
|
4480
|
-
local
|
|
4481
|
-
local escapedRepl = escapeLuaReplacement(newString)
|
|
4482
|
-
local newSource = string.gsub(source, escaped, escapedRepl, 1)
|
|
4505
|
+
-- Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
4506
|
+
local newSource = string.sub(source, 1, matchStart - 1) .. newString .. string.sub(source, matchStart + searchLen)
|
|
4483
4507
|
ScriptEditorService:UpdateSourceAsync(instance, function()
|
|
4484
4508
|
return newSource
|
|
4485
4509
|
end)
|
|
@@ -4677,10 +4701,10 @@ local function deleteScriptLines(requestData)
|
|
|
4677
4701
|
error = `Failed to delete script lines: {result}`,
|
|
4678
4702
|
}
|
|
4679
4703
|
end
|
|
4680
|
-
function escapeLuaPattern(s)
|
|
4704
|
+
local function escapeLuaPattern(s)
|
|
4681
4705
|
return (string.gsub(s, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
|
|
4682
4706
|
end
|
|
4683
|
-
function escapeLuaReplacement(s)
|
|
4707
|
+
local function escapeLuaReplacement(s)
|
|
4684
4708
|
return (string.gsub(s, "%%", "%%%%"))
|
|
4685
4709
|
end
|
|
4686
4710
|
local function caseInsensitiveLiteralReplace(src, searchStr, repl)
|
|
@@ -5273,7 +5297,7 @@ return {
|
|
|
5273
5297
|
<Properties>
|
|
5274
5298
|
<string name="Name">State</string>
|
|
5275
5299
|
<string name="Source"><![CDATA[-- Compiled with roblox-ts v3.0.0
|
|
5276
|
-
local CURRENT_VERSION = "2.7.0-next.
|
|
5300
|
+
local CURRENT_VERSION = "2.7.0-next.5"
|
|
5277
5301
|
local MAX_CONNECTIONS = 5
|
|
5278
5302
|
local BASE_PORT = 58741
|
|
5279
5303
|
local activeTabIndex = 0
|
|
@@ -203,6 +203,7 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
203
203
|
const instancePath = requestData.instancePath as string;
|
|
204
204
|
let oldString = requestData.old_string as string;
|
|
205
205
|
let newString = requestData.new_string as string;
|
|
206
|
+
const startLine = requestData.startLine as number | undefined;
|
|
206
207
|
|
|
207
208
|
if (!instancePath || oldString === undefined || newString === undefined) {
|
|
208
209
|
return { error: "Instance path, old_string, and new_string are required" };
|
|
@@ -221,27 +222,47 @@ function editScriptLines(requestData: Record<string, unknown>) {
|
|
|
221
222
|
|
|
222
223
|
const [success, result] = pcall(() => {
|
|
223
224
|
const source = readScriptSource(instance);
|
|
224
|
-
|
|
225
|
-
// Count occurrences to ensure uniqueness
|
|
226
|
-
let count = 0;
|
|
227
|
-
let searchPos = 1;
|
|
228
225
|
const searchLen = oldString.size();
|
|
226
|
+
let matchStart: number;
|
|
229
227
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (foundStart === undefined) break;
|
|
233
|
-
count++;
|
|
234
|
-
if (count > 1) break;
|
|
235
|
-
searchPos = foundStart + searchLen;
|
|
236
|
-
}
|
|
228
|
+
if (startLine !== undefined) {
|
|
229
|
+
if (startLine < 1) error(`startLine must be >= 1 (got ${startLine})`);
|
|
237
230
|
|
|
238
|
-
|
|
239
|
-
|
|
231
|
+
let lineStartByte = 1;
|
|
232
|
+
let currentLine = 1;
|
|
233
|
+
while (currentLine < startLine) {
|
|
234
|
+
const [nlPos] = string.find(source, "\n", lineStartByte, true);
|
|
235
|
+
if (nlPos === undefined) {
|
|
236
|
+
error(`startLine ${startLine} is past end of script (${currentLine} lines)`);
|
|
237
|
+
}
|
|
238
|
+
lineStartByte = (nlPos as number) + 1;
|
|
239
|
+
currentLine++;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const candidate = string.sub(source, lineStartByte, lineStartByte + searchLen - 1);
|
|
243
|
+
if (candidate !== oldString) {
|
|
244
|
+
error(`old_string does not match at line ${startLine}. Use get_script_source to verify the exact text at that line.`);
|
|
245
|
+
}
|
|
246
|
+
matchStart = lineStartByte;
|
|
247
|
+
} else {
|
|
248
|
+
let count = 0;
|
|
249
|
+
let searchPos = 1;
|
|
250
|
+
let firstMatch: number | undefined;
|
|
251
|
+
while (true) {
|
|
252
|
+
const [foundStart] = string.find(source, oldString, searchPos, true);
|
|
253
|
+
if (foundStart === undefined) break;
|
|
254
|
+
if (firstMatch === undefined) firstMatch = foundStart;
|
|
255
|
+
count++;
|
|
256
|
+
if (count > 1) break;
|
|
257
|
+
searchPos = foundStart + searchLen;
|
|
258
|
+
}
|
|
259
|
+
if (count === 0) error("old_string not found in script. If old_string contains repeated patterns (e.g. closing braces), pass startLine to anchor the edit.");
|
|
260
|
+
if (count > 1) error("old_string matches multiple locations. Provide more surrounding context, or pass startLine to anchor the edit to a specific line.");
|
|
261
|
+
matchStart = firstMatch as number;
|
|
262
|
+
}
|
|
240
263
|
|
|
241
|
-
//
|
|
242
|
-
const
|
|
243
|
-
const escapedRepl = escapeLuaReplacement(newString);
|
|
244
|
-
const [newSource] = string.gsub(source, escaped, escapedRepl, 1);
|
|
264
|
+
// Byte-slice replacement avoids Lua pattern escaping (safe for multi-byte chars like em dashes).
|
|
265
|
+
const newSource = string.sub(source, 1, matchStart - 1) + newString + string.sub(source, matchStart + searchLen);
|
|
245
266
|
|
|
246
267
|
ScriptEditorService.UpdateSourceAsync(instance, () => newSource);
|
|
247
268
|
|