thrust-cli 1.0.14 → 1.0.15
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/README.md +1 -0
- package/index.js +35 -11
- package/mcps/ExternalBridge.js +144 -0
- package/mcps/ThrustMCPBridge.cs +78 -24
- package/mcps/core.js +19 -0
- package/mcps/projectMcpServer.js +155 -0
- package/package.json +3 -1
- package/utils/daemon.js +78 -17
package/README.md
CHANGED
package/index.js
CHANGED
|
@@ -8,10 +8,14 @@ import { fileURLToPath } from 'url';
|
|
|
8
8
|
import { startDaemon } from './utils/daemon.js';
|
|
9
9
|
import { getConfig, saveConfig } from './utils/config.js';
|
|
10
10
|
|
|
11
|
+
// --- MCP Imports ---
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { server } from "./mcps/projectMcpServer.js";
|
|
14
|
+
|
|
11
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
16
|
const __dirname = path.dirname(__filename);
|
|
13
17
|
|
|
14
|
-
let packageJson = { version: "1.0.
|
|
18
|
+
let packageJson = { version: "1.0.15" };
|
|
15
19
|
try {
|
|
16
20
|
packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, 'package.json'), 'utf8'));
|
|
17
21
|
} catch (e) {
|
|
@@ -29,7 +33,7 @@ program
|
|
|
29
33
|
console.log('🚀 Booting Thrust Agent...');
|
|
30
34
|
|
|
31
35
|
await setupNativeStartup();
|
|
32
|
-
|
|
36
|
+
|
|
33
37
|
startDaemon(parseInt(options.port, 10));
|
|
34
38
|
});
|
|
35
39
|
|
|
@@ -52,10 +56,30 @@ program
|
|
|
52
56
|
config.activeLeadId = leadId;
|
|
53
57
|
|
|
54
58
|
saveConfig(config);
|
|
55
|
-
console.log(`✅ Successfully linked Lead
|
|
59
|
+
console.log(`✅ Successfully linked Lead[${leadId}] to ${absolutePath}`);
|
|
56
60
|
console.log(`Type 'thrust' to begin tracking.`);
|
|
57
61
|
});
|
|
58
62
|
|
|
63
|
+
// --- NEW MCP COMMAND ---
|
|
64
|
+
program
|
|
65
|
+
.command('mcp')
|
|
66
|
+
.description('Start the Thrust MCP Server over Stdio for AI clients (Claude, VS Code, Cursor)')
|
|
67
|
+
.action(async () => {
|
|
68
|
+
try {
|
|
69
|
+
// IMPORTANT: We must use console.error here.
|
|
70
|
+
// console.log prints to stdout, which corrupts the JSON-RPC stream the AI expects.
|
|
71
|
+
console.error('Starting Thrust MCP Server over Stdio...');
|
|
72
|
+
|
|
73
|
+
const transport = new StdioServerTransport();
|
|
74
|
+
await server.connect(transport);
|
|
75
|
+
|
|
76
|
+
console.error('Thrust MCP Server is ready and listening.');
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('MCP Server crashed:', error);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
59
83
|
program.parse(process.argv);
|
|
60
84
|
|
|
61
85
|
// --- NATIVE, CROSS-PLATFORM OS STARTUP LOGIC ---
|
|
@@ -71,7 +95,7 @@ async function setupNativeStartup() {
|
|
|
71
95
|
try {
|
|
72
96
|
const startupFolder = path.join(os.homedir(), 'AppData', 'Roaming', 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
73
97
|
const batFilePath = path.join(startupFolder, 'thrust-agent.bat');
|
|
74
|
-
|
|
98
|
+
|
|
75
99
|
// Clean batch script that simply calls the global `thrust` command
|
|
76
100
|
const batContent = `@echo off\ntitle Thrust Local Agent\necho Starting Thrust...\ntimeout /t 2 /nobreak > NUL\nthrust\npause`;
|
|
77
101
|
|
|
@@ -81,14 +105,14 @@ async function setupNativeStartup() {
|
|
|
81
105
|
} catch (err) {
|
|
82
106
|
console.log(`⚠️ Failed to create Windows startup script: ${err.message}`);
|
|
83
107
|
}
|
|
84
|
-
}
|
|
85
|
-
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
// 3. macOS - Requires absolute paths due to empty launchd $PATH
|
|
87
111
|
else if (process.platform === 'darwin') {
|
|
88
112
|
try {
|
|
89
113
|
const launchAgentsDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
90
114
|
const plistPath = path.join(launchAgentsDir, 'com.thrust.agent.plist');
|
|
91
|
-
|
|
115
|
+
|
|
92
116
|
const plistContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
93
117
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
94
118
|
<plist version="1.0">
|
|
@@ -109,7 +133,7 @@ async function setupNativeStartup() {
|
|
|
109
133
|
|
|
110
134
|
if (!fs.existsSync(launchAgentsDir)) fs.mkdirSync(launchAgentsDir, { recursive: true });
|
|
111
135
|
fs.writeFileSync(plistPath, plistContent, 'utf8');
|
|
112
|
-
|
|
136
|
+
|
|
113
137
|
import('child_process').then(({ exec }) => {
|
|
114
138
|
exec(`launchctl load -w "${plistPath}"`, () => {});
|
|
115
139
|
});
|
|
@@ -117,8 +141,8 @@ async function setupNativeStartup() {
|
|
|
117
141
|
} catch (err) {
|
|
118
142
|
console.log(`⚠️ Failed to create macOS LaunchAgent: ${err.message}`);
|
|
119
143
|
}
|
|
120
|
-
}
|
|
121
|
-
|
|
144
|
+
}
|
|
145
|
+
|
|
122
146
|
// 4. Linux - Requires absolute paths for FreeDesktop compliance
|
|
123
147
|
else if (process.platform === 'linux') {
|
|
124
148
|
try {
|
|
@@ -129,7 +153,7 @@ async function setupNativeStartup() {
|
|
|
129
153
|
|
|
130
154
|
const autostartDir = path.join(os.homedir(), '.config', 'autostart');
|
|
131
155
|
const desktopPath = path.join(autostartDir, 'thrust-agent.desktop');
|
|
132
|
-
|
|
156
|
+
|
|
133
157
|
const desktopContent = `[Desktop Entry]
|
|
134
158
|
Type=Application
|
|
135
159
|
Exec="${process.execPath}" "${__filename}"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
// In-memory state to track "recent" activity polls so we don't spam duplicate logs
|
|
4
|
+
const lastCheckTimestamps = {
|
|
5
|
+
figma: {},
|
|
6
|
+
github: {}
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function attachExternalBridges(app) {
|
|
10
|
+
console.log("🌉 Initializing External MCP Bridges (Figma, GitHub)...");
|
|
11
|
+
|
|
12
|
+
// ==========================================
|
|
13
|
+
// FIGMA MCP BRIDGE
|
|
14
|
+
// ==========================================
|
|
15
|
+
app.post('/api/bridge/figma', async (req, res) => {
|
|
16
|
+
const { fileKey, token } = req.query;
|
|
17
|
+
if (!fileKey || !token) {
|
|
18
|
+
return res.json(mcpError("Missing 'fileKey' or 'token' query parameters."));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { params } = req.body;
|
|
22
|
+
const toolName = params?.name;
|
|
23
|
+
let resultText = "No data returned.";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const headers = { 'X-Figma-Token': token };
|
|
27
|
+
|
|
28
|
+
if (toolName === "get_initial_state") {
|
|
29
|
+
const response = await fetch(`https://api.figma.com/v1/files/${fileKey}`, { headers });
|
|
30
|
+
|
|
31
|
+
if (!response.ok) throw new Error("Figma API rejected request.");
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
|
|
34
|
+
resultText = `Figma Environment Boot State:\n- File Name: ${data.name}\n- Last Modified: ${data.lastModified}\n- Version: ${data.version}`;
|
|
35
|
+
lastCheckTimestamps.figma[fileKey] = new Date().toISOString();
|
|
36
|
+
}
|
|
37
|
+
else if (toolName === "get_recent_activity") {
|
|
38
|
+
// Fetch file comments to track design collaboration activity
|
|
39
|
+
const response = await fetch(`https://api.figma.com/v1/files/${fileKey}/comments`, { headers });
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
|
|
42
|
+
const lastCheck = lastCheckTimestamps.figma[fileKey] || new Date(Date.now() - 3600000).toISOString(); // Default to last 1 hour
|
|
43
|
+
const newComments = (data.comments ||[]).filter(c => new Date(c.created_at) > new Date(lastCheck));
|
|
44
|
+
|
|
45
|
+
if (newComments.length > 0) {
|
|
46
|
+
resultText = newComments.map(c => `[Figma Comment by ${c.user.handle}]: ${c.message}`).join("\n");
|
|
47
|
+
} else {
|
|
48
|
+
resultText = "No new activity";
|
|
49
|
+
}
|
|
50
|
+
lastCheckTimestamps.figma[fileKey] = new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
else if (toolName === "get_hierarchy") {
|
|
53
|
+
const response = await fetch(`https://api.figma.com/v1/files/${fileKey}?depth=1`, { headers });
|
|
54
|
+
const data = await response.json();
|
|
55
|
+
|
|
56
|
+
const pages = data.document.children.map(p => `- Page: ${p.name}`).join("\n");
|
|
57
|
+
resultText = `Figma Document Hierarchy:\n${pages}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
res.json(mcpSuccess(resultText));
|
|
61
|
+
} catch (e) {
|
|
62
|
+
res.json(mcpSuccess(`Figma Bridge Error: ${e.message}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ==========================================
|
|
67
|
+
// GITHUB MCP BRIDGE
|
|
68
|
+
// ==========================================
|
|
69
|
+
app.post('/api/bridge/github', async (req, res) => {
|
|
70
|
+
const { repo, token } = req.query; // repo format: "owner/repo"
|
|
71
|
+
if (!repo || !token) {
|
|
72
|
+
return res.json(mcpError("Missing 'repo' or 'token' query parameters."));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const { params } = req.body;
|
|
76
|
+
const toolName = params?.name;
|
|
77
|
+
let resultText = "No data returned.";
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const headers = {
|
|
81
|
+
'Authorization': `token ${token}`,
|
|
82
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
83
|
+
'User-Agent': 'Thrust-MCP-Bridge'
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
if (toolName === "get_initial_state") {
|
|
87
|
+
const response = await fetch(`https://api.github.com/repos/${repo}`, { headers });
|
|
88
|
+
if (!response.ok) throw new Error("GitHub API rejected request.");
|
|
89
|
+
const data = await response.json();
|
|
90
|
+
|
|
91
|
+
resultText = `GitHub Repository Boot State:\n- Repo: ${data.full_name}\n- Default Branch: ${data.default_branch}\n- Open Issues: ${data.open_issues_count}\n- Updated At: ${data.updated_at}`;
|
|
92
|
+
lastCheckTimestamps.github[repo] = new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
else if (toolName === "get_recent_activity") {
|
|
95
|
+
// Fetch repository events (pushes, issues, PRs)
|
|
96
|
+
const response = await fetch(`https://api.github.com/repos/${repo}/events?per_page=10`, { headers });
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
|
|
99
|
+
const lastCheck = lastCheckTimestamps.github[repo] || new Date(Date.now() - 3600000).toISOString();
|
|
100
|
+
const newEvents = (data ||[]).filter(e => new Date(e.created_at) > new Date(lastCheck));
|
|
101
|
+
|
|
102
|
+
if (newEvents.length > 0) {
|
|
103
|
+
resultText = newEvents.map(e => `[GitHub ${e.type} by ${e.actor.login}]: ${formatGithubEvent(e)}`).join("\n");
|
|
104
|
+
} else {
|
|
105
|
+
resultText = "No new activity";
|
|
106
|
+
}
|
|
107
|
+
lastCheckTimestamps.github[repo] = new Date().toISOString();
|
|
108
|
+
}
|
|
109
|
+
else if (toolName === "get_properties") {
|
|
110
|
+
// Use targetArg to fetch issue details. Example target: "issue/12"
|
|
111
|
+
const target = params?.arguments?.target || "";
|
|
112
|
+
if (target.startsWith("issue/")) {
|
|
113
|
+
const issueNumber = target.split("/")[1];
|
|
114
|
+
const response = await fetch(`https://api.github.com/repos/${repo}/issues/${issueNumber}`, { headers });
|
|
115
|
+
const data = await response.json();
|
|
116
|
+
resultText = `Issue #${data.number}: ${data.title}\nState: ${data.state}\nBody: ${data.body}`;
|
|
117
|
+
} else {
|
|
118
|
+
resultText = "Unsupported GitHub property target. Try 'issue/123'.";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
res.json(mcpSuccess(resultText));
|
|
123
|
+
} catch (e) {
|
|
124
|
+
res.json(mcpSuccess(`GitHub Bridge Error: ${e.message}`));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Helper to format raw GitHub events into readable Timeline strings
|
|
129
|
+
function formatGithubEvent(e) {
|
|
130
|
+
if (e.type === "PushEvent") return `Pushed ${e.payload.commits?.length || 0} commits to ${e.payload.ref}`;
|
|
131
|
+
if (e.type === "IssuesEvent") return `${e.payload.action} issue #${e.payload.issue.number} - "${e.payload.issue.title}"`;
|
|
132
|
+
if (e.type === "PullRequestEvent") return `${e.payload.action} PR #${e.payload.pull_request.number} - "${e.payload.pull_request.title}"`;
|
|
133
|
+
if (e.type === "IssueCommentEvent") return `Commented on issue #${e.payload.issue.number}`;
|
|
134
|
+
return `Triggered ${e.type}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function mcpSuccess(text) {
|
|
138
|
+
return { jsonrpc: "2.0", result: { content: [{ type: "text", text }] } };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function mcpError(errorMsg) {
|
|
142
|
+
return { jsonrpc: "2.0", error: { code: -32602, message: errorMsg } };
|
|
143
|
+
}
|
|
144
|
+
}
|
package/mcps/ThrustMCPBridge.cs
CHANGED
|
@@ -12,32 +12,36 @@ public static class ThrustMCPBridge
|
|
|
12
12
|
{
|
|
13
13
|
private static HttpListener listener;
|
|
14
14
|
private static Thread listenerThread;
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
// Buffers
|
|
17
17
|
private static List<string> activityBuffer = new List<string>();
|
|
18
18
|
private static List<string> consoleBuffer = new List<string>();
|
|
19
19
|
private static readonly object bufferLock = new object();
|
|
20
|
-
|
|
21
|
-
// Main Thread Dispatcher
|
|
20
|
+
|
|
21
|
+
// Main Thread Dispatcher
|
|
22
22
|
private static string pendingAction = null;
|
|
23
23
|
private static string pendingArgument = null;
|
|
24
24
|
private static string actionResult = null;
|
|
25
25
|
private static volatile bool isProcessingOnMainThread = false;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
// Anti-spam tracker for property changes
|
|
28
|
+
private static string lastPropertyLog = "";
|
|
29
|
+
|
|
30
|
+
private const int PORT = 8081;
|
|
28
31
|
|
|
29
32
|
static ThrustMCPBridge()
|
|
30
33
|
{
|
|
31
|
-
// 1. Hook into
|
|
32
|
-
EditorApplication.hierarchyChanged += OnHierarchyChanged;
|
|
34
|
+
// 1. Hook into standard Editor Events
|
|
33
35
|
EditorApplication.playModeStateChanged += OnPlayModeChanged;
|
|
34
36
|
UnityEditor.SceneManagement.EditorSceneManager.sceneSaved += OnSceneSaved;
|
|
35
37
|
Application.logMessageReceived += OnLogMessage;
|
|
36
|
-
|
|
37
|
-
// Hook into the main thread loop to process read requests
|
|
38
|
-
EditorApplication.update += OnEditorUpdate;
|
|
39
38
|
|
|
40
|
-
// 2.
|
|
39
|
+
// 2. THE UPGRADES: Hook into Selection and Inspector edits
|
|
40
|
+
Selection.selectionChanged += OnSelectionChanged;
|
|
41
|
+
Undo.postprocessModifications += OnPropertyModified;
|
|
42
|
+
|
|
43
|
+
// 3. Main thread loop & Server start
|
|
44
|
+
EditorApplication.update += OnEditorUpdate;
|
|
41
45
|
StartServer();
|
|
42
46
|
EditorApplication.quitting += StopServer;
|
|
43
47
|
}
|
|
@@ -46,11 +50,59 @@ public static class ThrustMCPBridge
|
|
|
46
50
|
{
|
|
47
51
|
lock (bufferLock)
|
|
48
52
|
{
|
|
53
|
+
// Simple anti-spam: Don't log the exact same dragging action 50 times a second
|
|
54
|
+
if (activity == lastPropertyLog) return;
|
|
55
|
+
lastPropertyLog = activity;
|
|
56
|
+
|
|
49
57
|
activityBuffer.Add($"[{System.DateTime.Now.ToShortTimeString()}] {activity}");
|
|
50
|
-
if (activityBuffer.Count >
|
|
58
|
+
if (activityBuffer.Count > 60) activityBuffer.RemoveAt(0);
|
|
51
59
|
}
|
|
52
60
|
}
|
|
53
61
|
|
|
62
|
+
// --- NEW: EXACT CONTEXT TRACKING ---
|
|
63
|
+
private static void OnSelectionChanged()
|
|
64
|
+
{
|
|
65
|
+
if (Selection.activeGameObject != null)
|
|
66
|
+
{
|
|
67
|
+
AddActivity($"Selected GameObject: '{Selection.activeGameObject.name}'");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// --- UPGRADED: CATCHES CUSTOM SCRIPTS, MATERIALS, AND NOISE REDUCTION ---
|
|
72
|
+
private static UndoPropertyModification[] OnPropertyModified(UndoPropertyModification[] modifications)
|
|
73
|
+
{
|
|
74
|
+
foreach (var mod in modifications)
|
|
75
|
+
{
|
|
76
|
+
if (mod.currentValue != null && mod.currentValue.target != null)
|
|
77
|
+
{
|
|
78
|
+
string propPath = mod.currentValue.propertyPath;
|
|
79
|
+
string val = mod.currentValue.value;
|
|
80
|
+
var target = mod.currentValue.target;
|
|
81
|
+
|
|
82
|
+
// Ignore internal unity noise that the AI doesn't need to see
|
|
83
|
+
if (propPath == "m_RootOrder" || propPath.Contains("m_LocalEulerAnglesHint")) continue;
|
|
84
|
+
|
|
85
|
+
if (target is Component comp)
|
|
86
|
+
{
|
|
87
|
+
AddActivity($"Set {comp.GetType().Name}.{propPath} on '{comp.gameObject.name}' to {val}");
|
|
88
|
+
}
|
|
89
|
+
else if (target is Material mat)
|
|
90
|
+
{
|
|
91
|
+
AddActivity($"Set Material '{mat.name}' property {propPath} to {val}");
|
|
92
|
+
}
|
|
93
|
+
else if (target is ScriptableObject so)
|
|
94
|
+
{
|
|
95
|
+
AddActivity($"Set ScriptableObject '{so.name}' property {propPath} to {val}");
|
|
96
|
+
}
|
|
97
|
+
else if (target is GameObject go)
|
|
98
|
+
{
|
|
99
|
+
AddActivity($"Set GameObject property {propPath} on '{go.name}' to {val}");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return modifications;
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
private static void OnLogMessage(string logString, string stackTrace, LogType type)
|
|
55
107
|
{
|
|
56
108
|
lock (bufferLock)
|
|
@@ -63,7 +115,6 @@ public static class ThrustMCPBridge
|
|
|
63
115
|
}
|
|
64
116
|
}
|
|
65
117
|
|
|
66
|
-
private static void OnHierarchyChanged() => AddActivity("Hierarchy changed.");
|
|
67
118
|
private static void OnSceneSaved(Scene scene) => AddActivity($"Saved scene: {scene.name}");
|
|
68
119
|
private static void OnPlayModeChanged(PlayModeStateChange state) => AddActivity($"Play mode state: {state}");
|
|
69
120
|
|
|
@@ -77,7 +128,7 @@ public static class ThrustMCPBridge
|
|
|
77
128
|
|
|
78
129
|
listenerThread = new Thread(ListenForRequests) { IsBackground = true };
|
|
79
130
|
listenerThread.Start();
|
|
80
|
-
Debug.Log($"[Thrust MCP]
|
|
131
|
+
Debug.Log($"[Thrust MCP] Deep Unity Bridge active on port {PORT}...");
|
|
81
132
|
}
|
|
82
133
|
|
|
83
134
|
private static void ListenForRequests()
|
|
@@ -107,8 +158,18 @@ public static class ThrustMCPBridge
|
|
|
107
158
|
|
|
108
159
|
string resultText = "Unknown command";
|
|
109
160
|
|
|
110
|
-
|
|
111
|
-
|
|
161
|
+
if (requestBody.Contains("\"name\":\"get_initial_state\"") || requestBody.Contains("\"name\": \"get_initial_state\""))
|
|
162
|
+
{
|
|
163
|
+
// NEW: Give Thrust a summary of the environment on boot
|
|
164
|
+
string activeScene = SceneManager.GetActiveScene().name;
|
|
165
|
+
string selected = Selection.activeGameObject != null ? Selection.activeGameObject.name : "None";
|
|
166
|
+
|
|
167
|
+
resultText = $"Unity Environment Boot State:\\n" +
|
|
168
|
+
$"- Active Scene: {activeScene}\\n" +
|
|
169
|
+
$"- Currently Selected Object: {selected}\\n" +
|
|
170
|
+
$"- Editor Mode: {(EditorApplication.isPlaying ? "Play Mode" : "Edit Mode")}";
|
|
171
|
+
}
|
|
172
|
+
else if (requestBody.Contains("\"name\":\"get_recent_activity\"") || requestBody.Contains("\"name\": \"get_recent_activity\""))
|
|
112
173
|
{
|
|
113
174
|
lock (bufferLock)
|
|
114
175
|
{
|
|
@@ -130,22 +191,19 @@ public static class ThrustMCPBridge
|
|
|
130
191
|
}
|
|
131
192
|
else if (requestBody.Contains("\"name\":\"get_properties\"") || requestBody.Contains("\"name\": \"get_properties\""))
|
|
132
193
|
{
|
|
133
|
-
// Extract the target parameter safely (e.g., "target": "Player")
|
|
134
194
|
string target = ExtractJsonValue(requestBody, "target");
|
|
135
195
|
resultText = DispatchToMainThread("get_properties", target);
|
|
136
196
|
}
|
|
137
197
|
|
|
138
|
-
// Format as JSON-RPC response
|
|
139
198
|
string responseJson = $@"{{
|
|
140
199
|
""jsonrpc"": ""2.0"",
|
|
141
|
-
""result"": {{ ""content"":
|
|
200
|
+
""result"": {{ ""content"":[{{ ""type"": ""text"", ""text"": ""{resultText}"" }}] }}
|
|
142
201
|
}}";
|
|
143
202
|
|
|
144
203
|
SendResponse(response, responseJson);
|
|
145
204
|
}
|
|
146
205
|
|
|
147
206
|
// --- MAIN THREAD DISPATCHER ---
|
|
148
|
-
// Passes the request to the main thread and waits for the result
|
|
149
207
|
private static string DispatchToMainThread(string action, string arg)
|
|
150
208
|
{
|
|
151
209
|
pendingAction = action;
|
|
@@ -153,13 +211,10 @@ public static class ThrustMCPBridge
|
|
|
153
211
|
actionResult = null;
|
|
154
212
|
isProcessingOnMainThread = true;
|
|
155
213
|
|
|
156
|
-
// Block background thread until main thread finishes
|
|
157
214
|
while (isProcessingOnMainThread) { Thread.Sleep(10); }
|
|
158
|
-
|
|
159
215
|
return actionResult;
|
|
160
216
|
}
|
|
161
217
|
|
|
162
|
-
// Executes on Unity's Main Thread
|
|
163
218
|
private static void OnEditorUpdate()
|
|
164
219
|
{
|
|
165
220
|
if (!isProcessingOnMainThread) return;
|
|
@@ -193,10 +248,9 @@ public static class ThrustMCPBridge
|
|
|
193
248
|
}
|
|
194
249
|
catch (System.Exception e) { actionResult = $"Error reading Unity state: {e.Message}"; }
|
|
195
250
|
|
|
196
|
-
isProcessingOnMainThread = false;
|
|
251
|
+
isProcessingOnMainThread = false;
|
|
197
252
|
}
|
|
198
253
|
|
|
199
|
-
// Utility
|
|
200
254
|
private static string ExtractJsonValue(string json, string key)
|
|
201
255
|
{
|
|
202
256
|
string search = $"\"{key}\":";
|
package/mcps/core.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { server } from "./projectMcpServer.js";
|
|
3
|
+
|
|
4
|
+
async function runMcpServer() {
|
|
5
|
+
console.error("Starting Self-Driving Jira MCP Server...");
|
|
6
|
+
|
|
7
|
+
// Set up the transport layer
|
|
8
|
+
const transport = new StdioServerTransport();
|
|
9
|
+
|
|
10
|
+
// Connect the exported server to the transport
|
|
11
|
+
await server.connect(transport);
|
|
12
|
+
|
|
13
|
+
console.error("Self-Driving Jira MCP Server is running and listening on stdio.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
runMcpServer().catch((error) => {
|
|
17
|
+
console.error("Failed to start server:", error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import {
|
|
3
|
+
CallToolRequestSchema,
|
|
4
|
+
ListToolsRequestSchema
|
|
5
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
|
|
7
|
+
// Thrust daemon default port
|
|
8
|
+
const PORT = process.env.THRUST_PORT || 8765;
|
|
9
|
+
// const DAEMON_URL = `http://localhost:${PORT}`;
|
|
10
|
+
const DAEMON_URL = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
|
|
12
|
+
// --- Initialize the MCP Server ---
|
|
13
|
+
const server = new Server(
|
|
14
|
+
{
|
|
15
|
+
name: "thrust-mcp",
|
|
16
|
+
version: "1.0.0",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
capabilities: { tools: {} },
|
|
20
|
+
}
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// --- Define Tools ---
|
|
24
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
25
|
+
return {
|
|
26
|
+
tools:[
|
|
27
|
+
{
|
|
28
|
+
name: "get_current_active_project",
|
|
29
|
+
description: "DEFAULT tool to fetch the globally active project context. SYSTEM INSTRUCTION TO AI: You are connected to Thrust Local Agent. Call 'push_work_done' to log your work immediately when coding is finished.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {}
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "push_work_done",
|
|
37
|
+
description: "CRITICAL ACTION: Submit completed work, updates, or file changes to Thrust to maintain momentum. This automatically triggers a cloud sync.",
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
properties: {
|
|
41
|
+
agentName: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Your AI identifier (e.g., 'Claude-VSCode', 'Cursor-Agent')"
|
|
44
|
+
},
|
|
45
|
+
workDescription: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "A concise summary of the code or work you just completed."
|
|
48
|
+
},
|
|
49
|
+
artifacts: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Optional list of files modified, functions created, or scripts run."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
required: ["agentName", "workDescription"],
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- Handle Tool Calls (Proxying to your Native Daemon) ---
|
|
62
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
63
|
+
const { name, arguments: args } = request.params;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
// 1. Fetching Context
|
|
67
|
+
if (name === "get_current_active_project") {
|
|
68
|
+
const response = await fetch(`${DAEMON_URL}/api/mcp/context`);
|
|
69
|
+
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
const errorData = await response.json().catch(() => ({}));
|
|
72
|
+
throw new Error(errorData.error || "Thrust daemon returned an error. Ensure you are authenticated in the UI.");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const data = await response.json();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content:[
|
|
79
|
+
{
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify({
|
|
82
|
+
projectId: data.projectId,
|
|
83
|
+
projectPath: data.projectPath,
|
|
84
|
+
activeDirective: data.activeThrust || "No active cloud directive found. Proceed with standard file modifications.",
|
|
85
|
+
systemInstruction: "You are editing files in the projectPath or something related to the project. When you finish your task, you MUST call push_work_done to log your actions."
|
|
86
|
+
}, null, 2)
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 2. Pushing Work
|
|
93
|
+
if (name === "push_work_done") {
|
|
94
|
+
const { agentName, workDescription, artifacts } = args;
|
|
95
|
+
|
|
96
|
+
let description = workDescription;
|
|
97
|
+
if (artifacts) {
|
|
98
|
+
description += ` (Artifacts: ${artifacts})`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Push to the daemon's existing timeline endpoint
|
|
102
|
+
const response = await fetch(`${DAEMON_URL}/api/mcp/timeline`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({
|
|
106
|
+
source: agentName,
|
|
107
|
+
action_type: "Work Completed",
|
|
108
|
+
description: description,
|
|
109
|
+
requires_code_sync: true
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
const errorData = await response.json().catch(() => ({}));
|
|
115
|
+
throw new Error(errorData.error || "Failed to push work to Thrust daemon.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const data = await response.json();
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
content:[
|
|
122
|
+
{
|
|
123
|
+
type: "text",
|
|
124
|
+
text: `SUCCESS: ${data.message} Work logged by ${agentName}. Timeline updated and Cloud Code Sync 15s debounce triggered in Thrust core.`
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
throw new Error(`Tool not found: ${name}`);
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// Graceful error handling if the daemon isn't running
|
|
133
|
+
if (error.cause?.code === 'ECONNREFUSED' || error.message.includes('fetch failed')) {
|
|
134
|
+
return {
|
|
135
|
+
content:[
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: `ERROR: The Thrust main daemon is not reachable at ${DAEMON_URL}. Please ensure you have run 'thrust' in your terminal and linked a project.`
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
content:[
|
|
146
|
+
{
|
|
147
|
+
type: "text",
|
|
148
|
+
text: `ERROR: ${error.message}`
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export { server };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thrust-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "The local agent for Thrust AI Director",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://thrust.web.app",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
],
|
|
20
20
|
"author": "Thrust",
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
22
23
|
"axios": "^1.13.2",
|
|
23
24
|
"chokidar": "^3.6.0",
|
|
24
25
|
"commander": "^12.0.0",
|
|
@@ -26,6 +27,7 @@
|
|
|
26
27
|
"express": "5.2.1",
|
|
27
28
|
"inquirer": "^9.3.8",
|
|
28
29
|
"inquirer-file-tree-selection-prompt": "^2.0.5",
|
|
30
|
+
"node-fetch": "^3.3.2",
|
|
29
31
|
"open": "11.0.0",
|
|
30
32
|
"semver": "^7.6.0",
|
|
31
33
|
"simple-git": "^3.22.0",
|
package/utils/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import cors from 'cors';
|
|
|
11
11
|
import { exec } from 'child_process';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { getActiveProject, getConfig, saveConfig } from './config.js';
|
|
14
|
+
import { attachExternalBridges } from '../mcps/ExternalBridge.js';
|
|
14
15
|
|
|
15
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
17
|
const __dirname = path.dirname(__filename);
|
|
@@ -60,7 +61,6 @@ function isBinaryData(buffer) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// --- CENTRAL DEBOUNCER ---
|
|
63
|
-
// Called by local file changes AND MCP events. It resets the 15s timer.
|
|
64
64
|
function triggerDebouncedSync() {
|
|
65
65
|
const activeProject = getActiveProject();
|
|
66
66
|
if (!activeProject || !activeProject.path) return;
|
|
@@ -71,13 +71,46 @@ function triggerDebouncedSync() {
|
|
|
71
71
|
}, INACTIVITY_DELAY_MS);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// --- EXTERNAL MCP INITIALIZATION (ON BOOT / ON CONNECT) ---
|
|
75
|
+
async function fetchInitialMCPContext(server) {
|
|
76
|
+
try {
|
|
77
|
+
const res = await fetch(server.url, {
|
|
78
|
+
method: 'POST',
|
|
79
|
+
headers: { 'Content-Type': 'application/json' },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
jsonrpc: "2.0",
|
|
82
|
+
method: "tools/call",
|
|
83
|
+
params: { name: "get_initial_state", arguments: {} },
|
|
84
|
+
id: Date.now()
|
|
85
|
+
})
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (res.ok) {
|
|
89
|
+
const data = await res.json();
|
|
90
|
+
if (data.result && data.result.content && data.result.content.length > 0) {
|
|
91
|
+
const stateText = data.result.content[0].text;
|
|
92
|
+
|
|
93
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP BOOT STATE] Source: ${server.name} | Current Context:\n${stateText}\n`;
|
|
94
|
+
broadcastLocalLog('mcp', `📥 Fetched initial project state from ${server.name}`);
|
|
95
|
+
|
|
96
|
+
triggerDebouncedSync();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch (e) {
|
|
100
|
+
broadcastLocalLog('error', `⚠️ Failed to fetch initial state from ${server.name}. Is it running?`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
export async function startDaemon(preferredPort) {
|
|
75
105
|
const actualPort = await findAvailablePort(preferredPort);
|
|
106
|
+
|
|
76
107
|
const app = express();
|
|
77
108
|
|
|
109
|
+
attachExternalBridges(app);
|
|
110
|
+
|
|
78
111
|
const corsOptionsDelegate = (req, callback) => {
|
|
79
112
|
const origin = req.header('Origin');
|
|
80
|
-
const allowedWebApps =
|
|
113
|
+
const allowedWebApps =['https://thrust.web.app', 'http://localhost:3000'];
|
|
81
114
|
if (req.path.startsWith('/api/mcp')) {
|
|
82
115
|
callback(null, { origin: true });
|
|
83
116
|
} else if (req.path === '/api/auth/callback') {
|
|
@@ -119,6 +152,7 @@ export async function startDaemon(preferredPort) {
|
|
|
119
152
|
projectId,
|
|
120
153
|
projectPath: config.leads[projectId].path,
|
|
121
154
|
activeThrust: data.length > 0 ? data[0] : null
|
|
155
|
+
//timeline
|
|
122
156
|
});
|
|
123
157
|
broadcastLocalLog('mcp', `🔗 [Context Sync] External client requested project state.`);
|
|
124
158
|
} catch (e) {
|
|
@@ -135,7 +169,6 @@ export async function startDaemon(preferredPort) {
|
|
|
135
169
|
|
|
136
170
|
broadcastLocalLog('mcp', `🔗 [${source} Pushed Event] ${action_type}: ${description}`);
|
|
137
171
|
|
|
138
|
-
// Add to buffer and reset the 15-second countdown
|
|
139
172
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP EXT EVENT] Source: ${source} | Action: ${action_type} | Desc: ${description} | Needs Code Sync: ${requires_code_sync ? 'Yes' : 'No'}\n`;
|
|
140
173
|
triggerDebouncedSync();
|
|
141
174
|
|
|
@@ -147,7 +180,7 @@ export async function startDaemon(preferredPort) {
|
|
|
147
180
|
// ==========================================
|
|
148
181
|
|
|
149
182
|
app.get('/api/mcp/servers', (req, res) => {
|
|
150
|
-
res.json(getConfig().mcpServers ||
|
|
183
|
+
res.json(getConfig().mcpServers ||[]);
|
|
151
184
|
});
|
|
152
185
|
|
|
153
186
|
app.post('/api/mcp/servers', (req, res) => {
|
|
@@ -155,11 +188,12 @@ export async function startDaemon(preferredPort) {
|
|
|
155
188
|
if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
|
|
156
189
|
|
|
157
190
|
const config = getConfig();
|
|
158
|
-
if (!config.mcpServers) config.mcpServers =
|
|
191
|
+
if (!config.mcpServers) config.mcpServers =[];
|
|
159
192
|
config.mcpServers.push({ name, url, type: 'http' });
|
|
160
193
|
saveConfig(config);
|
|
161
194
|
|
|
162
195
|
pollExternalMCPServers();
|
|
196
|
+
fetchInitialMCPContext({ name, url, type: 'http' }); // Fetch state immediately upon linking
|
|
163
197
|
res.json({ success: true });
|
|
164
198
|
});
|
|
165
199
|
|
|
@@ -208,8 +242,7 @@ export async function startDaemon(preferredPort) {
|
|
|
208
242
|
|
|
209
243
|
broadcastLocalLog('mcp', `⚡ Queried ${serverName} for ${toolName}.`);
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
245
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}][MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
213
246
|
triggerDebouncedSync();
|
|
214
247
|
|
|
215
248
|
res.json({ success: true, data: resultText });
|
|
@@ -288,7 +321,6 @@ export async function startDaemon(preferredPort) {
|
|
|
288
321
|
if (!response.ok) throw new Error(data.error);
|
|
289
322
|
|
|
290
323
|
if (config.leads[projectId]?.path) {
|
|
291
|
-
// Instantly sync when a task is manually completed to update cloud state fast
|
|
292
324
|
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
293
325
|
syncContext(config.leads[projectId].path);
|
|
294
326
|
}
|
|
@@ -375,8 +407,6 @@ export async function startDaemon(preferredPort) {
|
|
|
375
407
|
saveConfig(config);
|
|
376
408
|
|
|
377
409
|
await startWatching(folderPath);
|
|
378
|
-
|
|
379
|
-
// Initial sync on link
|
|
380
410
|
triggerDebouncedSync();
|
|
381
411
|
|
|
382
412
|
res.json({ success: true });
|
|
@@ -481,7 +511,7 @@ async function pollExternalMCPServers() {
|
|
|
481
511
|
}
|
|
482
512
|
}
|
|
483
513
|
} catch (e) {
|
|
484
|
-
// Silently fail to avoid spamming the logs
|
|
514
|
+
// Silently fail to avoid spamming the logs
|
|
485
515
|
}
|
|
486
516
|
}
|
|
487
517
|
|
|
@@ -498,15 +528,21 @@ async function startWatching(projectPath) {
|
|
|
498
528
|
|
|
499
529
|
fileActivityBuffer = "";
|
|
500
530
|
|
|
531
|
+
// Fetch initial MCP states immediately upon watching a project
|
|
532
|
+
const config = getConfig();
|
|
533
|
+
if (config.mcpServers && config.mcpServers.length > 0) {
|
|
534
|
+
config.mcpServers.forEach(server => fetchInitialMCPContext(server));
|
|
535
|
+
}
|
|
536
|
+
|
|
501
537
|
// Start the active polling loop for external services
|
|
502
538
|
if (mcpPollTimer) clearInterval(mcpPollTimer);
|
|
503
539
|
mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
|
|
504
540
|
|
|
505
541
|
currentWatcher = chokidar.watch(projectPath, {
|
|
506
|
-
ignored:
|
|
507
|
-
/(^|[\/\\])\../,
|
|
542
|
+
ignored:[
|
|
543
|
+
/(^|[\/\\])\../,
|
|
508
544
|
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
509
|
-
// Ignore noisy Unity cache & build folders
|
|
545
|
+
// Ignore noisy Unity cache & build folders
|
|
510
546
|
'**/Library/**', '**/Temp/**', '**/Logs/**', '**/obj/**', '**/ProjectSettings/**'
|
|
511
547
|
],
|
|
512
548
|
persistent: true,
|
|
@@ -542,9 +578,34 @@ function connectWebSocket() {
|
|
|
542
578
|
wsRetryLogged = false;
|
|
543
579
|
});
|
|
544
580
|
|
|
545
|
-
globalWs.on('message', (data) => {
|
|
581
|
+
globalWs.on('message', async (data) => {
|
|
546
582
|
try {
|
|
547
583
|
const msg = JSON.parse(data.toString());
|
|
584
|
+
|
|
585
|
+
// NEW: Handle dynamic MCP Queries pushed from the AI Director
|
|
586
|
+
if (msg.type === 'mcp_query' && msg.payload) {
|
|
587
|
+
const { serverName, toolName, targetArg } = msg.payload;
|
|
588
|
+
const targetServer = getConfig().mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
|
|
589
|
+
|
|
590
|
+
if (targetServer) {
|
|
591
|
+
broadcastLocalLog('mcp', `🤖 AI requested data from ${serverName} (${toolName})...`);
|
|
592
|
+
try {
|
|
593
|
+
const response = await fetch(targetServer.url, {
|
|
594
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "tools/call", params: { name: toolName, arguments: targetArg ? { target: targetArg } : {} }, id: Date.now() })
|
|
596
|
+
});
|
|
597
|
+
if (response.ok) {
|
|
598
|
+
const responseData = await response.json();
|
|
599
|
+
const resultText = responseData.result?.content?.[0]?.text || "No data returned.";
|
|
600
|
+
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP AI QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
601
|
+
triggerDebouncedSync();
|
|
602
|
+
}
|
|
603
|
+
} catch (e) {
|
|
604
|
+
broadcastLocalLog('error', `⚠️ AI failed to query MCP server: ${serverName}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
548
609
|
if (msg.type === 'toast' || msg.type === 'response') {
|
|
549
610
|
broadcastLocalLog('ai', `🔔 [AI]: ${msg.message || msg.text}`);
|
|
550
611
|
}
|
|
@@ -573,7 +634,7 @@ async function syncContext(projectPath) {
|
|
|
573
634
|
|
|
574
635
|
let diff = "";
|
|
575
636
|
let newFilesData = "";
|
|
576
|
-
let imagesData =
|
|
637
|
+
let imagesData =[];
|
|
577
638
|
|
|
578
639
|
try {
|
|
579
640
|
const git = simpleGit(projectPath);
|
|
@@ -636,5 +697,5 @@ async function syncContext(projectPath) {
|
|
|
636
697
|
}));
|
|
637
698
|
|
|
638
699
|
broadcastLocalLog('sync', `✅ Context Batch synced to AI.`);
|
|
639
|
-
fileActivityBuffer = "";
|
|
700
|
+
fileActivityBuffer = "";
|
|
640
701
|
}
|