@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/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: `API error: ${res.status} ${res.statusText}` }],
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 latestVersion = await checkNpmVersion();
264
- if (latestVersion) {
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" && latestVersion !== currentVersion && compareVersionStrings(currentVersion, latestVersion) < 0) {
268
- const updateNotice = `> **Update available:** @everstateai/mcp ${currentVersion} → ${latestVersion}\n` +
269
- `> Run \`npx @everstateai/mcp update-all\` to update.\n\n`;
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
- let cachedLatestVersion: string | null = null;
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<string | null> {
750
+ async function checkNpmVersion(): Promise<NpmVersionInfo | null> {
302
751
  const now = Date.now();
303
- if (cachedLatestVersion && now - lastNpmCheck < NPM_CHECK_INTERVAL) {
304
- return cachedLatestVersion;
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
- cachedLatestVersion = data.version;
768
+ cachedVersionInfo = { version: data.version, changelog: data.changelog };
320
769
  lastNpmCheck = now;
321
- return data.version;
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);