thrust-cli 1.0.12 â 1.0.14
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/package.json +1 -1
- package/utils/daemon.js +115 -104
package/package.json
CHANGED
package/utils/daemon.js
CHANGED
|
@@ -20,8 +20,8 @@ const API_URL = GATEWAY_URL.replace('ws://', 'http://').replace('wss://', 'https
|
|
|
20
20
|
const AUTH_PROXY_URL = "https://everydaycats-thrust-auth-server.hf.space";
|
|
21
21
|
|
|
22
22
|
// --- DEBOUNCE & POLLING TIMERS ---
|
|
23
|
-
const INACTIVITY_DELAY_MS = 15 * 1000; //
|
|
24
|
-
const MCP_POLL_INTERVAL_MS = 18 * 1000; //
|
|
23
|
+
const INACTIVITY_DELAY_MS = 15 * 1000; // Wait for 15 seconds of silence before syncing
|
|
24
|
+
const MCP_POLL_INTERVAL_MS = 18 * 1000; // Poll external tools every 18 seconds
|
|
25
25
|
|
|
26
26
|
let currentWatcher = null;
|
|
27
27
|
let inactivityTimer = null;
|
|
@@ -59,6 +59,18 @@ function isBinaryData(buffer) {
|
|
|
59
59
|
return false;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// --- CENTRAL DEBOUNCER ---
|
|
63
|
+
// Called by local file changes AND MCP events. It resets the 15s timer.
|
|
64
|
+
function triggerDebouncedSync() {
|
|
65
|
+
const activeProject = getActiveProject();
|
|
66
|
+
if (!activeProject || !activeProject.path) return;
|
|
67
|
+
|
|
68
|
+
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
69
|
+
inactivityTimer = setTimeout(() => {
|
|
70
|
+
syncContext(activeProject.path);
|
|
71
|
+
}, INACTIVITY_DELAY_MS);
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
export async function startDaemon(preferredPort) {
|
|
63
75
|
const actualPort = await findAvailablePort(preferredPort);
|
|
64
76
|
const app = express();
|
|
@@ -67,7 +79,7 @@ export async function startDaemon(preferredPort) {
|
|
|
67
79
|
const origin = req.header('Origin');
|
|
68
80
|
const allowedWebApps = ['https://thrust.web.app', 'http://localhost:3000'];
|
|
69
81
|
if (req.path.startsWith('/api/mcp')) {
|
|
70
|
-
callback(null, { origin: true });
|
|
82
|
+
callback(null, { origin: true });
|
|
71
83
|
} else if (req.path === '/api/auth/callback') {
|
|
72
84
|
if (!origin || allowedWebApps.includes(origin) || origin === `http://localhost:${actualPort}`) {
|
|
73
85
|
callback(null, { origin: true, credentials: true });
|
|
@@ -102,7 +114,7 @@ export async function startDaemon(preferredPort) {
|
|
|
102
114
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
103
115
|
});
|
|
104
116
|
const data = await response.json();
|
|
105
|
-
|
|
117
|
+
|
|
106
118
|
res.json({
|
|
107
119
|
projectId,
|
|
108
120
|
projectPath: config.leads[projectId].path,
|
|
@@ -114,30 +126,26 @@ export async function startDaemon(preferredPort) {
|
|
|
114
126
|
}
|
|
115
127
|
});
|
|
116
128
|
|
|
117
|
-
app.post('/api/mcp/timeline',
|
|
129
|
+
app.post('/api/mcp/timeline', (req, res) => {
|
|
118
130
|
const { source, action_type, description, requires_code_sync } = req.body;
|
|
119
|
-
|
|
131
|
+
|
|
120
132
|
if (!source || !description) {
|
|
121
133
|
return res.status(400).json({ error: "Malformed MCP payload." });
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
broadcastLocalLog('mcp', `đ [${source} Pushed Event] ${action_type}: ${description}`);
|
|
125
137
|
|
|
138
|
+
// Add to buffer and reset the 15-second countdown
|
|
126
139
|
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
|
+
triggerDebouncedSync();
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
if (activeProject && activeProject.path) {
|
|
130
|
-
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
131
|
-
await syncContext(activeProject.path);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
res.json({ success: true, message: "Timeline event ingested and sync triggered." });
|
|
142
|
+
res.json({ success: true, message: "Timeline event ingested to buffer." });
|
|
135
143
|
});
|
|
136
144
|
|
|
137
145
|
// ==========================================
|
|
138
146
|
// MCP CLIENT CONFIGURATION (FEED-IN PULL)
|
|
139
147
|
// ==========================================
|
|
140
|
-
|
|
148
|
+
|
|
141
149
|
app.get('/api/mcp/servers', (req, res) => {
|
|
142
150
|
res.json(getConfig().mcpServers || []);
|
|
143
151
|
});
|
|
@@ -145,13 +153,12 @@ export async function startDaemon(preferredPort) {
|
|
|
145
153
|
app.post('/api/mcp/servers', (req, res) => {
|
|
146
154
|
const { name, url } = req.body;
|
|
147
155
|
if (!name || !url) return res.status(400).json({ error: "Missing name or url" });
|
|
148
|
-
|
|
156
|
+
|
|
149
157
|
const config = getConfig();
|
|
150
158
|
if (!config.mcpServers) config.mcpServers = [];
|
|
151
159
|
config.mcpServers.push({ name, url, type: 'http' });
|
|
152
160
|
saveConfig(config);
|
|
153
|
-
|
|
154
|
-
// Trigger an immediate poll when a new service is added
|
|
161
|
+
|
|
155
162
|
pollExternalMCPServers();
|
|
156
163
|
res.json({ success: true });
|
|
157
164
|
});
|
|
@@ -170,23 +177,20 @@ export async function startDaemon(preferredPort) {
|
|
|
170
177
|
// EXTERNAL MCP DIRECT QUERY (AI OR MANUAL)
|
|
171
178
|
// ==========================================
|
|
172
179
|
app.post('/api/mcp/query', async (req, res) => {
|
|
180
|
+
const { serverName, toolName, targetArg } = req.body;
|
|
181
|
+
const config = getConfig();
|
|
173
182
|
|
|
174
|
-
const { serverName, toolName, targetArg } = req.body;
|
|
175
|
-
const config = getConfig();
|
|
176
|
-
|
|
177
|
-
// Find the requested server by name (e.g., "Unity")
|
|
178
183
|
const server = config.mcpServers?.find(s => s.name.toLowerCase() === serverName.toLowerCase());
|
|
179
|
-
|
|
184
|
+
|
|
180
185
|
if (!server) return res.status(404).json({ error: `MCP Server '${serverName}' not found.` });
|
|
181
186
|
|
|
182
187
|
try {
|
|
183
|
-
// Build the JSON-RPC Payload
|
|
184
188
|
const payload = {
|
|
185
189
|
jsonrpc: "2.0",
|
|
186
190
|
method: "tools/call",
|
|
187
|
-
params: {
|
|
188
|
-
name: toolName,
|
|
189
|
-
arguments: targetArg ? { target: targetArg } : {}
|
|
191
|
+
params: {
|
|
192
|
+
name: toolName,
|
|
193
|
+
arguments: targetArg ? { target: targetArg } : {}
|
|
190
194
|
},
|
|
191
195
|
id: Date.now()
|
|
192
196
|
};
|
|
@@ -198,15 +202,15 @@ export async function startDaemon(preferredPort) {
|
|
|
198
202
|
});
|
|
199
203
|
|
|
200
204
|
if (!response.ok) throw new Error("Server responded with error");
|
|
201
|
-
|
|
205
|
+
|
|
202
206
|
const data = await response.json();
|
|
203
207
|
const resultText = data.result?.content?.[0]?.text || "No data returned.";
|
|
204
|
-
|
|
205
|
-
// Log it locally so the user sees the system thinking
|
|
208
|
+
|
|
206
209
|
broadcastLocalLog('mcp', `⥠Queried ${serverName} for ${toolName}.`);
|
|
207
|
-
|
|
208
|
-
//
|
|
210
|
+
|
|
211
|
+
// Add the query result to the buffer and reset the 15-second countdown
|
|
209
212
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP DIRECT QUERY RESULT] Source: ${serverName} | Tool: ${toolName} | Target: ${targetArg || 'none'} \nResult:\n${resultText}\n`;
|
|
213
|
+
triggerDebouncedSync();
|
|
210
214
|
|
|
211
215
|
res.json({ success: true, data: resultText });
|
|
212
216
|
|
|
@@ -284,6 +288,7 @@ export async function startDaemon(preferredPort) {
|
|
|
284
288
|
if (!response.ok) throw new Error(data.error);
|
|
285
289
|
|
|
286
290
|
if (config.leads[projectId]?.path) {
|
|
291
|
+
// Instantly sync when a task is manually completed to update cloud state fast
|
|
287
292
|
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
288
293
|
syncContext(config.leads[projectId].path);
|
|
289
294
|
}
|
|
@@ -370,10 +375,10 @@ export async function startDaemon(preferredPort) {
|
|
|
370
375
|
saveConfig(config);
|
|
371
376
|
|
|
372
377
|
await startWatching(folderPath);
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
|
|
379
|
+
// Initial sync on link
|
|
380
|
+
triggerDebouncedSync();
|
|
381
|
+
|
|
377
382
|
res.json({ success: true });
|
|
378
383
|
});
|
|
379
384
|
|
|
@@ -452,7 +457,6 @@ async function pollExternalMCPServers() {
|
|
|
452
457
|
|
|
453
458
|
for (const server of config.mcpServers) {
|
|
454
459
|
try {
|
|
455
|
-
// Send standard MCP JSON-RPC payload asking for recent activity
|
|
456
460
|
const res = await fetch(server.url, {
|
|
457
461
|
method: 'POST',
|
|
458
462
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -466,11 +470,9 @@ async function pollExternalMCPServers() {
|
|
|
466
470
|
|
|
467
471
|
if (res.ok) {
|
|
468
472
|
const data = await res.json();
|
|
469
|
-
|
|
470
|
-
// Parse standard MCP response { result: { content: [{ text: "..." }] } }
|
|
471
473
|
if (data.result && data.result.content && data.result.content.length > 0) {
|
|
472
474
|
const updateText = data.result.content[0].text;
|
|
473
|
-
|
|
475
|
+
|
|
474
476
|
if (updateText && updateText.trim() !== "" && updateText.trim() !== "No new activity") {
|
|
475
477
|
fileActivityBuffer += `\n[${new Date().toLocaleTimeString()}] [MCP POLL] Source: ${server.name} | Update: ${updateText}\n`;
|
|
476
478
|
broadcastLocalLog('mcp', `đ Pulled new data from ${server.name}`);
|
|
@@ -479,16 +481,12 @@ async function pollExternalMCPServers() {
|
|
|
479
481
|
}
|
|
480
482
|
}
|
|
481
483
|
} catch (e) {
|
|
482
|
-
|
|
484
|
+
// Silently fail to avoid spamming the logs if Unity is temporarily closed
|
|
483
485
|
}
|
|
484
486
|
}
|
|
485
487
|
|
|
486
488
|
if (hasNewData) {
|
|
487
|
-
|
|
488
|
-
if (activeProject && activeProject.path) {
|
|
489
|
-
if (inactivityTimer) clearTimeout(inactivityTimer);
|
|
490
|
-
await syncContext(activeProject.path);
|
|
491
|
-
}
|
|
489
|
+
triggerDebouncedSync();
|
|
492
490
|
}
|
|
493
491
|
}
|
|
494
492
|
|
|
@@ -505,7 +503,12 @@ async function startWatching(projectPath) {
|
|
|
505
503
|
mcpPollTimer = setInterval(pollExternalMCPServers, MCP_POLL_INTERVAL_MS);
|
|
506
504
|
|
|
507
505
|
currentWatcher = chokidar.watch(projectPath, {
|
|
508
|
-
ignored: [
|
|
506
|
+
ignored: [
|
|
507
|
+
/(^|[\/\\])\../,
|
|
508
|
+
'**/node_modules/**', '**/dist/**', '**/build/**',
|
|
509
|
+
// Ignore noisy Unity cache & build folders to prevent infinite debounce loops
|
|
510
|
+
'**/Library/**', '**/Temp/**', '**/Logs/**', '**/obj/**', '**/ProjectSettings/**'
|
|
511
|
+
],
|
|
509
512
|
persistent: true,
|
|
510
513
|
ignoreInitial: true
|
|
511
514
|
});
|
|
@@ -515,10 +518,7 @@ async function startWatching(projectPath) {
|
|
|
515
518
|
fileActivityBuffer += `[${new Date().toLocaleTimeString()}] ${event.toUpperCase()}: ${relativePath}\n`;
|
|
516
519
|
broadcastLocalLog('watch', `[${event.toUpperCase()}] ${relativePath}`);
|
|
517
520
|
|
|
518
|
-
|
|
519
|
-
inactivityTimer = setTimeout(() => {
|
|
520
|
-
syncContext(projectPath);
|
|
521
|
-
}, INACTIVITY_DELAY_MS);
|
|
521
|
+
triggerDebouncedSync();
|
|
522
522
|
});
|
|
523
523
|
} catch (err) {}
|
|
524
524
|
}
|
|
@@ -564,66 +564,77 @@ function connectWebSocket() {
|
|
|
564
564
|
}
|
|
565
565
|
|
|
566
566
|
async function syncContext(projectPath) {
|
|
567
|
-
if (!globalWs || globalWs.readyState !== WebSocket.OPEN)
|
|
567
|
+
if (!globalWs || globalWs.readyState !== WebSocket.OPEN) {
|
|
568
|
+
broadcastLocalLog('error', `â ī¸ Cannot sync: Not connected to Cloud.`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
568
571
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
+
broadcastLocalLog('system', `âŗ 15s silence reached. Preparing sync...`);
|
|
573
|
+
|
|
574
|
+
let diff = "";
|
|
575
|
+
let newFilesData = "";
|
|
576
|
+
let imagesData = [];
|
|
572
577
|
|
|
573
578
|
try {
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
if (
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
579
|
+
const git = simpleGit(projectPath);
|
|
580
|
+
const isRepo = await git.checkIsRepo().catch(() => false);
|
|
581
|
+
|
|
582
|
+
if (isRepo) {
|
|
583
|
+
const status = await git.status();
|
|
584
|
+
diff = await git.diff();
|
|
585
|
+
|
|
586
|
+
const binaryExts =[
|
|
587
|
+
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.ico',
|
|
588
|
+
'.pdf', '.zip', '.tar', '.gz', '.mp4', '.mp3', '.wav',
|
|
589
|
+
'.exe', '.dll', '.so', '.dylib',
|
|
590
|
+
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.bin'
|
|
591
|
+
];
|
|
592
|
+
|
|
593
|
+
for (const file of status.not_added) {
|
|
594
|
+
const ext = path.extname(file).toLowerCase();
|
|
595
|
+
const fullPath = path.join(projectPath, file);
|
|
596
|
+
if (!fs.existsSync(fullPath)) continue;
|
|
597
|
+
|
|
598
|
+
const stat = fs.statSync(fullPath);
|
|
599
|
+
if (stat.isDirectory() || stat.size > 5 * 1024 * 1024) continue;
|
|
600
|
+
|
|
601
|
+
const fileData = fs.readFileSync(fullPath);
|
|
602
|
+
const isBinary = binaryExts.includes(ext) || isBinaryData(fileData);
|
|
603
|
+
|
|
604
|
+
if (isBinary) {
|
|
605
|
+
const base64 = fileData.toString('base64');
|
|
606
|
+
const mime = ['.png','.jpg','.jpeg','.webp','.gif'].includes(ext)
|
|
607
|
+
? `image/${ext.replace('.','')}`
|
|
608
|
+
: 'application/octet-stream';
|
|
609
|
+
imagesData.push(`data:${mime};base64,${base64}`);
|
|
610
|
+
} else {
|
|
611
|
+
const content = fileData.toString('utf8');
|
|
612
|
+
newFilesData += `\n--- NEW FILE: ${file} (Scraped at ${new Date().toLocaleTimeString()}) ---\n${content.substring(0, 10000)}\n`;
|
|
613
|
+
}
|
|
609
614
|
}
|
|
615
|
+
} else {
|
|
616
|
+
broadcastLocalLog('system', `âšī¸ Folder is not a Git repo. Syncing MCP/Activity logs only.`);
|
|
610
617
|
}
|
|
618
|
+
} catch (e) {
|
|
619
|
+
console.error("Git/Sync processing error:", e);
|
|
620
|
+
}
|
|
611
621
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
buffer: fileActivityBuffer,
|
|
617
|
-
diffs: diff,
|
|
618
|
-
new_files: newFilesData,
|
|
619
|
-
images: imagesData
|
|
620
|
-
}
|
|
621
|
-
}));
|
|
622
|
+
if (!fileActivityBuffer.trim() && !diff && !newFilesData) {
|
|
623
|
+
broadcastLocalLog('system', `âšī¸ Buffer empty. Nothing to sync.`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
622
626
|
|
|
623
|
-
|
|
624
|
-
|
|
627
|
+
globalWs.send(JSON.stringify({
|
|
628
|
+
type: "context_sync",
|
|
629
|
+
projectId: getActiveProject().id,
|
|
630
|
+
data: {
|
|
631
|
+
buffer: fileActivityBuffer,
|
|
632
|
+
diffs: diff,
|
|
633
|
+
new_files: newFilesData,
|
|
634
|
+
images: imagesData
|
|
635
|
+
}
|
|
636
|
+
}));
|
|
625
637
|
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
638
|
+
broadcastLocalLog('sync', `â
Context Batch synced to AI.`);
|
|
639
|
+
fileActivityBuffer = "";
|
|
629
640
|
}
|