@everstateai/mcp 1.3.12 → 1.3.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.
@@ -21,9 +21,13 @@ export function getSyncTodosHook(config?: Partial<HookConfig>): string {
21
21
  * VERSION: ${HOOK_VERSIONS.syncTodos}
22
22
  * GENERATED: ${generatedAt}
23
23
  *
24
- * Syncs Claude's TodoWrite tasks with Everstate.
24
+ * Two-way sync between Claude's TodoWrite and Everstate dashboard.
25
25
  * Runs after every TodoWrite tool call.
26
26
  *
27
+ * Push: Sends Claude's todos to Everstate for matching and storage.
28
+ * Pull: Reads dashboard changes from API response and outputs instructions
29
+ * to stdout so Claude can update its TodoWrite list.
30
+ *
27
31
  * Claude Code passes hook data via STDIN as JSON:
28
32
  * { "tool_name": "TodoWrite", "tool_input": { "todos": [...] }, ... }
29
33
  */
@@ -88,6 +92,19 @@ async function main() {
88
92
  process.exit(0);
89
93
  }
90
94
 
95
+ // Read last sync timestamp for two-way sync
96
+ const syncStatePath = path.join(
97
+ process.env.CLAUDE_PROJECT_DIR || process.cwd(),
98
+ '.claude', '.sync-state.json'
99
+ );
100
+ let lastSyncedAt = null;
101
+ try {
102
+ if (fs.existsSync(syncStatePath)) {
103
+ const state = JSON.parse(fs.readFileSync(syncStatePath, 'utf8'));
104
+ lastSyncedAt = state.lastSyncedAt || null;
105
+ }
106
+ } catch {}
107
+
91
108
  // Send to API for matching and storage
92
109
  const response = await fetch(\`\${API_URL}/api/session/sync-todos\`, {
93
110
  method: 'POST',
@@ -99,12 +116,47 @@ async function main() {
99
116
  projectId,
100
117
  todos,
101
118
  sessionId: process.env.SESSION_ID || null,
119
+ lastSyncedAt,
102
120
  }),
103
121
  });
104
122
 
105
123
  if (!response.ok) {
106
124
  const text = await response.text();
107
125
  console.error('[everstate] Sync failed:', response.status, text);
126
+ process.exit(0);
127
+ }
128
+
129
+ // Parse response for two-way sync
130
+ const data = await response.json();
131
+
132
+ // Save sync timestamp for next run
133
+ try {
134
+ fs.mkdirSync(path.dirname(syncStatePath), { recursive: true });
135
+ fs.writeFileSync(syncStatePath, JSON.stringify({
136
+ lastSyncedAt: data.syncedAt || new Date().toISOString(),
137
+ }));
138
+ } catch {}
139
+
140
+ // Output dashboard changes to stdout (Claude sees this)
141
+ const changes = data.dashboardChanges || [];
142
+ if (changes.length > 0) {
143
+ const lines = [];
144
+ for (const c of changes) {
145
+ if (c.type === 'status_changed') {
146
+ lines.push('- "' + c.taskTitle + '" was changed to ' + c.newValue + ' on the dashboard');
147
+ } else if (c.type === 'deleted') {
148
+ lines.push('- "' + c.taskTitle + '" was deleted on the dashboard');
149
+ } else if (c.type === 'title_changed') {
150
+ lines.push('- "' + c.oldValue + '" was renamed to "' + c.newValue + '" on the dashboard');
151
+ }
152
+ }
153
+ if (lines.length > 0) {
154
+ console.log('');
155
+ console.log('[Everstate] Dashboard changes detected since last sync:');
156
+ console.log(lines.join('\\n'));
157
+ console.log('');
158
+ console.log('Please update your TodoWrite list to reflect these changes.');
159
+ }
108
160
  }
109
161
  } catch (error) {
110
162
  console.error('[everstate] Hook error:', error.message);
@@ -153,7 +205,8 @@ export function getSessionEndHook(config?: Partial<HookConfig>): string {
153
205
  # VERSION: ${HOOK_VERSIONS.sessionEnd}
154
206
  # GENERATED: ${generatedAt}
155
207
  #
156
- # Saves session summary when Claude session ends.
208
+ # Auto-saves session when Claude session ends.
209
+ # Calls auto-done which generates summary from tracked activity.
157
210
 
158
211
  API_KEY_PATH="${apiKeyPath}"
159
212
  API_URL="${apiUrl}"
@@ -182,8 +235,8 @@ if [ -z "$PROJECT_ID" ]; then
182
235
  exit 0
183
236
  fi
184
237
 
185
- # Signal session end to API
186
- curl -s -X POST "$API_URL/api/session/end" \\
238
+ # Call auto-done (generates summary from activity, skips if done() was already called)
239
+ curl -s -X POST "$API_URL/api/session/auto-done" \\
187
240
  -H "Content-Type: application/json" \\
188
241
  -H "Authorization: Bearer $API_KEY" \\
189
242
  -d "{\\"projectId\\": \\"$PROJECT_ID\\"}" > /dev/null 2>&1
@@ -193,57 +246,358 @@ exit 0
193
246
  }
194
247
 
195
248
  /**
196
- * Get the pre-compact.sh hook content (for context compaction)
249
+ * Get the pre-compact.js hook content
250
+ * Prints critical Everstate memory to stdout so it survives compaction.
197
251
  */
198
252
  export function getPreCompactHook(config?: Partial<HookConfig>): string {
199
253
  const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
200
254
  const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
201
255
  const generatedAt = config?.generatedAt || new Date().toISOString();
202
256
 
203
- return `#!/bin/bash
204
- # Everstate Pre-Compact Hook v${HOOK_VERSIONS.sessionEnd}
205
- # VERSION: ${HOOK_VERSIONS.sessionEnd}
206
- # GENERATED: ${generatedAt}
207
- #
208
- # Saves session context before Claude's context compaction.
257
+ return getContextInjectionHook({
258
+ hookName: 'Pre-Compact',
259
+ version: HOOK_VERSIONS.preCompact,
260
+ generatedAt,
261
+ apiKeyPath,
262
+ apiUrl,
263
+ preamble: 'Critical context preserved through compaction',
264
+ timeout: 8000,
265
+ includeBlockers: true,
266
+ includeTasks: true,
267
+ maxGotchas: 5,
268
+ maxDecisions: 5,
269
+ postAction: `
270
+ fetch(\\\`\\\${API_URL}/api/session/compact-warning\\\`, {
271
+ method: 'POST',
272
+ headers: { 'Content-Type': 'application/json', 'Authorization': \\\`Bearer \\\${apiKey}\\\` },
273
+ body: JSON.stringify({ projectId, message: 'Context compacted — critical context re-injected' }),
274
+ }).catch(() => {});`,
275
+ });
276
+ }
209
277
 
210
- API_KEY_PATH="${apiKeyPath}"
211
- API_URL="${apiUrl}"
278
+ /**
279
+ * Get the subagent-context.js hook content
280
+ * Injects critical context when subagents spawn.
281
+ */
282
+ export function getSubagentContextHook(config?: Partial<HookConfig>): string {
283
+ const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
284
+ const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
285
+ const generatedAt = config?.generatedAt || new Date().toISOString();
212
286
 
213
- # Read API key
214
- if [ ! -f "$API_KEY_PATH" ]; then
215
- exit 0
216
- fi
217
- API_KEY=$(cat "$API_KEY_PATH")
287
+ return getContextInjectionHook({
288
+ hookName: 'SubagentStart',
289
+ version: HOOK_VERSIONS.subagentContext,
290
+ generatedAt,
291
+ apiKeyPath,
292
+ apiUrl,
293
+ preamble: 'Project context for this subagent',
294
+ timeout: 5000,
295
+ includeBlockers: false,
296
+ includeTasks: false,
297
+ maxGotchas: 5,
298
+ maxDecisions: 3,
299
+ });
300
+ }
218
301
 
219
- # Find project config
220
- find_project_id() {
221
- local dir="$PWD"
222
- for i in {1..10}; do
223
- if [ -f "$dir/.everstate.json" ]; then
224
- grep -o '"projectId"[[:space:]]*:[[:space:]]*"[^"]*"' "$dir/.everstate.json" | cut -d'"' -f4
225
- return
226
- fi
227
- dir=$(dirname "$dir")
228
- [ "$dir" = "/" ] && break
229
- done
302
+ /**
303
+ * Shared template for hooks that inject Everstate context via stdout.
304
+ * Used by both PreCompact and SubagentStart hooks.
305
+ */
306
+ function getContextInjectionHook(opts: {
307
+ hookName: string;
308
+ version: string;
309
+ generatedAt: string;
310
+ apiKeyPath: string;
311
+ apiUrl: string;
312
+ preamble: string;
313
+ timeout: number;
314
+ includeBlockers: boolean;
315
+ includeTasks: boolean;
316
+ maxGotchas: number;
317
+ maxDecisions: number;
318
+ postAction?: string;
319
+ }): string {
320
+ return `#!/usr/bin/env node
321
+ /**
322
+ * Everstate ${opts.hookName} Hook v${opts.version}
323
+ * VERSION: ${opts.version}
324
+ * GENERATED: ${opts.generatedAt}
325
+ *
326
+ * Prints critical Everstate memory to stdout.
327
+ * Claude adds hook stdout to the agent's context.
328
+ */
329
+
330
+ const fs = require('fs');
331
+ const path = require('path');
332
+
333
+ const API_KEY_PATH = '${opts.apiKeyPath}';
334
+ const API_URL = '${opts.apiUrl}';
335
+
336
+ async function main() {
337
+ if (!fs.existsSync(API_KEY_PATH)) process.exit(0);
338
+ const apiKey = fs.readFileSync(API_KEY_PATH, 'utf8').trim();
339
+
340
+ let projectId = null;
341
+ let searchDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
342
+ for (let i = 0; i < 10; i++) {
343
+ for (const cn of ['.everstate.json', '.codekeep.json']) {
344
+ const cp = path.join(searchDir, cn);
345
+ if (fs.existsSync(cp)) {
346
+ try { projectId = JSON.parse(fs.readFileSync(cp, 'utf8')).projectId; } catch {}
347
+ }
348
+ if (projectId) break;
349
+ }
350
+ if (projectId) break;
351
+ const parent = path.dirname(searchDir);
352
+ if (parent === searchDir) break;
353
+ searchDir = parent;
354
+ }
355
+ if (!projectId) process.exit(0);
356
+
357
+ try {
358
+ const response = await fetch(\\\`\\\${API_URL}/api/context/handoff?projectId=\\\${projectId}\\\`, {
359
+ headers: { 'Authorization': \\\`Bearer \\\${apiKey}\\\` },
360
+ signal: AbortSignal.timeout(${opts.timeout}),
361
+ });
362
+ if (!response.ok) process.exit(0);
363
+ const ctx = await response.json();
364
+
365
+ const lines = ['[Everstate] ${opts.preamble}:', ''];
366
+
367
+ if (ctx.activeDirective) {
368
+ lines.push(\\\`## Current Directive: \\\${ctx.activeDirective.title}\\\`);
369
+ if (ctx.activeDirective.currentFocus) lines.push(\\\`> \\\${ctx.activeDirective.currentFocus}\\\`);
370
+ if (ctx.activeDirective.branch) lines.push(\\\`Branch: \\\${ctx.activeDirective.branch}\\\`);
371
+ lines.push('');
372
+ }
373
+
374
+ if (ctx.gotchas && ctx.gotchas.length > 0) {
375
+ lines.push('## Gotchas');
376
+ for (const g of ctx.gotchas.slice(0, ${opts.maxGotchas})) {
377
+ lines.push(\\\`- \\\${g.title}: \\\${g.description.substring(0, 150)}\\\`);
378
+ }
379
+ lines.push('');
380
+ }
381
+
382
+ if (ctx.recentDecisions && ctx.recentDecisions.length > 0) {
383
+ lines.push('## Decisions');
384
+ for (const d of ctx.recentDecisions.slice(0, ${opts.maxDecisions})) {
385
+ lines.push(\\\`- \\\${d.summary}\\\${d.rationale ? ' (' + d.rationale.substring(0, 100) + ')' : ''}\\\`);
386
+ }
387
+ lines.push('');
388
+ }
389
+ ${opts.includeTasks ? `
390
+ if (ctx.tasks) {
391
+ const inProgress = ctx.tasks.inProgress || [];
392
+ const pending = ctx.tasks.pending || [];
393
+ if (inProgress.length > 0 || pending.length > 0) {
394
+ lines.push('## Tasks');
395
+ for (const t of inProgress) lines.push(\\\`- [in_progress] \\\${t.title}\\\`);
396
+ for (const t of pending.slice(0, 5)) lines.push(\\\`- [pending] \\\${t.title}\\\`);
397
+ lines.push('');
398
+ }
399
+ }
400
+ ` : ''}${opts.includeBlockers ? `
401
+ if (ctx.blockers && ctx.blockers.length > 0) {
402
+ lines.push('## Blockers');
403
+ for (const b of ctx.blockers) {
404
+ lines.push(\\\`- \\\${b.title} (\\\${b.severity}): \\\${b.description.substring(0, 100)}\\\`);
405
+ }
406
+ lines.push('');
407
+ }
408
+ ` : ''}
409
+ if (ctx.doNotModify && ctx.doNotModify.length > 0) {
410
+ lines.push('Do not modify:');
411
+ for (const f of ctx.doNotModify) lines.push(\\\`- \\\${f.filePath}: \\\${f.reason}\\\`);
412
+ lines.push('');
413
+ }
414
+
415
+ if (lines.length > 2) console.log(lines.join('\\n'));
416
+ ${opts.postAction || ''}
417
+ } catch {
418
+ try {
419
+ const cachePath = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', '.gotcha-cache.json');
420
+ if (fs.existsSync(cachePath)) {
421
+ const gotchas = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
422
+ if (Array.isArray(gotchas) && gotchas.length > 0) {
423
+ console.log('[Everstate] Known gotchas:');
424
+ for (const g of gotchas.slice(0, ${opts.maxGotchas})) console.log(\\\`- \\\${g.title}: \\\${g.description.substring(0, 150)}\\\`);
425
+ }
426
+ }
427
+ } catch {}
428
+ }
230
429
  }
231
430
 
232
- PROJECT_ID=$(find_project_id)
233
- if [ -z "$PROJECT_ID" ]; then
234
- exit 0
235
- fi
431
+ main().catch(() => process.exit(0));
432
+ `;
433
+ }
434
+
435
+ /**
436
+ * Get the implicit-progress.js hook content (PostToolUse for Edit/Write/Bash)
437
+ */
438
+ export function getImplicitProgressHook(config?: Partial<HookConfig>): string {
439
+ const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
440
+ const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
441
+ const generatedAt = config?.generatedAt || new Date().toISOString();
442
+
443
+ return `#!/usr/bin/env node
444
+ /**
445
+ * Everstate Implicit Progress Hook v${HOOK_VERSIONS.syncTodos}
446
+ * VERSION: ${HOOK_VERSIONS.syncTodos}
447
+ * GENERATED: ${generatedAt}
448
+ *
449
+ * Automatically logs progress when files are edited or commands run.
450
+ * Runs as PostToolUse hook for Edit, Write, and Bash tools.
451
+ */
236
452
 
237
- # Get transcript from environment if available
238
- TRANSCRIPT="\${CLAUDE_TRANSCRIPT:-}"
453
+ const fs = require('fs');
454
+ const path = require('path');
239
455
 
240
- # Signal pre-compact to API
241
- curl -s -X POST "$API_URL/api/session/pre-compact" \\
242
- -H "Content-Type: application/json" \\
243
- -H "Authorization: Bearer $API_KEY" \\
244
- -d "{\\"projectId\\": \\"$PROJECT_ID\\", \\"transcript\\": \\"\$TRANSCRIPT\\"}" > /dev/null 2>&1
456
+ const API_KEY_PATH = '${apiKeyPath}';
457
+ const API_URL = '${apiUrl}';
245
458
 
246
- exit 0
459
+ let stdinData = '';
460
+ process.stdin.setEncoding('utf8');
461
+ process.stdin.on('data', chunk => stdinData += chunk);
462
+ process.stdin.on('end', () => main());
463
+
464
+ async function main() {
465
+ try {
466
+ if (!fs.existsSync(API_KEY_PATH)) process.exit(0);
467
+ const apiKey = fs.readFileSync(API_KEY_PATH, 'utf8').trim();
468
+
469
+ const hookData = JSON.parse(stdinData || '{}');
470
+ const toolName = hookData.tool_name;
471
+ const toolInput = hookData.tool_input || {};
472
+
473
+ let activity = null;
474
+ if (toolName === 'Edit' || toolName === 'Write') {
475
+ const filePath = toolInput.file_path || toolInput.filePath || '';
476
+ if (filePath) activity = { tool: toolName.toLowerCase(), file: path.basename(filePath), path: filePath };
477
+ } else if (toolName === 'Bash') {
478
+ const cmd = toolInput.command || '';
479
+ if (cmd && !cmd.startsWith('git status') && !cmd.startsWith('ls '))
480
+ activity = { tool: 'bash', command: cmd.substring(0, 100) };
481
+ }
482
+
483
+ if (!activity) process.exit(0);
484
+
485
+ // Find project
486
+ let projectId = null;
487
+ let searchDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
488
+ for (let i = 0; i < 10; i++) {
489
+ for (const cn of ['.everstate.json', '.codekeep.json']) {
490
+ const cp = path.join(searchDir, cn);
491
+ if (fs.existsSync(cp)) {
492
+ try { projectId = JSON.parse(fs.readFileSync(cp, 'utf8')).projectId; } catch {}
493
+ }
494
+ if (projectId) break;
495
+ }
496
+ if (projectId) break;
497
+ const parent = path.dirname(searchDir);
498
+ if (parent === searchDir) break;
499
+ searchDir = parent;
500
+ }
501
+ if (!projectId) process.exit(0);
502
+
503
+ // Debounce: 1 per file per 60s
504
+ const df = path.join(process.env.CLAUDE_PROJECT_DIR || process.cwd(), '.claude', '.progress-debounce.json');
505
+ let db = {};
506
+ try { if (fs.existsSync(df)) db = JSON.parse(fs.readFileSync(df, 'utf8')); } catch {}
507
+ const key = activity.file || activity.command || 'x';
508
+ const now = Date.now();
509
+ if (db[key] && (now - db[key]) < 60000) process.exit(0);
510
+ db[key] = now;
511
+ for (const [k, v] of Object.entries(db)) { if ((now - v) > 300000) delete db[k]; }
512
+ try { fs.mkdirSync(path.dirname(df), { recursive: true }); fs.writeFileSync(df, JSON.stringify(db)); } catch {}
513
+
514
+ await fetch(\`\${API_URL}/api/session/implicit-progress\`, {
515
+ method: 'POST',
516
+ headers: { 'Content-Type': 'application/json', 'Authorization': \`Bearer \${apiKey}\` },
517
+ body: JSON.stringify({ projectId, activity }),
518
+ }).catch(() => {});
519
+ } catch {}
520
+ process.exit(0);
521
+ }
522
+ `;
523
+ }
524
+
525
+ /**
526
+ * Get the gotcha-check.js hook content (PreToolUse for Edit/Write)
527
+ */
528
+ export function getGotchaCheckHook(config?: Partial<HookConfig>): string {
529
+ const generatedAt = config?.generatedAt || new Date().toISOString();
530
+
531
+ return `#!/usr/bin/env node
532
+ /**
533
+ * Everstate Gotcha Check Hook v${HOOK_VERSIONS.syncTodos}
534
+ * VERSION: ${HOOK_VERSIONS.syncTodos}
535
+ * GENERATED: ${generatedAt}
536
+ *
537
+ * Surfaces relevant gotchas before file edits.
538
+ * Runs as PreToolUse hook for Edit and Write tools.
539
+ *
540
+ * Claude Code passes hook data via STDIN as JSON:
541
+ * { "tool_name": "Edit", "tool_input": { "file_path": "..." }, ... }
542
+ */
543
+
544
+ const fs = require('fs');
545
+ const path = require('path');
546
+
547
+ let stdinData = '';
548
+ process.stdin.setEncoding('utf8');
549
+ process.stdin.on('data', chunk => stdinData += chunk);
550
+ process.stdin.on('end', () => main());
551
+
552
+ function main() {
553
+ try {
554
+ const hookData = JSON.parse(stdinData || '{}');
555
+ const filePath = hookData.tool_input?.file_path || hookData.tool_input?.filePath || '';
556
+ if (!filePath) {
557
+ process.exit(0);
558
+ }
559
+
560
+ // Read gotcha cache
561
+ const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
562
+ const cachePath = path.join(projectDir, '.claude', '.gotcha-cache.json');
563
+ if (!fs.existsSync(cachePath)) {
564
+ process.exit(0);
565
+ }
566
+
567
+ const gotchas = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
568
+ if (!Array.isArray(gotchas) || gotchas.length === 0) {
569
+ process.exit(0);
570
+ }
571
+
572
+ // Match gotchas against file path
573
+ const fileLC = filePath.toLowerCase();
574
+ const fileName = path.basename(filePath).toLowerCase();
575
+ const matched = [];
576
+
577
+ for (const gotcha of gotchas) {
578
+ if (!gotcha.keywords || !Array.isArray(gotcha.keywords)) continue;
579
+ for (const keyword of gotcha.keywords) {
580
+ if (fileLC.includes(keyword) || fileName.includes(keyword)) {
581
+ matched.push(gotcha);
582
+ break;
583
+ }
584
+ }
585
+ }
586
+
587
+ if (matched.length > 0) {
588
+ const sevIcon = { critical: '!!!', warning: '>>', info: '>' };
589
+ console.log('');
590
+ console.log('[Everstate] Gotcha warning for ' + path.basename(filePath) + ':');
591
+ for (const g of matched.slice(0, 3)) {
592
+ console.log((sevIcon[g.severity] || '>>') + ' ' + g.title + ': ' + g.description.substring(0, 150));
593
+ }
594
+ console.log('');
595
+ }
596
+ } catch {
597
+ // Silent failure - don't block tool execution
598
+ }
599
+ process.exit(0);
600
+ }
247
601
  `;
248
602
  }
249
603
 
@@ -252,4 +606,7 @@ export const hookTemplates = {
252
606
  sessionStart: getSessionStartHook,
253
607
  sessionEnd: getSessionEndHook,
254
608
  preCompact: getPreCompactHook,
609
+ subagentContext: getSubagentContextHook,
610
+ gotchaCheck: getGotchaCheckHook,
611
+ implicitProgress: getImplicitProgressHook,
255
612
  };
@@ -177,7 +177,9 @@ export function c(color: keyof Colors, text: string): string {
177
177
  export const HOOK_VERSIONS = {
178
178
  sessionStart: '1.1.0',
179
179
  sessionEnd: '1.1.0',
180
- syncTodos: '5.1.0', // Fixed: read from stdin instead of TOOL_INPUT env var
180
+ syncTodos: '6.0.0', // Two-way sync: dashboard changes reported back to Claude via stdout
181
+ preCompact: '2.0.0', // Re-injects critical context (gotchas, decisions, tasks) before compaction
182
+ subagentContext: '1.0.0', // Injects project context when subagents spawn
181
183
  };
182
184
 
183
185
  export const EVERSTATE_API_URL = 'https://www.everstate.ai';