@weppy/roblox-mcp 2.0.9 → 2.0.10

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 (42) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +3 -1
  4. package/docs/assets/screenshots/dashboard/dashboard_playtest.png +0 -0
  5. package/docs/assets/screenshots/plugin/sync/sync-overview.png +0 -0
  6. package/docs/en/installation/roblox-plugin.md +4 -4
  7. package/docs/en/pro-upgrade.md +2 -2
  8. package/docs/en/sync/overview.md +2 -2
  9. package/docs/es/README.md +3 -1
  10. package/docs/es/installation/roblox-plugin.md +4 -4
  11. package/docs/es/pro-upgrade.md +2 -2
  12. package/docs/es/sync/overview.md +2 -2
  13. package/docs/id/README.md +3 -1
  14. package/docs/id/installation/roblox-plugin.md +4 -4
  15. package/docs/id/pro-upgrade.md +2 -2
  16. package/docs/id/sync/overview.md +2 -2
  17. package/docs/ja/README.md +3 -1
  18. package/docs/ja/installation/roblox-plugin.md +4 -4
  19. package/docs/ja/pro-upgrade.md +2 -2
  20. package/docs/ja/sync/overview.md +2 -2
  21. package/docs/ko/README.md +3 -1
  22. package/docs/ko/installation/roblox-plugin.md +4 -4
  23. package/docs/ko/pro-upgrade.md +2 -2
  24. package/docs/ko/sync/overview.md +2 -2
  25. package/docs/pt-br/README.md +3 -1
  26. package/docs/pt-br/installation/roblox-plugin.md +4 -4
  27. package/docs/pt-br/pro-upgrade.md +2 -2
  28. package/docs/pt-br/sync/overview.md +2 -2
  29. package/package.json +1 -1
  30. package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
  31. package/plugins/weppy-roblox-mcp/dist/index.js +1 -1
  32. package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +0 -0
  33. package/docs/assets/screenshots/connection_popup.png +0 -0
  34. package/docs/assets/screenshots/sync.png +0 -0
  35. /package/docs/assets/screenshots/{connection_guide.png → plugin/connection/connection-guide.png} +0 -0
  36. /package/docs/assets/screenshots/{plugin_main.png → plugin/installation/main-screen.png} +0 -0
  37. /package/docs/assets/screenshots/{plugins_menu.png → plugin/installation/plugins-menu.png} +0 -0
  38. /package/docs/assets/screenshots/{settings.png → plugin/installation/settings-screen.png} +0 -0
  39. /package/docs/assets/screenshots/{weppy_plugin_toolbar.png → plugin/installation/toolbar-button.png} +0 -0
  40. /package/docs/assets/screenshots/{license/license-dashboard.png → plugin/license/dashboard-license-screen.png} +0 -0
  41. /package/docs/assets/screenshots/{license/license-plugin.png → plugin/license/plugin-license-screen.png} +0 -0
  42. /package/docs/assets/screenshots/{sync_conflict.png → plugin/sync/sync-conflict.png} +0 -0
@@ -6,14 +6,14 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
9
- "version": "2.0.9"
9
+ "version": "2.0.10"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "weppy-roblox-mcp",
14
14
  "source": "./plugins/weppy-roblox-mcp",
15
15
  "description": "Weppy Roblox MCP — MCP server that lets AI coding agents control a live Roblox Studio session with 21 tools, 140+ actions, bidirectional sync, automated playtest, and multi-place support",
16
- "version": "2.0.9",
16
+ "version": "2.0.10",
17
17
  "author": {
18
18
  "name": "hope1026"
19
19
  },
package/CHANGELOG.md CHANGED
@@ -21,6 +21,15 @@ All notable changes to this project will be documented in this file.
21
21
 
22
22
 
23
23
 
24
+
25
+ ## [2.0.10] - 2026-03-28
26
+
27
+ ### Improved
28
+
29
+ - Improved sync logic with play mode detection, suppression, and post-play reconciliation.
30
+ - Enhanced plugin UI for sync collision resolution.
31
+
32
+
24
33
  ## [2.0.9] - 2026-03-27
25
34
 
26
35
  ### Improved
package/README.md CHANGED
@@ -88,7 +88,7 @@ AI can directly handle scripts, instances, properties, terrain, lighting, assets
88
88
 
89
89
  AI works from a synchronized local mirror, so multi-file updates stay consistent.
90
90
 
91
- ![Sync workflow — Studio and local files synchronized in real time](docs/assets/screenshots/sync.png)
91
+ ![Sync workflow — Studio and local files synchronized in real time](docs/assets/screenshots/plugin/sync/sync-overview.png)
92
92
 
93
93
  - Basic: one-way sync (Studio -> Local)
94
94
  - Pro: bidirectional sync + per-type Direction/Apply Mode + history + multi-place
@@ -101,6 +101,8 @@ AI can control Roblox Studio playtests directly. It can start and stop Play (F5)
101
101
  - "Write a test that verifies the SpawnLocation is above the ground and run it."
102
102
  - "Validate that the script I just changed runs without errors in playtest."
103
103
 
104
+ ![WROX Playtest Dashboard — Test history and detailed report](docs/assets/screenshots/dashboard/dashboard_playtest.png)
105
+
104
106
  ### 4) WROX Dashboard: Monitor AI work in real time
105
107
 
106
108
  The MCP server provides a web dashboard where you can check connection status, tool execution history, sync state, and game change logs in real time.
@@ -18,7 +18,7 @@ Note:
18
18
  2. Click the **Plugins** tab in the top menu
19
19
  3. Click the **Plugins Folder** button
20
20
 
21
- ![Open Plugins Folder](../../assets/screenshots/plugins_menu.png)
21
+ ![Open Plugins Folder](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. **Copy** the `WeppyRobloxMCP.rbxm` file from the extracted folder into the opened Plugins folder
24
24
  5. **Restart Roblox Studio**
@@ -27,7 +27,7 @@ Note:
27
27
 
28
28
  After restarting, the **WROX** button will appear in the Plugins tab.
29
29
 
30
- ![WROX Button](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![WROX Button](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. Connect to AI Agent
33
33
 
@@ -52,13 +52,13 @@ The MCP server must be installed. Complete the guide for your AI app first:
52
52
  3. Click the **Connect** button in the plugin window
53
53
  4. Once **"Connected"** status is displayed, you're ready
54
54
 
55
- ![Plugin Main Screen](../../assets/screenshots/plugin_main.png)
55
+ ![Plugin Main Screen](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. Settings (Optional)
58
58
 
59
59
  Click the settings button in the top right of the plugin to change options.
60
60
 
61
- ![Settings Screen](../../assets/screenshots/settings.png)
61
+ ![Settings Screen](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **Auto Connect**: Automatically connect to MCP server when Studio starts
64
64
  - **Auto Reconnect**: Automatically attempt to reconnect when connection is lost
@@ -48,7 +48,7 @@ You only need to activate the license once, either in the plugin or in the dashb
48
48
  5. If the status does not update immediately, click **Refresh**.
49
49
  6. When activation succeeds, the status changes from Basic to Pro and Pro features become available.
50
50
 
51
- ![WROX Plugin license activation screen](../assets/screenshots/license/license-plugin.png)
51
+ ![WROX Plugin license activation screen](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### Activate in the dashboard
54
54
 
@@ -58,7 +58,7 @@ You only need to activate the license once, either in the plugin or in the dashb
58
58
  4. Click **Activate License** to activate the license.
59
59
  5. If needed, use **Refresh License** to fetch the latest status.
60
60
 
61
- ![WROX Dashboard license activation screen](../assets/screenshots/license/license-dashboard.png)
61
+ ![WROX Dashboard license activation screen](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### After activation
64
64
 
@@ -12,7 +12,7 @@ Without Sync, AI only sees snippets pasted into chat. With Sync enabled, AI work
12
12
 
13
13
  ## How it works
14
14
 
15
- ![Sync workflow — Studio tree mirrored to local files](../../assets/screenshots/sync.png)
15
+ ![Sync workflow — Studio tree mirrored to local files](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: initial mirror from Studio tree/instances to local files
18
18
  2. Incremental Sync: continuous update of new changes
@@ -109,7 +109,7 @@ In Pro, Direction and Apply Mode can be controlled per type.
109
109
 
110
110
  When changes are detected on both Studio and local sides during bidirectional sync, a conflict resolution dialog appears.
111
111
 
112
- ![Local Changes Detected — conflict resolution options (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — conflict resolution options (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: overwrite with Studio state as the source of truth
115
115
  - **Local Priority**: apply local files to Studio
package/docs/es/README.md CHANGED
@@ -79,7 +79,7 @@ No es solo generacion de codigo. Son **acciones ejecutables orientadas a producc
79
79
 
80
80
  La IA trabaja sobre un espejo local sincronizado, asi que los cambios en multiples archivos se mantienen consistentes.
81
81
 
82
- ![Flujo de Sync — Studio y archivos locales sincronizados en tiempo real](../assets/screenshots/sync.png)
82
+ ![Flujo de Sync — Studio y archivos locales sincronizados en tiempo real](../assets/screenshots/plugin/sync/sync-overview.png)
83
83
 
84
84
  - Basic: sincronizacion unidireccional (Studio -> Local)
85
85
  - Pro: sincronizacion bidireccional + Direction/Apply Mode por tipo + historial + multiplace
@@ -92,6 +92,8 @@ La IA puede controlar directamente el playtest de Studio. Puede iniciar y detene
92
92
  - "Escribe y ejecuta una prueba para confirmar que el SpawnLocation esta sobre el suelo."
93
93
  - "Valida con playtest que el script que acabo de cambiar funciona sin errores."
94
94
 
95
+ ![WROX Playtest Dashboard — historial de pruebas y reporte detallado](../assets/screenshots/dashboard/dashboard_playtest.png)
96
+
95
97
  ### 4) WROX Dashboard: monitorea el trabajo de la IA en tiempo real
96
98
 
97
99
  El Dashboard web proporcionado por el servidor MCP permite consultar en tiempo real el estado de conexion, el historial de ejecucion de herramientas, el estado de sincronizacion y el historial de cambios del juego.
@@ -18,7 +18,7 @@ Nota:
18
18
  2. Clic en la pestana **Plugins**
19
19
  3. Clic en **Plugins Folder**
20
20
 
21
- ![Abrir Plugins Folder](../../assets/screenshots/plugins_menu.png)
21
+ ![Abrir Plugins Folder](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. **Copia** `WeppyRobloxMCP.rbxm` a la carpeta abierta de Plugins
24
24
  5. **Reinicia Roblox Studio**
@@ -27,7 +27,7 @@ Nota:
27
27
 
28
28
  Despues de reiniciar, aparecera el boton **WROX** en Plugins.
29
29
 
30
- ![Boton WROX](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![Boton WROX](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. Conectar con el Agente de IA
33
33
 
@@ -52,13 +52,13 @@ El servidor MCP debe estar instalado. Completa primero la guia de tu app de IA:
52
52
  3. Clic en **Connect**
53
53
  4. Cuando veas **"Connected"**, listo
54
54
 
55
- ![Pantalla principal del plugin](../../assets/screenshots/plugin_main.png)
55
+ ![Pantalla principal del plugin](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. Configuracion (Opcional)
58
58
 
59
59
  Usa el boton de configuracion en la esquina superior derecha.
60
60
 
61
- ![Pantalla de configuracion](../../assets/screenshots/settings.png)
61
+ ![Pantalla de configuracion](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **Conexion automatica**
64
64
  - **Reconexion automatica**
@@ -48,7 +48,7 @@ Solo necesitas activar la licencia una vez, ya sea en el plugin o en el dashboar
48
48
  5. Si el estado no se actualiza de inmediato, haz clic en **Refresh**.
49
49
  6. Cuando la activación se complete, el estado cambiará de Basic a Pro y las funciones Pro quedarán disponibles.
50
50
 
51
- ![Pantalla de activación de licencia en el plugin](../assets/screenshots/license/license-plugin.png)
51
+ ![Pantalla de activación de licencia en el plugin](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### Activar en el dashboard
54
54
 
@@ -58,7 +58,7 @@ Solo necesitas activar la licencia una vez, ya sea en el plugin o en el dashboar
58
58
  4. Haz clic en **Activate License** para activar la licencia.
59
59
  5. Si hace falta, usa **Refresh License** para obtener el estado más reciente.
60
60
 
61
- ![Pantalla de activación de licencia en el dashboard](../assets/screenshots/license/license-dashboard.png)
61
+ ![Pantalla de activación de licencia en el dashboard](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### Después de activar
64
64
 
@@ -12,7 +12,7 @@ Sin Sync, la IA solo ve fragmentos pegados en el chat. Con Sync activo, trabaja
12
12
 
13
13
  ## Como funciona
14
14
 
15
- ![Flujo de Sync — arbol de Studio reflejado en archivos locales](../../assets/screenshots/sync.png)
15
+ ![Flujo de Sync — arbol de Studio reflejado en archivos locales](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: espejo inicial del arbol/instancias de Studio a local
18
18
  2. Incremental Sync: reflejo continuo de cambios nuevos
@@ -109,7 +109,7 @@ En Pro puedes controlar Direction y Apply Mode por tipo.
109
109
 
110
110
  Cuando se detectan cambios tanto en Studio como en local durante la sincronizacion bidireccional, aparece un dialogo de resolucion de conflictos.
111
111
 
112
- ![Local Changes Detected — opciones de resolucion de conflictos (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — opciones de resolucion de conflictos (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: sobrescribir usando el estado de Studio como fuente de verdad
115
115
  - **Local Priority**: aplicar los archivos locales a Studio
package/docs/id/README.md CHANGED
@@ -79,7 +79,7 @@ Ini bukan sekadar generate kode. Ini adalah **aksi yang benar-benar bisa dieksek
79
79
 
80
80
  AI bekerja dari mirror lokal yang tersinkron, jadi perubahan lintas banyak file tetap konsisten.
81
81
 
82
- ![Alur Sync — Studio dan file lokal tersinkron secara real time](../assets/screenshots/sync.png)
82
+ ![Alur Sync — Studio dan file lokal tersinkron secara real time](../assets/screenshots/plugin/sync/sync-overview.png)
83
83
 
84
84
  - Basic: sinkronisasi satu arah (Studio -> Local)
85
85
  - Pro: sinkronisasi dua arah + Direction/Apply Mode per tipe + riwayat + multiplace
@@ -92,6 +92,8 @@ AI bisa mengontrol playtest Studio secara langsung. AI dapat memulai dan menghen
92
92
  - "Tulis dan jalankan test untuk memastikan SpawnLocation berada di atas tanah."
93
93
  - "Validasi lewat playtest bahwa script yang baru saya ubah berjalan tanpa error."
94
94
 
95
+ ![WROX Playtest Dashboard — riwayat tes dan laporan detail](../assets/screenshots/dashboard/dashboard_playtest.png)
96
+
95
97
  ### 4) WROX Dashboard: pantau pekerjaan AI secara real-time
96
98
 
97
99
  Dashboard berbasis web yang disediakan server MCP memungkinkan Anda melihat status koneksi, riwayat eksekusi alat, status sinkronisasi, dan riwayat perubahan game secara real-time.
@@ -18,7 +18,7 @@ Catatan:
18
18
  2. Klik tab **Plugins**
19
19
  3. Klik **Plugins Folder**
20
20
 
21
- ![Buka Plugins Folder](../../assets/screenshots/plugins_menu.png)
21
+ ![Buka Plugins Folder](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. **Salin** `WeppyRobloxMCP.rbxm` ke folder Plugins
24
24
  5. **Restart Roblox Studio**
@@ -27,7 +27,7 @@ Catatan:
27
27
 
28
28
  Setelah restart, tombol **WROX** muncul di tab Plugins.
29
29
 
30
- ![Tombol WROX](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![Tombol WROX](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. Hubungkan AI Agent
33
33
 
@@ -52,13 +52,13 @@ MCP server harus sudah terinstal. Selesaikan dulu panduan app AI yang Anda pakai
52
52
  3. Klik **Connect**
53
53
  4. Jika status **"Connected"** tampil, koneksi berhasil
54
54
 
55
- ![Layar Utama Plugin](../../assets/screenshots/plugin_main.png)
55
+ ![Layar Utama Plugin](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. Pengaturan (Opsional)
58
58
 
59
59
  Gunakan tombol pengaturan di kanan atas plugin.
60
60
 
61
- ![Layar Pengaturan](../../assets/screenshots/settings.png)
61
+ ![Layar Pengaturan](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **Auto Connect**
64
64
  - **Auto Reconnect**
@@ -48,7 +48,7 @@ Kamu hanya perlu mengaktifkan lisensi sekali, baik di plugin maupun di dashboard
48
48
  5. Jika status belum langsung diperbarui, klik **Refresh**.
49
49
  6. Setelah aktivasi berhasil, status berubah dari Basic ke Pro dan fitur Pro bisa digunakan.
50
50
 
51
- ![Layar aktivasi lisensi di plugin](../assets/screenshots/license/license-plugin.png)
51
+ ![Layar aktivasi lisensi di plugin](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### Aktifkan di dashboard
54
54
 
@@ -58,7 +58,7 @@ Kamu hanya perlu mengaktifkan lisensi sekali, baik di plugin maupun di dashboard
58
58
  4. Klik **Activate License** untuk mengaktifkan lisensi.
59
59
  5. Jika perlu, gunakan **Refresh License** untuk mengambil status terbaru.
60
60
 
61
- ![Layar aktivasi lisensi di dashboard](../assets/screenshots/license/license-dashboard.png)
61
+ ![Layar aktivasi lisensi di dashboard](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### Setelah aktivasi
64
64
 
@@ -12,7 +12,7 @@ Tanpa Sync, AI hanya melihat potongan kode yang ditempel di chat. Dengan Sync ak
12
12
 
13
13
  ## Cara kerjanya
14
14
 
15
- ![Alur Sync — tree Studio di-mirror ke file lokal](../../assets/screenshots/sync.png)
15
+ ![Alur Sync — tree Studio di-mirror ke file lokal](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: mirror awal dari tree/instance Studio ke lokal
18
18
  2. Incremental Sync: perubahan baru terus disinkronkan
@@ -109,7 +109,7 @@ Di Pro, Direction dan Apply Mode bisa diatur per tipe.
109
109
 
110
110
  Ketika perubahan terdeteksi di sisi Studio maupun lokal selama sinkronisasi dua arah, dialog resolusi konflik akan muncul.
111
111
 
112
- ![Local Changes Detected — opsi resolusi konflik (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — opsi resolusi konflik (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: timpa dengan status Studio sebagai sumber kebenaran
115
115
  - **Local Priority**: terapkan file lokal ke Studio
package/docs/ja/README.md CHANGED
@@ -79,7 +79,7 @@ AIがStudio内で、スクリプト、インスタンス、プロパティ、地
79
79
 
80
80
  同期されたローカルミラーを基準にAIが作業するため、複数ファイルにまたがる変更も一貫して適用できます。
81
81
 
82
- ![Syncワークフロー — Studioとローカルファイルがリアルタイムで同期される様子](../assets/screenshots/sync.png)
82
+ ![Syncワークフロー — Studioとローカルファイルがリアルタイムで同期される様子](../assets/screenshots/plugin/sync/sync-overview.png)
83
83
 
84
84
  - Basic: 片方向同期(Studio -> Local)
85
85
  - Pro: 双方向同期 + タイプ別Direction/Apply Mode + 変更履歴 + マルチPlace
@@ -92,6 +92,8 @@ AIがStudioプレイテストを直接制御します。Play(F5)/Run(F8)
92
92
  - 「SpawnLocationが地面の上にあることを確認するテストを書いて実行して。」
93
93
  - 「今修正したスクリプトがエラーなしで動くかプレイテストで検証して。」
94
94
 
95
+ ![WROXプレイテスト ダッシュボード — テスト履歴と詳細レポート](../assets/screenshots/dashboard/dashboard_playtest.png)
96
+
95
97
  ### 4) WROX Dashboard: AI作業をリアルタイムでモニタリング
96
98
 
97
99
  MCPサーバーが提供するWebダッシュボードで、接続状態、ツール実行履歴、同期状態、ゲーム変更履歴をリアルタイムで確認します。
@@ -18,7 +18,7 @@ Roblox StudioでAIエージェントと連携するためのプラグインイ
18
18
  2. 上部メニューの **Plugins** タブをクリック
19
19
  3. **Plugins Folder** ボタンをクリック
20
20
 
21
- ![Plugins Folderを開く](../../assets/screenshots/plugins_menu.png)
21
+ ![Plugins Folderを開く](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. 解凍フォルダ内の `WeppyRobloxMCP.rbxm` を Plugins フォルダへ**コピー**
24
24
  5. **Roblox Studioを再起動**
@@ -27,7 +27,7 @@ Roblox StudioでAIエージェントと連携するためのプラグインイ
27
27
 
28
28
  再起動後、Pluginsタブに **WROX** ボタンが表示されます。
29
29
 
30
- ![WROXボタン](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![WROXボタン](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. AIエージェントに接続
33
33
 
@@ -52,13 +52,13 @@ MCPサーバーがインストールされている必要があります。利
52
52
  3. プラグインウィンドウで **Connect** をクリック
53
53
  4. **"Connected"** が表示されたら完了
54
54
 
55
- ![プラグインメイン画面](../../assets/screenshots/plugin_main.png)
55
+ ![プラグインメイン画面](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. 設定(任意)
58
58
 
59
59
  右上の設定ボタンからオプションを変更できます。
60
60
 
61
- ![設定画面](../../assets/screenshots/settings.png)
61
+ ![設定画面](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **自動接続**: Studio起動時に自動接続
64
64
  - **自動再接続**: 切断時に自動再接続
@@ -48,7 +48,7 @@ StudioのプレイテストをAIが実行・検証します。F5(Play)/F8(Run)
48
48
  5. 状態がすぐに更新されない場合は **Refresh** を押して再確認します。
49
49
  6. 有効化が完了すると、状態がBasicからProに変わり、Pro機能を使えるようになります。
50
50
 
51
- ![プラグインのライセンス有効化画面](../assets/screenshots/license/license-plugin.png)
51
+ ![プラグインのライセンス有効化画面](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### ダッシュボードで有効化
54
54
 
@@ -58,7 +58,7 @@ StudioのプレイテストをAIが実行・検証します。F5(Play)/F8(Run)
58
58
  4. **Activate License** を押してライセンスを有効化します。
59
59
  5. 必要に応じて **Refresh License** で最新状態を再取得します。
60
60
 
61
- ![ダッシュボードのライセンス有効化画面](../assets/screenshots/license/license-dashboard.png)
61
+ ![ダッシュボードのライセンス有効化画面](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### 有効化後の確認
64
64
 
@@ -12,7 +12,7 @@ Syncがない場合、AIはチャットに貼られた断片だけを見て判
12
12
 
13
13
  ## 基本の動作
14
14
 
15
- ![Syncワークフロー — Studioツリーがローカルファイルにミラーされる様子](../../assets/screenshots/sync.png)
15
+ ![Syncワークフロー — Studioツリーがローカルファイルにミラーされる様子](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: Studioツリー/インスタンスをローカルに初期ミラー
18
18
  2. Incremental Sync: 以後の変更分のみを継続反映
@@ -109,7 +109,7 @@ ProではタイプごとにDirection/Apply Modeを細かく制御できます。
109
109
 
110
110
  双方向同期中にStudioとローカルの両方で変更が検出されると、コンフリクト解決ダイアログが表示されます。
111
111
 
112
- ![Local Changes Detected — コンフリクト解決オプション (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — コンフリクト解決オプション (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: Studio側の状態を基準に上書き
115
115
  - **Local Priority**: ローカルファイルをStudioに反映
package/docs/ko/README.md CHANGED
@@ -79,7 +79,7 @@ npx -y @weppy/roblox-mcp
79
79
 
80
80
  AI가 로컬 동기화된 프로젝트를 기준으로 전체 구조를 이해해, 여러 파일에 걸친 변경을 일관되게 수행합니다.
81
81
 
82
- ![Sync 워크플로우 — Studio와 로컬 파일이 실시간으로 동기화되는 모습](../assets/screenshots/sync.png)
82
+ ![Sync 워크플로우 — Studio와 로컬 파일이 실시간으로 동기화되는 모습](../assets/screenshots/plugin/sync/sync-overview.png)
83
83
 
84
84
  - Basic: Studio -> Local 단방향 동기화
85
85
  - Pro: 양방향 동기화 + 타입별 Direction/Apply Mode + 변경 기록 + 멀티 Place
@@ -92,6 +92,8 @@ Studio 플레이테스트를 AI가 직접 제어합니다. F5(Play)/F8(Run) 시
92
92
  - "SpawnLocation이 지면 위에 있는지 테스트 스크립트를 작성해서 자동 실행해줘."
93
93
  - "방금 수정한 스크립트가 에러 없이 동작하는지 플레이테스트로 검증해줘."
94
94
 
95
+ ![WROX 플레이테스트 대시보드 — 테스트 기록 및 상세 리포트](../assets/screenshots/dashboard/dashboard_playtest.png)
96
+
95
97
  ### 4) WROX Dashboard: AI 작업을 실시간으로 모니터링
96
98
 
97
99
  MCP 서버가 제공하는 웹 대시보드에서 연결 상태, 도구 실행 기록, 동기화 상태, 게임 변경 이력을 실시간으로 확인합니다.
@@ -18,7 +18,7 @@ Roblox Studio에서 AI 에이전트와 연결하기 위한 플러그인 설치
18
18
  2. 상단 메뉴 **Plugins** 탭 클릭
19
19
  3. **Plugins Folder** 버튼 클릭
20
20
 
21
- ![Plugins Folder 열기](../../assets/screenshots/plugins_menu.png)
21
+ ![Plugins Folder 열기](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. 압축 해제한 폴더에서 `WeppyRobloxMCP.rbxm` 파일을 열린 Plugins 폴더에 **복사**
24
24
  5. **Roblox Studio 재시작**
@@ -27,7 +27,7 @@ Roblox Studio에서 AI 에이전트와 연결하기 위한 플러그인 설치
27
27
 
28
28
  재시작 후 Plugins 탭에 **WROX** 버튼이 나타납니다.
29
29
 
30
- ![WROX 버튼](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![WROX 버튼](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. AI 에이전트 연결
33
33
 
@@ -52,13 +52,13 @@ MCP 서버가 설치되어 있어야 합니다. 사용하는 AI 앱에 맞는
52
52
  3. 플러그인 창에서 **Connect** 버튼 클릭
53
53
  4. **"Connected"** 상태가 표시되면 연결 완료
54
54
 
55
- ![플러그인 메인 화면](../../assets/screenshots/plugin_main.png)
55
+ ![플러그인 메인 화면](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. 설정 (선택사항)
58
58
 
59
59
  플러그인 우측 상단의 설정 버튼을 클릭하면 다양한 옵션을 변경할 수 있습니다.
60
60
 
61
- ![설정 화면](../../assets/screenshots/settings.png)
61
+ ![설정 화면](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **자동 연결**: Studio 시작 시 자동으로 MCP 서버에 연결
64
64
  - **자동 재연결**: 연결이 끊어지면 자동으로 재연결 시도
@@ -48,7 +48,7 @@ Studio 플레이테스트를 AI가 실행하고 검증합니다. F5(Play)/F8(Run
48
48
  5. 상태가 바로 갱신되지 않으면 **Refresh** 버튼으로 다시 확인합니다.
49
49
  6. 활성화가 완료되면 Basic 대신 Pro 상태로 표시되고 Pro 기능을 사용할 수 있습니다.
50
50
 
51
- ![플러그인 라이선스 활성화 화면](../assets/screenshots/license/license-plugin.png)
51
+ ![플러그인 라이선스 활성화 화면](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### 대시보드에서 활성화
54
54
 
@@ -58,7 +58,7 @@ Studio 플레이테스트를 AI가 실행하고 검증합니다. F5(Play)/F8(Run
58
58
  4. **Activate License** 버튼을 눌러 라이선스를 활성화합니다.
59
59
  5. 필요하면 **Refresh License**로 최신 상태를 다시 조회합니다.
60
60
 
61
- ![대시보드 라이선스 활성화 화면](../assets/screenshots/license/license-dashboard.png)
61
+ ![대시보드 라이선스 활성화 화면](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### 활성화 후 확인
64
64
 
@@ -12,7 +12,7 @@ Sync가 없으면 AI는 대화에 붙여 넣은 코드 일부만 보고 판단
12
12
 
13
13
  ## 기본 동작 방식
14
14
 
15
- ![Sync 워크플로우 — Studio 트리가 로컬 파일로 동기화된 모습](../../assets/screenshots/sync.png)
15
+ ![Sync 워크플로우 — Studio 트리가 로컬 파일로 동기화된 모습](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: Studio 트리와 인스턴스를 로컬 미러로 초기 동기화
18
18
  2. Incremental Sync: 변경 감시로 이후 변경분만 반영
@@ -109,7 +109,7 @@ Pro에서는 타입별로 Direction/Apply Mode를 다르게 설정해 워크플
109
109
 
110
110
  양방향 동기화 중 Studio와 로컬 양쪽에서 변경이 감지되면, 아래와 같은 충돌 해결 화면이 나타납니다.
111
111
 
112
- ![Local Changes Detected — 충돌 해결 옵션 (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — 충돌 해결 옵션 (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: Studio 쪽 상태를 기준으로 덮어쓰기
115
115
  - **Local Priority**: 로컬 파일을 기준으로 Studio에 반영
@@ -79,7 +79,7 @@ Nao e apenas geracao de codigo. Sao **acoes executaveis para fluxo real de desen
79
79
 
80
80
  A IA trabalha com um espelho local sincronizado, entao alteracoes em varios arquivos continuam consistentes.
81
81
 
82
- ![Fluxo de Sync — Studio e arquivos locais sincronizados em tempo real](../assets/screenshots/sync.png)
82
+ ![Fluxo de Sync — Studio e arquivos locais sincronizados em tempo real](../assets/screenshots/plugin/sync/sync-overview.png)
83
83
 
84
84
  - Basic: sincronizacao unidirecional (Studio -> Local)
85
85
  - Pro: sincronizacao bidirecional + Direction/Apply Mode por tipo + historico + multiplace
@@ -92,6 +92,8 @@ A IA pode controlar diretamente o playtest do Studio. Ela pode iniciar e parar P
92
92
  - "Escreva e execute um teste para confirmar que o SpawnLocation esta acima do chao."
93
93
  - "Valide com playtest se o script que acabei de alterar roda sem erros."
94
94
 
95
+ ![WROX Playtest Dashboard — historico de testes e relatorio detalhado](../assets/screenshots/dashboard/dashboard_playtest.png)
96
+
95
97
  ### 4) WROX Dashboard: monitore as operacoes da IA em tempo real
96
98
 
97
99
  No dashboard web fornecido pelo servidor MCP, acompanhe em tempo real o status de conexao, historico de execucao de ferramentas, status de sincronizacao e historico de alteracoes do jogo.
@@ -18,7 +18,7 @@ Nota:
18
18
  2. Clique na aba **Plugins**
19
19
  3. Clique em **Plugins Folder**
20
20
 
21
- ![Abrir Plugins Folder](../../assets/screenshots/plugins_menu.png)
21
+ ![Abrir Plugins Folder](../../assets/screenshots/plugin/installation/plugins-menu.png)
22
22
 
23
23
  4. **Copie** `WeppyRobloxMCP.rbxm` para a pasta de Plugins
24
24
  5. **Reinicie o Roblox Studio**
@@ -27,7 +27,7 @@ Nota:
27
27
 
28
28
  Apos reiniciar, o botao **WROX** aparecera na aba Plugins.
29
29
 
30
- ![Botao WROX](../../assets/screenshots/weppy_plugin_toolbar.png)
30
+ ![Botao WROX](../../assets/screenshots/plugin/installation/toolbar-button.png)
31
31
 
32
32
  ## 4. Conectar ao Agente de IA
33
33
 
@@ -52,13 +52,13 @@ O servidor MCP deve estar instalado. Complete primeiro o guia do seu app de IA:
52
52
  3. Clique em **Connect**
53
53
  4. Quando aparecer **"Connected"**, esta pronto
54
54
 
55
- ![Tela Principal do Plugin](../../assets/screenshots/plugin_main.png)
55
+ ![Tela Principal do Plugin](../../assets/screenshots/plugin/installation/main-screen.png)
56
56
 
57
57
  ## 5. Configuracoes (Opcional)
58
58
 
59
59
  Use o botao de configuracoes no canto superior direito.
60
60
 
61
- ![Tela de Configuracoes](../../assets/screenshots/settings.png)
61
+ ![Tela de Configuracoes](../../assets/screenshots/plugin/installation/settings-screen.png)
62
62
 
63
63
  - **Conexao Automatica**
64
64
  - **Reconexao Automatica**
@@ -48,7 +48,7 @@ Você só precisa ativar a licença uma vez, no plugin ou no dashboard. As duas
48
48
  5. Se o status não atualizar imediatamente, clique em **Refresh**.
49
49
  6. Quando a ativação terminar, o status muda de Basic para Pro e os recursos Pro ficam disponíveis.
50
50
 
51
- ![Tela de ativação de licença no plugin](../assets/screenshots/license/license-plugin.png)
51
+ ![Tela de ativação de licença no plugin](../assets/screenshots/plugin/license/plugin-license-screen.png)
52
52
 
53
53
  ### Ativar no dashboard
54
54
 
@@ -58,7 +58,7 @@ Você só precisa ativar a licença uma vez, no plugin ou no dashboard. As duas
58
58
  4. Clique em **Activate License** para ativar a licença.
59
59
  5. Se necessário, use **Refresh License** para buscar o status mais recente.
60
60
 
61
- ![Tela de ativação de licença no dashboard](../assets/screenshots/license/license-dashboard.png)
61
+ ![Tela de ativação de licença no dashboard](../assets/screenshots/plugin/license/dashboard-license-screen.png)
62
62
 
63
63
  ### Depois da ativação
64
64
 
@@ -12,7 +12,7 @@ Sem Sync, a IA so enxerga trechos colados no chat. Com Sync ativo, ela trabalha
12
12
 
13
13
  ## Como funciona
14
14
 
15
- ![Fluxo de Sync — arvore do Studio espelhada em arquivos locais](../../assets/screenshots/sync.png)
15
+ ![Fluxo de Sync — arvore do Studio espelhada em arquivos locais](../../assets/screenshots/plugin/sync/sync-overview.png)
16
16
 
17
17
  1. Full Sync: espelho inicial da arvore/instancias do Studio para local
18
18
  2. Incremental Sync: atualizacao continua das mudancas novas
@@ -109,7 +109,7 @@ No Pro, voce controla Direction e Apply Mode por tipo.
109
109
 
110
110
  Quando mudancas sao detectadas tanto no Studio quanto no local durante a sincronizacao bidirecional, um dialogo de resolucao de conflitos aparece.
111
111
 
112
- ![Local Changes Detected — opcoes de resolucao de conflitos (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/sync_conflict.png)
112
+ ![Local Changes Detected — opcoes de resolucao de conflitos (Studio Priority / Local Priority / Per-File)](../../assets/screenshots/plugin/sync/sync-conflict.png)
113
113
 
114
114
  - **Studio Priority**: sobrescrever usando o estado do Studio como fonte de verdade
115
115
  - **Local Priority**: aplicar arquivos locais ao Studio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weppy/roblox-mcp",
3
- "version": "2.0.9",
3
+ "version": "2.0.10",
4
4
  "description": "MCP (Model Context Protocol) server for Roblox Studio integration - enables AI coding agents to interact with Roblox Studio in real-time",
5
5
  "main": "plugins/weppy-roblox-mcp/dist/index.js",
6
6
  "type": "module",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weppy-roblox-mcp",
3
3
  "description": "MCP server for Roblox Studio integration - AI-powered game development with specialized agents and skills",
4
- "version": "2.0.9",
4
+ "version": "2.0.10",
5
5
  "author": {
6
6
  "name": "hope1026"
7
7
  },
@@ -123,7 +123,7 @@ data: ${JSON.stringify(n)}
123
123
  `).filter(g=>g.length>0)}catch(h){if(h.code==="ENOENT"){n.status(200).json({entries:[],total:0,hasMore:!1});return}throw h}let p=[];for(let h=u.length-1;h>=0;h--)try{let g=JSON.parse(u[h]);if(o&&g.direction!==o||s&&g.type!==s)continue;p.push(g)}catch{continue}let d=p.length,m={entries:p.slice(a,a+i),total:d,hasMore:a+i<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 i=this.ctx.getDefaultRuntimePlaceId();i!=null&&(n=this.ctx.places.get(i))}if(!n)return"forward";let r=e;return n.directions[r]??"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:{...Bi}};let r=this.ctx.places.get(n);if(!r)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...Bi}};let i=r.fileWatcher?.getPendingCount()??0;return{state:r.state,instanceCount:r.instanceCount,scriptCount:r.scriptCount,lastFullSync:r.lastFullSync,lastIncrementalSync:r.lastIncrementalSync,syncRoot:this.ctx.config.getPlaceRoot(n),activeClientId:r.activeClientId,reverseSyncAvailable:i>0,modifiedFileCount:i,applyModes:{...r.applyModes}}}getConfigDirect(){return this.ctx.config.getConfig()}async getHistoryDirect(e,n){let r=parseInt(e,10),i=this.ctx.places.get(r);i&&await i.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(r),c=[];try{c=(await Mo.readFile(s,"utf-8")).split(`
124
124
  `).filter(f=>f.length>0)}catch(d){if(d.code==="ENOENT")return{entries:[],total:0,hasMore:!1};throw d}let l=[];for(let d=c.length-1;d>=0;d--)try{l.push(JSON.parse(c[d]))}catch{continue}let 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{...qi};let r=this.ctx.places.get(n);return r?{...r.directions}:{...qi}}getProgressDirect(e){let n=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(n==null)return{state:"idle",isSyncing:!1};let r=this.ctx.places.get(n);if(!r)return{state:"idle",isSyncing:!1};if(!r.syncProgress)return{state:r.state,isSyncing:!1,lastSync:{instanceCount:r.instanceCount,scriptCount:r.scriptCount,completedAt:r.lastFullSync}};let i=r.syncProgress,a=Date.now()-i.syncStartTime,o=i.totalInstances>0?Math.min(100,Math.round(i.processedInstances/i.totalInstances*100)):0,s;if(i.processedInstances>0&&o<100){let l=a/i.processedInstances,u=i.totalInstances-i.processedInstances;s=Math.round(l*u)}let c=a>0?Math.round(i.bytesReceived/a*1e3):0;return{state:r.state,isSyncing:!0,progressPercent:o,currentService:i.currentService,currentChunk:{index:i.currentChunkIndex,total:i.currentTotalChunks},instances:{processed:i.processedInstances,total:i.totalInstances},services:{processed:i.processedServices,total:i.totalServices},elapsedMs:a,estimatedRemainingMs:s,bytesReceived:i.bytesReceived,bytesPerSecond:c}}async readSyncedFile(e,n){let r=parseInt(e,10),i=this.ctx.places.get(r);if(!i)throw new Error(`Place ${e} not found in sync cache`);let a=i.index.resolvePropsPath(n);try{return{content:await Mo.readFile(a,"utf-8"),path:a}}catch(s){if(s.code!=="ENOENT")throw s}for(let s of Eo){let c=i.index.resolveScriptPath(n,s,!1);try{return{content:await Mo.readFile(c,"utf-8"),path:c}}catch{continue}}let o=i.index.resolveValuePath(n);try{return{content:await Mo.readFile(o,"utf-8"),path:o}}catch(s){if(s.code!=="ENOENT")throw s}throw new Error(`No synced file found for instance: ${n}`)}async writeSyncedFile(e,n,r){let i=parseInt(e,10),a=this.ctx.places.get(i);if(!a)throw new Error(`Place ${e} not found in sync cache`);await a.writer.writeScript(n,"Script",r,!1)}async executeViaDisk(e,n){let r=this.ctx.getDefaultRuntimePlaceId();if(r==null)throw new Error("No active sync place for disk execution");let i=this.ctx.places.get(r);if(!i)throw new Error(`Place ${r} not found in sync cache`);switch(e){case"set_script_source":{let a=n.scriptType||n.className||"Script";return await i.writer.writeScript(n.path,a,n.source,!1),{success:!0,path:n.path}}case"set_property":return await i.writer.writeProps(n.path,{className:n.className||"Instance",name:Ct(n.path),properties:{[n.property]:n.value}}),{success:!0,path:n.path};default:throw new Error(`Disk execution not supported for action: ${e}`)}}};import $e from"path";import{randomUUID as AQ}from"crypto";import{promises as Xt}from"fs";pe();var $f=class{constructor(e){this.ctx=e}preserveLocalFilesMap=new Map;pendingServiceTrees=new Map;async handleInitStart(e,n,r){if(n.previousPlaceId!==void 0&&n.previousPlaceId!==e&&(y.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){r.status(409).json({error:"Conflict",message:`Place ${this.ctx.activeFullSyncPlaceId} is currently syncing. Only one place can sync at a time.`});return}let i=await this.ctx.getOrCreatePlaceContext(e,n.placeName);if(i.activeClientId&&i.activeClientId!==n.clientId&&i.activeFullSyncSessionId!==null){r.status(409).json({error:"Conflict",message:`Another client (${i.activeClientId}) is currently syncing this place`});return}if(this.ctx.activeFullSyncPlaceId=e,this.ctx.touchRuntimePlace(e),i.activeClientId=n.clientId,i.placeName=n.placeName,n.directions&&typeof n.directions=="object"){let p=n.directions,d=f=>f==="forward"||f==="reverse"||f==="bidirectional";d(p.scripts)&&(i.directions.scripts=p.scripts),d(p.values)&&(i.directions.values=p.values),d(p.containers)&&(i.directions.containers=p.containers),d(p.data)&&(i.directions.data=p.data),d(p.services)&&(i.directions.services=p.services),y.info("Sync directions received",{placeId:e,directions:i.directions})}else i.directions={...qi};if(n.applyModes&&typeof n.applyModes=="object"){let p=n.applyModes,d=f=>f==="auto"||f==="manual";d(p.scripts)&&(i.applyModes.scripts=p.scripts),d(p.values)&&(i.applyModes.values=p.values),d(p.containers)&&(i.applyModes.containers=p.containers),d(p.data)&&(i.applyModes.data=p.data),d(p.services)&&(i.applyModes.services=p.services)}else i.applyModes={...Bi};i.forwardRestoreQueue=[];let a=AQ();i.activeFullSyncSessionId=a,this.pendingServiceTrees.set(a,new Map),i.instanceCount=0,i.scriptCount=0;let o=this.ctx.config.getPlaceRoot(e),s=$e.join(o,`explorer_tmp_${a}`);await Xt.mkdir(s,{recursive:!0}),i.tmpIndex=new Yr(o,s),i.tmpWriter=new jo(this.ctx.config,i.tmpIndex,e),i.collisionDirMap=new Map;let c=n.preserveLocalFiles;Array.isArray(c)&&c.length>0&&(this.setPreserveLocalFiles(a,c),y.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=$e.join(o,".sync-meta.json");await Xt.mkdir($e.dirname(u),{recursive:!0}),await this.ctx.atomicWriteFile(u,JSON.stringify(l,null,2)+`
125
125
  `),this.startTTLTimerForPlace(i,a),i.state="initializing",i.index.resetNameCounters(),i.syncProgress={syncStartTime:Date.now(),totalInstances:n.totalInstances,totalServices:n.totalServices,processedInstances:0,processedServices:0,currentService:null,currentChunkIndex:0,currentTotalChunks:0,bytesReceived:0,processedChunks:0},i.writer.appendChangeLog(`FULL_SYNC_START clientId=${n.clientId} placeId=${n.placeId}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncStart",direction:"forward",path:`place_${n.placeId}`,details:`services:${n.totalServices} instances:${n.totalInstances}`}),y.info("Full sync started",{syncId:a,clientId:n.clientId,placeId:n.placeId,placeName:n.placeName,totalServices:n.totalServices,totalInstances:n.totalInstances}),r.status(200).json({status:"started",syncId:i.activeFullSyncSessionId})}async handleInitChunk(e,n,r){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId||!i.tmpIndex||!i.tmpWriter){r.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&n.clientId&&i.activeClientId!==n.clientId){r.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.getOrCreatePendingServiceTree(a,n),s=0,c=i.collisionDirMap;for(let u of n.instances){if(this.ctx.config.isForbiddenPath(u.path))continue;let p=i.tmpIndex.resolveParentDir(u.path),d=c.get(p)??p,{resolved:f,retroactiveRename:m}=i.tmpIndex.registerCollision(u.path,u.siblingIndex??void 0,d);m&&(await Qr(i.tmpIndex,d,m.from,m.to),this.rewritePendingEffectivePaths(o,i.tmpIndex.getExplorerRoot(),$e.join(d,m.from),$e.join(d,m.to)));let h=i.tmpIndex.resolveChildrenDir(u.path),g=$e.join(d,f);g!==h&&c.set(h,g);let x=i.tmpIndex.sanitizeName(u.name),w=u;if(d!==p||f!==x){let k=i.tmpIndex.getExplorerRoot(),I=$e.relative(k,d).split($e.sep).filter(H=>H.length>0),O=Xe(["game",...I,f]);w={...u,path:O}}let _=await i.tmpWriter.writeInstance(w);i.tmpIndex.setClassName(u.path,u.className,u.siblingIndex),s++,(_.propsWritten||_.valueWritten)&&i.instanceCount++,_.scriptWritten&&i.scriptCount++,o.instances.push({effectivePath:w.path,originalPath:u.path,className:u.className})}let l=this.isLastChunk(o,n.chunkIndex,n.totalChunks);if(l){let u=this.buildServiceTree(i,o);await i.tmpWriter.writeTree(n.serviceName,u);let p=this.pendingServiceTrees.get(a);p?.delete(n.serviceName),p&&p.size===0&&this.pendingServiceTrees.delete(a)}i.syncProgress&&(i.syncProgress.processedInstances+=s,i.syncProgress.currentService=n.serviceName,i.syncProgress.currentChunkIndex=n.chunkIndex,i.syncProgress.currentTotalChunks=n.totalChunks,i.syncProgress.processedChunks++,i.syncProgress.bytesReceived+=JSON.stringify(n).length,l&&i.syncProgress.processedServices++),y.debug("Sync chunk processed",{placeId:e,serviceName:n.serviceName,chunkIndex:n.chunkIndex,totalChunks:n.totalChunks,processed:s}),r.status(200).json({processed:s,service:n.serviceName})}async handleInitComplete(e,n,r){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId){r.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&n.clientId&&i.activeClientId!==n.clientId){r.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.ctx.config.getPlaceRoot(e),s=$e.join(o,"explorer"),c=$e.join(o,`explorer_tmp_${a}`),l=this.getAndClearPreserveLocalFiles(a),u=new Map,p=[];if(l.length>0){for(let m of l){let h=$e.resolve(o,m);try{let g=await Xt.readFile(h,"utf-8");u.set(m,g)}catch{p.push(m)}}y.info("Backed up local files for preservation",{placeId:e,requested:l.length,backed:u.size,deleted:p.length})}try{await Xt.rm(s,{recursive:!0,force:!0})}catch{}await Xt.rename(c,s),i.instanceCount=n.instanceCount,i.scriptCount=n.scriptCount,i.lastFullSync=new Date().toISOString();let d={version:1,placeId:i.placeId,placeName:i.placeName,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncMode:"mirror"},f=$e.join(o,".sync-meta.json");if(await this.ctx.atomicWriteFile(f,JSON.stringify(d,null,2)+`
126
- `),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,x]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,x)}for(let[g,x]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,x)}i.index.resetNameCounters(),i.index.mergeNameMappingsFrom(i.tmpIndex)}if(i.tmpIndex=null,i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),this.pendingServiceTrees.delete(a),i.collisionDirMap=null,await i.index.saveToDisk(),i.state="syncing",i.activeFullSyncSessionId=null,i.syncProgress=null,this.ctx.touchRuntimePlace(e),this.ctx.activeFullSyncPlaceId=null,await this.ctx.startFileWatcherForPlace(i),i.fileWatcher&&await i.fileWatcher.waitUntilReady(),u.size>0){for(let[m,h]of u){let g=$e.resolve(o,m);try{await Xt.mkdir($e.dirname(g),{recursive:!0}),await Xt.writeFile(g,h,"utf-8")}catch(x){y.warn("Failed to restore preserved file",{path:m,error:x instanceof Error?x.message:String(x)})}}y.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let g=$e.resolve(o,h);try{await Xt.unlink(g),i.index.removeHash(g),m++}catch(x){x.code!=="ENOENT"&&y.warn("Failed to delete preserved-as-deleted file",{path:h,error:x instanceof Error?x.message:String(x)})}}m>0&&(await i.index.saveToDisk(),y.info("Deleted locally-removed files from new sync",{placeId:e,count:m}))}i.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${i.instanceCount} scripts=${i.scriptCount}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",path:`place_${e}`,details:`instances:${i.instanceCount} scripts:${i.scriptCount}`}),y.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),r.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}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 r=setTimeout(async()=>{y.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${n}`);try{await Xt.rm(a,{recursive:!0,force:!0})}catch(o){y.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))},vN);r&&typeof r=="object"&&"unref"in r&&r.unref(),e.incompleteSyncTimer=r}getOrCreatePendingServiceTree(e,n){let r=this.pendingServiceTrees.get(e);r||(r=new Map,this.pendingServiceTrees.set(e,r));let i=r.get(n.serviceName);if(i)return i;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return r.set(n.serviceName,a),a}isLastChunk(e,n,r){return r<=1?!0:e.zeroBasedChunkIndex?n>=r-1:n>=r}buildServiceTree(e,n){let r={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of n.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=gt(i.originalPath);this.upsertTreeNode(r,a,o,i.className)}return this.recomputeTreeChildCounts(r),r.syncedAt=new Date().toISOString(),r}resolveEffectiveSegments(e,n){return e.tmpIndex?gt(n).map(r=>e.tmpIndex.sanitizeName(r)):gt(n)}rewritePendingEffectivePaths(e,n,r,i){let a=$e.relative(n,r).split($e.sep).filter(l=>l.length>0),o=$e.relative(n,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Xe(["game",...a]),c=Xe(["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,r,i){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=r[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=i,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?i:"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=r=>{let i=r.children??[];r.children=i;for(let a of i)n(a);r.childCount=i.length};for(let r of e.children)n(r);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 Xt.readdir(e,{withFileTypes:!0});for(let r of n)if(r.isDirectory()){if(r.name.startsWith("explorer_tmp_")){let i=$e.join(e,r.name);y.warn("Removing stale temp directory from crashed sync",{dir:r.name});try{await Xt.rm(i,{recursive:!0,force:!0})}catch(a){y.error(`Failed to remove stale temp dir: ${r.name}`,a instanceof Error?a:new Error(String(a)))}}if(r.name.startsWith("place_")){let i=$e.join(e,r.name);try{let a=await Xt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);y.warn("Removing stale temp directory from crashed sync",{dir:`${r.name}/${o.name}`});try{await Xt.rm(s,{recursive:!0,force:!0})}catch(c){y.error(`Failed to remove stale temp dir: ${r.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;y.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import Gn from"path";import{promises as kN}from"fs";pe();var Cf=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(r),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(i.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=i.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await i.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(r),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let r=e.body,i=r.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=r.appliedFiles??r.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(i);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}this.ctx.touchRuntimePlace(i);let s=this.ctx.config.getPlaceRoot(i),c=0,l=[];for(let u of a){let p=Gn.resolve(s,u);if(!kf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await kN.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=Ct(m),g=Et(m);g&&h&&await o.writer.removeFromTree(g,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",path:`place_${i}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let r=e.body;if(!r.fsPath||!r.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=r,o=this.ctx.config.getSyncRoot();if(!kf(o,Gn.resolve(o,i))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:i});return}let s;if(r.placeId&&(s=this.ctx.places.get(r.placeId)),!s){let d=i.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=Gn.resolve(c,i);if(!kf(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"){u.resolveFile(l,"apply-studio"),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await kN.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:i,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({added:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(!i.fileWatcher){n.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(r),y.info("Reverse rescan completed",{placeId:r,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let r=e.resolveInstancePathFromFsPath(n);if(r)return r;let i=e.getExplorerRoot(),a=Gn.relative(i,n);if(a.startsWith("..")||a===""||Gn.isAbsolute(a))return null;let o=a.split(Gn.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Gn.basename(n),c=Gn.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Co.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Gn.join(d,f);let m=Gn.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Xe(p)}};pe();function IN(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={})}var Uo=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new cf(e),this.apiHandler=new Pf(this),this.changeProcessor=new If(this),this.initHandler=new $f(this),this.reverseHandler=new Cf(this),this.places=new sf({max:3,dispose:(n,r)=>{y.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===r&&(this.activeRuntimeSyncPlaceId=null),n.fileWatcher&&(n.fileWatcher.stop().catch(i=>{y.error("Error stopping file watcher during dispose",i)}),n.fileWatcher=null),n.writer.stopChangeLogFlusher(),n.incompleteSyncTimer&&(clearTimeout(n.incompleteSyncTimer),n.incompleteSyncTimer=null),n.index.saveToDisk().catch(i=>{y.error("Error saving index during dispose",i)}),n.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(n.activeFullSyncSessionId),n.tmpWriter&&(n.tmpWriter.stopChangeLogFlusher(),n.tmpWriter=null),n.tmpIndex=null,n.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let r=this.places.get(e);if(r&&n){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==i&&(y.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),r=void 0)}if(!r){let i=await this.config.resolvePlaceRoot(e,n),a=Yw.join(i,"explorer");await Lo.mkdir(i,{recursive:!0}),await Lo.mkdir(a,{recursive:!0});let o=new Yr(i,a);await o.loadFromDisk();let s=new jo(this.config,o,e),c=new pf(this.config,o,i);s.startChangeLogFlusher(),r={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:{...qi},applyModes:{...Bi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,r),y.info("Created new place context",{placeId:e,placeRoot:i})}return r}async handleSyncInit(e,n){try{IN(e.body);let r=bN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.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(i.phase){case"start":await this.initHandler.handleInitStart(a,i,n);break;case"chunk":await this.initHandler.handleInitChunk(a,i,n);break;case"complete":await this.initHandler.handleInitComplete(a,i,n);break}}catch(r){this.sendError(n,r,"handleSyncInit")}}async handleSyncUpdate(e,n){try{IN(e.body);let r=_N(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.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(o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=yN(o,i.changes);y.info("Sync update received",{placeId:a,clientId:i.clientId,changeCount:s.length,receivedCount:i.changes.length,types:s.map(f=>f.type)});let c=[],l=[],u=0,p=new Map;for(let f of s)try{let m=await this.changeProcessor.processChangeForPlace(o,f,p);m?l.push(m):u++}catch(m){let h="path"in f?f.path:"oldPath"in f?f.oldPath:"unknown";c.push({path:h,error:m instanceof Error?m.message:String(m)})}for(let f of p.values())try{await _f(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=xN&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let d={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(d.conflicts=l),n.status(200).json(d)}catch(r){this.sendError(n,r,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){if(this.activeRuntimeSyncPlaceId!==null&&this.activeRuntimeSyncPlaceId!==void 0){if(this.places.has(this.activeRuntimeSyncPlaceId))return this.activeRuntimeSyncPlaceId;this.activeRuntimeSyncPlaceId=null}if(this.activeFullSyncPlaceId!==null&&this.activeFullSyncPlaceId!==void 0&&this.places.has(this.activeFullSyncPlaceId))return this.activeRuntimeSyncPlaceId=this.activeFullSyncPlaceId,this.activeRuntimeSyncPlaceId;for(let[e,n]of this.places.entries())if(n.state==="syncing"||n.state==="initializing")return this.activeRuntimeSyncPlaceId=e,e;return this.activeRuntimeSyncPlaceId=null,null}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let r=e.query.placeId;if(r){let i=parseInt(r,10);if(!isNaN(i))return i}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let r=this.resolveQueryPlaceId(e);if(r==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let i=this.places.get(r);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(r);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...To]};n.status(200).json(o)}catch(r){this.sendError(n,r,"handleSyncStatus")}}async handleSyncStop(e,n){try{let r=e.body,i=r.placeId??this.getDefaultRuntimePlaceId();if(i==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){n.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(r.clientId&&a.activeClientId&&r.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(await a.fileWatcher.stop(),a.fileWatcher=null),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let s=this.config.getPlaceRoot(i),c=Yw.join(s,`explorer_tmp_${o}`);await Lo.rm(c,{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,this.activeFullSyncPlaceId===i&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(i),y.info("Sync stopped",{placeId:i,reason:r.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(r){this.sendError(n,r,"handleSyncStop")}}async handleSyncConfig(e,n){try{if(e.method==="GET"){n.status(200).json(this.config.getConfig());return}let r=wN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),n.status(200).json({status:"updated",config:this.config.getConfig()})}catch(r){this.sendError(n,r,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),y.info("SyncController initialized")}catch(e){y.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),y.info("SyncController shut down")}catch(e){y.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let r=e+".tmp."+NQ().slice(0,8);try{await Lo.writeFile(r,n,"utf-8"),await Lo.rename(r,e)}catch(i){throw await Lo.unlink(r).catch(()=>{}),i}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(r){this.sendError(n,r,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(r){this.sendError(n,r,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(r){this.sendError(n,r,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(r){this.sendError(n,r,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(r){this.sendError(n,r,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(r){this.sendError(n,r,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(r){this.sendError(n,r,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){y.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let n=Yw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new bf(n,e.index),e.writer.setOnWriteCallback(r=>{e.fileWatcher?.suppressPath(r)}),e.fileWatcher.setDirectionChecker(r=>{let i=FA(r);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(r=>{e.forwardRestoreQueue.includes(r)||(e.forwardRestoreQueue.push(r),y.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:r,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),y.info("File watcher started for reverse sync",{placeId:e.placeId})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}getConfigDirect(){return this.apiHandler.getConfigDirect()}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,r){await this.apiHandler.writeSyncedFile(e,n,r)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,r){let i=n instanceof Error?n.message:String(n);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}y.error(`SyncController.${r} failed`,n instanceof Error?n:new Error(i)),e.status(500).json({error:"Internal error",message:`${r}: ${i}`})}};import{randomUUID as OQ}from"crypto";function PN(t,e){let n=OQ(),r=n.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${r.substring(0,4)}-${r.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:i,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,mcpInstances:new Map,sseClients:new Set,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var $N=ri(xo(),1);pe();function CN(t){let e=$N.default.json({limit:"5mb"});t.app.use((n,r,i)=>{if(n.path.startsWith("/sync/")){i();return}e(n,r,i)}),t.app.use((n,r,i)=>{r.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((n,r,i)=>{y.debug(`${n.method} ${n.path}`,{ip:n.ip}),i()})}import{randomUUID as kl}from"crypto";pe();function DQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var We=DQ()??"2.0.9";Rf();function zN(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 LQ(t){let n=Date.now();return Array.from(t.pluginClients.values()).filter(r=>n-r.lastSeen<1e4)}function AN(t){let n=Date.now();for(let[r,i]of t.mcpInstances)i.lastSeen&&n-i.lastSeen>15e3&&(t.mcpInstances.delete(r),i.sessionId&&t.executionContextManager?.endSession(i.sessionId),y.debug("Removed stale MCP instance",{instanceId:r,lastSeen:i.lastSeen}))}function NN(t,e,n){let r=e.query.clientId;if(r&&t.pluginClients.has(r)){let i=t.pluginClients.get(r);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){y.warn("Invalid selection update request",{body:i}),n.status(400).json({error:"Invalid request body"});return}let a=r||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),y.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(i){y.error("Error handling selection update",i),n.status(500).json({error:"Internal server error"})}}function ON(t,e,n){let r=parseInt(e.query.maxAge)||3e4,i=Sl(t,r);i?n.json({cached:!0,...i}):n.json({cached:!1,message:"No cached selection available"})}function Sl(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let r;if(n)r=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!r||a.timestamp>r.timestamp)&&(r=a);if(!r)return null;let i=Date.now()-r.timestamp;return e===0||i<=e?r:null}function DN(t,e,n){try{let r=e.body;if(!r.clientId){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(r.clientId),a=Date.now(),o={clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,placeId:r.placeId,pluginVersion:r.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(r.clientId,o),t.pendingCommands.has(r.clientId)||t.pendingCommands.set(r.clientId,[]),r.pluginVersion&&(t.pluginVersion=r.pluginVersion),$t(t,"connection",{clientId:r.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:r.clientId,message:`Plugin connected \u2014 ${r.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(r.pluginVersion&&t.analyticsManager.setPluginVersion(r.pluginVersion),t.analyticsManager.trackPluginConnected()),y.info("Plugin client registered",{clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,isReconnect:!!i}),n.json({status:"ok",clientId:r.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,connectedAt:o.connectedAt,aiClientNames:zN(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering plugin client",r),n.status(500).json({error:"Internal server error"})}}function MN(t,e,n){let r=e.body?.clientId;if(!r){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(r);t.pluginClients.delete(r),t.pendingCommands.delete(r),i&&($t(t,"connection",{clientId:r,status:"disconnected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:r,message:`Plugin disconnected \u2014 ${r}`})),y.info("Plugin client unregistered",{clientId:r,existed:i}),n.json({status:"ok",existed:i})}function LN(t,e,n){try{let r=e.body;if(!r.instanceId){n.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:r.instanceId,...typeof r.sessionId=="string"?{sessionId:r.sessionId}:{},pid:r.pid,connectedAt:i,isServer:!1,lastSeen:i};r.aiClientName&&(a.aiClientName=r.aiClientName),r.cwd&&(a.cwd=r.cwd),"projectRoot"in r&&(a.projectRoot=r.projectRoot),t.mcpInstances.set(r.instanceId,a),$t(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:r.instanceId,status:"registered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:r.instanceId,message:`MCP registered \u2014 ${a.aiClientName??r.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),y.info("MCP instance registered (client mode)",{instanceId:r.instanceId,pid:r.pid,cwd:r.cwd}),n.json({status:"ok",instanceId:r.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering MCP instance",r),n.status(500).json({error:"Internal server error"})}}function UN(t,e,n){let r=e.body?.instanceId;if(!r){n.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(r),a=!!i;t.mcpInstances.delete(r),i?.sessionId&&t.executionContextManager?.endSession(i.sessionId),a&&($t(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:r,status:"unregistered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:r,message:`MCP unregistered \u2014 ${i?.aiClientName??r}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),y.info("MCP instance unregistered",{instanceId:r,existed:a}),n.json({status:"ok",existed:a})}function FN(t,e,n){let{instanceId:r,aiClientName:i}=e.body;if(r&&i){let a=t.mcpInstances.get(r);a&&(a.aiClientName=i)}n.json({status:"ok"})}function qN(t){let n=Date.now();for(let[r,i]of t.pluginClients)n-i.lastSeen>3e4&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return AN(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,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:ml(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function BN(t,e){e.json(qN(t))}function ZN(t,e,n){let r=e.query.instanceId;r&&t.mcpInstances.has(r)&&(t.mcpInstances.get(r).lastSeen=Date.now()),AN(t);let i=LQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:We,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:zN(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,dashboardSseClients:t.dashboardSseClients?.size??0,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,placeName:l.placeName,pluginVersion:l.pluginVersion,lastSeen:Date.now()-l.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(c)}function HN(t,e,n,r){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){y.warn("Shutdown request rejected from non-localhost",{ip:i}),n.status(403).json({error:"Forbidden: localhost only"});return}y.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await r(),y.info("Graceful shutdown completed"),process.exit(0)}catch(o){y.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function jf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){y.warn("Failed to fetch connection info from server",{error:e})}return qN(t)}pe();var zf={query_instances:{discriminator:"action",mapping:{get:"get_instance",children:"get_instance_children",find_child:"find_first_child",find_descendant:"find_first_descendant",wait_for_child:"wait_for_child",class_info:"get_class_info",search_name:"search_by_name",search_class:"search_by_class",search_property:"search_by_property",search_tag:"search_by_tag",file_tree:"get_file_tree",project_structure:"get_project_structure",descendants:"get_descendants",ancestors:"get_ancestors"},paramAliases:{search_by_name:{query:"pattern"},search_by_property:{root:"rootPath"},search_by_tag:{root:"rootPath"},get_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",mapping:{create:"create_instance",create_with_props:"create_instance_with_properties",delete:"delete_instance",clone:"clone_instance",move:"move_instance",rename:"rename_instance",pivot:"pivot_to",create_tree:"create_instance_tree",mass_create:"mass_create_instances",mass_delete:"mass_delete_instances",mass_duplicate:"mass_duplicate",smart_duplicate:"smart_duplicate"},paramAliases:{clone_instance:{path:"sourcePath"}}},manage_properties:{discriminator:"action",mapping:{get:"get_property",set:"set_property",get_all:"get_all_properties",set_multiple:"set_multiple_properties",get_attr:"get_attribute",set_attr:"set_attribute",get_all_attrs:"get_all_attributes",delete_attr:"delete_attribute",add_tag:"add_tag",remove_tag:"remove_tag",check_tag:"has_tag",get_tags:"get_tags",get_tagged:"get_tagged",set_calculated:"set_calculated_property",set_relative:"set_relative_property",mass_set:"mass_set_property",mass_get:"mass_get_property",modify_children:"modify_children"},paramAliases:{get_tagged:{tagName:"tag",root:"rootPath"},set_relative_property:{amount:"value"}}},manage_scripts:{discriminator:"action",mapping:{get_source:"get_script_source",set_source:"set_script_source",create:"create_script",delete:"delete_script",edit_replace:"edit_script_lines",edit_insert:"insert_script_lines",edit_delete:"delete_script_lines",search:"search_in_scripts",replace:"replace_in_scripts",get_dependencies:"get_script_dependencies"},paramAliases:{edit_script_lines:{newLines:"newContent"},insert_script_lines:{lines:"content"},replace_in_scripts:{pattern:"searchPattern"}}},manage_lighting:{discriminator:"action",mapping:{lighting:"set_lighting",atmosphere:"set_atmosphere",sky:"set_sky",terrain_props:"set_terrain",time:"set_time_of_day"}},manage_selection:{discriminator:"action",mapping:{get:"get_selection",set:"set_selection",clear:"clear_selection",cached:"get_cached_selection",context:"get_selection_context",details:"get_selection_details",add:"add_to_selection",remove:"remove_from_selection",watch:"watch_selection"}},manage_camera:{discriminator:"action",mapping:{info:"get_camera_info",focus_path:"focus_camera_path",focus_position:"focus_camera_position",suggest:"get_suggested_camera_view"},paramAliases:{get_suggested_camera_view:{path:"targetPath"}}},manage_tween:{discriminator:"action",mapping:{create:"create_tween",play:"play_tween",pause:"pause_tween",cancel:"cancel_tween"}},manage_audio:{discriminator:"action",mapping:{play:"play_sound",stop:"stop_sound",pause:"pause_sound",resume:"resume_sound",set_listener:"set_listener"}},manage_animation:{discriminator:"action",mapping:{load:"load_animation",play:"play_animation",stop:"stop_animation",get_tracks:"get_animation_tracks"}},manage_physics:{discriminator:"action",mapping:{register_group:"register_collision_group",set_collidable:"set_collidable",get_groups:"get_collision_groups"}},manage_effects:{discriminator:"action",mapping:{emit:"emit_particles",clear:"clear_particles",toggle:"toggle_effect"}},manage_terrain:{discriminator:"action",mapping:{fill_block:"terrain_fill_block",fill_ball:"terrain_fill_ball",fill_cylinder:"terrain_fill_cylinder",fill_wedge:"terrain_fill_wedge",clear_region:"terrain_clear",clear_bounds:"terrain_clear_region",replace_material:"terrain_replace_material",colors_get:"terrain_get_material_color",colors_set:"terrain_set_material_color",read_voxel:"terrain_read_voxel",read_voxels:"terrain_read_voxels",write_voxels:"terrain_write_voxels",generate:"terrain_generate",smooth:"terrain_smooth"}},spatial_query:{discriminator:"action",mapping:{raycast:"raycast",find_ground:"find_ground",check_placement:"check_placement",multi_raycast:"multi_raycast",scan_area:"scan_area",find_flat:"find_flat_areas",find_spawn:"find_spawn_positions",analyze_walkable:"analyze_walkable_area",spatial_map:"get_spatial_map",find_space:"find_empty_space",bounds:"get_bounds",snap_grid:"snap_to_grid",collision:"check_collision"},paramAliases:{get_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",mapping:{insert:"insert_model",info:"get_asset_info",search:"search_creator_store",search_insert:"search_and_insert_model",insert_free:"insert_free_model",insert_package:"insert_package",export:"export_selection"},paramAliases:{search_creator_store:{maxResults:"limit"}}},manage_sync:{discriminator:"action",mapping:{status:"sync_status",config:"sync_config",history:"sync_history",directions:"sync_directions",read_file:"sync_read_file",write_file:"sync_write_file",progress:"sync_progress"}},workspace_state:{discriminator:"action",mapping:{sync:"sync_workspace_state",snapshot:"get_workspace_snapshot",changes:"get_recent_changes",viewport:"get_viewport_info",clear_history:"clear_change_history",metadata:"get_workspace_metadata",scripts:"get_script_list",selection_info:"get_selection_info",clear_cache:"clear_state_cache"}},manage_logs:{discriminator:"action",mapping:{get:"get_output_logs",clear:"clear_output_logs",errors:"get_recent_errors"},paramAliases:{get_output_logs:{level:"type"}}},system_info:{discriminator:"action",mapping:{ping:"ping",connection:"get_connection_info",usage:"get_usage_status",place_info:"get_place_info",services:"get_services",studio_settings:"get_studio_settings",play:"start_playtest",stop:"stop_playtest",pause:"pause_playtest",resume:"resume_playtest",play_status:"get_play_status",run_test:"run_test"}}};var UQ={get_cached_selection:"internal",sync_status:"internal",sync_config:"internal",sync_history:"internal",sync_directions:"internal",sync_read_file:"internal",sync_write_file:"internal",sync_progress:"internal",get_connection_info:"internal",run_test:"internal"};function Af(t){return UQ[t]||"plugin"}var Nf=100,FQ=Object.entries(zf).reduce((t,[e,n])=>{for(let r of Object.values(n.mapping))t[r]=e;return t},{});function VN(t){return{toolName:FQ[t]||t,actionName:t}}function WN(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 GN(t,e,n){y.info("Plugin connected via SSE"),n.setHeader("Content-Type","text/event-stream"),n.setHeader("Cache-Control","no-cache"),n.setHeader("Connection","keep-alive"),t.sseClients.add(n),oS(n,{event:"command",id:kl(),data:{action:"connected",requestId:kl(),params:{serverVersion:We,timestamp:Date.now()}}});let r=setInterval(()=>{oS(n,{event:"command",id:kl(),data:{action:"keepalive",requestId:kl(),params:{timestamp:Date.now()}}})},3e4);n.on("close",()=>{y.info("Plugin disconnected from SSE"),clearInterval(r),t.sseClients.delete(n)})}function oS(t,e){let n=JSON.stringify(e.data);t.write(`event: ${e.event}
126
+ `),this.clearTTLTimerForPlace(i,a),i.index.clearAllHashes(),i.index.clearClassMappings(),i.tmpIndex){let m=i.tmpIndex.getExplorerRoot(),h=i.index.getExplorerRoot();for(let[g,x]of i.tmpIndex.getAllHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateHashByValue(_,x)}for(let[g,x]of i.tmpIndex.getAllFileHashes()){let w=$e.relative(m,g),_=$e.resolve(h,w);i.index.updateFileHashByValue(_,x)}i.index.resetNameCounters(),i.index.mergeNameMappingsFrom(i.tmpIndex)}if(i.tmpIndex=null,i.tmpWriter&&(i.tmpWriter.stopChangeLogFlusher(),i.tmpWriter=null),this.pendingServiceTrees.delete(a),i.collisionDirMap=null,await i.index.saveToDisk(),i.state="syncing",i.activeFullSyncSessionId=null,i.syncProgress=null,this.ctx.touchRuntimePlace(e),this.ctx.activeFullSyncPlaceId=null,await this.ctx.startFileWatcherForPlace(i),i.fileWatcher&&await i.fileWatcher.waitUntilReady(),u.size>0){for(let[m,h]of u){let g=$e.resolve(o,m);try{await Xt.mkdir($e.dirname(g),{recursive:!0}),await Xt.writeFile(g,h,"utf-8")}catch(x){y.warn("Failed to restore preserved file",{path:m,error:x instanceof Error?x.message:String(x)})}}y.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let g=$e.resolve(o,h);try{await Xt.unlink(g),i.index.removeHash(g),m++}catch(x){x.code!=="ENOENT"&&y.warn("Failed to delete preserved-as-deleted file",{path:h,error:x instanceof Error?x.message:String(x)})}}m>0&&(await i.index.saveToDisk(),y.info("Deleted locally-removed files from new sync",{placeId:e,count:m}))}i.writer.appendChangeLog(`FULL_SYNC_COMPLETE instances=${i.instanceCount} scripts=${i.scriptCount}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncComplete",direction:"forward",path:`place_${e}`,details:`instances:${i.instanceCount} scripts:${i.scriptCount}`}),y.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),r.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}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 r=setTimeout(async()=>{y.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:n});let i=this.ctx.config.getPlaceRoot(e.placeId),a=$e.join(i,`explorer_tmp_${n}`);try{await Xt.rm(a,{recursive:!0,force:!0})}catch(o){y.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))},vN);r&&typeof r=="object"&&"unref"in r&&r.unref(),e.incompleteSyncTimer=r}getOrCreatePendingServiceTree(e,n){let r=this.pendingServiceTrees.get(e);r||(r=new Map,this.pendingServiceTrees.set(e,r));let i=r.get(n.serviceName);if(i)return i;let a={serviceName:n.tree?.name??n.serviceName,serviceClassName:n.tree?.className??n.serviceClassName,zeroBasedChunkIndex:n.chunkIndex===0,instances:[]};return r.set(n.serviceName,a),a}isLastChunk(e,n,r){return r<=1?!0:e.zeroBasedChunkIndex?n>=r-1:n>=r}buildServiceTree(e,n){let r={name:n.serviceName,className:n.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of n.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=gt(i.originalPath);this.upsertTreeNode(r,a,o,i.className)}return this.recomputeTreeChildCounts(r),r.syncedAt=new Date().toISOString(),r}resolveEffectiveSegments(e,n){return e.tmpIndex?gt(n).map(r=>e.tmpIndex.sanitizeName(r)):gt(n)}rewritePendingEffectivePaths(e,n,r,i){let a=$e.relative(n,r).split($e.sep).filter(l=>l.length>0),o=$e.relative(n,i).split($e.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Xe(["game",...a]),c=Xe(["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,r,i){if(n.length<=1)return;let a=e.children;for(let o=1;o<n.length;o++){let s=n[o],c=r[o],l=o===n.length-1,u=a.find(p=>p.name===s);u?l&&(u.className=i,c!==void 0&&c!==s&&(u.originalName=c)):(u={name:s,className:l?i:"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=r=>{let i=r.children??[];r.children=i;for(let a of i)n(a);r.childCount=i.length};for(let r of e.children)n(r);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 Xt.readdir(e,{withFileTypes:!0});for(let r of n)if(r.isDirectory()){if(r.name.startsWith("explorer_tmp_")){let i=$e.join(e,r.name);y.warn("Removing stale temp directory from crashed sync",{dir:r.name});try{await Xt.rm(i,{recursive:!0,force:!0})}catch(a){y.error(`Failed to remove stale temp dir: ${r.name}`,a instanceof Error?a:new Error(String(a)))}}if(r.name.startsWith("place_")){let i=$e.join(e,r.name);try{let a=await Xt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=$e.join(i,o.name);y.warn("Removing stale temp directory from crashed sync",{dir:`${r.name}/${o.name}`});try{await Xt.rm(s,{recursive:!0,force:!0})}catch(c){y.error(`Failed to remove stale temp dir: ${r.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(n){if(n.code==="ENOENT")return;y.warn("Failed to scan for stale temp dirs",{error:n instanceof Error?n.message:String(n)})}}};import Gn from"path";import{promises as kN}from"fs";pe();var Cf=class{constructor(e){this.ctx=e}async handleReversePending(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(r),n.status(200).json(a)}async handleReverseSyncChanges(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(i.state!=="syncing"){n.status(400).json({error:"Not syncing",message:"Reverse sync is only available when sync is active"});return}let a=i.fileWatcher?.drainPendingChanges()??[],o=a.length>0?await i.reader.buildChangesFromPending(a):[];this.ctx.touchRuntimePlace(r),n.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,n){let r=e.body,i=r.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){n.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=r.appliedFiles??r.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(i);if(!o){n.status(404).json({error:"Place not found",message:`No sync context for place ${i}`});return}this.ctx.touchRuntimePlace(i);let s=this.ctx.config.getPlaceRoot(i),c=0,l=[];for(let u of a){let p=Gn.resolve(s,u);if(!kf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await kN.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=Ct(m),g=Et(m);g&&h&&await o.writer.removeFromTree(g,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",path:`place_${i}`,details:`applied:${c} failed:${l.length}`}),n.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,n){let r=e.body;if(!r.fsPath||!r.resolution){n.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=r,o=this.ctx.config.getSyncRoot();if(!kf(o,Gn.resolve(o,i))){n.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){n.status(200).json({status:"skipped",fsPath:i});return}let s;if(r.placeId&&(s=this.ctx.places.get(r.placeId)),!s){let d=i.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=Gn.resolve(c,i);if(!kf(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"){u.resolveFile(l,"apply-studio"),await u.saveToDisk(),n.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await kN.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:i,instancePath:h,fileType:m,content:d});return}n.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,n){let r=this.ctx.resolveQueryPlaceId(e);if(r==null){n.status(200).json({added:0});return}let i=this.ctx.places.get(r);if(!i){n.status(404).json({error:"Place not found",message:`No sync context for place ${r}`});return}if(!i.fileWatcher){n.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(r),y.info("Reverse rescan completed",{placeId:r,added:a}),n.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,n){let r=e.resolveInstancePathFromFsPath(n);if(r)return r;let i=e.getExplorerRoot(),a=Gn.relative(i,n);if(a.startsWith("..")||a===""||Gn.isAbsolute(a))return null;let o=a.split(Gn.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Gn.basename(n),c=Gn.dirname(n),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(Co.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Gn.join(d,f);let m=Gn.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Xe(p)}};pe();function IN(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={})}var Uo=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new cf(e),this.apiHandler=new Pf(this),this.changeProcessor=new If(this),this.initHandler=new $f(this),this.reverseHandler=new Cf(this),this.places=new sf({max:3,dispose:(n,r)=>{y.info("Disposing place context (LRU eviction)",{placeId:r}),this.activeFullSyncPlaceId===r&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===r&&(this.activeRuntimeSyncPlaceId=null),n.fileWatcher&&(n.fileWatcher.stop().catch(i=>{y.error("Error stopping file watcher during dispose",i)}),n.fileWatcher=null),n.writer.stopChangeLogFlusher(),n.incompleteSyncTimer&&(clearTimeout(n.incompleteSyncTimer),n.incompleteSyncTimer=null),n.index.saveToDisk().catch(i=>{y.error("Error saving index during dispose",i)}),n.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(n.activeFullSyncSessionId),n.tmpWriter&&(n.tmpWriter.stopChangeLogFlusher(),n.tmpWriter=null),n.tmpIndex=null,n.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,n){let r=this.places.get(e);if(r&&n){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,n);a!==i&&(y.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),r=void 0)}if(!r){let i=await this.config.resolvePlaceRoot(e,n),a=Yw.join(i,"explorer");await Lo.mkdir(i,{recursive:!0}),await Lo.mkdir(a,{recursive:!0});let o=new Yr(i,a);await o.loadFromDisk();let s=new jo(this.config,o,e),c=new pf(this.config,o,i);s.startChangeLogFlusher(),r={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:{...qi},applyModes:{...Bi},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,r),y.info("Created new place context",{placeId:e,placeRoot:i})}return r}async handleSyncInit(e,n){try{IN(e.body);let r=bN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.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(i.phase){case"start":await this.initHandler.handleInitStart(a,i,n);break;case"chunk":await this.initHandler.handleInitChunk(a,i,n);break;case"complete":await this.initHandler.handleInitComplete(a,i,n);break}}catch(r){this.sendError(n,r,"handleSyncInit")}}async handleSyncUpdate(e,n){try{IN(e.body);let r=_N(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data,a=i.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(o.activeFullSyncSessionId!==null||o.state==="initializing"){n.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=yN(o,i.changes);y.info("Sync update received",{placeId:a,clientId:i.clientId,changeCount:s.length,receivedCount:i.changes.length,types:s.map(f=>f.type)});let c=[],l=[],u=0,p=new Map;for(let f of s)try{let m=await this.changeProcessor.processChangeForPlace(o,f,p);m?l.push(m):u++}catch(m){let h="path"in f?f.path:"oldPath"in f?f.oldPath:"unknown";c.push({path:h,error:m instanceof Error?m.message:String(m)})}for(let f of p.values())try{await _f(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=xN&&(await o.index.saveToDisk(),o.changesSinceLastSave=0),o.lastIncrementalSync=new Date().toISOString();let d={processed:u,failed:c.length,errors:c,syncedAt:o.lastIncrementalSync};l.length>0&&(d.conflicts=l),n.status(200).json(d)}catch(r){this.sendError(n,r,"handleSyncUpdate")}}getDefaultRuntimePlaceId(){if(this.activeRuntimeSyncPlaceId!==null&&this.activeRuntimeSyncPlaceId!==void 0){if(this.places.has(this.activeRuntimeSyncPlaceId))return this.activeRuntimeSyncPlaceId;this.activeRuntimeSyncPlaceId=null}if(this.activeFullSyncPlaceId!==null&&this.activeFullSyncPlaceId!==void 0&&this.places.has(this.activeFullSyncPlaceId))return this.activeRuntimeSyncPlaceId=this.activeFullSyncPlaceId,this.activeRuntimeSyncPlaceId;for(let[e,n]of this.places.entries())if(n.state==="syncing"||n.state==="initializing")return this.activeRuntimeSyncPlaceId=e,e;return this.activeRuntimeSyncPlaceId=null,null}touchRuntimePlace(e){this.activeRuntimeSyncPlaceId=e}clearRuntimePlaceIfMatch(e){this.activeRuntimeSyncPlaceId===e&&(this.activeRuntimeSyncPlaceId=null)}resolveQueryPlaceId(e,n="runtime"){let r=e.query.placeId;if(r){let i=parseInt(r,10);if(!isNaN(i))return i}return n==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,n){try{let r=this.resolveQueryPlaceId(e);if(r==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let i=this.places.get(r);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};n.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(r);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(r),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...To]};n.status(200).json(o)}catch(r){this.sendError(n,r,"handleSyncStatus")}}async handleSyncStop(e,n){try{let r=e.body,i=r.placeId??this.getDefaultRuntimePlaceId();if(i==null){n.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){n.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(r.clientId&&a.activeClientId&&r.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${a.activeClientId}`});return}let o=a.activeFullSyncSessionId;if(o&&(this.initHandler.clearPreserveLocalFiles(o),this.initHandler.clearPendingServiceTrees(o)),a.fileWatcher&&(await a.fileWatcher.stop(),a.fileWatcher=null),a.incompleteSyncTimer&&(clearTimeout(a.incompleteSyncTimer),a.incompleteSyncTimer=null),o){let s=this.config.getPlaceRoot(i),c=Yw.join(s,`explorer_tmp_${o}`);await Lo.rm(c,{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,this.activeFullSyncPlaceId===i&&(this.activeFullSyncPlaceId=null),this.clearRuntimePlaceIfMatch(i),y.info("Sync stopped",{placeId:i,reason:r.reason??"requested"}),n.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(r){this.sendError(n,r,"handleSyncStop")}}async handleSyncConfig(e,n){try{if(e.method==="GET"){n.status(200).json(this.config.getConfig());return}let r=wN(e.body);if(!r.success){n.status(400).json({error:"Validation error",message:r.error});return}let i=r.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),n.status(200).json({status:"updated",config:this.config.getConfig()})}catch(r){this.sendError(n,r,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),y.info("SyncController initialized")}catch(e){y.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),y.info("SyncController shut down")}catch(e){y.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,n){let r=e+".tmp."+NQ().slice(0,8);try{await Lo.writeFile(r,n,"utf-8"),await Lo.rename(r,e)}catch(i){throw await Lo.unlink(r).catch(()=>{}),i}}async handlePreCheck(e,n){try{await this.apiHandler.handlePreCheck(e,n)}catch(r){this.sendError(n,r,"handlePreCheck")}}async handleSyncDirections(e,n){try{await this.apiHandler.handleSyncDirections(e,n)}catch(r){this.sendError(n,r,"handleSyncDirections")}}async handleForwardRestoreList(e,n){try{await this.apiHandler.handleForwardRestoreList(e,n)}catch(r){this.sendError(n,r,"handleForwardRestoreList")}}async handleReversePending(e,n){try{await this.reverseHandler.handleReversePending(e,n)}catch(r){this.sendError(n,r,"handleReversePending")}}async handleReverseSyncChanges(e,n){try{await this.reverseHandler.handleReverseSyncChanges(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,n){try{await this.reverseHandler.handleReverseSyncResult(e,n)}catch(r){this.sendError(n,r,"handleReverseSyncResult")}}async handleResolveConflict(e,n){try{await this.reverseHandler.handleResolveConflict(e,n)}catch(r){this.sendError(n,r,"handleResolveConflict")}}async handleReverseRescan(e,n){try{await this.reverseHandler.handleReverseRescan(e,n)}catch(r){this.sendError(n,r,"handleReverseRescan")}}async handleSyncHistory(e,n){try{await this.apiHandler.handleSyncHistory(e,n)}catch(r){this.sendError(n,r,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){y.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let n=Yw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new bf(n,e.index),e.writer.setOnWriteCallback(r=>{e.fileWatcher?.suppressPath(r)}),e.fileWatcher.setDirectionChecker(r=>{let i=FA(r);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(r=>{e.forwardRestoreQueue.includes(r)||(e.forwardRestoreQueue.push(r),y.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:r,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),y.info("File watcher started for reverse sync",{placeId:e.placeId})}getStatusSummary(){return this.apiHandler.getStatusSummary()}getDirectionForCategory(e){return this.apiHandler.getDirectionForCategory(e)}getStatusDirect(e){return this.apiHandler.getStatusDirect(e)}getConfigDirect(){return this.apiHandler.getConfigDirect()}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,r){await this.apiHandler.writeSyncedFile(e,n,r)}async executeViaDisk(e,n){return this.apiHandler.executeViaDisk(e,n)}sendError(e,n,r){let i=n instanceof Error?n.message:String(n);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=n.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}y.error(`SyncController.${r} failed`,n instanceof Error?n:new Error(i)),e.status(500).json({error:"Internal error",message:`${r}: ${i}`})}};import{randomUUID as OQ}from"crypto";function PN(t,e){let n=OQ(),r=n.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${r.substring(0,4)}-${r.substring(4,8)}`;return{config:t,app:e,instanceId:n,sessionId:i,startTime:Date.now(),baseUrl:`http://${t.httpHost}:${t.httpPort}`,commandQueue:new Map,pendingCommands:new Map,globalPendingCommands:[],totalCommandsProcessed:0,pluginClients:new Map,mcpInstances:new Map,sseClients:new Set,cachedSelectionMap:new Map,isClientMode:!1,clientModeHealthTimer:null,clientModeConsecutiveHealthFailures:0,clientModeUpstreamReachable:!0,clientModeUpstreamContextCaptureEnabled:!0,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,executionContextManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null,serverLastCommandAt:null}}var $N=ri(xo(),1);pe();function CN(t){let e=$N.default.json({limit:"5mb"});t.app.use((n,r,i)=>{if(n.path.startsWith("/sync/")){i();return}e(n,r,i)}),t.app.use((n,r,i)=>{r.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),r.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),r.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((n,r,i)=>{y.debug(`${n.method} ${n.path}`,{ip:n.ip}),i()})}import{randomUUID as kl}from"crypto";pe();function DQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var We=DQ()??"2.0.10";Rf();function zN(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 LQ(t){let n=Date.now();return Array.from(t.pluginClients.values()).filter(r=>n-r.lastSeen<1e4)}function AN(t){let n=Date.now();for(let[r,i]of t.mcpInstances)i.lastSeen&&n-i.lastSeen>15e3&&(t.mcpInstances.delete(r),i.sessionId&&t.executionContextManager?.endSession(i.sessionId),y.debug("Removed stale MCP instance",{instanceId:r,lastSeen:i.lastSeen}))}function NN(t,e,n){let r=e.query.clientId;if(r&&t.pluginClients.has(r)){let i=t.pluginClients.get(r);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){y.warn("Invalid selection update request",{body:i}),n.status(400).json({error:"Invalid request body"});return}let a=r||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),y.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),n.json({status:"ok",timestamp:o})}catch(i){y.error("Error handling selection update",i),n.status(500).json({error:"Internal server error"})}}function ON(t,e,n){let r=parseInt(e.query.maxAge)||3e4,i=Sl(t,r);i?n.json({cached:!0,...i}):n.json({cached:!1,message:"No cached selection available"})}function Sl(t,e=3e4,n){if(t.cachedSelectionMap.size===0)return null;let r;if(n)r=t.cachedSelectionMap.get(n);else for(let a of t.cachedSelectionMap.values())(!r||a.timestamp>r.timestamp)&&(r=a);if(!r)return null;let i=Date.now()-r.timestamp;return e===0||i<=e?r:null}function DN(t,e,n){try{let r=e.body;if(!r.clientId){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(r.clientId),a=Date.now(),o={clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,placeId:r.placeId,pluginVersion:r.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(r.clientId,o),t.pendingCommands.has(r.clientId)||t.pendingCommands.set(r.clientId,[]),r.pluginVersion&&(t.pluginVersion=r.pluginVersion),$t(t,"connection",{clientId:r.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:r.clientId,message:`Plugin connected \u2014 ${r.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(r.pluginVersion&&t.analyticsManager.setPluginVersion(r.pluginVersion),t.analyticsManager.trackPluginConnected()),y.info("Plugin client registered",{clientId:r.clientId,projectName:r.projectName,placeName:r.placeName,isReconnect:!!i}),n.json({status:"ok",clientId:r.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,connectedAt:o.connectedAt,aiClientNames:zN(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering plugin client",r),n.status(500).json({error:"Internal server error"})}}function MN(t,e,n){let r=e.body?.clientId;if(!r){n.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(r);t.pluginClients.delete(r),t.pendingCommands.delete(r),i&&($t(t,"connection",{clientId:r,status:"disconnected"}),dl(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:r,message:`Plugin disconnected \u2014 ${r}`})),y.info("Plugin client unregistered",{clientId:r,existed:i}),n.json({status:"ok",existed:i})}function LN(t,e,n){try{let r=e.body;if(!r.instanceId){n.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:r.instanceId,...typeof r.sessionId=="string"?{sessionId:r.sessionId}:{},pid:r.pid,connectedAt:i,isServer:!1,lastSeen:i};r.aiClientName&&(a.aiClientName=r.aiClientName),r.cwd&&(a.cwd=r.cwd),"projectRoot"in r&&(a.projectRoot=r.projectRoot),t.mcpInstances.set(r.instanceId,a),$t(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:r.instanceId,status:"registered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:r.instanceId,message:`MCP registered \u2014 ${a.aiClientName??r.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),y.info("MCP instance registered (client mode)",{instanceId:r.instanceId,pid:r.pid,cwd:r.cwd}),n.json({status:"ok",instanceId:r.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,mcpInstanceCount:t.mcpInstances.size+1})}catch(r){y.error("Error registering MCP instance",r),n.status(500).json({error:"Internal server error"})}}function UN(t,e,n){let r=e.body?.instanceId;if(!r){n.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(r),a=!!i;t.mcpInstances.delete(r),i?.sessionId&&t.executionContextManager?.endSession(i.sessionId),a&&($t(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:r,status:"unregistered"}),dl(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:r,message:`MCP unregistered \u2014 ${i?.aiClientName??r}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),y.info("MCP instance unregistered",{instanceId:r,existed:a}),n.json({status:"ok",existed:a})}function FN(t,e,n){let{instanceId:r,aiClientName:i}=e.body;if(r&&i){let a=t.mcpInstances.get(r);a&&(a.aiClientName=i)}n.json({status:"ok"})}function qN(t){let n=Date.now();for(let[r,i]of t.pluginClients)n-i.lastSeen>3e4&&(t.pluginClients.delete(r),t.pendingCommands.delete(r));return AN(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:We,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:ml(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function BN(t,e){e.json(qN(t))}function ZN(t,e,n){let r=e.query.instanceId;r&&t.mcpInstances.has(r)&&(t.mcpInstances.get(r).lastSeen=Date.now()),AN(t);let i=LQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:We,enableContextCapture:t.executionContextManager?.isEnabled()??t.config.enableContextCapture??!0,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:zN(t),pluginVersion:t.pluginVersion||void 0,sseClients:a,dashboardSseClients:t.dashboardSseClients?.size??0,pollingClients:i.length,pluginClients:i.map(l=>({clientId:l.clientId,projectName:l.projectName,placeName:l.placeName,pluginVersion:l.pluginVersion,lastSeen:Date.now()-l.lastSeen})),...t.isClientMode?{upstream:{reachable:t.clientModeUpstreamReachable,consecutiveFailures:t.clientModeConsecutiveHealthFailures,lastSuccessAt:t.clientModeLastHealthSuccessAt,lastFailureAt:t.clientModeLastHealthFailureAt,lastError:t.clientModeLastHealthError,baseUrl:t.baseUrl}}:{}};n.json(c)}function HN(t,e,n,r){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){y.warn("Shutdown request rejected from non-localhost",{ip:i}),n.status(403).json({error:"Forbidden: localhost only"});return}y.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),n.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await r(),y.info("Graceful shutdown completed"),process.exit(0)}catch(o){y.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function jf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){y.warn("Failed to fetch connection info from server",{error:e})}return qN(t)}pe();var zf={query_instances:{discriminator:"action",mapping:{get:"get_instance",children:"get_instance_children",find_child:"find_first_child",find_descendant:"find_first_descendant",wait_for_child:"wait_for_child",class_info:"get_class_info",search_name:"search_by_name",search_class:"search_by_class",search_property:"search_by_property",search_tag:"search_by_tag",file_tree:"get_file_tree",project_structure:"get_project_structure",descendants:"get_descendants",ancestors:"get_ancestors"},paramAliases:{search_by_name:{query:"pattern"},search_by_property:{root:"rootPath"},search_by_tag:{root:"rootPath"},get_project_structure:{root:"rootPath"}}},mutate_instances:{discriminator:"action",mapping:{create:"create_instance",create_with_props:"create_instance_with_properties",delete:"delete_instance",clone:"clone_instance",move:"move_instance",rename:"rename_instance",pivot:"pivot_to",create_tree:"create_instance_tree",mass_create:"mass_create_instances",mass_delete:"mass_delete_instances",mass_duplicate:"mass_duplicate",smart_duplicate:"smart_duplicate"},paramAliases:{clone_instance:{path:"sourcePath"}}},manage_properties:{discriminator:"action",mapping:{get:"get_property",set:"set_property",get_all:"get_all_properties",set_multiple:"set_multiple_properties",get_attr:"get_attribute",set_attr:"set_attribute",get_all_attrs:"get_all_attributes",delete_attr:"delete_attribute",add_tag:"add_tag",remove_tag:"remove_tag",check_tag:"has_tag",get_tags:"get_tags",get_tagged:"get_tagged",set_calculated:"set_calculated_property",set_relative:"set_relative_property",mass_set:"mass_set_property",mass_get:"mass_get_property",modify_children:"modify_children"},paramAliases:{get_tagged:{tagName:"tag",root:"rootPath"},set_relative_property:{amount:"value"}}},manage_scripts:{discriminator:"action",mapping:{get_source:"get_script_source",set_source:"set_script_source",create:"create_script",delete:"delete_script",edit_replace:"edit_script_lines",edit_insert:"insert_script_lines",edit_delete:"delete_script_lines",search:"search_in_scripts",replace:"replace_in_scripts",get_dependencies:"get_script_dependencies"},paramAliases:{edit_script_lines:{newLines:"newContent"},insert_script_lines:{lines:"content"},replace_in_scripts:{pattern:"searchPattern"}}},manage_lighting:{discriminator:"action",mapping:{lighting:"set_lighting",atmosphere:"set_atmosphere",sky:"set_sky",terrain_props:"set_terrain",time:"set_time_of_day"}},manage_selection:{discriminator:"action",mapping:{get:"get_selection",set:"set_selection",clear:"clear_selection",cached:"get_cached_selection",context:"get_selection_context",details:"get_selection_details",add:"add_to_selection",remove:"remove_from_selection",watch:"watch_selection"}},manage_camera:{discriminator:"action",mapping:{info:"get_camera_info",focus_path:"focus_camera_path",focus_position:"focus_camera_position",suggest:"get_suggested_camera_view"},paramAliases:{get_suggested_camera_view:{path:"targetPath"}}},manage_tween:{discriminator:"action",mapping:{create:"create_tween",play:"play_tween",pause:"pause_tween",cancel:"cancel_tween"}},manage_audio:{discriminator:"action",mapping:{play:"play_sound",stop:"stop_sound",pause:"pause_sound",resume:"resume_sound",set_listener:"set_listener"}},manage_animation:{discriminator:"action",mapping:{load:"load_animation",play:"play_animation",stop:"stop_animation",get_tracks:"get_animation_tracks"}},manage_physics:{discriminator:"action",mapping:{register_group:"register_collision_group",set_collidable:"set_collidable",get_groups:"get_collision_groups"}},manage_effects:{discriminator:"action",mapping:{emit:"emit_particles",clear:"clear_particles",toggle:"toggle_effect"}},manage_terrain:{discriminator:"action",mapping:{fill_block:"terrain_fill_block",fill_ball:"terrain_fill_ball",fill_cylinder:"terrain_fill_cylinder",fill_wedge:"terrain_fill_wedge",clear_region:"terrain_clear",clear_bounds:"terrain_clear_region",replace_material:"terrain_replace_material",colors_get:"terrain_get_material_color",colors_set:"terrain_set_material_color",read_voxel:"terrain_read_voxel",read_voxels:"terrain_read_voxels",write_voxels:"terrain_write_voxels",generate:"terrain_generate",smooth:"terrain_smooth"}},spatial_query:{discriminator:"action",mapping:{raycast:"raycast",find_ground:"find_ground",check_placement:"check_placement",multi_raycast:"multi_raycast",scan_area:"scan_area",find_flat:"find_flat_areas",find_spawn:"find_spawn_positions",analyze_walkable:"analyze_walkable_area",spatial_map:"get_spatial_map",find_space:"find_empty_space",bounds:"get_bounds",snap_grid:"snap_to_grid",collision:"check_collision"},paramAliases:{get_spatial_map:{path:"rootPath"}}},manage_assets:{discriminator:"action",mapping:{insert:"insert_model",info:"get_asset_info",search:"search_creator_store",search_insert:"search_and_insert_model",insert_free:"insert_free_model",insert_package:"insert_package",export:"export_selection"},paramAliases:{search_creator_store:{maxResults:"limit"}}},manage_sync:{discriminator:"action",mapping:{status:"sync_status",config:"sync_config",history:"sync_history",directions:"sync_directions",read_file:"sync_read_file",write_file:"sync_write_file",progress:"sync_progress"}},workspace_state:{discriminator:"action",mapping:{sync:"sync_workspace_state",snapshot:"get_workspace_snapshot",changes:"get_recent_changes",viewport:"get_viewport_info",clear_history:"clear_change_history",metadata:"get_workspace_metadata",scripts:"get_script_list",selection_info:"get_selection_info",clear_cache:"clear_state_cache"}},manage_logs:{discriminator:"action",mapping:{get:"get_output_logs",clear:"clear_output_logs",errors:"get_recent_errors"},paramAliases:{get_output_logs:{level:"type"}}},system_info:{discriminator:"action",mapping:{ping:"ping",connection:"get_connection_info",usage:"get_usage_status",place_info:"get_place_info",services:"get_services",studio_settings:"get_studio_settings",play:"start_playtest",stop:"stop_playtest",pause:"pause_playtest",resume:"resume_playtest",play_status:"get_play_status",run_test:"run_test"}}};var UQ={get_cached_selection:"internal",sync_status:"internal",sync_config:"internal",sync_history:"internal",sync_directions:"internal",sync_read_file:"internal",sync_write_file:"internal",sync_progress:"internal",get_connection_info:"internal",run_test:"internal"};function Af(t){return UQ[t]||"plugin"}var Nf=100,FQ=Object.entries(zf).reduce((t,[e,n])=>{for(let r of Object.values(n.mapping))t[r]=e;return t},{});function VN(t){return{toolName:FQ[t]||t,actionName:t}}function WN(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 GN(t,e,n){y.info("Plugin connected via SSE"),n.setHeader("Content-Type","text/event-stream"),n.setHeader("Cache-Control","no-cache"),n.setHeader("Connection","keep-alive"),t.sseClients.add(n),oS(n,{event:"command",id:kl(),data:{action:"connected",requestId:kl(),params:{serverVersion:We,timestamp:Date.now()}}});let r=setInterval(()=>{oS(n,{event:"command",id:kl(),data:{action:"keepalive",requestId:kl(),params:{timestamp:Date.now()}}})},3e4);n.on("close",()=>{y.info("Plugin disconnected from SSE"),clearInterval(r),t.sseClients.delete(n)})}function oS(t,e){let n=JSON.stringify(e.data);t.write(`event: ${e.event}
127
127
  `),t.write(`id: ${e.id}
128
128
  `),t.write(`data: ${n}
129
129
 
Binary file