@darkclaw921/umgt 0.0.0-stage → 0.2.0

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.
Files changed (295) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +161 -2
  3. package/architecture.md +105 -0
  4. package/bin/lm-game-hub.js +7 -0
  5. package/dist/server/activity.d.ts +17 -0
  6. package/dist/server/activity.js +55 -0
  7. package/dist/server/activity.js.map +1 -0
  8. package/dist/server/agents/activity.d.ts +36 -0
  9. package/dist/server/agents/activity.js +117 -0
  10. package/dist/server/agents/activity.js.map +1 -0
  11. package/dist/server/agents/claude.d.ts +5 -0
  12. package/dist/server/agents/claude.js +107 -0
  13. package/dist/server/agents/claude.js.map +1 -0
  14. package/dist/server/agents/codex.d.ts +5 -0
  15. package/dist/server/agents/codex.js +128 -0
  16. package/dist/server/agents/codex.js.map +1 -0
  17. package/dist/server/agents/core-tools.d.ts +23 -0
  18. package/dist/server/agents/core-tools.js +298 -0
  19. package/dist/server/agents/core-tools.js.map +1 -0
  20. package/dist/server/agents/core.d.ts +9 -0
  21. package/dist/server/agents/core.js +106 -0
  22. package/dist/server/agents/core.js.map +1 -0
  23. package/dist/server/agents/index.d.ts +5 -0
  24. package/dist/server/agents/index.js +41 -0
  25. package/dist/server/agents/index.js.map +1 -0
  26. package/dist/server/agents/kimi.d.ts +5 -0
  27. package/dist/server/agents/kimi.js +123 -0
  28. package/dist/server/agents/kimi.js.map +1 -0
  29. package/dist/server/agents/rpc.d.ts +19 -0
  30. package/dist/server/agents/rpc.js +102 -0
  31. package/dist/server/agents/rpc.js.map +1 -0
  32. package/dist/server/agents/types.d.ts +51 -0
  33. package/dist/server/agents/types.js +7 -0
  34. package/dist/server/agents/types.js.map +1 -0
  35. package/dist/server/app.d.ts +21 -0
  36. package/dist/server/app.js +38 -0
  37. package/dist/server/app.js.map +1 -0
  38. package/dist/server/asset-catalog.d.ts +5 -0
  39. package/dist/server/asset-catalog.js +525 -0
  40. package/dist/server/asset-catalog.js.map +1 -0
  41. package/dist/server/asset-media.d.ts +16 -0
  42. package/dist/server/asset-media.js +170 -0
  43. package/dist/server/asset-media.js.map +1 -0
  44. package/dist/server/asset-selection.d.ts +71 -0
  45. package/dist/server/asset-selection.js +273 -0
  46. package/dist/server/asset-selection.js.map +1 -0
  47. package/dist/server/assets.d.ts +69 -0
  48. package/dist/server/assets.js +669 -0
  49. package/dist/server/assets.js.map +1 -0
  50. package/dist/server/browser-gateway.d.ts +82 -0
  51. package/dist/server/browser-gateway.js +285 -0
  52. package/dist/server/browser-gateway.js.map +1 -0
  53. package/dist/server/chat-computer.d.ts +13 -0
  54. package/dist/server/chat-computer.js +29 -0
  55. package/dist/server/chat-computer.js.map +1 -0
  56. package/dist/server/chat-games.d.ts +38 -0
  57. package/dist/server/chat-games.js +202 -0
  58. package/dist/server/chat-games.js.map +1 -0
  59. package/dist/server/chat.d.ts +42 -0
  60. package/dist/server/chat.js +318 -0
  61. package/dist/server/chat.js.map +1 -0
  62. package/dist/server/cli.d.ts +14 -0
  63. package/dist/server/cli.js +117 -0
  64. package/dist/server/cli.js.map +1 -0
  65. package/dist/server/core-client.d.ts +69 -0
  66. package/dist/server/core-client.js +357 -0
  67. package/dist/server/core-client.js.map +1 -0
  68. package/dist/server/directory-picker.d.ts +14 -0
  69. package/dist/server/directory-picker.js +113 -0
  70. package/dist/server/directory-picker.js.map +1 -0
  71. package/dist/server/engine.d.ts +36 -0
  72. package/dist/server/engine.js +507 -0
  73. package/dist/server/engine.js.map +1 -0
  74. package/dist/server/events.d.ts +12 -0
  75. package/dist/server/events.js +137 -0
  76. package/dist/server/events.js.map +1 -0
  77. package/dist/server/game-launch.d.ts +16 -0
  78. package/dist/server/game-launch.js +143 -0
  79. package/dist/server/game-launch.js.map +1 -0
  80. package/dist/server/html5.d.ts +9 -0
  81. package/dist/server/html5.js +107 -0
  82. package/dist/server/html5.js.map +1 -0
  83. package/dist/server/interview.d.ts +241 -0
  84. package/dist/server/interview.js +190 -0
  85. package/dist/server/interview.js.map +1 -0
  86. package/dist/server/mcp.d.ts +27 -0
  87. package/dist/server/mcp.js +91 -0
  88. package/dist/server/mcp.js.map +1 -0
  89. package/dist/server/plugins.d.ts +8 -0
  90. package/dist/server/plugins.js +144 -0
  91. package/dist/server/plugins.js.map +1 -0
  92. package/dist/server/process.d.ts +15 -0
  93. package/dist/server/process.js +89 -0
  94. package/dist/server/process.js.map +1 -0
  95. package/dist/server/publication-materials.d.ts +18 -0
  96. package/dist/server/publication-materials.js +104 -0
  97. package/dist/server/publication-materials.js.map +1 -0
  98. package/dist/server/publication-mcp.d.ts +3 -0
  99. package/dist/server/publication-mcp.js +43 -0
  100. package/dist/server/publication-mcp.js.map +1 -0
  101. package/dist/server/publication.d.ts +54 -0
  102. package/dist/server/publication.js +334 -0
  103. package/dist/server/publication.js.map +1 -0
  104. package/dist/server/push.d.ts +81 -0
  105. package/dist/server/push.js +122 -0
  106. package/dist/server/push.js.map +1 -0
  107. package/dist/server/reports.d.ts +4 -0
  108. package/dist/server/reports.js +40 -0
  109. package/dist/server/reports.js.map +1 -0
  110. package/dist/server/roblox-assets.d.ts +41 -0
  111. package/dist/server/roblox-assets.js +131 -0
  112. package/dist/server/roblox-assets.js.map +1 -0
  113. package/dist/server/roblox-connection.d.ts +54 -0
  114. package/dist/server/roblox-connection.js +331 -0
  115. package/dist/server/roblox-connection.js.map +1 -0
  116. package/dist/server/roblox-install.d.ts +4 -0
  117. package/dist/server/roblox-install.js +170 -0
  118. package/dist/server/roblox-install.js.map +1 -0
  119. package/dist/server/roblox-mcp.d.ts +2 -0
  120. package/dist/server/roblox-mcp.js +34 -0
  121. package/dist/server/roblox-mcp.js.map +1 -0
  122. package/dist/server/roblox-play.d.ts +23 -0
  123. package/dist/server/roblox-play.js +84 -0
  124. package/dist/server/roblox-play.js.map +1 -0
  125. package/dist/server/roblox-project-tools.d.ts +11 -0
  126. package/dist/server/roblox-project-tools.js +119 -0
  127. package/dist/server/roblox-project-tools.js.map +1 -0
  128. package/dist/server/roblox-setup.d.ts +37 -0
  129. package/dist/server/roblox-setup.js +206 -0
  130. package/dist/server/roblox-setup.js.map +1 -0
  131. package/dist/server/roblox-skills.d.ts +15 -0
  132. package/dist/server/roblox-skills.js +15 -0
  133. package/dist/server/roblox-skills.js.map +1 -0
  134. package/dist/server/roblox.d.ts +30 -0
  135. package/dist/server/roblox.js +248 -0
  136. package/dist/server/roblox.js.map +1 -0
  137. package/dist/server/routes.d.ts +11 -0
  138. package/dist/server/routes.js +223 -0
  139. package/dist/server/routes.js.map +1 -0
  140. package/dist/server/security.d.ts +4 -0
  141. package/dist/server/security.js +64 -0
  142. package/dist/server/security.js.map +1 -0
  143. package/dist/server/skills.d.ts +4 -0
  144. package/dist/server/skills.js +123 -0
  145. package/dist/server/skills.js.map +1 -0
  146. package/dist/server/store.d.ts +25 -0
  147. package/dist/server/store.js +89 -0
  148. package/dist/server/store.js.map +1 -0
  149. package/dist/server/templates/roblox-coin-run.d.ts +2 -0
  150. package/dist/server/templates/roblox-coin-run.js +326 -0
  151. package/dist/server/templates/roblox-coin-run.js.map +1 -0
  152. package/dist/server/templates/roblox-skills.json +55 -0
  153. package/dist/server/templates/yandex-sdk.d.ts +3 -0
  154. package/dist/server/templates/yandex-sdk.js +177 -0
  155. package/dist/server/templates/yandex-sdk.js.map +1 -0
  156. package/dist/server/updates.d.ts +28 -0
  157. package/dist/server/updates.js +99 -0
  158. package/dist/server/updates.js.map +1 -0
  159. package/dist/server/video-media.d.ts +47 -0
  160. package/dist/server/video-media.js +224 -0
  161. package/dist/server/video-media.js.map +1 -0
  162. package/dist/server/video-routes.d.ts +7 -0
  163. package/dist/server/video-routes.js +91 -0
  164. package/dist/server/video-routes.js.map +1 -0
  165. package/dist/server/video.d.ts +214 -0
  166. package/dist/server/video.js +301 -0
  167. package/dist/server/video.js.map +1 -0
  168. package/dist/server/web-build.d.ts +16 -0
  169. package/dist/server/web-build.js +232 -0
  170. package/dist/server/web-build.js.map +1 -0
  171. package/dist/server/workflow.d.ts +221 -0
  172. package/dist/server/workflow.js +826 -0
  173. package/dist/server/workflow.js.map +1 -0
  174. package/dist/server/yandex-config.d.ts +33 -0
  175. package/dist/server/yandex-config.js +12 -0
  176. package/dist/server/yandex-config.js.map +1 -0
  177. package/dist/server/yandex-godot.d.ts +5 -0
  178. package/dist/server/yandex-godot.js +107 -0
  179. package/dist/server/yandex-godot.js.map +1 -0
  180. package/dist/shared/aspects.d.ts +48 -0
  181. package/dist/shared/aspects.js +22 -0
  182. package/dist/shared/aspects.js.map +1 -0
  183. package/dist/shared/assistant.d.ts +7 -0
  184. package/dist/shared/assistant.js +36 -0
  185. package/dist/shared/assistant.js.map +1 -0
  186. package/dist/shared/chat.d.ts +46 -0
  187. package/dist/shared/chat.js +2 -0
  188. package/dist/shared/chat.js.map +1 -0
  189. package/dist/shared/settings.d.ts +11 -0
  190. package/dist/shared/settings.js +25 -0
  191. package/dist/shared/settings.js.map +1 -0
  192. package/dist/shared/types.d.ts +510 -0
  193. package/dist/shared/types.js +2 -0
  194. package/dist/shared/types.js.map +1 -0
  195. package/dist/web/apple-touch-icon.png +0 -0
  196. package/dist/web/assets/AgentActivity-sYJeY5Yf.js +9 -0
  197. package/dist/web/assets/ArrowRight.es-9w41WqD8.js +1 -0
  198. package/dist/web/assets/ArrowUpRight.es-BmIrpjWN.js +1 -0
  199. package/dist/web/assets/AssetMediaPreview-D2cd6oQO.js +1 -0
  200. package/dist/web/assets/AssetSelectionWizard-a43_6NO7.js +1 -0
  201. package/dist/web/assets/CaretDown.es-Cq33NYip.js +1 -0
  202. package/dist/web/assets/CheckCircle.es-D5B3hdvz.js +1 -0
  203. package/dist/web/assets/CoreAccount-D4ZC8CWv.js +1 -0
  204. package/dist/web/assets/DownloadSimple.es-DwOWFbs2.js +1 -0
  205. package/dist/web/assets/FileText.es-D83IIiOG.js +1 -0
  206. package/dist/web/assets/ImageSquare.es-DxCFt99_.js +1 -0
  207. package/dist/web/assets/KidsApp-BXWaALoK.css +1 -0
  208. package/dist/web/assets/KidsApp-D-8t3HPy.js +2 -0
  209. package/dist/web/assets/MagnifyingGlass.es-rkdOU2rH.js +1 -0
  210. package/dist/web/assets/PaperPlaneRight.es-U0twumpP.js +1 -0
  211. package/dist/web/assets/PencilSimple.es-JVHwRvGx.js +1 -0
  212. package/dist/web/assets/PlainChat-BC4K_tmj.css +1 -0
  213. package/dist/web/assets/PlainChat-CMxYwKko.js +1 -0
  214. package/dist/web/assets/Sparkle.es-CmGpvnNm.js +1 -0
  215. package/dist/web/assets/Square.es-BjNN9Ypc.js +1 -0
  216. package/dist/web/assets/UploadSimple.es-DxIuxpCC.js +1 -0
  217. package/dist/web/assets/WarningCircle.es-D63H8cL9.js +1 -0
  218. package/dist/web/assets/WorkflowJournal-Did0sRta.js +1 -0
  219. package/dist/web/assets/YandexConfigFields-DDL22Bfw.js +1 -0
  220. package/dist/web/assets/YandexGamesView-R64RSJRL.js +2 -0
  221. package/dist/web/assets/bg-build-DIalyCDw.svg +3 -0
  222. package/dist/web/assets/bg-idea-DHcE-Dhs.svg +3 -0
  223. package/dist/web/assets/bg-materials-UDVFaueI.svg +3 -0
  224. package/dist/web/assets/bg-plan-DpfwIsqJ.svg +3 -0
  225. package/dist/web/assets/bg-play-CNc1QZFK.svg +3 -0
  226. package/dist/web/assets/dist-7vx7MTaJ.js +1 -0
  227. package/dist/web/assets/index-Izzbh11U.css +1 -0
  228. package/dist/web/assets/index-sNciOZYG.js +13 -0
  229. package/dist/web/assets/jsx-runtime-CdVFa8IP.js +1 -0
  230. package/dist/web/assets/manrope-cyrillic-wght-normal-Dvxsihut.woff2 +0 -0
  231. package/dist/web/assets/manrope-greek-wght-normal-DL7QRZyv.woff2 +0 -0
  232. package/dist/web/assets/manrope-latin-ext-wght-normal-Ch3YOpNY.woff2 +0 -0
  233. package/dist/web/assets/manrope-latin-wght-normal-DHIcAJRg.woff2 +0 -0
  234. package/dist/web/assets/manrope-vietnamese-wght-normal-usUDDRr7.woff2 +0 -0
  235. package/dist/web/assets/resources-DppXCagL.js +1 -0
  236. package/dist/web/assets/robot-happy-B6YvlU94.svg +24 -0
  237. package/dist/web/assets/robot-hello-CCsQgsS-.svg +24 -0
  238. package/dist/web/assets/robot-listen-Buuz9xII.svg +24 -0
  239. package/dist/web/assets/robot-sad-CRtiNVWs.svg +24 -0
  240. package/dist/web/assets/robot-think-CJ5fDBTm.svg +24 -0
  241. package/dist/web/assets/star-big-CSFDKjws.svg +22 -0
  242. package/dist/web/assets/star-empty-CC-5vioJ.svg +3 -0
  243. package/dist/web/assets/star-next-ztuDjOHU.svg +9 -0
  244. package/dist/web/assets/star-on-l_krO3aV.svg +10 -0
  245. package/dist/web/assets/ui-wHE_yBK0.js +41 -0
  246. package/dist/web/assets/umgt-logo-DAOkTN9u.png +0 -0
  247. package/dist/web/assets/unbounded-cyrillic-wght-normal-C627f1f8.woff2 +0 -0
  248. package/dist/web/assets/unbounded-latin-ext-wght-normal-D6H-S6MW.woff2 +0 -0
  249. package/dist/web/assets/unbounded-latin-wght-normal-D5afBTBx.woff2 +0 -0
  250. package/dist/web/assets/unbounded-vietnamese-wght-normal-rat7GD3o.woff2 +0 -0
  251. package/dist/web/assets/workflow-BhcU9uvG.css +1 -0
  252. package/dist/web/assets/workflow-BrIKN5ER.js +34 -0
  253. package/dist/web/favicon.ico +0 -0
  254. package/dist/web/icons/favicon-16.png +0 -0
  255. package/dist/web/icons/favicon-32.png +0 -0
  256. package/dist/web/icons/icon-192.png +0 -0
  257. package/dist/web/icons/icon-512.png +0 -0
  258. package/dist/web/icons/maskable-192.png +0 -0
  259. package/dist/web/icons/maskable-512.png +0 -0
  260. package/dist/web/index.html +28 -0
  261. package/dist/web/manifest.webmanifest +19 -0
  262. package/dist/web/sw.js +25 -0
  263. package/docs/agent-activity-ui.md +33 -0
  264. package/docs/agent-activity.md +43 -0
  265. package/docs/agent-permissions-ui.md +15 -0
  266. package/docs/api.md +145 -0
  267. package/docs/approval-journal.md +13 -0
  268. package/docs/asset-media.md +27 -0
  269. package/docs/asset-providers.md +34 -0
  270. package/docs/asset-selection.md +31 -0
  271. package/docs/automatic-approvals.md +15 -0
  272. package/docs/background-game-launch.md +17 -0
  273. package/docs/billing-validation.md +23 -0
  274. package/docs/core-isolation.md +37 -0
  275. package/docs/core.md +45 -0
  276. package/docs/generated-text-ui.md +41 -0
  277. package/docs/interactive-interview-ui.md +29 -0
  278. package/docs/interactive-interview.md +52 -0
  279. package/docs/local-codex.md +51 -0
  280. package/docs/local-meshy.md +13 -0
  281. package/docs/markdown.md +11 -0
  282. package/docs/permission-ui.md +11 -0
  283. package/docs/plan.md +47 -0
  284. package/docs/remote-core-deployment.md +47 -0
  285. package/docs/roblox.md +90 -0
  286. package/docs/validation.md +228 -0
  287. package/docs/video-reference.md +35 -0
  288. package/docs/workflow-journal-ui.md +21 -0
  289. package/docs/yandex-foundation.md +27 -0
  290. package/docs/yandex-games.md +45 -0
  291. package/package.json +95 -4
  292. package/scripts/create-roblox-demo.mjs +31 -0
  293. package/scripts/diagnose-roblox-mcp.mjs +45 -0
  294. package/scripts/open-hub-chrome.mjs +60 -0
  295. package/scripts/smoke-agent.mjs +91 -0
@@ -0,0 +1,40 @@
1
+ import { z } from "zod";
2
+ /** Core determines identity from its session; the browser cannot supply an account. */
3
+ export function registerReports(app, core) {
4
+ const params = z.object({ id: z.string().uuid() });
5
+ const message = z
6
+ .object({
7
+ body: z.string().trim().min(1).max(10000),
8
+ attachments: z
9
+ .array(z
10
+ .object({
11
+ name: z.string().min(1).max(150),
12
+ data: z.string().max(2800000),
13
+ })
14
+ .strict())
15
+ .max(3)
16
+ .default([]),
17
+ })
18
+ .strict();
19
+ app.get("/api/v1/reports", async (req) => {
20
+ const query = z
21
+ .object({
22
+ limit: z.coerce.number().int().min(1).max(100).default(50),
23
+ offset: z.coerce.number().int().nonnegative().max(1000000).default(0),
24
+ })
25
+ .strict()
26
+ .parse(req.query);
27
+ return core.request(`/reports?${new URLSearchParams({ limit: String(query.limit), offset: String(query.offset) })}`);
28
+ });
29
+ app.get("/api/v1/reports/:id", async (req) => core.request(`/reports/${params.parse(req.params).id}`));
30
+ app.post("/api/v1/reports", { bodyLimit: 8500000 }, async (req, reply) => reply
31
+ .code(201)
32
+ .send(await core.request("/reports", message
33
+ .extend({
34
+ kind: z.enum(["bug", "wish"]),
35
+ title: z.string().trim().min(1).max(160),
36
+ })
37
+ .parse(req.body))));
38
+ app.post("/api/v1/reports/:id/messages", { bodyLimit: 8500000 }, async (req) => core.request(`/reports/${params.parse(req.params).id}/messages`, message.parse(req.body)));
39
+ }
40
+ //# sourceMappingURL=reports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reports.js","sourceRoot":"","sources":["../../src/server/reports.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { Asset, Project, HubSettings } from '../shared/types.js';
3
+ import type { HubContext } from './app.js';
4
+ import type { StudioConnection } from './roblox-connection.js';
5
+ import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
6
+ export declare const robloxAssetTerms = "https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use";
7
+ export declare function robloxResult(result: CallToolResult): unknown;
8
+ export declare function creatorStoreAssets(result: CallToolResult): Asset[];
9
+ export declare function registerRobloxAssets(app: FastifyInstance, hub: HubContext, connection: StudioConnection, settings: () => HubSettings, projectById: (id: string) => Project, assertIdle: (id: string) => void): {
10
+ search: (p: Project, q: string, type?: string, signal?: AbortSignal) => Promise<{
11
+ assets: Asset[];
12
+ warnings: never[];
13
+ }>;
14
+ details: (p: Project, id: string) => {
15
+ provider: string;
16
+ name: string;
17
+ type: string;
18
+ thumbnail?: string;
19
+ media?: import("../shared/types.js").AssetMedia[];
20
+ mediaTotal?: number;
21
+ mediaGalleryUrl?: string;
22
+ previewVideoUrl?: string;
23
+ sourceUrl: string;
24
+ license: string;
25
+ author?: string;
26
+ tags: string[];
27
+ downloadUrl?: string;
28
+ formats?: string[];
29
+ importMode: 'automatic' | 'manual';
30
+ previewAudioUrl?: string;
31
+ licenseUrl?: string;
32
+ attribution?: string;
33
+ provenanceUrl?: string;
34
+ verifiedAt?: string;
35
+ id: string;
36
+ };
37
+ import: (p: Project, id: string, signal?: AbortSignal) => Promise<Asset & {
38
+ sourceId?: string;
39
+ directory: string;
40
+ }>;
41
+ };
@@ -0,0 +1,131 @@
1
+ import { mkdir, readFile, writeFile, lstat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ export const robloxAssetTerms = 'https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use';
5
+ export function robloxResult(result) {
6
+ if (result.isError)
7
+ throw new Error(result.content.filter(b => b.type === 'text').map(b => b.text).join('\n'));
8
+ for (const b of result.content)
9
+ if (b.type === 'text') {
10
+ try {
11
+ return JSON.parse(b.text);
12
+ }
13
+ catch { }
14
+ }
15
+ throw new Error('Studio returned no asset metadata');
16
+ }
17
+ export function creatorStoreAssets(result) {
18
+ const data = z.object({ results: z.array(z.object({ assetId: z.string().regex(/^\d+$/), assetType: z.enum(['Model', 'Audio']), name: z.string(), creatorName: z.string().optional(), isFree: z.boolean(), source: z.string(), creatorStoreUrl: z.string().url() })) }).parse(robloxResult(result));
19
+ return data.results.filter(x => x.isFree && x.source === 'creator_store').map(x => ({ id: x.assetId, provider: 'roblox', name: x.name, type: x.assetType === 'Audio' ? 'audio' : 'models', sourceUrl: `https://create.roblox.com/store/asset/${x.assetId}`, author: x.creatorName ?? 'Creator Store', license: 'Roblox Creator Store', licenseUrl: robloxAssetTerms, provenanceUrl: `https://create.roblox.com/store/asset/${x.assetId}`, verifiedAt: new Date().toISOString(), formats: [x.assetType === 'Audio' ? 'Roblox Audio ID' : 'Roblox Model'], tags: ['roblox', 'creator-store', 'free'], importMode: 'automatic' }));
20
+ }
21
+ export function registerRobloxAssets(app, hub, connection, settings, projectById, assertIdle) {
22
+ const selected = new Map();
23
+ const pending = new Set();
24
+ const project = (id) => { const p = projectById(id); if (p.engine !== 'roblox')
25
+ throw Object.assign(new Error('Creator Store requires a Roblox project'), { statusCode: 422 }); return p; };
26
+ const search = async (p, q, type, signal) => {
27
+ signal?.throwIfAborted();
28
+ q = z.string().max(200).parse(q);
29
+ await connection.connect(settings(), signal);
30
+ const kinds = type === 'audio' ? ['Audio'] : type === 'models' ? ['Model'] : type && type !== 'all' ? [] : ['Model', 'Audio'];
31
+ const assets = [];
32
+ for (const assetType of kinds) {
33
+ const found = creatorStoreAssets(await connection.call(p.id, 'search_asset', { query: q, assetType, scope: 'creator_store', priceFilter: 'free', maxResults: 8 }, signal));
34
+ assets.push(...found);
35
+ for (const a of found)
36
+ selected.set(p.id + ':' + a.id, a);
37
+ }
38
+ if (selected.size > 1000) {
39
+ const keep = assets.map(a => [p.id + ':' + a.id, a]);
40
+ selected.clear();
41
+ for (const [key, a] of keep)
42
+ selected.set(key, a);
43
+ }
44
+ return { assets, warnings: [] };
45
+ };
46
+ const importSelected = async (p, id, signal) => {
47
+ signal?.throwIfAborted();
48
+ const existing = hub.store.project(p.id).list('assets').find(a => a.provider === 'roblox' && (a.sourceId === id || a.id === id));
49
+ if (existing)
50
+ return existing;
51
+ const asset = selected.get(p.id + ':' + id);
52
+ if (!asset)
53
+ throw Object.assign(new Error('Find this free asset in Creator Store before importing'), { statusCode: 409 });
54
+ if (pending.has(p.id))
55
+ throw Object.assign(new Error('Asset import already running'), { statusCode: 409 });
56
+ pending.add(p.id);
57
+ try {
58
+ await connection.connect(settings(), signal);
59
+ if ((await connection.playing(p.id, undefined, signal)).playing)
60
+ throw Object.assign(new Error('Остановите игру перед добавлением ассетов'), { statusCode: 409 });
61
+ const folder = 'LMHubAssets', name = 'Asset' + id;
62
+ const setup = await connection.call(p.id, 'execute_luau', { datamodel_type: 'Edit', code: `local r=game:GetService("ReplicatedStorage"); if not r:FindFirstChild("${folder}") then local f=Instance.new("Folder");f.Name="${folder}";f.Parent=r end; return "ready"` }, signal);
63
+ if (setup.isError)
64
+ throw new Error('Cannot create asset folder');
65
+ const inserted = await connection.call(p.id, 'insert_asset', { assetId: id, assetName: name, assetType: asset.type === 'audio' ? 'Audio' : 'Model', parentPath: 'game.ReplicatedStorage.' + folder }, signal);
66
+ if (inserted.isError)
67
+ throw new Error(JSON.stringify(inserted.content));
68
+ const snapshot = robloxResult(await connection.call(p.id, 'execute_luau', { datamodel_type: 'Edit', code: `
69
+ local model=game:GetService("ReplicatedStorage").${folder}:FindFirstChild("${name}");assert(model,"Asset was not inserted"); local removed=0
70
+ for _,d in model:GetDescendants() do if d:IsA("LuaSourceContainer") then d:Destroy();removed+=1 end end
71
+ local node={['$className']='Model'}; local count=0
72
+ local function vector(v) return {v.X,v.Y,v.Z} end
73
+ local function content(v) return tostring(v) end
74
+ if model:IsA('Sound') then node={['$className']='Sound',['$properties']={SoundId=model.SoundId,Volume=0.3,Looped=false}}
75
+ else
76
+ for _,d in model:GetDescendants() do if d:IsA('BasePart') then
77
+ count+=1;assert(count<=500,'Model too large');d.Anchored=true;d.CanCollide=false;d.CanTouch=false
78
+ local props={Anchored=true,CanCollide=false,CanTouch=false,Size=vector(d.Size),CFrame={d.CFrame:GetComponents()},Color={d.Color.R,d.Color.G,d.Color.B},Material=d.Material.Value,Transparency=d.Transparency}
79
+ if d:IsA('MeshPart') then props.MeshId=content(d.MeshId);props.TextureID=content(d.TextureID) end
80
+ local child={['$className']=d:IsA('MeshPart') and 'MeshPart' or 'Part',['$properties']=props}
81
+ if d:IsA('Part') then props.Shape=d.Shape.Value end
82
+ local mesh=d:FindFirstChildOfClass('SpecialMesh');if mesh then child.Mesh={['$className']='SpecialMesh',['$properties']={MeshId=content(mesh.MeshId),TextureId=content(mesh.TextureId),MeshType=mesh.MeshType.Value,Scale=vector(mesh.Scale),Offset=vector(mesh.Offset)}} end
83
+ node['Part'..count]=child
84
+ end end
85
+ end
86
+ return game:GetService('HttpService'):JSONEncode({node=node,parts=count,removedScripts=removed})` }, signal));
87
+ const parsed = z.object({ node: z.record(z.string(), z.unknown()), parts: z.number(), removedScripts: z.number() }).parse(snapshot);
88
+ const manifestPath = join(p.path, 'default.project.json'), manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
89
+ manifest.tree.ReplicatedStorage ??= { $className: 'ReplicatedStorage' };
90
+ manifest.tree.ReplicatedStorage.LMHubAssets ??= { $className: 'Folder' };
91
+ manifest.tree.ReplicatedStorage.LMHubAssets[name] = parsed.node;
92
+ const directory = `assets/roblox-${id}`;
93
+ for (const path of [join(p.path, 'assets'), join(p.path, directory)]) {
94
+ try {
95
+ if ((await lstat(path)).isSymbolicLink())
96
+ throw new Error('Asset path is a symlink');
97
+ }
98
+ catch (e) {
99
+ if (e.code !== 'ENOENT')
100
+ throw e;
101
+ }
102
+ }
103
+ await mkdir(join(p.path, directory), { recursive: true });
104
+ const record = { ...asset, id: 'roblox-' + id, sourceId: id, importedAt: new Date().toISOString(), directory, files: ['asset.json', 'CREDITS.txt'], nativePath: 'ReplicatedStorage.' + folder + '.' + name, removedScripts: parsed.removedScripts, parts: parsed.parts };
105
+ await writeFile(join(p.path, directory, 'asset.json'), JSON.stringify({ record, scene: parsed.node }, null, 2));
106
+ await writeFile(join(p.path, directory, 'CREDITS.txt'), `${asset.name}\nAuthor: ${asset.author}\nSource: ${asset.sourceUrl}\nTerms: ${robloxAssetTerms}\nFree to use in Roblox under Creator Store terms; not CC0.\nImported scripts removed; decorative collisions disabled.\n`);
107
+ await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
108
+ hub.store.project(p.id).put('assets', record);
109
+ hub.events.publish('asset.imported', record, p.id);
110
+ return record;
111
+ }
112
+ finally {
113
+ pending.delete(p.id);
114
+ }
115
+ };
116
+ app.get('/api/v1/projects/:id/roblox/assets', async (request) => search(project(request.params.id), request.query.q ?? '', request.query.type));
117
+ app.post('/api/v1/projects/:id/roblox/assets', async (request) => {
118
+ const p = project(request.params.id);
119
+ assertIdle(p.id);
120
+ const { id } = z.object({ id: z.string().regex(/^\d+$/) }).strict().parse(request.body);
121
+ return importSelected(p, id);
122
+ });
123
+ const details = (p, id) => {
124
+ const known = selected.get(p.id + ':' + id) ?? hub.store.project(p.id).list('assets').find(a => a.provider === 'roblox' && (a.sourceId === id || a.id === id));
125
+ if (!known)
126
+ throw Object.assign(new Error('Search Creator Store before requesting this asset'), { statusCode: 404 });
127
+ return { ...known, id: known.sourceId ?? known.id };
128
+ };
129
+ return { search, details, import: importSelected };
130
+ }
131
+ //# sourceMappingURL=roblox-assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roblox-assets.js","sourceRoot":"","sources":["../../src/server/roblox-assets.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import type { Tool, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import type { HubSettings, Diagnostic } from '../shared/types.js';
3
+ import type { AgentMcpServer } from './agents/types.js';
4
+ export declare const markerName = "LMGameHubProject";
5
+ export interface StudioInstance {
6
+ id: string;
7
+ name: string;
8
+ placeId?: string;
9
+ }
10
+ export declare class StudioConnectionError extends Error {
11
+ reason: 'permission_denied' | 'mcp_missing' | 'connection_failed' | 'protocol_error' | 'target_missing' | 'target_ambiguous';
12
+ constructor(reason: 'permission_denied' | 'mcp_missing' | 'connection_failed' | 'protocol_error' | 'target_missing' | 'target_ambiguous', message: string);
13
+ }
14
+ export declare function discoverStudioMcp(settings: HubSettings): Promise<AgentMcpServer | undefined>;
15
+ export declare function parseStudios(result: unknown): StudioInstance[];
16
+ export declare function markerMatches(result: unknown, projectId: string): boolean;
17
+ /** Studio's built-in GetStudioState tool returns this explicit mode line. */
18
+ export declare function studioPlayState(result: CallToolResult): boolean;
19
+ export declare class StudioConnection {
20
+ private options;
21
+ private client?;
22
+ private transport?;
23
+ private opening?;
24
+ private tools;
25
+ private signature;
26
+ private controller;
27
+ private toolsChanged;
28
+ constructor(options?: {
29
+ discoveryTimeoutMs?: number;
30
+ preferredStudio?: (projectId: string) => string | undefined;
31
+ });
32
+ connect(settings: HubSettings, signal?: AbortSignal): Promise<void>;
33
+ private open;
34
+ private refreshTools;
35
+ raw(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<CallToolResult>;
36
+ instances(signal?: AbortSignal): Promise<StudioInstance[]>;
37
+ owns(studioId: string, projectId: string, signal?: AbortSignal): Promise<boolean>;
38
+ target(projectId: string, signal?: AbortSignal): Promise<StudioInstance>;
39
+ scopedTools(): Tool[];
40
+ call(projectId: string, name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<CallToolResult>;
41
+ private playTool;
42
+ playing(projectId: string, studioId?: string, signal?: AbortSignal): Promise<{
43
+ playing: boolean;
44
+ studioId: string;
45
+ }>;
46
+ setPlaying(projectId: string, playing: boolean, signal?: AbortSignal, timeoutMs?: number): Promise<{
47
+ playing: boolean;
48
+ studioId: string;
49
+ }>;
50
+ close(): Promise<void>;
51
+ }
52
+ export declare function diagnoseStudioMcp(settings: HubSettings, signal?: AbortSignal): Promise<Diagnostic & {
53
+ reason?: string;
54
+ }>;
@@ -0,0 +1,331 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
+ import { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { stat } from 'node:fs/promises';
5
+ import { setTimeout as delay } from 'node:timers/promises';
6
+ import { join } from 'node:path';
7
+ import { homedir } from 'node:os';
8
+ import { detectRoblox } from './roblox.js';
9
+ export const markerName = 'LMGameHubProject';
10
+ export class StudioConnectionError extends Error {
11
+ reason;
12
+ constructor(reason, message) {
13
+ super(message);
14
+ this.reason = reason;
15
+ }
16
+ }
17
+ export async function discoverStudioMcp(settings) {
18
+ const explicit = settings.robloxMcpPath?.trim();
19
+ if (explicit)
20
+ return await stat(explicit).then(x => x.isFile() ? (process.platform === 'win32' && /\.(bat|cmd)$/i.test(explicit) ? { command: 'cmd.exe', args: ['/d', '/s', '/c', `"${explicit}"`], env: {} } : { command: explicit, args: [], env: {} }) : undefined).catch(() => undefined);
21
+ if (process.platform === 'win32') {
22
+ const launcher = join(process.env.LOCALAPPDATA ?? join(homedir(), 'AppData/Local'), 'Roblox/mcp.bat');
23
+ return await stat(launcher).then(x => x.isFile() ? { command: 'cmd.exe', args: ['/d', '/s', '/c', `"${launcher}"`], env: {} } : undefined).catch(() => undefined);
24
+ }
25
+ const studio = await detectRoblox(settings.robloxStudioPath);
26
+ return studio.mcpExecutable ? { command: studio.mcpExecutable, args: [], env: {} } : undefined;
27
+ }
28
+ function values(value) {
29
+ if (Array.isArray(value))
30
+ return value.flatMap(values);
31
+ if (value && typeof value === 'object') {
32
+ const object = value;
33
+ return [object, ...Object.values(object).flatMap(values)];
34
+ }
35
+ if (typeof value === 'string') {
36
+ try {
37
+ return [value, ...values(JSON.parse(value))];
38
+ }
39
+ catch {
40
+ return [value];
41
+ }
42
+ }
43
+ return [];
44
+ }
45
+ export function parseStudios(result) {
46
+ const studios = new Map();
47
+ for (const value of values(result)) {
48
+ if (!value || typeof value !== 'object' || Array.isArray(value))
49
+ continue;
50
+ const record = value;
51
+ const id = record.studio_id ?? record.studioId ?? record.id;
52
+ const name = record.name ?? record.studio_name ?? record.place_name;
53
+ if (typeof id === 'string' && id.length <= 200 && id.trim() && typeof name === 'string')
54
+ studios.set(id, { id, name, placeId: String(record.place_id ?? record.placeId ?? '') || undefined });
55
+ }
56
+ // Some Studio builds return a prose list rather than structured JSON.
57
+ if (!studios.size)
58
+ for (const value of values(result))
59
+ if (typeof value === 'string') {
60
+ const pattern = /(?:studio[_ ]id|studioId)\s*[:=]\s*[`"']?([a-zA-Z0-9_-]{1,200})[`"']?/gi;
61
+ for (const match of value.matchAll(pattern))
62
+ studios.set(match[1], { id: match[1], name: match[1] });
63
+ }
64
+ return [...studios.values()];
65
+ }
66
+ export function markerMatches(result, projectId) {
67
+ if (result?.isError)
68
+ return false;
69
+ // Exact leaf values only. A partial UUID, script text or error mentioning the
70
+ // project cannot establish ownership.
71
+ return values(result).some(value => value === projectId || value === JSON.stringify(projectId));
72
+ }
73
+ /** Studio's built-in GetStudioState tool returns this explicit mode line. */
74
+ export function studioPlayState(result) {
75
+ if (result.isError)
76
+ throw new StudioConnectionError('connection_failed', 'get_studio_state вернул ошибку. Состояние Play не подтверждено.');
77
+ const modes = new Set();
78
+ for (const block of result.content)
79
+ if (block.type === 'text')
80
+ for (const match of block.text.matchAll(/^\s*-?\s*Current Studio Mode:\s*(Play|Edit)\s*$/gmi))
81
+ modes.add(match[1].toLowerCase() === 'play');
82
+ if (modes.size !== 1)
83
+ throw new StudioConnectionError('protocol_error', 'Неизвестный ответ get_studio_state: ожидается Current Studio Mode: Play или Edit. Состояние игры не подтверждено; проверьте Studio MCP.');
84
+ return [...modes][0];
85
+ }
86
+ export class StudioConnection {
87
+ options;
88
+ client;
89
+ transport;
90
+ opening;
91
+ tools = [];
92
+ signature = '';
93
+ controller = new AbortController();
94
+ toolsChanged = true;
95
+ constructor(options = {}) {
96
+ this.options = options;
97
+ }
98
+ async connect(settings, signal) {
99
+ signal?.throwIfAborted();
100
+ const config = await discoverStudioMcp(settings);
101
+ if (!config)
102
+ throw new StudioConnectionError('mcp_missing', 'StudioMCP не найден. Обновите Studio; путь к MCP можно переопределить в дополнительных настройках.');
103
+ const signature = JSON.stringify(config);
104
+ if (this.client && this.signature === signature) {
105
+ await this.refreshTools(signal);
106
+ return;
107
+ }
108
+ if (this.opening) {
109
+ await this.opening;
110
+ return this.connect(settings, signal);
111
+ }
112
+ this.opening = this.open(config, signature, signal);
113
+ try {
114
+ await this.opening;
115
+ }
116
+ finally {
117
+ this.opening = undefined;
118
+ }
119
+ }
120
+ async open(config, signature, signal) {
121
+ await this.close();
122
+ this.controller = new AbortController();
123
+ signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
124
+ const transport = new StdioClientTransport({ ...config, stderr: 'pipe' }), client = new Client({ name: 'lm-game-hub-studio', version: '0.1.0' });
125
+ client.setNotificationHandler(ToolListChangedNotificationSchema, () => { if (this.client === client)
126
+ this.toolsChanged = true; });
127
+ let denied = false;
128
+ transport.stderr?.on('data', chunk => { if (/operation not permitted|permission denied|access is denied|os error (?:1|13)/i.test(String(chunk)))
129
+ denied = true; });
130
+ // Close the spawned proxy on cancellation, including during initialize.
131
+ const abort = () => { void transport.close(); };
132
+ signal?.addEventListener('abort', abort, { once: true });
133
+ try {
134
+ await client.connect(transport, { timeout: 12_000, signal });
135
+ // StudioMCP starts before a Studio window has registered. Its first list
136
+ // can be empty or contain only list_roblox_studios; execute_luau arrives
137
+ // with tools/list_changed. Keep this proxy alive so Studio can join it.
138
+ this.client = client;
139
+ this.transport = transport;
140
+ this.signature = signature;
141
+ await this.refreshTools(signal);
142
+ client.onclose = () => { if (this.client === client) {
143
+ this.client = undefined;
144
+ this.tools = [];
145
+ } };
146
+ }
147
+ catch (error) {
148
+ if (this.client === client) {
149
+ this.client = undefined;
150
+ this.transport = undefined;
151
+ this.tools = [];
152
+ this.signature = '';
153
+ }
154
+ await client.close().catch(() => { });
155
+ await transport.close().catch(() => { });
156
+ signal?.throwIfAborted();
157
+ if (denied)
158
+ throw new StudioConnectionError('permission_denied', 'Среда запрещает локальные сокеты StudioMCP (Operation not permitted). Запустите Hub в обычной локальной сессии с доступом к loopback; Hub не меняет разрешения CLI.');
159
+ if (error instanceof StudioConnectionError)
160
+ throw error;
161
+ throw new StudioConnectionError('connection_failed', 'StudioMCP не отвечает. Войдите в Studio, откройте place и включите Assistant → … → Manage MCP Servers → Enable Studio as MCP server. Затем нажмите «Проверить подключение».');
162
+ }
163
+ finally {
164
+ signal?.removeEventListener('abort', abort);
165
+ }
166
+ }
167
+ async refreshTools(signal) {
168
+ const client = this.client;
169
+ if (!client)
170
+ throw new StudioConnectionError('connection_failed', 'Соединение со Studio закрыто. Повторите проверку.');
171
+ signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
172
+ const tools = [], cursors = new Set();
173
+ let cursor;
174
+ this.toolsChanged = false;
175
+ do {
176
+ const page = await client.listTools(cursor ? { cursor } : {}, { timeout: 12_000, signal });
177
+ tools.push(...page.tools);
178
+ cursor = page.nextCursor;
179
+ if (tools.length > 300 || cursor && cursors.has(cursor))
180
+ throw new StudioConnectionError('protocol_error', 'StudioMCP вернул некорректный список инструментов.');
181
+ if (cursor)
182
+ cursors.add(cursor);
183
+ } while (cursor);
184
+ if (this.client === client)
185
+ this.tools = tools;
186
+ }
187
+ async raw(name, args, signal) {
188
+ if (!this.client)
189
+ throw new StudioConnectionError('connection_failed', 'Соединение со Studio закрыто. Повторите проверку.');
190
+ return await this.client.callTool({ name, arguments: args }, undefined, { timeout: name === 'execute_luau' ? 90_000 : 30_000, signal });
191
+ }
192
+ async instances(signal) {
193
+ if (this.toolsChanged)
194
+ await this.refreshTools(signal);
195
+ if (!this.tools.some(tool => tool.name === 'list_roblox_studios'))
196
+ return [];
197
+ const result = await this.raw('list_roblox_studios', {}, signal);
198
+ if (result.isError)
199
+ throw new StudioConnectionError('connection_failed', 'Studio MCP не вернула подключённые окна. Включите Assistant → … → Manage MCP Servers → Enable Studio as MCP server и повторите проверку.');
200
+ return parseStudios(result);
201
+ }
202
+ async owns(studioId, projectId, signal) {
203
+ const tool = this.tools.find(x => x.name === 'execute_luau');
204
+ const properties = tool?.inputSchema.properties ?? {};
205
+ const codeKey = ['code', 'luau_code', 'script'].find(key => properties[key]?.type === 'string');
206
+ const modelSchema = properties.datamodel_type;
207
+ if (!codeKey || properties.studio_id?.type !== 'string' || modelSchema?.type !== 'string' || modelSchema.enum && !modelSchema.enum.includes('Edit') || tool?.inputSchema.required?.some(key => ![codeKey, 'studio_id', 'datamodel_type'].includes(key)))
208
+ throw new StudioConnectionError('protocol_error', 'Схема execute_luau не позволяет проверить идентификатор проекта в указанном окне Studio. Подробности можно получить через scripts/diagnose-roblox-mcp.mjs.');
209
+ const code = `local r = game:GetService("ReplicatedStorage"):FindFirstChild("${markerName}"); return if r and r:IsA("StringValue") then r.Value else ""`;
210
+ let result = await this.raw('execute_luau', { studio_id: studioId, datamodel_type: 'Edit', [codeKey]: code }, signal);
211
+ // Play exposes Server/Client instead of Edit. The replicated project
212
+ // marker can still be read without modifying the running game.
213
+ if (result.isError && (!modelSchema.enum || modelSchema.enum.includes('Server'))) {
214
+ this.playTool('get_studio_state');
215
+ const playing = studioPlayState(await this.raw('get_studio_state', { studio_id: studioId }, signal));
216
+ if (playing)
217
+ result = await this.raw('execute_luau', { studio_id: studioId, datamodel_type: 'Server', [codeKey]: code }, signal);
218
+ }
219
+ if (result.isError)
220
+ throw new StudioConnectionError('protocol_error', 'Studio MCP не смог прочитать идентификатор проекта в Edit. Проверка остановлена; существующая сцена не будет открыта повторно.');
221
+ return markerMatches(result, projectId);
222
+ }
223
+ async target(projectId, signal) {
224
+ signal = signal ? AbortSignal.any([signal, this.controller.signal]) : this.controller.signal;
225
+ const matches = [];
226
+ const deadline = Date.now() + (this.options.discoveryTimeoutMs ?? 5000);
227
+ let instances = [];
228
+ do {
229
+ await this.refreshTools(signal);
230
+ instances = await this.instances(signal);
231
+ // A registered window can announce its tools after list_studios returns.
232
+ if (instances.length) {
233
+ await this.refreshTools(signal);
234
+ if (this.tools.some(tool => tool.name === 'execute_luau'))
235
+ break;
236
+ }
237
+ if (Date.now() >= deadline)
238
+ break;
239
+ await delay(Math.min(250, deadline - Date.now()), undefined, { signal });
240
+ } while (true);
241
+ if (!this.tools.length)
242
+ throw new StudioConnectionError('connection_failed', 'Studio MCP запущен, но Studio не передала инструменты. В открытой сцене включите Assistant → … → Manage MCP Servers → Enable Studio as MCP server, затем нажмите «Проверить подключение».');
243
+ if (!this.tools.some(tool => tool.name === 'list_roblox_studios'))
244
+ throw new StudioConnectionError('protocol_error', 'StudioMCP не предоставил list_roblox_studios для безопасного выбора окна. Проверьте подключение через scripts/diagnose-roblox-mcp.mjs.');
245
+ if (!instances.length)
246
+ throw new StudioConnectionError('connection_failed', 'Studio MCP запущен, но ни одно окно Studio не подключено. В открытой сцене включите Assistant → … → Manage MCP Servers → Enable Studio as MCP server, затем нажмите «Проверить подключение».');
247
+ if (!this.tools.some(tool => tool.name === 'execute_luau'))
248
+ throw new StudioConnectionError('protocol_error', 'Окно Studio подключено, но MCP ещё не предоставил execute_luau для проверки проекта. Повторите проверку; подробности доступны через scripts/diagnose-roblox-mcp.mjs.');
249
+ if (instances.length > 20)
250
+ throw new StudioConnectionError('target_ambiguous', 'Слишком много окон Studio для безопасного выбора. Закройте лишние окна.');
251
+ for (const studio of instances)
252
+ if (await this.owns(studio.id, projectId, signal))
253
+ matches.push(studio);
254
+ const preferred = this.options.preferredStudio?.(projectId), selected = matches.find(studio => studio.id === preferred);
255
+ if (selected)
256
+ return selected;
257
+ if (matches.length > 1)
258
+ throw new StudioConnectionError('target_ambiguous', 'Открыто несколько копий place этого проекта. Закройте лишнюю копию и повторите проверку.');
259
+ if (!matches.length)
260
+ throw new StudioConnectionError('target_missing', 'Place этого проекта не подключён. Нажмите «Настроить автоматически»: Hub откроет сборку с идентификатором проекта. Включите Studio MCP, если это первый запуск.');
261
+ return matches[0];
262
+ }
263
+ scopedTools() { return this.tools.filter(tool => !['upload_image', 'subagent'].includes(tool.name) && (tool.name === 'list_roblox_studios' || tool.inputSchema.properties?.studio_id?.type === 'string')); }
264
+ async call(projectId, name, args, signal) {
265
+ const target = await this.target(projectId, signal);
266
+ if (!this.scopedTools().some(tool => tool.name === name))
267
+ throw new Error('Инструмент недоступен в подключении проекта');
268
+ if (name === 'list_roblox_studios')
269
+ return { content: [{ type: 'text', text: JSON.stringify({ studios: [{ studio_id: target.id, name: target.name, place_id: target.placeId }] }) }] };
270
+ if (args.studio_id !== undefined && args.studio_id !== target.id)
271
+ throw new Error('Запрещено обращаться к другому окну Studio');
272
+ return this.raw(name, { ...args, studio_id: target.id }, signal);
273
+ }
274
+ playTool(name) {
275
+ const tool = this.scopedTools().find(tool => tool.name === name), properties = tool?.inputSchema.properties ?? {};
276
+ const keys = name === 'start_stop_play' ? ['studio_id', 'is_start'] : ['studio_id'];
277
+ if (!tool || properties.studio_id?.type !== 'string' || name === 'start_stop_play' && properties.is_start?.type !== 'boolean' || tool.inputSchema.required?.some(key => !keys.includes(key)))
278
+ throw new StudioConnectionError('protocol_error', `Схема ${name} не поддерживает безопасное управление Play. Ожидается studio_id${name === 'start_stop_play' ? ' и boolean is_start' : ''}; проверьте схемы через scripts/diagnose-roblox-mcp.mjs.`);
279
+ }
280
+ async playing(projectId, studioId, signal) {
281
+ const target = await this.target(projectId, signal);
282
+ if (studioId !== undefined && target.id !== studioId)
283
+ throw new StudioConnectionError('target_missing', 'Окно Studio изменилось во время запуска игры. Команда остановлена; повторите проверку подключения.');
284
+ this.playTool('get_studio_state');
285
+ return { playing: studioPlayState(await this.raw('get_studio_state', { studio_id: target.id }, signal)), studioId: target.id };
286
+ }
287
+ async setPlaying(projectId, playing, signal, timeoutMs = 15_000) {
288
+ const before = await this.playing(projectId, undefined, signal);
289
+ this.playTool('start_stop_play');
290
+ if (before.playing === playing)
291
+ return before;
292
+ // Revalidate ownership immediately before the mutation and pin subsequent
293
+ // confirmation to that same window, even if another project copy appears.
294
+ const target = await this.target(projectId, signal);
295
+ if (target.id !== before.studioId)
296
+ throw new StudioConnectionError('target_missing', 'Окно Studio изменилось перед командой Play. Повторите проверку подключения.');
297
+ this.playTool('start_stop_play');
298
+ const result = await this.raw('start_stop_play', { studio_id: target.id, is_start: playing }, signal);
299
+ if (result.isError) {
300
+ const detail = result.content.filter(block => block.type === 'text').map(block => block.text).join('\n').slice(0, 2000);
301
+ throw new StudioConnectionError('connection_failed', `Studio не смогла ${playing ? 'запустить' : 'остановить'} Play${detail ? ': ' + detail : '.'}`);
302
+ }
303
+ const deadline = Date.now() + timeoutMs;
304
+ do {
305
+ const state = await this.playing(projectId, before.studioId, signal);
306
+ if (state.playing === playing)
307
+ return state;
308
+ if (Date.now() >= deadline)
309
+ break;
310
+ await delay(Math.min(250, deadline - Date.now()), undefined, { signal });
311
+ } while (true);
312
+ throw new StudioConnectionError('connection_failed', `Studio не подтвердила ${playing ? 'запуск' : 'остановку'} Play. Проверьте состояние игры в Studio и повторите команду.`);
313
+ }
314
+ async close() { this.controller.abort(); const client = this.client, transport = this.transport; this.client = undefined; this.transport = undefined; this.tools = []; this.signature = ''; await client?.close().catch(() => { }); await transport?.close().catch(() => { }); }
315
+ }
316
+ export async function diagnoseStudioMcp(settings, signal) {
317
+ const connection = new StudioConnection();
318
+ try {
319
+ await connection.connect(settings, signal);
320
+ const instances = await connection.instances(signal);
321
+ return { id: 'roblox-mcp', name: 'Studio MCP', available: true, message: `MCP initialize выполнен; подключено окон: ${instances.length}. Доступ агента требует совпадения идентификатора проекта.` };
322
+ }
323
+ catch (error) {
324
+ signal?.throwIfAborted();
325
+ return { id: 'roblox-mcp', name: 'Studio MCP', available: false, reason: error instanceof StudioConnectionError ? error.reason : 'connection_failed', message: error instanceof Error ? error.message : String(error) };
326
+ }
327
+ finally {
328
+ await connection.close();
329
+ }
330
+ }
331
+ //# sourceMappingURL=roblox-connection.js.map