@weppy/roblox-mcp 2.7.0 → 2.7.1
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/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +14 -17
- package/install.sh +3 -4
- package/package.json +1 -1
- package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogDetailPage-CIarzJIx.js → ChangelogDetailPage-DxktONbL.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ChangelogPage-C1uCKyqh.js → ChangelogPage-DOY-SmWM.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConfirmModal-BDWSInRi.js → ConfirmModal-CmJCz5rg.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ConnectionPage-CWDwVCKg.js → ConnectionPage-BiiIQs7f.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{GameChangeDetail-Bk8YUy4G.js → GameChangeDetail-D1kE2L_v.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{InfoLabel-BJIFNYA5.js → InfoLabel-B9KT3kfU.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{OverviewPage-Di0p7YY2.js → OverviewPage-T2WCTHpo.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{PlaytestPage-Dxgz0SKW.js → PlaytestPage-CW8Xfp6b.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{SettingsPage-DVgxRsfC.js → SettingsPage-DcUt5qjG.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{StatusBadge-ilvVufN2.js → StatusBadge-Bxe88ki2.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{SyncPage-IICMnq8v.js → SyncPage-C09YF6D5.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{Tabs-pDS5zzr9.js → Tabs-BNtOqYlc.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/TierComparison-Dyf-knpe.js +1 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{ToolsPage-DEhsoa36.js → ToolsPage-HKbmsKMQ.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{TooltipText-bSKcc3-P.js → TooltipText-Baw38jOU.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{UiStudioPage-BHhd3zUv.js → UiStudioPage-Cbgl7xIC.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{WhatsNewPage-ErpENbsZ.js → WhatsNewPage-BlOSWgSn.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{index-BPAvjNNu.js → index-FfT2oWAB.js} +28 -28
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{sample-requests-DDQ3XchF.js → sample-requests-X_hsWdLX.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/{useLiveUptime-HYmdQdHf.js → useLiveUptime-DhJz484L.js} +1 -1
- package/plugins/weppy-roblox-mcp/dashboard/dist/index.html +1 -1
- package/plugins/weppy-roblox-mcp/dist/index.js +4 -4
- package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
- package/plugins/weppy-roblox-mcp/dashboard/dist/assets/TierComparison-BXMCd1IB.js +0 -2
|
@@ -126,9 +126,9 @@ ScreenGui > Frame(BackgroundTransparency=1) > \uC2E4\uC81C UI \uC694\uC18C. Scre
|
|
|
126
126
|
|
|
127
127
|
Property \uAC12 \uC778\uCF54\uB529:
|
|
128
128
|
UDim2\uB294 {xScale, xOffset, yScale, yOffset}, UDim\uC740 {scale, offset}, Color3\uB294 {r, g, b}, Vector2/Vector3\uB294 {x,y}/{x,y,z}, Enum\uC740 item name string\uC744 \uC0AC\uC6A9\uD55C\uB2E4.
|
|
129
|
-
`},targetPath:{type:"string",description:"Path to existing UI instance. Both `StarterGui.MyGui` and `game.StarterGui.MyGui` are accepted. Used by: update, delete (required)."},changes:{type:"object",description:"\uBD80\uBD84 \uBCC0\uACBD \uC2A4\uD399. {properties?: {...}, addChildren?: Tree[], removeChildren?: [string]}\nproperties\uB294 `Instance:SetPropertyValue` \uD638\uD658 \uD0A4\xB7\uAC12 \uC30D. \uAC12 \uC778\uCF54\uB529\uC740 tree \uC124\uBA85\uC758 Property \uAC12 \uC778\uCF54\uB529\uC744 \uB530\uB978\uB2E4.\n"},includeVisualAnalysis:{type:"boolean",description:"Opt-in only. Used by: check. When true, the server may read a saved preview snapshot PNG + visible GUI metadata and merge AI visual suggestions into check_results. Default false; no screenshot capture is triggered by check."},snapshotId:{type:"string",description:"Saved manage_ui.preview snapshot_id to use for visual analysis. Used by: check when includeVisualAnalysis=true. If omitted, the latest compatible saved snapshot is used when available."}},allOf:[{if:{properties:{action:{const:"create_tree"}}},then:{required:["tree"]}},{if:{properties:{action:{const:"update"}}},then:{required:["targetPath","changes"]}},{if:{properties:{action:{const:"delete"}}},then:{required:["targetPath"]}}],required:["action"]}};var ck={name:"manage_lighting",description:"[PRO] Configure environment: lighting, atmosphere, sky, terrain properties, time of day.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["lighting","atmosphere","sky","terrain_props","time"],description:"[PRO] Environment action. lighting: set Lighting service properties. atmosphere: set Atmosphere properties. sky: set Sky properties. terrain_props: set Terrain water/visual properties. time: set time of day."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},properties:{type:"object",description:"Dictionary of properties to set. Used by: lighting, atmosphere, sky, terrain_props. Supports Color3 {r,g,b} (0-255), numbers, booleans, Enum strings.",additionalProperties:!0},createIfMissing:{type:"boolean",description:"Create Atmosphere/Sky instance if missing. Used by: atmosphere, sky. Default: true."},time:{type:"string",description:'Time string in "HH:MM:SS" format (e.g., "14:30:00"). Used by: time. Provide this OR clockTime.'},clockTime:{type:"number",description:"Numeric time in 24-hour format (e.g., 14.5 for 2:30 PM). Used by: time. Provide this OR time."}},required:["action"]}};var lk={name:"manage_selection",description:"Get, set, or clear selection. [PRO] context, details, add/remove items, watch changes.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","set","clear","cached","context","details","add","remove","watch"],description:"Selection action. Basic: get (current selection), set (replace selection), clear (deselect all), cached (from cache without round-trip). [PRO]: context (detailed with source/properties), details (hierarchical with ancestors/descendants), add (add to selection), remove (remove from selection), watch (monitor changes)."},paths:{type:"array",description:"Array of instance paths. Used by: set (required), add (required), remove (required).",items:{type:"string"}},maxAge:{type:"number",description:"Maximum age of cached data in milliseconds. Used by: cached. Default: 30000. Set to 0 for any age."},maxDepth:{type:"number",description:"[PRO] Maximum depth for descendant tree traversal. Used by: details. Default: 1."},includeSource:{type:"boolean",description:"[PRO] Include script source code. Used by: context. Default: true."},includeProperties:{type:"boolean",description:"[PRO] Include all readable properties. Used by: context. Default: true."},includeChildren:{type:"boolean",description:"[PRO] Include immediate children. Used by: context. Default: false."},includeAncestors:{type:"boolean",description:"[PRO] Include full ancestor chain. Used by: details. Default: false."}},allOf:[{if:{properties:{action:{const:"set"}}},then:{required:["paths"]}},{if:{properties:{action:{const:"add"}}},then:{required:["paths"]}},{if:{properties:{action:{const:"remove"}}},then:{required:["paths"]}}],required:["action"]}};var uk={name:"manage_camera",description:"Camera operations: get info, focus on instance/position, suggest view, capture Edit-mode viewport screenshot (Edit mode only; not usable during playtest).",inputSchema:{type:"object",properties:{action:{type:"string",enum:["info","focus_path","focus_position","suggest","screenshot"],description:'Camera action. info: get current camera position, rotation, FOV, viewport size. focus_path: move camera to focus on instance by path. focus_position: move camera to focus on world position. suggest: get suggested camera view for a target. [PRO] screenshot: **EDIT MODE ONLY \u2014 DO NOT call while a playtest is active.** Captures the current Studio Edit-mode viewport as a PNG image (returns MCP image content). If you are uncertain whether a playtest is running, call system_info.play_status first and only proceed when state == "edit"; otherwise the call returns an error. Requires "Allow Mesh / Image APIs" Studio setting (Game Settings > Security). Resolution capped by plugin setting screenshotMaxDimension (default 1024px on longest side; kept at full resolution by Claude while reducing token cost ~40% vs 1280). Play-mode capture is not supported in v1 because Roblox platform blocks converting CaptureService temporary contentId into EditableImage from any non-edit-DM context.'},path:{type:"string",description:"Instance path to focus on. Used by: focus_path (if not provided, focuses on selection), suggest (if not provided, uses selection)."},position:{type:"object",description:"World position to focus on as Vector3. Used by: focus_position (required).",required:["x","y","z"],properties:{x:{type:"number",description:"X coordinate"},y:{type:"number",description:"Y coordinate"},z:{type:"number",description:"Z coordinate"}}},distance:{type:"number",description:"Distance from target in studs. Used by: focus_path, focus_position. Auto-calculated if not provided."},duration:{type:"number",description:"Animation duration in seconds. Used by: focus_path, focus_position. Default: 0.5."},offset:{type:"object",description:"Camera offset direction from target (normalized and scaled by distance). Used by: focus_path, focus_position. Default: {x:1, y:0.5, z:1}.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},lookAt:{type:"object",description:"Point for camera to look at. Used by: focus_position.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},respectAutoFocusSetting:{type:"boolean",description:"If true, only focus when plugin Auto Focus setting is enabled. Used by: focus_path, focus_position. Default: false."}},allOf:[{if:{properties:{action:{const:"focus_position"}}},then:{required:["position"]}}],required:["action"]}};var pk={name:"manage_tween",description:"[PRO] Tween service: create, play, pause, cancel tweens for smooth animations.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","play","pause","cancel"],description:"[PRO] Tween action. create: create a new tween with target properties. play: play a created tween. pause: pause a running tween. cancel: cancel a tween."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Instance path to tween. Used by: create (required)."},tweenInfo:{type:"object",description:"Tween configuration. Used by: create.",properties:{time:{type:"number",description:"Duration in seconds. Default: 1."},easingStyle:{type:"string",description:'Easing style (e.g., "Linear", "Quad", "Cubic", "Sine", "Bounce", "Elastic"). Default: "Quad".'},easingDirection:{type:"string",description:'Easing direction. Default: "Out".',enum:["In","Out","InOut"]},repeatCount:{type:"number",description:"Number of times to repeat. -1 for infinite. Default: 0."},reverses:{type:"boolean",description:"Whether the tween reverses on each repeat. Default: false."},delayTime:{type:"number",description:"Delay before tween starts in seconds. Default: 0."}}},properties:{type:"object",description:"Target property values to tween to. Used by: create. Supports Vector3, Color3, numbers, etc.",additionalProperties:!0},tweenId:{type:"string",description:"Tween identifier returned by create. Used by: play, pause, cancel."}},allOf:[{if:{properties:{action:{const:"create"}}},then:{required:["path"]}}],required:["action"]}};var dk={name:"manage_audio",description:"[PRO] Audio management: play, stop, pause, resume sounds. Set audio listener.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["play","stop","pause","resume","set_listener"],description:"[PRO] Audio action. play: play a sound (creates Sound if needed). stop: stop a playing sound. pause: pause a playing sound. resume: resume a paused sound. set_listener: set the audio listener type/target."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the Sound instance or parent to create Sound in. Used by: play, stop, pause, resume."},soundId:{type:"string",description:'Roblox sound asset ID (e.g., "rbxassetid://1234567"). Used by: play (if creating a new Sound).'},volume:{type:"number",description:"Sound volume (0-10). Used by: play. Default: 0.5."},looped:{type:"boolean",description:"Whether the sound loops. Used by: play. Default: false."},playbackSpeed:{type:"number",description:"Playback speed multiplier. Used by: play. Default: 1."},listenerType:{type:"string",description:"Listener type for SoundService. Used by: set_listener.",enum:["Camera","CFrame","ObjectPosition","ObjectCFrame"]},listenerPath:{type:"string",description:"Instance path for ObjectPosition/ObjectCFrame listener type. Used by: set_listener."}},required:["action"]}};var fk={name:"manage_animation",description:"[PRO] Animation: load, play, stop animations. Get animation tracks from humanoid/controller.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["load","play","stop","get_tracks"],description:"[PRO] Animation action. load: load an animation on a Humanoid/AnimationController. play: play a loaded animation track. stop: stop a playing animation. get_tracks: list all loaded animation tracks."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the Humanoid, AnimationController, or Model containing one. Used by: load, play, stop, get_tracks."},animationId:{type:"string",description:'Roblox animation asset ID (e.g., "rbxassetid://1234567"). Used by: load.'},trackId:{type:"string",description:"Animation track identifier returned by load. Used by: play (required), stop (required)."},speed:{type:"number",description:"Playback speed multiplier. Used by: play. Default: 1."},fadeTime:{type:"number",description:"Fade time in seconds when starting/stopping. Used by: play, stop. Default: 0.1."},weight:{type:"number",description:"Animation weight (0-1) for blending. Used by: play. Default: 1."},priority:{type:"string",description:"Animation priority. Used by: play.",enum:["Core","Idle","Movement","Action","Action2","Action3","Action4"]}},allOf:[{if:{properties:{action:{const:"play"}}},then:{required:["trackId"]}},{if:{properties:{action:{const:"stop"}}},then:{required:["trackId"]}}],required:["action"]}};var mk={name:"manage_physics",description:"[PRO] Physics collision groups: register, set collidable between groups, list groups.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["register_group","set_collidable","get_groups"],description:"[PRO] Physics action. register_group: register a new collision group with PhysicsService. set_collidable: set whether two groups can collide. get_groups: list all registered collision groups."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},groupName:{type:"string",description:"Collision group name. Used by: register_group (required)."},group1:{type:"string",description:"First collision group name. Used by: set_collidable (required)."},group2:{type:"string",description:"Second collision group name. Used by: set_collidable (required)."},collidable:{type:"boolean",description:"Whether the two groups can collide. Used by: set_collidable (required)."}},allOf:[{if:{properties:{action:{const:"register_group"}}},then:{required:["groupName"]}},{if:{properties:{action:{const:"set_collidable"}}},then:{required:["group1","group2","collidable"]}}],required:["action"]}};var hk={name:"manage_effects",description:"[PRO] Particle effects: emit particles, clear all particles, toggle effect enabled state.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["emit","clear","toggle"],description:"[PRO] Effects action. emit: emit a burst of particles from a ParticleEmitter. clear: clear all particles from a ParticleEmitter or all emitters under an instance. toggle: enable or disable a ParticleEmitter, Beam, Trail, or other effect."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the effect instance (ParticleEmitter, Beam, Trail) or parent containing effects. Used by: emit, clear, toggle."},count:{type:"number",description:"Number of particles to emit in a burst. Used by: emit. Default: 16."},enabled:{type:"boolean",description:"Enable or disable the effect. Used by: toggle."}},required:["action"]}};var gk={name:"manage_terrain",description:"[PRO] Terrain operations: fill shapes, clear regions, replace materials, manage colors, read/write voxels, generate procedural terrain, smooth terrain.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["fill_block","fill_ball","fill_cylinder","fill_wedge","clear_region","clear_bounds","replace_material","colors_get","colors_set","read_voxel","read_voxels","write_voxels","generate","smooth"],description:"[PRO] Terrain action. fill_block/fill_ball/fill_cylinder/fill_wedge: fill shapes with material. clear_region/clear_bounds: clear terrain. replace_material: swap materials in region. colors_get/colors_set: manage material colors. read_voxel: single voxel. read_voxels/write_voxels: bulk voxels. generate: procedural terrain. smooth: smooth terrain."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},material:{type:"string",description:"Terrain material name (e.g., Grass, Rock, Water, Sand, Slate, Concrete). Used by: fill_*, replace_material, colors_get, colors_set."},center:{type:"object",description:"Center position as Vector3. Used by: fill_ball.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},position:{type:"object",description:"Position as Vector3. Used by: read_voxel.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},cframe:{type:"object",description:"Position and rotation for fill shapes. Used by: fill_block, fill_cylinder, fill_wedge.",required:["position"],properties:{position:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},rotation:{type:"object",description:"Optional rotation in degrees.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},size:{type:"object",description:"Size in studs. Used by: fill_block, fill_wedge.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},radius:{type:"number",description:"Radius in studs. Used by: fill_ball, fill_cylinder."},height:{type:"number",description:"Height in studs. Used by: fill_cylinder."},region:{type:"object",description:"Rectangular region with min/max corners. Used by: clear_region, replace_material, read_voxels, write_voxels, generate, smooth.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},min:{type:"object",description:"Minimum corner for bounds-based clear. Used by: clear_bounds.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",description:"Maximum corner for bounds-based clear. Used by: clear_bounds.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},sourceMaterial:{type:"string",description:"Material to replace. Used by: replace_material."},targetMaterial:{type:"string",description:"Replacement material. Used by: replace_material."},color:{type:"object",description:"RGB color (0-255). Used by: colors_set.",required:["r","g","b"],properties:{r:{type:"number",minimum:0,maximum:255},g:{type:"number",minimum:0,maximum:255},b:{type:"number",minimum:0,maximum:255}}},resolution:{type:"number",description:"Voxel resolution (studs per voxel). Used by: read_voxels, write_voxels. Default: 4."},materials:{type:"array",description:"3D array of material names [x][y][z]. Used by: write_voxels.",items:{type:"array",items:{type:"array",items:{type:"string"}}}},occupancy:{type:"array",description:"3D array of occupancy values [x][y][z] (0-1). Used by: write_voxels.",items:{type:"array",items:{type:"array",items:{type:"number",minimum:0,maximum:1}}}},baseHeight:{type:"number",description:"Base terrain height in studs. Used by: generate. Default: 32."},amplitude:{type:"number",description:"Height variation amplitude in studs. Used by: generate. Default: 24."},frequency:{type:"number",description:"Noise frequency (0.001-0.1). Used by: generate. Default: 0.01.",minimum:.001,maximum:.1},seed:{type:"number",description:"Random seed for terrain generation. Used by: generate."},layers:{type:"array",description:"Material layers by height. Used by: generate. Each: {material, maxHeight}.",items:{type:"object",required:["material","maxHeight"],properties:{material:{type:"string"},maxHeight:{type:"number"}}}},intensity:{type:"number",description:"Smoothing intensity (0-1). Used by: smooth. Default: 0.5.",minimum:0,maximum:1}},required:["action"]}};var yk={name:"spatial_query",description:"[PRO] Spatial queries: raycast, find ground, check placement, multi-raycast, scan area, find flat areas, find spawn positions, analyze walkable, spatial map, find empty space, get bounds, snap to grid, check collision.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["raycast","find_ground","check_placement","multi_raycast","scan_area","find_flat","find_spawn","analyze_walkable","spatial_map","find_space","bounds","snap_grid","collision"],description:"[PRO] Spatial action. raycast: single ray. find_ground: ground below point. check_placement: collision-free placement check. multi_raycast: batch rays. scan_area: heightmap generation. find_flat: flat areas for building. find_spawn: spawn positions. analyze_walkable: walkability grid. spatial_map: all BasePart positions. find_space: empty space for object. bounds: bounding box. snap_grid: snap position to grid. collision: AABB collision check."},origin:{type:"object",description:"Ray origin as Vector3. Used by: raycast.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},direction:{type:"object",description:"Ray direction and length as Vector3. Used by: raycast.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},maxDistance:{type:"number",description:"Maximum distance for ground search. Used by: find_ground. Default: 1000."},position:{type:"object",description:"Position as Vector3. Used by: find_ground (cast from here), check_placement (center), collision (hypothetical position).",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},size:{type:"object",description:"Size as Vector3. Used by: check_placement (object size), find_space (required space size), scan_area (area X/Z dimensions).",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},filterType:{type:"string",description:'Raycast filter type. Used by: raycast, multi_raycast. Default: "Exclude".',enum:["Exclude","Include"]},filterList:{type:"array",description:"Instance paths to filter. Used by: raycast, multi_raycast, find_ground, check_placement, scan_area. Alias: filterInstances.",items:{type:"string"}},filterInstances:{type:"array",description:"Instance paths to filter (alias for filterList). Used by: raycast, multi_raycast, find_ground, check_placement, scan_area.",items:{type:"string"}},ignoreWater:{type:"boolean",description:"Ignore terrain water. Used by: raycast, multi_raycast. Default: false."},rays:{type:"array",description:"Array of ray specifications. Used by: multi_raycast. Max 50 rays.",maxItems:50,items:{type:"object",required:["origin","direction"],properties:{origin:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},direction:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}}},center:{type:"object",description:"Center point for area scan. Used by: scan_area.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},area:{type:"object",description:"Bounding box for analysis. Used by: analyze_walkable. Contains min/max.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},searchArea:{type:"object",description:"Bounding box for search. Used by: find_flat, find_spawn, find_space.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},minSize:{type:"object",description:"Minimum flat area size. Used by: find_flat.",required:["x","z"],properties:{x:{type:"number"},z:{type:"number"}}},maxSlope:{type:"number",description:"Maximum slope in degrees. Used by: find_flat (default: 10), analyze_walkable (default: 45)."},resolution:{type:"number",description:"Grid resolution in studs. Used by: scan_area, analyze_walkable. Default: 4."},gridSize:{type:"number",description:"Grid snap size in studs. Used by: snap_grid (default: 4), find_space (default: 4)."},path:{type:"string",description:"Instance path. Used by: bounds (optional), snap_grid (required), collision (required), spatial_map (rootPath alias)."},paths:{type:"array",description:"Multiple instance paths for batch bounds. Used by: bounds.",items:{type:"string"}},tolerance:{type:"number",description:"Height variation tolerance in studs. Used by: find_flat."},gridSnap:{type:"number",description:"Grid snap size. Alias for gridSize."},axes:{type:"array",description:'Axes to snap. Used by: snap_grid. Default: ["x","y","z"]. Use ["x","z"] for horizontal only.',items:{type:"string",enum:["x","y","z"]}},ignorePaths:{type:"array",description:"Instance paths to ignore in collision check. Used by: collision.",items:{type:"string"}},offset:{type:"number",description:"Vertical offset for ground position. Used by: find_ground. Default: 0."},rotation:{type:"object",description:"Rotation in degrees. Used by: check_placement.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},checkGround:{type:"boolean",description:"Verify ground support for placement. Used by: check_placement. Default: true."},spawnSize:{type:"object",description:"Size of spawning entity. Used by: find_spawn. Default: {x:4, y:5, z:4}.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},minSpacing:{type:"number",description:"Minimum distance between spawn positions. Used by: find_spawn. Default: 10."},preferOutdoor:{type:"boolean",description:"Prefer open sky positions. Used by: find_spawn. Default: false."},count:{type:"number",description:"Number of results to find. Used by: find_spawn. Default: 10."},characterHeight:{type:"number",description:"Character height for clearance checks. Used by: analyze_walkable. Default: 5."},maxStepHeight:{type:"number",description:"Maximum step height. Used by: analyze_walkable. Default: 2."},rootPath:{type:"string",description:'Root path for spatial map scan. Used by: spatial_map. Default: "game.Workspace".'},includeModels:{type:"boolean",description:"Include Model bounding boxes. Used by: spatial_map. Default: true."},maxResults:{type:"number",description:"Maximum results. Used by: spatial_map (default: 500), scan_area (default: 500), find_flat (default: 10)."},padding:{type:"number",description:"Minimum distance from other objects. Used by: find_space. Default: 1."}},allOf:[{if:{properties:{action:{const:"snap_grid"}}},then:{required:["path"]}},{if:{properties:{action:{const:"collision"}}},then:{required:["path"]}}],required:["action"]}};var vk={name:"manage_assets",description:"[PRO] Asset management: insert models by ID, get asset info, search creator store, insert free models/packages, export selection.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["insert","info","search","search_insert","insert_free","insert_package","export"],description:"[PRO] Asset action. insert: insert model by asset ID. info: get asset metadata. search: search Creator Store. search_insert: search and insert first match. insert_free: insert free model. insert_package: insert package. export: export current selection."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},assetId:{type:"number",description:"Roblox asset ID. Used by: insert (required), info (required), insert_free (required), insert_package (required)."},parent:{type:"string",description:'Parent path for inserted asset. Used by: insert, search_insert, insert_free, insert_package. Default: "game.Workspace".'},name:{type:"string",description:"Optional name for inserted instance."},position:{type:"object",description:"Position to place the model. Used by: insert, insert_free.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},query:{type:"string",description:'Search keyword. Used by: search (required), search_insert (required). E.g., "monster", "tree", "car".'},maxResults:{type:"number",description:"Maximum search results. Used by: search. Default: 10."},category:{type:"string",description:"Asset category filter. Used by: search."},sortType:{type:"string",description:"Sort order for search results. Used by: search."},format:{type:"string",description:"Export format. Used by: export."},includeProperties:{type:"boolean",description:"Include all properties in export. Used by: export. Default: false."},includeChildren:{type:"boolean",description:"Include children in export. Used by: export. Default: false."},maxDepth:{type:"number",description:"Maximum depth for recursive child export. Used by: export. Default: 5."}},allOf:[{if:{properties:{action:{const:"insert"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"info"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"search"}}},then:{required:["query"]}},{if:{properties:{action:{const:"search_insert"}}},then:{required:["query"]}},{if:{properties:{action:{const:"insert_free"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"insert_package"}}},then:{required:["assetId"]}}],required:["action"]}};var bk={name:"manage_sync",description:"[PRO] Project sync management: status, history, direction settings, read/write synced files.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["status_current_place","history","directions","read_file","write_file","progress"],description:"[PRO] Sync action. status_current_place: get sync status for the current connected place. history: get change history. directions: get per-type sync directions. read_file: read a synced file. write_file: write to a synced file. progress: get real-time sync progress and bandwidth."},placeId:{type:"number",description:"Place ID for sync operations. Used by: history, directions, read_file, write_file."},instancePath:{type:"string",description:"Instance path for file read/write. Used by: read_file, write_file."},content:{type:"string",description:"File content to write. Used by: write_file."},query:{type:"object",description:"Query parameters for history. Used by: history.",properties:{limit:{type:"number",description:"Maximum entries to return."},offset:{type:"number",description:"Number of entries to skip."}}}},allOf:[{if:{properties:{action:{const:"read_file"}}},then:{required:["placeId","instancePath"]}},{if:{properties:{action:{const:"write_file"}}},then:{required:["placeId","instancePath","content"]}}],required:["action"]}};var xk={name:"workspace_state",description:"[PRO] Workspace state: full sync, snapshot, recent changes, viewport info, clear history, metadata, scripts, selection info, clear cache.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["sync","snapshot","changes","viewport","clear_history","metadata","scripts","selection_info","clear_cache"],description:"[PRO] State action. sync: fetch current Workspace state (hierarchy, history, stats). snapshot: get full instance tree structure. changes: get recent changes (added/removed/modified). viewport: get camera position, FOV, viewport size, selection bounds. clear_history: clear change history. metadata: get workspace metadata. scripts: get script list. selection_info: get selection info. clear_cache: clear state cache."},includeMetadata:{type:"boolean",description:"Include metadata (instance counts, timestamps). Used by: sync. Default: true."},limit:{type:"number",description:"Maximum number of changes to return. Used by: changes. Default: 20."},includeCameraInfo:{type:"boolean",description:"Include camera position and settings. Used by: viewport. Default: true."},includeSelectionBounds:{type:"boolean",description:"Include current selection bounds info. Used by: viewport. Default: true."}},required:["action"]}};var _k={name:"manage_logs",description:"Output logs: get filtered logs, poll incrementally with sinceSeq cursor, clear buffer, get recent errors.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","clear","errors"],description:"Log action. get: retrieve logs with optional level/limit/since/sinceSeq filters. clear: clear internal log buffer without resetting seq. errors: quick access to recent errors only."},level:{type:"string",description:'Log level filter. Used by: get. Default: "all".',enum:["all","error","warning","info"]},limit:{type:"number",description:"Maximum entries to return. Used by: get (default: 100, max: 500), errors (default: 20, max: 100)."},pattern:{type:"string",description:"Text pattern to filter log messages. Used by: get."},since:{type:"number",description:"Unix timestamp in milliseconds. Only logs after this time. Used by: get."},sinceSeq:{type:"number",description:"Return only logs after this sequence number. Cursor mode returns logs oldest-to-newest and includes lastSeq/oldestSeq/hasMore/cursorStatus. Used by: get."}},required:["action"]}};var Sk={name:"system_info",description:"System info: ping, connection status, usage tier. [PRO] place info, services list, studio settings, playtest control, automated test runner.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["ping","connection","usage","place_info","services","studio_settings","play","stop","pause","resume","play_status","run_test"],description:'System action. Basic: ping (test connection, returns pong), connection (server/plugin connection info with session IDs), usage (current tier: basic/pro). [PRO]: place_info (place ID, name, game ID, creator), services (list all Roblox services), studio_settings (Studio preferences), play (start playtest, mode: "play"=F5 or "run"=F8), stop (stop playtest), pause (pause playtest), resume (resume paused playtest), play_status (get current state: edit/running/paused with mode and available actions), run_test (inject test script, run playtest, collect logs, and write local report files).'},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},message:{type:"string",description:"Optional message to echo back. Used by: ping."},mode:{type:"string",description:'Playtest mode. "play" = Play mode (F5, default), "run" = Run mode (F8). Used by: play, run_test.',enum:["play","run"]},script:{type:"string",description:"Luau test body to inject into ServerScriptService.__MCP_TestRunner. Used by: run_test."},test_name:{type:"string",description:"Optional report display name for the automated playtest run. Used by: run_test."},timeout:{type:"number",description:"Timeout in seconds for the automated playtest run. Default: 60. Maximum: 300. Used by: run_test."}},allOf:[{if:{properties:{action:{const:"run_test"}}},then:{required:["script"]}}],required:["action"]}};var wk={name:"manage_studio",description:`Roblox Studio Editor \uC0C1\uD0DC\xB7\uBDF0\uD3EC\uD2B8 \uB80C\uB354\uB9C1 \uD1A0\uAE00\uC744 \uC81C\uC5B4. AI \uC5D0\uC774\uC804\uD2B8\uAC00 View \uD0ED \uC124\uC815 (UI \uD504\uB9AC\uBDF0, \uADF8\uB9AC\uB4DC, \uC81C\uC57D\uC870\uAC74 \uD45C\uC2DC \uB4F1) \uC744 \uD504\uB85C\uADF8\uB7A8 \uBC29\uC2DD\uC73C\uB85C \uC870\uC791\uD574 \uC2A4\uD06C\uB9B0\uC0F7\xB7QA \uC6CC\uD06C\uD50C\uB85C\uB97C \uC790\uB3D9\uD654\uD560 \uB54C \uC0AC\uC6A9. ScreenGui.Enabled \uB4F1 \uC0AC\uC6A9\uC790 \uC778\uC2A4\uD134\uC2A4 \uD504\uB85C\uD37C\uD2F0\uB294 \uC808\uB300 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uC74C \u2014 Studio \uC138\uC158 \uC124\uC815\uB9CC \uBCC0\uACBD.
|
|
130
|
-
`,inputSchema:{type:"object",properties:{action:{type:"string",enum:["toggle_ui_preview"],description:`Studio editor action. [PRO]: toggle_ui_preview (
|
|
131
|
-
`},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},enabled:{type:"boolean",description
|
|
129
|
+
`},targetPath:{type:"string",description:"Path to existing UI instance. Both `StarterGui.MyGui` and `game.StarterGui.MyGui` are accepted. Used by: update, delete (required)."},changes:{type:"object",description:"\uBD80\uBD84 \uBCC0\uACBD \uC2A4\uD399. {properties?: {...}, addChildren?: Tree[], removeChildren?: [string]}\nproperties\uB294 `Instance:SetPropertyValue` \uD638\uD658 \uD0A4\xB7\uAC12 \uC30D. \uAC12 \uC778\uCF54\uB529\uC740 tree \uC124\uBA85\uC758 Property \uAC12 \uC778\uCF54\uB529\uC744 \uB530\uB978\uB2E4.\n"},includeVisualAnalysis:{type:"boolean",description:"Opt-in only. Used by: check. When true, the server may read a saved preview snapshot PNG + visible GUI metadata and merge AI visual suggestions into check_results. Default false; no screenshot capture is triggered by check."},snapshotId:{type:"string",description:"Saved manage_ui.preview snapshot_id to use for visual analysis. Used by: check when includeVisualAnalysis=true. If omitted, the latest compatible saved snapshot is used when available."}},allOf:[{if:{properties:{action:{const:"create_tree"}}},then:{required:["tree"]}},{if:{properties:{action:{const:"update"}}},then:{required:["targetPath","changes"]}},{if:{properties:{action:{const:"delete"}}},then:{required:["targetPath"]}}],required:["action"]}};var ck={name:"manage_lighting",description:"[PRO] Configure environment: lighting, atmosphere, sky, terrain properties, time of day.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["lighting","atmosphere","sky","terrain_props","time"],description:"[PRO] Environment action. lighting: set Lighting service properties. atmosphere: set Atmosphere properties. sky: set Sky properties. terrain_props: set Terrain water/visual properties. time: set time of day."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},properties:{type:"object",description:"Dictionary of properties to set. Used by: lighting, atmosphere, sky, terrain_props. Supports Color3 {r,g,b} (0-255), numbers, booleans, Enum strings.",additionalProperties:!0},createIfMissing:{type:"boolean",description:"Create Atmosphere/Sky instance if missing. Used by: atmosphere, sky. Default: true."},time:{type:"string",description:'Time string in "HH:MM:SS" format (e.g., "14:30:00"). Used by: time. Provide this OR clockTime.'},clockTime:{type:"number",description:"Numeric time in 24-hour format (e.g., 14.5 for 2:30 PM). Used by: time. Provide this OR time."}},required:["action"]}};var lk={name:"manage_selection",description:"Get, set, or clear selection. [PRO] context, details, add/remove items, watch changes.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","set","clear","cached","context","details","add","remove","watch"],description:"Selection action. Basic: get (current selection), set (replace selection), clear (deselect all), cached (from cache without round-trip). [PRO]: context (detailed with source/properties), details (hierarchical with ancestors/descendants), add (add to selection), remove (remove from selection), watch (monitor changes)."},paths:{type:"array",description:"Array of instance paths. Used by: set (required), add (required), remove (required).",items:{type:"string"}},maxAge:{type:"number",description:"Maximum age of cached data in milliseconds. Used by: cached. Default: 30000. Set to 0 for any age."},maxDepth:{type:"number",description:"[PRO] Maximum depth for descendant tree traversal. Used by: details. Default: 1."},includeSource:{type:"boolean",description:"[PRO] Include script source code. Used by: context. Default: true."},includeProperties:{type:"boolean",description:"[PRO] Include all readable properties. Used by: context. Default: true."},includeChildren:{type:"boolean",description:"[PRO] Include immediate children. Used by: context. Default: false."},includeAncestors:{type:"boolean",description:"[PRO] Include full ancestor chain. Used by: details. Default: false."}},allOf:[{if:{properties:{action:{const:"set"}}},then:{required:["paths"]}},{if:{properties:{action:{const:"add"}}},then:{required:["paths"]}},{if:{properties:{action:{const:"remove"}}},then:{required:["paths"]}}],required:["action"]}};var uk={name:"manage_camera",description:"Camera operations: get info, focus on instance/position, suggest view, capture Edit-mode viewport screenshot (Edit mode only; not usable during playtest).",inputSchema:{type:"object",properties:{action:{type:"string",enum:["info","focus_path","focus_position","suggest","screenshot"],description:'Camera action. info: get current camera position, rotation, FOV, viewport size. focus_path: move camera to focus on instance by path. focus_position: move camera to focus on world position. suggest: get suggested camera view for a target. [PRO] screenshot: **EDIT MODE ONLY \u2014 DO NOT call while a playtest is active.** Captures the current Studio Edit-mode viewport as a PNG image (returns MCP image content). If you are uncertain whether a playtest is running, call system_info.play_status first and only proceed when state == "edit"; otherwise the call returns an error. Requires "Allow Mesh / Image APIs" Studio setting (Game Settings > Security). Resolution capped by plugin setting screenshotMaxDimension (default 1024px on longest side; kept at full resolution by Claude while reducing token cost ~40% vs 1280). Play-mode capture is not supported in v1 because Roblox platform blocks converting CaptureService temporary contentId into EditableImage from any non-edit-DM context.'},path:{type:"string",description:"Instance path to focus on. Used by: focus_path (if not provided, focuses on selection), suggest (if not provided, uses selection)."},position:{type:"object",description:"World position to focus on as Vector3. Used by: focus_position (required).",required:["x","y","z"],properties:{x:{type:"number",description:"X coordinate"},y:{type:"number",description:"Y coordinate"},z:{type:"number",description:"Z coordinate"}}},distance:{type:"number",description:"Distance from target in studs. Used by: focus_path, focus_position. Auto-calculated if not provided."},duration:{type:"number",description:"Animation duration in seconds. Used by: focus_path, focus_position. Default: 0.5."},offset:{type:"object",description:"Camera offset direction from target (normalized and scaled by distance). Used by: focus_path, focus_position. Default: {x:1, y:0.5, z:1}.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},lookAt:{type:"object",description:"Point for camera to look at. Used by: focus_position.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},respectAutoFocusSetting:{type:"boolean",description:"If true, only focus when plugin Auto Focus setting is enabled. Used by: focus_path, focus_position. Default: false."}},allOf:[{if:{properties:{action:{const:"focus_position"}}},then:{required:["position"]}}],required:["action"]}};var pk={name:"manage_tween",description:"[PRO] Tween service: create, play, pause, cancel tweens for smooth animations.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["create","play","pause","cancel"],description:"[PRO] Tween action. create: create a new tween with target properties. play: play a created tween. pause: pause a running tween. cancel: cancel a tween."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Instance path to tween. Used by: create (required)."},tweenInfo:{type:"object",description:"Tween configuration. Used by: create.",properties:{time:{type:"number",description:"Duration in seconds. Default: 1."},easingStyle:{type:"string",description:'Easing style (e.g., "Linear", "Quad", "Cubic", "Sine", "Bounce", "Elastic"). Default: "Quad".'},easingDirection:{type:"string",description:'Easing direction. Default: "Out".',enum:["In","Out","InOut"]},repeatCount:{type:"number",description:"Number of times to repeat. -1 for infinite. Default: 0."},reverses:{type:"boolean",description:"Whether the tween reverses on each repeat. Default: false."},delayTime:{type:"number",description:"Delay before tween starts in seconds. Default: 0."}}},properties:{type:"object",description:"Target property values to tween to. Used by: create. Supports Vector3, Color3, numbers, etc.",additionalProperties:!0},tweenId:{type:"string",description:"Tween identifier returned by create. Used by: play, pause, cancel."}},allOf:[{if:{properties:{action:{const:"create"}}},then:{required:["path"]}}],required:["action"]}};var dk={name:"manage_audio",description:"[PRO] Audio management: play, stop, pause, resume sounds. Set audio listener.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["play","stop","pause","resume","set_listener"],description:"[PRO] Audio action. play: play a sound (creates Sound if needed). stop: stop a playing sound. pause: pause a playing sound. resume: resume a paused sound. set_listener: set the audio listener type/target."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the Sound instance or parent to create Sound in. Used by: play, stop, pause, resume."},soundId:{type:"string",description:'Roblox sound asset ID (e.g., "rbxassetid://1234567"). Used by: play (if creating a new Sound).'},volume:{type:"number",description:"Sound volume (0-10). Used by: play. Default: 0.5."},looped:{type:"boolean",description:"Whether the sound loops. Used by: play. Default: false."},playbackSpeed:{type:"number",description:"Playback speed multiplier. Used by: play. Default: 1."},listenerType:{type:"string",description:"Listener type for SoundService. Used by: set_listener.",enum:["Camera","CFrame","ObjectPosition","ObjectCFrame"]},listenerPath:{type:"string",description:"Instance path for ObjectPosition/ObjectCFrame listener type. Used by: set_listener."}},required:["action"]}};var fk={name:"manage_animation",description:"[PRO] Animation: load, play, stop animations. Get animation tracks from humanoid/controller.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["load","play","stop","get_tracks"],description:"[PRO] Animation action. load: load an animation on a Humanoid/AnimationController. play: play a loaded animation track. stop: stop a playing animation. get_tracks: list all loaded animation tracks."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the Humanoid, AnimationController, or Model containing one. Used by: load, play, stop, get_tracks."},animationId:{type:"string",description:'Roblox animation asset ID (e.g., "rbxassetid://1234567"). Used by: load.'},trackId:{type:"string",description:"Animation track identifier returned by load. Used by: play (required), stop (required)."},speed:{type:"number",description:"Playback speed multiplier. Used by: play. Default: 1."},fadeTime:{type:"number",description:"Fade time in seconds when starting/stopping. Used by: play, stop. Default: 0.1."},weight:{type:"number",description:"Animation weight (0-1) for blending. Used by: play. Default: 1."},priority:{type:"string",description:"Animation priority. Used by: play.",enum:["Core","Idle","Movement","Action","Action2","Action3","Action4"]}},allOf:[{if:{properties:{action:{const:"play"}}},then:{required:["trackId"]}},{if:{properties:{action:{const:"stop"}}},then:{required:["trackId"]}}],required:["action"]}};var mk={name:"manage_physics",description:"[PRO] Physics collision groups: register, set collidable between groups, list groups.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["register_group","set_collidable","get_groups"],description:"[PRO] Physics action. register_group: register a new collision group with PhysicsService. set_collidable: set whether two groups can collide. get_groups: list all registered collision groups."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},groupName:{type:"string",description:"Collision group name. Used by: register_group (required)."},group1:{type:"string",description:"First collision group name. Used by: set_collidable (required)."},group2:{type:"string",description:"Second collision group name. Used by: set_collidable (required)."},collidable:{type:"boolean",description:"Whether the two groups can collide. Used by: set_collidable (required)."}},allOf:[{if:{properties:{action:{const:"register_group"}}},then:{required:["groupName"]}},{if:{properties:{action:{const:"set_collidable"}}},then:{required:["group1","group2","collidable"]}}],required:["action"]}};var hk={name:"manage_effects",description:"[PRO] Particle effects: emit particles, clear all particles, toggle effect enabled state.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["emit","clear","toggle"],description:"[PRO] Effects action. emit: emit a burst of particles from a ParticleEmitter. clear: clear all particles from a ParticleEmitter or all emitters under an instance. toggle: enable or disable a ParticleEmitter, Beam, Trail, or other effect."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},path:{type:"string",description:"Path to the effect instance (ParticleEmitter, Beam, Trail) or parent containing effects. Used by: emit, clear, toggle."},count:{type:"number",description:"Number of particles to emit in a burst. Used by: emit. Default: 16."},enabled:{type:"boolean",description:"Enable or disable the effect. Used by: toggle."}},required:["action"]}};var gk={name:"manage_terrain",description:"[PRO] Terrain operations: fill shapes, clear regions, replace materials, manage colors, read/write voxels, generate procedural terrain, smooth terrain.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["fill_block","fill_ball","fill_cylinder","fill_wedge","clear_region","clear_bounds","replace_material","colors_get","colors_set","read_voxel","read_voxels","write_voxels","generate","smooth"],description:"[PRO] Terrain action. fill_block/fill_ball/fill_cylinder/fill_wedge: fill shapes with material. clear_region/clear_bounds: clear terrain. replace_material: swap materials in region. colors_get/colors_set: manage material colors. read_voxel: single voxel. read_voxels/write_voxels: bulk voxels. generate: procedural terrain. smooth: smooth terrain."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},material:{type:"string",description:"Terrain material name (e.g., Grass, Rock, Water, Sand, Slate, Concrete). Used by: fill_*, replace_material, colors_get, colors_set."},center:{type:"object",description:"Center position as Vector3. Used by: fill_ball.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},position:{type:"object",description:"Position as Vector3. Used by: read_voxel.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},cframe:{type:"object",description:"Position and rotation for fill shapes. Used by: fill_block, fill_cylinder, fill_wedge.",required:["position"],properties:{position:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},rotation:{type:"object",description:"Optional rotation in degrees.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},size:{type:"object",description:"Size in studs. Used by: fill_block, fill_wedge.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},radius:{type:"number",description:"Radius in studs. Used by: fill_ball, fill_cylinder."},height:{type:"number",description:"Height in studs. Used by: fill_cylinder."},region:{type:"object",description:"Rectangular region with min/max corners. Used by: clear_region, replace_material, read_voxels, write_voxels, generate, smooth.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},min:{type:"object",description:"Minimum corner for bounds-based clear. Used by: clear_bounds.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",description:"Maximum corner for bounds-based clear. Used by: clear_bounds.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},sourceMaterial:{type:"string",description:"Material to replace. Used by: replace_material."},targetMaterial:{type:"string",description:"Replacement material. Used by: replace_material."},color:{type:"object",description:"RGB color (0-255). Used by: colors_set.",required:["r","g","b"],properties:{r:{type:"number",minimum:0,maximum:255},g:{type:"number",minimum:0,maximum:255},b:{type:"number",minimum:0,maximum:255}}},resolution:{type:"number",description:"Voxel resolution (studs per voxel). Used by: read_voxels, write_voxels. Default: 4."},materials:{type:"array",description:"3D array of material names [x][y][z]. Used by: write_voxels.",items:{type:"array",items:{type:"array",items:{type:"string"}}}},occupancy:{type:"array",description:"3D array of occupancy values [x][y][z] (0-1). Used by: write_voxels.",items:{type:"array",items:{type:"array",items:{type:"number",minimum:0,maximum:1}}}},baseHeight:{type:"number",description:"Base terrain height in studs. Used by: generate. Default: 32."},amplitude:{type:"number",description:"Height variation amplitude in studs. Used by: generate. Default: 24."},frequency:{type:"number",description:"Noise frequency (0.001-0.1). Used by: generate. Default: 0.01.",minimum:.001,maximum:.1},seed:{type:"number",description:"Random seed for terrain generation. Used by: generate."},layers:{type:"array",description:"Material layers by height. Used by: generate. Each: {material, maxHeight}.",items:{type:"object",required:["material","maxHeight"],properties:{material:{type:"string"},maxHeight:{type:"number"}}}},intensity:{type:"number",description:"Smoothing intensity (0-1). Used by: smooth. Default: 0.5.",minimum:0,maximum:1}},required:["action"]}};var yk={name:"spatial_query",description:"[PRO] Spatial queries: raycast, find ground, check placement, multi-raycast, scan area, find flat areas, find spawn positions, analyze walkable, spatial map, find empty space, get bounds, snap to grid, check collision.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["raycast","find_ground","check_placement","multi_raycast","scan_area","find_flat","find_spawn","analyze_walkable","spatial_map","find_space","bounds","snap_grid","collision"],description:"[PRO] Spatial action. raycast: single ray. find_ground: ground below point. check_placement: collision-free placement check. multi_raycast: batch rays. scan_area: heightmap generation. find_flat: flat areas for building. find_spawn: spawn positions. analyze_walkable: walkability grid. spatial_map: all BasePart positions. find_space: empty space for object. bounds: bounding box. snap_grid: snap position to grid. collision: AABB collision check."},origin:{type:"object",description:"Ray origin as Vector3. Used by: raycast.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},direction:{type:"object",description:"Ray direction and length as Vector3. Used by: raycast.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},maxDistance:{type:"number",description:"Maximum distance for ground search. Used by: find_ground. Default: 1000."},position:{type:"object",description:"Position as Vector3. Used by: find_ground (cast from here), check_placement (center), collision (hypothetical position).",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},size:{type:"object",description:"Size as Vector3. Used by: check_placement (object size), find_space (required space size), scan_area (area X/Z dimensions).",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},filterType:{type:"string",description:'Raycast filter type. Used by: raycast, multi_raycast. Default: "Exclude".',enum:["Exclude","Include"]},filterList:{type:"array",description:"Instance paths to filter. Used by: raycast, multi_raycast, find_ground, check_placement, scan_area. Alias: filterInstances.",items:{type:"string"}},filterInstances:{type:"array",description:"Instance paths to filter (alias for filterList). Used by: raycast, multi_raycast, find_ground, check_placement, scan_area.",items:{type:"string"}},ignoreWater:{type:"boolean",description:"Ignore terrain water. Used by: raycast, multi_raycast. Default: false."},rays:{type:"array",description:"Array of ray specifications. Used by: multi_raycast. Max 50 rays.",maxItems:50,items:{type:"object",required:["origin","direction"],properties:{origin:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},direction:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}}},center:{type:"object",description:"Center point for area scan. Used by: scan_area.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},area:{type:"object",description:"Bounding box for analysis. Used by: analyze_walkable. Contains min/max.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},searchArea:{type:"object",description:"Bounding box for search. Used by: find_flat, find_spawn, find_space.",required:["min","max"],properties:{min:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},max:{type:"object",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}}}},minSize:{type:"object",description:"Minimum flat area size. Used by: find_flat.",required:["x","z"],properties:{x:{type:"number"},z:{type:"number"}}},maxSlope:{type:"number",description:"Maximum slope in degrees. Used by: find_flat (default: 10), analyze_walkable (default: 45)."},resolution:{type:"number",description:"Grid resolution in studs. Used by: scan_area, analyze_walkable. Default: 4."},gridSize:{type:"number",description:"Grid snap size in studs. Used by: snap_grid (default: 4), find_space (default: 4)."},path:{type:"string",description:"Instance path. Used by: bounds (optional), snap_grid (required), collision (required), spatial_map (rootPath alias)."},paths:{type:"array",description:"Multiple instance paths for batch bounds. Used by: bounds.",items:{type:"string"}},tolerance:{type:"number",description:"Height variation tolerance in studs. Used by: find_flat."},gridSnap:{type:"number",description:"Grid snap size. Alias for gridSize."},axes:{type:"array",description:'Axes to snap. Used by: snap_grid. Default: ["x","y","z"]. Use ["x","z"] for horizontal only.',items:{type:"string",enum:["x","y","z"]}},ignorePaths:{type:"array",description:"Instance paths to ignore in collision check. Used by: collision.",items:{type:"string"}},offset:{type:"number",description:"Vertical offset for ground position. Used by: find_ground. Default: 0."},rotation:{type:"object",description:"Rotation in degrees. Used by: check_placement.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},checkGround:{type:"boolean",description:"Verify ground support for placement. Used by: check_placement. Default: true."},spawnSize:{type:"object",description:"Size of spawning entity. Used by: find_spawn. Default: {x:4, y:5, z:4}.",properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},minSpacing:{type:"number",description:"Minimum distance between spawn positions. Used by: find_spawn. Default: 10."},preferOutdoor:{type:"boolean",description:"Prefer open sky positions. Used by: find_spawn. Default: false."},count:{type:"number",description:"Number of results to find. Used by: find_spawn. Default: 10."},characterHeight:{type:"number",description:"Character height for clearance checks. Used by: analyze_walkable. Default: 5."},maxStepHeight:{type:"number",description:"Maximum step height. Used by: analyze_walkable. Default: 2."},rootPath:{type:"string",description:'Root path for spatial map scan. Used by: spatial_map. Default: "game.Workspace".'},includeModels:{type:"boolean",description:"Include Model bounding boxes. Used by: spatial_map. Default: true."},maxResults:{type:"number",description:"Maximum results. Used by: spatial_map (default: 500), scan_area (default: 500), find_flat (default: 10)."},padding:{type:"number",description:"Minimum distance from other objects. Used by: find_space. Default: 1."}},allOf:[{if:{properties:{action:{const:"snap_grid"}}},then:{required:["path"]}},{if:{properties:{action:{const:"collision"}}},then:{required:["path"]}}],required:["action"]}};var vk={name:"manage_assets",description:"[PRO] Asset management: insert models by ID, get asset info, search creator store, insert free models/packages, export selection.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["insert","info","search","search_insert","insert_free","insert_package","export"],description:"[PRO] Asset action. insert: insert model by asset ID. info: get asset metadata. search: search Creator Store. search_insert: search and insert first match. insert_free: insert free model. insert_package: insert package. export: export current selection."},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},assetId:{type:"number",description:"Roblox asset ID. Used by: insert (required), info (required), insert_free (required), insert_package (required)."},parent:{type:"string",description:'Parent path for inserted asset. Used by: insert, search_insert, insert_free, insert_package. Default: "game.Workspace".'},name:{type:"string",description:"Optional name for inserted instance."},position:{type:"object",description:"Position to place the model. Used by: insert, insert_free.",required:["x","y","z"],properties:{x:{type:"number"},y:{type:"number"},z:{type:"number"}}},query:{type:"string",description:'Search keyword. Used by: search (required), search_insert (required). E.g., "monster", "tree", "car".'},maxResults:{type:"number",description:"Maximum search results. Used by: search. Default: 10."},category:{type:"string",description:"Asset category filter. Used by: search."},sortType:{type:"string",description:"Sort order for search results. Used by: search."},format:{type:"string",description:"Export format. Used by: export."},includeProperties:{type:"boolean",description:"Include all properties in export. Used by: export. Default: false."},includeChildren:{type:"boolean",description:"Include children in export. Used by: export. Default: false."},maxDepth:{type:"number",description:"Maximum depth for recursive child export. Used by: export. Default: 5."}},allOf:[{if:{properties:{action:{const:"insert"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"info"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"search"}}},then:{required:["query"]}},{if:{properties:{action:{const:"search_insert"}}},then:{required:["query"]}},{if:{properties:{action:{const:"insert_free"}}},then:{required:["assetId"]}},{if:{properties:{action:{const:"insert_package"}}},then:{required:["assetId"]}}],required:["action"]}};var bk={name:"manage_sync",description:"[PRO] Project sync management: status, history, direction settings, read/write synced files.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["status_current_place","history","directions","read_file","write_file","progress"],description:"[PRO] Sync action. status_current_place: get sync status for the current connected place. history: get change history. directions: get per-type sync directions. read_file: read a synced file. write_file: write to a synced file. progress: get real-time sync progress and bandwidth."},placeId:{type:"number",description:"Place ID for sync operations. Used by: history, directions, read_file, write_file."},instancePath:{type:"string",description:"Instance path for file read/write. Used by: read_file, write_file."},content:{type:"string",description:"File content to write. Used by: write_file."},query:{type:"object",description:"Query parameters for history. Used by: history.",properties:{limit:{type:"number",description:"Maximum entries to return."},offset:{type:"number",description:"Number of entries to skip."}}}},allOf:[{if:{properties:{action:{const:"read_file"}}},then:{required:["placeId","instancePath"]}},{if:{properties:{action:{const:"write_file"}}},then:{required:["placeId","instancePath","content"]}}],required:["action"]}};var xk={name:"workspace_state",description:"[PRO] Workspace state: full sync, snapshot, recent changes, viewport info, clear history, metadata, scripts, selection info, clear cache.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["sync","snapshot","changes","viewport","clear_history","metadata","scripts","selection_info","clear_cache"],description:"[PRO] State action. sync: fetch current Workspace state (hierarchy, history, stats). snapshot: get full instance tree structure. changes: get recent changes (added/removed/modified). viewport: get camera position, FOV, viewport size, selection bounds. clear_history: clear change history. metadata: get workspace metadata. scripts: get script list. selection_info: get selection info. clear_cache: clear state cache."},includeMetadata:{type:"boolean",description:"Include metadata (instance counts, timestamps). Used by: sync. Default: true."},limit:{type:"number",description:"Maximum number of changes to return. Used by: changes. Default: 20."},includeCameraInfo:{type:"boolean",description:"Include camera position and settings. Used by: viewport. Default: true."},includeSelectionBounds:{type:"boolean",description:"Include current selection bounds info. Used by: viewport. Default: true."}},required:["action"]}};var _k={name:"manage_logs",description:"Output logs: get filtered logs, poll incrementally with sinceSeq cursor, clear buffer, get recent errors.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["get","clear","errors"],description:"Log action. get: retrieve logs with optional level/limit/since/sinceSeq filters. clear: clear internal log buffer without resetting seq. errors: quick access to recent errors only."},level:{type:"string",description:'Log level filter. Used by: get. Default: "all".',enum:["all","error","warning","info"]},limit:{type:"number",description:"Maximum entries to return. Used by: get (default: 100, max: 500), errors (default: 20, max: 100)."},pattern:{type:"string",description:"Text pattern to filter log messages. Used by: get."},since:{type:"number",description:"Unix timestamp in milliseconds. Only logs after this time. Used by: get."},sinceSeq:{type:"number",description:"Return only logs after this sequence number. Cursor mode returns logs oldest-to-newest and includes lastSeq/oldestSeq/hasMore/cursorStatus. Used by: get."}},required:["action"]}};var Sk={name:"system_info",description:"System info: ping, connection status, usage tier. [PRO] place info, services list, studio settings, playtest control, automated test runner.",inputSchema:{type:"object",properties:{action:{type:"string",enum:["ping","connection","usage","place_info","services","studio_settings","play","stop","pause","resume","play_status","run_test"],description:'System action. Basic: ping (test connection, returns pong), connection (server/plugin connection info with session IDs), usage (current tier: basic/pro). [PRO]: place_info (place ID, name, game ID, creator), services (list all Roblox services), studio_settings (Studio preferences), play (start playtest, mode: "play"=F5 or "run"=F8), stop (stop playtest), pause (pause playtest), resume (resume paused playtest), play_status (get current state: edit/running/paused with mode and available actions), run_test (inject test script, run playtest, collect logs, and write local report files).'},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},message:{type:"string",description:"Optional message to echo back. Used by: ping."},mode:{type:"string",description:'Playtest mode. "play" = Play mode (F5, default), "run" = Run mode (F8). Used by: play, run_test.',enum:["play","run"]},script:{type:"string",description:"Luau test body to inject into ServerScriptService.__MCP_TestRunner. Used by: run_test."},test_name:{type:"string",description:"Optional report display name for the automated playtest run. Used by: run_test."},timeout:{type:"number",description:"Timeout in seconds for the automated playtest run. Default: 60. Maximum: 300. Used by: run_test."}},allOf:[{if:{properties:{action:{const:"run_test"}}},then:{required:["script"]}}],required:["action"]}};var wk={name:"manage_studio",description:`Control Roblox Studio editor view/rendering settings for screenshot and QA workflows. Use it to toggle Studio session-level View settings such as UI preview; it does not edit game UI objects or their properties.
|
|
130
|
+
`,inputSchema:{type:"object",properties:{action:{type:"string",enum:["toggle_ui_preview"],description:`Studio editor action. [PRO]: toggle_ui_preview (toggle StarterGui UI rendering in Edit mode through the Studio UI preview setting).
|
|
131
|
+
`},contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},enabled:{type:"boolean",description:`Boolean value to set. Omit to toggle the current value.
|
|
132
132
|
Used by: toggle_ui_preview (optional).
|
|
133
133
|
`}},required:["action"]}};var kk={name:"batch_execute",description:'[PRO] Execute multiple commands in a single batch. Each command is an object with "tool" name and "args". Commands execute sequentially; optionally continue on error.',inputSchema:{type:"object",properties:{commands:{type:"array",description:"Array of commands to execute sequentially. Each command specifies a tool name and its arguments.",items:{type:"object",required:["tool","args"],properties:{tool:{type:"string",description:'Tool name to invoke (e.g., "mutate_instances", "manage_properties").'},args:{type:"object",description:"Arguments to pass to the tool, including action and other parameters.",additionalProperties:!0}}}},stopOnError:{type:"boolean",description:"If true, stop executing remaining commands when one fails. Default: true."}},required:["commands"]}};var Ck={name:"execute_luau",description:"[PRO] Execute arbitrary Luau code in Roblox Studio sandbox. Blocked services: HttpService, DataStoreService, MessagingService. Cannot access CoreGui/CorePackages.",inputSchema:{type:"object",properties:{contextId:{type:"string",description:"Optional execution context identifier. Used to continue an existing context for mutating actions."},contextSummary:{type:"object",properties:{intent:{type:"string"},affectedAreas:{type:"array",items:{type:"object",required:["kind","target","label"],properties:{kind:{type:"string",enum:["script","instance","property","system","gameplay","ui","terrain","lighting","asset","other"]},target:{type:"string"},label:{type:"string"}}}},testScenario:{type:"string"},expectedBehavior:{type:"string"},observedBehavior:{type:"string"}},description:"Optional structured execution context attached to this tool call."},replayMetadata:{type:"object",properties:{mode:{type:"string"},timeout:{type:"number"},seed:{type:"number"},inputRefs:{type:"array",items:{type:"string"}},setupRefs:{type:"array",items:{type:"string"}},assertionRefs:{type:"array",items:{type:"string"}},artifactRefs:{type:"array",items:{type:"string"}}},description:"Optional replay-ready metadata attached to this tool call."},source:{type:"string",description:"Luau source code to execute. The code runs in a sandboxed environment with access to game services (except blocked ones). Return values are serialized and sent back as the tool result."}},required:["source"]}};var Yu=[rk,ik,ak,ok,sk,ck,lk,uk,pk,dk,fk,mk,hk,gk,yk,vk,bk,xk,_k,Sk,wk,kk,Ck];var Ik="weppy://ui-studio/guide",Pk="weppy://ui-studio/tokens",kA='# UI Studio Guide\n\n**Audience:** AI agents (Claude / Codex / Gemini / Cursor) and developers building Roblox UI through UI Studio.\n**Enforcement:** This document is served as MCP resource `weppy://ui-studio/guide` when `manage_ui` is invoked. The `manage_ui.check` action runs Design Check suggestions for the numeric thresholds in \xA7A. Style sections (\xA7B, \xA7C) describe the design space and are not Design Check-enforced.\n\n## How to use this guide\n\nThis guide is split into three layers. Read them with different intent:\n\n1. **\xA7A \u2014 Floor.** Non-negotiable. Every UI you produce must satisfy these. They protect accessibility and platform usability regardless of style.\n2. **\xA7B \u2014 Vocabulary.** A menu, not a checklist. Each subsection lists *families* of choices. Pick the one that fits the brief; reject the rest. Do not blend everything.\n3. **\xA7C \u2014 Direction.** Heuristics for translating a `design_brief` into a coherent visual identity. The brief decides; the guide only widens the option space.\n\nA good UI Studio output is **not** an average of all options. It commits to one direction and executes it well within the Floor.\n\n### Context-first brief gate\n\nDo not start ambiguous UI work by dumping enum lists at the user. If the user asks for "a UI", "redesign the UI", or another broad request, call `manage_ui.design_brief` with an omitted or partial `brief` and use its Studio-aware response.\n\nBefore asking for missing fields, inspect the current context:\n\n- If `StarterGui` has no `ScreenGui`, treat broad requests as a new UI creation task.\n- If existing `ScreenGui` objects exist and the user asks to change or redesign UI, summarize likely target roots and offer concrete change scopes.\n- If the current selection is a GUI object, treat it as the strongest target signal unless the user asks for the whole UI.\n- Use `Workspace` names, existing UI names/text/colours, and `contextSummary` to infer game type and style confidence.\n\nAsk at most one question at a time. A good question includes the current-state summary, one recommended choice, one or two alternatives, and a free-form escape hatch. When confidence is strong, the question should be a confirmation prompt such as "I recommend this brief; should I proceed?" rather than a raw request for `purpose`, `platform`, or `tone`.\n\nRecommendations must include their evidence and an exact `brief_patch` the agent can apply if the user says "use the recommendation". Do not silently commit identity-defining choices when confidence is weak. Safe technical defaults such as `reference_assets=[]`, `accessibility={min_text_size=14, contrast_ratio=4.5}`, and a resolution/safe-zone target may be recommended, but they become final only after the user accepts the recommendation or provides equivalent intent.\n\n### Asset recommendation gate\n\nBefore image-heavy UI generation, check whether usable assets already exist. A useful recommendation can come from:\n\n- user-provided `brief.reference_assets`\n- existing `ImageLabel.Image` / `ImageButton.Image` values in the relevant `StarterGui` UI\n- a conservative `manage_assets.search` query when the purpose and game type imply a specific need, such as `coin icon`, `shop pet thumbnail`, `quest portrait`, or `fantasy menu banner`\n\nDo not use a recommended asset until the user accepts it. If `manage_ui.design_brief` returns `asset_recommendations`, summarize the strongest candidate, explain the evidence, and ask one question: use it, skip image assets, or provide a different asset. User references outrank existing UI images; existing UI images outrank Creator Store search. Low-confidence or generic Creator Store results should not become the default.\n\nIf no accepted asset is available, intentionally choose no-imagery or one assetless recipe from \xA7B.8. Do not invent `rbxassetid://...` values.\n\nNatural language is preferred in user-facing prompts. Internal enum values (`purpose`, `platform`, `tone`, palette keys) should appear as machine-readable recommendation values, not as the main question.\n\n## Table of Contents\n\n- \xA7A. Floor (non-negotiable)\n - A.1 Touch & focus targets\n - A.2 Platform safe zones\n - A.3 Text contrast\n - A.4 `TextScaled` discipline\n - A.5 Accessibility checklist\n - A.6 In-game UI: transparent root default\n- \xA7B. Vocabulary (expandable menu)\n - B.1 Palette families\n - B.2 Typography families\n - B.3 Layout families\n - B.4 Surface treatments\n - B.5 Corner & edge languages\n - B.6 Density & spacing\n - B.7 Motion families\n - B.8 Imagery & icon language\n - B.9 Ornamentation & depth\n - B.10 Composition\n- \xA7C. Direction (brief \u2192 choices)\n - C.0 Reach for game quality\n - C.1 Reading the brief\n - C.2 Worked contrasts (one brief, multiple valid directions)\n - C.3 Anti-homogenization rules\n- \xA7D. Tree encoding\n\n---\n\n## \xA7A. Floor (non-negotiable)\n\nThese rules apply to every output. The `manage_ui.check` action checks these numbers.\n\n### A.1 Touch & focus targets\n\nMeasured against `AbsoluteSize` (i.e., the resolved Scale + Offset).\n\n| Platform | Min hit size | Min spacing | Recommended |\n|---|---|---|---|\n| Mobile | 44 \xD7 44 | 8 | 48 \xD7 48 |\n| Tablet | 40 \xD7 40 | 8 | 44 \xD7 44 |\n| Desktop | 24 \xD7 24 (click area 32 \xD7 32) | 4 | 32 \xD7 32 |\n| Console | 48 \xD7 48 (focus outline \u2265 2 px) | 12 | 56 \xD7 56 |\n\nMobile minimum follows Apple HIG. Console minimum accounts for TV viewing distance.\n\n### A.2 Platform safe zones\n\nReject primary interactions outside these regions.\n\n- **Mobile portrait (390 \xD7 844, iPhone-class):** top inset \u2265 88 (status bar 44 + notch 44), bottom \u2265 34 (home indicator), sides \u2265 16.\n- **Mobile landscape (844 \xD7 390):** sides \u2265 44 (notch), top/bottom \u2265 12.\n- **Tablet (1024 \xD7 768):** top/bottom \u2265 20, sides \u2265 24.\n- **Desktop 1080p / 1440p:** outer margin 32\u201348; no enforced safe zone, but consider Windows snap regions.\n- **Console (Xbox / Switch TV, 1920 \xD7 1080):** TV-safe 90% \u2014 primary interactions inside x \u2208 [96, 1824], y \u2208 [54, 1026].\n\nFor dynamic insets, prefer `GuiService:GetGuiInset()` and the `ScreenInsets` property over hard-coded constants.\n\n### A.3 Text contrast\n\nWCAG AA: contrast ratio \u2265 4.5:1 for body text, \u2265 3.0:1 for large text (\u2265 18 px bold or \u2265 24 px regular).\n\n- Compute against the resolved background, not the parent\'s nominal `BackgroundColor3`. Translucent backgrounds blend with whatever sits behind them.\n- `TextStrokeTransparency < 1`: blend stroke colour by `(1 \u2212 transparency)` and re-evaluate.\n- Do not treat `UIStroke` as text background. `UIStroke` outlines the `GuiObject`, not the glyphs; glyph-surrounding strokes are `TextStrokeColor3` / `TextStrokeTransparency`.\n\n### A.4 `TextScaled` discipline\n\n`TextScaled = true` without a `UITextSizeConstraint` produces unpredictable sizing across locales (Korean, Japanese, Arabic glyphs differ in metrics from Latin).\n\n- Prefer explicit `TextSize`.\n- If `TextScaled` is required (e.g., responsive title), **always** pair with `UITextSizeConstraint` with `MinTextSize` and `MaxTextSize` set.\n\n### A.5 Accessibility checklist\n\n- [ ] All text contrast \u2265 4.5:1 (large text \u2265 3.0:1), computed against the effective background and text stroke, not `UIStroke`.\n- [ ] All touch targets meet \xA7A.1 minimums.\n- [ ] Every `TextScaled = true` has a sibling `UITextSizeConstraint`.\n- [ ] State is never communicated by colour alone \u2014 pair with icon, text, or shape.\n- [ ] Console / gamepad: `NextSelectionUp/Down/Left/Right` chain is explicit; focus highlight has \u2265 2 px contrast outline.\n- [ ] Motion-sensitive users: respect any reduced-motion signal; avoid full-screen flashes > 3 Hz.\n\n### A.6 In-game UI: transparent root default\n\nIn-game UI such as HUDs, floating buttons, toasts, and cards sits on top of live gameplay. The default root structure is therefore:\n\n```text\nScreenGui\n \u2514\u2500 Frame (BackgroundTransparency = 1, transparent root)\n \u2514\u2500 actual UI elements\n```\n\nA full-screen opaque `Frame` as the first child of a `ScreenGui` hides the game world. Reserve that for `modal` flows. For `HUD`, `button`, `toast`, `card`, and most `menu` work, use a transparent root and bounded panels or edge-anchored controls. `manage_ui.check` reports `backdrop_default` when a non-modal brief produces a full-screen opaque root.\n\n---\n\n## \xA7B. Vocabulary (expandable menu)\n\nEach subsection is a menu of *families*. Pick one family per subsection that matches the brief; reject the rest. Mixing families across subsections is encouraged when intentional (e.g., "brutalist palette + editorial typography"); mixing *within* a subsection produces visual incoherence.\n\n### B.1 Palette families\n\nThe token file (`weppy://ui-studio/tokens`, key `palettes`) ships many families. Each carries `mood`, `pairs_with`, `avoid_for` metadata \u2014 read them before picking.\n\nFamilies currently shipped (representative; consult tokens for the canonical list and metadata):\n\n`modern_dark` \xB7 `modern_light` \xB7 `casual_bright` \xB7 `retro_80s` \xB7 `minimalist_mono` \xB7 `brutalist_concrete` \xB7 `y2k_chrome` \xB7 `cottagecore_warm` \xB7 `cyberpunk_neon` \xB7 `horror_blood` \xB7 `editorial_serif` \xB7 `pastel_dream` \xB7 `terminal_green` \xB7 `ghibli_sky` \xB7 `arcade_pop`\n\nCustom palettes (`color_palette.source = "custom"`) are valid when no shipped family fits. Document the chosen mood in your reasoning.\n\n### B.2 Typography families\n\nPick a *family*, not a single font. Roblox built-ins span more breadth than most projects use.\n\n| Family | Typical fonts | When to consider |\n|---|---|---|\n| Geometric sans | `Gotham`, `Sarpanch`, `Montserrat` | Modern, neutral, corporate-friendly |\n| Humanist sans | `SourceSans`, `Nunito`, `Inter` | Friendly, readable, multilingual (Korean via SourceSans) |\n| Display / poster | `Bangers`, `FredokaOne`, `LuckiestGuy`, `Bungee` | Casual games, headlines, energy |\n| Editorial serif | `Merriweather`, `Garamond`, `RobotoSlab` | Narrative, lore, premium feel |\n| Monospace / terminal | `RobotoMono`, `Code`, `JetBrainsMono` | Sci-fi, hacker, debug HUD |\n| Handwritten / script | `Caveat`, `IndieFlower`, `PermanentMarker` | Cottagecore, indie, sketch UIs |\n| Pixel / bitmap | `PressStart2P`-style | Retro arcade, 8/16-bit homages |\n| Horror / decorative | `Creepster`, `Nosifer` | Genre accents only \u2014 never body |\n\nType sizes are a *modular scale*, not a fixed table. Pick a ratio and derive sizes from it:\n\n- 1.200 (minor third) \u2014 dense UIs, data heavy.\n- 1.333 (perfect fourth) \u2014 balanced default.\n- 1.500 (perfect fifth) \u2014 generous, marketing.\n- 1.618 (golden) \u2014 editorial, cinematic.\n\nBody text floor: 14 px on mobile, 12 px on desktop. Captions never below 11 px.\n\n### B.3 Layout families\n\n| Family | Signature | Fits |\n|---|---|---|\n| Centered hierarchy | Single focal point centered, supporting elements radiating | Modal, splash, victory screen |\n| Asymmetric grid | Off-center anchors, deliberate negative space | Editorial menus, lore-heavy UIs |\n| Edge-bound HUD | Anchored to corners and edges; centre is gameplay | In-game HUD, combat overlay |\n| Stacked card flow | Vertical column of equal-weight cards | Inventory, shop, social feed |\n| Layered depth | Foreground / midground / background with parallax cues | Cinematic menus, story games |\n| Split panel | Two-thirds primary + one-third detail | Settings, character editor |\n| Diagonal / oblique | Rotated frames, off-axis bands | Sports, arcade, energy-driven |\n| Overflow-bleed | Elements cropped by viewport edges | Posters, fashion, bold marketing |\n\nChoose one. Stacked cards with diagonal accents on top of an asymmetric grid is noise.\n\n`UIListLayout` is appropriate when \u2265 3 homogeneous siblings exist. For overlap, rotation, or deliberate misalignment, use manual `AnchorPoint` + `Position`. `UIListLayout` ignores child `Position`.\n\n`UIAspectRatioConstraint` with `AspectType = "ScaleWithParentSize"`, `DominantAxis = "Width"` is the safe default for `ImageLabel` / `ImageButton` using Scale-based `Size` \u2014 it survives device rotation.\n\n### B.4 Surface treatments\n\n| Treatment | Visual | Pairs with |\n|---|---|---|\n| Flat | Single fill, no depth cue | Minimalist, brutalist, terminal |\n| Soft elevation | Subtle drop shadow, no border | Modern, friendly |\n| Glassmorphic | Translucent fill + backdrop blur | Modern dark, futuristic \u2014 *but see anti-homogenization* |\n| Neumorphic | Inset / outset soft shadows on same-tone surface | Pastel, tactile UIs |\n| Paper / matte | Textured fill, no shadow | Cottagecore, editorial, indie |\n| Holographic / chrome | Gradient fills with iridescent shifts | Y2K, retro-future |\n| CRT / scanline | Vignette + scanline overlay | Terminal, horror, retro |\n| Brutalist raw | Hard 1\u20132 px borders, no shadow, exposed structure | Brutalist, debug aesthetic |\n\n### B.5 Corner & edge languages\n\nPick *one* corner language for the whole UI. Mixing radii signals lack of intent.\n\n- **Sharp (0 px):** brutalist, editorial, terminal.\n- **Soft (4\u20138 px):** modern, friendly defaults \u2014 but watch for AI-default blandness.\n- **Rounded (12\u201324 px):** casual, kid-friendly, mobile-native.\n- **Pill (50% of height):** chips, tags, primary CTAs in playful UIs.\n- **Organic / asymmetric:** custom `UICorner` per corner \u2014 handcrafted, indie, rare.\n- **None + heavy stroke:** brutalist sub-style.\n\n### B.6 Density & spacing\n\nSpacing should follow a scale, never arbitrary pixels. The token file ships `spacing: [4, 8, 12, 16, 24, 32, 48, 64]` as the default scale. Pick a *density* per project:\n\n- **Compact** \u2014 base unit 4, scale `[4, 8, 12, 16]`. Data-dense, pro tools.\n- **Standard** \u2014 base unit 8, scale `[8, 16, 24, 32]`. Most games.\n- **Generous** \u2014 base unit 12, scale `[12, 24, 36, 48, 64]`. Cinematic, premium.\n- **Editorial** \u2014 base unit 16 with golden-ratio derivations. Story-driven.\n\n### B.7 Motion families\n\nRoblox motion via `TweenService` or `UIScale` / `UIGradient` animation.\n\n- **None.** Brutalist, terminal \u2014 interaction is instant. Strong choice when intentional.\n- **Functional ease.** 120\u2013200 ms `EasingStyle.Quad` for state changes. Modern default.\n- **Playful overshoot.** `Back` ease with overshoot for casual / kid UIs.\n- **Cinematic glide.** 400\u2013800 ms `Quint` or `Sine` for menus, transitions.\n- **Mechanical snap.** Stepped frames, no easing \u2014 retro arcade, pixel UIs.\n\nRespect a project-level reduced-motion flag if present; never animate > 3 Hz full-screen.\n\n**Game-feel accents (sub-family).** \uAC8C\uC784 UI \uD2B9\uC720\uC758 micro-feedback. \uC704 motion family \uC5B4\uB290 \uAC83\uACFC\uB3C4 \uC870\uD569 \uAC00\uB2A5\uD558\uC9C0\uB9CC \uC758\uB3C4\uC801\uC73C\uB85C \uCC44\uD0DD\uD560 \uAC83 \u2014 brutalist / terminal \uACB0\uC5D0\uC11C\uB294 *zero accent* \uAC00 \uC2DC\uADF8\uB2C8\uCC98\uB2E4.\n\n- **Press squash** \u2014 \uBC84\uD2BC down \uC2DC `UIScale` 0.94~0.97 \uB85C 80~120 ms \uC2A4\uCF00\uC77C \uB2E4\uC6B4, release \uC2DC 1.0 \uBCF5\uADC0.\n- **Currency tick** \u2014 \uC22B\uC790 \uBCC0\uD654 \uC2DC 60~200 ms \uCE74\uC6B4\uD2B8\uC5C5, \uB9C8\uC9C0\uB9C9 \uD504\uB808\uC784\uC5D0 `UIScale` 1.05 \u2192 1.0 \uBC31 \uC774\uC988\uB85C \uAC15\uC870.\n- **Glint sweep** \u2014 `UIGradient.Offset` \uC744 \uD654\uBA74 \uD3ED\uB9CC\uD07C 800~2000 ms \uC5D0 \uAC78\uCCD0 sweep. \uBCF4\uC0C1 / \uD68D\uB4DD / \uC2E0\uC0C1 \uD45C\uC2DC.\n- **Focus halo** \u2014 \uD638\uBC84 / \uCEE8\uD2B8\uB864\uB7EC selection \uC2DC \uC678\uACFD \uAE00\uB85C \uB610\uB294 \uC774\uC911 stroke \uD398\uC774\uB4DC \uC778 (120~200 ms).\n\n\uCC44\uD0DD\uC740 \uC635\uC158\uC774\uC9C0\uB9CC \uD55C \uBC88 \uCC44\uD0DD\uD588\uB2E4\uBA74 \uAC19\uC740 surface \uD328\uBC00\uB9AC \uC548\uC5D0\uC11C *\uC77C\uAD00* \uC801\uC73C\uB85C \u2014 primary CTA \uB9CC squash \uD558\uACE0 \uB2E4\uB978 \uBC84\uD2BC\uC740 \uC548 \uD558\uBA74 \uC0AC\uC6A9\uC790\uB294 \uC6B0\uC5F0\uC73C\uB85C \uBC1B\uC544\uB4E4\uC778\uB2E4.\n\n### B.8 Imagery & icon language\n\nImagery \uB294 \uAC8C\uC784 UI \uB97C SaaS \uB300\uC2DC\uBCF4\uB4DC\uC640 \uAD6C\uBCC4 \uC9D3\uB294 *\uAC00\uC7A5 \uD070 \uB2E8\uC77C \uBCC0\uC218* \uB2E4. \uC5B4\uB5A4 family \uB97C \uACE0\uB97C\uC9C0 *\uBC18\uB4DC\uC2DC \uC758\uB3C4* \uB85C \uACB0\uC815\uD55C\uB2E4.\n\n| Family | \uC2DC\uADF8\uB2C8\uCC98 | \uC801\uD569 |\n|---|---|---|\n| **No-imagery** | ImageLabel \uC0AC\uC6A9 \uC548 \uD568, \uD14D\uC2A4\uD2B8\xB7\uB3C4\uD615\uB9CC | brutalist, terminal, editorial, minimalist \uACB0\uC5D0\uC11C *\uC801\uADF9\uC801 \uC120\uD0DD* \u2014 \uD68C\uD53C \uC544\uB2D8 |\n| **Pictogram set** | 16 / 20 / 24 / 32 px \uB2E8\uC0C9 \uB610\uB294 \uB4C0\uC624\uD1A4 \uC544\uC774\uCF58. \uC77C\uAD00\uB41C \uB77C\uC778\xB7\uB3C4\uD615 \uC5B4\uD718 | HUD, \uBAA8\uB358 UI, \uBAA8\uBC14\uC77C \uAC8C\uC784 |\n| **Item / pet thumbnail** | \uCE74\uB4DC\uC758 50\u201370% \uBA74\uC801\uC744 \uCC28\uC9C0\uD558\uB294 \uC77C\uB7EC\uC2A4\uD2B8, \uD14D\uC2A4\uD2B8\uB294 \uBCF4\uC870 | \uC778\uBCA4\uD1A0\uB9AC, \uC2DC\uBBAC\uB808\uC774\uD130\xB7\uD3AB\xB7RPG \uACB0, \uC0C1\uC810 \uCE74\uB4DC |\n| **Character portrait** | \uB465\uADFC/\uC0AC\uAC01 \uD504\uB808\uC784 \uC548 \uCE90\uB9AD\uD130 | \uB2E4\uC774\uC5BC\uB85C\uADF8, \uD504\uB85C\uD544, \uD018\uC2A4\uD2B8 \uD5E4\uB354 |\n| **Decorative illustration** | \uBC30\uACBD \uB610\uB294 \uD5E4\uB354\uC758 \uBD84\uC704\uAE30 \uC77C\uB7EC\uC2A4\uD2B8, \uC778\uD130\uB799\uC158 \uC5C6\uC74C | \uBA54\uB274, \uC2A4\uD50C\uB798\uC2DC, \uC2A4\uD1A0\uB9AC \uAC8C\uC784 |\n| **Diegetic surface** | \uC885\uC774 / \uC804\uAD11\uD310 / \uD640\uB85C\uADF8\uB7A8 \uB4F1 \uAC8C\uC784 \uC138\uACC4 \uC548\uC758 \uD45C\uBA74\uC744 \uD749\uB0B4 | \uC784\uBA38\uC2DC\uBE0C, \uD638\uB7EC, \uC0AC\uC774\uD30C\uC774 |\n\n\uADDC\uCE59:\n\n- \uD55C \uD654\uBA74\uC5D0 imagery family \uB294 **\uD558\uB098** \uB85C commit. pictogram + item-thumbnail + portrait \uB97C \uD55C \uD654\uBA74\uC5D0\uC11C \uC11E\uC73C\uBA74 \uB178\uC774\uC988.\n- ImageLabel / ImageButton \uC758 `Image` \uC18D\uC131\uC740 `rbxassetid://...`. \uAC00\uC774\uB4DC\uC5D0\uC11C *\uD2B9\uC815 asset id \uB97C \uBC15\uC9C0 \uC54A\uB294\uB2E4* \u2014 \uADF8\uB7EC\uBA74 \uADF8 id \uAC00 \uB610 \uB2E4\uB978 default \uAC00 \uB41C\uB2E4. \uC0AC\uC6A9\uC790\uAC00 reference \uB85C \uC81C\uACF5\uD558\uAC70\uB098 `manage_assets.search` \uACB0\uACFC\uB97C \uC0AC\uC6A9\uD55C\uB2E4.\n- Pictogram set \uCC44\uD0DD \uC2DC \uD55C \uBB36\uC74C\uB9CC \u2014 \uB4C0\uC624\uD1A4\xB7\uB77C\uC778\xB7\uC194\uB9AC\uB4DC \uC11E\uC9C0 \uB9D0 \uAC83.\n- Item thumbnail family \uCC44\uD0DD \uC2DC \uCE74\uB4DC \uBCF8\uBB38 \uD14D\uC2A4\uD2B8\uB294 \uBD80\uC81C\xB7\uC218\uB7C9\xB7\uAC00\uACA9 \uC815\uB3C4\uB85C \uC9E7\uAC8C \uC720\uC9C0.\n- `UIAspectRatioConstraint` \uB85C \uC774\uBBF8\uC9C0 \uBE44\uC728 \uBCF4\uD638 (\xA7B.3 \uCC38\uC870).\n- **No-imagery \uB3C4 \uC815\uB2F9\uD55C \uC120\uD0DD.** brief \uAC00 minimalist / brutalist / terminal \uC77C \uB54C \uC815\uB2F5\uC77C \uC218 \uC788\uB2E4. \uBAA8\uB4E0 \uAC8C\uC784 UI \uAC00 \uC774\uBBF8\uC9C0\uB97C \uAC00\uC838\uC57C \uD55C\uB2E4\uB294 \uB73B \uC544\uB2D8.\n\nAsset \uC5C6\uC774\uB3C4 game UI \uB2F5\uAC8C \uBCF4\uC77C \uC218 \uC788\uB2E4. \uC2E4\uC81C `Image` asset \uC774 \uC5C6\uC73C\uBA74 \uB2E4\uC74C Roblox-safe icon recipe \uC911 \uD558\uB098\uB97C \uACE0\uB978\uB2E4:\n\n- **Glyph badge** \u2014 `TextLabel` \uC5D0 \uB2E8\uC77C \uAE00\uB9AC\uD504/\uC9E7\uC740 \uC57D\uC5B4\uB97C \uB123\uACE0, \uC6D0\uD615/\uAC01\uC9C4 `Frame` + `UICorner` + `UIStroke` \uB85C \uAC10\uC2FC\uB2E4. Currency, stat, tab, rarity marker \uC5D0 \uC801\uD569\uD558\uB2E4.\n- **Primitive pictogram** \u2014 \uC791\uC740 `Frame` \uC5EC\uB7EC \uAC1C\uB85C \uCE7C\uB0A0/\uBC29\uD328/\uBCF4\uC11D/\uD654\uC0B4\uD45C \uAC19\uC740 \uB2E8\uC21C \uC2E4\uB8E8\uC5E3\uC744 \uB9CC\uB4E0\uB2E4. \uAC19\uC740 \uB450\uAED8\uC640 \uC0C9\uC0C1 family \uB97C \uC720\uC9C0\uD55C\uB2E4.\n- **Framed item placeholder** \u2014 \uCE74\uB4DC 50\u201370% \uB97C \uCC28\uC9C0\uD558\uB294 \uD070 badge \uC601\uC5ED\uC744 \uB9CC\uB4E4\uACE0, \uB0B4\uBD80\uC5D0 glyph/shape + rarity border + \uC9E7\uC740 item label \uC744 \uB454\uB2E4. \uC2E4\uC81C asset \uC774 \uC5C6\uC744 \uB54C inventory/shop \uC774 \uD14D\uC2A4\uD2B8 \uD45C\uCC98\uB7FC \uBCF4\uC774\uB294 \uAC83\uC744 \uB9C9\uB294\uB2E4.\n- **Diegetic label plate** \u2014 \uC885\uC774 \uC870\uAC01, \uAE08\uC18D \uBA85\uD328, \uD640\uB85C\uADF8\uB7A8 \uCE69 \uAC19\uC740 \uD45C\uBA74\uC744 `Frame`, `UIGradient`, `UIStroke` \uB85C \uAD6C\uC131\uD558\uACE0 \uD14D\uC2A4\uD2B8\uB97C \uADF8 \uC704\uC5D0 \uC62C\uB9B0\uB2E4.\n\n\uC2E4\uC81C asset id \uB97C \uBAA8\uB974\uBA74 `rbxassetid://...` \uB97C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uB294\uB2E4. \uB300\uC2E0 \uC704 recipe \uB85C placeholder \uB97C \uB9CC\uB4E4\uACE0, \uB098\uC911\uC5D0 `manage_ui.update` \uB85C `ImageLabel.Image` \uB97C \uAD50\uCCB4\uD560 \uC218 \uC788\uAC8C path/name \uC744 \uBA85\uD655\uD788 \uB454\uB2E4.\n\n### B.9 Ornamentation & depth\n\n\uAE4A\uC774 / \uC7A5\uC2DD\uC740 \uAC8C\uC784 UI \uC758 \uB450 \uBC88\uC9F8\uB85C \uD070 \uBCC0\uC218\uB2E4. \uC774 \uC139\uC158\uC740 zero ornament \uB3C4 \uC815\uB2F9\uD55C \uC120\uD0DD\uC9C0\uB85C \uB454\uB2E4.\n\n| Family | \uC2DC\uADF8\uB2C8\uCC98 | \uC801\uD569 |\n|---|---|---|\n| **Flat** | \uB2E8\uC77C fill, 0 stroke \uB610\uB294 1 px hairline | minimalist, modern, terminal \u2014 \uC801\uADF9\uC801 \uC120\uD0DD |\n| **Single stroke** | 1~2 px \uB2E8\uC0C9 \uC678\uACFD\uC120 | brutalist, editorial |\n| **Double stroke + bevel** | \uC678\uACFD \uC9C4\uD55C stroke + \uC548\uCABD \uBC1D\uC740 stroke + \uC0C1\uB2E8 light gradient | \uB9CC\uD654 / \uC544\uCF00\uC774\uB4DC / \uCE90\uC8FC\uC5BC \uAC8C\uC784 \uC2DC\uADF8\uB2C8\uCC98 |\n| **9-slice ribbon / banner** | `ScaleType = "Slice"` \uC774\uBBF8\uC9C0 \uD328\uB110, \uC591 \uB05D\uC774 \uAD6C\uBD80\uB7EC\uC9C4 \uB9AC\uBCF8 | RPG, \uD310\uD0C0\uC9C0, \uC774\uBCA4\uD2B8 \uD328\uB110 |\n| **Glow halo** | \uD328\uB110 \uB4A4 \uC0B4\uC9DD \uD070 \uAC00\uC0B0 glow \uD504\uB808\uC784 | \uBCF4\uC0C1, primary CTA, \uB808\uC804\uB354\uB9AC |\n| **Etched / engraved** | inner shadow \uD504\uB808\uC784 + \uBBF8\uC138 noise | \uD638\uB7EC, \uB2E4\uD06C \uD310\uD0C0\uC9C0 |\n| **Holographic / chrome** | \uB2E4\uC911 stop UIGradient + \uD68C\uC804 sweep | Y2K, \uC0AC\uC774\uBC84 |\n| **Stamped badge** | \uC6D0 / \uB2E4\uAC01\uD615, \uD68C\uC804 / offset, \uCE74\uB4DC \uCF54\uB108\uC5D0 \uBD80\uCC29 | \uC2E0\uC0C1 / \uC138\uC77C / \uC774\uBCA4\uD2B8 \uB77C\uBCA8 |\n\n\uADDC\uCE59:\n\n- \uD55C \uD654\uBA74\uC5D0 ornament family \uB294 **\uD558\uB098** \uB85C commit. \uB2E8 *\uC5ED\uD560\uBCC4* \uC0AC\uC6A9\uC740 \uC815\uB2F9 \u2014 \uBA54\uC778 \uD328\uB110\uC740 Double-stroke + bevel, \uCF54\uB108\uC5D0 Stamped badge 1 \uAC1C \uAC19\uC740 \uC870\uD569.\n- **Flat \uB3C4 \uC801\uADF9\uC801 \uC120\uD0DD**. \uBAA8\uB4E0 \uD654\uBA74\uC774 \uC7A5\uC2DD\uC801\uC774\uC5B4\uC57C \uD55C\uB2E4\uB294 \uB73B \uC544\uB2D8.\n- \uAE4A\uC774 \uD45C\uD604 \uCD94\uCC9C \uAE30\uBC95:\n - \uD328\uB110 \uB4A4 \uC5B4\uB450\uC6B4 offset frame (4~8 px) \u2014 \uB5A0 \uC788\uB294 \uC778\uC0C1.\n - UIGradient \uB97C fill \uC5D0 \uC801\uC6A9\uD574 \uC0C1\uB2E8 \uBC1D\uAC8C / \uD558\uB2E8 \uC5B4\uB461\uAC8C \u2014 \uB465\uAE00\uB824 \uBCF4\uC774\uB294 \uAE4A\uC774.\n - \uC774\uC911 UIStroke \uC0C9 \uBCC0\uD654 \u2014 \uC678\uACFD \uC5B4\uB461\uAC8C, \uB0B4\uACFD \uBC1D\uAC8C \u2192 \uBCA0\uBCA8 \uD6A8\uACFC.\n - \uD328\uB110 \uC548\uCABD 1 px \uBC1D\uC740 hairline \u2014 UIStroke (`ApplyStrokeMode = "Border"`) \uB610\uB294 \uC548\uCABD Frame.\n- 9-slice ribbon \uC740 `ImageLabel.ScaleType = "Slice"` + `SliceCenter = Rect.new(...)` \uB97C \uC0AC\uC6A9\uD574\uC57C \uC591 \uB05D\uC774 \uAE68\uC9C0\uC9C0 \uC54A\uB294\uB2E4.\n\n### B.10 Composition\n\n\xA7B.3 Layout \uC774 "\uC5B4\uB514\uC5D0 \uB450\uB294\uAC00" \uB77C\uBA74 Composition \uC740 "\uC5B4\uB5A4 \uBB34\uAC8C\uB85C \uB450\uB294\uAC00" \u2014 \uD55C \uB2E8\uACC4 \uBBF8\uC2DC\uC801\uC774\uACE0 layout \uACFC *\uC9C1\uAD50* \uD55C\uB2E4.\n\n| Family | \uC2DC\uADF8\uB2C8\uCC98 | \uC801\uD569 |\n|---|---|---|\n| **Single focal** | \uD654\uBA74\uC758 60% \uC774\uC0C1\uC774 \uD55C \uC694\uC18C(\uAC70\uB300 CTA, hero \uC77C\uB7EC\uC2A4\uD2B8, \uBCF4\uC0C1 \uCE74\uB4DC). \uB098\uBA38\uC9C0\uB294 \uBCF4\uC870 | \uC2A4\uD50C\uB798\uC2DC, \uC2B9\uB9AC \uD654\uBA74, primary CTA \uC6B0\uC138 |\n| **Triptych** | \uB3D9\uB4F1 \uBB34\uAC8C 3 \uBD84\uD560 | \uC77C\uC77C \uBCF4\uC0C1, \uBAA8\uB4DC \uC120\uD0DD |\n| **Edge-anchored chip row** | \uD654\uBA74 \uAC00\uC7A5\uC790\uB9AC(\uC0C1 / \uD558)\uC5D0 \uCE69 \uD55C \uC904. \uC911\uC559\uC740 \uAC8C\uC784\uD50C\uB808\uC774 | HUD |\n| **Asymmetric overlap** | \uD070 \uD328\uB110 + \uC704\uC5D0 \uC0B4\uC9DD \uACB9\uCE5C \uC791\uC740 \uCE74\uB4DC / \uBC30\uC9C0 | \uAE4A\uC774 \uC5F0\uCD9C \uBA54\uB274 |\n| **Stacked stack** | \uB3D9\uC77C \uBB34\uAC8C \uCE74\uB4DC \uC218\uC9C1 \uC2A4\uD0DD | \uC124\uC815, \uC18C\uC15C \uD53C\uB4DC \u2014 *\uD604\uC7AC \uB370\uBAA8\uAC00 \uBA38\uBB34\uB974\uB294 \uB514\uD3F4\uD2B8* |\n| **Header-with-banner** | \uD070 \uBC30\uB108 / \uB9AC\uBCF8\uC774 \uD328\uB110 \uC0C1\uB2E8\uC744 \uAC00\uB85C\uC9C0\uB984, \uD5E4\uB354\uC640 \uD328\uB110\uC774 \uACB9\uCE68 | RPG \uD018\uC2A4\uD2B8 \uD5E4\uB354, \uC774\uBCA4\uD2B8 |\n\n\uADDC\uCE59:\n\n- composition family \uC640 \xA7B.3 layout family \uB294 **\uC9C1\uAD50**. \uAC19\uC740 layout(\uC608: stacked card flow) \uC774\uB77C\uB3C4 composition \uC740 single-focal \uC77C \uC218\uB3C4 triptych \uC77C \uC218\uB3C4 \uC788\uC74C.\n- **"Stacked stack" \uC73C\uB85C \uD68C\uADC0\uD558\uC9C0 \uB9D0 \uAC83.** \uC758\uB3C4\uD574\uC11C \uACE0\uB978 \uACB0\uACFC\uAC00 \uC544\uB2C8\uBA74 \uB2E4\uB978 family \uB85C \uB2E4\uC2DC \uC2DC\uC791. \uC774 family \uB294 *settings / feed* \uCC98\uB7FC \uB3D9\uB4F1 \uD56D\uBAA9\uC744 \uBCF4\uC5EC\uC918\uC57C \uD560 \uB54C\uB9CC \uC815\uB2F5.\n- Single focal \uCC44\uD0DD \uC2DC \uC8FC\uC5ED \uC694\uC18C\uB294 *\uC2DC\uAC01\uC801\uC73C\uB85C \uC555\uB3C4\uC801* \uC774\uC5B4\uC57C \uD55C\uB2E4 \u2014 \uD06C\uAE30\xB7\uC0C9\xB7\uC7A5\uC2DD \uC911 \uC801\uC5B4\uB3C4 \uB450 \uCD95\uC5D0\uC11C \uBCF4\uC870 \uC694\uC18C\uB97C \uB2A5\uAC00.\n\n---\n\n## \xA7C. Direction (brief \u2192 choices)\n\n### C.0 Reach for game quality\n\n\uD55C \uD654\uBA74\uC744 \uB9C8\uBB34\uB9AC\uD558\uAE30 \uC804\uC5D0, \uB2E4\uC74C \uB2A5\uB825 \uC911 *\uCD5C\uC18C \uD558\uB098* \uC5D0 \uC758\uB3C4\uB97C \uAC16\uACE0 \uB3C4\uB2EC\uD588\uB294\uC9C0 \uD655\uC778\uD55C\uB2E4. 4 \uAC1C \uBAA8\uB450 \uB07C\uC6CC \uB123\uC744 \uD544\uC694 \uC5C6\uC74C \u2014 \uB07C\uC6CC \uB123\uC73C\uBA74 noise.\n\n1. **\uC758\uB3C4\uB41C imagery \uACB0\uC815** (\xA7B.8). family \uD558\uB098\uB97C \uACE8\uB790\uB2E4. *No-imagery \uB3C4 \uC815\uB2F9\uD55C \uC120\uD0DD.*\n2. **\uAE4A\uC774 / \uC7A5\uC2DD family commit** (\xA7B.9). family \uD558\uB098\uC5D0 \uBA38\uBB3C\uB800\uB2E4. *Flat \uB3C4 \uC815\uB2F9\uD55C \uC120\uD0DD.*\n3. **Focal hierarchy** (\xA7B.10). \uD55C \uC694\uC18C\uAC00 \uC2DC\uAC01\uC801\uC73C\uB85C \uBA85\uD655\uD788 1 \uC21C\uC704. \uBAA8\uB4E0 \uBC84\uD2BC\uC774 \uAC19\uC740 \uBB34\uAC8C\uBA74 \uC2E4\uD328.\n4. **Signature moment.** \uC774 \uD654\uBA74\uC744 \uB2E4\uB978 \uD654\uBA74\uACFC \uAD6C\uBCC4 \uC9D3\uB294 \uD55C \uAC00\uC9C0 \uC2DC\uAC01 \uBAA8\uD2F0\uD504(\uD2B9\uC815 \uCF54\uB108 \uCC98\uB9AC, ribbon, glint, badge, \uC77C\uB7EC\uC2A4\uD2B8, *zero-ornament \uC758 \uC758\uB3C4\uB41C \uCE68\uBB35* \u2014 \uBB34\uC5C7\uC774\uB4E0).\n5. **Icon/asset fallback clarity.** \uC2E4\uC81C \uC774\uBBF8\uC9C0 asset \uC774 \uC5C6\uC73C\uBA74 glyph badge, primitive pictogram, framed placeholder \uC911 \uD558\uB098\uB97C \uBA85\uC2DC\uC801\uC73C\uB85C \uC0AC\uC6A9\uD588\uB2E4. asset id \uB97C \uC9C0\uC5B4\uB0B4\uC9C0 \uC54A\uC558\uB2E4.\n\nminimalist / brutalist / terminal \uACB0\uC5D0\uC11C\uB294 1\xB72 \uAC00 *\uC758\uB3C4\uB41C zero* \uC778 \uAC83\uC774 \uC2DC\uADF8\uB2C8\uCC98(4 \uBC88 \uCDA9\uC871)\uB2E4. \uB2E8, "default \uB85C 4 \uAC1C \uBAA8\uB450 0" \uC778 \uACB0\uACFC\uB294 \uAC70\uBD80 \uB300\uC0C1\uC774\uB2E4 \u2014 \uC758\uB3C4 \uC5C6\uC774 \uBE44\uC5B4 \uC788\uB294 \uAC83\uACFC \uC758\uB3C4\uB41C \uCE68\uBB35\uC740 \uB2E4\uB974\uB2E4.\n\n\uC774 \uAC8C\uC774\uD2B8\uB294 \xA7A Floor \uC640 \uB2E4\uB974\uB2E4. Floor \uB294 \uC811\uADFC\uC131 *\uBC14\uB2E5\uC120* \uC774\uACE0 Design Check \uAC00 \uC81C\uC548\uD55C\uB2E4. \uC5EC\uAE30 \xA7C.0 \uC740 *\uC0C1\uD55C* \uC774 \uC544\uB2C8\uB77C *\uBC29\uD5A5* \u2014 \uBAA8\uB378\uC774 \uC2A4\uC2A4\uB85C confirm \uD558\uB294 \uC790\uAE30-\uAC80\uC99D \uB2E8\uACC4\uB2E4.\n\n### C.1 Reading the brief\n\n`design_brief` carries the signals that drive vocabulary selection. Treat them as constraints that *eliminate* options, not prescriptions for a single answer.\n\n| Brief field | Use as |\n|---|---|\n| `purpose` | Layout family hint (HUD \u2192 edge-bound; modal \u2192 centered hierarchy; form \u2192 stacked cards or split panel). |\n| `platform` | Floor enforcement (touch sizes, safe zones) and density default (mobile \u2192 standard or compact; console \u2192 generous). |\n| `tone` | Primary palette + corner + motion family. The single strongest signal. |\n| `reference_assets` | Override everything else when they imply a clear style. Reference > tone. |\n| `resolution_targets` | Safe-zone strictness; doesn\'t drive style. |\n| `color_palette.source` | If `tokens`, scope to the named family. If `custom`, infer mood from the colours and document it. |\n| `accessibility` | Tighten Floor numbers if the brief specifies values above the \xA7A defaults. |\n\n### C.2 Worked contrasts\n\nSame brief, multiple valid directions. The point: there is no single right answer.\n\n**Brief A:** `purpose: menu, platform: all, tone: dark, reference_assets: []`\n\n- *Direction A1 \u2014 Modern dark:* `modern_dark` palette \xB7 geometric sans \xB7 stacked cards \xB7 soft elevation \xB7 8 px corners \xB7 functional ease.\n- *Direction A2 \u2014 Brutalist:* `brutalist_concrete` palette \xB7 monospace \xB7 asymmetric grid \xB7 brutalist raw surfaces \xB7 0 px corners \xB7 no motion.\n- *Direction A3 \u2014 Cinematic:* custom near-black palette with single accent \xB7 editorial serif \xB7 layered depth \xB7 cinematic glide motion \xB7 2 px gold hairlines.\n\nAll three satisfy the brief. Pick one and commit.\n\n**Brief B:** `purpose: HUD, platform: console, tone: retro, reference_assets: [pixel-art-screenshot]`\n\n- Reference asset overrides `tone: retro` toward 8/16-bit specifically: `arcade_pop` palette \xB7 pixel font \xB7 edge-bound HUD \xB7 flat surfaces \xB7 0 px corners \xB7 mechanical-snap motion \xB7 console-density spacing.\n\n### C.3 Anti-homogenization rules\n\nThese are the *most important* rules in this guide. Default AI output converges on a recognizable, generic aesthetic. Reject it.\n\n- **Reject the default AI look.** If the output has all of: 8 px corner radius, generic blue accent (#3B82F6 family), centered hero on glassmorphic card, geometric sans body text \u2014 restart with a different vocabulary stack.\n- **Each game deserves a visual identity that could not be mistaken for another.** Two different briefs producing visually similar outputs is a failure, even if both are technically correct.\n- **Reference assets dominate.** When the brief includes references, mirror their aesthetic before falling back to defaults.\n- **Use divergent vocabulary in your own reasoning.** Prefer "consider one of \u2026" or "one of: A | B | C" over "the recommended approach is \u2026.\n- **One direction per surface.** A single screen commits to one palette family, one corner language, one typography family, one motion family. Variety lives across screens, not within one.\n- **`Gotham` is a default, not a recommendation.** If you reach for it without thought, you have skipped \xA7B.2.\n- **Reject the Roblox AI slab look.** \uCD9C\uB825\uC774 \uB2E4\uC74C\uC744 \uBAA8\uB450 \uAC00\uC9C0\uBA74 \u2014 \uC9C1\uC0AC\uAC01\uD615 \uCE74\uB4DC 2~3 \uC7A5\uC774 \uB3D9\uC77C \uBB34\uAC8C\uB85C \uB098\uC5F4, ImageLabel 0 \uAC1C, \uBAA8\uB4E0 \uBC84\uD2BC\uC774 \uAC19\uC740 \uC2DC\uAC01 \uBB34\uAC8C, ornament family \uBBF8\uC120\uD0DD, \uD14D\uC2A4\uD2B8 100% \u2014 \uC989\uC2DC \uB2E4\uC2DC \uC2DC\uC791\uD55C\uB2E4. \uD314\uB808\uD2B8\uB9CC \uBC14\uAFBC \uC2AC\uB798\uBE0C\uB294 \uB2E4\uB978 \uAC8C\uC784\uC774 \uC544\uB2C8\uB2E4. \uAC19\uC740 \uAC00\uC774\uB4DC\uB97C \uB530\uB978 \uB450 \uCD9C\uB825\uC774 "\uD314\uB808\uD2B8 swap \uC73C\uB85C \uBCF4\uC77C \uC218 \uC788\uB294" \uACB0\uACFC\uB294 \uC2E4\uD328\uB2E4.\n\n---\n\n## \xA7D. Tree encoding\n\n`create_tree` and `update.changes.properties` accept Roblox property values in a constrained JSON shape. The plugin-side `TypeConverter` resolves these:\n\n| Roblox type | JSON shape | Notes |\n|---|---|---|\n| `UDim2` | `{xScale, xOffset, yScale, yOffset}` | Use this for `Size` and `Position`. Do not use array form. |\n| `UDim` | `{scale, offset}` | Used by padding, corner radius, and similar properties. |\n| `Color3` | `{r, g, b}` | Channels may be 0-1 or 0-255; keep one style within a tree. |\n| `Vector2` | `{x, y}` | |\n| `Vector3` | `{x, y, z}` | |\n| `NumberSequence` | `number` or `[{time, value, envelope?}, ...]` | Single number becomes a constant sequence. |\n| `ColorSequence` | `[{time, value: {r, g, b}}, ...]` | |\n| `Font` | string enum name or `{family, weight, style}` | String enum names are simplest for built-in fonts. |\n| Enum properties | string item name | Example: `"Left"` for `TextXAlignment`, `"Vertical"` for `FillDirection`. |\n\n**Multiline text.** Roblox \uD14D\uC2A4\uD2B8 \uC5D4\uC9C4\uC740 `\\n` *literal*(\uBC31\uC2AC\uB798\uC2DC + n \uB450 \uAE00\uC790)\uC744 escape \uCC98\uB9AC\uD558\uC9C0 \uC54A\uB294\uB2E4 \u2014 \uD654\uBA74\uC5D0 \uADF8\uB300\uB85C `\\n` \uC774 \uBCF4\uC778\uB2E4. multiline TextLabel \uC740 \uB450 \uAC00\uC9C0 \uBC29\uBC95 \uC911 \uD558\uB098\uB85C \uC778\uCF54\uB529\uD55C\uB2E4:\n\n1. **JSON newline escape \uC0AC\uC6A9** \u2014 JSON \uD45C\uC900\uC5D0 \uB530\uB77C `"line1\\nline2"` \uCC98\uB7FC \uC791\uC131\uD55C\uB2E4. JSON \uD30C\uC11C\uAC00 \uC774\uB97C \uC2E4\uC81C \uC904\uBC14\uAFC8 \uBC14\uC774\uD2B8\uB85C \uB514\uCF54\uB529\uD558\uBBC0\uB85C Text \uC18D\uC131\uC5D0 \uADF8\uB300\uB85C \uB4E4\uC5B4\uAC00\uB3C4 \uC815\uC0C1 \uB80C\uB354\uB41C\uB2E4. \uC774 \uACBD\uC6B0 `TextWrapped = true` \uB3C4 \uAC19\uC774 \uC124\uC815\uD55C\uB2E4. (JSON \uD398\uC774\uB85C\uB4DC\uB97C \uC9C1\uC811 \uC791\uC131\uD560 \uB54C `\\\\n` \uC73C\uB85C \uB450 \uBC88 escape \uD558\uC9C0 \uC54A\uB3C4\uB85D \uC8FC\uC758 \u2014 \uADF8\uB7EC\uBA74 literal \uC774 \uB41C\uB2E4.)\n2. **\uC904\uB9C8\uB2E4 \uBCC4 TextLabel + UIListLayout** \u2014 \uB354 \uC548\uC804\uD55C \uBC29\uBC95. \uAC01 \uC904\uC744 \uBCC4 `TextLabel` \uB85C \uB9CC\uB4E4\uACE0 \uBD80\uBAA8 Frame \uC5D0 `UIListLayout` \uC744 \uB454\uB2E4. \uC904 \uAC04\uACA9 / \uC815\uB82C / \uC0C9\uC744 \uC904\uB9C8\uB2E4 \uB2E4\uB974\uAC8C \uC904 \uC218 \uC788\uB2E4\uB294 \uBCF4\uB108\uC2A4\uAC00 \uC788\uB2E4.\n\n\uB370\uBAA8 UI \uC5D0\uC11C "Reward\\\\n4,800 CR" \uCC98\uB7FC \uBCF4\uC774\uB294 \uACB0\uACFC\uB294 1 \uBC88\uC744 \uC2DC\uB3C4\uD558\uB2E4 \uB450 \uBC88 escape \uD55C \uCF00\uC774\uC2A4\uB2E4. \uD2B8\uB9AC \uC815\uC758\uC5D0\uC11C \uC77C\uBD80\uB7EC \uBC31\uC2AC\uB798\uC2DC\uB97C \uD45C\uC2DC\uD560 \uC758\uB3C4\uAC00 \uC544\uB2C8\uB77C\uBA74, multiline \uC740 \uC704 \uB450 \uD328\uD134 \uC911 \uD558\uB098\uB85C \uC791\uC131\uD55C\uB2E4.\n\nCommon pitfalls:\n\n- `Size` and `Position` should always use the UDim2 object form.\n- `BackgroundColor3` accepts 0-1 and 0-255 values; avoid mixing the two styles in one tree.\n- For an in-game transparent root (\xA7A.6), set `BackgroundTransparency = 1` on the first `Frame` under `ScreenGui`.\n\n---\n\n*This document is the runtime resource source under `tool-codegen/resources/ui-studio/`. After editing, run `npm run codegen` to refresh the embedded constant in `mcp-server/src/resources/design-guide.generated.ts`.*\n',CA=`{
|
|
134
134
|
"$schema": "./design-tokens.schema.json",
|
|
@@ -501,7 +501,7 @@ data: ${JSON.stringify(n)}
|
|
|
501
501
|
`)}catch(i){g.warn("Failed to flush change log",{error:i instanceof Error?i.message:String(i),entries:e.length})}}startChangeLogFlusher(){this.flushTimer||(this.flushTimer=setInterval(()=>{this.flushChangeLog(),this.flushHistory()},Sce),this.flushTimer&&typeof this.flushTimer=="object"&&"unref"in this.flushTimer&&this.flushTimer.unref())}async stopChangeLogFlusher(){this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),await this.flushChangeLog(),await this.flushHistory()}async updateSyncIndex(e,n){this.index.updateBothHashes(e,n)}relativePath(e){return dt.relative(this.config.getSyncRoot(),e)}extractServiceName(e){return Dt(e)[0]??""}findTreeNode(e,n){let i=Dt(n);if(i.length<=1)return null;let r=e.children,a;for(let o=1;o<i.length;o++){if(!r||(a=r.find(s=>s.name===i[o]),!a))return null;r=a.children}return a??null}detachTreeNode(e,n){let i=this.getParentPath(n),r=this.getLeafName(n);if(!i||!r)return null;let a=this.findTreeNode(e,i),o=a?.children??e.children,s=o.findIndex(l=>l.name===r);if(s<0)return null;let[c]=o.splice(s,1);return a?a.childCount=o.length:e.childCount=o.length,c??null}insertTreeNode(e,n,i){let r=this.findTreeNode(e,n);if(r){r.children||(r.children=[]);let o=r.children.findIndex(s=>s.name===i.name);o>=0?r.children[o]=i:r.children.push(i),r.childCount=r.children.length;return}let a=e.children.findIndex(o=>o.name===i.name);a>=0?e.children[a]=i:e.children.push(i),e.childCount=e.children.length}getLeafName(e){let n=Dt(e);return n.length===0?null:n[n.length-1]??null}getParentPath(e){let n=Dt(e);return n.length<=1?null:`game.${n.slice(0,-1).join(".")}`}async copyDirectory(e,n){await this.ensureDir(n);let i=await Ye.readdir(e,{withFileTypes:!0});for(let r of i){let a=dt.join(e,r.name),o=dt.join(n,r.name);r.isDirectory()?await this.copyDirectory(a,o):await Ye.copyFile(a,o)}}async tryRename(e,n){try{return this.onWriteCallback?.(e),this.onWriteCallback?.(n),await Ye.rename(e,n),!0}catch{return!1}}};function lP(t,e){if(t===e)return!0;if(t===null||e===null||typeof t!=typeof e||typeof t!="object")return!1;if(Array.isArray(t))return!Array.isArray(e)||t.length!==e.length?!1:t.every((o,s)=>lP(o,e[s]));let n=t,i=e,r=Object.keys(n),a=Object.keys(i);return r.length!==a.length?!1:r.every(o=>Object.hasOwn(i,o)&&lP(n[o],i[o]))}function kce(t,e){let n={},i=new Set([...Object.keys(t??{}),...Object.keys(e)]);for(let r of i){let a=t?.[r]??null,o=e[r]??null;lP(a,o)||(n[r]={before:a,after:o})}return n}import qe from"path";import{promises as co}from"fs";ie();var vv=class{index;placeRoot;constructor(e,n,i){this.index=n,this.placeRoot=i??e.getSyncRoot()}async buildReverseChanges(){let{modified:e,deleted:n}=await this.index.scanForFileModifications(),i=[];for(let a of e){if(this.isForwardOnlyPath(a.fsPath)){this.logForwardOnlySkip(a.fsPath,"buildReverseChanges:modified");continue}try{let o;a.fileType==="script"?o=await this.readModifiedScript(a.fsPath,a.instancePath):a.fileType==="value"?o=await this.readModifiedValue(a.fsPath,a.instancePath):o=await this.readModifiedProps(a.fsPath,a.instancePath),i.push(o)}catch(o){g.warn("Failed to read modified file for reverse sync",{path:a.fsPath,error:o instanceof Error?o.message:String(o)})}}let r=new Set;for(let a of n){if(this.isForwardOnlyPath(a.fsPath)){this.logForwardOnlySkip(a.fsPath,"buildReverseChanges:deleted");continue}if(r.has(a.instancePath))continue;r.add(a.instancePath);let o=qe.relative(this.placeRoot,a.fsPath);i.push({type:"instanceRemoved",instancePath:a.instancePath,fsPath:o,fileType:a.fileType,content:null})}return i}async buildChangesFromPending(e){let n=[],i=new Set,r=new Set,a=new Set;for(let o of e)if(o.type==="add"||o.type==="change"){let s=qe.dirname(o.fsPath),c=this.getFileType(o.fsPath);(c==="props"||c==="script"||c==="value")&&a.add(s)}for(let o of e)try{if(this.isForwardOnlyPath(o.fsPath)){this.logForwardOnlySkip(o.fsPath,`buildChangesFromPending:${o.type}`);continue}if(o.type==="addDir"||o.type==="unlinkDir"){let d=this.resolveInstancePathFromDirectory(o.fsPath);if(!d)continue;let f=this.getSiblingIndexForDirectory(o.fsPath),m=this.buildPendingDedupeKey(d,f);if(o.type==="unlinkDir"){if(i.has(m)||(i.add(m),this.isServiceInstance(d)))continue;let _=qe.relative(this.placeRoot,o.fsPath),S={type:"instanceRemoved",instancePath:d,fsPath:_,fileType:"directory",content:null};f&&(S.siblingIndex=f),n.push(S);continue}if(r.has(m)||this.isServiceInstance(d)||a.has(o.fsPath)||!await this.isContainerDirectory(o.fsPath))continue;let y=qe.relative(this.placeRoot,o.fsPath),v={type:"containerCreated",instancePath:d,fsPath:y,fileType:"directory",content:null,className:"Folder",parentPath:this.extractParentPath(d)};f&&(v.siblingIndex=f),r.add(m),n.push(v);continue}let s=this.resolveInstancePathFromFile(o.fsPath);if(!s)continue;let c=this.getSiblingIndexForFile(o.fsPath);if(o.type==="unlink"){let d=this.buildPendingDedupeKey(s,c);if(i.has(d)||(i.add(d),this.isServiceInstance(s)))continue;let f=qe.relative(this.placeRoot,o.fsPath),m=this.getFileType(o.fsPath);if(m!=="script"&&m!=="props"&&m!=="value")continue;let h={type:"instanceRemoved",instancePath:s,fsPath:f,fileType:m,content:null};c&&(h.siblingIndex=c),n.push(h);continue}let l=this.getFileType(o.fsPath),u=!this.index.hasEntry(o.fsPath)&&!this.index.hasInstancePath(s),p;if(u&&o.type==="add"){if(this.isServiceInstance(s))continue;if(l==="script")p=await this.readNewScript(o.fsPath,s);else if(l==="value")p=await this.readNewValue(o.fsPath,s);else if(l==="props")p=await this.readNewInstance(o.fsPath,s);else continue}else if(l==="script")p=await this.readModifiedScript(o.fsPath,s);else if(l==="value")p=await this.readModifiedValue(o.fsPath,s);else if(l==="props")p=await this.readModifiedProps(o.fsPath,s);else continue;c&&(p.siblingIndex=c),n.push(p)}catch(s){g.warn("Failed to build change from pending entry",{path:o.fsPath,type:o.type,error:s instanceof Error?s.message:String(s)})}return n}resolveInstancePathFromFile(e){return this.index.resolveInstancePathFromFsPath(e)}getSiblingIndexForFile(e){let n=this.index.getExplorerRoot();if(qe.basename(e)==="_tree.json")return;let r=qe.relative(n,e);if(r.startsWith(".."))return;let a=r.split(qe.sep).filter(p=>p.length>0);if(a.length<2)return;let o=a[a.length-2],s=qe.dirname(e),c=qe.dirname(s),l=this.index.getOriginalInstance(c,o);return l?l.siblingIndex:Qr(o).siblingIndex}isForwardOnlyPath(e){return this.index.isPathUnderClass(e,uc)}logForwardOnlySkip(e,n){g.info("Skipped reverse change for forward-only path",{source:n,path:qe.relative(this.placeRoot,e),className:this.index.getClassNameForFsPath(e)??"unknown"})}getFileType(e){return this.index.getFileTypeFromPath(e)}resolveInstancePathFromDirectory(e){let n=this.index.getExplorerRoot(),i=qe.relative(n,e);if(i.startsWith("..")||i===""||qe.isAbsolute(i))return null;let r=i.split(qe.sep).filter(s=>s.length>0);if(r.length===0)return null;let a=["game"],o=n;for(let s of r){o=qe.join(o,s);let c=qe.dirname(o);a.push(this.index.getOriginalNameForDir(c,s))}return ot(a)}getSiblingIndexForDirectory(e){let n=qe.basename(e),i=qe.dirname(e),r=this.index.getOriginalInstance(i,n);return r?r.siblingIndex:Qr(n).siblingIndex}async isContainerDirectory(e){try{let n=await co.readdir(e,{withFileTypes:!0});for(let i of n)if(i.isFile()&&nv.some(r=>i.name.endsWith(r)))return!1;return!0}catch(n){if(n.code==="ENOENT")return!1;throw n}}buildPendingDedupeKey(e,n){return n?`${e}#${n}`:e}async readModifiedScript(e,n){let i=await co.readFile(e,"utf-8"),r=qe.relative(this.placeRoot,e);return{type:"scriptSource",instancePath:n,fsPath:r,fileType:"script",content:i}}async readModifiedProps(e,n){let i=await co.readFile(e,"utf-8"),r=JSON.parse(i),a=qe.relative(this.placeRoot,e);return{type:"properties",instancePath:n,fsPath:a,fileType:"props",content:r}}async readModifiedValue(e,n){let i=await co.readFile(e,"utf-8"),r=JSON.parse(i),a=qe.relative(this.placeRoot,e);return{type:"valueChanged",instancePath:n,fsPath:a,fileType:"value",content:r}}async readNewScript(e,n){let i=await co.readFile(e,"utf-8"),r=qe.relative(this.placeRoot,e),a=qe.basename(e),o="ModuleScript";a.endsWith(".server.luau")?o="Script":a.endsWith(".client.luau")&&(o="LocalScript");let s=this.extractParentPath(n);return{type:"scriptCreated",instancePath:n,fsPath:r,fileType:"script",content:i,className:o,parentPath:s}}async readNewValue(e,n){let i=await co.readFile(e,"utf-8"),r=JSON.parse(i),a=qe.relative(this.placeRoot,e),o=this.extractParentPath(n);return{type:"valueCreated",instancePath:n,fsPath:a,fileType:"value",content:r,className:r.className,parentPath:o}}async readNewInstance(e,n){let i=await co.readFile(e,"utf-8"),r=JSON.parse(i),a=qe.relative(this.placeRoot,e),o=this.extractParentPath(n);return{type:"instanceCreated",instancePath:n,fsPath:a,fileType:"props",content:r,className:r.className,parentPath:o}}extractParentPath(e){return Ht(e)}isServiceInstance(e){let n=ti(e);return n.length!==2||n[0]!=="game"?!1:MI.has(n[1])}};ie();import Ece from"path";ie();import Qi from"path";import{createHash as J6}from"crypto";import{promises as pP}from"fs";var Cce=500,uP=class{explorerRoot;syncIndex;scanIntervalMs;previousSnapshot=new Map;pendingChanges=new Map;scanTimer=null;readyResolvers=[];isReady=!1;isPaused=!1;isScanning=!1;refreshSnapshotOnNextScan=!1;scanEpoch=0;pendingSequence=0;suppressedPaths=new Map;directionChecker=null;onForwardViolation=null;getPendingKey(e,n){return`${Qi.resolve(e)}\0${n}`}getPendingSlot(e){return e==="addDir"||e==="unlinkDir"?"dir":"file"}getPendingRank(e){return e==="change"?1:2}isStaleScan(e){return this.isPaused||e!==this.scanEpoch}resolveReadyWaiters(){for(let e of this.readyResolvers)e();this.readyResolvers=[]}queuePendingChange(e){let n=this.getPendingSlot(e.type),i=this.getPendingKey(e.fsPath,n),r=this.getPendingRank(e.type),a=this.pendingChanges.get(i);a&&a.rank>r||this.pendingChanges.set(i,{change:e,rank:r,sequence:++this.pendingSequence})}constructor(e,n,i=Cce){this.explorerRoot=Qi.resolve(e),this.syncIndex=n,this.scanIntervalMs=i}setDirectionChecker(e){this.directionChecker=e}setOnForwardViolation(e){this.onForwardViolation=e}suppressPath(e,n){let i=Qi.resolve(e);if(n===void 0){this.suppressedPaths.set(i,{mode:"generic"});return}if(n==="missing"){this.suppressedPaths.set(i,{mode:"missing"});return}this.suppressedPaths.set(i,{mode:"hash",hash:n})}async start(){this.scanTimer&&await this.stop("restart"),g.info("SnapshotChangeScanner: starting",{explorerRoot:this.explorerRoot,scanIntervalMs:this.scanIntervalMs}),this.isPaused=!1,this.refreshSnapshotOnNextScan=!1,this.suppressedPaths.clear(),this.scanEpoch+=1;let e=this.scanEpoch;this.previousSnapshot=await this.syncIndex.buildSnapshotSeed(),!this.isStaleScan(e)&&(this.isReady=!0,this.resolveReadyWaiters(),this.scanTimer=setInterval(()=>{this.scanSnapshotDiffs().catch(n=>{g.warn("SnapshotChangeScanner: background scan failed",{error:n instanceof Error?n.message:String(n)})})},this.scanIntervalMs),typeof this.scanTimer=="object"&&this.scanTimer&&"unref"in this.scanTimer&&this.scanTimer.unref())}async stop(e){g.info("SnapshotChangeScanner: stopping",{reason:e??"unspecified",explorerRoot:this.explorerRoot,isReady:this.isReady,isPaused:this.isPaused,pendingChanges:this.pendingChanges.size}),this.scanTimer&&(clearInterval(this.scanTimer),this.scanTimer=null),this.scanEpoch+=1,this.clearTransientState(),this.previousSnapshot=new Map,this.refreshSnapshotOnNextScan=!1,this.isPaused=!1,this.suppressedPaths.clear(),this.resolveReadyWaiters(),this.isReady=!1,this.isScanning=!1}beginFullSyncReplacement(){this.scanEpoch+=1,this.isPaused=!0,this.isReady=!1,this.refreshSnapshotOnNextScan=!1,this.suppressedPaths.clear(),this.clearTransientState()}async endFullSyncReplacement(){if(this.clearTransientState(),this.scanTimer===null){await this.start();return}let e=++this.scanEpoch;this.isReady=!1,this.finalizeResumedBaseline(e)}isActivelyScanning(){return this.scanTimer!==null&&!this.isPaused}injectPending(e){this.isPaused||(this.queuePendingChange(e),g.debug("SnapshotChangeScanner: injected pending change",{relativePath:e.relativePath,type:e.type}))}drainPendingChanges(){let e=Array.from(this.pendingChanges.values()).sort((n,i)=>n.sequence-i.sequence).map(n=>n.change);return this.pendingChanges.clear(),e}getPendingCount(){return this.pendingChanges.size}getLastDetected(){let e=null;for(let n of this.pendingChanges.values()){let i=n.change;(!e||i.detectedAt>e)&&(e=i.detectedAt)}return e}waitUntilReady(){return this.isReady?Promise.resolve():new Promise(e=>{this.readyResolvers.push(e)})}async rescan(){let e=await this.scanSnapshotDiffs(),n=await this.scanStoredModifications();return e+n}async scanSnapshotDiffs(){if(this.isPaused||this.isScanning)return 0;let e=this.scanEpoch;this.isScanning=!0;try{let n=await this.syncIndex.buildSnapshotSeed();if(this.isStaleScan(e))return 0;if(this.refreshSnapshotOnNextScan)return this.previousSnapshot=n,this.refreshSnapshotOnNextScan=!1,this.suppressedPaths.clear(),0;let i=Z6(this.previousSnapshot,n),r=await this.processCandidates(i,e);if(this.isStaleScan(e))return 0;let a=this.commitPendingChanges(r);return this.previousSnapshot=n,this.suppressedPaths.clear(),a>0&&g.info("SnapshotChangeScanner: scan found pending changes",{added:a}),a}finally{this.isScanning=!1}}async scanStoredModifications(){if(this.isPaused)return 0;let e=this.scanEpoch,n=await this.scanStoredModificationsInternal(e);if(!n||this.isStaleScan(e))return 0;for(let i of n.forwardViolations)this.onForwardViolation?.(i);return this.commitPendingChanges(n.pending)}async scanStoredModificationsInternal(e){let n=await this.syncIndex.getModifiedFiles(),i={pending:[],forwardViolations:[]};for(let r of n){if(e!==this.scanEpoch)return null;let a=r.fsPath,o=Qi.relative(this.explorerRoot,a);if(await this.isSuppressedPath(a))continue;if(this.shouldRedirectToForwardRestore(o,a)){i.forwardViolations.push(o);continue}let s;switch(r.changeType){case"modified":s="change";break;case"added":s="add";break;case"deleted":s="unlink";break;default:continue}i.pending.push({fsPath:a,relativePath:o,type:s,detectedAt:new Date().toISOString()})}return i}async processCandidates(e,n){let i=[];for(let r of e){if(this.isStaleScan(n))return[];let a=Qi.join(this.explorerRoot,r.relativePath),o=new Date().toISOString();if(await this.isSuppressedPath(a)||r.kind==="file"&&sP(r.relativePath))continue;if(this.shouldRedirectToForwardRestore(r.relativePath,a)){this.onForwardViolation?.(r.relativePath);continue}if(r.kind==="dir"){i.push({fsPath:a,relativePath:r.relativePath,type:r.changeType,detectedAt:o});continue}if(r.changeType==="unlink"){i.push({fsPath:a,relativePath:r.relativePath,type:"unlink",detectedAt:o});continue}let s=this.syncIndex.getStoredFileHash(a);try{let c=await pP.readFile(a,"utf-8"),l=J6("md5").update(c,"utf-8").digest("hex");if(s&&l===s){g.debug("SnapshotChangeScanner: echo skipped",{relativePath:r.relativePath,changeType:r.changeType});continue}i.push({fsPath:a,relativePath:r.relativePath,type:r.changeType,detectedAt:o})}catch(c){if(c.code==="ENOENT"){i.push({fsPath:a,relativePath:r.relativePath,type:"unlink",detectedAt:o});continue}g.warn("SnapshotChangeScanner: failed to read candidate file",{path:r.relativePath,changeType:r.changeType,error:c instanceof Error?c.message:String(c)})}}return i}commitPendingChanges(e){let n=0;for(let i of e){let r=this.getPendingSlot(i.type),a=this.getPendingKey(i.fsPath,r),o=this.getPendingRank(i.type),s=this.pendingChanges.get(a);s&&s.rank>o||(s||n++,this.pendingChanges.set(a,{change:i,rank:o,sequence:++this.pendingSequence}))}return n}clearTransientState(){this.pendingChanges.clear()}async finalizeResumedBaseline(e){try{let n=await this.syncIndex.buildSnapshotSeed();if(e!==this.scanEpoch)return;this.previousSnapshot=n,this.refreshSnapshotOnNextScan=!1;let i=await this.scanStoredModificationsInternal(e);if(e!==this.scanEpoch)return;if(i){for(let r of i.forwardViolations)this.onForwardViolation?.(r);this.commitPendingChanges(i.pending)}this.suppressedPaths.clear(),this.isPaused=!1,this.isReady=!0,this.resolveReadyWaiters()}catch(n){if(e!==this.scanEpoch)return;g.warn("SnapshotChangeScanner: resume baseline refresh failed",{error:n instanceof Error?n.message:String(n)}),this.refreshSnapshotOnNextScan=!0,this.isPaused=!1,this.isReady=!0,this.resolveReadyWaiters()}}shouldRedirectToForwardRestore(e,n){return this.syncIndex.isPathUnderClass(n,uc)?!0:this.directionChecker?this.directionChecker(e)==="forward":!1}async isSuppressedPath(e){let n=Qi.resolve(e);for(let[i,r]of this.suppressedPaths){if(n===i||n.startsWith(`${i}${Qi.sep}`))return r.mode==="generic"?!0:r.mode==="missing"?!await Ice(n):await Pce(n)===r.hash;if(i.startsWith(`${n}${Qi.sep}`))return r.mode==="generic"}return!1}};async function Ice(t){try{return await pP.access(t),!0}catch{return!1}}async function Pce(t){try{let e=await pP.readFile(t,"utf-8");return J6("md5").update(e,"utf-8").digest("hex")}catch{return null}}var K6=uP;var et="snapshot-scanner",bv=class extends K6{placeId;detectorExplorerRoot;lifecycleGeneration=0;constructor(e,n,i={}){super(e,n,i.scanIntervalMs),this.placeId=i.placeId??null,this.detectorExplorerRoot=Ece.resolve(e)}async start(){let e=++this.lifecycleGeneration;g.info("Reverse detector bootstrap scheduled",{placeId:this.placeId,mode:et,explorerRoot:this.detectorExplorerRoot}),await super.start(),e===this.lifecycleGeneration&&g.info("Reverse detector bootstrap completed",{placeId:this.placeId,mode:et})}async stop(e){this.lifecycleGeneration+=1,g.info("Reverse detector stopping",{placeId:this.placeId,mode:et,reason:e??"unspecified"}),await super.stop(e)}beginFullSyncReplacement(){g.info("Reverse detector paused for full sync replacement",{placeId:this.placeId,mode:et}),super.beginFullSyncReplacement()}async endFullSyncReplacement(){g.info("Reverse detector baseline refresh scheduled",{placeId:this.placeId,mode:et}),await super.endFullSyncReplacement()}async rescan(){g.debug("Reverse detector manual rescan requested",{placeId:this.placeId,mode:et});let e=await super.rescan();return g.info("Reverse detector manual rescan completed",{placeId:this.placeId,mode:et,added:e,pendingCount:this.getPendingCount()}),e}};import Ic from"path";import{promises as X6}from"fs";function lo(t,e,n,i,r=!1){if(i&&i>=1){let s=Ht(e);if(s){let c=t.getEffectiveDir(s,i);if(c){let l=Ot(e);if(n){let u=t.getResolvedName(e,n);if(u)l=u;else if(r)throw new Error(`Missing collision mapping for ${e}#${n}`)}return dP(t,c,l)}if(r)throw new Error(`Missing parent collision mapping for ${s}#${i}`)}}let a=t.getEffectiveDir(e,n??1);if(a){let s=t.getExplorerRoot(),c=Ic.relative(s,a).split(Ic.sep).filter(l=>l.length>0);return ot(["game",...c])}let o=e;if(n){let s=t.getResolvedName(e,n);if(s)o=Bp(o,s);else if(r)throw new Error(`Missing collision mapping for ${e}#${n}`)}return o}function Rce(t,e){let n=ti(t),i=ti(e);if(n.length<=i.length)return!1;for(let r=0;r<i.length;r++)if(n[r]!==i[r])return!1;return!0}function Y6(t,e){e=Tce(t,e);let n=new Map;for(let r=0;r<e.length;r++){let a=e[r];if(a?.type==="instanceRemoved")try{let o=lo(t.index,a.path,a.siblingIndex,a.parentSiblingIndex,!0);n.set(r,o)}catch{continue}}if(n.size<=1)return e;let i=new Set;for(let[r,a]of n)for(let[o,s]of n)if(r!==o&&Rce(a,s)){i.add(r);break}return i.size===0?e:e.filter((r,a)=>!i.has(a))}function Tce(t,e){let n=new Map;for(let a=0;a<e.length;a++){let o=e[a];if(o?.type!=="instanceRemoved"||o.siblingIndex===void 0)continue;let s=n.get(o.path);s||(s=[],n.set(o.path,s)),s.push(a)}let i=!1;for(let a of n.values()){if(a.length<2)continue;let o=new Set;for(let s of a){let c=e[s].siblingIndex;if(o.has(c)){i=!0;break}o.add(c)}if(i)break}if(!i)return e;let r=[...e];for(let[a,o]of n){if(o.length<2)continue;let s=uo(t.index,a,void 0,!1),c=new Set(t.index.getRegisteredSiblingIndices(a,s));if(c.size===0)continue;let l=new Set;for(let u of o){let p=e[u];if(p.type!=="instanceRemoved")continue;let d=p.siblingIndex;if(!l.has(d)&&c.has(d))l.add(d);else{let f;for(let m of c)if(!l.has(m)){f=m;break}f!==void 0&&(l.add(f),r[u]={...p,siblingIndex:f})}}}return r}function uo(t,e,n,i=!1){if(n&&n>=1){let r=Ht(e);if(r){let a=t.getEffectiveDir(r,n);if(a)return a;if(i)throw new Error(`Missing parent collision mapping for ${r}#${n}`)}}return t.resolveParentDir(e)}async function ea(t,e,n,i,r){let a=Ic.join(e,n),o=Ic.join(e,i);try{(await X6.stat(a)).isDirectory()&&(r&&(r.suppressPath(a),r.suppressPath(o)),await X6.rename(a,o),t.renameHashKeysUnder(a,o),t.renameReverseNameMappingsUnder(a,o),t.renameInstanceDirMappingsUnder(a,o))}catch{return}}function dP(t,e,n){let i=Ic.relative(t.getExplorerRoot(),e).split(Ic.sep).filter(r=>r.length>0);return ot(["game",...i,n])}async function xv(t,e){let n=t.index.planCollisionReindexAfterBatchRemovals(e.instancePath,[...e.removedSiblingIndices],e.parentDir);for(let i of n.renames)await ea(t.index,e.parentDir,i.from,i.to,t.fileWatcher),await Pc(t.writer,t.index,e.parentDir,i.from,i.to);t.index.applyCollisionReindexAfterBatchRemovals(e.instancePath,e.parentDir,n)}async function Pc(t,e,n,i,r){let a=dP(e,n,i),o=dP(e,n,r);await t.renameInTree(a,o,r)}import _v from"path";var Q6=5*60*1e3,eU=100;function fP(t){return t!==null&&typeof t=="object"&&!Array.isArray(t)}function Hp(t){return typeof t=="string"&&t.length>0}function Sv(t,e){let n=_v.resolve(t),i=_v.resolve(e),r=_v.relative(n,i);return r===""||!r.startsWith("..")&&!_v.isAbsolute(r)}function tU(t){if(!fP(t))return{success:!1,error:"Body must be an object"};let e=t.phase;if(e!=="start"&&e!=="chunk"&&e!=="complete")return{success:!1,error:`Invalid phase: ${String(e)}`};if(!Hp(t.clientId))return{success:!1,error:"clientId is required"};switch(e){case"start":return typeof t.placeId!="number"?{success:!1,error:"placeId must be a number"}:Hp(t.placeName)?typeof t.totalServices!="number"?{success:!1,error:"totalServices must be a number"}:typeof t.totalInstances!="number"?{success:!1,error:"totalInstances must be a number"}:t.previousPlaceId!==void 0&&typeof t.previousPlaceId!="number"?{success:!1,error:"previousPlaceId must be a number"}:{success:!0,data:t}:{success:!1,error:"placeName is required"};case"chunk":return Hp(t.serviceName)?Hp(t.serviceClassName)?Array.isArray(t.instances)?typeof t.chunkIndex!="number"?{success:!1,error:"chunkIndex must be a number"}:typeof t.totalChunks!="number"?{success:!1,error:"totalChunks must be a number"}:{success:!0,data:t}:{success:!1,error:"instances must be an array"}:{success:!1,error:"serviceClassName is required"}:{success:!1,error:"serviceName is required"};case"complete":return typeof t.instanceCount!="number"?{success:!1,error:"instanceCount must be a number"}:typeof t.scriptCount!="number"?{success:!1,error:"scriptCount must be a number"}:{success:!0,data:t}}}function nU(t){if(!fP(t))return{success:!1,error:"Body must be an object"};if(!Hp(t.clientId))return{success:!1,error:"clientId is required"};if(t.placeId!==void 0&&typeof t.placeId!="number")return{success:!1,error:"placeId must be a number"};if(typeof t.timestamp!="number")return{success:!1,error:"timestamp must be a number"};if(!Array.isArray(t.changes)||t.changes.length===0)return{success:!1,error:"changes must be a non-empty array"};let e=new Set(["propertyChanged","instanceAdded","instanceRemoved","instanceRenamed","instanceMoved","scriptSourceChanged"]);for(let n=0;n<t.changes.length;n++){let i=t.changes[n];if(!fP(i)||!e.has(i.type))return{success:!1,error:`changes[${n}]: invalid type '${String(i?.type)}'`}}return{success:!0,data:t}}import ir from"path";import{promises as po}from"fs";ie();var wv=class{constructor(e){this.ctx=e}async processChangeForPlace(e,n,i){let r=OL(n.type);if(e.directions[r]==="reverse")return g.info("Reverse enforcement: rejecting Studio change on reverse-only path",{placeId:e.placeId,changeType:n.type,dirKey:r,path:"path"in n?n.path:void 0}),(n.type==="scriptSourceChanged"||n.type==="propertyChanged")&&await this.injectLocalAsReverse(e,n),null;switch(n.type){case"propertyChanged":{let o=lo(e.index,n.path,n.siblingIndex,n.parentSiblingIndex,!0),s=await this.checkPropertyConflictForPlace(e,o,n);if(s)return s;let c=n;if(c.attributes!==void 0||c.tags!==void 0){let l=e.index.resolvePropsPath(o),u=await this.buildPropertyChangedPropsData(l,o,n);await e.writer.writeProps(o,u)}else c.properties&&typeof c.properties=="object"&&!Array.isArray(c.properties)?await e.writer.updateProperties(o,c.properties):n.property!==void 0?await e.writer.updateProperty(o,n.property,n.value):g.warn("propertyChanged: missing both property and properties",{path:o});return null}case"instanceAdded":{let o=lc.has(n.className),s=n.path,c=n.name;{let p=uo(e.index,n.path,n.parentSiblingIndex,!0),{resolved:d,retroactiveRename:f}=e.index.registerCollision(n.path,n.siblingIndex,p);f&&(await ea(e.index,p,f.from,f.to,e.fileWatcher),await Pc(e.writer,e.index,p,f.from,f.to));let m=e.index.sanitizeName(n.name);if(d!==m||p!==e.index.resolveParentDir(n.path)){let h=e.index.getExplorerRoot(),y=ir.relative(h,p).split(ir.sep).filter(v=>v.length>0);s=ot(["game",...y,d]),c=d}}await e.writer.writeInstance({path:s,name:n.name,className:n.className,properties:n.properties,attributes:n.attributes,tags:n.tags,scriptSource:n.scriptSource,hasChildren:!1,childCount:0,siblingIndex:n.siblingIndex}),this.setOriginalClassMapping(e,n.path,n.className,n.siblingIndex);let l={name:c,className:n.className,childCount:0,...c!==n.name?{originalName:n.name}:{}},u=Ht(s)||n.parentPath;return await e.writer.addToTree(u,l),e.instanceCount++,o&&n.scriptSource!==void 0&&e.scriptCount++,null}case"instanceRemoved":{let o=lo(e.index,n.path,n.siblingIndex,n.parentSiblingIndex,!0),s=uo(e.index,n.path,n.parentSiblingIndex,!0);g.info("Processing instanceRemoved",{path:o}),await e.writer.removeInstance(o),this.removeOriginalClassMapping(e,n.path,o);let c=Ot(o),l=Ht(o);if(l&&c&&await e.writer.removeFromTree(l,c),n.siblingIndex!==void 0){let p=`${s}\0${n.path}`,d=i.get(p);d||(d={instancePath:n.path,parentDir:s,removedSiblingIndices:new Set},i.set(p,d)),d.removedSiblingIndices.add(n.siblingIndex)}let u=Ht(o);if(u)try{let p=e.index.resolveChildrenDir(u);ir.dirname(p)!==e.index.getExplorerRoot()&&(await po.readdir(p)).length===0&&await po.rmdir(p)}catch{return null}return null}case"instanceRenamed":{let o=lo(e.index,n.oldPath,n.siblingIndex,n.parentSiblingIndex,!0),s=Ht(n.oldPath)===Ht(n.newPath),c=s?uo(e.index,n.oldPath,n.parentSiblingIndex,!0):null,l=uo(e.index,n.newPath,n.parentSiblingIndex,!0),{resolved:u,retroactiveRename:p}=e.index.registerCollision(n.newPath,n.siblingIndex,l);p&&(await ea(e.index,l,p.from,p.to,e.fileWatcher),await Pc(e.writer,e.index,l,p.from,p.to));let d=u!==e.index.sanitizeName(n.newName)?Bp(n.newPath,u):n.newPath;if(await e.writer.renameInstance(o,d,n.newName),this.renameOriginalClassMapping(e,n.oldPath,n.newPath),await e.writer.renameInTree(o,d,u,n.newName),s&&c&&n.siblingIndex!==void 0){let f=e.index.planCollisionReindexAfterRemoval(n.oldPath,n.siblingIndex,c);for(let m of f.renames)await ea(e.index,c,m.from,m.to,e.fileWatcher),await Pc(e.writer,e.index,c,m.from,m.to);e.index.applyCollisionReindexAfterRemoval(n.oldPath,c,f)}return null}case"instanceMoved":{let o=lo(e.index,n.oldPath,n.siblingIndex,n.parentSiblingIndex,!0),s=uo(e.index,n.newPath,n.parentSiblingIndex,!0),{resolved:c,retroactiveRename:l}=e.index.registerCollision(n.newPath,n.siblingIndex,s);l&&(await ea(e.index,s,l.from,l.to,e.fileWatcher),await Pc(e.writer,e.index,s,l.from,l.to));let u=Ot(n.newPath)||"",p=c!==e.index.sanitizeName(u)?Bp(n.newPath,c):n.newPath;return await e.writer.moveInstance(o,p),this.renameOriginalClassMapping(e,n.oldPath,n.newPath),await e.writer.moveInTree(o,p),null}case"scriptSourceChanged":{let o=lo(e.index,n.path,n.siblingIndex,n.parentSiblingIndex,!0),s=await this.checkScriptConflictForPlace(e,o,n.source);if(s)return s;let c=await this.resolveScriptClassForPlace(e,o);return await this.cleanupStaleScriptFilesForPlace(e,o,c),await e.writer.writeScript(o,c,n.source,!1),null}}}async injectLocalAsReverse(e,n){if(e.fileWatcher)try{if(n.type==="scriptSourceChanged"){let i=await this.resolveScriptClassForPlace(e,n.path),r=e.index.resolveScriptPath(n.path,i,!1),a=ir.join(this.ctx.config.getPlaceRoot(e.placeId),"explorer"),o=ir.relative(a,r);e.fileWatcher.injectPending({fsPath:r,relativePath:o,type:"change",detectedAt:new Date().toISOString()})}else if(n.type==="propertyChanged"){let i=e.index.resolvePropsPath(n.path),r=ir.join(this.ctx.config.getPlaceRoot(e.placeId),"explorer"),a=ir.relative(r,i);e.fileWatcher.injectPending({fsPath:i,relativePath:a,type:"change",detectedAt:new Date().toISOString()})}}catch(i){g.debug("injectLocalAsReverse: could not inject",{changeType:n.type,error:i instanceof Error?i.message:String(i)})}}async resolveScriptClassForPlace(e,n){try{let i=e.index.resolvePropsPath(n),r=await po.readFile(i,"utf-8"),a=JSON.parse(r);if(a.className&&lc.has(a.className))return a.className}catch{return"ModuleScript"}return"ModuleScript"}async cleanupStaleScriptFilesForPlace(e,n,i){let r=ev[i],a=ir.join(this.ctx.config.getPlaceRoot(e.placeId),"explorer");for(let[o,s]of Object.entries(ev)){if(s===r)continue;let c=e.index.resolveScriptPath(n,o,!1);try{await po.unlink(c),e.index.removeHash(c),g.info("Cleaned up stale script file",{placeId:e.placeId,removed:ir.relative(a,c),currentClassName:i})}catch(l){l.code!=="ENOENT"&&g.warn("Failed to clean up stale script file",{path:c,error:l instanceof Error?l.message:String(l)})}}}async checkPropertyConflictForPlace(e,n,i){try{let r=e.index.resolvePropsPath(n),a=e.index.getFileHash(r);if(!a)return null;let o;try{o=await po.readFile(r,"utf-8")}catch{return null}return e.index.computeHash(o)===a?null:{fsPath:ir.relative(this.ctx.config.getPlaceRoot(e.placeId),r),instancePath:n,fileType:"props",studioContent:await this.buildStudioPropsConflictContent(r,n,i),fileContent:o}}catch{return null}}async checkScriptConflictForPlace(e,n,i){try{let r=await this.resolveScriptClassForPlace(e,n),a=e.index.resolveScriptPath(n,r,!1),o=e.index.getFileHash(a);if(!o)return null;let s;try{s=await po.readFile(a,"utf-8")}catch{return null}return e.index.computeHash(s)===o?null:{fsPath:ir.relative(this.ctx.config.getPlaceRoot(e.placeId),a),instancePath:n,fileType:"script",studioContent:i,fileContent:s}}catch{return null}}async applyDeferredCollisionBatchReindexForPlace(e,n){await xv(e,n)}async buildStudioPropsConflictContent(e,n,i){let r=await this.buildPropertyChangedPropsData(e,n,i);return JSON.stringify(r,null,2)}async buildPropertyChangedPropsData(e,n,i){let r=null;try{let s=await po.readFile(e,"utf-8");r=JSON.parse(s)}catch{r=null}let a=i.properties?{...i.properties}:{...r?.properties??{}};i.property&&(a[i.property]=i.value);let o={name:r?.name??Ot(n)??"",className:i.className??r?.className??"Folder",properties:a};return i.attributes!==void 0?Object.keys(i.attributes).length>0&&(o.attributes={...i.attributes}):r?.attributes&&(o.attributes={...r.attributes}),i.tags!==void 0?i.tags.length>0&&(o.tags=[...i.tags]):r?.tags&&(o.tags=[...r.tags]),o}setOriginalClassMapping(e,n,i,r){e.index.setClassName(n,i,r)}removeOriginalClassMapping(e,n,i){n!==i&&e.index.removeClassMappingsUnderInstancePath(n)}renameOriginalClassMapping(e,n,i){n!==i&&e.index.renameClassMappingKeys(n,i)}};import rU from"path";import{promises as kv}from"fs";ie();var Cv=class{constructor(e){this.ctx=e}async handlePreCheck(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({hasPreviousSync:!1,changes:[],summary:{modified:0,deleted:0,added:0,total:0}});return}let r=this.ctx.config.getPlaceRoot(i),a=rU.join(r,".sync-index.json"),o=!1;try{await kv.access(a),o=!0}catch{o=!1}if(!o){n.status(200).json({hasPreviousSync:!1,changes:[],summary:{modified:0,deleted:0,added:0,total:0}});return}let s=rU.join(r,"explorer"),c=new Yi(r,s);await c.loadFromDisk();let l=await c.getModifiedFiles(),u=0,p=0,d=0;for(let m of l)switch(m.changeType){case"modified":u++;break;case"deleted":p++;break;case"added":d++;break}let f=l.map(m=>({relPath:m.relPath,instancePath:m.instancePath,changeType:m.changeType,fileType:m.fileType}));n.status(200).json({hasPreviousSync:!0,changes:f,summary:{modified:u,deleted:p,added:d,total:f.length}})}async handleSyncDirections(e,n){let i=e.body,r=i.placeId??this.ctx.getDefaultRuntimePlaceId();if(r==null){n.status(400).json({error:"Missing placeId"});return}let a=this.ctx.places.get(r);if(!a){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(i.directions&&typeof i.directions=="object"){let o=c=>c==="forward"||c==="reverse"||c==="bidirectional",s=i.directions;o(s.scripts)&&(a.directions.scripts=s.scripts),o(s.values)&&(a.directions.values=s.values),o(s.containers)&&(a.directions.containers=s.containers),o(s.data)&&(a.directions.data=s.data),o(s.services)&&(a.directions.services=s.services),g.info("Sync directions updated",{placeId:r,directions:a.directions})}if(i.applyModes&&typeof i.applyModes=="object"){let o=c=>c==="auto"||c==="manual",s=i.applyModes;o(s.scripts)&&(a.applyModes.scripts=s.scripts),o(s.values)&&(a.applyModes.values=s.values),o(s.containers)&&(a.applyModes.containers=s.containers),o(s.data)&&(a.applyModes.data=s.data),o(s.services)&&(a.applyModes.services=s.services)}this.ctx.touchRuntimePlace(r),n.status(200).json({status:"updated",directions:a.directions,applyModes:a.applyModes})}async handleForwardRestoreList(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({paths:[]});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}let a=r.forwardRestoreQueue.splice(0);this.ctx.touchRuntimePlace(i),g.info("Forward restore list drained",{placeId:i,count:a.length}),n.status(200).json({paths:a})}async handleSyncHistory(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({entries:[],total:0,hasMore:!1});return}let r=an(e.query.limit,50,1,200),a=an(e.query.offset,0,0),o=e.query.direction,s=e.query.type,c=this.ctx.places.get(i);c&&(await c.writer.flushHistory(),this.ctx.touchRuntimePlace(i));let l=this.ctx.config.getHistoryPath(i),p=(await uv(l,1e4)).reverse().filter(h=>!(o&&h.direction!==o||s&&h.type!==s)),d=p.length,m={entries:p.slice(a,a+r),total:d,hasMore:a+r<d};n.status(200).json(m)}getStatusSummary(){if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0)return{active:!0,placeId:this.ctx.activeFullSyncPlaceId};for(let[e,n]of this.ctx.places.entries())if(n.state==="syncing")return{active:!0,placeId:e};return{active:!1}}getDirectionForCategory(e){let n;if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&(n=this.ctx.places.get(this.ctx.activeFullSyncPlaceId)),!n){let r=this.ctx.getDefaultRuntimePlaceId();r!=null&&(n=this.ctx.places.get(r))}if(!n)return"forward";let i=e;return n.directions[i]??"forward"}getStatusDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...no},reverseDetectorActive:!1,reverseDetectorMode:et,fileWatcherActive:!1};let i=this.ctx.places.get(n);if(!i)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...no},reverseDetectorActive:!1,reverseDetectorMode:et,fileWatcherActive:!1};let r=i.fileWatcher?.getPendingCount()??0,a=i.fileWatcher?.isActivelyScanning()??!1,o=i.fileWatcher!==null;return{state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:i.activeClientId,reverseSyncAvailable:r>0,modifiedFileCount:r,applyModes:{...i.applyModes},reverseDetectorActive:a,reverseDetectorMode:et,fileWatcherActive:o}}async getHistoryDirect(e,n){let i=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(i==null||!Number.isFinite(i))return{entries:[],total:0,hasMore:!1};let r=this.ctx.places.get(i);r&&await r.writer.flushHistory();let a=Math.min(Math.max(n?.limit??50,1),200),o=Math.max(n?.offset??0,0),s=this.ctx.config.getHistoryPath(i),l=(await uv(s,1e4)).reverse(),u=l.length;return{entries:l.slice(o,o+a),total:u,hasMore:o+a<u}}getDirectionsDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{...to};let i=this.ctx.places.get(n);return i?{...i.directions}:{...to}}getProgressDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{state:"idle",isSyncing:!1};let i=this.ctx.places.get(n);if(!i)return{state:"idle",isSyncing:!1};if(!i.syncProgress)return{state:i.state,isSyncing:!1,lastSync:{instanceCount:i.instanceCount,scriptCount:i.scriptCount,completedAt:i.lastFullSync}};let r=i.syncProgress,a=Date.now()-r.syncStartTime,o=r.totalInstances>0?Math.min(100,Math.round(r.processedInstances/r.totalInstances*100)):0,s;if(r.processedInstances>0&&o<100){let l=a/r.processedInstances,u=r.totalInstances-r.processedInstances;s=Math.round(l*u)}let c=a>0?Math.round(r.bytesReceived/a*1e3):0;return{state:i.state,isSyncing:!0,progressPercent:o,currentService:r.currentService,currentChunk:{index:r.currentChunkIndex,total:r.currentTotalChunks},instances:{processed:r.processedInstances,total:r.totalInstances},services:{processed:r.processedServices,total:r.totalServices},elapsedMs:a,estimatedRemainingMs:s,bytesReceived:r.bytesReceived,bytesPerSecond:c}}async readSyncedFile(e,n){let i=parseInt(e,10),r=this.ctx.places.get(i);if(!r)throw new Error(`Place ${e} is not loaded in the sync cache. Start sync for this place before reading synced files.`);let a=r.index.resolvePropsPath(n);try{return{content:await kv.readFile(a,"utf-8"),path:a}}catch(s){if(s.code!=="ENOENT")throw s}for(let s of lc){let c=r.index.resolveScriptPath(n,s,!1);try{return{content:await kv.readFile(c,"utf-8"),path:c}}catch{continue}}let o=r.index.resolveValuePath(n);try{return{content:await kv.readFile(o,"utf-8"),path:o}}catch(s){if(s.code!=="ENOENT")throw s}throw new Error(`No synced file found for instance: ${n}. Ensure the instance has been synced to disk and that the canonical instance path is correct.`)}async writeSyncedFile(e,n,i){let r=parseInt(e,10),a=this.ctx.places.get(r);if(!a)throw new Error(`Place ${e} is not loaded in the sync cache. Start sync for this place before writing synced files.`);await a.writer.writeScript(n,"Script",i,!1)}async executeViaDisk(e,n){let i=this.ctx.getDefaultRuntimePlaceId();if(i==null)throw new Error("No active sync place for disk execution. Start sync for the current place before using reverse-sync disk execution.");let r=this.ctx.places.get(i);if(!r)throw new Error(`Place ${i} is not loaded in the sync cache. Restart sync before retrying disk execution.`);switch(e){case"manage_scripts_set_source":{let a=n.scriptType||n.className||"Script";return await r.writer.writeScript(n.path,a,n.source,!1),{success:!0,path:n.path}}case"manage_properties_set":return await r.writer.writeProps(n.path,{className:n.className||"Instance",name:Ot(n.path),properties:{[n.property]:n.value}}),{success:!0,path:n.path};default:throw new Error(`Disk execution is not supported for command: ${e}. This command must run through the plugin route.`)}}};import Re from"path";import{createHash as $ce,randomUUID as Dce}from"crypto";import{promises as hn}from"fs";ie();function Oce(t){return $ce("md5").update(t,"utf-8").digest("hex")}var Iv=class{constructor(e){this.ctx=e}preserveLocalFilesMap=new Map;pendingServiceTrees=new Map;async handleInitStart(e,n,i){if(n.previousPlaceId!==void 0&&n.previousPlaceId!==e&&(g.info("Place promotion detected",{from:n.previousPlaceId,to:e}),await this.ctx.config.promotePlaceRoot(n.previousPlaceId,e,n.placeName),this.ctx.places.get(n.previousPlaceId)&&this.ctx.places.delete(n.previousPlaceId),this.ctx.activeFullSyncPlaceId===n.previousPlaceId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(n.previousPlaceId)),this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&this.ctx.activeFullSyncPlaceId!==e){i.status(409).json({error:"Conflict",message:`Place ${this.ctx.activeFullSyncPlaceId} is currently syncing. Only one place can sync at a time.`});return}let r=await this.ctx.getOrCreatePlaceContext(e,n.placeName);if(r.activeClientId&&r.activeClientId!==n.clientId&&r.activeFullSyncSessionId!==null){i.status(409).json({error:"Conflict",message:`Another client (${r.activeClientId}) is currently syncing this place`});return}if(this.ctx.activeFullSyncPlaceId=e,this.ctx.touchRuntimePlace(e),r.activeClientId=n.clientId,r.placeName=n.placeName,n.directions&&typeof n.directions=="object"){let p=n.directions,d=f=>f==="forward"||f==="reverse"||f==="bidirectional";d(p.scripts)&&(r.directions.scripts=p.scripts),d(p.values)&&(r.directions.values=p.values),d(p.containers)&&(r.directions.containers=p.containers),d(p.data)&&(r.directions.data=p.data),d(p.services)&&(r.directions.services=p.services),g.info("Sync directions received",{placeId:e,directions:r.directions})}else r.directions={...to};if(n.applyModes&&typeof n.applyModes=="object"){let p=n.applyModes,d=f=>f==="auto"||f==="manual";d(p.scripts)&&(r.applyModes.scripts=p.scripts),d(p.values)&&(r.applyModes.values=p.values),d(p.containers)&&(r.applyModes.containers=p.containers),d(p.data)&&(r.applyModes.data=p.data),d(p.services)&&(r.applyModes.services=p.services)}else r.applyModes={...no};r.forwardRestoreQueue=[];let a=Dce();r.activeFullSyncSessionId=a,this.pendingServiceTrees.set(a,new Map),r.instanceCount=0,r.scriptCount=0;let o=this.ctx.config.getPlaceRoot(e),s=Re.join(o,`explorer_tmp_${a}`);await hn.mkdir(s,{recursive:!0}),r.tmpIndex=new Yi(o,s),r.tmpWriter=new Cc(this.ctx.config,r.tmpIndex,e),r.collisionDirMap=new Map;let c=n.preserveLocalFiles;Array.isArray(c)&&c.length>0&&(this.setPreserveLocalFiles(a,c),g.info("PreserveLocalFiles set for sync",{syncId:a,fileCount:c.length}));let l={version:1,placeId:n.placeId,placeName:n.placeName,lastFullSync:null,lastIncrementalSync:null,instanceCount:0,scriptCount:0,syncMode:"mirror"},u=Re.join(o,".sync-meta.json");await hn.mkdir(Re.dirname(u),{recursive:!0}),await this.ctx.atomicWriteFile(u,JSON.stringify(l,null,2)+`
|
|
502
502
|
`),this.startTTLTimerForPlace(r,a),r.state="initializing",r.index.resetNameCounters(),r.syncProgress={syncStartTime:Date.now(),totalInstances:n.totalInstances,totalServices:n.totalServices,processedInstances:0,processedServices:0,currentService:null,currentChunkIndex:0,currentTotalChunks:0,bytesReceived:0,processedChunks:0},r.writer.appendChangeLog(`FULL_SYNC_START clientId=${n.clientId} placeId=${n.placeId}`),r.writer.setBootstrapMode(!0),r.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncStart",direction:"forward",source:"studio",path:`place_${n.placeId}`,details:`services:${n.totalServices} instances:${n.totalInstances}`}),g.info("Full sync started",{syncId:a,clientId:n.clientId,placeId:n.placeId,placeName:n.placeName,totalServices:n.totalServices,totalInstances:n.totalInstances}),i.status(200).json({status:"started",syncId:r.activeFullSyncSessionId})}async handleInitChunk(e,n,i){let r=this.ctx.places.get(e);if(!r||!r.activeFullSyncSessionId||!r.tmpIndex||!r.tmpWriter){i.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(r.activeClientId&&n.clientId&&r.activeClientId!==n.clientId){i.status(409).json({error:"Conflict",message:`This sync session belongs to client ${r.activeClientId}`});return}let a=r.activeFullSyncSessionId,o=this.getOrCreatePendingServiceTree(a,n),s=0,c=r.collisionDirMap;for(let u of n.instances){if(this.ctx.config.isForbiddenPath(u.path))continue;let p=r.tmpIndex.resolveParentDir(u.path),d=c.get(p)??p,{resolved:f,retroactiveRename:m}=r.tmpIndex.registerCollision(u.path,u.siblingIndex??void 0,d);m&&(await ea(r.tmpIndex,d,m.from,m.to),this.rewritePendingEffectivePaths(o,r.tmpIndex.getExplorerRoot(),Re.join(d,m.from),Re.join(d,m.to)));let h=r.tmpIndex.resolveChildrenDir(u.path),y=Re.join(d,f);y!==h&&c.set(h,y);let v=r.tmpIndex.sanitizeName(u.name),_=u;if(d!==p||f!==v){let k=r.tmpIndex.getExplorerRoot(),w=Re.relative(k,d).split(Re.sep).filter(q=>q.length>0),z=ot(["game",...w,f]);_={...u,path:z}}let S=await r.tmpWriter.writeInstance(_);r.tmpIndex.setClassName(u.path,u.className,u.siblingIndex),s++,(S.propsWritten||S.valueWritten)&&r.instanceCount++,S.scriptWritten&&r.scriptCount++,o.instances.push({effectivePath:_.path,originalPath:u.path,className:u.className})}let l=this.isLastChunk(o,n.chunkIndex,n.totalChunks);if(l){let u=this.buildServiceTree(r,o);await r.tmpWriter.writeTree(n.serviceName,u);let p=this.pendingServiceTrees.get(a);p?.delete(n.serviceName),p&&p.size===0&&this.pendingServiceTrees.delete(a)}r.syncProgress&&(r.syncProgress.processedInstances+=s,r.syncProgress.currentService=n.serviceName,r.syncProgress.currentChunkIndex=n.chunkIndex,r.syncProgress.currentTotalChunks=n.totalChunks,r.syncProgress.processedChunks++,r.syncProgress.bytesReceived+=JSON.stringify(n).length,l&&r.syncProgress.processedServices++),g.debug("Sync chunk processed",{placeId:e,serviceName:n.serviceName,chunkIndex:n.chunkIndex,totalChunks:n.totalChunks,processed:s}),i.status(200).json({processed:s,service:n.serviceName})}async handleInitComplete(e,n,i){let r=this.ctx.places.get(e);if(!r||!r.activeFullSyncSessionId){i.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(r.activeClientId&&n.clientId&&r.activeClientId!==n.clientId){i.status(409).json({error:"Conflict",message:`This sync session belongs to client ${r.activeClientId}`});return}let a=r.activeFullSyncSessionId,o=this.ctx.config.getPlaceRoot(e),s=Re.join(o,"explorer"),c=Re.join(o,`explorer_tmp_${a}`),l=r.fileWatcher,u=this.getAndClearPreserveLocalFiles(a),p=new Map,d=[];if(u.length>0){for(let f of u){let m=Re.resolve(o,f);try{let h=await hn.readFile(m,"utf-8");p.set(f,h)}catch{d.push(f)}}g.info("Backed up local files for preservation",{placeId:e,requested:u.length,backed:p.size,deleted:d.length})}l&&l.beginFullSyncReplacement();try{try{await hn.rm(s,{recursive:!0,force:!0})}catch{}await hn.rename(c,s),r.instanceCount=n.instanceCount,r.scriptCount=n.scriptCount,r.lastFullSync=new Date().toISOString();let f={version:1,placeId:r.placeId,placeName:r.placeName,lastFullSync:r.lastFullSync,lastIncrementalSync:r.lastIncrementalSync,instanceCount:r.instanceCount,scriptCount:r.scriptCount,syncMode:"mirror"},m=Re.join(o,".sync-meta.json");if(await this.ctx.atomicWriteFile(m,JSON.stringify(f,null,2)+`
|
|
503
503
|
`),this.clearTTLTimerForPlace(r,a),r.index.clearAllHashes(),r.index.clearClassMappings(),r.tmpIndex){let h=r.tmpIndex.getExplorerRoot(),y=r.index.getExplorerRoot();for(let[v,_]of r.tmpIndex.getAllHashes()){let S=Re.relative(h,v),k=Re.resolve(y,S);r.index.updateHashByValue(k,_)}for(let[v,_]of r.tmpIndex.getAllFileHashes()){let S=Re.relative(h,v),k=Re.resolve(y,S);r.index.updateFileHashByValue(k,_)}r.index.resetNameCounters(),r.index.mergeNameMappingsFrom(r.tmpIndex)}if(r.tmpIndex=null,r.tmpWriter&&(r.tmpWriter.stopChangeLogFlusher(),r.tmpWriter=null),this.pendingServiceTrees.delete(a),r.collisionDirMap=null,await r.index.saveToDisk(),r.state="syncing",r.activeFullSyncSessionId=null,r.syncProgress=null,this.ctx.touchRuntimePlace(e),this.ctx.activeFullSyncPlaceId=null,p.size>0){for(let[h,y]of p){let v=Re.resolve(o,h);try{await hn.mkdir(Re.dirname(v),{recursive:!0}),await hn.writeFile(v,y,"utf-8"),l?.suppressPath(v,Oce(y))}catch(_){g.warn("Failed to restore preserved file",{path:h,error:_ instanceof Error?_.message:String(_)})}}g.info("Restored preserved local files",{placeId:e,count:p.size})}if(d.length>0){let h=0;for(let y of d){let v=Re.resolve(o,y);try{await hn.unlink(v),r.index.removeHash(v),l?.suppressPath(v,"missing"),h++}catch(_){_.code!=="ENOENT"&&g.warn("Failed to delete preserved-as-deleted file",{path:y,error:_ instanceof Error?_.message:String(_)})}}h>0&&(await r.index.saveToDisk(),g.info("Deleted locally-removed files from new sync",{placeId:e,count:h}))}try{let h=this.ctx.config.getSyncRoot();await iv(h,o)}catch(h){g.warn("Failed to generate place sourcemap after full sync",{placeId:e,error:h instanceof Error?h.message:String(h)})}try{let h=this.ctx.config.getSyncRoot(),y={version:1,lastActivePlaceId:r.placeId,lastActivePlaceName:r.placeName??null,lastActiveAt:new Date().toISOString()},v=Re.join(h,".project-meta.json");await this.ctx.atomicWriteFile(v,JSON.stringify(y,null,2)+`
|
|
504
|
-
`),await pc(h)}catch(h){g.warn("Failed to refresh root representative sourcemap after full sync",{placeId:e,error:h instanceof Error?h.message:String(h)})}this.ctx.startFileWatcherForPlace(r).catch(h=>{g.warn("Failed to start reverse detector after full sync",{placeId:e,error:h instanceof Error?h.message:String(h)}),r.fileWatcher=null}),r.writer.setBootstrapMode(!1),r.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${r.instanceCount} scripts=${r.scriptCount}`),r.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",source:"studio",path:`place_${e}`,details:`instances:${r.instanceCount} scripts:${r.scriptCount}`}),g.info("Full sync completed",{placeId:e,instanceCount:r.instanceCount,scriptCount:r.scriptCount}),i.status(200).json({status:"completed",instanceCount:r.instanceCount,scriptCount:r.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}catch(f){throw l&&l.endFullSyncReplacement(),f}}setPreserveLocalFiles(e,n){this.preserveLocalFilesMap.set(e,n)}getAndClearPreserveLocalFiles(e){let n=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),n}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,n){let i=setTimeout(async()=>{g.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let r=this.ctx.config.getPlaceRoot(e.placeId),a=Re.join(r,`explorer_tmp_${n}`);try{await hn.rm(a,{recursive:!0,force:!0})}catch(o){g.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===n&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(n),e.collisionDirMap=null,e.tmpWriter&&(e.tmpWriter.stopChangeLogFlusher(),e.tmpWriter=null),this.ctx.activeFullSyncPlaceId===e.placeId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(e.placeId))},Q6);i&&typeof i=="object"&&"unref"in i&&i.unref(),e.incompleteSyncTimer=i}getOrCreatePendingServiceTree(e,n){let i=this.pendingServiceTrees.get(e);i||(i=new Map,this.pendingServiceTrees.set(e,i));let r=i.get(n.serviceName);if(r)return r;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return i.set(n.serviceName,a),a}isLastChunk(e,n,i){return i<=1?!0:e.zeroBasedChunkIndex?n>=i-1:n>=i}buildServiceTree(e,n){let i={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let r of n.instances){let a=this.resolveEffectiveSegments(e,r.effectivePath),o=Dt(r.originalPath);this.upsertTreeNode(i,a,o,r.className)}return this.recomputeTreeChildCounts(i),i.syncedAt=new Date().toISOString(),i}resolveEffectiveSegments(e,n){return e.tmpIndex?Dt(n).map(i=>e.tmpIndex.sanitizeName(i)):Dt(n)}rewritePendingEffectivePaths(e,n,i,r){let a=Re.relative(n,i).split(Re.sep).filter(l=>l.length>0),o=Re.relative(n,r).split(Re.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=ot(["game",...a]),c=ot(["game",...o]);for(let l of e.instances){if(l.effectivePath===s){l.effectivePath=c;continue}(l.effectivePath.startsWith(`${s}.`)||l.effectivePath.startsWith(`${s}[`))&&(l.effectivePath=`${c}${l.effectivePath.slice(s.length)}`)}}upsertTreeNode(e,n,i,r){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=i[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=r,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?r:"Folder",childCount:0,children:[]},c!==void 0&&c!==s&&(u.originalName=c),a.push(u)),u.children||(u.children=[]),a=u.children}}recomputeTreeChildCounts(e){let n=i=>{let r=i.children??[];i.children=r;for(let a of r)n(a);i.childCount=r.length};for(let i of e.children)n(i);e.childCount=e.children.length}clearTTLTimerForPlace(e,n){e.incompleteSyncTimer&&e.activeFullSyncSessionId===n&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let n=await hn.readdir(e,{withFileTypes:!0});for(let i of n)if(i.isDirectory()){if(i.name.startsWith("explorer_tmp_")){let r=Re.join(e,i.name);g.warn("Removing stale temp directory from crashed sync",{dir:i.name});try{await hn.rm(r,{recursive:!0,force:!0})}catch(a){g.error(`Failed to remove stale temp dir: ${i.name}`,a instanceof Error?a:new Error(String(a)))}}if(i.name.startsWith("place_")){let r=Re.join(e,i.name);try{let a=await hn.readdir(r,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=Re.join(r,o.name);g.warn("Removing stale temp directory from crashed sync",{dir:`${i.name}/${o.name}`});try{await hn.rm(s,{recursive:!0,force:!0})}catch(c){g.error(`Failed to remove stale temp dir: ${i.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;g.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import ar from"path";import{promises as Pv}from"fs";ie();var Ev=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}let a={pending:r.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:r.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:r.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(i),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({changes:[],count:0});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}if(r.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=r.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await r.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(i),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let i=e.body,r=i.placeId??this.ctx.getDefaultRuntimePlaceId();if(r==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=i.appliedFiles??i.appliedPaths;if(!a||!Array.isArray(a)){n.status(400).json({error:"Validation error",message:"appliedFiles (or appliedPaths) must be an array of relative file paths"});return}let o=this.ctx.places.get(r);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}this.ctx.touchRuntimePlace(r);let s=this.ctx.config.getPlaceRoot(r),c=0,l=[];for(let u of a){let p=ar.resolve(s,u);if(!Sv(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await Pv.readFile(p,"utf-8"),f=o.index.computeHash(d);o.index.updateHashByValue(p,f),o.index.updateFileHashByValue(p,f),c++}catch(d){let f=d.code;if(f==="ENOENT"){o.index.removeHash(p),o.index.removeHashesUnder(p);let m=this.resolveInstancePathForAppliedPath(o.index,p);if(m){let h=Ot(m),y=Ht(m);y&&h&&await o.writer.removeFromTree(y,h)}c++}else f==="EISDIR"?c++:l.push({path:u,error:d instanceof Error?d.message:String(d)})}}c>0&&await o.index.saveToDisk(),c>0&&o.writer.appendHistory({timestamp:new Date().toISOString(),type:"reverseApply",direction:"reverse",source:"local",path:`place_${r}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let i=e.body;if(!i.fsPath||!i.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:r,resolution:a}=i,o=this.ctx.config.getSyncRoot();if(!Sv(o,ar.resolve(o,r))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:r});return}let s;if(i.placeId&&(s=this.ctx.places.get(i.placeId)),!s){let d=r.match(/^place_(\d+)(?:_[^/]+)?\//);if(d){let f=parseInt(d[1],10);s=this.ctx.places.get(f)}else s=Array.from(this.ctx.places.values())[0]}if(!s){n.status(404).json({error:"No active place context",message:"No sync session is active. Start a sync first."});return}let c=this.ctx.config.getPlaceRoot(s.placeId),l=ar.resolve(c,r);if(!Sv(c,l)){n.status(403).json({error:"Forbidden",message:"Path is outside the place root"});return}let u=s.index,p=s.reader;if(a==="apply-studio"){if(typeof i.studioContent!="string"){n.status(400).json({error:"Validation error",message:"studioContent is required for apply-studio resolution"});return}await Pv.mkdir(ar.dirname(l),{recursive:!0});let d=i.studioContent,f=u.computeHash(d);s.fileWatcher?.suppressPath(l,f),await Pv.writeFile(l,d,"utf-8"),u.updateHashByValue(l,f),u.updateFileHashByValue(l,f),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:r});return}if(a==="apply-file"){let d=await Pv.readFile(l,"utf-8"),f=u.computeHash(d);u.resolveFile(l,"apply-file",f),await u.saveToDisk();let m=p.getFileType(l),h=p.resolveInstancePathFromFile(l);n.status(200).json({status:"resolved",resolution:"apply-file",fsPath:r,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({added:0});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}if(!r.fileWatcher){n.status(200).json({added:0});return}let a=await r.fileWatcher.rescan();this.ctx.touchRuntimePlace(i),g.info("Reverse rescan completed",{placeId:i,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let i=e.resolveInstancePathFromFsPath(n);if(i)return i;let r=e.getExplorerRoot(),a=ar.relative(r,n);if(a.startsWith("..")||a===""||ar.isAbsolute(a))return null;let o=a.split(ar.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=ar.basename(n),c=ar.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(nv.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=r;for(let f of o){d=ar.join(d,f);let m=ar.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return ot(p)}};ie();function iU(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let n of e.instances)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={});if(Array.isArray(e.changes))for(let n of e.changes)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={})}function Nce(t){return t.type==="instanceAdded"||t.type==="instanceRemoved"||t.type==="instanceRenamed"||t.type==="instanceMoved"||t.type==="scriptSourceChanged"}var Tr=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;reverseDetectorFactory;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;currentConnectedPlaceId=null;currentConnectedPlaceName=null;constructor(e,n={}){this.config=new Qy(e,n),this.reverseDetectorFactory=n.reverseDetectorFactory??(i=>new bv(i.explorerRoot,i.syncIndex,{placeId:i.placeId})),this.apiHandler=new Cv(this),this.changeProcessor=new wv(this),this.initHandler=new Iv(this),this.reverseHandler=new Ev(this),this.places=new yv({max:3,dispose:(i,r)=>{g.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),i.fileWatcher&&(g.info("Disposing place context: stopping reverse detector",{placeId:r,reason:"lru-dispose",mode:et}),i.fileWatcher.stop("lru-dispose").catch(a=>{g.error("Error stopping reverse detector during dispose",a)}),i.fileWatcher=null),i.writer.stopChangeLogFlusher().catch(a=>{g.error("LRU dispose \uC911 changelog flush \uC2E4\uD328",a)}),i.incompleteSyncTimer&&(clearTimeout(i.incompleteSyncTimer),i.incompleteSyncTimer=null),i.index.clearAllMaps(),i.index.saveToDisk().catch(a=>{g.error("Error saving index during dispose",a)}),i.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(i.activeFullSyncSessionId),i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),i.tmpIndex=null,i.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let i=this.places.get(e);if(i&&n){let r=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==r&&(g.info("Place root migrated, recreating context",{placeId:e,from:r,to:a}),this.places.delete(e),i=void 0)}if(!i){let r=await this.config.resolvePlaceRoot(e,n),a=mP.join(r,"explorer");await Ec.mkdir(r,{recursive:!0}),await Ec.mkdir(a,{recursive:!0});let o=new Yi(r,a);await o.loadFromDisk();let s=new Cc(this.config,o,e),c=new vv(this.config,o,r);s.startChangeLogFlusher(),i={placeId:e,placeName:"",index:o,writer:s,reader:c,fileWatcher:null,state:"idle",activeClientId:null,activeFullSyncSessionId:null,instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,tmpIndex:null,tmpWriter:null,incompleteSyncTimer:null,changesSinceLastSave:0,directions:{...to},applyModes:{...no},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,i),g.info("Created new place context",{placeId:e,placeRoot:r})}return i}async handleSyncInit(e,n){try{iU(e.body);let i=tU(e.body);if(!i.success){n.status(400).json({error:"Validation error",message:i.error});return}let r=i.data,a=r.phase==="start"?r.placeId??null:r.phase==="chunk"||r.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in start phase or must be set by previous start"});return}switch(r.phase){case"start":this.setCurrentConnectedPlace(a,r.placeName),await this.initHandler.handleInitStart(a,r,n);break;case"chunk":await this.initHandler.handleInitChunk(a,r,n);break;case"complete":await this.initHandler.handleInitComplete(a,r,n);break}}catch(i){this.sendError(n,i,"handleSyncInit")}}async handleSyncUpdate(e,n){try{iU(e.body);let i=nU(e.body);if(!i.success){n.status(400).json({error:"Validation error",message:i.error});return}let r=i.data,a=r.placeId??this.getDefaultRuntimePlaceId();if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in body or must have an active sync session"});return}let o=await this.getOrCreatePlaceContext(a);if(this.setCurrentConnectedPlace(a,o.placeName||this.currentConnectedPlaceName),o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=r.clientId,this.touchRuntimePlace(a);let s=Y6(o,r.changes);g.info("Sync update received",{placeId:a,clientId:r.clientId,changeCount:s.length,receivedCount:r.changes.length,types:s.map(m=>m.type)});let c=[],l=[],u=0,p=!1,d=new Map;for(let m of s)try{let h=await this.changeProcessor.processChangeForPlace(o,m,d);h?l.push(h):(u++,Nce(m)&&(p=!0))}catch(h){let y="path"in m?m.path:"oldPath"in m?m.oldPath:"unknown";c.push({path:y,error:h instanceof Error?h.message:String(h)})}for(let m of d.values())try{await xv(o,m)}catch(h){c.push({path:m.instancePath,error:h instanceof Error?h.message:String(h)})}if(p)try{let m=this.config.getSyncRoot(),h=this.config.getPlaceRoot(a);await iv(m,h),await pc(m)}catch(m){g.warn("Failed to refresh sourcemaps after incremental sync update",{placeId:a,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=eU&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let f={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(f.conflicts=l),n.status(200).json(f)}catch(i){this.sendError(n,i,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){return this.currentConnectedPlaceId}getDefaultRuntimePlaceName(){if(this.currentConnectedPlaceName)return this.currentConnectedPlaceName;if(this.currentConnectedPlaceId!==null&&this.currentConnectedPlaceId!==void 0){let e=this.places.get(this.currentConnectedPlaceId);if(e?.placeName)return e.placeName}return null}setCurrentConnectedPlace(e,n){if(this.currentConnectedPlaceId=e,e==null){this.currentConnectedPlaceName=null;return}if(typeof n=="string"&&n.length>0){this.currentConnectedPlaceName=n;let r=this.places.get(e);r&&(r.placeName=n);return}let i=this.places.get(e);i?.placeName&&(this.currentConnectedPlaceName=i.placeName)}clearCurrentConnectedPlaceIfMatch(e){this.currentConnectedPlaceId===e&&(this.currentConnectedPlaceId=null,this.currentConnectedPlaceName=null)}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let i=e.query.placeId;if(i){let r=parseInt(i,10);if(!isNaN(r))return r}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let i=u=>({state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:u,activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,reverseDetectorActive:!1,reverseDetectorMode:et,fileWatcherActive:!1}),r=this.resolveQueryPlaceId(e);if(r==null){let u=i(this.config.getSyncRoot());n.status(200).json(u);return}let a=this.places.get(r);if(!a){let u=i(this.config.getPlaceRoot(r));n.status(200).json(u);return}let o=a.fileWatcher?.getPendingCount()??0,s=a.fileWatcher?.isActivelyScanning()??!1,c=a.fileWatcher!==null;this.touchRuntimePlace(r);let l={state:a.state,instanceCount:a.instanceCount,scriptCount:a.scriptCount,lastFullSync:a.lastFullSync,lastIncrementalSync:a.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:a.activeClientId,reverseSyncAvailable:o>0,modifiedFileCount:o,applyModes:a.applyModes,directions:a.directions,reverseDetectorActive:s,reverseDetectorMode:et,fileWatcherActive:c,forwardOnlyClasses:[...uc]};n.status(200).json(l)}catch(i){this.sendError(n,i,"handleSyncStatus")}}async handleSyncStop(e,n){try{let i=e.body,r=i.placeId??this.getDefaultRuntimePlaceId();if(g.info("handleSyncStop requested",{placeId:r,clientId:i.clientId??null,reason:i.reason??"requested"}),r==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(r);if(!a){g.info("handleSyncStop no place context",{placeId:r,clientId:i.clientId??null}),n.status(200).json({status:"idle",state:"idle",placeId:r,message:`No sync context for place ${r}`});return}if(i.clientId&&a.activeClientId&&i.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){g.warn("handleSyncStop rejected due to active client mismatch",{placeId:r,requestedClientId:i.clientId,activeClientId:a.activeClientId,state:a.state,activeFullSyncSessionId:a.activeFullSyncSessionId}),n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId,s=a.state,c=a.fileWatcher!==null;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(g.info("handleSyncStop suspending reverse detector",{placeId:r,state:a.state,activeClientId:a.activeClientId,mode:et}),a.fileWatcher.beginFullSyncReplacement()),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let l=this.config.getPlaceRoot(r),u=mP.join(l,`explorer_tmp_${o}`);await Ec.rm(u,{recursive:!0,force:!0}).catch(()=>{})}a.tmpWriter&&(a.tmpWriter.stopChangeLogFlusher(),a.tmpWriter=null),a.tmpIndex=null,a.collisionDirMap=null,a.activeFullSyncSessionId=null,a.syncProgress=null,a.state="idle",a.activeClientId=null,a.instanceCount=0,a.scriptCount=0,a.forwardRestoreQueue=[],this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(r),g.info("handleSyncStop responding idle",{placeId:r,previousState:s,activeFullSyncSessionId:o,hadWatcher:c}),g.info("Sync stopped",{placeId:r,reason:i.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:r})}catch(i){this.sendError(n,i,"handleSyncStop")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),g.info("SyncController initialized")}catch(e){g.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{g.info("SyncController shutdown: clearing place contexts",{placeCount:this.places.size}),this.places.clear(),g.info("SyncController shut down")}catch(e){g.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let i=e+".tmp."+Ace().slice(0,8);try{await Ec.writeFile(i,n,"utf-8"),await Ec.rename(i,e)}catch(r){throw await Ec.unlink(i).catch(()=>{}),r}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(i){this.sendError(n,i,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(i){this.sendError(n,i,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(i){this.sendError(n,i,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(i){this.sendError(n,i,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(i){this.sendError(n,i,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(i){this.sendError(n,i,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(i){this.sendError(n,i,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(i){this.sendError(n,i,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(i){this.sendError(n,i,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){g.debug("Skipping reverse detector start - place not syncing",{placeId:e.placeId,state:e.state});return}if(e.fileWatcher){if(e.fileWatcher.isActivelyScanning()){g.info("startFileWatcherForPlace: reusing active reverse detector",{placeId:e.placeId,mode:et});return}g.info("startFileWatcherForPlace: resuming paused reverse detector",{placeId:e.placeId,mode:et}),e.fileWatcher.endFullSyncReplacement();return}let n=this.createReverseChangeDetector(e);this.configureReverseChangeDetector(e,n),e.fileWatcher=n,n.start().then(()=>{e.fileWatcher===n&&g.info("Reverse detector started for reverse sync",{placeId:e.placeId,mode:et})}).catch(i=>{g.warn("Failed to start reverse detector",{placeId:e.placeId,mode:et,error:i instanceof Error?i.message:String(i)}),e.fileWatcher===n&&(e.fileWatcher=null)})}createReverseChangeDetector(e){return this.reverseDetectorFactory({explorerRoot:mP.join(this.config.getPlaceRoot(e.placeId),"explorer"),syncIndex:e.index,placeId:e.placeId})}configureReverseChangeDetector(e,n){e.writer.setOnWriteCallback(i=>{n.suppressPath(i)}),n.setDirectionChecker(i=>{let r=AL(i);return e.directions[r]}),n.setOnForwardViolation(i=>{e.forwardRestoreQueue.includes(i)||(e.forwardRestoreQueue.push(i),g.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:i,queueSize:e.forwardRestoreQueue.length}))})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}async getHistoryDirect(e,n){return this.apiHandler.getHistoryDirect(e,n)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,n){return this.apiHandler.readSyncedFile(e,n)}async writeSyncedFile(e,n,i){await this.apiHandler.writeSyncedFile(e,n,i)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,i){let r=n instanceof Error?n.message:String(n);if(r.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:r});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:r});return}g.error(`SyncController.${i} failed`,n instanceof Error?n:new Error(r)),e.status(500).json({error:"Internal error",message:`${i}: ${r}`})}};import{randomUUID as jce}from"crypto";function aU(t,e){let n=jce(),i=n.replace(/-/g,"").substring(0,8).toUpperCase(),r=`${i.substring(0,4)}-${i.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:r,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,pluginCommandSessionsByClientId:new Map,pluginCommandClientIdsByProcessToken:new Map,mcpInstances:new Map,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,shuttingDown:!1,onUpstreamPermanentlyDown:null,clientModeIdleTimeoutMs:t.clientModeIdleTimeoutMs??36e5,clientModeIdleWatchdogTimer:null,shutdownFn:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalCommandExecutor:null,dashboardFolderPicker:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,pendingDashboardSyncRootPin:null,playtestControlCommand:null,dashboardSyncRootSwitchInFlight:!1,uiStudioRecorders:new Map,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var oU=kt(Qa(),1);ie();function sU(t){let e=oU.default.json({limit:"5mb"});t.app.use((n,i,r)=>{if(n.path.startsWith("/sync/")){r();return}e(n,i,r)}),t.app.use((n,i,r)=>{i.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),i.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),i.setHeader("Access-Control-Allow-Headers","Content-Type"),r()}),t.app.use((n,i,r)=>{g.debug(`${n.method} ${n.path}`,{ip:n.ip}),r()})}Ap();import{randomUUID as MP}from"crypto";ie();function Mce(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var tt=Mce()??"2.7.0";Gp();function Dv(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let n of t.mcpInstances.values())n.aiClientName&&e.add(n.aiClientName);return Array.from(e)}function fU(t){let n=Date.now();for(let[i,r]of t.mcpInstances)r.lastSeen&&n-r.lastSeen>15e3&&(t.mcpInstances.delete(i),r.sessionId&&t.executionContextManager?.endSession(r.sessionId),g.debug("Removed stale MCP instance",{instanceId:i,lastSeen:r.lastSeen}))}function mU(t,e,n){let i=e.query.clientId;if(i&&t.pluginClients.has(i)){let r=t.pluginClients.get(i);r.lastSeen=Date.now()}try{let r=e.body;if(!r||!Array.isArray(r.selection)||typeof r.count!="number"){g.warn("Invalid selection update request",{body:r}),n.status(400).json({error:"Invalid request body"});return}let a=i||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:r.selection,count:r.count,timestamp:o,clientId:a}),g.debug("Selection cache updated",{count:r.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(r){g.error("Error handling selection update",r),n.status(500).json({error:"Internal server error"})}}function hU(t,e,n){let i=parseInt(e.query.maxAge)||3e4,r=Jp(t,i);r?n.json({cached:!0,...r}):n.json({cached:!1,message:"No cached selection available"})}function Jp(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let i;if(n)i=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!i||a.timestamp>i.timestamp)&&(i=a);if(!i)return null;let r=Date.now()-i.timestamp;return e===0||r<=e?i:null}function _P(t,e){let n=t.pluginClients.get(e.clientId),i=Date.now(),r={clientId:e.clientId,projectName:e.projectName,placeName:e.placeName,placeId:e.placeId,pluginVersion:e.pluginVersion,connectedAt:n?.connectedAt||i,lastSeen:i,commandsProcessed:n?.commandsProcessed||0,connectionType:n?.connectionType||"polling",inFlightRequestId:n?.inFlightRequestId,...n?.commandSessionState?{commandSessionState:n.commandSessionState}:{},...n?.processToken?{processToken:n.processToken}:{}};return t.pluginClients.set(e.clientId,r),t.syncController&&typeof t.syncController.setCurrentConnectedPlace=="function"&&typeof e.placeId=="number"&&Number.isFinite(e.placeId)&&t.syncController.setCurrentConnectedPlace(e.placeId,e.placeName??null),t.pendingCommands.has(e.clientId)||t.pendingCommands.set(e.clientId,[]),e.pluginVersion&&(t.pluginVersion=e.pluginVersion),typeof r.placeId=="number"&&Number.isFinite(r.placeId)&&JI(t,r.placeId,r.placeName??null),t.analyticsManager&&(e.pluginVersion&&t.analyticsManager.setPluginVersion(e.pluginVersion),t.analyticsManager.trackPluginConnected()),g.info("Plugin client registered",{clientId:e.clientId,projectName:e.projectName,placeName:e.placeName,isReconnect:!!n}),{clientInfo:r,sessionId:t.sessionId,mcpVersion:tt,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Dv(t)}}function SP(t,e){let n=t.pluginClients.get(e),i=t.pluginClients.has(e);if(t.pluginClients.delete(e),t.pendingCommands.delete(e),i&&t.syncController&&typeof t.syncController.clearCurrentConnectedPlaceIfMatch=="function"&&typeof n?.placeId=="number"&&Number.isFinite(n.placeId)){let r=[...t.pluginClients.values()].filter(a=>typeof a.placeId=="number"&&Number.isFinite(a.placeId)).sort((a,o)=>o.lastSeen-a.lastSeen)[0];r?t.syncController.setCurrentConnectedPlace?.(r.placeId??null,r.placeName??null):t.syncController.clearCurrentConnectedPlaceIfMatch(n.placeId)}return i&&(at(t,"connection",{clientId:e,status:"disconnected"}),vc(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:e,message:`Plugin disconnected \u2014 ${e}`})),g.info("Plugin client unregistered",{clientId:e,existed:i}),i}function Ov(t,e,n){try{let i=e.body;if(!i.clientId){n.status(400).json({error:"Missing clientId"});return}let r=_P(t,{clientId:i.clientId,projectName:i.projectName,placeName:i.placeName,placeId:i.placeId,pluginVersion:i.pluginVersion});n.json({status:"ok",clientId:i.clientId,serverInstanceId:t.instanceId,sessionId:r.sessionId,mcpVersion:r.mcpVersion,connectedAt:r.clientInfo.connectedAt,aiClientNames:r.aiClientNames,serverStartTime:t.startTime,mcpInstanceCount:r.mcpInstanceCount})}catch(i){g.error("Error registering plugin client",i),n.status(500).json({error:"Internal server error"})}}function Av(t,e,n){let i=e.body?.clientId;if(!i){n.status(400).json({error:"Missing clientId"});return}let r=SP(t,i);n.json({status:"ok",existed:r})}function gU(t,e,n){try{let i=e.body;if(!i.instanceId){n.status(400).json({error:"Missing instanceId"});return}let r=Date.now(),a={instanceId:i.instanceId,...typeof i.sessionId=="string"?{sessionId:i.sessionId}:{},pid:i.pid,connectedAt:r,isServer:!1,lastSeen:r};i.aiClientName&&(a.aiClientName=i.aiClientName),i.cwd&&(a.cwd=i.cwd),"projectRoot"in i&&(a.projectRoot=i.projectRoot),t.mcpInstances.set(i.instanceId,a),at(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:i.instanceId,status:"registered"}),vc(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:i.instanceId,message:`MCP registered \u2014 ${a.aiClientName??i.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),g.info("MCP instance registered (client mode)",{instanceId:i.instanceId,pid:i.pid,cwd:i.cwd}),n.json({status:"ok",instanceId:i.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:tt,mcpInstanceCount:t.mcpInstances.size+1})}catch(i){g.error("Error registering MCP instance",i),n.status(500).json({error:"Internal server error"})}}function yU(t,e,n){let i=e.body?.instanceId;if(!i){n.status(400).json({error:"Missing instanceId"});return}let r=t.mcpInstances.get(i),a=!!r;t.mcpInstances.delete(i),r?.sessionId&&t.executionContextManager?.endSession(r.sessionId),a&&(at(t,"mcp_status",{aiClientName:r?.aiClientName??"Unknown",instanceId:i,status:"unregistered"}),vc(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:i,message:`MCP unregistered \u2014 ${r?.aiClientName??i}`,...r?.aiClientName?{aiClientName:r.aiClientName}:{}})),g.info("MCP instance unregistered",{instanceId:i,existed:a}),n.json({status:"ok",existed:a})}function vU(t,e,n){let{instanceId:i,aiClientName:r}=e.body;if(i&&r){let a=t.mcpInstances.get(i);a&&(a.aiClientName=r)}n.json({status:"ok"})}function bU(t){let e=3e4,n=Date.now(),i=tr({appDataDir:t.config.appDataDir});for(let[r,a]of t.pluginClients)n-a.lastSeen>e&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return fU(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:tt,uptime:n-t.startTime,serverStartTime:t.startTime,serverExecutable:process.execPath,serverHost:t.config.httpHost,serverPort:t.config.httpPort,serverPid:process.pid,mcpInstances:[{instanceId:t.instanceId,pid:process.pid,connectedAt:t.startTime,isServer:!0,cwd:process.cwd(),projectRoot:i,lastSeen:n,...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function Nv(t,e){e.json(bU(t))}function jv(t,e,n){let i=e.query.instanceId;i&&t.mcpInstances.has(i)&&(t.mcpInstances.get(i).lastSeen=Date.now()),fU(t);let r=io(t),s={...{status:"online",connectedClients:r.length,queuedCommands:f6(t),uptime:Date.now()-t.startTime,version:tt,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,shuttingDown:t.shuttingDown,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Dv(t),pluginVersion:t.pluginVersion||void 0,dashboardSseClients:t.dashboardSseClients?.size??0,websocketClients:r.length,pluginClients:r.map(c=>({clientId:c.clientId,projectName:c.projectName,placeName:c.placeName,pluginVersion:c.pluginVersion,connectionType:c.connectionType,commandSessionState:c.commandSessionState,lastSeen:c.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(s)}function xU(t,e,n,i){let r=e.ip||e.socket.remoteAddress||"";if(!(r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1"||r==="localhost")){g.warn("Shutdown request rejected from non-localhost",{ip:r}),n.status(403).json({error:"Forbidden: localhost only"});return}g.info("Shutdown request received, initiating graceful shutdown",{requestedBy:r,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await i(),g.info("Graceful shutdown completed"),process.exit(0)}catch(o){g.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function Mv(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){g.warn("Failed to fetch connection info from server",{error:e})}return bU(t)}ie();Zp();var $c={query_instances:{discriminator:"action",validActions:new Set(["get","children","find_child","find_descendant","wait_for_child","class_info","search_name","search_class","search_property","search_tag","file_tree","project_structure","descendants","ancestors"]),paramAliases:{query_instances_search_name:{query:"pattern"},query_instances_search_property:{root:"rootPath"},query_instances_search_tag:{root:"rootPath"},query_instances_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",validActions:new Set(["create","create_with_props","delete","clone","move","rename","pivot","create_tree","mass_create","mass_delete","mass_duplicate","smart_duplicate"]),paramAliases:{mutate_instances_clone:{path:"sourcePath"},mutate_instances_smart_duplicate:{spacing:"offset"}}},manage_properties:{discriminator:"action",validActions:new Set(["get","set","get_all","set_multiple","get_attr","set_attr","get_all_attrs","delete_attr","add_tag","remove_tag","check_tag","get_tags","get_tagged","set_calculated","set_relative","mass_set","mass_get","modify_children"]),paramAliases:{manage_properties_get_tagged:{tagName:"tag",root:"rootPath"},manage_properties_set_relative:{amount:"value"}}},manage_scripts:{discriminator:"action",validActions:new Set(["get_source","set_source","create","delete","edit_replace","edit_insert","edit_delete","search","replace","get_dependencies"]),paramAliases:{manage_scripts_edit_replace:{newLines:"newContent"},manage_scripts_edit_insert:{lines:"content"},manage_scripts_replace:{pattern:"searchPattern"}}},manage_ui:{discriminator:"action",validActions:new Set(["design_brief","create_tree","update","delete","preview","check"])},manage_lighting:{discriminator:"action",validActions:new Set(["lighting","atmosphere","sky","terrain_props","time"])},manage_selection:{discriminator:"action",validActions:new Set(["get","set","clear","cached","context","details","add","remove","watch"])},manage_camera:{discriminator:"action",validActions:new Set(["info","focus_path","focus_position","suggest","screenshot"]),paramAliases:{manage_camera_suggest:{path:"targetPath"}}},manage_tween:{discriminator:"action",validActions:new Set(["create","play","pause","cancel"])},manage_audio:{discriminator:"action",validActions:new Set(["play","stop","pause","resume","set_listener"])},manage_animation:{discriminator:"action",validActions:new Set(["load","play","stop","get_tracks"])},manage_physics:{discriminator:"action",validActions:new Set(["register_group","set_collidable","get_groups"])},manage_effects:{discriminator:"action",validActions:new Set(["emit","clear","toggle"])},manage_terrain:{discriminator:"action",validActions:new Set(["fill_block","fill_ball","fill_cylinder","fill_wedge","clear_region","clear_bounds","replace_material","colors_get","colors_set","read_voxel","read_voxels","write_voxels","generate","smooth"])},spatial_query:{discriminator:"action",validActions:new Set(["raycast","find_ground","check_placement","multi_raycast","scan_area","find_flat","find_spawn","analyze_walkable","spatial_map","find_space","bounds","snap_grid","collision"]),paramAliases:{spatial_query_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",validActions:new Set(["insert","info","search","search_insert","insert_free","insert_package","export"]),paramAliases:{manage_assets_search:{maxResults:"limit"}}},manage_sync:{discriminator:"action",validActions:new Set(["status_current_place","history","directions","read_file","write_file","progress"])},workspace_state:{discriminator:"action",validActions:new Set(["sync","snapshot","changes","viewport","clear_history","metadata","scripts","selection_info","clear_cache"])},manage_logs:{discriminator:"action",validActions:new Set(["get","clear","errors"]),paramAliases:{manage_logs_get:{level:"type"}}},system_info:{discriminator:"action",validActions:new Set(["ping","connection","usage","place_info","services","studio_settings","play","stop","pause","resume","play_status","run_test"])},manage_studio:{discriminator:"action",validActions:new Set(["toggle_ui_preview"])}};var Uce={manage_selection_cached:"internal",manage_sync_status_current_place:"internal",manage_sync_history:"internal",manage_sync_directions:"internal",manage_sync_read_file:"internal",manage_sync_write_file:"internal",manage_sync_progress:"internal",system_info_connection:"internal",system_info_run_test:"internal"};function zv(t){return Uce[t]||"plugin"}var _U=new Set(["batch_execute","execute_luau","extended_call_method","extended_call_methods","extended_get_properties","extended_get_property","extended_set_properties","extended_set_property","manage_animation_get_tracks","manage_animation_load","manage_animation_play","manage_animation_stop","manage_assets_export","manage_assets_info","manage_assets_insert","manage_assets_insert_free","manage_assets_insert_package","manage_assets_search","manage_assets_search_insert","manage_audio_pause","manage_audio_play","manage_audio_resume","manage_audio_set_listener","manage_audio_stop","manage_camera_screenshot","manage_effects_clear","manage_effects_emit","manage_effects_toggle","manage_lighting_atmosphere","manage_lighting_lighting","manage_lighting_sky","manage_lighting_terrain_props","manage_lighting_time","manage_physics_get_groups","manage_physics_register_group","manage_physics_set_collidable","manage_properties_mass_get","manage_properties_mass_set","manage_properties_modify_children","manage_properties_set_calculated","manage_properties_set_relative","manage_scripts_replace","manage_selection_add","manage_selection_context","manage_selection_details","manage_selection_remove","manage_selection_watch","manage_studio_toggle_ui_preview","manage_sync_directions","manage_sync_history","manage_sync_progress","manage_sync_read_file","manage_sync_status_current_place","manage_sync_write_file","manage_terrain_clear_bounds","manage_terrain_clear_region","manage_terrain_colors_get","manage_terrain_colors_set","manage_terrain_fill_ball","manage_terrain_fill_block","manage_terrain_fill_cylinder","manage_terrain_fill_wedge","manage_terrain_generate","manage_terrain_read_voxel","manage_terrain_read_voxels","manage_terrain_replace_material","manage_terrain_smooth","manage_terrain_write_voxels","manage_tween_cancel","manage_tween_create","manage_tween_pause","manage_tween_play","manage_ui_check","manage_ui_create_tree","manage_ui_delete","manage_ui_design_brief","manage_ui_preview","manage_ui_update","mutate_instances_create_tree","mutate_instances_mass_create","mutate_instances_mass_delete","mutate_instances_mass_duplicate","mutate_instances_smart_duplicate","query_instances_ancestors","query_instances_descendants","query_instances_file_tree","query_instances_project_structure","query_instances_search_property","query_instances_search_tag","spatial_query_analyze_walkable","spatial_query_bounds","spatial_query_check_placement","spatial_query_collision","spatial_query_find_flat","spatial_query_find_ground","spatial_query_find_space","spatial_query_find_spawn","spatial_query_multi_raycast","spatial_query_raycast","spatial_query_scan_area","spatial_query_snap_grid","spatial_query_spatial_map","system_info_pause","system_info_place_info","system_info_play","system_info_play_status","system_info_resume","system_info_run_test","system_info_services","system_info_stop","system_info_studio_settings","workspace_state_changes","workspace_state_clear_cache","workspace_state_clear_history","workspace_state_metadata","workspace_state_scripts","workspace_state_selection_info","workspace_state_snapshot","workspace_state_sync","workspace_state_viewport"]),wP=new Set(["system_info_connection","system_info_ping","system_info_usage"]);function or(t){return _U.has(t)?"pro":"basic"}function Fce(t){if(t===null||typeof t!="object")return;let e=t.proFallback;if(e===null||typeof e!="object")return;let n=e;if(typeof n.executedCommand!="string")return;let i=Array.isArray(n.alternatives)?n.alternatives.filter(r=>typeof r=="string"):void 0;return{executedCommand:n.executedCommand,...typeof n.requestedCommand=="string"?{requestedCommand:n.requestedCommand}:{},...i?{alternatives:i}:{}}}var qce=/^Pro action '[^']+' is blocked in Basic mode\. Basic fallback '([^']+)' failed: (.*)$/s,Bce=/^Pro action '[^']+' is blocked in Basic mode\.(?! Basic fallback ')/,Hce=/instance not found|not found in|parent not found/,Zce=/is required|missing required/,Vce=/unknown action|unknown command/,Wce=/must be|invalid|cannot/;function Gce(t){let e=qce.exec(t);return e?{fallbackCommand:e[1],reason:e[2]}:null}function Jce(t){return Bce.test(t)}function Kce(t){let e=t.toLowerCase();return Hce.test(e)?"not_found":Zce.test(e)?"missing_param":Vce.test(e)?"unknown_action":Wce.test(e)?"validation":"other"}function Dc(t){if(t.resultSuccess){let n=Fce(t.resultData);return n?{kind:"success_fallback",fallbackCommand:n.executedCommand,...n.alternatives?{alternatives:n.alternatives}:{}}:{kind:"success_ok"}}if(t.tier!=="pro"||!t.resultError)return{kind:"error"};let e=Gce(t.resultError);return e?{kind:"blocked_fallback_failed",fallbackCommand:e.fallbackCommand,blockedDetail:Kce(e.reason)}:Jce(t.resultError)?{kind:"blocked_unsupported"}:{kind:"error"}}Fv();import{randomBytes as nle}from"node:crypto";var rle=["manage_ui_create_tree","manage_ui_update","manage_ui_delete"],RU=["manage_ui_preview","manage_ui_check"],ile=["game_genre","ui_role","style_family","layout_family","device_policy","safe_area_policy","existing_ui_strategy"];function RP(t){return t&&typeof t=="object"?t:void 0}function PP(t){return typeof t=="number"&&Number.isFinite(t)?t:0}function qv(t){let e=RP(t);if(!e)return;let n={};for(let i of ile){let r=e[i];typeof r=="string"&&r.trim().length>0&&(n[i]=r)}return Object.keys(n).length>0?n:void 0}function EP(t){let e=qv(t.qualityPlanSummary);if(e)return e;let n=RP(t.contextSummary),i=qv(n?.qualityPlanSummary);if(i)return i;let r=qv(n?.qualityPlan);if(r)return r;let a=qv(n?.quality_plan);if(a)return a}function TU(t,e){return e?{...t??{},...e}:t}function ale(t){return typeof t=="string"?{snapshotId:t}:t&&typeof t=="object"?{snapshotId:typeof t.snapshotId=="string"?t.snapshotId:null,...t.designCheckSummary!==void 0&&{designCheckSummary:t.designCheckSummary},...t.capturedAt!==void 0&&{capturedAt:t.capturedAt}}:{snapshotId:null}}function $U(t,e,n,i,r){if(!e)return;let a=PP(e.priority_high),o=PP(e.priority_medium),s=PP(e.priority_low),c=typeof e.total=="number"&&Number.isFinite(e.total)?e.total:a+o+s,l=n??(typeof e.target=="string"?e.target:void 0);return{source:t,...l!==void 0&&{targetPath:l},...typeof i=="string"&&{snapshotId:i},...r!==void 0&&{capturedAt:r},priority_high:a,priority_medium:o,priority_low:s,total:c}}function DU(t,e){if(!e)return t;if(!t)return e;let n=t.targetPath===e.targetPath?t.targetPath:void 0,i=t.snapshotId===e.snapshotId?t.snapshotId:void 0;return{source:e.source,...n!==void 0&&{targetPath:n},...i!==void 0&&{snapshotId:i},...e.capturedAt!==void 0&&{capturedAt:e.capturedAt},priority_high:t.priority_high+e.priority_high,priority_medium:t.priority_medium+e.priority_medium,priority_low:t.priority_low+e.priority_low,total:t.total+e.total}}function ole(t){let e=RP(t);if(e)for(let n of["userMessage","intent","expectedBehavior"]){let i=e[n];if(typeof i=="string"&&i.trim().length>0)return i.trim().slice(0,80)}}function sle(){return{source:"unavailable",priority_high:0,priority_medium:0,priority_low:0,total:0}}function OU(t){let e=t.now??Date.now,n=null,i=null,r=l=>`${l}_${e()}_${nle(3).toString("hex")}`,a=()=>{i&&clearTimeout(i),i=setTimeout(()=>{s("idle_timeout")},t.idleMs)},o=async l=>{try{return ale(await t.capturePreview(l))}catch{return{snapshotId:null}}},s=async l=>{if(!n)return;i&&(clearTimeout(i),i=null);let u=n;n=null;let p={},d;for(let h of u.affectedPaths)if(u.afterSnapshotsPathPolicy[h]==="skip")p[h]=null;else{let y=await o(h);p[h]=y.snapshotId,d=DU(d,$U("after_snapshot",y.designCheckSummary,h,y.snapshotId,y.capturedAt))}let f=d??u.latestDesignCheckSummary??sle(),m={requestId:u.requestId,...u.contextId!==void 0&&{contextId:u.contextId},...u.label!==void 0&&{label:u.label},startedAt:u.startedAtIso,closedAt:new Date(e()).toISOString(),closeReason:l,ok:u.ok,affectedPaths:Array.from(u.affectedPaths).sort(),mutationCount:u.mutationCount,beforeSnapshots:u.beforeSnapshots,afterSnapshots:p,childMutationIds:u.childMutationIds,...u.qualityPlanSummary!==void 0&&{qualityPlanSummary:u.qualityPlanSummary},postChangeDesignCheckSummary:f};await IU(t.placeDir,m)},c=l=>({requestId:r("req"),contextId:l.contextId,label:ole(l.contextSummary),startedAtIso:new Date(e()).toISOString(),startedAtMs:e(),affectedPaths:new Set,beforeSnapshots:{},afterSnapshotsPathPolicy:{},childMutationIds:[],mutationCount:0,ok:!0,qualityPlanSummary:EP(l),latestDesignCheckSummary:void 0,pendingMutation:void 0});return{async beforeMutation(l){if(RU.includes(l.command)){if(n){let p=r("mut");n.qualityPlanSummary=TU(n.qualityPlanSummary,EP(l)),n.pendingMutation={mutationId:p,startedMs:e(),args:l}}return}if(!rle.includes(l.command))return;n&&n.contextId!==l.contextId&&await s("context_change"),n||(n=c(l)),n.qualityPlanSummary=TU(n.qualityPlanSummary,EP(l)),l.targetPath&&!(l.targetPath in n.beforeSnapshots)&&(n.affectedPaths.add(l.targetPath),l.command==="manage_ui_create_tree"?n.beforeSnapshots[l.targetPath]=null:n.beforeSnapshots[l.targetPath]=(await o(l.targetPath)).snapshotId,l.command==="manage_ui_delete"?n.afterSnapshotsPathPolicy[l.targetPath]="skip":l.targetPath in n.afterSnapshotsPathPolicy||(n.afterSnapshotsPathPolicy[l.targetPath]="capture"));let u=r("mut");n.pendingMutation={mutationId:u,startedMs:e(),args:l}},async afterMutation(l){if(!n?.pendingMutation)return;let{mutationId:u,args:p}=n.pendingMutation,d={mutationId:u,requestId:n.requestId,ts:new Date(e()).toISOString(),command:p.command,origin:p.origin,...p.targetPath!==void 0&&{targetPath:p.targetPath},...p.briefId!==void 0&&{briefId:p.briefId},...l.diffSummary!==void 0&&{diffSummary:l.diffSummary},...l.changeDetails!==void 0&&{changeDetails:l.changeDetails},ok:l.ok,...l.error!==void 0&&{error:l.error},durationMs:l.durationMs};if(await PU(t.placeDir,d),n.pendingMutation=void 0,RU.includes(p.command)){n.latestDesignCheckSummary=DU(n.latestDesignCheckSummary,$U("check_results",l.designCheckSummary,p.targetPath,l.snapshotId,l.capturedAt));return}if(n.mutationCount++,n.childMutationIds.push(u),l.ok||(n.ok=!1),!n.contextId){await s("legacy_single");return}a()},async flushNow(){n&&await s("explicit")}}}var NP=100,Hle=12e4,Zle=15e3,YU=new Set(["manage_ui_create_tree","manage_ui_update","manage_ui_delete"]),Vle={Text:{category:"text",label:"\uD14D\uC2A4\uD2B8"},PlaceholderText:{category:"text",label:"\uD50C\uB808\uC774\uC2A4\uD640\uB354"},TextColor3:{category:"color",label:"\uAE00\uC790\uC0C9"},BackgroundColor3:{category:"color",label:"\uBC30\uACBD\uC0C9"},BorderColor3:{category:"color",label:"\uD14C\uB450\uB9AC\uC0C9"},ImageColor3:{category:"color",label:"\uC774\uBBF8\uC9C0\uC0C9"},Size:{category:"size",label:"\uC0AC\uC774\uC988"},TextSize:{category:"size",label:"\uAE00\uC790 \uD06C\uAE30"},Position:{category:"layout",label:"\uC704\uCE58"},AnchorPoint:{category:"layout",label:"\uC575\uCEE4"},Rotation:{category:"layout",label:"\uD68C\uC804"},LayoutOrder:{category:"layout",label:"\uB808\uC774\uC544\uC6C3 \uC21C\uC11C"},Visible:{category:"state",label:"\uD45C\uC2DC \uC0C1\uD0DC"},Active:{category:"state",label:"\uD65C\uC131 \uC0C1\uD0DC"},Interactable:{category:"state",label:"\uC0C1\uD638\uC791\uC6A9 \uC0C1\uD0DC"},Selectable:{category:"state",label:"\uC120\uD0DD \uAC00\uB2A5"},Enabled:{category:"state",label:"\uD65C\uC131\uD654"},Image:{category:"asset",label:"\uC774\uBBF8\uC9C0"},Texture:{category:"asset",label:"\uD14D\uC2A4\uCC98"},SoundId:{category:"asset",label:"\uC0AC\uC6B4\uB4DC"}},Wle={text:"\uD14D\uC2A4\uD2B8",color:"\uCEEC\uB7EC",size:"\uC0AC\uC774\uC988",layout:"\uB808\uC774\uC544\uC6C3",state:"\uC0C1\uD0DC",asset:"\uC5D0\uC14B",property:"\uC18D\uC131"},Gle=Object.entries($c).reduce((t,[e,n])=>{for(let i of n.validActions)t[`${e}_${i}`]=e;return t},{});function VU(t){return{toolName:Gle[t]||t,commandName:t}}function WU(t,e){if(typeof t.contextId=="string"||!e||typeof e!="object")return t;let n=e.contextId;return typeof n=="string"?{...t,contextId:n}:t}function Hv(t,e){if(t!=="manage_sync_status_current_place")return e;let{placeId:n,...i}=e;return i}async function Jle(t,e){let n=t.syncController;if(!n)return null;try{let i=e.placeId,r=n.getDefaultRuntimePlaceId(),a=null;if(typeof i=="number"&&Number.isFinite(i))a=i;else if(typeof i=="string"&&i.trim().length>0){let s=Number.parseInt(i,10);Number.isFinite(s)&&(a=s)}if(a===null&&(a=r),a===null||!Number.isFinite(a))return null;let o=a===r?n.getDefaultRuntimePlaceName():null;return typeof o=="string"&&o.length>0?await n.config.resolvePlaceRoot(a,o):n.config.getPlaceRoot(a)}catch{return null}}function Kle(t){let e=t.tree;if(!e||typeof e!="object")return;let n=e,i=typeof n.name=="string"&&n.name.length>0?n.name:void 0;return i?`${typeof n.parent=="string"&&n.parent.length>0?n.parent:"StarterGui"}.${i}`:void 0}function Xle(t,e){let n=e.targetPath;if(typeof n=="string"&&n.length>0)return n;if(t==="manage_ui_create_tree")return Kle(e)}function Yle(t){let e=Vle[t];if(e)return e;let n=t.toLowerCase();return n.includes("color")?{category:"color",label:"\uC0C9\uC0C1"}:t==="Size"||n.endsWith("size")||n.includes("scale")?{category:"size",label:"\uC0AC\uC774\uC988"}:n.includes("position")||n.includes("anchor")||n.includes("rotation")||n.includes("layout")?{category:"layout",label:"\uB808\uC774\uC544\uC6C3"}:n.includes("visible")||n.includes("active")||n.includes("enabled")||n.includes("selectable")||n.includes("interactable")?{category:"state",label:"\uC0C1\uD0DC"}:n.includes("image")||n.includes("texture")||n.includes("sound")||n.includes("asset")?{category:"asset",label:"\uC5D0\uC14B"}:n.includes("text")||n.includes("placeholder")?{category:"text",label:"\uD14D\uC2A4\uD2B8"}:{category:"property",label:t}}function GU(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function Qle(t){if(!t||typeof t!="object")return new Map;let e=t,n=e.changed_properties??e.changedProperties;if(!Array.isArray(n))return new Map;let i=new Map;for(let r of n){if(!r||typeof r!="object")continue;let a=r,o=a.property;if(typeof o!="string"||o.length===0)continue;let s=GU(a,"before"),c=GU(a,"after");i.set(o,{...s&&{before:a.before},...c&&{after:a.after},hasBefore:s,hasAfter:c})}return i}function QU(t,e){let i=t.changes?.properties;if(!i||typeof i!="object")return;let r=Qle(e),a=Object.entries(i).map(([o,s])=>{let c=Yle(o),l=r.get(o),u={property:o,category:c.category,label:c.label,after:l?.hasAfter?l.after:s};return l?.hasBefore&&(u.before=l.before),u});return a.length>0?a:void 0}function eF(t){if(!t||t.length===0)return;let e=new Map;for(let r of t)e.set(r.category,(e.get(r.category)??0)+1);let i=["text","color","size","layout","state","asset","property"].map(r=>{let a=e.get(r)??0;return a>0?`${Wle[r]} ${a}\uAC1C`:null}).filter(r=>r!==null);return i.length>0?`${i.join(", ")} \uBCC0\uACBD`:void 0}function JU(t,e){if(t==="manage_ui_update"){let n=e.changes,i=eF(QU(e));if(i)return i;let r=n?.properties?Object.keys(n.properties).length:0,a=Array.isArray(n?.addChildren)?n.addChildren.length:0,o=Array.isArray(n?.removeChildren)?n.removeChildren.length:0;return`${r} props, +${a}, -${o}`}if(t==="manage_ui_create_tree")return"tree \uC0DD\uC131";if(t==="manage_ui_delete")return"\uC81C\uAC70\uB428"}function ad(t){return t&&typeof t=="object"?t:void 0}function Zv(t,e){let n=t[e];return typeof n=="number"&&Number.isFinite(n)?n:0}function tF(t){let e=ad(t),n=ad(e?.design_check_summary);if(n)return{priority_high:Zv(n,"priority_high"),priority_medium:Zv(n,"priority_medium"),priority_low:Zv(n,"priority_low"),total:Zv(n,"total"),...typeof n.scope=="string"&&{scope:n.scope},...typeof n.target=="string"&&{target:n.target}}}function nF(t){let n=ad(t)?.snapshot_id;return typeof n=="string"?n:null}function KU(t){if(typeof t=="string"&&t.trim().length>0)return t;if(typeof t=="number"&&Number.isFinite(t)){let e=t<1e10?t*1e3:t;return new Date(e).toISOString()}}function rF(t){let e=ad(t),n=ad(e?.meta);return KU(n?.captured_at)??KU(n?.evaluated_at)}function eue(t,e){let n=t.uiStudioRecorders.get(e);if(n)return n;let i=OU({placeDir:e,idleMs:Zle,capturePreview:async r=>{try{let a=await mo(t,"manage_ui_preview",{targetPath:r},MP());if(!a.success||!a.data||typeof a.data!="object")return null;let o=tF(a.data),s=rF(a.data);return{snapshotId:nF(a.data),...o!==void 0&&{designCheckSummary:o},...s!==void 0&&{capturedAt:s}}}catch{return null}}});return t.uiStudioRecorders.set(e,i),i}async function tue(t,e,n){if(!YU.has(e))return null;let i=await Jle(t,n);if(!i)return null;let r=eue(t,i),a=e,o=typeof n.contextId=="string"?n.contextId:void 0,s=n.contextSummary,c=s!==void 0&&typeof s=="object"?s:void 0,l=Xle(a,n),u=typeof n.briefId=="string"?n.briefId:void 0,p={command:a,...o!==void 0&&{contextId:o},...c!==void 0&&{contextSummary:c},...l!==void 0&&{targetPath:l},...u!==void 0&&{briefId:u},origin:n.__origin==="dashboard"?"dashboard":"ai"};try{return await r.beforeMutation(p),{recorder:r,command:a,params:n,startedAt:Date.now()}}catch(d){return g.debug("UI Studio recorder beforeMutation \uC2E4\uD328 (\uBB34\uC2DC)",{error:d}),null}}async function XU(t,e){if(t)try{let n=t.command==="manage_ui_update"?QU(t.params,e.data):void 0,i=t.command==="manage_ui_update"?eF(n)??JU(t.command,t.params):JU(t.command,t.params),r=tF(e.data),a=nF(e.data),o=rF(e.data);await t.recorder.afterMutation({ok:e.success,durationMs:Date.now()-t.startedAt,...i!==void 0&&{diffSummary:i},...n!==void 0&&{changeDetails:n},...e.error!==void 0&&{error:e.error},...r!==void 0&&{designCheckSummary:r},...a!==null&&{snapshotId:a},...o!==void 0&&{capturedAt:o}})}catch(n){g.debug("UI Studio recorder afterMutation \uC2E4\uD328 (\uBB34\uC2DC)",{error:n})}}async function jP(t,e,n,i,r,a){if(!t.historyManager)return;let o=Dc({resultSuccess:r.success,resultData:r.data,resultError:r.error,tier:or(n)});if(r.success){let s=o.kind==="success_fallback"&&r.data&&typeof r.data=="object"?{...r.data,outcomeStatus:"fallback",requestedCommand:n,executedCommand:o.fallbackCommand,...o.alternatives?{alternatives:o.alternatives}:{}}:r.data;await t.historyManager.recordSuccess(e,Hv(n,i),s,a,n);return}if(o.kind==="blocked_unsupported"){await t.historyManager.recordBlockedOutcome(e,Hv(n,i),r.error||"Pro action blocked in Basic mode","blocked_unsupported",{executionTimeMs:a,command:n});return}if(o.kind==="blocked_fallback_failed"){await t.historyManager.recordBlockedOutcome(e,Hv(n,i),r.error||"Pro action blocked in Basic mode","blocked_fallback_failed",{executionTimeMs:a,command:n,...o.fallbackCommand?{fallbackCommand:o.fallbackCommand}:{},...o.blockedDetail?{blockedDetail:o.blockedDetail}:{}});return}await t.historyManager.recordFailure(e,Hv(n,i),r.error||"Unknown error",a,void 0,n)}function nue(t,e,n){if(!e)return;let i=t.pluginClients.get(e);i?.inFlightRequestId===n&&(i.inFlightRequestId=void 0)}function rue(t,e,n){if(g.debug("Broadcasting command",{command:e.data.command,requestId:e.data.requestId,targetClientId:n||"all",activeWebsocketClients:io(t).length}),n){let i=t.pendingCommands.get(n)||[];i.length>=NP&&i.shift(),i.push(e),t.pendingCommands.set(n,i)}else{let i=io(t).sort((o,s)=>s.lastSeen-o.lastSeen),r=e.data.params?.placeId,a;if(r!==void 0&&i.length>1&&(a=i.find(o=>o.placeId===r),a&&g.debug("Routed command by placeId",{clientId:a.clientId,placeId:r})),a||(a=i[0],i.length>1&&g.warn("Multiple plugin clients connected, routing to most recent",{targetClientId:a?.clientId,targetPlaceId:a?.placeId,totalClients:i.length,command:e.data.command})),a){let o=t.pendingCommands.get(a.clientId)||[];o.length>=NP&&o.shift(),o.push(e),t.pendingCommands.set(a.clientId,o),g.debug("Routed command to client",{clientId:a.clientId,projectName:a.projectName,placeId:a.placeId})}else t.globalPendingCommands.length>=NP&&t.globalPendingCommands.shift(),t.globalPendingCommands.push(e)}lv(t,n)}async function iF(t,e,n){let i=Date.now(),r="",a={};try{let o=e.body;r=o.command??"",a=o.params||{};let s=o.requestId;if(o.instanceId||(t.serverLastCommandAt=Date.now()),o.instanceId){let m=t.mcpInstances.get(o.instanceId);m&&(m.lastCommandAt=Date.now(),m.lastSeen=m.lastCommandAt)}let c=VU(r),l=typeof o.timeout=="number"&&Number.isFinite(o.timeout)&&o.timeout>0?{timeout:o.timeout}:void 0,u=o.instanceId?t.mcpInstances.get(o.instanceId)?.sessionId??t.sessionId:t.sessionId;if(!r){n.status(400).json({error:"Missing command"});return}if(g.debug("Received execute request",{command:r,requestId:s}),r==="manage_camera_play_screenshot"){let m="manage_camera.play_screenshot is not supported. Screenshot capture is available only in Edit mode via manage_camera.screenshot.",h=Date.now()-i;t.historyManager&&await t.historyManager.recordFailure("manage_camera",a,m,h,"UNSUPPORTED_COMMAND",r),n.json({requestId:s,success:!1,error:m});return}if(r==="get_cached_selection"){let m=a.maxAge,h=Jp(t,m!==void 0?m:3e4),y=Date.now()-i;if(!h){let S={requestId:s,success:!0,data:{cached:!1,message:"No cached selection data available. The plugin may not be connected or no selection changes have occurred yet."}};t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,S.data,y,c.commandName),n.json(S);return}let v=Date.now()-h.timestamp,_={cached:!0,selection:h.selection,count:h.count,timestamp:h.timestamp,age:v};t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,_,y,c.commandName),n.json({requestId:s,success:!0,data:_});return}if(r==="get_connection_info"){let m=await Mv(t),h=Date.now()-i;t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,m,h,c.commandName),n.json({requestId:s,success:!0,data:m});return}if(zv(r)==="internal"){if(!t.internalCommandExecutor){n.status(503).json({requestId:s,success:!1,error:`Internal command executor is not initialized for command: ${r}`});return}let m={...a,__sessionId:u},h=await t.internalCommandExecutor(r,m),y=Date.now()-i,v=WU(m,h.data);t.historyManager&&await jP(t,c.toolName,c.commandName,v,{success:h.success,data:h.data,error:h.error||"Internal command failed"},y),n.json({requestId:s,...h});return}let p=await mo(t,r,a,s,l),d=Date.now()-i,f={...WU(a,p.data),__sessionId:u};t.historyManager&&await jP(t,c.toolName,c.commandName,f,p,d),n.json(p)}catch(o){let s=Date.now()-i,c=Mn(o);if(t.historyManager&&r){let l=VU(r);await t.historyManager.recordFailure(l.toolName,a,c,s,"EXCEPTION",l.commandName)}g.error("Execute request failed",o),n.status(500).json({success:!1,error:c})}}async function aF(t,e,n){let i=Date.now(),r=e.body,a=r.action;if(!a||typeof a!="string"){n.status(400).json({success:!1,error:"Missing required field: action"});return}if(!new Set(["get","set","get_all","set_multiple","get_attr","set_attr","get_all_attrs","delete_attr","add_tag","remove_tag","check_tag","get_tags","get_tagged"]).has(a)){n.status(400).json({success:!1,error:`Unsupported action: ${a}`});return}let s=`manage_properties_${a}`,c=MP(),{action:l,...u}=r;try{t.serverLastCommandAt=Date.now();let p=await mo(t,s,u,c),d=Date.now()-i;t.historyManager&&await jP(t,"manage_properties",s,u,p,d),n.json(p)}catch(p){let d=Date.now()-i,f=Mn(p);t.historyManager&&await t.historyManager.recordFailure("manage_properties",u,f,d,"EXCEPTION",s),g.error("/api/properties \uC694\uCCAD \uCC98\uB9AC \uC2E4\uD328",p),n.status(500).json({success:!1,error:f})}}async function mo(t,e,n,i,r){g.debug("Executing command locally",{command:e,requestId:i});let a=YU.has(e)?await tue(t,e,n):null,o=t.config.requestTimeout,s=r?.timeout??Math.max(t.config.requestTimeout,Hle),c=new Promise((l,u)=>{let p={requestId:i,command:e,params:n,timestamp:Date.now(),queueTimeoutMs:o,ackedTimeoutMs:s,state:fn.QUEUED,resolve:l,reject:u,timeoutId:null};t.commandQueue.set(i,p),p.timeoutId=setTimeout(()=>{t.commandQueue.delete(p.requestId),nue(t,p.targetClientId,p.requestId),p.reject(new Error(`Command timeout after ${o}ms while ${p.state}: ${p.command}`))},o)});return rue(t,{event:"command",id:MP(),data:{command:e,requestId:i,params:n}}),c.then(async l=>{let u=l;if(e==="manage_ui_preview"&&l.success&&l.data)try{let{processPreviewResult:p,resolvePreviewPlaceDirFromCtx:d}=await Promise.resolve().then(()=>(AP(),OP)),f=await d(t,n),m=await p("manage_ui_preview",l.data,f);u={...l,data:m.data,...m.mcpContent?{mcpContent:m.mcpContent}:{}}}catch(p){g.warn("preview post-processing failed",{error:p instanceof Error?p.message:String(p)})}if(e==="manage_ui_check"&&l.success&&l.data&&n.includeVisualAnalysis===!0)try{let{resolvePreviewPlaceDirFromCtx:p}=await Promise.resolve().then(()=>(AP(),OP)),{processCheckResult:d}=await Promise.resolve().then(()=>(ZU(),HU)),f=await p(t,n),m=await d("manage_ui_check",l.data,f,n);u={...u,data:m.data}}catch(p){g.warn("check visual post-processing failed",{error:p instanceof Error?p.message:String(p)})}return u}).then(async l=>(await XU(a,{success:l.success,...l.data!==void 0&&{data:l.data},...l.error!==void 0&&{error:l.error}}),l),async l=>{throw await XU(a,{success:!1,error:Mn(l)}),l})}ie();function Vv(t,e){let n={};e.provider!==void 0&&(n.provider=e.provider),e.tier!==void 0&&(n.tier=e.tier),e.status!==void 0&&(n.status=e.status),e.canUsePro!==void 0&&(n.canUsePro=e.canUsePro),e.cleared===!0&&(n.cleared=!0),at(t,"license",n)}function cr(t,e,n,i){t.status(e).json({success:!1,error:{code:n,message:i}})}function iue(t){if(typeof t!="string")return;let e=t.trim().toLowerCase();return e.length>0?e:void 0}function Wv(t,e){if(e==="plugin")return t;let n={...t};return delete n.sessionToken,n}function od(t,e,n,i){let r=iue(i??e.query.provider);return r?t.licenseState?.supportsProvider(r)===!1?(cr(n,400,"LICENSE_PROVIDER_UNSUPPORTED",`provider "${r}" is not supported`),null):r:(cr(n,400,"LICENSE_PROVIDER_REQUIRED","provider is required"),null)}function sd(t,e){return t.licenseState?!0:(cr(e,503,"LICENSE_NOT_INITIALIZED","License system is not initialized"),!1)}function aue(t){if(!t||typeof t!="object")return!1;let e=t;return(e.canUsePro===!0||e.canUsePro===!1)&&typeof e.status=="string"&&typeof e.checkedAt=="number"}function cd(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=typeof r?.licenseKey=="string"?r.licenseKey:"";if(!a.trim()){cr(n,400,"LICENSE_KEY_REQUIRED","licenseKey is required");return}let o=od(t,e,n,r?.provider);if(!o)return;let s=typeof r?.pluginClientId=="string"?r.pluginClientId:typeof e.query.clientId=="string"?e.query.clientId:void 0,c=typeof r?.deviceId=="string"?r.deviceId:void 0,l={licenseKey:a,provider:o,clientType:i};s&&(l.pluginClientId=s),c&&(l.deviceId=c),t.licenseState.activateGateway(l).then(u=>{t.analyticsManager?.setTier(u.canUsePro?"pro":"basic"),Vv(t,{provider:o,tier:u.canUsePro?"pro":"basic",status:u.status,canUsePro:u.canUsePro}),n.json(Wv(u,i))}).catch(u=>{g.warn("License activate failed",{error:u instanceof Error?u.message:"unknown_error"}),cr(n,502,"LICENSE_ACTIVATE_FAILED",u instanceof Error?u.message:"License activation failed")})}function ld(t,e,n,i="plugin"){if(!sd(t,n))return;let r=od(t,e,n);if(r)try{let a=t.licenseState.getStatus(r);t.analyticsManager?.setTier(a.canUsePro?"pro":"basic"),n.json(Wv(a,i))}catch(a){g.warn("License status check failed",{error:a instanceof Error?a.message:"unknown_error"}),cr(n,502,"LICENSE_STATUS_FAILED",a instanceof Error?a.message:"License status check failed")}}function ud(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=od(t,e,n,r?.provider);if(!a)return;let o={provider:a,clientType:i};i==="plugin"&&typeof r?.sessionToken=="string"&&r.sessionToken.trim()&&(o.sessionToken=r.sessionToken.trim()),typeof r?.pluginClientId=="string"&&(o.pluginClientId=r.pluginClientId),typeof r?.deviceId=="string"&&(o.deviceId=r.deviceId),t.licenseState.refreshGateway(o).then(s=>{t.analyticsManager?.setTier(s.canUsePro?"pro":"basic"),Vv(t,{provider:a,tier:s.canUsePro?"pro":"basic",status:s.status,canUsePro:s.canUsePro}),n.json(Wv(s,i))}).catch(s=>{g.warn("License refresh failed",{error:s instanceof Error?s.message:"unknown_error"}),cr(n,502,"LICENSE_REFRESH_FAILED",s instanceof Error?s.message:"License refresh failed")})}function pd(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=od(t,e,n,r?.provider);a&&t.licenseState.resetGateway({provider:a,clientType:i}).then(o=>{t.analyticsManager?.setTier("basic"),Vv(t,{provider:a,tier:"basic",status:o.status,canUsePro:o.canUsePro,cleared:!0}),n.json(Wv(o,i))}).catch(o=>{g.warn("License reset failed",{error:o instanceof Error?o.message:"unknown_error"}),cr(n,502,"LICENSE_RESET_FAILED",o instanceof Error?o.message:"License reset failed")})}function zP(t,e,n){if(!sd(t,n))return;let i=e.body,r=od(t,e,n,i?.provider);if(!r)return;if(i?.clientType!=="plugin"){cr(n,400,"LICENSE_CLIENT_TYPE_INVALID","clientType must be plugin");return}let a=typeof i?.sessionToken=="string"&&i.sessionToken.trim()?i.sessionToken.trim():void 0,o=aue(i?.snapshot)?i.snapshot:void 0;if(!o&&!a){cr(n,400,"LICENSE_BOOTSTRAP_REQUIRED","snapshot or sessionToken is required");return}let s={provider:r,clientType:"plugin"};o&&(s.snapshot=o),a&&(s.sessionToken=a),t.licenseState.bootstrap(s).then(c=>{t.analyticsManager?.setTier(c.canUsePro?"pro":"basic"),Vv(t,{provider:r,tier:c.canUsePro?"pro":"basic",status:c.status,canUsePro:c.canUsePro}),n.json({ok:!0})}).catch(c=>{g.warn("License bootstrap failed",{error:c instanceof Error?c.message:"unknown_error"}),cr(n,502,"LICENSE_BOOTSTRAP_FAILED",c instanceof Error?c.message:"License bootstrap failed")})}function oF(t){t.app.post("/license/bootstrap",(e,n)=>zP(t,e,n)),t.app.post("/license/activate",(e,n)=>cd(t,e,n,"plugin")),t.app.post("/license/refresh",(e,n)=>ud(t,e,n,"plugin")),t.app.post("/license/reset",(e,n)=>pd(t,e,n,"plugin")),t.app.get("/license/status",(e,n)=>ld(t,e,n,"plugin"))}$t();ie();import{promises as Gv}from"fs";import LP from"path";function oue(t){let e=t.config.appDataDir??on();return LP.join(e,"observability","logs")}async function Mc(t,e,n){let i=e.query.clientId;if(i&&t.pluginClients.has(i)){let r=t.pluginClients.get(i);r.lastSeen=Date.now()}try{let r=e.body;if(!r||!Array.isArray(r.logs)){g.warn("Invalid logs request",{body:r}),n.status(400).json({error:"Invalid request body"});return}let a=oue(t);await Gv.mkdir(a,{recursive:!0});let c=`plugin-${new Date().toISOString().split("T")[0]}.log`,l=LP.join(a,c),u=LP.join(a,"current.log"),p=r.logs.map(d=>`[${new Date(d.timestamp*1e3).toISOString().replace("T"," ").substring(0,19)}] [${d.level}] ${d.message}`).join(`
|
|
504
|
+
`),await pc(h)}catch(h){g.warn("Failed to refresh root representative sourcemap after full sync",{placeId:e,error:h instanceof Error?h.message:String(h)})}this.ctx.startFileWatcherForPlace(r).catch(h=>{g.warn("Failed to start reverse detector after full sync",{placeId:e,error:h instanceof Error?h.message:String(h)}),r.fileWatcher=null}),r.writer.setBootstrapMode(!1),r.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${r.instanceCount} scripts=${r.scriptCount}`),r.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",source:"studio",path:`place_${e}`,details:`instances:${r.instanceCount} scripts:${r.scriptCount}`}),g.info("Full sync completed",{placeId:e,instanceCount:r.instanceCount,scriptCount:r.scriptCount}),i.status(200).json({status:"completed",instanceCount:r.instanceCount,scriptCount:r.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}catch(f){throw l&&l.endFullSyncReplacement(),f}}setPreserveLocalFiles(e,n){this.preserveLocalFilesMap.set(e,n)}getAndClearPreserveLocalFiles(e){let n=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),n}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,n){let i=setTimeout(async()=>{g.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let r=this.ctx.config.getPlaceRoot(e.placeId),a=Re.join(r,`explorer_tmp_${n}`);try{await hn.rm(a,{recursive:!0,force:!0})}catch(o){g.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===n&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(n),e.collisionDirMap=null,e.tmpWriter&&(e.tmpWriter.stopChangeLogFlusher(),e.tmpWriter=null),this.ctx.activeFullSyncPlaceId===e.placeId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(e.placeId))},Q6);i&&typeof i=="object"&&"unref"in i&&i.unref(),e.incompleteSyncTimer=i}getOrCreatePendingServiceTree(e,n){let i=this.pendingServiceTrees.get(e);i||(i=new Map,this.pendingServiceTrees.set(e,i));let r=i.get(n.serviceName);if(r)return r;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return i.set(n.serviceName,a),a}isLastChunk(e,n,i){return i<=1?!0:e.zeroBasedChunkIndex?n>=i-1:n>=i}buildServiceTree(e,n){let i={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let r of n.instances){let a=this.resolveEffectiveSegments(e,r.effectivePath),o=Dt(r.originalPath);this.upsertTreeNode(i,a,o,r.className)}return this.recomputeTreeChildCounts(i),i.syncedAt=new Date().toISOString(),i}resolveEffectiveSegments(e,n){return e.tmpIndex?Dt(n).map(i=>e.tmpIndex.sanitizeName(i)):Dt(n)}rewritePendingEffectivePaths(e,n,i,r){let a=Re.relative(n,i).split(Re.sep).filter(l=>l.length>0),o=Re.relative(n,r).split(Re.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=ot(["game",...a]),c=ot(["game",...o]);for(let l of e.instances){if(l.effectivePath===s){l.effectivePath=c;continue}(l.effectivePath.startsWith(`${s}.`)||l.effectivePath.startsWith(`${s}[`))&&(l.effectivePath=`${c}${l.effectivePath.slice(s.length)}`)}}upsertTreeNode(e,n,i,r){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=i[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=r,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?r:"Folder",childCount:0,children:[]},c!==void 0&&c!==s&&(u.originalName=c),a.push(u)),u.children||(u.children=[]),a=u.children}}recomputeTreeChildCounts(e){let n=i=>{let r=i.children??[];i.children=r;for(let a of r)n(a);i.childCount=r.length};for(let i of e.children)n(i);e.childCount=e.children.length}clearTTLTimerForPlace(e,n){e.incompleteSyncTimer&&e.activeFullSyncSessionId===n&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let n=await hn.readdir(e,{withFileTypes:!0});for(let i of n)if(i.isDirectory()){if(i.name.startsWith("explorer_tmp_")){let r=Re.join(e,i.name);g.warn("Removing stale temp directory from crashed sync",{dir:i.name});try{await hn.rm(r,{recursive:!0,force:!0})}catch(a){g.error(`Failed to remove stale temp dir: ${i.name}`,a instanceof Error?a:new Error(String(a)))}}if(i.name.startsWith("place_")){let r=Re.join(e,i.name);try{let a=await hn.readdir(r,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=Re.join(r,o.name);g.warn("Removing stale temp directory from crashed sync",{dir:`${i.name}/${o.name}`});try{await hn.rm(s,{recursive:!0,force:!0})}catch(c){g.error(`Failed to remove stale temp dir: ${i.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;g.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import ar from"path";import{promises as Pv}from"fs";ie();var Ev=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}let a={pending:r.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:r.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:r.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(i),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({changes:[],count:0});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}if(r.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=r.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await r.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(i),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let i=e.body,r=i.placeId??this.ctx.getDefaultRuntimePlaceId();if(r==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=i.appliedFiles??i.appliedPaths;if(!a||!Array.isArray(a)){n.status(400).json({error:"Validation error",message:"appliedFiles (or appliedPaths) must be an array of relative file paths"});return}let o=this.ctx.places.get(r);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}this.ctx.touchRuntimePlace(r);let s=this.ctx.config.getPlaceRoot(r),c=0,l=[];for(let u of a){let p=ar.resolve(s,u);if(!Sv(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await Pv.readFile(p,"utf-8"),f=o.index.computeHash(d);o.index.updateHashByValue(p,f),o.index.updateFileHashByValue(p,f),c++}catch(d){let f=d.code;if(f==="ENOENT"){o.index.removeHash(p),o.index.removeHashesUnder(p);let m=this.resolveInstancePathForAppliedPath(o.index,p);if(m){let h=Ot(m),y=Ht(m);y&&h&&await o.writer.removeFromTree(y,h)}c++}else f==="EISDIR"?c++:l.push({path:u,error:d instanceof Error?d.message:String(d)})}}c>0&&await o.index.saveToDisk(),c>0&&o.writer.appendHistory({timestamp:new Date().toISOString(),type:"reverseApply",direction:"reverse",source:"local",path:`place_${r}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let i=e.body;if(!i.fsPath||!i.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:r,resolution:a}=i,o=this.ctx.config.getSyncRoot();if(!Sv(o,ar.resolve(o,r))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:r});return}let s;if(i.placeId&&(s=this.ctx.places.get(i.placeId)),!s){let d=r.match(/^place_(\d+)(?:_[^/]+)?\//);if(d){let f=parseInt(d[1],10);s=this.ctx.places.get(f)}else s=Array.from(this.ctx.places.values())[0]}if(!s){n.status(404).json({error:"No active place context",message:"No sync session is active. Start a sync first."});return}let c=this.ctx.config.getPlaceRoot(s.placeId),l=ar.resolve(c,r);if(!Sv(c,l)){n.status(403).json({error:"Forbidden",message:"Path is outside the place root"});return}let u=s.index,p=s.reader;if(a==="apply-studio"){if(typeof i.studioContent!="string"){n.status(400).json({error:"Validation error",message:"studioContent is required for apply-studio resolution"});return}await Pv.mkdir(ar.dirname(l),{recursive:!0});let d=i.studioContent,f=u.computeHash(d);s.fileWatcher?.suppressPath(l,f),await Pv.writeFile(l,d,"utf-8"),u.updateHashByValue(l,f),u.updateFileHashByValue(l,f),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:r});return}if(a==="apply-file"){let d=await Pv.readFile(l,"utf-8"),f=u.computeHash(d);u.resolveFile(l,"apply-file",f),await u.saveToDisk();let m=p.getFileType(l),h=p.resolveInstancePathFromFile(l);n.status(200).json({status:"resolved",resolution:"apply-file",fsPath:r,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let i=this.ctx.resolveQueryPlaceId(e);if(i==null){n.status(200).json({added:0});return}let r=this.ctx.places.get(i);if(!r){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}if(!r.fileWatcher){n.status(200).json({added:0});return}let a=await r.fileWatcher.rescan();this.ctx.touchRuntimePlace(i),g.info("Reverse rescan completed",{placeId:i,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let i=e.resolveInstancePathFromFsPath(n);if(i)return i;let r=e.getExplorerRoot(),a=ar.relative(r,n);if(a.startsWith("..")||a===""||ar.isAbsolute(a))return null;let o=a.split(ar.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=ar.basename(n),c=ar.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(nv.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=r;for(let f of o){d=ar.join(d,f);let m=ar.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return ot(p)}};ie();function iU(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let n of e.instances)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={});if(Array.isArray(e.changes))for(let n of e.changes)Array.isArray(n.properties)&&n.properties.length===0&&(n.properties={}),Array.isArray(n.attributes)&&n.attributes.length===0&&(n.attributes={})}function Nce(t){return t.type==="instanceAdded"||t.type==="instanceRemoved"||t.type==="instanceRenamed"||t.type==="instanceMoved"||t.type==="scriptSourceChanged"}var Tr=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;reverseDetectorFactory;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;currentConnectedPlaceId=null;currentConnectedPlaceName=null;constructor(e,n={}){this.config=new Qy(e,n),this.reverseDetectorFactory=n.reverseDetectorFactory??(i=>new bv(i.explorerRoot,i.syncIndex,{placeId:i.placeId})),this.apiHandler=new Cv(this),this.changeProcessor=new wv(this),this.initHandler=new Iv(this),this.reverseHandler=new Ev(this),this.places=new yv({max:3,dispose:(i,r)=>{g.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),i.fileWatcher&&(g.info("Disposing place context: stopping reverse detector",{placeId:r,reason:"lru-dispose",mode:et}),i.fileWatcher.stop("lru-dispose").catch(a=>{g.error("Error stopping reverse detector during dispose",a)}),i.fileWatcher=null),i.writer.stopChangeLogFlusher().catch(a=>{g.error("LRU dispose \uC911 changelog flush \uC2E4\uD328",a)}),i.incompleteSyncTimer&&(clearTimeout(i.incompleteSyncTimer),i.incompleteSyncTimer=null),i.index.clearAllMaps(),i.index.saveToDisk().catch(a=>{g.error("Error saving index during dispose",a)}),i.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(i.activeFullSyncSessionId),i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),i.tmpIndex=null,i.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let i=this.places.get(e);if(i&&n){let r=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==r&&(g.info("Place root migrated, recreating context",{placeId:e,from:r,to:a}),this.places.delete(e),i=void 0)}if(!i){let r=await this.config.resolvePlaceRoot(e,n),a=mP.join(r,"explorer");await Ec.mkdir(r,{recursive:!0}),await Ec.mkdir(a,{recursive:!0});let o=new Yi(r,a);await o.loadFromDisk();let s=new Cc(this.config,o,e),c=new vv(this.config,o,r);s.startChangeLogFlusher(),i={placeId:e,placeName:"",index:o,writer:s,reader:c,fileWatcher:null,state:"idle",activeClientId:null,activeFullSyncSessionId:null,instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,tmpIndex:null,tmpWriter:null,incompleteSyncTimer:null,changesSinceLastSave:0,directions:{...to},applyModes:{...no},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,i),g.info("Created new place context",{placeId:e,placeRoot:r})}return i}async handleSyncInit(e,n){try{iU(e.body);let i=tU(e.body);if(!i.success){n.status(400).json({error:"Validation error",message:i.error});return}let r=i.data,a=r.phase==="start"?r.placeId??null:r.phase==="chunk"||r.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in start phase or must be set by previous start"});return}switch(r.phase){case"start":this.setCurrentConnectedPlace(a,r.placeName),await this.initHandler.handleInitStart(a,r,n);break;case"chunk":await this.initHandler.handleInitChunk(a,r,n);break;case"complete":await this.initHandler.handleInitComplete(a,r,n);break}}catch(i){this.sendError(n,i,"handleSyncInit")}}async handleSyncUpdate(e,n){try{iU(e.body);let i=nU(e.body);if(!i.success){n.status(400).json({error:"Validation error",message:i.error});return}let r=i.data,a=r.placeId??this.getDefaultRuntimePlaceId();if(a==null){n.status(400).json({error:"Missing placeId",message:"placeId is required in body or must have an active sync session"});return}let o=await this.getOrCreatePlaceContext(a);if(this.setCurrentConnectedPlace(a,o.placeName||this.currentConnectedPlaceName),o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=r.clientId,this.touchRuntimePlace(a);let s=Y6(o,r.changes);g.info("Sync update received",{placeId:a,clientId:r.clientId,changeCount:s.length,receivedCount:r.changes.length,types:s.map(m=>m.type)});let c=[],l=[],u=0,p=!1,d=new Map;for(let m of s)try{let h=await this.changeProcessor.processChangeForPlace(o,m,d);h?l.push(h):(u++,Nce(m)&&(p=!0))}catch(h){let y="path"in m?m.path:"oldPath"in m?m.oldPath:"unknown";c.push({path:y,error:h instanceof Error?h.message:String(h)})}for(let m of d.values())try{await xv(o,m)}catch(h){c.push({path:m.instancePath,error:h instanceof Error?h.message:String(h)})}if(p)try{let m=this.config.getSyncRoot(),h=this.config.getPlaceRoot(a);await iv(m,h),await pc(m)}catch(m){g.warn("Failed to refresh sourcemaps after incremental sync update",{placeId:a,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=eU&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let f={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(f.conflicts=l),n.status(200).json(f)}catch(i){this.sendError(n,i,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){return this.currentConnectedPlaceId}getDefaultRuntimePlaceName(){if(this.currentConnectedPlaceName)return this.currentConnectedPlaceName;if(this.currentConnectedPlaceId!==null&&this.currentConnectedPlaceId!==void 0){let e=this.places.get(this.currentConnectedPlaceId);if(e?.placeName)return e.placeName}return null}setCurrentConnectedPlace(e,n){if(this.currentConnectedPlaceId=e,e==null){this.currentConnectedPlaceName=null;return}if(typeof n=="string"&&n.length>0){this.currentConnectedPlaceName=n;let r=this.places.get(e);r&&(r.placeName=n);return}let i=this.places.get(e);i?.placeName&&(this.currentConnectedPlaceName=i.placeName)}clearCurrentConnectedPlaceIfMatch(e){this.currentConnectedPlaceId===e&&(this.currentConnectedPlaceId=null,this.currentConnectedPlaceName=null)}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let i=e.query.placeId;if(i){let r=parseInt(i,10);if(!isNaN(r))return r}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let i=u=>({state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:u,activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,reverseDetectorActive:!1,reverseDetectorMode:et,fileWatcherActive:!1}),r=this.resolveQueryPlaceId(e);if(r==null){let u=i(this.config.getSyncRoot());n.status(200).json(u);return}let a=this.places.get(r);if(!a){let u=i(this.config.getPlaceRoot(r));n.status(200).json(u);return}let o=a.fileWatcher?.getPendingCount()??0,s=a.fileWatcher?.isActivelyScanning()??!1,c=a.fileWatcher!==null;this.touchRuntimePlace(r);let l={state:a.state,instanceCount:a.instanceCount,scriptCount:a.scriptCount,lastFullSync:a.lastFullSync,lastIncrementalSync:a.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:a.activeClientId,reverseSyncAvailable:o>0,modifiedFileCount:o,applyModes:a.applyModes,directions:a.directions,reverseDetectorActive:s,reverseDetectorMode:et,fileWatcherActive:c,forwardOnlyClasses:[...uc]};n.status(200).json(l)}catch(i){this.sendError(n,i,"handleSyncStatus")}}async handleSyncStop(e,n){try{let i=e.body,r=i.placeId??this.getDefaultRuntimePlaceId();if(g.info("handleSyncStop requested",{placeId:r,clientId:i.clientId??null,reason:i.reason??"requested"}),r==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(r);if(!a){g.info("handleSyncStop no place context",{placeId:r,clientId:i.clientId??null}),n.status(200).json({status:"idle",state:"idle",placeId:r,message:`No sync context for place ${r}`});return}if(i.clientId&&a.activeClientId&&i.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){g.warn("handleSyncStop rejected due to active client mismatch",{placeId:r,requestedClientId:i.clientId,activeClientId:a.activeClientId,state:a.state,activeFullSyncSessionId:a.activeFullSyncSessionId}),n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId,s=a.state,c=a.fileWatcher!==null;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(g.info("handleSyncStop suspending reverse detector",{placeId:r,state:a.state,activeClientId:a.activeClientId,mode:et}),a.fileWatcher.beginFullSyncReplacement()),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let l=this.config.getPlaceRoot(r),u=mP.join(l,`explorer_tmp_${o}`);await Ec.rm(u,{recursive:!0,force:!0}).catch(()=>{})}a.tmpWriter&&(a.tmpWriter.stopChangeLogFlusher(),a.tmpWriter=null),a.tmpIndex=null,a.collisionDirMap=null,a.activeFullSyncSessionId=null,a.syncProgress=null,a.state="idle",a.activeClientId=null,a.instanceCount=0,a.scriptCount=0,a.forwardRestoreQueue=[],this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(r),g.info("handleSyncStop responding idle",{placeId:r,previousState:s,activeFullSyncSessionId:o,hadWatcher:c}),g.info("Sync stopped",{placeId:r,reason:i.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:r})}catch(i){this.sendError(n,i,"handleSyncStop")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),g.info("SyncController initialized")}catch(e){g.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{g.info("SyncController shutdown: clearing place contexts",{placeCount:this.places.size}),this.places.clear(),g.info("SyncController shut down")}catch(e){g.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let i=e+".tmp."+Ace().slice(0,8);try{await Ec.writeFile(i,n,"utf-8"),await Ec.rename(i,e)}catch(r){throw await Ec.unlink(i).catch(()=>{}),r}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(i){this.sendError(n,i,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(i){this.sendError(n,i,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(i){this.sendError(n,i,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(i){this.sendError(n,i,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(i){this.sendError(n,i,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(i){this.sendError(n,i,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(i){this.sendError(n,i,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(i){this.sendError(n,i,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(i){this.sendError(n,i,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){g.debug("Skipping reverse detector start - place not syncing",{placeId:e.placeId,state:e.state});return}if(e.fileWatcher){if(e.fileWatcher.isActivelyScanning()){g.info("startFileWatcherForPlace: reusing active reverse detector",{placeId:e.placeId,mode:et});return}g.info("startFileWatcherForPlace: resuming paused reverse detector",{placeId:e.placeId,mode:et}),e.fileWatcher.endFullSyncReplacement();return}let n=this.createReverseChangeDetector(e);this.configureReverseChangeDetector(e,n),e.fileWatcher=n,n.start().then(()=>{e.fileWatcher===n&&g.info("Reverse detector started for reverse sync",{placeId:e.placeId,mode:et})}).catch(i=>{g.warn("Failed to start reverse detector",{placeId:e.placeId,mode:et,error:i instanceof Error?i.message:String(i)}),e.fileWatcher===n&&(e.fileWatcher=null)})}createReverseChangeDetector(e){return this.reverseDetectorFactory({explorerRoot:mP.join(this.config.getPlaceRoot(e.placeId),"explorer"),syncIndex:e.index,placeId:e.placeId})}configureReverseChangeDetector(e,n){e.writer.setOnWriteCallback(i=>{n.suppressPath(i)}),n.setDirectionChecker(i=>{let r=AL(i);return e.directions[r]}),n.setOnForwardViolation(i=>{e.forwardRestoreQueue.includes(i)||(e.forwardRestoreQueue.push(i),g.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:i,queueSize:e.forwardRestoreQueue.length}))})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}async getHistoryDirect(e,n){return this.apiHandler.getHistoryDirect(e,n)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,n){return this.apiHandler.readSyncedFile(e,n)}async writeSyncedFile(e,n,i){await this.apiHandler.writeSyncedFile(e,n,i)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,i){let r=n instanceof Error?n.message:String(n);if(r.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:r});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:r});return}g.error(`SyncController.${i} failed`,n instanceof Error?n:new Error(r)),e.status(500).json({error:"Internal error",message:`${i}: ${r}`})}};import{randomUUID as jce}from"crypto";function aU(t,e){let n=jce(),i=n.replace(/-/g,"").substring(0,8).toUpperCase(),r=`${i.substring(0,4)}-${i.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:r,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,pluginCommandSessionsByClientId:new Map,pluginCommandClientIdsByProcessToken:new Map,mcpInstances:new Map,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,shuttingDown:!1,onUpstreamPermanentlyDown:null,clientModeIdleTimeoutMs:t.clientModeIdleTimeoutMs??36e5,clientModeIdleWatchdogTimer:null,shutdownFn:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalCommandExecutor:null,dashboardFolderPicker:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,pendingDashboardSyncRootPin:null,playtestControlCommand:null,dashboardSyncRootSwitchInFlight:!1,uiStudioRecorders:new Map,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var oU=kt(Qa(),1);ie();function sU(t){let e=oU.default.json({limit:"5mb"});t.app.use((n,i,r)=>{if(n.path.startsWith("/sync/")){r();return}e(n,i,r)}),t.app.use((n,i,r)=>{i.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),i.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),i.setHeader("Access-Control-Allow-Headers","Content-Type"),r()}),t.app.use((n,i,r)=>{g.debug(`${n.method} ${n.path}`,{ip:n.ip}),r()})}Ap();import{randomUUID as MP}from"crypto";ie();function Mce(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var tt=Mce()??"2.7.1";Gp();function Dv(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let n of t.mcpInstances.values())n.aiClientName&&e.add(n.aiClientName);return Array.from(e)}function fU(t){let n=Date.now();for(let[i,r]of t.mcpInstances)r.lastSeen&&n-r.lastSeen>15e3&&(t.mcpInstances.delete(i),r.sessionId&&t.executionContextManager?.endSession(r.sessionId),g.debug("Removed stale MCP instance",{instanceId:i,lastSeen:r.lastSeen}))}function mU(t,e,n){let i=e.query.clientId;if(i&&t.pluginClients.has(i)){let r=t.pluginClients.get(i);r.lastSeen=Date.now()}try{let r=e.body;if(!r||!Array.isArray(r.selection)||typeof r.count!="number"){g.warn("Invalid selection update request",{body:r}),n.status(400).json({error:"Invalid request body"});return}let a=i||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:r.selection,count:r.count,timestamp:o,clientId:a}),g.debug("Selection cache updated",{count:r.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(r){g.error("Error handling selection update",r),n.status(500).json({error:"Internal server error"})}}function hU(t,e,n){let i=parseInt(e.query.maxAge)||3e4,r=Jp(t,i);r?n.json({cached:!0,...r}):n.json({cached:!1,message:"No cached selection available"})}function Jp(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let i;if(n)i=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!i||a.timestamp>i.timestamp)&&(i=a);if(!i)return null;let r=Date.now()-i.timestamp;return e===0||r<=e?i:null}function _P(t,e){let n=t.pluginClients.get(e.clientId),i=Date.now(),r={clientId:e.clientId,projectName:e.projectName,placeName:e.placeName,placeId:e.placeId,pluginVersion:e.pluginVersion,connectedAt:n?.connectedAt||i,lastSeen:i,commandsProcessed:n?.commandsProcessed||0,connectionType:n?.connectionType||"polling",inFlightRequestId:n?.inFlightRequestId,...n?.commandSessionState?{commandSessionState:n.commandSessionState}:{},...n?.processToken?{processToken:n.processToken}:{}};return t.pluginClients.set(e.clientId,r),t.syncController&&typeof t.syncController.setCurrentConnectedPlace=="function"&&typeof e.placeId=="number"&&Number.isFinite(e.placeId)&&t.syncController.setCurrentConnectedPlace(e.placeId,e.placeName??null),t.pendingCommands.has(e.clientId)||t.pendingCommands.set(e.clientId,[]),e.pluginVersion&&(t.pluginVersion=e.pluginVersion),typeof r.placeId=="number"&&Number.isFinite(r.placeId)&&JI(t,r.placeId,r.placeName??null),t.analyticsManager&&(e.pluginVersion&&t.analyticsManager.setPluginVersion(e.pluginVersion),t.analyticsManager.trackPluginConnected()),g.info("Plugin client registered",{clientId:e.clientId,projectName:e.projectName,placeName:e.placeName,isReconnect:!!n}),{clientInfo:r,sessionId:t.sessionId,mcpVersion:tt,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Dv(t)}}function SP(t,e){let n=t.pluginClients.get(e),i=t.pluginClients.has(e);if(t.pluginClients.delete(e),t.pendingCommands.delete(e),i&&t.syncController&&typeof t.syncController.clearCurrentConnectedPlaceIfMatch=="function"&&typeof n?.placeId=="number"&&Number.isFinite(n.placeId)){let r=[...t.pluginClients.values()].filter(a=>typeof a.placeId=="number"&&Number.isFinite(a.placeId)).sort((a,o)=>o.lastSeen-a.lastSeen)[0];r?t.syncController.setCurrentConnectedPlace?.(r.placeId??null,r.placeName??null):t.syncController.clearCurrentConnectedPlaceIfMatch(n.placeId)}return i&&(at(t,"connection",{clientId:e,status:"disconnected"}),vc(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:e,message:`Plugin disconnected \u2014 ${e}`})),g.info("Plugin client unregistered",{clientId:e,existed:i}),i}function Ov(t,e,n){try{let i=e.body;if(!i.clientId){n.status(400).json({error:"Missing clientId"});return}let r=_P(t,{clientId:i.clientId,projectName:i.projectName,placeName:i.placeName,placeId:i.placeId,pluginVersion:i.pluginVersion});n.json({status:"ok",clientId:i.clientId,serverInstanceId:t.instanceId,sessionId:r.sessionId,mcpVersion:r.mcpVersion,connectedAt:r.clientInfo.connectedAt,aiClientNames:r.aiClientNames,serverStartTime:t.startTime,mcpInstanceCount:r.mcpInstanceCount})}catch(i){g.error("Error registering plugin client",i),n.status(500).json({error:"Internal server error"})}}function Av(t,e,n){let i=e.body?.clientId;if(!i){n.status(400).json({error:"Missing clientId"});return}let r=SP(t,i);n.json({status:"ok",existed:r})}function gU(t,e,n){try{let i=e.body;if(!i.instanceId){n.status(400).json({error:"Missing instanceId"});return}let r=Date.now(),a={instanceId:i.instanceId,...typeof i.sessionId=="string"?{sessionId:i.sessionId}:{},pid:i.pid,connectedAt:r,isServer:!1,lastSeen:r};i.aiClientName&&(a.aiClientName=i.aiClientName),i.cwd&&(a.cwd=i.cwd),"projectRoot"in i&&(a.projectRoot=i.projectRoot),t.mcpInstances.set(i.instanceId,a),at(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:i.instanceId,status:"registered"}),vc(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:i.instanceId,message:`MCP registered \u2014 ${a.aiClientName??i.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),g.info("MCP instance registered (client mode)",{instanceId:i.instanceId,pid:i.pid,cwd:i.cwd}),n.json({status:"ok",instanceId:i.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:tt,mcpInstanceCount:t.mcpInstances.size+1})}catch(i){g.error("Error registering MCP instance",i),n.status(500).json({error:"Internal server error"})}}function yU(t,e,n){let i=e.body?.instanceId;if(!i){n.status(400).json({error:"Missing instanceId"});return}let r=t.mcpInstances.get(i),a=!!r;t.mcpInstances.delete(i),r?.sessionId&&t.executionContextManager?.endSession(r.sessionId),a&&(at(t,"mcp_status",{aiClientName:r?.aiClientName??"Unknown",instanceId:i,status:"unregistered"}),vc(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:i,message:`MCP unregistered \u2014 ${r?.aiClientName??i}`,...r?.aiClientName?{aiClientName:r.aiClientName}:{}})),g.info("MCP instance unregistered",{instanceId:i,existed:a}),n.json({status:"ok",existed:a})}function vU(t,e,n){let{instanceId:i,aiClientName:r}=e.body;if(i&&r){let a=t.mcpInstances.get(i);a&&(a.aiClientName=r)}n.json({status:"ok"})}function bU(t){let e=3e4,n=Date.now(),i=tr({appDataDir:t.config.appDataDir});for(let[r,a]of t.pluginClients)n-a.lastSeen>e&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return fU(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:tt,uptime:n-t.startTime,serverStartTime:t.startTime,serverExecutable:process.execPath,serverHost:t.config.httpHost,serverPort:t.config.httpPort,serverPid:process.pid,mcpInstances:[{instanceId:t.instanceId,pid:process.pid,connectedAt:t.startTime,isServer:!0,cwd:process.cwd(),projectRoot:i,lastSeen:n,...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function Nv(t,e){e.json(bU(t))}function jv(t,e,n){let i=e.query.instanceId;i&&t.mcpInstances.has(i)&&(t.mcpInstances.get(i).lastSeen=Date.now()),fU(t);let r=io(t),s={...{status:"online",connectedClients:r.length,queuedCommands:f6(t),uptime:Date.now()-t.startTime,version:tt,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,shuttingDown:t.shuttingDown,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:Dv(t),pluginVersion:t.pluginVersion||void 0,dashboardSseClients:t.dashboardSseClients?.size??0,websocketClients:r.length,pluginClients:r.map(c=>({clientId:c.clientId,projectName:c.projectName,placeName:c.placeName,pluginVersion:c.pluginVersion,connectionType:c.connectionType,commandSessionState:c.commandSessionState,lastSeen:c.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(s)}function xU(t,e,n,i){let r=e.ip||e.socket.remoteAddress||"";if(!(r==="127.0.0.1"||r==="::1"||r==="::ffff:127.0.0.1"||r==="localhost")){g.warn("Shutdown request rejected from non-localhost",{ip:r}),n.status(403).json({error:"Forbidden: localhost only"});return}g.info("Shutdown request received, initiating graceful shutdown",{requestedBy:r,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await i(),g.info("Graceful shutdown completed"),process.exit(0)}catch(o){g.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function Mv(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){g.warn("Failed to fetch connection info from server",{error:e})}return bU(t)}ie();Zp();var $c={query_instances:{discriminator:"action",validActions:new Set(["get","children","find_child","find_descendant","wait_for_child","class_info","search_name","search_class","search_property","search_tag","file_tree","project_structure","descendants","ancestors"]),paramAliases:{query_instances_search_name:{query:"pattern"},query_instances_search_property:{root:"rootPath"},query_instances_search_tag:{root:"rootPath"},query_instances_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",validActions:new Set(["create","create_with_props","delete","clone","move","rename","pivot","create_tree","mass_create","mass_delete","mass_duplicate","smart_duplicate"]),paramAliases:{mutate_instances_clone:{path:"sourcePath"},mutate_instances_smart_duplicate:{spacing:"offset"}}},manage_properties:{discriminator:"action",validActions:new Set(["get","set","get_all","set_multiple","get_attr","set_attr","get_all_attrs","delete_attr","add_tag","remove_tag","check_tag","get_tags","get_tagged","set_calculated","set_relative","mass_set","mass_get","modify_children"]),paramAliases:{manage_properties_get_tagged:{tagName:"tag",root:"rootPath"},manage_properties_set_relative:{amount:"value"}}},manage_scripts:{discriminator:"action",validActions:new Set(["get_source","set_source","create","delete","edit_replace","edit_insert","edit_delete","search","replace","get_dependencies"]),paramAliases:{manage_scripts_edit_replace:{newLines:"newContent"},manage_scripts_edit_insert:{lines:"content"},manage_scripts_replace:{pattern:"searchPattern"}}},manage_ui:{discriminator:"action",validActions:new Set(["design_brief","create_tree","update","delete","preview","check"])},manage_lighting:{discriminator:"action",validActions:new Set(["lighting","atmosphere","sky","terrain_props","time"])},manage_selection:{discriminator:"action",validActions:new Set(["get","set","clear","cached","context","details","add","remove","watch"])},manage_camera:{discriminator:"action",validActions:new Set(["info","focus_path","focus_position","suggest","screenshot"]),paramAliases:{manage_camera_suggest:{path:"targetPath"}}},manage_tween:{discriminator:"action",validActions:new Set(["create","play","pause","cancel"])},manage_audio:{discriminator:"action",validActions:new Set(["play","stop","pause","resume","set_listener"])},manage_animation:{discriminator:"action",validActions:new Set(["load","play","stop","get_tracks"])},manage_physics:{discriminator:"action",validActions:new Set(["register_group","set_collidable","get_groups"])},manage_effects:{discriminator:"action",validActions:new Set(["emit","clear","toggle"])},manage_terrain:{discriminator:"action",validActions:new Set(["fill_block","fill_ball","fill_cylinder","fill_wedge","clear_region","clear_bounds","replace_material","colors_get","colors_set","read_voxel","read_voxels","write_voxels","generate","smooth"])},spatial_query:{discriminator:"action",validActions:new Set(["raycast","find_ground","check_placement","multi_raycast","scan_area","find_flat","find_spawn","analyze_walkable","spatial_map","find_space","bounds","snap_grid","collision"]),paramAliases:{spatial_query_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",validActions:new Set(["insert","info","search","search_insert","insert_free","insert_package","export"]),paramAliases:{manage_assets_search:{maxResults:"limit"}}},manage_sync:{discriminator:"action",validActions:new Set(["status_current_place","history","directions","read_file","write_file","progress"])},workspace_state:{discriminator:"action",validActions:new Set(["sync","snapshot","changes","viewport","clear_history","metadata","scripts","selection_info","clear_cache"])},manage_logs:{discriminator:"action",validActions:new Set(["get","clear","errors"]),paramAliases:{manage_logs_get:{level:"type"}}},system_info:{discriminator:"action",validActions:new Set(["ping","connection","usage","place_info","services","studio_settings","play","stop","pause","resume","play_status","run_test"])},manage_studio:{discriminator:"action",validActions:new Set(["toggle_ui_preview"])}};var Uce={manage_selection_cached:"internal",manage_sync_status_current_place:"internal",manage_sync_history:"internal",manage_sync_directions:"internal",manage_sync_read_file:"internal",manage_sync_write_file:"internal",manage_sync_progress:"internal",system_info_connection:"internal",system_info_run_test:"internal"};function zv(t){return Uce[t]||"plugin"}var _U=new Set(["batch_execute","execute_luau","extended_call_method","extended_call_methods","extended_get_properties","extended_get_property","extended_set_properties","extended_set_property","manage_animation_get_tracks","manage_animation_load","manage_animation_play","manage_animation_stop","manage_assets_export","manage_assets_info","manage_assets_insert","manage_assets_insert_free","manage_assets_insert_package","manage_assets_search","manage_assets_search_insert","manage_audio_pause","manage_audio_play","manage_audio_resume","manage_audio_set_listener","manage_audio_stop","manage_camera_screenshot","manage_effects_clear","manage_effects_emit","manage_effects_toggle","manage_lighting_atmosphere","manage_lighting_lighting","manage_lighting_sky","manage_lighting_terrain_props","manage_lighting_time","manage_physics_get_groups","manage_physics_register_group","manage_physics_set_collidable","manage_properties_mass_get","manage_properties_mass_set","manage_properties_modify_children","manage_properties_set_calculated","manage_properties_set_relative","manage_scripts_replace","manage_selection_add","manage_selection_context","manage_selection_details","manage_selection_remove","manage_selection_watch","manage_studio_toggle_ui_preview","manage_sync_directions","manage_sync_history","manage_sync_progress","manage_sync_read_file","manage_sync_status_current_place","manage_sync_write_file","manage_terrain_clear_bounds","manage_terrain_clear_region","manage_terrain_colors_get","manage_terrain_colors_set","manage_terrain_fill_ball","manage_terrain_fill_block","manage_terrain_fill_cylinder","manage_terrain_fill_wedge","manage_terrain_generate","manage_terrain_read_voxel","manage_terrain_read_voxels","manage_terrain_replace_material","manage_terrain_smooth","manage_terrain_write_voxels","manage_tween_cancel","manage_tween_create","manage_tween_pause","manage_tween_play","manage_ui_check","manage_ui_create_tree","manage_ui_delete","manage_ui_design_brief","manage_ui_preview","manage_ui_update","mutate_instances_create_tree","mutate_instances_mass_create","mutate_instances_mass_delete","mutate_instances_mass_duplicate","mutate_instances_smart_duplicate","query_instances_ancestors","query_instances_descendants","query_instances_file_tree","query_instances_project_structure","query_instances_search_property","query_instances_search_tag","spatial_query_analyze_walkable","spatial_query_bounds","spatial_query_check_placement","spatial_query_collision","spatial_query_find_flat","spatial_query_find_ground","spatial_query_find_space","spatial_query_find_spawn","spatial_query_multi_raycast","spatial_query_raycast","spatial_query_scan_area","spatial_query_snap_grid","spatial_query_spatial_map","system_info_pause","system_info_place_info","system_info_play","system_info_play_status","system_info_resume","system_info_run_test","system_info_services","system_info_stop","system_info_studio_settings","workspace_state_changes","workspace_state_clear_cache","workspace_state_clear_history","workspace_state_metadata","workspace_state_scripts","workspace_state_selection_info","workspace_state_snapshot","workspace_state_sync","workspace_state_viewport"]),wP=new Set(["system_info_connection","system_info_ping","system_info_usage"]);function or(t){return _U.has(t)?"pro":"basic"}function Fce(t){if(t===null||typeof t!="object")return;let e=t.proFallback;if(e===null||typeof e!="object")return;let n=e;if(typeof n.executedCommand!="string")return;let i=Array.isArray(n.alternatives)?n.alternatives.filter(r=>typeof r=="string"):void 0;return{executedCommand:n.executedCommand,...typeof n.requestedCommand=="string"?{requestedCommand:n.requestedCommand}:{},...i?{alternatives:i}:{}}}var qce=/^Pro action '[^']+' is blocked in Basic mode\. Basic fallback '([^']+)' failed: (.*)$/s,Bce=/^Pro action '[^']+' is blocked in Basic mode\.(?! Basic fallback ')/,Hce=/instance not found|not found in|parent not found/,Zce=/is required|missing required/,Vce=/unknown action|unknown command/,Wce=/must be|invalid|cannot/;function Gce(t){let e=qce.exec(t);return e?{fallbackCommand:e[1],reason:e[2]}:null}function Jce(t){return Bce.test(t)}function Kce(t){let e=t.toLowerCase();return Hce.test(e)?"not_found":Zce.test(e)?"missing_param":Vce.test(e)?"unknown_action":Wce.test(e)?"validation":"other"}function Dc(t){if(t.resultSuccess){let n=Fce(t.resultData);return n?{kind:"success_fallback",fallbackCommand:n.executedCommand,...n.alternatives?{alternatives:n.alternatives}:{}}:{kind:"success_ok"}}if(t.tier!=="pro"||!t.resultError)return{kind:"error"};let e=Gce(t.resultError);return e?{kind:"blocked_fallback_failed",fallbackCommand:e.fallbackCommand,blockedDetail:Kce(e.reason)}:Jce(t.resultError)?{kind:"blocked_unsupported"}:{kind:"error"}}Fv();import{randomBytes as nle}from"node:crypto";var rle=["manage_ui_create_tree","manage_ui_update","manage_ui_delete"],RU=["manage_ui_preview","manage_ui_check"],ile=["game_genre","ui_role","style_family","layout_family","device_policy","safe_area_policy","existing_ui_strategy"];function RP(t){return t&&typeof t=="object"?t:void 0}function PP(t){return typeof t=="number"&&Number.isFinite(t)?t:0}function qv(t){let e=RP(t);if(!e)return;let n={};for(let i of ile){let r=e[i];typeof r=="string"&&r.trim().length>0&&(n[i]=r)}return Object.keys(n).length>0?n:void 0}function EP(t){let e=qv(t.qualityPlanSummary);if(e)return e;let n=RP(t.contextSummary),i=qv(n?.qualityPlanSummary);if(i)return i;let r=qv(n?.qualityPlan);if(r)return r;let a=qv(n?.quality_plan);if(a)return a}function TU(t,e){return e?{...t??{},...e}:t}function ale(t){return typeof t=="string"?{snapshotId:t}:t&&typeof t=="object"?{snapshotId:typeof t.snapshotId=="string"?t.snapshotId:null,...t.designCheckSummary!==void 0&&{designCheckSummary:t.designCheckSummary},...t.capturedAt!==void 0&&{capturedAt:t.capturedAt}}:{snapshotId:null}}function $U(t,e,n,i,r){if(!e)return;let a=PP(e.priority_high),o=PP(e.priority_medium),s=PP(e.priority_low),c=typeof e.total=="number"&&Number.isFinite(e.total)?e.total:a+o+s,l=n??(typeof e.target=="string"?e.target:void 0);return{source:t,...l!==void 0&&{targetPath:l},...typeof i=="string"&&{snapshotId:i},...r!==void 0&&{capturedAt:r},priority_high:a,priority_medium:o,priority_low:s,total:c}}function DU(t,e){if(!e)return t;if(!t)return e;let n=t.targetPath===e.targetPath?t.targetPath:void 0,i=t.snapshotId===e.snapshotId?t.snapshotId:void 0;return{source:e.source,...n!==void 0&&{targetPath:n},...i!==void 0&&{snapshotId:i},...e.capturedAt!==void 0&&{capturedAt:e.capturedAt},priority_high:t.priority_high+e.priority_high,priority_medium:t.priority_medium+e.priority_medium,priority_low:t.priority_low+e.priority_low,total:t.total+e.total}}function ole(t){let e=RP(t);if(e)for(let n of["userMessage","intent","expectedBehavior"]){let i=e[n];if(typeof i=="string"&&i.trim().length>0)return i.trim().slice(0,80)}}function sle(){return{source:"unavailable",priority_high:0,priority_medium:0,priority_low:0,total:0}}function OU(t){let e=t.now??Date.now,n=null,i=null,r=l=>`${l}_${e()}_${nle(3).toString("hex")}`,a=()=>{i&&clearTimeout(i),i=setTimeout(()=>{s("idle_timeout")},t.idleMs)},o=async l=>{try{return ale(await t.capturePreview(l))}catch{return{snapshotId:null}}},s=async l=>{if(!n)return;i&&(clearTimeout(i),i=null);let u=n;n=null;let p={},d;for(let h of u.affectedPaths)if(u.afterSnapshotsPathPolicy[h]==="skip")p[h]=null;else{let y=await o(h);p[h]=y.snapshotId,d=DU(d,$U("after_snapshot",y.designCheckSummary,h,y.snapshotId,y.capturedAt))}let f=d??u.latestDesignCheckSummary??sle(),m={requestId:u.requestId,...u.contextId!==void 0&&{contextId:u.contextId},...u.label!==void 0&&{label:u.label},startedAt:u.startedAtIso,closedAt:new Date(e()).toISOString(),closeReason:l,ok:u.ok,affectedPaths:Array.from(u.affectedPaths).sort(),mutationCount:u.mutationCount,beforeSnapshots:u.beforeSnapshots,afterSnapshots:p,childMutationIds:u.childMutationIds,...u.qualityPlanSummary!==void 0&&{qualityPlanSummary:u.qualityPlanSummary},postChangeDesignCheckSummary:f};await IU(t.placeDir,m)},c=l=>({requestId:r("req"),contextId:l.contextId,label:ole(l.contextSummary),startedAtIso:new Date(e()).toISOString(),startedAtMs:e(),affectedPaths:new Set,beforeSnapshots:{},afterSnapshotsPathPolicy:{},childMutationIds:[],mutationCount:0,ok:!0,qualityPlanSummary:EP(l),latestDesignCheckSummary:void 0,pendingMutation:void 0});return{async beforeMutation(l){if(RU.includes(l.command)){if(n){let p=r("mut");n.qualityPlanSummary=TU(n.qualityPlanSummary,EP(l)),n.pendingMutation={mutationId:p,startedMs:e(),args:l}}return}if(!rle.includes(l.command))return;n&&n.contextId!==l.contextId&&await s("context_change"),n||(n=c(l)),n.qualityPlanSummary=TU(n.qualityPlanSummary,EP(l)),l.targetPath&&!(l.targetPath in n.beforeSnapshots)&&(n.affectedPaths.add(l.targetPath),l.command==="manage_ui_create_tree"?n.beforeSnapshots[l.targetPath]=null:n.beforeSnapshots[l.targetPath]=(await o(l.targetPath)).snapshotId,l.command==="manage_ui_delete"?n.afterSnapshotsPathPolicy[l.targetPath]="skip":l.targetPath in n.afterSnapshotsPathPolicy||(n.afterSnapshotsPathPolicy[l.targetPath]="capture"));let u=r("mut");n.pendingMutation={mutationId:u,startedMs:e(),args:l}},async afterMutation(l){if(!n?.pendingMutation)return;let{mutationId:u,args:p}=n.pendingMutation,d={mutationId:u,requestId:n.requestId,ts:new Date(e()).toISOString(),command:p.command,origin:p.origin,...p.targetPath!==void 0&&{targetPath:p.targetPath},...p.briefId!==void 0&&{briefId:p.briefId},...l.diffSummary!==void 0&&{diffSummary:l.diffSummary},...l.changeDetails!==void 0&&{changeDetails:l.changeDetails},ok:l.ok,...l.error!==void 0&&{error:l.error},durationMs:l.durationMs};if(await PU(t.placeDir,d),n.pendingMutation=void 0,RU.includes(p.command)){n.latestDesignCheckSummary=DU(n.latestDesignCheckSummary,$U("check_results",l.designCheckSummary,p.targetPath,l.snapshotId,l.capturedAt));return}if(n.mutationCount++,n.childMutationIds.push(u),l.ok||(n.ok=!1),!n.contextId){await s("legacy_single");return}a()},async flushNow(){n&&await s("explicit")}}}var NP=100,Hle=12e4,Zle=15e3,YU=new Set(["manage_ui_create_tree","manage_ui_update","manage_ui_delete"]),Vle={Text:{category:"text",label:"\uD14D\uC2A4\uD2B8"},PlaceholderText:{category:"text",label:"\uD50C\uB808\uC774\uC2A4\uD640\uB354"},TextColor3:{category:"color",label:"\uAE00\uC790\uC0C9"},BackgroundColor3:{category:"color",label:"\uBC30\uACBD\uC0C9"},BorderColor3:{category:"color",label:"\uD14C\uB450\uB9AC\uC0C9"},ImageColor3:{category:"color",label:"\uC774\uBBF8\uC9C0\uC0C9"},Size:{category:"size",label:"\uC0AC\uC774\uC988"},TextSize:{category:"size",label:"\uAE00\uC790 \uD06C\uAE30"},Position:{category:"layout",label:"\uC704\uCE58"},AnchorPoint:{category:"layout",label:"\uC575\uCEE4"},Rotation:{category:"layout",label:"\uD68C\uC804"},LayoutOrder:{category:"layout",label:"\uB808\uC774\uC544\uC6C3 \uC21C\uC11C"},Visible:{category:"state",label:"\uD45C\uC2DC \uC0C1\uD0DC"},Active:{category:"state",label:"\uD65C\uC131 \uC0C1\uD0DC"},Interactable:{category:"state",label:"\uC0C1\uD638\uC791\uC6A9 \uC0C1\uD0DC"},Selectable:{category:"state",label:"\uC120\uD0DD \uAC00\uB2A5"},Enabled:{category:"state",label:"\uD65C\uC131\uD654"},Image:{category:"asset",label:"\uC774\uBBF8\uC9C0"},Texture:{category:"asset",label:"\uD14D\uC2A4\uCC98"},SoundId:{category:"asset",label:"\uC0AC\uC6B4\uB4DC"}},Wle={text:"\uD14D\uC2A4\uD2B8",color:"\uCEEC\uB7EC",size:"\uC0AC\uC774\uC988",layout:"\uB808\uC774\uC544\uC6C3",state:"\uC0C1\uD0DC",asset:"\uC5D0\uC14B",property:"\uC18D\uC131"},Gle=Object.entries($c).reduce((t,[e,n])=>{for(let i of n.validActions)t[`${e}_${i}`]=e;return t},{});function VU(t){return{toolName:Gle[t]||t,commandName:t}}function WU(t,e){if(typeof t.contextId=="string"||!e||typeof e!="object")return t;let n=e.contextId;return typeof n=="string"?{...t,contextId:n}:t}function Hv(t,e){if(t!=="manage_sync_status_current_place")return e;let{placeId:n,...i}=e;return i}async function Jle(t,e){let n=t.syncController;if(!n)return null;try{let i=e.placeId,r=n.getDefaultRuntimePlaceId(),a=null;if(typeof i=="number"&&Number.isFinite(i))a=i;else if(typeof i=="string"&&i.trim().length>0){let s=Number.parseInt(i,10);Number.isFinite(s)&&(a=s)}if(a===null&&(a=r),a===null||!Number.isFinite(a))return null;let o=a===r?n.getDefaultRuntimePlaceName():null;return typeof o=="string"&&o.length>0?await n.config.resolvePlaceRoot(a,o):n.config.getPlaceRoot(a)}catch{return null}}function Kle(t){let e=t.tree;if(!e||typeof e!="object")return;let n=e,i=typeof n.name=="string"&&n.name.length>0?n.name:void 0;return i?`${typeof n.parent=="string"&&n.parent.length>0?n.parent:"StarterGui"}.${i}`:void 0}function Xle(t,e){let n=e.targetPath;if(typeof n=="string"&&n.length>0)return n;if(t==="manage_ui_create_tree")return Kle(e)}function Yle(t){let e=Vle[t];if(e)return e;let n=t.toLowerCase();return n.includes("color")?{category:"color",label:"\uC0C9\uC0C1"}:t==="Size"||n.endsWith("size")||n.includes("scale")?{category:"size",label:"\uC0AC\uC774\uC988"}:n.includes("position")||n.includes("anchor")||n.includes("rotation")||n.includes("layout")?{category:"layout",label:"\uB808\uC774\uC544\uC6C3"}:n.includes("visible")||n.includes("active")||n.includes("enabled")||n.includes("selectable")||n.includes("interactable")?{category:"state",label:"\uC0C1\uD0DC"}:n.includes("image")||n.includes("texture")||n.includes("sound")||n.includes("asset")?{category:"asset",label:"\uC5D0\uC14B"}:n.includes("text")||n.includes("placeholder")?{category:"text",label:"\uD14D\uC2A4\uD2B8"}:{category:"property",label:t}}function GU(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function Qle(t){if(!t||typeof t!="object")return new Map;let e=t,n=e.changed_properties??e.changedProperties;if(!Array.isArray(n))return new Map;let i=new Map;for(let r of n){if(!r||typeof r!="object")continue;let a=r,o=a.property;if(typeof o!="string"||o.length===0)continue;let s=GU(a,"before"),c=GU(a,"after");i.set(o,{...s&&{before:a.before},...c&&{after:a.after},hasBefore:s,hasAfter:c})}return i}function QU(t,e){let i=t.changes?.properties;if(!i||typeof i!="object")return;let r=Qle(e),a=Object.entries(i).map(([o,s])=>{let c=Yle(o),l=r.get(o),u={property:o,category:c.category,label:c.label,after:l?.hasAfter?l.after:s};return l?.hasBefore&&(u.before=l.before),u});return a.length>0?a:void 0}function eF(t){if(!t||t.length===0)return;let e=new Map;for(let r of t)e.set(r.category,(e.get(r.category)??0)+1);let i=["text","color","size","layout","state","asset","property"].map(r=>{let a=e.get(r)??0;return a>0?`${Wle[r]} ${a}\uAC1C`:null}).filter(r=>r!==null);return i.length>0?`${i.join(", ")} \uBCC0\uACBD`:void 0}function JU(t,e){if(t==="manage_ui_update"){let n=e.changes,i=eF(QU(e));if(i)return i;let r=n?.properties?Object.keys(n.properties).length:0,a=Array.isArray(n?.addChildren)?n.addChildren.length:0,o=Array.isArray(n?.removeChildren)?n.removeChildren.length:0;return`${r} props, +${a}, -${o}`}if(t==="manage_ui_create_tree")return"tree \uC0DD\uC131";if(t==="manage_ui_delete")return"\uC81C\uAC70\uB428"}function ad(t){return t&&typeof t=="object"?t:void 0}function Zv(t,e){let n=t[e];return typeof n=="number"&&Number.isFinite(n)?n:0}function tF(t){let e=ad(t),n=ad(e?.design_check_summary);if(n)return{priority_high:Zv(n,"priority_high"),priority_medium:Zv(n,"priority_medium"),priority_low:Zv(n,"priority_low"),total:Zv(n,"total"),...typeof n.scope=="string"&&{scope:n.scope},...typeof n.target=="string"&&{target:n.target}}}function nF(t){let n=ad(t)?.snapshot_id;return typeof n=="string"?n:null}function KU(t){if(typeof t=="string"&&t.trim().length>0)return t;if(typeof t=="number"&&Number.isFinite(t)){let e=t<1e10?t*1e3:t;return new Date(e).toISOString()}}function rF(t){let e=ad(t),n=ad(e?.meta);return KU(n?.captured_at)??KU(n?.evaluated_at)}function eue(t,e){let n=t.uiStudioRecorders.get(e);if(n)return n;let i=OU({placeDir:e,idleMs:Zle,capturePreview:async r=>{try{let a=await mo(t,"manage_ui_preview",{targetPath:r},MP());if(!a.success||!a.data||typeof a.data!="object")return null;let o=tF(a.data),s=rF(a.data);return{snapshotId:nF(a.data),...o!==void 0&&{designCheckSummary:o},...s!==void 0&&{capturedAt:s}}}catch{return null}}});return t.uiStudioRecorders.set(e,i),i}async function tue(t,e,n){if(!YU.has(e))return null;let i=await Jle(t,n);if(!i)return null;let r=eue(t,i),a=e,o=typeof n.contextId=="string"?n.contextId:void 0,s=n.contextSummary,c=s!==void 0&&typeof s=="object"?s:void 0,l=Xle(a,n),u=typeof n.briefId=="string"?n.briefId:void 0,p={command:a,...o!==void 0&&{contextId:o},...c!==void 0&&{contextSummary:c},...l!==void 0&&{targetPath:l},...u!==void 0&&{briefId:u},origin:n.__origin==="dashboard"?"dashboard":"ai"};try{return await r.beforeMutation(p),{recorder:r,command:a,params:n,startedAt:Date.now()}}catch(d){return g.debug("UI Studio recorder beforeMutation \uC2E4\uD328 (\uBB34\uC2DC)",{error:d}),null}}async function XU(t,e){if(t)try{let n=t.command==="manage_ui_update"?QU(t.params,e.data):void 0,i=t.command==="manage_ui_update"?eF(n)??JU(t.command,t.params):JU(t.command,t.params),r=tF(e.data),a=nF(e.data),o=rF(e.data);await t.recorder.afterMutation({ok:e.success,durationMs:Date.now()-t.startedAt,...i!==void 0&&{diffSummary:i},...n!==void 0&&{changeDetails:n},...e.error!==void 0&&{error:e.error},...r!==void 0&&{designCheckSummary:r},...a!==null&&{snapshotId:a},...o!==void 0&&{capturedAt:o}})}catch(n){g.debug("UI Studio recorder afterMutation \uC2E4\uD328 (\uBB34\uC2DC)",{error:n})}}async function jP(t,e,n,i,r,a){if(!t.historyManager)return;let o=Dc({resultSuccess:r.success,resultData:r.data,resultError:r.error,tier:or(n)});if(r.success){let s=o.kind==="success_fallback"&&r.data&&typeof r.data=="object"?{...r.data,outcomeStatus:"fallback",requestedCommand:n,executedCommand:o.fallbackCommand,...o.alternatives?{alternatives:o.alternatives}:{}}:r.data;await t.historyManager.recordSuccess(e,Hv(n,i),s,a,n);return}if(o.kind==="blocked_unsupported"){await t.historyManager.recordBlockedOutcome(e,Hv(n,i),r.error||"Pro action blocked in Basic mode","blocked_unsupported",{executionTimeMs:a,command:n});return}if(o.kind==="blocked_fallback_failed"){await t.historyManager.recordBlockedOutcome(e,Hv(n,i),r.error||"Pro action blocked in Basic mode","blocked_fallback_failed",{executionTimeMs:a,command:n,...o.fallbackCommand?{fallbackCommand:o.fallbackCommand}:{},...o.blockedDetail?{blockedDetail:o.blockedDetail}:{}});return}await t.historyManager.recordFailure(e,Hv(n,i),r.error||"Unknown error",a,void 0,n)}function nue(t,e,n){if(!e)return;let i=t.pluginClients.get(e);i?.inFlightRequestId===n&&(i.inFlightRequestId=void 0)}function rue(t,e,n){if(g.debug("Broadcasting command",{command:e.data.command,requestId:e.data.requestId,targetClientId:n||"all",activeWebsocketClients:io(t).length}),n){let i=t.pendingCommands.get(n)||[];i.length>=NP&&i.shift(),i.push(e),t.pendingCommands.set(n,i)}else{let i=io(t).sort((o,s)=>s.lastSeen-o.lastSeen),r=e.data.params?.placeId,a;if(r!==void 0&&i.length>1&&(a=i.find(o=>o.placeId===r),a&&g.debug("Routed command by placeId",{clientId:a.clientId,placeId:r})),a||(a=i[0],i.length>1&&g.warn("Multiple plugin clients connected, routing to most recent",{targetClientId:a?.clientId,targetPlaceId:a?.placeId,totalClients:i.length,command:e.data.command})),a){let o=t.pendingCommands.get(a.clientId)||[];o.length>=NP&&o.shift(),o.push(e),t.pendingCommands.set(a.clientId,o),g.debug("Routed command to client",{clientId:a.clientId,projectName:a.projectName,placeId:a.placeId})}else t.globalPendingCommands.length>=NP&&t.globalPendingCommands.shift(),t.globalPendingCommands.push(e)}lv(t,n)}async function iF(t,e,n){let i=Date.now(),r="",a={};try{let o=e.body;r=o.command??"",a=o.params||{};let s=o.requestId;if(o.instanceId||(t.serverLastCommandAt=Date.now()),o.instanceId){let m=t.mcpInstances.get(o.instanceId);m&&(m.lastCommandAt=Date.now(),m.lastSeen=m.lastCommandAt)}let c=VU(r),l=typeof o.timeout=="number"&&Number.isFinite(o.timeout)&&o.timeout>0?{timeout:o.timeout}:void 0,u=o.instanceId?t.mcpInstances.get(o.instanceId)?.sessionId??t.sessionId:t.sessionId;if(!r){n.status(400).json({error:"Missing command"});return}if(g.debug("Received execute request",{command:r,requestId:s}),r==="manage_camera_play_screenshot"){let m="manage_camera.play_screenshot is not supported. Screenshot capture is available only in Edit mode via manage_camera.screenshot.",h=Date.now()-i;t.historyManager&&await t.historyManager.recordFailure("manage_camera",a,m,h,"UNSUPPORTED_COMMAND",r),n.json({requestId:s,success:!1,error:m});return}if(r==="get_cached_selection"){let m=a.maxAge,h=Jp(t,m!==void 0?m:3e4),y=Date.now()-i;if(!h){let S={requestId:s,success:!0,data:{cached:!1,message:"No cached selection data available. The plugin may not be connected or no selection changes have occurred yet."}};t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,S.data,y,c.commandName),n.json(S);return}let v=Date.now()-h.timestamp,_={cached:!0,selection:h.selection,count:h.count,timestamp:h.timestamp,age:v};t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,_,y,c.commandName),n.json({requestId:s,success:!0,data:_});return}if(r==="get_connection_info"){let m=await Mv(t),h=Date.now()-i;t.historyManager&&await t.historyManager.recordSuccess(c.toolName,a,m,h,c.commandName),n.json({requestId:s,success:!0,data:m});return}if(zv(r)==="internal"){if(!t.internalCommandExecutor){n.status(503).json({requestId:s,success:!1,error:`Internal command executor is not initialized for command: ${r}`});return}let m={...a,__sessionId:u},h=await t.internalCommandExecutor(r,m),y=Date.now()-i,v=WU(m,h.data);t.historyManager&&await jP(t,c.toolName,c.commandName,v,{success:h.success,data:h.data,error:h.error||"Internal command failed"},y),n.json({requestId:s,...h});return}let p=await mo(t,r,a,s,l),d=Date.now()-i,f={...WU(a,p.data),__sessionId:u};t.historyManager&&await jP(t,c.toolName,c.commandName,f,p,d),n.json(p)}catch(o){let s=Date.now()-i,c=Mn(o);if(t.historyManager&&r){let l=VU(r);await t.historyManager.recordFailure(l.toolName,a,c,s,"EXCEPTION",l.commandName)}g.error("Execute request failed",o),n.status(500).json({success:!1,error:c})}}async function aF(t,e,n){let i=Date.now(),r=e.body,a=r.action;if(!a||typeof a!="string"){n.status(400).json({success:!1,error:"Missing required field: action"});return}if(!new Set(["get","set","get_all","set_multiple","get_attr","set_attr","get_all_attrs","delete_attr","add_tag","remove_tag","check_tag","get_tags","get_tagged"]).has(a)){n.status(400).json({success:!1,error:`Unsupported action: ${a}`});return}let s=`manage_properties_${a}`,c=MP(),{action:l,...u}=r;try{t.serverLastCommandAt=Date.now();let p=await mo(t,s,u,c),d=Date.now()-i;t.historyManager&&await jP(t,"manage_properties",s,u,p,d),n.json(p)}catch(p){let d=Date.now()-i,f=Mn(p);t.historyManager&&await t.historyManager.recordFailure("manage_properties",u,f,d,"EXCEPTION",s),g.error("/api/properties \uC694\uCCAD \uCC98\uB9AC \uC2E4\uD328",p),n.status(500).json({success:!1,error:f})}}async function mo(t,e,n,i,r){g.debug("Executing command locally",{command:e,requestId:i});let a=YU.has(e)?await tue(t,e,n):null,o=t.config.requestTimeout,s=r?.timeout??Math.max(t.config.requestTimeout,Hle),c=new Promise((l,u)=>{let p={requestId:i,command:e,params:n,timestamp:Date.now(),queueTimeoutMs:o,ackedTimeoutMs:s,state:fn.QUEUED,resolve:l,reject:u,timeoutId:null};t.commandQueue.set(i,p),p.timeoutId=setTimeout(()=>{t.commandQueue.delete(p.requestId),nue(t,p.targetClientId,p.requestId),p.reject(new Error(`Command timeout after ${o}ms while ${p.state}: ${p.command}`))},o)});return rue(t,{event:"command",id:MP(),data:{command:e,requestId:i,params:n}}),c.then(async l=>{let u=l;if(e==="manage_ui_preview"&&l.success&&l.data)try{let{processPreviewResult:p,resolvePreviewPlaceDirFromCtx:d}=await Promise.resolve().then(()=>(AP(),OP)),f=await d(t,n),m=await p("manage_ui_preview",l.data,f);u={...l,data:m.data,...m.mcpContent?{mcpContent:m.mcpContent}:{}}}catch(p){g.warn("preview post-processing failed",{error:p instanceof Error?p.message:String(p)})}if(e==="manage_ui_check"&&l.success&&l.data&&n.includeVisualAnalysis===!0)try{let{resolvePreviewPlaceDirFromCtx:p}=await Promise.resolve().then(()=>(AP(),OP)),{processCheckResult:d}=await Promise.resolve().then(()=>(ZU(),HU)),f=await p(t,n),m=await d("manage_ui_check",l.data,f,n);u={...u,data:m.data}}catch(p){g.warn("check visual post-processing failed",{error:p instanceof Error?p.message:String(p)})}return u}).then(async l=>(await XU(a,{success:l.success,...l.data!==void 0&&{data:l.data},...l.error!==void 0&&{error:l.error}}),l),async l=>{throw await XU(a,{success:!1,error:Mn(l)}),l})}ie();function Vv(t,e){let n={};e.provider!==void 0&&(n.provider=e.provider),e.tier!==void 0&&(n.tier=e.tier),e.status!==void 0&&(n.status=e.status),e.canUsePro!==void 0&&(n.canUsePro=e.canUsePro),e.cleared===!0&&(n.cleared=!0),at(t,"license",n)}function cr(t,e,n,i){t.status(e).json({success:!1,error:{code:n,message:i}})}function iue(t){if(typeof t!="string")return;let e=t.trim().toLowerCase();return e.length>0?e:void 0}function Wv(t,e){if(e==="plugin")return t;let n={...t};return delete n.sessionToken,n}function od(t,e,n,i){let r=iue(i??e.query.provider);return r?t.licenseState?.supportsProvider(r)===!1?(cr(n,400,"LICENSE_PROVIDER_UNSUPPORTED",`provider "${r}" is not supported`),null):r:(cr(n,400,"LICENSE_PROVIDER_REQUIRED","provider is required"),null)}function sd(t,e){return t.licenseState?!0:(cr(e,503,"LICENSE_NOT_INITIALIZED","License system is not initialized"),!1)}function aue(t){if(!t||typeof t!="object")return!1;let e=t;return(e.canUsePro===!0||e.canUsePro===!1)&&typeof e.status=="string"&&typeof e.checkedAt=="number"}function cd(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=typeof r?.licenseKey=="string"?r.licenseKey:"";if(!a.trim()){cr(n,400,"LICENSE_KEY_REQUIRED","licenseKey is required");return}let o=od(t,e,n,r?.provider);if(!o)return;let s=typeof r?.pluginClientId=="string"?r.pluginClientId:typeof e.query.clientId=="string"?e.query.clientId:void 0,c=typeof r?.deviceId=="string"?r.deviceId:void 0,l={licenseKey:a,provider:o,clientType:i};s&&(l.pluginClientId=s),c&&(l.deviceId=c),t.licenseState.activateGateway(l).then(u=>{t.analyticsManager?.setTier(u.canUsePro?"pro":"basic"),Vv(t,{provider:o,tier:u.canUsePro?"pro":"basic",status:u.status,canUsePro:u.canUsePro}),n.json(Wv(u,i))}).catch(u=>{g.warn("License activate failed",{error:u instanceof Error?u.message:"unknown_error"}),cr(n,502,"LICENSE_ACTIVATE_FAILED",u instanceof Error?u.message:"License activation failed")})}function ld(t,e,n,i="plugin"){if(!sd(t,n))return;let r=od(t,e,n);if(r)try{let a=t.licenseState.getStatus(r);t.analyticsManager?.setTier(a.canUsePro?"pro":"basic"),n.json(Wv(a,i))}catch(a){g.warn("License status check failed",{error:a instanceof Error?a.message:"unknown_error"}),cr(n,502,"LICENSE_STATUS_FAILED",a instanceof Error?a.message:"License status check failed")}}function ud(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=od(t,e,n,r?.provider);if(!a)return;let o={provider:a,clientType:i};i==="plugin"&&typeof r?.sessionToken=="string"&&r.sessionToken.trim()&&(o.sessionToken=r.sessionToken.trim()),typeof r?.pluginClientId=="string"&&(o.pluginClientId=r.pluginClientId),typeof r?.deviceId=="string"&&(o.deviceId=r.deviceId),t.licenseState.refreshGateway(o).then(s=>{t.analyticsManager?.setTier(s.canUsePro?"pro":"basic"),Vv(t,{provider:a,tier:s.canUsePro?"pro":"basic",status:s.status,canUsePro:s.canUsePro}),n.json(Wv(s,i))}).catch(s=>{g.warn("License refresh failed",{error:s instanceof Error?s.message:"unknown_error"}),cr(n,502,"LICENSE_REFRESH_FAILED",s instanceof Error?s.message:"License refresh failed")})}function pd(t,e,n,i="plugin"){if(!sd(t,n))return;let r=e.body,a=od(t,e,n,r?.provider);a&&t.licenseState.resetGateway({provider:a,clientType:i}).then(o=>{t.analyticsManager?.setTier("basic"),Vv(t,{provider:a,tier:"basic",status:o.status,canUsePro:o.canUsePro,cleared:!0}),n.json(Wv(o,i))}).catch(o=>{g.warn("License reset failed",{error:o instanceof Error?o.message:"unknown_error"}),cr(n,502,"LICENSE_RESET_FAILED",o instanceof Error?o.message:"License reset failed")})}function zP(t,e,n){if(!sd(t,n))return;let i=e.body,r=od(t,e,n,i?.provider);if(!r)return;if(i?.clientType!=="plugin"){cr(n,400,"LICENSE_CLIENT_TYPE_INVALID","clientType must be plugin");return}let a=typeof i?.sessionToken=="string"&&i.sessionToken.trim()?i.sessionToken.trim():void 0,o=aue(i?.snapshot)?i.snapshot:void 0;if(!o&&!a){cr(n,400,"LICENSE_BOOTSTRAP_REQUIRED","snapshot or sessionToken is required");return}let s={provider:r,clientType:"plugin"};o&&(s.snapshot=o),a&&(s.sessionToken=a),t.licenseState.bootstrap(s).then(c=>{t.analyticsManager?.setTier(c.canUsePro?"pro":"basic"),Vv(t,{provider:r,tier:c.canUsePro?"pro":"basic",status:c.status,canUsePro:c.canUsePro}),n.json({ok:!0})}).catch(c=>{g.warn("License bootstrap failed",{error:c instanceof Error?c.message:"unknown_error"}),cr(n,502,"LICENSE_BOOTSTRAP_FAILED",c instanceof Error?c.message:"License bootstrap failed")})}function oF(t){t.app.post("/license/bootstrap",(e,n)=>zP(t,e,n)),t.app.post("/license/activate",(e,n)=>cd(t,e,n,"plugin")),t.app.post("/license/refresh",(e,n)=>ud(t,e,n,"plugin")),t.app.post("/license/reset",(e,n)=>pd(t,e,n,"plugin")),t.app.get("/license/status",(e,n)=>ld(t,e,n,"plugin"))}$t();ie();import{promises as Gv}from"fs";import LP from"path";function oue(t){let e=t.config.appDataDir??on();return LP.join(e,"observability","logs")}async function Mc(t,e,n){let i=e.query.clientId;if(i&&t.pluginClients.has(i)){let r=t.pluginClients.get(i);r.lastSeen=Date.now()}try{let r=e.body;if(!r||!Array.isArray(r.logs)){g.warn("Invalid logs request",{body:r}),n.status(400).json({error:"Invalid request body"});return}let a=oue(t);await Gv.mkdir(a,{recursive:!0});let c=`plugin-${new Date().toISOString().split("T")[0]}.log`,l=LP.join(a,c),u=LP.join(a,"current.log"),p=r.logs.map(d=>`[${new Date(d.timestamp*1e3).toISOString().replace("T"," ").substring(0,19)}] [${d.level}] ${d.message}`).join(`
|
|
505
505
|
`)+`
|
|
506
506
|
`;await Gv.appendFile(l,p,"utf-8");try{let d="";try{d=await Gv.readFile(u,"utf-8")}catch{}let m=(d+p).split(`
|
|
507
507
|
`).slice(-500).join(`
|