@everstateai/mcp 1.3.12 → 1.3.13
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 +26 -3
- package/dist/index.js +462 -12
- package/dist/index.js.map +1 -1
- package/dist/setup/auto-update.d.ts.map +1 -1
- package/dist/setup/auto-update.js +223 -0
- package/dist/setup/auto-update.js.map +1 -1
- package/dist/setup/hooks/templates.d.ts +18 -1
- package/dist/setup/hooks/templates.d.ts.map +1 -1
- package/dist/setup/hooks/templates.js +381 -41
- package/dist/setup/hooks/templates.js.map +1 -1
- package/dist/setup/types.d.ts +2 -0
- package/dist/setup/types.d.ts.map +1 -1
- package/dist/setup/types.js +3 -1
- package/dist/setup/types.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +98 -8
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +503 -13
- package/src/setup/auto-update.ts +263 -0
- package/src/setup/hooks/templates.ts +398 -41
- package/src/setup/types.ts +3 -1
- package/src/setup.ts +113 -8
package/src/index.ts
CHANGED
|
@@ -101,6 +101,24 @@ async function runMcpServer() {
|
|
|
101
101
|
// Session state - persists projectId across tool calls within this proxy instance
|
|
102
102
|
let sessionProjectId = INITIAL_PROJECT_ID;
|
|
103
103
|
|
|
104
|
+
// Auto-session state: track last tool call time and whether sync has been called
|
|
105
|
+
let lastToolCallTime = 0;
|
|
106
|
+
let sessionSynced = false;
|
|
107
|
+
const AUTO_SYNC_IDLE_MS = 5 * 60 * 1000; // 5 minutes
|
|
108
|
+
|
|
109
|
+
// Context window tracking
|
|
110
|
+
// Claude's context window is ~200K tokens (~800K chars).
|
|
111
|
+
// We track cumulative MCP response sizes as a proxy for context growth.
|
|
112
|
+
// MCP responses are typically the largest context consumers since user messages
|
|
113
|
+
// and Claude's own output are smaller relative to tool results.
|
|
114
|
+
const CONTEXT_WINDOW_CHARS = 800_000; // ~200K tokens * 4 chars/token
|
|
115
|
+
const CONTEXT_WARNING_THRESHOLD = 0.75; // 75%
|
|
116
|
+
const CONTEXT_CRITICAL_THRESHOLD = 0.90; // 90%
|
|
117
|
+
let cumulativeResponseChars = 0;
|
|
118
|
+
let toolCallCount = 0;
|
|
119
|
+
let contextWarningIssued = false;
|
|
120
|
+
let contextCriticalIssued = false;
|
|
121
|
+
|
|
104
122
|
if (!API_KEY) {
|
|
105
123
|
console.error("EVERSTATE_API_KEY environment variable is required");
|
|
106
124
|
console.error("");
|
|
@@ -173,6 +191,261 @@ async function runMcpServer() {
|
|
|
173
191
|
}
|
|
174
192
|
}
|
|
175
193
|
|
|
194
|
+
// ============ BATCH SYNC ============
|
|
195
|
+
// Buffer write operations locally and flush on a configurable interval
|
|
196
|
+
// to reduce API calls. Enabled via .everstate.json: { "batchSync": { "enabled": true, "intervalSeconds": 30 } }
|
|
197
|
+
|
|
198
|
+
interface BatchSyncConfig {
|
|
199
|
+
enabled: boolean;
|
|
200
|
+
intervalSeconds: number;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface BufferEntry {
|
|
204
|
+
id: string;
|
|
205
|
+
queuedAt: string;
|
|
206
|
+
toolName: string;
|
|
207
|
+
arguments: Record<string, unknown>;
|
|
208
|
+
retryCount: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface BatchBuffer {
|
|
212
|
+
version: number;
|
|
213
|
+
lastFlushAt: string | null;
|
|
214
|
+
lastFlushResult: { success: number; failed: number } | null;
|
|
215
|
+
entries: BufferEntry[];
|
|
216
|
+
failed: BufferEntry[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readBatchConfig(): BatchSyncConfig {
|
|
220
|
+
let dir = process.cwd();
|
|
221
|
+
const root = path.parse(dir).root;
|
|
222
|
+
while (dir !== root) {
|
|
223
|
+
const configPath = path.join(dir, ".everstate.json");
|
|
224
|
+
if (fs.existsSync(configPath)) {
|
|
225
|
+
try {
|
|
226
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
227
|
+
if (config.batchSync) {
|
|
228
|
+
const interval = config.batchSync.intervalSeconds ?? 30;
|
|
229
|
+
const MAX_INTERVAL = 300; // 5 minutes max — prevents stale data loss on crash
|
|
230
|
+
return {
|
|
231
|
+
enabled: config.batchSync.enabled ?? false,
|
|
232
|
+
intervalSeconds: Math.min(Math.max(interval, 5), MAX_INTERVAL),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
} catch { /* continue */ }
|
|
236
|
+
}
|
|
237
|
+
dir = path.dirname(dir);
|
|
238
|
+
}
|
|
239
|
+
return { enabled: false, intervalSeconds: 30 };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const batchConfig = readBatchConfig();
|
|
243
|
+
const BUFFER_DIR = path.join(process.cwd(), ".claude");
|
|
244
|
+
const BUFFER_PATH = path.join(BUFFER_DIR, ".batch-buffer.json");
|
|
245
|
+
const MAX_RETRIES = 3;
|
|
246
|
+
const MAX_BUFFER_SIZE = 200;
|
|
247
|
+
const BUFFER_STALE_MS = 60 * 60 * 1000; // 1 hour
|
|
248
|
+
|
|
249
|
+
function emptyBuffer(): BatchBuffer {
|
|
250
|
+
return { version: 1, lastFlushAt: null, lastFlushResult: null, entries: [], failed: [] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readBuffer(): BatchBuffer {
|
|
254
|
+
try {
|
|
255
|
+
if (fs.existsSync(BUFFER_PATH)) {
|
|
256
|
+
const buf = JSON.parse(fs.readFileSync(BUFFER_PATH, "utf8")) as BatchBuffer;
|
|
257
|
+
// Discard stale entries on startup
|
|
258
|
+
const cutoff = Date.now() - BUFFER_STALE_MS;
|
|
259
|
+
buf.entries = buf.entries.filter(e => new Date(e.queuedAt).getTime() > cutoff);
|
|
260
|
+
buf.failed = (buf.failed || []).slice(-50);
|
|
261
|
+
return buf;
|
|
262
|
+
}
|
|
263
|
+
} catch { /* corrupted file, start fresh */ }
|
|
264
|
+
return emptyBuffer();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function writeBuffer(buffer: BatchBuffer): void {
|
|
268
|
+
try {
|
|
269
|
+
if (!fs.existsSync(BUFFER_DIR)) fs.mkdirSync(BUFFER_DIR, { recursive: true });
|
|
270
|
+
const tmpPath = BUFFER_PATH + ".tmp";
|
|
271
|
+
fs.writeFileSync(tmpPath, JSON.stringify(buffer, null, 2));
|
|
272
|
+
fs.renameSync(tmpPath, BUFFER_PATH);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error("[Everstate] Failed to write buffer:", err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function addToBuffer(toolName: string, args: Record<string, unknown>): void {
|
|
279
|
+
const buffer = readBuffer();
|
|
280
|
+
if (buffer.entries.length >= MAX_BUFFER_SIZE) {
|
|
281
|
+
// Drop oldest entries
|
|
282
|
+
const dropped = buffer.entries.splice(0, buffer.entries.length - MAX_BUFFER_SIZE + 1);
|
|
283
|
+
console.error(`[Everstate] Buffer full, dropped ${dropped.length} oldest entries`);
|
|
284
|
+
}
|
|
285
|
+
buffer.entries.push({
|
|
286
|
+
id: `buf_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
287
|
+
queuedAt: new Date().toISOString(),
|
|
288
|
+
toolName,
|
|
289
|
+
arguments: args,
|
|
290
|
+
retryCount: 0,
|
|
291
|
+
});
|
|
292
|
+
writeBuffer(buffer);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Tools that MUST execute immediately (return data the agent needs)
|
|
296
|
+
const IMMEDIATE_TOOLS = new Set([
|
|
297
|
+
"sync", "done", "recall", "status",
|
|
298
|
+
"ask_user", "check_user_response",
|
|
299
|
+
"everstate_list_projects", "everstate_switch_project", "everstate_current_project",
|
|
300
|
+
"list_projects", "switch_project", "current_project",
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
// Meta-tool action prefixes that must execute immediately (reads/interactive)
|
|
304
|
+
const IMMEDIATE_META_PREFIXES = [
|
|
305
|
+
"help", "session.", "search.", "context.", "heal.", "quality.",
|
|
306
|
+
"google.", "inbound.", "files.", "git.", "team.",
|
|
307
|
+
"work.get_", "work.list_", "work.search_", "work.get_portfolio",
|
|
308
|
+
"decisions.list", "decisions.get",
|
|
309
|
+
"gotchas.list", "gotchas.get",
|
|
310
|
+
"admin.get_", "admin.list_", "admin.check_", "admin.validate_", "admin.export_",
|
|
311
|
+
"project.", "progress.list", "progress.get",
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
function isBatchable(toolName: string, args: Record<string, unknown>): boolean {
|
|
315
|
+
if (IMMEDIATE_TOOLS.has(toolName)) return false;
|
|
316
|
+
if (toolName === "everstate") {
|
|
317
|
+
const action = (args.action as string) || "";
|
|
318
|
+
for (const prefix of IMMEDIATE_META_PREFIXES) {
|
|
319
|
+
if (action === prefix || action.startsWith(prefix)) return false;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
// Core tools: log is batchable, everything else is immediate by default
|
|
324
|
+
if (toolName === "log") return true;
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function generateSyntheticResponse(toolName: string, args: Record<string, unknown>): string {
|
|
329
|
+
if (toolName === "log") {
|
|
330
|
+
const msg = (args.message as string) || "";
|
|
331
|
+
const type = (args.type as string) || "progress";
|
|
332
|
+
const labels: Record<string, string> = {
|
|
333
|
+
achievement: "Achievement logged",
|
|
334
|
+
progress: "Progress recorded",
|
|
335
|
+
note: "Note saved",
|
|
336
|
+
warning: "Warning recorded",
|
|
337
|
+
};
|
|
338
|
+
return `${labels[type] || "Logged"} (queued for sync): ${msg.substring(0, 120)}${msg.length > 120 ? "..." : ""}`;
|
|
339
|
+
}
|
|
340
|
+
if (toolName === "everstate") {
|
|
341
|
+
const action = (args.action as string) || "";
|
|
342
|
+
return `Queued for sync: ${action}`;
|
|
343
|
+
}
|
|
344
|
+
return `Queued for sync: ${toolName}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function flushBuffer(): Promise<void> {
|
|
348
|
+
const buffer = readBuffer();
|
|
349
|
+
if (buffer.entries.length === 0) return;
|
|
350
|
+
|
|
351
|
+
const count = buffer.entries.length;
|
|
352
|
+
console.error(`[Everstate] Flushing ${count} buffered items...`);
|
|
353
|
+
|
|
354
|
+
let success = 0;
|
|
355
|
+
let failed = 0;
|
|
356
|
+
const remaining: BufferEntry[] = [];
|
|
357
|
+
const failedEntries: BufferEntry[] = [...(buffer.failed || [])];
|
|
358
|
+
|
|
359
|
+
// Try batch endpoint first
|
|
360
|
+
try {
|
|
361
|
+
const res = await fetch(`${API_BASE}/mcp/batch-execute`, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
headers: {
|
|
364
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
365
|
+
"Content-Type": "application/json",
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
calls: buffer.entries.map(e => ({
|
|
369
|
+
name: e.toolName,
|
|
370
|
+
arguments: e.arguments,
|
|
371
|
+
})),
|
|
372
|
+
}),
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
if (res.ok) {
|
|
376
|
+
success = count;
|
|
377
|
+
} else if (res.status === 404) {
|
|
378
|
+
// Batch endpoint not deployed yet — fall back to individual calls
|
|
379
|
+
for (const entry of buffer.entries) {
|
|
380
|
+
try {
|
|
381
|
+
const r = await fetch(`${API_BASE}/mcp/execute`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: {
|
|
384
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
385
|
+
"Content-Type": "application/json",
|
|
386
|
+
},
|
|
387
|
+
body: JSON.stringify({
|
|
388
|
+
name: entry.toolName,
|
|
389
|
+
arguments: entry.arguments,
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
if (r.ok) {
|
|
393
|
+
success++;
|
|
394
|
+
} else {
|
|
395
|
+
entry.retryCount++;
|
|
396
|
+
if (entry.retryCount >= MAX_RETRIES) {
|
|
397
|
+
failedEntries.push(entry);
|
|
398
|
+
failed++;
|
|
399
|
+
} else {
|
|
400
|
+
remaining.push(entry);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} catch {
|
|
404
|
+
entry.retryCount++;
|
|
405
|
+
if (entry.retryCount >= MAX_RETRIES) {
|
|
406
|
+
failedEntries.push(entry);
|
|
407
|
+
failed++;
|
|
408
|
+
} else {
|
|
409
|
+
remaining.push(entry);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
// Server error — keep all entries for retry
|
|
415
|
+
for (const entry of buffer.entries) {
|
|
416
|
+
entry.retryCount++;
|
|
417
|
+
if (entry.retryCount >= MAX_RETRIES) {
|
|
418
|
+
failedEntries.push(entry);
|
|
419
|
+
failed++;
|
|
420
|
+
} else {
|
|
421
|
+
remaining.push(entry);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Network error — keep all entries for retry
|
|
427
|
+
for (const entry of buffer.entries) {
|
|
428
|
+
entry.retryCount++;
|
|
429
|
+
if (entry.retryCount >= MAX_RETRIES) {
|
|
430
|
+
failedEntries.push(entry);
|
|
431
|
+
failed++;
|
|
432
|
+
} else {
|
|
433
|
+
remaining.push(entry);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
buffer.entries = remaining;
|
|
439
|
+
buffer.failed = failedEntries.slice(-50);
|
|
440
|
+
buffer.lastFlushAt = new Date().toISOString();
|
|
441
|
+
buffer.lastFlushResult = { success, failed };
|
|
442
|
+
writeBuffer(buffer);
|
|
443
|
+
|
|
444
|
+
if (success > 0 || failed > 0) {
|
|
445
|
+
console.error(`[Everstate] Flush: ${success} sent, ${failed} failed, ${remaining.length} retrying`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
176
449
|
// Type for API responses
|
|
177
450
|
interface ToolsResponse {
|
|
178
451
|
tools: Array<{
|
|
@@ -223,6 +496,59 @@ async function runMcpServer() {
|
|
|
223
496
|
enrichedArgs.projectId = sessionProjectId;
|
|
224
497
|
}
|
|
225
498
|
|
|
499
|
+
// Auto-session: trigger sync if this is the first tool call or idle > 5 minutes
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const isIdle = lastToolCallTime > 0 && (now - lastToolCallTime) > AUTO_SYNC_IDLE_MS;
|
|
502
|
+
const needsSync = !sessionSynced || isIdle;
|
|
503
|
+
const isSyncCall = name === "sync" || name === "done";
|
|
504
|
+
|
|
505
|
+
// Flush buffer before auto-sync (idle means buffered items are sitting stale)
|
|
506
|
+
if (needsSync && isIdle && batchConfig.enabled) {
|
|
507
|
+
await flushBuffer();
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (needsSync && !isSyncCall && sessionProjectId) {
|
|
511
|
+
try {
|
|
512
|
+
console.error(`[Everstate] Auto-sync: ${!sessionSynced ? 'first tool call' : 'idle > 5min'}`);
|
|
513
|
+
await fetch(`${API_BASE}/mcp/execute`, {
|
|
514
|
+
method: "POST",
|
|
515
|
+
headers: {
|
|
516
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
517
|
+
"Content-Type": "application/json",
|
|
518
|
+
},
|
|
519
|
+
body: JSON.stringify({
|
|
520
|
+
name: "sync",
|
|
521
|
+
arguments: { projectId: sessionProjectId, mode: "minimal" },
|
|
522
|
+
}),
|
|
523
|
+
});
|
|
524
|
+
sessionSynced = true;
|
|
525
|
+
} catch {
|
|
526
|
+
// Non-critical: don't block tool call if auto-sync fails
|
|
527
|
+
console.error("[Everstate] Auto-sync failed, continuing...");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (isSyncCall) {
|
|
532
|
+
sessionSynced = true;
|
|
533
|
+
}
|
|
534
|
+
lastToolCallTime = now;
|
|
535
|
+
|
|
536
|
+
// Force-flush buffer before done() so session summary reflects all progress
|
|
537
|
+
if (name === "done" && batchConfig.enabled) {
|
|
538
|
+
await flushBuffer();
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Batch sync: buffer batchable write operations instead of calling API immediately
|
|
542
|
+
if (batchConfig.enabled && isBatchable(name, enrichedArgs)) {
|
|
543
|
+
const syntheticResponse = generateSyntheticResponse(name, enrichedArgs);
|
|
544
|
+
addToBuffer(name, enrichedArgs);
|
|
545
|
+
const bufSize = readBuffer().entries.length;
|
|
546
|
+
console.error(`[Everstate] Batched: ${name} (buffer: ${bufSize} items)`);
|
|
547
|
+
return {
|
|
548
|
+
content: [{ type: "text" as const, text: syntheticResponse }],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
226
552
|
const res = await fetch(`${API_BASE}/mcp/execute`, {
|
|
227
553
|
method: "POST",
|
|
228
554
|
headers: {
|
|
@@ -233,8 +559,17 @@ async function runMcpServer() {
|
|
|
233
559
|
});
|
|
234
560
|
|
|
235
561
|
if (!res.ok) {
|
|
562
|
+
// Read the response body for the actual error message
|
|
563
|
+
let errorDetail = `${res.status} ${res.statusText}`;
|
|
564
|
+
try {
|
|
565
|
+
const errorBody = await res.json() as { content?: Array<{ text?: string }> };
|
|
566
|
+
const msg = errorBody?.content?.[0]?.text;
|
|
567
|
+
if (msg) errorDetail = msg;
|
|
568
|
+
} catch {
|
|
569
|
+
// Body not JSON, use status text
|
|
570
|
+
}
|
|
236
571
|
return {
|
|
237
|
-
content: [{ type: "text" as const, text:
|
|
572
|
+
content: [{ type: "text" as const, text: errorDetail }],
|
|
238
573
|
isError: true,
|
|
239
574
|
};
|
|
240
575
|
}
|
|
@@ -245,6 +580,71 @@ async function runMcpServer() {
|
|
|
245
580
|
project?: { id?: string };
|
|
246
581
|
};
|
|
247
582
|
|
|
583
|
+
// Track context growth from MCP responses
|
|
584
|
+
toolCallCount++;
|
|
585
|
+
const responseText = result.content?.map((c: { text?: string }) => c.text || '').join('') || '';
|
|
586
|
+
cumulativeResponseChars += responseText.length;
|
|
587
|
+
|
|
588
|
+
// Also count the request args as they're in context too
|
|
589
|
+
const requestChars = JSON.stringify(enrichedArgs).length;
|
|
590
|
+
cumulativeResponseChars += requestChars;
|
|
591
|
+
|
|
592
|
+
const contextUsage = cumulativeResponseChars / CONTEXT_WINDOW_CHARS;
|
|
593
|
+
const contextPercent = Math.round(contextUsage * 100);
|
|
594
|
+
|
|
595
|
+
// Force-flush buffer when context is running low — session may end abruptly
|
|
596
|
+
if (contextUsage >= CONTEXT_WARNING_THRESHOLD && batchConfig.enabled) {
|
|
597
|
+
flushBuffer().catch(() => {}); // Non-blocking
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Append context warning to tool response
|
|
601
|
+
if (contextUsage >= CONTEXT_CRITICAL_THRESHOLD && !contextCriticalIssued) {
|
|
602
|
+
contextCriticalIssued = true;
|
|
603
|
+
const warning = `\n\n---\n` +
|
|
604
|
+
`**CONTEXT CRITICAL (${contextPercent}% estimated usage)**\n\n` +
|
|
605
|
+
`This session's context window is nearly full. You should:\n` +
|
|
606
|
+
`1. Call \`done\` with a detailed summary and next steps\n` +
|
|
607
|
+
`2. Tell the user to start a new session — Everstate will restore context automatically\n\n` +
|
|
608
|
+
`Continuing will lead to context compaction which loses conversation detail.\n` +
|
|
609
|
+
`---`;
|
|
610
|
+
if (result.content?.[0]?.text) {
|
|
611
|
+
result.content[0].text += warning;
|
|
612
|
+
}
|
|
613
|
+
} else if (contextUsage >= CONTEXT_WARNING_THRESHOLD && !contextWarningIssued) {
|
|
614
|
+
contextWarningIssued = true;
|
|
615
|
+
const warning = `\n\n---\n` +
|
|
616
|
+
`**CONTEXT WARNING (${contextPercent}% estimated usage)**\n\n` +
|
|
617
|
+
`This session is consuming context quickly (~${toolCallCount} tool calls, ~${Math.round(cumulativeResponseChars / 4000)}K tokens estimated).\n` +
|
|
618
|
+
`Consider wrapping up soon. When ready:\n` +
|
|
619
|
+
`1. Call \`done\` with a summary of what was accomplished and what's next\n` +
|
|
620
|
+
`2. Suggest the user start a new session — Everstate will pick up where you left off\n` +
|
|
621
|
+
`---`;
|
|
622
|
+
if (result.content?.[0]?.text) {
|
|
623
|
+
result.content[0].text += warning;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Log the warning to Everstate for analytics
|
|
627
|
+
try {
|
|
628
|
+
await fetch(`${API_BASE}/mcp/execute`, {
|
|
629
|
+
method: "POST",
|
|
630
|
+
headers: {
|
|
631
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
632
|
+
"Content-Type": "application/json",
|
|
633
|
+
},
|
|
634
|
+
body: JSON.stringify({
|
|
635
|
+
name: "log",
|
|
636
|
+
arguments: {
|
|
637
|
+
projectId: sessionProjectId,
|
|
638
|
+
type: "warning",
|
|
639
|
+
message: `Context window at ${contextPercent}% estimated usage after ${toolCallCount} tool calls (~${Math.round(cumulativeResponseChars / 4)} tokens in MCP responses)`,
|
|
640
|
+
},
|
|
641
|
+
}),
|
|
642
|
+
});
|
|
643
|
+
} catch {
|
|
644
|
+
// Non-critical
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
248
648
|
// Update session state based on tool results
|
|
249
649
|
if (name === "switch_project" || name === "everstate_switch_project") {
|
|
250
650
|
const data = result.data || result.project;
|
|
@@ -260,19 +660,51 @@ async function runMcpServer() {
|
|
|
260
660
|
// For sync calls, check for npm package updates and prepend notice
|
|
261
661
|
if (name === "sync" && result.content?.[0]?.text) {
|
|
262
662
|
try {
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
663
|
+
const versionInfo = await checkNpmVersion();
|
|
664
|
+
if (versionInfo) {
|
|
265
665
|
const { getMcpProxyVersion: getVersion } = await import("./setup/version.js");
|
|
266
666
|
const currentVersion = getVersion();
|
|
267
|
-
if (currentVersion !== "0.0.0" &&
|
|
268
|
-
|
|
269
|
-
|
|
667
|
+
if (currentVersion !== "0.0.0" && versionInfo.version !== currentVersion && compareVersionStrings(currentVersion, versionInfo.version) < 0) {
|
|
668
|
+
// Build update notice with optional changelog
|
|
669
|
+
let updateNotice = `> **Update available:** @everstateai/mcp ${currentVersion} → ${versionInfo.version}\n`;
|
|
670
|
+
if (versionInfo.changelog) {
|
|
671
|
+
updateNotice += `> _What's new: ${versionInfo.changelog}_\n`;
|
|
672
|
+
}
|
|
673
|
+
updateNotice += `> Auto-updating in background... (disable with \`everstate({ action: "admin.settings", params: { autoUpdate: false } })\`)\n\n`;
|
|
270
674
|
result.content[0].text = updateNotice + result.content[0].text;
|
|
675
|
+
|
|
676
|
+
// Trigger auto-update in background (non-blocking)
|
|
677
|
+
autoUpdateNpmPackage(versionInfo.version).catch(() => {});
|
|
271
678
|
}
|
|
272
679
|
}
|
|
273
680
|
} catch {
|
|
274
681
|
// Non-critical, continue without update notice
|
|
275
682
|
}
|
|
683
|
+
|
|
684
|
+
// Check for MEMORY.md coexistence and add note if found
|
|
685
|
+
try {
|
|
686
|
+
const fs = await import('fs');
|
|
687
|
+
const path = await import('path');
|
|
688
|
+
const projectDir = process.cwd();
|
|
689
|
+
const memoryPaths = [
|
|
690
|
+
path.join(projectDir, 'MEMORY.md'),
|
|
691
|
+
path.join(projectDir, '.claude', 'MEMORY.md'),
|
|
692
|
+
];
|
|
693
|
+
const hasMemoryMd = memoryPaths.some(p => fs.existsSync(p));
|
|
694
|
+
if (hasMemoryMd && result.content?.[0]?.text) {
|
|
695
|
+
const memoryNote = `> **Note:** MEMORY.md detected. MEMORY.md = your personal notes (local). Everstate = shared project memory (cloud, cross-tool). They complement each other.\n\n`;
|
|
696
|
+
// Insert after any update notice but before the main content
|
|
697
|
+
const text = result.content[0].text;
|
|
698
|
+
const headerMatch = text.match(/^(# Everstate Session Sync\n\n)/);
|
|
699
|
+
if (headerMatch) {
|
|
700
|
+
result.content[0].text = headerMatch[1] + memoryNote + text.slice(headerMatch[1].length);
|
|
701
|
+
} else {
|
|
702
|
+
result.content[0].text = memoryNote + text;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
} catch {
|
|
706
|
+
// Non-critical
|
|
707
|
+
}
|
|
276
708
|
}
|
|
277
709
|
|
|
278
710
|
return result;
|
|
@@ -291,17 +723,34 @@ async function runMcpServer() {
|
|
|
291
723
|
const transport = new StdioServerTransport();
|
|
292
724
|
await server.connect(transport);
|
|
293
725
|
console.error("Everstate MCP Proxy connected to", API_BASE);
|
|
726
|
+
|
|
727
|
+
// Start batch sync flush timer if enabled
|
|
728
|
+
if (batchConfig.enabled) {
|
|
729
|
+
console.error(`[Everstate] Batch sync: ON (flush every ${batchConfig.intervalSeconds}s)`);
|
|
730
|
+
setInterval(async () => {
|
|
731
|
+
try {
|
|
732
|
+
await flushBuffer();
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.error("[Everstate] Flush error:", err);
|
|
735
|
+
}
|
|
736
|
+
}, batchConfig.intervalSeconds * 1000);
|
|
737
|
+
}
|
|
294
738
|
}
|
|
295
739
|
|
|
296
740
|
// Cache npm version check (once per MCP server lifetime, max every 4 hours)
|
|
297
|
-
|
|
741
|
+
interface NpmVersionInfo {
|
|
742
|
+
version: string;
|
|
743
|
+
changelog?: string;
|
|
744
|
+
}
|
|
745
|
+
let cachedVersionInfo: NpmVersionInfo | null = null;
|
|
298
746
|
let lastNpmCheck = 0;
|
|
299
747
|
const NPM_CHECK_INTERVAL = 4 * 60 * 60 * 1000; // 4 hours
|
|
748
|
+
let autoUpdateInProgress = false;
|
|
300
749
|
|
|
301
|
-
async function checkNpmVersion(): Promise<
|
|
750
|
+
async function checkNpmVersion(): Promise<NpmVersionInfo | null> {
|
|
302
751
|
const now = Date.now();
|
|
303
|
-
if (
|
|
304
|
-
return
|
|
752
|
+
if (cachedVersionInfo && now - lastNpmCheck < NPM_CHECK_INTERVAL) {
|
|
753
|
+
return cachedVersionInfo;
|
|
305
754
|
}
|
|
306
755
|
|
|
307
756
|
try {
|
|
@@ -314,11 +763,11 @@ async function checkNpmVersion(): Promise<string | null> {
|
|
|
314
763
|
clearTimeout(timeout);
|
|
315
764
|
|
|
316
765
|
if (res.ok) {
|
|
317
|
-
const data = (await res.json()) as { version?: string };
|
|
766
|
+
const data = (await res.json()) as { version?: string; changelog?: string };
|
|
318
767
|
if (data.version) {
|
|
319
|
-
|
|
768
|
+
cachedVersionInfo = { version: data.version, changelog: data.changelog };
|
|
320
769
|
lastNpmCheck = now;
|
|
321
|
-
return
|
|
770
|
+
return cachedVersionInfo;
|
|
322
771
|
}
|
|
323
772
|
}
|
|
324
773
|
} catch {
|
|
@@ -327,6 +776,47 @@ async function checkNpmVersion(): Promise<string | null> {
|
|
|
327
776
|
return null;
|
|
328
777
|
}
|
|
329
778
|
|
|
779
|
+
/**
|
|
780
|
+
* Auto-update the npm package if enabled and a new version is available.
|
|
781
|
+
* This runs in the background and doesn't block tool calls.
|
|
782
|
+
*/
|
|
783
|
+
async function autoUpdateNpmPackage(latestVersion: string): Promise<void> {
|
|
784
|
+
if (autoUpdateInProgress) return;
|
|
785
|
+
|
|
786
|
+
// Check if auto-update is disabled
|
|
787
|
+
const fs = await import('fs');
|
|
788
|
+
const path = await import('path');
|
|
789
|
+
const settingsPath = path.join(process.env.HOME || '', '.everstate', 'settings.json');
|
|
790
|
+
|
|
791
|
+
try {
|
|
792
|
+
if (fs.existsSync(settingsPath)) {
|
|
793
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
794
|
+
if (settings.autoUpdate === false) {
|
|
795
|
+
return; // User has disabled auto-updates
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
// If we can't read settings, proceed with auto-update
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
autoUpdateInProgress = true;
|
|
803
|
+
console.error(`[Everstate] Auto-updating to v${latestVersion}...`);
|
|
804
|
+
|
|
805
|
+
try {
|
|
806
|
+
const { execSync } = await import('child_process');
|
|
807
|
+
// Use npm to update the package globally or locally depending on how it's installed
|
|
808
|
+
execSync('npm install -g @everstateai/mcp@latest', {
|
|
809
|
+
stdio: 'pipe',
|
|
810
|
+
timeout: 60000, // 1 minute timeout
|
|
811
|
+
});
|
|
812
|
+
console.error(`[Everstate] Successfully updated to v${latestVersion}. Restart your AI tool to use the new version.`);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
console.error(`[Everstate] Auto-update failed: ${String(error)}. Run 'npx @everstateai/mcp update-all' manually.`);
|
|
815
|
+
} finally {
|
|
816
|
+
autoUpdateInProgress = false;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
330
820
|
function compareVersionStrings(a: string, b: string): number {
|
|
331
821
|
const aParts = a.split(".").map(Number);
|
|
332
822
|
const bParts = b.split(".").map(Number);
|