thrust-cli 1.0.10 → 1.0.12
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/frontend/index.html +480 -343
- package/frontend/index2.html +468 -0
- package/mcps/ThrustMCPBridge.cs +226 -0
- package/package.json +1 -3
- package/utils/config.js +13 -3
- package/utils/daemon.js +468 -91
- package/utils/daemon2.js +321 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
using UnityEngine;
|
|
2
|
+
using UnityEditor;
|
|
3
|
+
using System.Net;
|
|
4
|
+
using System.Threading;
|
|
5
|
+
using System.IO;
|
|
6
|
+
using System.Text;
|
|
7
|
+
using System.Collections.Generic;
|
|
8
|
+
using UnityEngine.SceneManagement;
|
|
9
|
+
|
|
10
|
+
[InitializeOnLoad]
|
|
11
|
+
public static class ThrustMCPBridge
|
|
12
|
+
{
|
|
13
|
+
private static HttpListener listener;
|
|
14
|
+
private static Thread listenerThread;
|
|
15
|
+
|
|
16
|
+
// Buffers
|
|
17
|
+
private static List<string> activityBuffer = new List<string>();
|
|
18
|
+
private static List<string> consoleBuffer = new List<string>();
|
|
19
|
+
private static readonly object bufferLock = new object();
|
|
20
|
+
|
|
21
|
+
// Main Thread Dispatcher variables
|
|
22
|
+
private static string pendingAction = null;
|
|
23
|
+
private static string pendingArgument = null;
|
|
24
|
+
private static string actionResult = null;
|
|
25
|
+
private static volatile bool isProcessingOnMainThread = false;
|
|
26
|
+
|
|
27
|
+
private const int PORT = 8081;
|
|
28
|
+
|
|
29
|
+
static ThrustMCPBridge()
|
|
30
|
+
{
|
|
31
|
+
// 1. Hook into Unity Editor Events
|
|
32
|
+
EditorApplication.hierarchyChanged += OnHierarchyChanged;
|
|
33
|
+
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
|
34
|
+
UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
|
|
35
|
+
Application.logMessageReceived += OnLogMessage;
|
|
36
|
+
|
|
37
|
+
// Hook into the main thread loop to process read requests
|
|
38
|
+
EditorApplication.update += OnEditorUpdate;
|
|
39
|
+
|
|
40
|
+
// 2. Start the local MCP Server
|
|
41
|
+
StartServer();
|
|
42
|
+
EditorApplication.quitting += StopServer;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private static void AddActivity(string activity)
|
|
46
|
+
{
|
|
47
|
+
lock (bufferLock)
|
|
48
|
+
{
|
|
49
|
+
activityBuffer.Add($"[{System.DateTime.Now.ToShortTimeString()}] {activity}");
|
|
50
|
+
if (activityBuffer.Count > 50) activityBuffer.RemoveAt(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private static void OnLogMessage(string logString, string stackTrace, LogType type)
|
|
55
|
+
{
|
|
56
|
+
lock (bufferLock)
|
|
57
|
+
{
|
|
58
|
+
if (type == LogType.Error || type == LogType.Exception || type == LogType.Warning)
|
|
59
|
+
{
|
|
60
|
+
consoleBuffer.Add($"[{type}] {logString}");
|
|
61
|
+
if (consoleBuffer.Count > 30) consoleBuffer.RemoveAt(0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static void OnHierarchyChanged() => AddActivity("Hierarchy changed.");
|
|
67
|
+
private static void OnSceneSaved(Scene scene) => AddActivity($"Saved scene: {scene.name}");
|
|
68
|
+
private static void OnPlayModeChanged(PlayModeStateChange state) => AddActivity($"Play mode state: {state}");
|
|
69
|
+
|
|
70
|
+
// --- HTTP SERVER LOOP (Background Thread) ---
|
|
71
|
+
private static void StartServer()
|
|
72
|
+
{
|
|
73
|
+
if (listener != null && listener.IsListening) return;
|
|
74
|
+
listener = new HttpListener();
|
|
75
|
+
listener.Prefixes.Add($"http://localhost:{PORT}/");
|
|
76
|
+
listener.Start();
|
|
77
|
+
|
|
78
|
+
listenerThread = new Thread(ListenForRequests) { IsBackground = true };
|
|
79
|
+
listenerThread.Start();
|
|
80
|
+
Debug.Log($"[Thrust MCP] Two-Way Unity Bridge active on port {PORT}...");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private static void ListenForRequests()
|
|
84
|
+
{
|
|
85
|
+
while (listener != null && listener.IsListening)
|
|
86
|
+
{
|
|
87
|
+
try { ProcessRequest(listener.GetContext()); }
|
|
88
|
+
catch (HttpListenerException) { /* Ignored on shutdown */ }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private static void ProcessRequest(HttpListenerContext context)
|
|
93
|
+
{
|
|
94
|
+
var request = context.Request;
|
|
95
|
+
var response = context.Response;
|
|
96
|
+
|
|
97
|
+
response.AddHeader("Access-Control-Allow-Origin", "*");
|
|
98
|
+
response.AddHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
99
|
+
|
|
100
|
+
if (request.HttpMethod == "OPTIONS") { SendResponse(response, ""); return; }
|
|
101
|
+
|
|
102
|
+
string requestBody;
|
|
103
|
+
using (var reader = new StreamReader(request.InputStream, request.ContentEncoding))
|
|
104
|
+
{
|
|
105
|
+
requestBody = reader.ReadToEnd();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
string resultText = "Unknown command";
|
|
109
|
+
|
|
110
|
+
// 1. Parse requested tool name (Crude JSON parsing to avoid dependencies)
|
|
111
|
+
if (requestBody.Contains("\"name\":\"get_recent_activity\"") || requestBody.Contains("\"name\": \"get_recent_activity\""))
|
|
112
|
+
{
|
|
113
|
+
lock (bufferLock)
|
|
114
|
+
{
|
|
115
|
+
resultText = activityBuffer.Count == 0 ? "No new activity" : string.Join("\\n", activityBuffer);
|
|
116
|
+
activityBuffer.Clear();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (requestBody.Contains("\"name\":\"get_console_logs\"") || requestBody.Contains("\"name\": \"get_console_logs\""))
|
|
120
|
+
{
|
|
121
|
+
lock (bufferLock)
|
|
122
|
+
{
|
|
123
|
+
resultText = consoleBuffer.Count == 0 ? "No recent errors/warnings." : string.Join("\\n", consoleBuffer).Replace("\"", "'");
|
|
124
|
+
consoleBuffer.Clear();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else if (requestBody.Contains("\"name\":\"get_hierarchy\"") || requestBody.Contains("\"name\": \"get_hierarchy\""))
|
|
128
|
+
{
|
|
129
|
+
resultText = DispatchToMainThread("get_hierarchy", "");
|
|
130
|
+
}
|
|
131
|
+
else if (requestBody.Contains("\"name\":\"get_properties\"") || requestBody.Contains("\"name\": \"get_properties\""))
|
|
132
|
+
{
|
|
133
|
+
// Extract the target parameter safely (e.g., "target": "Player")
|
|
134
|
+
string target = ExtractJsonValue(requestBody, "target");
|
|
135
|
+
resultText = DispatchToMainThread("get_properties", target);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Format as JSON-RPC response
|
|
139
|
+
string responseJson = $@"{{
|
|
140
|
+
""jsonrpc"": ""2.0"",
|
|
141
|
+
""result"": {{ ""content"": [{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
|
|
142
|
+
}}";
|
|
143
|
+
|
|
144
|
+
SendResponse(response, responseJson);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- MAIN THREAD DISPATCHER ---
|
|
148
|
+
// Passes the request to the main thread and waits for the result
|
|
149
|
+
private static string DispatchToMainThread(string action, string arg)
|
|
150
|
+
{
|
|
151
|
+
pendingAction = action;
|
|
152
|
+
pendingArgument = arg;
|
|
153
|
+
actionResult = null;
|
|
154
|
+
isProcessingOnMainThread = true;
|
|
155
|
+
|
|
156
|
+
// Block background thread until main thread finishes
|
|
157
|
+
while (isProcessingOnMainThread) { Thread.Sleep(10); }
|
|
158
|
+
|
|
159
|
+
return actionResult;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Executes on Unity's Main Thread
|
|
163
|
+
private static void OnEditorUpdate()
|
|
164
|
+
{
|
|
165
|
+
if (!isProcessingOnMainThread) return;
|
|
166
|
+
|
|
167
|
+
try
|
|
168
|
+
{
|
|
169
|
+
if (pendingAction == "get_hierarchy")
|
|
170
|
+
{
|
|
171
|
+
var rootObjects = SceneManager.GetActiveScene().GetRootGameObjects();
|
|
172
|
+
string hierarchy = "Active Scene Hierarchy:\\n";
|
|
173
|
+
foreach (var go in rootObjects) hierarchy += $"- {go.name}\\n";
|
|
174
|
+
actionResult = hierarchy;
|
|
175
|
+
}
|
|
176
|
+
else if (pendingAction == "get_properties")
|
|
177
|
+
{
|
|
178
|
+
if (string.IsNullOrEmpty(pendingArgument)) {
|
|
179
|
+
actionResult = "Error: No target specified.";
|
|
180
|
+
} else {
|
|
181
|
+
GameObject go = GameObject.Find(pendingArgument);
|
|
182
|
+
if (go == null) {
|
|
183
|
+
actionResult = $"GameObject '{pendingArgument}' not found in active scene.";
|
|
184
|
+
} else {
|
|
185
|
+
actionResult = $"Properties for '{go.name}':\\nPosition: {go.transform.position}\\nRotation: {go.transform.eulerAngles}\\nComponents:\\n";
|
|
186
|
+
foreach (var comp in go.GetComponents<Component>())
|
|
187
|
+
{
|
|
188
|
+
if(comp != null) actionResult += $"- {comp.GetType().Name}\\n";
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (System.Exception e) { actionResult = $"Error reading Unity state: {e.Message}"; }
|
|
195
|
+
|
|
196
|
+
isProcessingOnMainThread = false; // Release the background thread
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Utility
|
|
200
|
+
private static string ExtractJsonValue(string json, string key)
|
|
201
|
+
{
|
|
202
|
+
string search = $"\"{key}\":";
|
|
203
|
+
int index = json.IndexOf(search);
|
|
204
|
+
if (index == -1) search = $"\"{key}\" :"; index = json.IndexOf(search);
|
|
205
|
+
if (index == -1) return "";
|
|
206
|
+
|
|
207
|
+
int start = json.IndexOf("\"", index + search.Length) + 1;
|
|
208
|
+
int end = json.IndexOf("\"", start);
|
|
209
|
+
return json.Substring(start, end - start);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private static void SendResponse(HttpListenerResponse response, string json)
|
|
213
|
+
{
|
|
214
|
+
byte[] buffer = Encoding.UTF8.GetBytes(json);
|
|
215
|
+
response.ContentType = "application/json";
|
|
216
|
+
response.ContentLength64 = buffer.Length;
|
|
217
|
+
response.OutputStream.Write(buffer, 0, buffer.Length);
|
|
218
|
+
response.OutputStream.Close();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private static void StopServer()
|
|
222
|
+
{
|
|
223
|
+
if (listener != null) { listener.Stop(); listener.Close(); listener = null; }
|
|
224
|
+
if (listenerThread != null) { listenerThread.Abort(); listenerThread = null; }
|
|
225
|
+
}
|
|
226
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thrust-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "The local agent for Thrust AI Director",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://thrust.web.app",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
],
|
|
20
20
|
"author": "Thrust",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"auto-launch": "^5.0.6",
|
|
23
22
|
"axios": "^1.13.2",
|
|
24
23
|
"chokidar": "^3.6.0",
|
|
25
24
|
"commander": "^12.0.0",
|
|
@@ -27,7 +26,6 @@
|
|
|
27
26
|
"express": "5.2.1",
|
|
28
27
|
"inquirer": "^9.3.8",
|
|
29
28
|
"inquirer-file-tree-selection-prompt": "^2.0.5",
|
|
30
|
-
"jsonwebtoken": "^9.0.2",
|
|
31
29
|
"open": "11.0.0",
|
|
32
30
|
"semver": "^7.6.0",
|
|
33
31
|
"simple-git": "^3.22.0",
|
package/utils/config.js
CHANGED
|
@@ -6,9 +6,13 @@ const CONFIG_DIR = path.join(os.homedir(), '.thrust');
|
|
|
6
6
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_CONFIG = {
|
|
9
|
-
|
|
9
|
+
auth: {
|
|
10
|
+
token: null,
|
|
11
|
+
user: null
|
|
12
|
+
},
|
|
10
13
|
activeLeadId: null,
|
|
11
|
-
leads: {}
|
|
14
|
+
leads: {},
|
|
15
|
+
mcpServers: [] // NEW: Stores external MCP servers we poll (Feed-In Client)
|
|
12
16
|
};
|
|
13
17
|
|
|
14
18
|
export function initConfig() {
|
|
@@ -28,7 +32,13 @@ export function getConfig() {
|
|
|
28
32
|
initConfig();
|
|
29
33
|
try {
|
|
30
34
|
const data = fs.readFileSync(CONFIG_FILE, 'utf8');
|
|
31
|
-
|
|
35
|
+
const parsed = JSON.parse(data);
|
|
36
|
+
|
|
37
|
+
// Ensure legacy configs get the new objects injected safely
|
|
38
|
+
if (!parsed.auth) parsed.auth = { token: null, user: null };
|
|
39
|
+
if (!parsed.mcpServers) parsed.mcpServers = [];
|
|
40
|
+
|
|
41
|
+
return parsed;
|
|
32
42
|
} catch (error) {
|
|
33
43
|
console.error('⚠️ [Config Error] Corrupted config file. Returning defaults.', error.message);
|
|
34
44
|
return { ...DEFAULT_CONFIG };
|