@weppy/roblox-mcp 2.0.6 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +9 -0
- package/README.md +1 -0
- package/docs/en/installation/README.md +4 -0
- package/docs/en/installation/roblox-explorer.md +12 -9
- package/docs/en/sync/overview.md +2 -2
- package/docs/es/README.md +1 -0
- package/docs/es/installation/README.md +4 -0
- package/docs/es/installation/roblox-explorer.md +12 -9
- package/docs/es/sync/overview.md +2 -2
- package/docs/id/README.md +1 -0
- package/docs/id/installation/README.md +4 -0
- package/docs/id/installation/roblox-explorer.md +12 -9
- package/docs/id/sync/overview.md +2 -2
- package/docs/ja/README.md +1 -0
- package/docs/ja/installation/README.md +4 -0
- package/docs/ja/installation/roblox-explorer.md +12 -9
- package/docs/ja/sync/overview.md +2 -2
- package/docs/ko/README.md +1 -0
- package/docs/ko/installation/README.md +4 -0
- package/docs/ko/installation/roblox-explorer.md +12 -9
- package/docs/ko/sync/overview.md +2 -2
- package/docs/pt-br/README.md +1 -0
- package/docs/pt-br/installation/README.md +4 -0
- package/docs/pt-br/installation/roblox-explorer.md +12 -9
- package/docs/pt-br/sync/overview.md +2 -2
- package/llms-full.txt +1 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
- package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +1 -1
- package/plugins/weppy-roblox-mcp/dist/index.js +1 -1
- package/plugins/weppy-roblox-mcp/roblox-plugin/WeppyRobloxMCP.rbxm +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
|
+
"version": "2.0.7"
|
|
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.
|
|
16
|
+
"version": "2.0.7",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "hope1026"
|
|
19
19
|
},
|
package/CHANGELOG.md
CHANGED
|
@@ -18,6 +18,15 @@ All notable changes to this project will be documented in this file.
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
|
|
22
|
+
## [2.0.7] - 2026-03-25
|
|
23
|
+
|
|
24
|
+
### What's New
|
|
25
|
+
|
|
26
|
+
- Published Weppy Roblox Explorer to VS Code Marketplace and Open VSX Registry. Install directly from your editor's extension marketplace.
|
|
27
|
+
- Updated installation guide to reflect new marketplace links for Roblox Explorer.
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
## [2.0.6] - 2026-03-25
|
|
22
31
|
|
|
23
32
|
### Improved
|
package/README.md
CHANGED
|
@@ -115,6 +115,7 @@ The MCP server provides a web dashboard where you can check connection status, t
|
|
|
115
115
|
|
|
116
116
|
View the full instance tree of your Roblox Studio place directly inside VSCode. Navigate services, open synced scripts and property files, and track sync status — all without switching to Studio.
|
|
117
117
|
Roblox Explorer is a companion VSCode extension for sync data generated by Weppy Roblox MCP. Tree browsing works from synced files, and live sync state or direction indicators are enhanced when the local MCP server is running.
|
|
118
|
+
Install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) or [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer).
|
|
118
119
|
|
|
119
120
|

|
|
120
121
|
|
|
@@ -76,6 +76,10 @@ This optional extension requires the Roblox MCP setup above, because it reads `r
|
|
|
76
76
|
|
|
77
77
|
👉 [Roblox Explorer Installation Guide](roblox-explorer.md)
|
|
78
78
|
|
|
79
|
+
Direct listings:
|
|
80
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
81
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
82
|
+
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## After Installation
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (VSCode Extension)
|
|
2
2
|
|
|
3
3
|
Roblox Studio Explorer-like tree view for VSCode. Browse synced instance trees, open scripts directly, and track sync status — all inside your editor.
|
|
4
4
|
This is a companion extension for Weppy Roblox MCP, not a standalone Roblox integration.
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## Requirements
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ This is a companion extension for Weppy Roblox MCP, not a standalone Roblox inte
|
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
Search for **
|
|
16
|
+
Search for **Weppy Roblox Explorer** in the VSCode Extensions sidebar (`Ctrl+Shift+X` / `Cmd+Shift+X`) and click **Install**.
|
|
17
17
|
|
|
18
|
-
Or install directly from
|
|
18
|
+
Or install directly from:
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
Core tree browsing works from synced files on disk. Live sync state, direction indicators, and Explorer telemetry forwarding are available when the local MCP server is reachable.
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ Core tree browsing works from synced files on disk. Live sync state, direction i
|
|
|
43
46
|
|
|
44
47
|
| Command | Description |
|
|
45
48
|
|---------|-------------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | Manually refresh the instance tree |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | Search instances across all services |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | Open the file backing a selected instance |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | Copy the full instance path (e.g. `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | Show the backing file in the default VSCode explorer |
|
|
51
54
|
|
|
52
55
|
## Related
|
|
53
56
|
|
package/docs/en/sync/overview.md
CHANGED
|
@@ -22,10 +22,10 @@ Default local path is `roblox-project-sync/place_{placeId}/explorer`.
|
|
|
22
22
|
|
|
23
23
|
### Browse sync data in VSCode
|
|
24
24
|
|
|
25
|
-
Install the [
|
|
25
|
+
Install the [Weppy Roblox Explorer](../installation/roblox-explorer.md) extension to browse the synced instance tree in VSCode, just like in Roblox Studio.
|
|
26
26
|
Explorer reads the sync files generated here, and it can show additional live sync state or direction details when the local MCP server is running.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- Service/instance tree with Roblox class icons
|
|
31
31
|
- Click a script to open it for editing
|
package/docs/es/README.md
CHANGED
|
@@ -106,6 +106,7 @@ El Dashboard web proporcionado por el servidor MCP permite consultar en tiempo r
|
|
|
106
106
|
|
|
107
107
|
Visualiza el arbol completo de instancias de tu lugar en Roblox Studio directamente dentro de VSCode. Navega los servicios, abre scripts y archivos de propiedades sincronizados, y rastrea el estado de sincronizacion — todo sin cambiar a Studio.
|
|
108
108
|
Roblox Explorer es una extension complementaria de VSCode para los datos de sync generados por Weppy Roblox MCP. El arbol base funciona con los archivos sincronizados en disco, y los indicadores en vivo de estado sync o direction se enriquecen cuando el servidor MCP local esta en ejecucion.
|
|
109
|
+
Instálalo desde [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) o [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer).
|
|
109
110
|
|
|
110
111
|

|
|
111
112
|
|
|
@@ -71,6 +71,10 @@ Esta extensión opcional requiere completar antes la instalación de Roblox MCP,
|
|
|
71
71
|
|
|
72
72
|
👉 [Guía de instalación de Roblox Explorer](roblox-explorer.md)
|
|
73
73
|
|
|
74
|
+
Instalación directa:
|
|
75
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
76
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
77
|
+
|
|
74
78
|
---
|
|
75
79
|
|
|
76
80
|
## Después de la Instalación
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (Extensión VSCode)
|
|
2
2
|
|
|
3
3
|
Vista de árbol de instancias similar al Explorer de Roblox Studio, dentro de VSCode. Navega instancias sincronizadas, abre scripts directamente y consulta el estado de sincronización sin salir del editor.
|
|
4
4
|
Esta es una extensión complementaria para Weppy Roblox MCP, no una integración de Roblox independiente.
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## Requisitos
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ Esta es una extensión complementaria para Weppy Roblox MCP, no una integración
|
|
|
13
13
|
|
|
14
14
|
## Instalación
|
|
15
15
|
|
|
16
|
-
Busca **
|
|
16
|
+
Busca **Weppy Roblox Explorer** en la barra lateral de Extensiones de VSCode (`Ctrl+Shift+X` / `Cmd+Shift+X`) y haz clic en **Install**.
|
|
17
17
|
|
|
18
|
-
O instala directamente desde
|
|
18
|
+
O instala directamente desde estos marketplaces:
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
La navegación básica del árbol funciona con los archivos sincronizados en disco. El estado sync en vivo, los indicadores de direction y el reenvío de telemetría del Explorer están disponibles cuando el servidor MCP local es accesible.
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ La navegación básica del árbol funciona con los archivos sincronizados en dis
|
|
|
43
46
|
|
|
44
47
|
| Comando | Descripción |
|
|
45
48
|
|---------|-------------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | Actualizar manualmente el árbol de instancias |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | Buscar instancias en todos los servicios |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | Abrir el archivo de respaldo de una instancia seleccionada |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | Copiar la ruta completa de la instancia (ej. `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | Mostrar el archivo en el explorador predeterminado de VSCode |
|
|
51
54
|
|
|
52
55
|
## Relacionado
|
|
53
56
|
|
package/docs/es/sync/overview.md
CHANGED
|
@@ -22,10 +22,10 @@ La ruta local por defecto es `roblox-project-sync/place_{placeId}/explorer`.
|
|
|
22
22
|
|
|
23
23
|
### Explorar datos sincronizados en VSCode
|
|
24
24
|
|
|
25
|
-
Instala la extension [
|
|
25
|
+
Instala la extension [Weppy Roblox Explorer](../installation/roblox-explorer.md) para explorar el arbol de instancias sincronizado en VSCode, igual que en Roblox Studio.
|
|
26
26
|
Explorer lee los archivos sync generados aqui, y puede mostrar ademas estado sync en vivo e informacion de direction cuando el servidor MCP local esta en ejecucion.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- Arbol de servicios/instancias con iconos de clases Roblox
|
|
31
31
|
- Haz clic en un script para abrirlo y editarlo
|
package/docs/id/README.md
CHANGED
|
@@ -106,6 +106,7 @@ Dashboard berbasis web yang disediakan server MCP memungkinkan Anda melihat stat
|
|
|
106
106
|
|
|
107
107
|
Lihat seluruh pohon instance dari place Roblox Studio langsung di dalam VSCode. Navigasi service, buka script dan file properti yang tersinkron, dan pantau status sinkronisasi — semua tanpa beralih ke Studio.
|
|
108
108
|
Roblox Explorer adalah ekstensi pendamping VSCode untuk data sync yang dihasilkan oleh Weppy Roblox MCP. Penjelajahan tree dasar bekerja dari file yang sudah tersinkron di disk, dan indikator live untuk status sync atau direction menjadi lebih lengkap saat server MCP lokal sedang berjalan.
|
|
109
|
+
Instal dari [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) atau [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer).
|
|
109
110
|
|
|
110
111
|

|
|
111
112
|
|
|
@@ -76,6 +76,10 @@ Ekstensi opsional ini mengharuskan setup Roblox MCP di atas sudah selesai lebih
|
|
|
76
76
|
|
|
77
77
|
👉 [Panduan Instalasi Roblox Explorer](roblox-explorer.md)
|
|
78
78
|
|
|
79
|
+
Instal langsung:
|
|
80
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
81
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
82
|
+
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## Setelah Instalasi Selesai
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (Ekstensi VSCode)
|
|
2
2
|
|
|
3
3
|
Tampilan pohon instansi seperti Explorer Roblox Studio, langsung di VSCode. Jelajahi instansi yang tersinkronisasi, buka skrip langsung, dan pantau status sinkronisasi tanpa meninggalkan editor.
|
|
4
4
|
Ini adalah ekstensi pendamping untuk Weppy Roblox MCP, bukan integrasi Roblox yang berdiri sendiri.
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## Persyaratan
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ Ini adalah ekstensi pendamping untuk Weppy Roblox MCP, bukan integrasi Roblox ya
|
|
|
13
13
|
|
|
14
14
|
## Cara Instalasi
|
|
15
15
|
|
|
16
|
-
Cari **
|
|
16
|
+
Cari **Weppy Roblox Explorer** di sidebar Ekstensi VSCode (`Ctrl+Shift+X` / `Cmd+Shift+X`) dan klik **Install**.
|
|
17
17
|
|
|
18
|
-
Atau instal langsung dari
|
|
18
|
+
Atau instal langsung dari marketplace berikut:
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
Penjelajahan tree dasar bekerja dari file sync di disk. Status sync live, indikator direction, dan pengiriman telemetri Explorer tersedia saat server MCP lokal dapat dijangkau.
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ Penjelajahan tree dasar bekerja dari file sync di disk. Status sync live, indika
|
|
|
43
46
|
|
|
44
47
|
| Perintah | Deskripsi |
|
|
45
48
|
|----------|-----------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | Perbarui pohon instansi secara manual |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | Cari instansi di semua layanan |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | Buka file pendukung instansi yang dipilih |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | Salin jalur instansi lengkap (contoh: `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | Tampilkan file di explorer VSCode default |
|
|
51
54
|
|
|
52
55
|
## Terkait
|
|
53
56
|
|
package/docs/id/sync/overview.md
CHANGED
|
@@ -22,10 +22,10 @@ Path lokal default adalah `roblox-project-sync/place_{placeId}/explorer`.
|
|
|
22
22
|
|
|
23
23
|
### Jelajahi data sync di VSCode
|
|
24
24
|
|
|
25
|
-
Instal ekstensi [
|
|
25
|
+
Instal ekstensi [Weppy Roblox Explorer](../installation/roblox-explorer.md) untuk menjelajahi tree instance yang sudah tersinkron di VSCode, seperti di Roblox Studio.
|
|
26
26
|
Explorer membaca file sync yang dihasilkan di sini, dan juga bisa menampilkan status sync live serta informasi direction saat server MCP lokal sedang berjalan.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- Tree service/instance dengan ikon kelas Roblox
|
|
31
31
|
- Klik script untuk membukanya dan mengedit
|
package/docs/ja/README.md
CHANGED
|
@@ -106,6 +106,7 @@ MCPサーバーが提供するWebダッシュボードで、接続状態、ツ
|
|
|
106
106
|
|
|
107
107
|
Roblox Studioのインスタンスツリー全体をVSCode内で直接確認できます。サービスを探索し、同期されたスクリプトやプロパティファイルを開き、同期状態を追跡 — Studioに切り替える必要はありません。
|
|
108
108
|
Roblox Explorerは、Weppy Roblox MCPが生成するsyncデータを扱うcompanion VSCode拡張です。基本のツリー閲覧は同期済みファイルだけで動作し、ローカルMCPサーバーが動作中ならlive sync状態やdirection表示がより正確に反映されます。
|
|
109
|
+
[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) または [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer) からインストールできます。
|
|
109
110
|
|
|
110
111
|

|
|
111
112
|
|
|
@@ -76,6 +76,10 @@ GitHubからプラグインファイルをダウンロードして、Roblox Stud
|
|
|
76
76
|
|
|
77
77
|
👉 [Roblox Explorerインストールガイド](roblox-explorer.md)
|
|
78
78
|
|
|
79
|
+
直接インストール:
|
|
80
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
81
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
82
|
+
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## インストール後
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (VSCode拡張)
|
|
2
2
|
|
|
3
3
|
Roblox StudioのExplorerと同じインスタンスツリーをVSCodeで表示できる拡張機能です。同期済みインスタンスの探索、スクリプトの直接開封、Sync状態の確認をエディタ内で行えます。
|
|
4
4
|
この拡張はWeppy Roblox MCP用のcompanion拡張であり、単体のRoblox連携ツールではありません。
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## 要件
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ Roblox StudioのExplorerと同じインスタンスツリーをVSCodeで表示
|
|
|
13
13
|
|
|
14
14
|
## インストール方法
|
|
15
15
|
|
|
16
|
-
VSCode Extensionsサイドバー(`Ctrl+Shift+X` / `Cmd+Shift+X`)で **
|
|
16
|
+
VSCode Extensionsサイドバー(`Ctrl+Shift+X` / `Cmd+Shift+X`)で **Weppy Roblox Explorer** を検索し、**Install** をクリックします。
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
または次のマーケットプレイスから直接インストールできます。
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
基本のツリー閲覧はディスク上のsyncファイルだけで動作します。ローカルMCPサーバーに接続できる場合は、live sync状態、direction表示、Explorerテレメトリ転送も利用できます。
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ VSCode Extensionsサイドバー(`Ctrl+Shift+X` / `Cmd+Shift+X`)で **Weepy
|
|
|
43
46
|
|
|
44
47
|
| コマンド | 説明 |
|
|
45
48
|
|---------|------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | インスタンスツリーを手動で更新 |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | 全サービスでインスタンスを検索 |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | 選択したインスタンスのファイルを開く |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | 完全なインスタンスパスをコピー(例: `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | デフォルトのVSCodeエクスプローラーでファイルを表示 |
|
|
51
54
|
|
|
52
55
|
## 関連ドキュメント
|
|
53
56
|
|
package/docs/ja/sync/overview.md
CHANGED
|
@@ -22,10 +22,10 @@ Syncがない場合、AIはチャットに貼られた断片だけを見て判
|
|
|
22
22
|
|
|
23
23
|
### VSCodeで同期データを閲覧する
|
|
24
24
|
|
|
25
|
-
[
|
|
25
|
+
[Weppy Roblox Explorer](../installation/roblox-explorer.md) 拡張機能をインストールすると、Roblox Studioと同じようにVSCode上で同期済みインスタンスツリーを閲覧できます。
|
|
26
26
|
Explorerはここで生成されたsyncファイルを読み取り、ローカルMCPサーバーが動作中なら追加でlive sync状態とdirection情報も表示できます。
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- Robloxクラスアイコン付きのサービス/インスタンスツリー
|
|
31
31
|
- スクリプトをクリックして編集用に開く
|
package/docs/ko/README.md
CHANGED
|
@@ -106,6 +106,7 @@ MCP 서버가 제공하는 웹 대시보드에서 연결 상태, 도구 실행
|
|
|
106
106
|
|
|
107
107
|
Roblox Studio의 전체 인스턴스 트리를 VSCode 안에서 바로 확인할 수 있습니다. 서비스를 탐색하고, 동기화된 스크립트와 속성 파일을 열고, 동기화 상태를 추적하세요 — Studio로 전환할 필요 없이.
|
|
108
108
|
Roblox Explorer는 Weppy Roblox MCP가 생성한 sync 데이터를 위한 companion VSCode 확장입니다. 기본 트리 탐색은 동기화된 파일만으로 동작하고, 로컬 MCP 서버가 실행 중이면 실시간 sync 상태와 direction 표시가 더 정확하게 반영됩니다.
|
|
109
|
+
설치는 [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) 또는 [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)에서 할 수 있습니다.
|
|
109
110
|
|
|
110
111
|

|
|
111
112
|
|
|
@@ -76,6 +76,10 @@ GitHub에서 플러그인 파일을 다운로드한 뒤, Roblox Studio의 Plugin
|
|
|
76
76
|
|
|
77
77
|
👉 [Roblox Explorer 설치 가이드](roblox-explorer.md)
|
|
78
78
|
|
|
79
|
+
바로 설치:
|
|
80
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
81
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
82
|
+
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## 설치 완료 후
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (VSCode 확장)
|
|
2
2
|
|
|
3
3
|
Roblox Studio의 Explorer와 동일한 인스턴스 트리를 VSCode에서 볼 수 있는 확장입니다. 동기화된 인스턴스를 탐색하고, 스크립트를 바로 열고, Sync 상태를 에디터 안에서 확인할 수 있습니다.
|
|
4
4
|
이 확장은 Weppy Roblox MCP용 companion 확장이며, 단독 Roblox 연동 도구는 아닙니다.
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## 요구 사항
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ Roblox Studio의 Explorer와 동일한 인스턴스 트리를 VSCode에서 볼
|
|
|
13
13
|
|
|
14
14
|
## 설치 방법
|
|
15
15
|
|
|
16
|
-
VSCode Extensions 사이드바(`Ctrl+Shift+X` / `Cmd+Shift+X`)에서 **
|
|
16
|
+
VSCode Extensions 사이드바(`Ctrl+Shift+X` / `Cmd+Shift+X`)에서 **Weppy Roblox Explorer**를 검색하고 **Install**을 클릭합니다.
|
|
17
17
|
|
|
18
|
-
또는
|
|
18
|
+
또는 아래 마켓플레이스에서 바로 설치할 수 있습니다.
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
기본 트리 탐색은 디스크에 있는 sync 파일만으로 동작합니다. 로컬 MCP 서버에 연결되면 실시간 sync 상태, direction 표시, Explorer 텔레메트리 전달 기능을 함께 사용할 수 있습니다.
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ VSCode Extensions 사이드바(`Ctrl+Shift+X` / `Cmd+Shift+X`)에서 **Weepy Rob
|
|
|
43
46
|
|
|
44
47
|
| 명령어 | 설명 |
|
|
45
48
|
|--------|------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | 인스턴스 트리 수동 새로고침 |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | 모든 서비스에서 인스턴스 검색 |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | 선택한 인스턴스의 파일 열기 |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | 전체 인스턴스 경로 복사 (예: `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | 기본 VSCode 탐색기에서 파일 표시 |
|
|
51
54
|
|
|
52
55
|
## 관련 문서
|
|
53
56
|
|
package/docs/ko/sync/overview.md
CHANGED
|
@@ -22,10 +22,10 @@ Sync가 없으면 AI는 대화에 붙여 넣은 코드 일부만 보고 판단
|
|
|
22
22
|
|
|
23
23
|
### VSCode에서 Sync 데이터 탐색
|
|
24
24
|
|
|
25
|
-
[
|
|
25
|
+
[Weppy Roblox Explorer](../installation/roblox-explorer.md) 확장을 설치하면, 동기화된 인스턴스 트리를 Roblox Studio와 동일한 형태로 VSCode에서 탐색할 수 있습니다.
|
|
26
26
|
Explorer는 여기서 생성된 sync 파일을 읽고, 로컬 MCP 서버가 실행 중이면 추가로 실시간 sync 상태와 direction 정보를 표시할 수 있습니다.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- 서비스/인스턴스 트리를 Roblox 클래스 아이콘과 함께 표시
|
|
31
31
|
- 스크립트 파일을 클릭하면 바로 편집 가능
|
package/docs/pt-br/README.md
CHANGED
|
@@ -106,6 +106,7 @@ No dashboard web fornecido pelo servidor MCP, acompanhe em tempo real o status d
|
|
|
106
106
|
|
|
107
107
|
Visualize a arvore completa de instancias do seu lugar no Roblox Studio diretamente dentro do VSCode. Navegue pelos servicos, abra scripts e arquivos de propriedades sincronizados, e acompanhe o status de sincronizacao — tudo sem trocar para o Studio.
|
|
108
108
|
Roblox Explorer e uma extensao complementar do VSCode para os dados de sync gerados pelo Weppy Roblox MCP. A navegacao basica da arvore funciona a partir dos arquivos sincronizados em disco, e os indicadores ao vivo de status sync ou direction ficam mais completos quando o servidor MCP local esta em execucao.
|
|
109
|
+
Instale pela [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) ou pela [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer).
|
|
109
110
|
|
|
110
111
|

|
|
111
112
|
|
|
@@ -76,6 +76,10 @@ Esta extensao opcional exige que a instalacao do Roblox MCP acima ja esteja conc
|
|
|
76
76
|
|
|
77
77
|
👉 [Guia de instalação do Roblox Explorer](roblox-explorer.md)
|
|
78
78
|
|
|
79
|
+
Instalação direta:
|
|
80
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
81
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
82
|
+
|
|
79
83
|
---
|
|
80
84
|
|
|
81
85
|
## Depois da instalação
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Weppy Roblox Explorer (Extensão VSCode)
|
|
2
2
|
|
|
3
3
|
Visualização em árvore de instâncias similar ao Explorer do Roblox Studio, dentro do VSCode. Navegue por instâncias sincronizadas, abra scripts diretamente e acompanhe o status de sincronização sem sair do editor.
|
|
4
4
|
Esta é uma extensao complementar do Weppy Roblox MCP, e não uma integração Roblox independente.
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|
|
|
8
8
|
## Requisitos
|
|
9
9
|
|
|
@@ -13,9 +13,12 @@ Esta é uma extensao complementar do Weppy Roblox MCP, e não uma integração R
|
|
|
13
13
|
|
|
14
14
|
## Instalação
|
|
15
15
|
|
|
16
|
-
Pesquise **
|
|
16
|
+
Pesquise **Weppy Roblox Explorer** na barra lateral de Extensões do VSCode (`Ctrl+Shift+X` / `Cmd+Shift+X`) e clique em **Install**.
|
|
17
17
|
|
|
18
|
-
Ou instale diretamente
|
|
18
|
+
Ou instale diretamente por um destes marketplaces:
|
|
19
|
+
|
|
20
|
+
- [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer)
|
|
21
|
+
- [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer)
|
|
19
22
|
|
|
20
23
|
A navegação basica da arvore funciona a partir dos arquivos sincronizados em disco. Status sync ao vivo, indicadores de direction e envio de telemetria do Explorer ficam disponiveis quando o servidor MCP local está acessivel.
|
|
21
24
|
|
|
@@ -43,11 +46,11 @@ A navegação basica da arvore funciona a partir dos arquivos sincronizados em d
|
|
|
43
46
|
|
|
44
47
|
| Comando | Descrição |
|
|
45
48
|
|---------|-----------|
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
49
|
+
| `Weppy Roblox Explorer: Refresh` | Atualizar manualmente a árvore de instâncias |
|
|
50
|
+
| `Weppy Roblox Explorer: Search Instances` | Pesquisar instâncias em todos os serviços |
|
|
51
|
+
| `Weppy Roblox Explorer: Open Backing File` | Abrir o arquivo de suporte de uma instância selecionada |
|
|
52
|
+
| `Weppy Roblox Explorer: Copy Instance Path` | Copiar o caminho completo da instância (ex. `game.Workspace.Part`) |
|
|
53
|
+
| `Weppy Roblox Explorer: Reveal in Explorer` | Mostrar o arquivo no explorador padrão do VSCode |
|
|
51
54
|
|
|
52
55
|
## Relacionado
|
|
53
56
|
|
|
@@ -22,10 +22,10 @@ O caminho local padrao e `roblox-project-sync/place_{placeId}/explorer`.
|
|
|
22
22
|
|
|
23
23
|
### Explorar dados sincronizados no VSCode
|
|
24
24
|
|
|
25
|
-
Instale a extensao [
|
|
25
|
+
Instale a extensao [Weppy Roblox Explorer](../installation/roblox-explorer.md) para explorar a arvore de instancias sincronizada no VSCode, assim como no Roblox Studio.
|
|
26
26
|
O Explorer le os arquivos de sync gerados aqui e tambem pode mostrar status sync ao vivo e informacoes de direction quando o servidor MCP local estiver em execucao.
|
|
27
27
|
|
|
28
|
-

|
|
29
29
|
|
|
30
30
|
- Arvore de servicos/instancias com icones de classes Roblox
|
|
31
31
|
- Clique em um script para abri-lo e editar
|
package/llms-full.txt
CHANGED
|
@@ -201,7 +201,7 @@ In the agent pane, click ⋯ → MCP Servers → Manage MCP Servers → View raw
|
|
|
201
201
|
|
|
202
202
|
Browse synced instance trees inside VSCode with Roblox class icons.
|
|
203
203
|
|
|
204
|
-
Search for **
|
|
204
|
+
Search for **Weppy Roblox Explorer** in the VSCode Extensions sidebar and click Install, or install from [VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer) or [Open VSX](https://open-vsx.org/extension/weppy/weppy-roblox-explorer).
|
|
205
205
|
|
|
206
206
|
Features:
|
|
207
207
|
- Instance tree with Roblox class icons (dark/light theme)
|
package/llms.txt
CHANGED
|
@@ -66,5 +66,7 @@ Or manually:
|
|
|
66
66
|
|
|
67
67
|
- GitHub: https://github.com/hope1026/weppy-roblox-mcp
|
|
68
68
|
- npm: https://www.npmjs.com/package/@weppy/roblox-mcp
|
|
69
|
+
- Roblox Explorer (VS Code Marketplace): https://marketplace.visualstudio.com/items?itemName=weppy.weppy-roblox-explorer
|
|
70
|
+
- Roblox Explorer (Open VSX): https://open-vsx.org/extension/weppy/weppy-roblox-explorer
|
|
69
71
|
- Issues: https://github.com/hope1026/weppy-roblox-mcp/issues
|
|
70
72
|
- Discussions: https://github.com/hope1026/weppy-roblox-mcp/discussions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@weppy/roblox-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
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",
|
|
@@ -123,7 +123,7 @@ data: ${JSON.stringify(r)}
|
|
|
123
123
|
`).filter(v=>v.length>0)}catch(h){if(h.code==="ENOENT"){r.status(200).json({entries:[],total:0,hasMore:!1});return}throw h}let p=[];for(let h=u.length-1;h>=0;h--)try{let v=JSON.parse(u[h]);if(o&&v.direction!==o||s&&v.type!==s)continue;p.push(v)}catch{continue}let d=p.length,m={entries:p.slice(a,a+i),total:d,hasMore:a+i<d};r.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,r]of this.ctx.places.entries())if(r.state==="syncing")return{active:!0,placeId:e};return{active:!1}}getDirectionForCategory(e){let r;if(this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&(r=this.ctx.places.get(this.ctx.activeFullSyncPlaceId)),!r){let i=this.ctx.getDefaultRuntimePlaceId();i!=null&&(r=this.ctx.places.get(i))}if(!r)return"forward";let n=e;return r.directions[n]??"forward"}getStatusDirect(e){let r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...Li}};let n=this.ctx.places.get(r);if(!n)return{state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.ctx.config.getPlaceRoot(r),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0,applyModes:{...Li}};let i=n.fileWatcher?.getPendingCount()??0;return{state:n.state,instanceCount:n.instanceCount,scriptCount:n.scriptCount,lastFullSync:n.lastFullSync,lastIncrementalSync:n.lastIncrementalSync,syncRoot:this.ctx.config.getPlaceRoot(r),activeClientId:n.activeClientId,reverseSyncAvailable:i>0,modifiedFileCount:i,applyModes:{...n.applyModes}}}getConfigDirect(){return this.ctx.config.getConfig()}async getHistoryDirect(e,r){let n=parseInt(e,10),i=this.ctx.places.get(n);i&&await i.writer.flushHistory();let a=Math.min(Math.max(r?.limit??50,1),200),o=Math.max(r?.offset??0,0),s=this.ctx.config.getHistoryPath(n),c=[];try{c=(await Eo.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 r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{...Mi};let n=this.ctx.places.get(r);return n?{...n.directions}:{...Mi}}getProgressDirect(e){let r=e!=null?parseInt(e,10):this.ctx.getDefaultRuntimePlaceId();if(r==null)return{state:"idle",isSyncing:!1};let n=this.ctx.places.get(r);if(!n)return{state:"idle",isSyncing:!1};if(!n.syncProgress)return{state:n.state,isSyncing:!1,lastSync:{instanceCount:n.instanceCount,scriptCount:n.scriptCount,completedAt:n.lastFullSync}};let i=n.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:n.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,r){let n=parseInt(e,10),i=this.ctx.places.get(n);if(!i)throw new Error(`Place ${e} not found in sync cache`);let a=i.index.resolvePropsPath(r);try{return{content:await Eo.readFile(a,"utf-8"),path:a}}catch(s){if(s.code!=="ENOENT")throw s}for(let s of bo){let c=i.index.resolveScriptPath(r,s,!1);try{return{content:await Eo.readFile(c,"utf-8"),path:c}}catch{continue}}let o=i.index.resolveValuePath(r);try{return{content:await Eo.readFile(o,"utf-8"),path:o}}catch(s){if(s.code!=="ENOENT")throw s}throw new Error(`No synced file found for instance: ${r}`)}async writeSyncedFile(e,r,n){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(r,"Script",n,!1)}async executeViaDisk(e,r){let n=this.ctx.getDefaultRuntimePlaceId();if(n==null)throw new Error("No active sync place for disk execution");let i=this.ctx.places.get(n);if(!i)throw new Error(`Place ${n} not found in sync cache`);switch(e){case"set_script_source":{let a=r.scriptType||r.className||"Script";return await i.writer.writeScript(r.path,a,r.source,!1),{success:!0,path:r.path}}case"set_property":return await i.writer.writeProps(r.path,{className:r.className||"Instance",name:$t(r.path),properties:{[r.property]:r.value}}),{success:!0,path:r.path};default:throw new Error(`Disk execution not supported for action: ${e}`)}}};import Pe from"path";import{randomUUID as rQ}from"crypto";import{promises as Wt}from"fs";ue();var bf=class{constructor(e){this.ctx=e}preserveLocalFilesMap=new Map;pendingServiceTrees=new Map;async handleInitStart(e,r,n){if(r.previousPlaceId!==void 0&&r.previousPlaceId!==e&&(g.info("Place promotion detected",{from:r.previousPlaceId,to:e}),await this.ctx.config.promotePlaceRoot(r.previousPlaceId,e,r.placeName),this.ctx.places.get(r.previousPlaceId)&&this.ctx.places.delete(r.previousPlaceId),this.ctx.activeFullSyncPlaceId===r.previousPlaceId&&(this.ctx.activeFullSyncPlaceId=null),this.ctx.clearRuntimePlaceIfMatch(r.previousPlaceId)),this.ctx.activeFullSyncPlaceId!==null&&this.ctx.activeFullSyncPlaceId!==void 0&&this.ctx.activeFullSyncPlaceId!==e){n.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,r.placeName);if(i.activeClientId&&i.activeClientId!==r.clientId&&i.activeFullSyncSessionId!==null){n.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=r.clientId,i.placeName=r.placeName,r.directions&&typeof r.directions=="object"){let p=r.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),g.info("Sync directions received",{placeId:e,directions:i.directions})}else i.directions={...Mi};if(r.applyModes&&typeof r.applyModes=="object"){let p=r.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={...Li};i.forwardRestoreQueue=[];let a=rQ();i.activeFullSyncSessionId=a,this.pendingServiceTrees.set(a,new Map),i.instanceCount=0,i.scriptCount=0;let o=this.ctx.config.getPlaceRoot(e),s=Pe.join(o,`explorer_tmp_${a}`);await Wt.mkdir(s,{recursive:!0}),i.tmpIndex=new Qn(o,s),i.tmpWriter=new wo(this.ctx.config,i.tmpIndex,e),i.collisionDirMap=new Map;let c=r.preserveLocalFiles;Array.isArray(c)&&c.length>0&&(this.setPreserveLocalFiles(a,c),g.info("PreserveLocalFiles set for sync",{syncId:a,fileCount:c.length}));let l={version:1,placeId:r.placeId,placeName:r.placeName,lastFullSync:null,lastIncrementalSync:null,instanceCount:0,scriptCount:0,syncMode:"mirror"},u=Pe.join(o,".sync-meta.json");await Wt.mkdir(Pe.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:r.totalInstances,totalServices:r.totalServices,processedInstances:0,processedServices:0,currentService:null,currentChunkIndex:0,currentTotalChunks:0,bytesReceived:0,processedChunks:0},i.writer.appendChangeLog(`FULL_SYNC_START clientId=${r.clientId} placeId=${r.placeId}`),i.writer.appendHistory({timestamp:new Date().toISOString(),type:"fullSyncStart",direction:"forward",path:`place_${r.placeId}`,details:`services:${r.totalServices} instances:${r.totalInstances}`}),g.info("Full sync started",{syncId:a,clientId:r.clientId,placeId:r.placeId,placeName:r.placeName,totalServices:r.totalServices,totalInstances:r.totalInstances}),n.status(200).json({status:"started",syncId:i.activeFullSyncSessionId})}async handleInitChunk(e,r,n){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId||!i.tmpIndex||!i.tmpWriter){n.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&r.clientId&&i.activeClientId!==r.clientId){n.status(409).json({error:"Conflict",message:`This sync session belongs to client ${i.activeClientId}`});return}let a=i.activeFullSyncSessionId,o=this.getOrCreatePendingServiceTree(a,r),s=0,c=i.collisionDirMap;for(let u of r.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 ei(i.tmpIndex,d,m.from,m.to),this.rewritePendingEffectivePaths(o,i.tmpIndex.getExplorerRoot(),Pe.join(d,m.from),Pe.join(d,m.to)));let h=i.tmpIndex.resolveChildrenDir(u.path),v=Pe.join(d,f);v!==h&&c.set(h,v);let b=i.tmpIndex.sanitizeName(u.name),w=u;if(d!==p||f!==b){let k=i.tmpIndex.getExplorerRoot(),$=Pe.relative(k,d).split(Pe.sep).filter(A=>A.length>0),O=Je(["game",...$,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,r.chunkIndex,r.totalChunks);if(l){let u=this.buildServiceTree(i,o);await i.tmpWriter.writeTree(r.serviceName,u);let p=this.pendingServiceTrees.get(a);p?.delete(r.serviceName),p&&p.size===0&&this.pendingServiceTrees.delete(a)}i.syncProgress&&(i.syncProgress.processedInstances+=s,i.syncProgress.currentService=r.serviceName,i.syncProgress.currentChunkIndex=r.chunkIndex,i.syncProgress.currentTotalChunks=r.totalChunks,i.syncProgress.processedChunks++,i.syncProgress.bytesReceived+=JSON.stringify(r).length,l&&i.syncProgress.processedServices++),g.debug("Sync chunk processed",{placeId:e,serviceName:r.serviceName,chunkIndex:r.chunkIndex,totalChunks:r.totalChunks,processed:s}),n.status(200).json({processed:s,service:r.serviceName})}async handleInitComplete(e,r,n){let i=this.ctx.places.get(e);if(!i||!i.activeFullSyncSessionId){n.status(400).json({error:"No active sync session",message:"Call sync/init with phase=start first"});return}if(i.activeClientId&&r.clientId&&i.activeClientId!==r.clientId){n.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=Pe.join(o,"explorer"),c=Pe.join(o,`explorer_tmp_${a}`),l=this.getAndClearPreserveLocalFiles(a),u=new Map,p=[];if(l.length>0){for(let m of l){let h=Pe.resolve(o,m);try{let v=await Wt.readFile(h,"utf-8");u.set(m,v)}catch{p.push(m)}}g.info("Backed up local files for preservation",{placeId:e,requested:l.length,backed:u.size,deleted:p.length})}try{await Wt.rm(s,{recursive:!0,force:!0})}catch{}await Wt.rename(c,s),i.instanceCount=r.instanceCount,i.scriptCount=r.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=Pe.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[v,b]of i.tmpIndex.getAllHashes()){let w=Pe.relative(m,v),_=Pe.resolve(h,w);i.index.updateHashByValue(_,b)}for(let[v,b]of i.tmpIndex.getAllFileHashes()){let w=Pe.relative(m,v),_=Pe.resolve(h,w);i.index.updateFileHashByValue(_,b)}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 v=Pe.resolve(o,m);try{await Wt.mkdir(Pe.dirname(v),{recursive:!0}),await Wt.writeFile(v,h,"utf-8")}catch(b){g.warn("Failed to restore preserved file",{path:m,error:b instanceof Error?b.message:String(b)})}}g.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let v=Pe.resolve(o,h);try{await Wt.unlink(v),i.index.removeHash(v),m++}catch(b){b.code!=="ENOENT"&&g.warn("Failed to delete preserved-as-deleted file",{path:h,error:b instanceof Error?b.message:String(b)})}}m>0&&(await i.index.saveToDisk(),g.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}`}),g.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),n.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,r){this.preserveLocalFilesMap.set(e,r)}getAndClearPreserveLocalFiles(e){let r=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),r}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,r){let n=setTimeout(async()=>{g.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:r});let i=this.ctx.config.getPlaceRoot(e.placeId),a=Pe.join(i,`explorer_tmp_${r}`);try{await Wt.rm(a,{recursive:!0,force:!0})}catch(o){g.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===r&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(r),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))},iD);n&&typeof n=="object"&&"unref"in n&&n.unref(),e.incompleteSyncTimer=n}getOrCreatePendingServiceTree(e,r){let n=this.pendingServiceTrees.get(e);n||(n=new Map,this.pendingServiceTrees.set(e,n));let i=n.get(r.serviceName);if(i)return i;let a={serviceName:r.tree?.name??r.serviceName,serviceClassName:r.tree?.className??r.serviceClassName,zeroBasedChunkIndex:r.chunkIndex===0,instances:[]};return n.set(r.serviceName,a),a}isLastChunk(e,r,n){return n<=1?!0:e.zeroBasedChunkIndex?r>=n-1:r>=n}buildServiceTree(e,r){let n={name:r.serviceName,className:r.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of r.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=ft(i.originalPath);this.upsertTreeNode(n,a,o,i.className)}return this.recomputeTreeChildCounts(n),n.syncedAt=new Date().toISOString(),n}resolveEffectiveSegments(e,r){return e.tmpIndex?ft(r).map(n=>e.tmpIndex.sanitizeName(n)):ft(r)}rewritePendingEffectivePaths(e,r,n,i){let a=Pe.relative(r,n).split(Pe.sep).filter(l=>l.length>0),o=Pe.relative(r,i).split(Pe.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Je(["game",...a]),c=Je(["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,r,n,i){if(r.length<=1)return;let a=e.children;for(let o=1;o<r.length;o++){let s=r[o],c=n[o],l=o===r.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 r=n=>{let i=n.children??[];n.children=i;for(let a of i)r(a);n.childCount=i.length};for(let n of e.children)r(n);e.childCount=e.children.length}clearTTLTimerForPlace(e,r){e.incompleteSyncTimer&&e.activeFullSyncSessionId===r&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let r=await Wt.readdir(e,{withFileTypes:!0});for(let n of r)if(n.isDirectory()){if(n.name.startsWith("explorer_tmp_")){let i=Pe.join(e,n.name);g.warn("Removing stale temp directory from crashed sync",{dir:n.name});try{await Wt.rm(i,{recursive:!0,force:!0})}catch(a){g.error(`Failed to remove stale temp dir: ${n.name}`,a instanceof Error?a:new Error(String(a)))}}if(n.name.startsWith("place_")){let i=Pe.join(e,n.name);try{let a=await Wt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=Pe.join(i,o.name);g.warn("Removing stale temp directory from crashed sync",{dir:`${n.name}/${o.name}`});try{await Wt.rm(s,{recursive:!0,force:!0})}catch(c){g.error(`Failed to remove stale temp dir: ${n.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(r){if(r.code==="ENOENT")return;g.warn("Failed to scan for stale temp dirs",{error:r instanceof Error?r.message:String(r)})}}};import Hr from"path";import{promises as uD}from"fs";ue();var xf=class{constructor(e){this.ctx=e}async handleReversePending(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(n),r.status(200).json(a)}async handleReverseSyncChanges(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(i.state!=="syncing"){r.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(n),r.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,r){let n=e.body,i=n.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){r.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=n.appliedFiles??n.appliedPaths;if(!a||!Array.isArray(a)){r.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){r.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=Hr.resolve(s,u);if(!gf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await uD.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=$t(m),v=Pt(m);v&&h&&await o.writer.removeFromTree(v,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}`}),r.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,r){let n=e.body;if(!n.fsPath||!n.resolution){r.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=n,o=this.ctx.config.getSyncRoot();if(!gf(o,Hr.resolve(o,i))){r.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){r.status(200).json({status:"skipped",fsPath:i});return}let s;if(n.placeId&&(s=this.ctx.places.get(n.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){r.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=Hr.resolve(c,i);if(!gf(c,l)){r.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(),r.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await uD.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);r.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}r.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({added:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(!i.fileWatcher){r.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(n),g.info("Reverse rescan completed",{placeId:n,added:a}),r.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,r){let n=e.resolveInstancePathFromFsPath(r);if(n)return n;let i=e.getExplorerRoot(),a=Hr.relative(i,r);if(a.startsWith("..")||a===""||Hr.isAbsolute(a))return null;let o=a.split(Hr.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Hr.basename(r),c=Hr.dirname(r),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(yo.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Hr.join(d,f);let m=Hr.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Je(p)}};ue();function pD(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let r of e.instances)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={});if(Array.isArray(e.changes))for(let r of e.changes)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={})}var Co=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new Qd(e),this.apiHandler=new yf(this),this.changeProcessor=new vf(this),this.initHandler=new bf(this),this.reverseHandler=new xf(this),this.places=new Yd({max:3,dispose:(r,n)=>{g.info("Disposing place context (LRU eviction)",{placeId:n}),this.activeFullSyncPlaceId===n&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===n&&(this.activeRuntimeSyncPlaceId=null),r.fileWatcher&&(r.fileWatcher.stop().catch(i=>{g.error("Error stopping file watcher during dispose",i)}),r.fileWatcher=null),r.writer.stopChangeLogFlusher(),r.incompleteSyncTimer&&(clearTimeout(r.incompleteSyncTimer),r.incompleteSyncTimer=null),r.index.saveToDisk().catch(i=>{g.error("Error saving index during dispose",i)}),r.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(r.activeFullSyncSessionId),r.tmpWriter&&(r.tmpWriter.stopChangeLogFlusher(),r.tmpWriter=null),r.tmpIndex=null,r.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,r){let n=this.places.get(e);if(n&&r){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,r);a!==i&&(g.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),n=void 0)}if(!n){let i=await this.config.resolvePlaceRoot(e,r),a=Lw.join(i,"explorer");await To.mkdir(i,{recursive:!0}),await To.mkdir(a,{recursive:!0});let o=new Qn(i,a);await o.loadFromDisk();let s=new wo(this.config,o,e),c=new rf(this.config,o,i);s.startChangeLogFlusher(),n={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:{...Mi},applyModes:{...Li},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,n),g.info("Created new place context",{placeId:e,placeRoot:i})}return n}async handleSyncInit(e,r){try{pD(e.body);let n=oD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){r.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,r);break;case"chunk":await this.initHandler.handleInitChunk(a,i,r);break;case"complete":await this.initHandler.handleInitComplete(a,i,r);break}}catch(n){this.sendError(r,n,"handleSyncInit")}}async handleSyncUpdate(e,r){try{pD(e.body);let n=sD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){r.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"){r.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=nD(o,i.changes);g.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 ff(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=aD&&(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),r.status(200).json(d)}catch(n){this.sendError(r,n,"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,r]of this.places.entries())if(r.state==="syncing"||r.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,r="runtime"){let n=e.query.placeId;if(n){let i=parseInt(n,10);if(!isNaN(i))return i}return r==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,r){try{let n=this.resolveQueryPlaceId(e);if(n==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let i=this.places.get(n);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(n);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(n),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...xo]};r.status(200).json(o)}catch(n){this.sendError(r,n,"handleSyncStatus")}}async handleSyncStop(e,r){try{let n=e.body,i=n.placeId??this.getDefaultRuntimePlaceId();if(i==null){r.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){r.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(n.clientId&&a.activeClientId&&n.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){r.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=Lw.join(s,`explorer_tmp_${o}`);await To.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),g.info("Sync stopped",{placeId:i,reason:n.reason??"requested"}),r.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(n){this.sendError(r,n,"handleSyncStop")}}async handleSyncConfig(e,r){try{if(e.method==="GET"){r.status(200).json(this.config.getConfig());return}let n=cD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),r.status(200).json({status:"updated",config:this.config.getConfig()})}catch(n){this.sendError(r,n,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),g.info("SyncController initialized")}catch(e){g.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),g.info("SyncController shut down")}catch(e){g.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,r){let n=e+".tmp."+nQ().slice(0,8);try{await To.writeFile(n,r,"utf-8"),await To.rename(n,e)}catch(i){throw await To.unlink(n).catch(()=>{}),i}}async handlePreCheck(e,r){try{await this.apiHandler.handlePreCheck(e,r)}catch(n){this.sendError(r,n,"handlePreCheck")}}async handleSyncDirections(e,r){try{await this.apiHandler.handleSyncDirections(e,r)}catch(n){this.sendError(r,n,"handleSyncDirections")}}async handleForwardRestoreList(e,r){try{await this.apiHandler.handleForwardRestoreList(e,r)}catch(n){this.sendError(r,n,"handleForwardRestoreList")}}async handleReversePending(e,r){try{await this.reverseHandler.handleReversePending(e,r)}catch(n){this.sendError(r,n,"handleReversePending")}}async handleReverseSyncChanges(e,r){try{await this.reverseHandler.handleReverseSyncChanges(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,r){try{await this.reverseHandler.handleReverseSyncResult(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncResult")}}async handleResolveConflict(e,r){try{await this.reverseHandler.handleResolveConflict(e,r)}catch(n){this.sendError(r,n,"handleResolveConflict")}}async handleReverseRescan(e,r){try{await this.reverseHandler.handleReverseRescan(e,r)}catch(n){this.sendError(r,n,"handleReverseRescan")}}async handleSyncHistory(e,r){try{await this.apiHandler.handleSyncHistory(e,r)}catch(n){this.sendError(r,n,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){g.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let r=Lw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new df(r,e.index),e.writer.setOnWriteCallback(n=>{e.fileWatcher?.suppressPath(n)}),e.fileWatcher.setDirectionChecker(n=>{let i=IN(n);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(n=>{e.forwardRestoreQueue.includes(n)||(e.forwardRestoreQueue.push(n),g.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:n,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),g.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,r){return this.apiHandler.getHistoryDirect(e,r)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,r){return this.apiHandler.readSyncedFile(e,r)}async writeSyncedFile(e,r,n){await this.apiHandler.writeSyncedFile(e,r,n)}async executeViaDisk(e,r){return this.apiHandler.executeViaDisk(e,r)}sendError(e,r,n){let i=r instanceof Error?r.message:String(r);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=r.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}g.error(`SyncController.${n} failed`,r instanceof Error?r:new Error(i)),e.status(500).json({error:"Internal error",message:`${n}: ${i}`})}};import{randomUUID as iQ}from"crypto";function dD(t,e){let r=iQ(),n=r.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${n.substring(0,4)}-${n.substring(4,8)}`;return{config:t,app:e,instanceId:r,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,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null}}var fD=ri(uo(),1);ue();function mD(t){let e=fD.default.json({limit:"5mb"});t.app.use((r,n,i)=>{if(r.path.startsWith("/sync/")){i();return}e(r,n,i)}),t.app.use((r,n,i)=>{n.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),n.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((r,n,i)=>{g.debug(`${r.method} ${r.path}`,{ip:r.ip}),i()})}import{randomUUID as gl}from"crypto";ue();function aQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var Ve=aQ()??"2.0.6";Sf();function bD(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let r of t.mcpInstances.values())r.aiClientName&&e.add(r.aiClientName);return Array.from(e)}function sQ(t){let r=Date.now();return Array.from(t.pluginClients.values()).filter(n=>r-n.lastSeen<1e4)}function xD(t){let r=Date.now();for(let[n,i]of t.mcpInstances)i.lastSeen&&r-i.lastSeen>15e3&&(t.mcpInstances.delete(n),g.debug("Removed stale MCP instance",{instanceId:n,lastSeen:i.lastSeen}))}function _D(t,e,r){let n=e.query.clientId;if(n&&t.pluginClients.has(n)){let i=t.pluginClients.get(n);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){g.warn("Invalid selection update request",{body:i}),r.status(400).json({error:"Invalid request body"});return}let a=n||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),g.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),r.json({status:"ok",timestamp:o})}catch(i){g.error("Error handling selection update",i),r.status(500).json({error:"Internal server error"})}}function wD(t,e,r){let n=parseInt(e.query.maxAge)||3e4,i=hl(t,n);i?r.json({cached:!0,...i}):r.json({cached:!1,message:"No cached selection available"})}function hl(t,e=3e4,r){if(t.cachedSelectionMap.size===0)return null;let n;if(r)n=t.cachedSelectionMap.get(r);else for(let a of t.cachedSelectionMap.values())(!n||a.timestamp>n.timestamp)&&(n=a);if(!n)return null;let i=Date.now()-n.timestamp;return e===0||i<=e?n:null}function SD(t,e,r){try{let n=e.body;if(!n.clientId){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(n.clientId),a=Date.now(),o={clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,placeId:n.placeId,pluginVersion:n.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(n.clientId,o),t.pendingCommands.has(n.clientId)||t.pendingCommands.set(n.clientId,[]),n.pluginVersion&&(t.pluginVersion=n.pluginVersion),kt(t,"connection",{clientId:n.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),al(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:n.clientId,message:`Plugin connected \u2014 ${n.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(n.pluginVersion&&t.analyticsManager.setPluginVersion(n.pluginVersion),t.analyticsManager.trackPluginConnected()),g.info("Plugin client registered",{clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,isReconnect:!!i}),r.json({status:"ok",clientId:n.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,connectedAt:o.connectedAt,aiClientNames:bD(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){g.error("Error registering plugin client",n),r.status(500).json({error:"Internal server error"})}}function kD(t,e,r){let n=e.body?.clientId;if(!n){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(n);t.pluginClients.delete(n),t.pendingCommands.delete(n),i&&(kt(t,"connection",{clientId:n,status:"disconnected"}),al(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:n,message:`Plugin disconnected \u2014 ${n}`})),g.info("Plugin client unregistered",{clientId:n,existed:i}),r.json({status:"ok",existed:i})}function $D(t,e,r){try{let n=e.body;if(!n.instanceId){r.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:n.instanceId,pid:n.pid,connectedAt:i,isServer:!1,lastSeen:i};n.aiClientName&&(a.aiClientName=n.aiClientName),n.cwd&&(a.cwd=n.cwd),"projectRoot"in n&&(a.projectRoot=n.projectRoot),t.mcpInstances.set(n.instanceId,a),kt(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:n.instanceId,status:"registered"}),al(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:n.instanceId,message:`MCP registered \u2014 ${a.aiClientName??n.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),g.info("MCP instance registered (client mode)",{instanceId:n.instanceId,pid:n.pid,cwd:n.cwd}),r.json({status:"ok",instanceId:n.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){g.error("Error registering MCP instance",n),r.status(500).json({error:"Internal server error"})}}function PD(t,e,r){let n=e.body?.instanceId;if(!n){r.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(n),a=!!i;t.mcpInstances.delete(n),a&&(kt(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:n,status:"unregistered"}),al(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:n,message:`MCP unregistered \u2014 ${i?.aiClientName??n}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),g.info("MCP instance unregistered",{instanceId:n,existed:a}),r.json({status:"ok",existed:a})}function ID(t,e,r){let{instanceId:n,aiClientName:i}=e.body;if(n&&i){let a=t.mcpInstances.get(n);a&&(a.aiClientName=i)}r.json({status:"ok"})}function ED(t){let r=Date.now();for(let[n,i]of t.pluginClients)r-i.lastSeen>3e4&&(t.pluginClients.delete(n),t.pendingCommands.delete(n));return xD(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,uptime:r-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:mn(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function TD(t,e){e.json(ED(t))}function CD(t,e,r){let n=e.query.instanceId;n&&t.mcpInstances.has(n)&&(t.mcpInstances.get(n).lastSeen=Date.now()),xD(t);let i=sQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:Ve,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:bD(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}}:{}};r.json(c)}function RD(t,e,r,n){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){g.warn("Shutdown request rejected from non-localhost",{ip:i}),r.status(403).json({error:"Forbidden: localhost only"});return}g.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),r.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await n(),g.info("Graceful shutdown completed"),process.exit(0)}catch(o){g.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function kf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){g.warn("Failed to fetch connection info from server",{error:e})}return ED(t)}ue();var $f={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 cQ={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 Pf(t){return cQ[t]||"plugin"}var If=100,lQ=Object.entries($f).reduce((t,[e,r])=>{for(let n of Object.values(r.mapping))t[n]=e;return t},{});function zD(t){return{toolName:lQ[t]||t,actionName:t}}function jD(t,e,r){g.info("Plugin connected via SSE"),r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),t.sseClients.add(r),Ww(r,{event:"command",id:gl(),data:{action:"connected",requestId:gl(),params:{serverVersion:Ve,timestamp:Date.now()}}});let n=setInterval(()=>{Ww(r,{event:"command",id:gl(),data:{action:"keepalive",requestId:gl(),params:{timestamp:Date.now()}}})},3e4);r.on("close",()=>{g.info("Plugin disconnected from SSE"),clearInterval(n),t.sseClients.delete(r)})}function Ww(t,e){let r=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[v,b]of i.tmpIndex.getAllHashes()){let w=Pe.relative(m,v),_=Pe.resolve(h,w);i.index.updateHashByValue(_,b)}for(let[v,b]of i.tmpIndex.getAllFileHashes()){let w=Pe.relative(m,v),_=Pe.resolve(h,w);i.index.updateFileHashByValue(_,b)}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 v=Pe.resolve(o,m);try{await Wt.mkdir(Pe.dirname(v),{recursive:!0}),await Wt.writeFile(v,h,"utf-8")}catch(b){g.warn("Failed to restore preserved file",{path:m,error:b instanceof Error?b.message:String(b)})}}g.info("Restored preserved local files",{placeId:e,count:u.size})}if(p.length>0){let m=0;for(let h of p){let v=Pe.resolve(o,h);try{await Wt.unlink(v),i.index.removeHash(v),m++}catch(b){b.code!=="ENOENT"&&g.warn("Failed to delete preserved-as-deleted file",{path:h,error:b instanceof Error?b.message:String(b)})}}m>0&&(await i.index.saveToDisk(),g.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}`}),g.info("Full sync completed",{placeId:e,instanceCount:i.instanceCount,scriptCount:i.scriptCount}),n.status(200).json({status:"completed",instanceCount:i.instanceCount,scriptCount:i.scriptCount,syncRoot:this.ctx.config.getPlaceRoot(e)})}setPreserveLocalFiles(e,r){this.preserveLocalFilesMap.set(e,r)}getAndClearPreserveLocalFiles(e){let r=this.preserveLocalFilesMap.get(e)||[];return this.preserveLocalFilesMap.delete(e),r}clearPreserveLocalFiles(e){this.preserveLocalFilesMap.delete(e)}clearPendingServiceTrees(e){this.pendingServiceTrees.delete(e)}startTTLTimerForPlace(e,r){let n=setTimeout(async()=>{g.warn("Incomplete sync TTL expired, cleaning up",{placeId:e.placeId,syncId:r});let i=this.ctx.config.getPlaceRoot(e.placeId),a=Pe.join(i,`explorer_tmp_${r}`);try{await Wt.rm(a,{recursive:!0,force:!0})}catch(o){g.error("Failed to clean up expired temp dir",o instanceof Error?o:new Error(String(o)))}e.incompleteSyncTimer=null,e.activeFullSyncSessionId===r&&(e.activeFullSyncSessionId=null,e.activeClientId=null,e.state="idle",e.tmpIndex=null,this.pendingServiceTrees.delete(r),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))},iD);n&&typeof n=="object"&&"unref"in n&&n.unref(),e.incompleteSyncTimer=n}getOrCreatePendingServiceTree(e,r){let n=this.pendingServiceTrees.get(e);n||(n=new Map,this.pendingServiceTrees.set(e,n));let i=n.get(r.serviceName);if(i)return i;let a={serviceName:r.tree?.name??r.serviceName,serviceClassName:r.tree?.className??r.serviceClassName,zeroBasedChunkIndex:r.chunkIndex===0,instances:[]};return n.set(r.serviceName,a),a}isLastChunk(e,r,n){return n<=1?!0:e.zeroBasedChunkIndex?r>=n-1:r>=n}buildServiceTree(e,r){let n={name:r.serviceName,className:r.serviceClassName,childCount:0,children:[],syncedAt:new Date().toISOString()};for(let i of r.instances){let a=this.resolveEffectiveSegments(e,i.effectivePath),o=ft(i.originalPath);this.upsertTreeNode(n,a,o,i.className)}return this.recomputeTreeChildCounts(n),n.syncedAt=new Date().toISOString(),n}resolveEffectiveSegments(e,r){return e.tmpIndex?ft(r).map(n=>e.tmpIndex.sanitizeName(n)):ft(r)}rewritePendingEffectivePaths(e,r,n,i){let a=Pe.relative(r,n).split(Pe.sep).filter(l=>l.length>0),o=Pe.relative(r,i).split(Pe.sep).filter(l=>l.length>0);if(a.length===0||o.length===0)return;let s=Je(["game",...a]),c=Je(["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,r,n,i){if(r.length<=1)return;let a=e.children;for(let o=1;o<r.length;o++){let s=r[o],c=n[o],l=o===r.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 r=n=>{let i=n.children??[];n.children=i;for(let a of i)r(a);n.childCount=i.length};for(let n of e.children)r(n);e.childCount=e.children.length}clearTTLTimerForPlace(e,r){e.incompleteSyncTimer&&e.activeFullSyncSessionId===r&&(clearTimeout(e.incompleteSyncTimer),e.incompleteSyncTimer=null)}async cleanupStaleTempDirs(){let e=this.ctx.config.getSyncRoot();try{let r=await Wt.readdir(e,{withFileTypes:!0});for(let n of r)if(n.isDirectory()){if(n.name.startsWith("explorer_tmp_")){let i=Pe.join(e,n.name);g.warn("Removing stale temp directory from crashed sync",{dir:n.name});try{await Wt.rm(i,{recursive:!0,force:!0})}catch(a){g.error(`Failed to remove stale temp dir: ${n.name}`,a instanceof Error?a:new Error(String(a)))}}if(n.name.startsWith("place_")){let i=Pe.join(e,n.name);try{let a=await Wt.readdir(i,{withFileTypes:!0});for(let o of a)if(o.isDirectory()&&o.name.startsWith("explorer_tmp_")){let s=Pe.join(i,o.name);g.warn("Removing stale temp directory from crashed sync",{dir:`${n.name}/${o.name}`});try{await Wt.rm(s,{recursive:!0,force:!0})}catch(c){g.error(`Failed to remove stale temp dir: ${n.name}/${o.name}`,c instanceof Error?c:new Error(String(c)))}}}catch{continue}}}}catch(r){if(r.code==="ENOENT")return;g.warn("Failed to scan for stale temp dirs",{error:r instanceof Error?r.message:String(r)})}}};import Hr from"path";import{promises as uD}from"fs";ue();var xf=class{constructor(e){this.ctx=e}async handleReversePending(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({pending:0,hasConflicts:!1,lastDetected:null});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}let a={pending:i.fileWatcher?.getPendingCount()??0,hasConflicts:!1,lastDetected:i.fileWatcher?.getLastDetected()??null,forwardRestoreNeeded:i.forwardRestoreQueue.length};this.ctx.touchRuntimePlace(n),r.status(200).json(a)}async handleReverseSyncChanges(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({changes:[],count:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(i.state!=="syncing"){r.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(n),r.status(200).json({changes:o,count:o.length})}async handleReverseSyncResult(e,r){let n=e.body,i=n.placeId??this.ctx.getDefaultRuntimePlaceId();if(i==null){r.status(400).json({error:"Validation error",message:"placeId is required (in body or via active sync session)"});return}let a=n.appliedFiles??n.appliedPaths;if(!a||!Array.isArray(a)){r.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){r.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=Hr.resolve(s,u);if(!gf(s,p)){l.push({path:u,error:"Path is outside the place root"});continue}try{let d=await uD.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=$t(m),v=Pt(m);v&&h&&await o.writer.removeFromTree(v,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}`}),r.status(200).json({updated:c,failed:l.length,errors:l})}async handleResolveConflict(e,r){let n=e.body;if(!n.fsPath||!n.resolution){r.status(400).json({error:"Validation error",message:"fsPath and resolution are required"});return}let{fsPath:i,resolution:a}=n,o=this.ctx.config.getSyncRoot();if(!gf(o,Hr.resolve(o,i))){r.status(403).json({error:"Forbidden",message:"Path is outside the sync root"});return}if(a==="skip"){r.status(200).json({status:"skipped",fsPath:i});return}let s;if(n.placeId&&(s=this.ctx.places.get(n.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){r.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=Hr.resolve(c,i);if(!gf(c,l)){r.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(),r.status(200).json({status:"resolved",resolution:"apply-studio",fsPath:i});return}if(a==="apply-file"){let d=await uD.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);r.status(200).json({status:"resolved",resolution:"apply-file",fsPath:i,instancePath:h,fileType:m,content:d});return}r.status(400).json({error:"Invalid resolution",message:`Unknown resolution: ${a}`})}async handleReverseRescan(e,r){let n=this.ctx.resolveQueryPlaceId(e);if(n==null){r.status(200).json({added:0});return}let i=this.ctx.places.get(n);if(!i){r.status(404).json({error:"Place not found",message:`No sync context for place ${n}`});return}if(!i.fileWatcher){r.status(200).json({added:0});return}let a=await i.fileWatcher.rescan();this.ctx.touchRuntimePlace(n),g.info("Reverse rescan completed",{placeId:n,added:a}),r.status(200).json({added:a})}resolveInstancePathForAppliedPath(e,r){let n=e.resolveInstancePathFromFsPath(r);if(n)return n;let i=e.getExplorerRoot(),a=Hr.relative(i,r);if(a.startsWith("..")||a===""||Hr.isAbsolute(a))return null;let o=a.split(Hr.sep).filter(f=>f.length>0);if(o.length<2)return null;let s=Hr.basename(r),c=Hr.dirname(r),l=e.getOriginalInstance(c,s);if(l)return l.instancePath;let u=s.toLowerCase();if(yo.some(f=>u.endsWith(f))||u==="_tree.json")return null;let p=["game"],d=i;for(let f of o){d=Hr.join(d,f);let m=Hr.dirname(d);p.push(e.getOriginalNameForDir(m,f))}return Je(p)}};ue();function pD(t){if(!t||typeof t!="object")return;let e=t;if(Array.isArray(e.instances))for(let r of e.instances)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={});if(Array.isArray(e.changes))for(let r of e.changes)Array.isArray(r.properties)&&r.properties.length===0&&(r.properties={}),Array.isArray(r.attributes)&&r.attributes.length===0&&(r.attributes={})}var Co=class{config;places;apiHandler;changeProcessor;initHandler;reverseHandler;activeFullSyncPlaceId=null;activeRuntimeSyncPlaceId=null;constructor(e){this.config=new Qd(e),this.apiHandler=new yf(this),this.changeProcessor=new vf(this),this.initHandler=new bf(this),this.reverseHandler=new xf(this),this.places=new Yd({max:3,dispose:(r,n)=>{g.info("Disposing place context (LRU eviction)",{placeId:n}),this.activeFullSyncPlaceId===n&&(this.activeFullSyncPlaceId=null),this.activeRuntimeSyncPlaceId===n&&(this.activeRuntimeSyncPlaceId=null),r.fileWatcher&&(r.fileWatcher.stop().catch(i=>{g.error("Error stopping file watcher during dispose",i)}),r.fileWatcher=null),r.writer.stopChangeLogFlusher(),r.incompleteSyncTimer&&(clearTimeout(r.incompleteSyncTimer),r.incompleteSyncTimer=null),r.index.saveToDisk().catch(i=>{g.error("Error saving index during dispose",i)}),r.activeFullSyncSessionId&&this.initHandler.clearPendingServiceTrees(r.activeFullSyncSessionId),r.tmpWriter&&(r.tmpWriter.stopChangeLogFlusher(),r.tmpWriter=null),r.tmpIndex=null,r.collisionDirMap=null}})}getSyncRoot(){return this.config.getSyncRoot()}async getOrCreatePlaceContext(e,r){let n=this.places.get(e);if(n&&r){let i=this.config.getPlaceRoot(e),a=await this.config.resolvePlaceRoot(e,r);a!==i&&(g.info("Place root migrated, recreating context",{placeId:e,from:i,to:a}),this.places.delete(e),n=void 0)}if(!n){let i=await this.config.resolvePlaceRoot(e,r),a=Lw.join(i,"explorer");await To.mkdir(i,{recursive:!0}),await To.mkdir(a,{recursive:!0});let o=new Qn(i,a);await o.loadFromDisk();let s=new wo(this.config,o,e),c=new rf(this.config,o,i);s.startChangeLogFlusher(),n={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:{...Mi},applyModes:{...Li},forwardRestoreQueue:[],syncProgress:null,collisionDirMap:null},this.places.set(e,n),g.info("Created new place context",{placeId:e,placeRoot:i})}return n}async handleSyncInit(e,r){try{pD(e.body);let n=oD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.phase==="start"?i.placeId??null:i.phase==="chunk"||i.phase==="complete"?this.activeFullSyncPlaceId:null;if(a==null){r.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,r);break;case"chunk":await this.initHandler.handleInitChunk(a,i,r);break;case"complete":await this.initHandler.handleInitComplete(a,i,r);break}}catch(n){this.sendError(r,n,"handleSyncInit")}}async handleSyncUpdate(e,r){try{pD(e.body);let n=sD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data,a=i.placeId??this.getDefaultRuntimePlaceId();if(a==null){r.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"){r.status(409).json({error:"Conflict",message:`Full sync in progress for place ${a}`});return}o.activeClientId=i.clientId,this.touchRuntimePlace(a);let s=nD(o,i.changes);g.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 ff(o,f)}catch(m){c.push({path:f.instancePath,error:m instanceof Error?m.message:String(m)})}o.changesSinceLastSave+=u,o.changesSinceLastSave>=aD&&(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),r.status(200).json(d)}catch(n){this.sendError(r,n,"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,r]of this.places.entries())if(r.state==="syncing"||r.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,r="runtime"){let n=e.query.placeId;if(n){let i=parseInt(n,10);if(!isNaN(i))return i}return r==="full"?this.activeFullSyncPlaceId:this.getDefaultRuntimePlaceId()}async handleSyncStatus(e,r){try{let n=this.resolveQueryPlaceId(e);if(n==null){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getSyncRoot(),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let i=this.places.get(n);if(!i){let s={state:"idle",instanceCount:0,scriptCount:0,lastFullSync:null,lastIncrementalSync:null,syncRoot:this.config.getPlaceRoot(n),activeClientId:null,reverseSyncAvailable:!1,modifiedFileCount:0};r.status(200).json(s);return}let a=i.fileWatcher?.getPendingCount()??0;this.touchRuntimePlace(n);let o={state:i.state,instanceCount:i.instanceCount,scriptCount:i.scriptCount,lastFullSync:i.lastFullSync,lastIncrementalSync:i.lastIncrementalSync,syncRoot:this.config.getPlaceRoot(n),activeClientId:i.activeClientId,reverseSyncAvailable:a>0,modifiedFileCount:a,applyModes:i.applyModes,directions:i.directions,fileWatcherActive:i.fileWatcher!==null,forwardOnlyClasses:[...xo]};r.status(200).json(o)}catch(n){this.sendError(r,n,"handleSyncStatus")}}async handleSyncStop(e,r){try{let n=e.body,i=n.placeId??this.getDefaultRuntimePlaceId();if(i==null){r.status(200).json({status:"idle",state:"idle",placeId:null,message:"No active sync place"});return}let a=this.places.get(i);if(!a){r.status(200).json({status:"idle",state:"idle",placeId:i,message:`No sync context for place ${i}`});return}if(n.clientId&&a.activeClientId&&n.clientId!==a.activeClientId&&(a.activeFullSyncSessionId!==null||a.state==="syncing"||a.state==="initializing")){r.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=Lw.join(s,`explorer_tmp_${o}`);await To.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),g.info("Sync stopped",{placeId:i,reason:n.reason??"requested"}),r.status(200).json({status:"stopped",state:"idle",placeId:i})}catch(n){this.sendError(r,n,"handleSyncStop")}}async handleSyncConfig(e,r){try{if(e.method==="GET"){r.status(200).json(this.config.getConfig());return}let n=cD(e.body);if(!n.success){r.status(400).json({error:"Validation error",message:n.error});return}let i=n.data;i.maxDepth!==void 0&&this.config.updateConfig({maxDepth:i.maxDepth}),i.maxInstances!==void 0&&this.config.updateConfig({maxInstances:i.maxInstances}),r.status(200).json({status:"updated",config:this.config.getConfig()})}catch(n){this.sendError(r,n,"handleSyncConfig")}}async initialize(){try{await this.config.loadFromMeta(),await this.initHandler.cleanupStaleTempDirs(),g.info("SyncController initialized")}catch(e){g.error("SyncController initialization failed",e instanceof Error?e:new Error(String(e)))}}async shutdown(){try{this.places.clear(),g.info("SyncController shut down")}catch(e){g.error("SyncController shutdown error",e instanceof Error?e:new Error(String(e)))}}async atomicWriteFile(e,r){let n=e+".tmp."+nQ().slice(0,8);try{await To.writeFile(n,r,"utf-8"),await To.rename(n,e)}catch(i){throw await To.unlink(n).catch(()=>{}),i}}async handlePreCheck(e,r){try{await this.apiHandler.handlePreCheck(e,r)}catch(n){this.sendError(r,n,"handlePreCheck")}}async handleSyncDirections(e,r){try{await this.apiHandler.handleSyncDirections(e,r)}catch(n){this.sendError(r,n,"handleSyncDirections")}}async handleForwardRestoreList(e,r){try{await this.apiHandler.handleForwardRestoreList(e,r)}catch(n){this.sendError(r,n,"handleForwardRestoreList")}}async handleReversePending(e,r){try{await this.reverseHandler.handleReversePending(e,r)}catch(n){this.sendError(r,n,"handleReversePending")}}async handleReverseSyncChanges(e,r){try{await this.reverseHandler.handleReverseSyncChanges(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncChanges")}}async handleReverseSyncResult(e,r){try{await this.reverseHandler.handleReverseSyncResult(e,r)}catch(n){this.sendError(r,n,"handleReverseSyncResult")}}async handleResolveConflict(e,r){try{await this.reverseHandler.handleResolveConflict(e,r)}catch(n){this.sendError(r,n,"handleResolveConflict")}}async handleReverseRescan(e,r){try{await this.reverseHandler.handleReverseRescan(e,r)}catch(n){this.sendError(r,n,"handleReverseRescan")}}async handleSyncHistory(e,r){try{await this.apiHandler.handleSyncHistory(e,r)}catch(n){this.sendError(r,n,"handleSyncHistory")}}async startFileWatcherForPlace(e){if(e.state!=="syncing"){g.debug("Skipping file watcher start - place not syncing",{placeId:e.placeId,state:e.state});return}e.fileWatcher&&await e.fileWatcher.stop();let r=Lw.join(this.config.getPlaceRoot(e.placeId),"explorer");e.fileWatcher=new df(r,e.index),e.writer.setOnWriteCallback(n=>{e.fileWatcher?.suppressPath(n)}),e.fileWatcher.setDirectionChecker(n=>{let i=IN(n);return e.directions[i]}),e.fileWatcher.setOnForwardViolation(n=>{e.forwardRestoreQueue.includes(n)||(e.forwardRestoreQueue.push(n),g.info("Forward violation queued for restore",{placeId:e.placeId,relativePath:n,queueSize:e.forwardRestoreQueue.length}))}),await e.fileWatcher.start(),g.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,r){return this.apiHandler.getHistoryDirect(e,r)}getDirectionsDirect(e){return this.apiHandler.getDirectionsDirect(e)}getProgressDirect(e){return this.apiHandler.getProgressDirect(e)}async readSyncedFile(e,r){return this.apiHandler.readSyncedFile(e,r)}async writeSyncedFile(e,r,n){await this.apiHandler.writeSyncedFile(e,r,n)}async executeViaDisk(e,r){return this.apiHandler.executeViaDisk(e,r)}sendError(e,r,n){let i=r instanceof Error?r.message:String(r);if(i.includes("Path traversal detected")){e.status(403).json({error:"Forbidden",message:i});return}let a=r.code;if(a==="ENOSPC"||a==="EPERM"||a==="EACCES"){e.status(500).json({error:"Disk error",message:i});return}g.error(`SyncController.${n} failed`,r instanceof Error?r:new Error(i)),e.status(500).json({error:"Internal error",message:`${n}: ${i}`})}};import{randomUUID as iQ}from"crypto";function dD(t,e){let r=iQ(),n=r.replace(/-/g,"").substring(0,8).toUpperCase(),i=`${n.substring(0,4)}-${n.substring(4,8)}`;return{config:t,app:e,instanceId:r,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,clientModeLastHealthSuccessAt:null,clientModeLastHealthFailureAt:null,clientModeLastHealthError:null,historyManager:null,analyticsManager:null,licenseState:null,syncController:null,internalActionExecutor:null,activeSyncOwnerInstanceId:null,activeProjectRoot:null,playtestControlCommand:null,aiClientName:"",pluginVersion:"",syncedSessionToken:null}}var fD=ri(uo(),1);ue();function mD(t){let e=fD.default.json({limit:"5mb"});t.app.use((r,n,i)=>{if(r.path.startsWith("/sync/")){i();return}e(r,n,i)}),t.app.use((r,n,i)=>{n.setHeader("Access-Control-Allow-Origin","http://localhost:3002"),n.setHeader("Access-Control-Allow-Methods","GET, POST, OPTIONS"),n.setHeader("Access-Control-Allow-Headers","Content-Type"),i()}),t.app.use((r,n,i)=>{g.debug(`${r.method} ${r.path}`,{ip:r.ip}),i()})}import{randomUUID as gl}from"crypto";ue();function aQ(){let t=process.env.WEPPY_ROBLOX_MCP_VERSION?.trim();return t||null}var Ve=aQ()??"2.0.7";Sf();function bD(t){let e=new Set;t.aiClientName&&e.add(t.aiClientName);for(let r of t.mcpInstances.values())r.aiClientName&&e.add(r.aiClientName);return Array.from(e)}function sQ(t){let r=Date.now();return Array.from(t.pluginClients.values()).filter(n=>r-n.lastSeen<1e4)}function xD(t){let r=Date.now();for(let[n,i]of t.mcpInstances)i.lastSeen&&r-i.lastSeen>15e3&&(t.mcpInstances.delete(n),g.debug("Removed stale MCP instance",{instanceId:n,lastSeen:i.lastSeen}))}function _D(t,e,r){let n=e.query.clientId;if(n&&t.pluginClients.has(n)){let i=t.pluginClients.get(n);i.lastSeen=Date.now()}try{let i=e.body;if(!i||!Array.isArray(i.selection)||typeof i.count!="number"){g.warn("Invalid selection update request",{body:i}),r.status(400).json({error:"Invalid request body"});return}let a=n||"unknown",o=Date.now();t.cachedSelectionMap.set(a,{selection:i.selection,count:i.count,timestamp:o,clientId:a}),g.debug("Selection cache updated",{count:i.count,clientId:a,timestamp:o}),r.json({status:"ok",timestamp:o})}catch(i){g.error("Error handling selection update",i),r.status(500).json({error:"Internal server error"})}}function wD(t,e,r){let n=parseInt(e.query.maxAge)||3e4,i=hl(t,n);i?r.json({cached:!0,...i}):r.json({cached:!1,message:"No cached selection available"})}function hl(t,e=3e4,r){if(t.cachedSelectionMap.size===0)return null;let n;if(r)n=t.cachedSelectionMap.get(r);else for(let a of t.cachedSelectionMap.values())(!n||a.timestamp>n.timestamp)&&(n=a);if(!n)return null;let i=Date.now()-n.timestamp;return e===0||i<=e?n:null}function SD(t,e,r){try{let n=e.body;if(!n.clientId){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.get(n.clientId),a=Date.now(),o={clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,placeId:n.placeId,pluginVersion:n.pluginVersion,connectedAt:i?.connectedAt||a,lastSeen:a,commandsProcessed:i?.commandsProcessed||0,connectionType:"polling"};t.pluginClients.set(n.clientId,o),t.pendingCommands.has(n.clientId)||t.pendingCommands.set(n.clientId,[]),n.pluginVersion&&(t.pluginVersion=n.pluginVersion),kt(t,"connection",{clientId:n.clientId,placeId:o.projectName,placeName:o.placeName,status:"connected"}),al(t,{timestamp:new Date().toISOString(),type:"plugin",status:"connected",clientId:n.clientId,message:`Plugin connected \u2014 ${n.clientId}`,...o.placeId!==void 0?{placeId:o.placeId}:{},...o.placeName?{placeName:o.placeName}:{}}),t.analyticsManager&&(n.pluginVersion&&t.analyticsManager.setPluginVersion(n.pluginVersion),t.analyticsManager.trackPluginConnected()),g.info("Plugin client registered",{clientId:n.clientId,projectName:n.projectName,placeName:n.placeName,isReconnect:!!i}),r.json({status:"ok",clientId:n.clientId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,connectedAt:o.connectedAt,aiClientNames:bD(t),serverStartTime:t.startTime,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){g.error("Error registering plugin client",n),r.status(500).json({error:"Internal server error"})}}function kD(t,e,r){let n=e.body?.clientId;if(!n){r.status(400).json({error:"Missing clientId"});return}let i=t.pluginClients.has(n);t.pluginClients.delete(n),t.pendingCommands.delete(n),i&&(kt(t,"connection",{clientId:n,status:"disconnected"}),al(t,{timestamp:new Date().toISOString(),type:"plugin",status:"disconnected",clientId:n,message:`Plugin disconnected \u2014 ${n}`})),g.info("Plugin client unregistered",{clientId:n,existed:i}),r.json({status:"ok",existed:i})}function $D(t,e,r){try{let n=e.body;if(!n.instanceId){r.status(400).json({error:"Missing instanceId"});return}let i=Date.now(),a={instanceId:n.instanceId,pid:n.pid,connectedAt:i,isServer:!1,lastSeen:i};n.aiClientName&&(a.aiClientName=n.aiClientName),n.cwd&&(a.cwd=n.cwd),"projectRoot"in n&&(a.projectRoot=n.projectRoot),t.mcpInstances.set(n.instanceId,a),kt(t,"mcp_status",{aiClientName:a.aiClientName??"Unknown",instanceId:n.instanceId,status:"registered"}),al(t,{timestamp:new Date().toISOString(),type:"mcp",status:"registered",instanceId:n.instanceId,message:`MCP registered \u2014 ${a.aiClientName??n.instanceId}`,...a.aiClientName?{aiClientName:a.aiClientName}:{}}),g.info("MCP instance registered (client mode)",{instanceId:n.instanceId,pid:n.pid,cwd:n.cwd}),r.json({status:"ok",instanceId:n.instanceId,serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,mcpInstanceCount:t.mcpInstances.size+1})}catch(n){g.error("Error registering MCP instance",n),r.status(500).json({error:"Internal server error"})}}function PD(t,e,r){let n=e.body?.instanceId;if(!n){r.status(400).json({error:"Missing instanceId"});return}let i=t.mcpInstances.get(n),a=!!i;t.mcpInstances.delete(n),a&&(kt(t,"mcp_status",{aiClientName:i?.aiClientName??"Unknown",instanceId:n,status:"unregistered"}),al(t,{timestamp:new Date().toISOString(),type:"mcp",status:"unregistered",instanceId:n,message:`MCP unregistered \u2014 ${i?.aiClientName??n}`,...i?.aiClientName?{aiClientName:i.aiClientName}:{}})),g.info("MCP instance unregistered",{instanceId:n,existed:a}),r.json({status:"ok",existed:a})}function ID(t,e,r){let{instanceId:n,aiClientName:i}=e.body;if(n&&i){let a=t.mcpInstances.get(n);a&&(a.aiClientName=i)}r.json({status:"ok"})}function ED(t){let r=Date.now();for(let[n,i]of t.pluginClients)r-i.lastSeen>3e4&&(t.pluginClients.delete(n),t.pendingCommands.delete(n));return xD(t),{serverInstanceId:t.instanceId,sessionId:t.sessionId,mcpVersion:Ve,uptime:r-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:mn(),...t.aiClientName?{aiClientName:t.aiClientName}:{}},...Array.from(t.mcpInstances.values())],mcpInstanceCount:t.mcpInstances.size+1}}function TD(t,e){e.json(ED(t))}function CD(t,e,r){let n=e.query.instanceId;n&&t.mcpInstances.has(n)&&(t.mcpInstances.get(n).lastSeen=Date.now()),xD(t);let i=sQ(t),a=t.sseClients.size,c={...{status:"online",connectedClients:a+i.length,queuedCommands:t.commandQueue.size,uptime:Date.now()-t.startTime,version:Ve,isClientMode:t.isClientMode,pid:process.pid,sessionId:t.sessionId},instanceId:t.instanceId,mcpInstanceCount:t.mcpInstances.size+1,aiClientNames:bD(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}}:{}};r.json(c)}function RD(t,e,r,n){let i=e.ip||e.socket.remoteAddress||"";if(!(i==="127.0.0.1"||i==="::1"||i==="::ffff:127.0.0.1"||i==="localhost")){g.warn("Shutdown request rejected from non-localhost",{ip:i}),r.status(403).json({error:"Forbidden: localhost only"});return}g.info("Shutdown request received, initiating graceful shutdown",{requestedBy:i,uptime:Date.now()-t.startTime}),r.json({status:"shutting_down",message:"Server will shutdown gracefully",pid:process.pid}),setTimeout(async()=>{try{await n(),g.info("Graceful shutdown completed"),process.exit(0)}catch(o){g.error("Error during graceful shutdown",o),process.exit(1)}},100)}async function kf(t){if(t.isClientMode)try{let e=await fetch(`${t.baseUrl}/connection-info`);if(e.ok)return await e.json()}catch(e){g.warn("Failed to fetch connection info from server",{error:e})}return ED(t)}ue();var $f={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 cQ={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 Pf(t){return cQ[t]||"plugin"}var If=100,lQ=Object.entries($f).reduce((t,[e,r])=>{for(let n of Object.values(r.mapping))t[n]=e;return t},{});function zD(t){return{toolName:lQ[t]||t,actionName:t}}function jD(t,e,r){g.info("Plugin connected via SSE"),r.setHeader("Content-Type","text/event-stream"),r.setHeader("Cache-Control","no-cache"),r.setHeader("Connection","keep-alive"),t.sseClients.add(r),Ww(r,{event:"command",id:gl(),data:{action:"connected",requestId:gl(),params:{serverVersion:Ve,timestamp:Date.now()}}});let n=setInterval(()=>{Ww(r,{event:"command",id:gl(),data:{action:"keepalive",requestId:gl(),params:{timestamp:Date.now()}}})},3e4);r.on("close",()=>{g.info("Plugin disconnected from SSE"),clearInterval(n),t.sseClients.delete(r)})}function Ww(t,e){let r=JSON.stringify(e.data);t.write(`event: ${e.event}
|
|
127
127
|
`),t.write(`id: ${e.id}
|
|
128
128
|
`),t.write(`data: ${r}
|
|
129
129
|
|
|
Binary file
|