@everstateai/mcp 1.3.2 → 1.3.4

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.
Files changed (102) hide show
  1. package/README.md +42 -6
  2. package/dist/commands/api-client.d.ts +35 -0
  3. package/dist/commands/api-client.d.ts.map +1 -0
  4. package/dist/commands/api-client.js +138 -0
  5. package/dist/commands/api-client.js.map +1 -0
  6. package/dist/commands/done.d.ts +7 -0
  7. package/dist/commands/done.d.ts.map +1 -0
  8. package/dist/commands/done.js +57 -0
  9. package/dist/commands/done.js.map +1 -0
  10. package/dist/commands/recall.d.ts +7 -0
  11. package/dist/commands/recall.d.ts.map +1 -0
  12. package/dist/commands/recall.js +36 -0
  13. package/dist/commands/recall.js.map +1 -0
  14. package/dist/commands/status.d.ts +7 -0
  15. package/dist/commands/status.d.ts.map +1 -0
  16. package/dist/commands/status.js +33 -0
  17. package/dist/commands/status.js.map +1 -0
  18. package/dist/commands/sync.d.ts +7 -0
  19. package/dist/commands/sync.d.ts.map +1 -0
  20. package/dist/commands/sync.js +34 -0
  21. package/dist/commands/sync.js.map +1 -0
  22. package/dist/index.d.ts +9 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +68 -5
  25. package/dist/index.js.map +1 -1
  26. package/dist/setup/auto-update.d.ts +20 -0
  27. package/dist/setup/auto-update.d.ts.map +1 -0
  28. package/dist/setup/auto-update.js +295 -0
  29. package/dist/setup/auto-update.js.map +1 -0
  30. package/dist/setup/commands/doctor.d.ts +15 -0
  31. package/dist/setup/commands/doctor.d.ts.map +1 -0
  32. package/dist/setup/commands/doctor.js +264 -0
  33. package/dist/setup/commands/doctor.js.map +1 -0
  34. package/dist/setup/commands/repair.d.ts +14 -0
  35. package/dist/setup/commands/repair.d.ts.map +1 -0
  36. package/dist/setup/commands/repair.js +252 -0
  37. package/dist/setup/commands/repair.js.map +1 -0
  38. package/dist/setup/environments.d.ts +48 -0
  39. package/dist/setup/environments.d.ts.map +1 -0
  40. package/dist/setup/environments.js +222 -0
  41. package/dist/setup/environments.js.map +1 -0
  42. package/dist/setup/hooks/templates.d.ts +30 -0
  43. package/dist/setup/hooks/templates.d.ts.map +1 -0
  44. package/dist/setup/hooks/templates.js +253 -0
  45. package/dist/setup/hooks/templates.js.map +1 -0
  46. package/dist/setup/types.d.ts +122 -0
  47. package/dist/setup/types.d.ts.map +1 -0
  48. package/dist/setup/types.js +66 -0
  49. package/dist/setup/types.js.map +1 -0
  50. package/dist/setup/validators/api-key.d.ts +8 -0
  51. package/dist/setup/validators/api-key.d.ts.map +1 -0
  52. package/dist/setup/validators/api-key.js +233 -0
  53. package/dist/setup/validators/api-key.js.map +1 -0
  54. package/dist/setup/validators/connectivity.d.ts +8 -0
  55. package/dist/setup/validators/connectivity.d.ts.map +1 -0
  56. package/dist/setup/validators/connectivity.js +150 -0
  57. package/dist/setup/validators/connectivity.js.map +1 -0
  58. package/dist/setup/validators/hooks.d.ts +8 -0
  59. package/dist/setup/validators/hooks.d.ts.map +1 -0
  60. package/dist/setup/validators/hooks.js +431 -0
  61. package/dist/setup/validators/hooks.js.map +1 -0
  62. package/dist/setup/validators/index.d.ts +18 -0
  63. package/dist/setup/validators/index.d.ts.map +1 -0
  64. package/dist/setup/validators/index.js +123 -0
  65. package/dist/setup/validators/index.js.map +1 -0
  66. package/dist/setup/validators/mcp-config.d.ts +9 -0
  67. package/dist/setup/validators/mcp-config.d.ts.map +1 -0
  68. package/dist/setup/validators/mcp-config.js +302 -0
  69. package/dist/setup/validators/mcp-config.js.map +1 -0
  70. package/dist/setup/validators/project.d.ts +8 -0
  71. package/dist/setup/validators/project.d.ts.map +1 -0
  72. package/dist/setup/validators/project.js +202 -0
  73. package/dist/setup/validators/project.js.map +1 -0
  74. package/dist/setup/version.d.ts +58 -0
  75. package/dist/setup/version.d.ts.map +1 -0
  76. package/dist/setup/version.js +262 -0
  77. package/dist/setup/version.js.map +1 -0
  78. package/dist/setup.d.ts +1 -0
  79. package/dist/setup.d.ts.map +1 -1
  80. package/dist/setup.js +397 -214
  81. package/dist/setup.js.map +1 -1
  82. package/package.json +1 -1
  83. package/src/commands/api-client.ts +117 -0
  84. package/src/commands/done.ts +63 -0
  85. package/src/commands/recall.ts +40 -0
  86. package/src/commands/status.ts +36 -0
  87. package/src/commands/sync.ts +37 -0
  88. package/src/index.ts +64 -5
  89. package/src/setup/auto-update.ts +328 -0
  90. package/src/setup/commands/doctor.ts +266 -0
  91. package/src/setup/commands/repair.ts +260 -0
  92. package/src/setup/environments.ts +225 -0
  93. package/src/setup/hooks/templates.ts +255 -0
  94. package/src/setup/types.ts +207 -0
  95. package/src/setup/validators/api-key.ts +218 -0
  96. package/src/setup/validators/connectivity.ts +176 -0
  97. package/src/setup/validators/hooks.ts +447 -0
  98. package/src/setup/validators/index.ts +137 -0
  99. package/src/setup/validators/mcp-config.ts +288 -0
  100. package/src/setup/validators/project.ts +179 -0
  101. package/src/setup/version.ts +267 -0
  102. package/src/setup.ts +471 -232
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Hook Templates
3
+ *
4
+ * Versioned hook templates for installation and updates.
5
+ * Each hook includes a VERSION header for validation.
6
+ */
7
+
8
+ import { HookConfig, HOOK_VERSIONS, EVERSTATE_API_URL, getEverstateDir } from '../types.js';
9
+
10
+ /**
11
+ * Get the sync-todos.js hook content
12
+ */
13
+ export function getSyncTodosHook(config?: Partial<HookConfig>): string {
14
+ const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
15
+ const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
16
+ const generatedAt = config?.generatedAt || new Date().toISOString();
17
+
18
+ return `#!/usr/bin/env node
19
+ /**
20
+ * Everstate Sync Todos Hook v${HOOK_VERSIONS.syncTodos}
21
+ * VERSION: ${HOOK_VERSIONS.syncTodos}
22
+ * GENERATED: ${generatedAt}
23
+ *
24
+ * Syncs Claude's TodoWrite tasks with Everstate.
25
+ * Runs after every TodoWrite tool call.
26
+ *
27
+ * Claude Code passes hook data via STDIN as JSON:
28
+ * { "tool_name": "TodoWrite", "tool_input": { "todos": [...] }, ... }
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+
34
+ const API_KEY_PATH = '${apiKeyPath}';
35
+ const API_URL = '${apiUrl}';
36
+
37
+ // Read all stdin first (Claude Code sends hook data via stdin)
38
+ let stdinData = '';
39
+ process.stdin.setEncoding('utf8');
40
+ process.stdin.on('data', chunk => stdinData += chunk);
41
+ process.stdin.on('end', () => main());
42
+
43
+ async function main() {
44
+ try {
45
+ // Read API key
46
+ if (!fs.existsSync(API_KEY_PATH)) {
47
+ process.exit(0); // Don't block Claude
48
+ }
49
+ const apiKey = fs.readFileSync(API_KEY_PATH, 'utf8').trim();
50
+
51
+ // Parse input from Claude (comes via stdin)
52
+ let todos = [];
53
+ try {
54
+ const hookData = JSON.parse(stdinData || '{}');
55
+ // Claude Code sends: { tool_name, tool_input: { todos: [...] }, ... }
56
+ todos = hookData.tool_input?.todos || [];
57
+ } catch {
58
+ process.exit(0);
59
+ }
60
+
61
+ if (todos.length === 0) {
62
+ process.exit(0);
63
+ }
64
+
65
+ // Find project config - check CLAUDE_PROJECT_DIR first, then cwd
66
+ let projectId = null;
67
+ let searchDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
68
+
69
+ for (let i = 0; i < 10; i++) {
70
+ for (const configName of ['.everstate.json', '.codekeep.json']) {
71
+ const configPath = path.join(searchDir, configName);
72
+ if (fs.existsSync(configPath)) {
73
+ try {
74
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
75
+ projectId = config.projectId;
76
+ break;
77
+ } catch {}
78
+ }
79
+ }
80
+ if (projectId) break;
81
+ const parent = path.dirname(searchDir);
82
+ if (parent === searchDir) break;
83
+ searchDir = parent;
84
+ }
85
+
86
+ if (!projectId) {
87
+ // No project config - skip sync
88
+ process.exit(0);
89
+ }
90
+
91
+ // Send to API for matching and storage
92
+ const response = await fetch(\`\${API_URL}/api/session/sync-todos\`, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'Authorization': \`Bearer \${apiKey}\`,
97
+ },
98
+ body: JSON.stringify({
99
+ projectId,
100
+ todos,
101
+ sessionId: process.env.SESSION_ID || null,
102
+ }),
103
+ });
104
+
105
+ if (!response.ok) {
106
+ const text = await response.text();
107
+ console.error('[everstate] Sync failed:', response.status, text);
108
+ }
109
+ } catch (error) {
110
+ console.error('[everstate] Hook error:', error.message);
111
+ }
112
+
113
+ process.exit(0);
114
+ }
115
+ `;
116
+ }
117
+
118
+ /**
119
+ * Get the session-start.sh hook content
120
+ */
121
+ export function getSessionStartHook(config?: Partial<HookConfig>): string {
122
+ const generatedAt = config?.generatedAt || new Date().toISOString();
123
+
124
+ return `#!/bin/bash
125
+ # Everstate Session Start Hook v${HOOK_VERSIONS.sessionStart}
126
+ # VERSION: ${HOOK_VERSIONS.sessionStart}
127
+ # GENERATED: ${generatedAt}
128
+ #
129
+ # Loads context at session start.
130
+ # Place in .claude/hooks/ directory.
131
+
132
+ # Check for MCP - if available, tools handle context loading
133
+ if command -v claude &> /dev/null; then
134
+ # Claude Code detected - MCP tools will load context
135
+ exit 0
136
+ fi
137
+
138
+ # Fallback for environments without MCP
139
+ echo "[everstate] Session starting - use sync() to load context"
140
+ `;
141
+ }
142
+
143
+ /**
144
+ * Get the session-end.sh hook content
145
+ */
146
+ export function getSessionEndHook(config?: Partial<HookConfig>): string {
147
+ const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
148
+ const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
149
+ const generatedAt = config?.generatedAt || new Date().toISOString();
150
+
151
+ return `#!/bin/bash
152
+ # Everstate Session End Hook v${HOOK_VERSIONS.sessionEnd}
153
+ # VERSION: ${HOOK_VERSIONS.sessionEnd}
154
+ # GENERATED: ${generatedAt}
155
+ #
156
+ # Saves session summary when Claude session ends.
157
+
158
+ API_KEY_PATH="${apiKeyPath}"
159
+ API_URL="${apiUrl}"
160
+
161
+ # Read API key
162
+ if [ ! -f "$API_KEY_PATH" ]; then
163
+ exit 0
164
+ fi
165
+ API_KEY=$(cat "$API_KEY_PATH")
166
+
167
+ # Find project config
168
+ find_project_id() {
169
+ local dir="$PWD"
170
+ for i in {1..10}; do
171
+ if [ -f "$dir/.everstate.json" ]; then
172
+ grep -o '"projectId"[[:space:]]*:[[:space:]]*"[^"]*"' "$dir/.everstate.json" | cut -d'"' -f4
173
+ return
174
+ fi
175
+ dir=$(dirname "$dir")
176
+ [ "$dir" = "/" ] && break
177
+ done
178
+ }
179
+
180
+ PROJECT_ID=$(find_project_id)
181
+ if [ -z "$PROJECT_ID" ]; then
182
+ exit 0
183
+ fi
184
+
185
+ # Signal session end to API
186
+ curl -s -X POST "$API_URL/api/session/end" \\
187
+ -H "Content-Type: application/json" \\
188
+ -H "Authorization: Bearer $API_KEY" \\
189
+ -d "{\\"projectId\\": \\"$PROJECT_ID\\"}" > /dev/null 2>&1
190
+
191
+ exit 0
192
+ `;
193
+ }
194
+
195
+ /**
196
+ * Get the pre-compact.sh hook content (for context compaction)
197
+ */
198
+ export function getPreCompactHook(config?: Partial<HookConfig>): string {
199
+ const apiKeyPath = config?.apiKeyPath || `${getEverstateDir()}/api-key`;
200
+ const apiUrl = config?.apiUrl || EVERSTATE_API_URL;
201
+ const generatedAt = config?.generatedAt || new Date().toISOString();
202
+
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.
209
+
210
+ API_KEY_PATH="${apiKeyPath}"
211
+ API_URL="${apiUrl}"
212
+
213
+ # Read API key
214
+ if [ ! -f "$API_KEY_PATH" ]; then
215
+ exit 0
216
+ fi
217
+ API_KEY=$(cat "$API_KEY_PATH")
218
+
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
230
+ }
231
+
232
+ PROJECT_ID=$(find_project_id)
233
+ if [ -z "$PROJECT_ID" ]; then
234
+ exit 0
235
+ fi
236
+
237
+ # Get transcript from environment if available
238
+ TRANSCRIPT="\${CLAUDE_TRANSCRIPT:-}"
239
+
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
245
+
246
+ exit 0
247
+ `;
248
+ }
249
+
250
+ export const hookTemplates = {
251
+ syncTodos: getSyncTodosHook,
252
+ sessionStart: getSessionStartHook,
253
+ sessionEnd: getSessionEndHook,
254
+ preCompact: getPreCompactHook,
255
+ };
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Everstate Setup Types
3
+ *
4
+ * Shared types for the setup, doctor, and repair commands.
5
+ */
6
+
7
+ // ============================================================================
8
+ // Validation Types
9
+ // ============================================================================
10
+
11
+ export type ValidationStatus = 'pass' | 'warn' | 'fail';
12
+
13
+ export type RepairType = 'install' | 'update' | 'configure' | 'delete' | 'chmod';
14
+
15
+ export interface RepairAction {
16
+ type: RepairType;
17
+ description: string;
18
+ automatic: boolean; // Can be auto-repaired without user input
19
+ }
20
+
21
+ export interface ValidationResult {
22
+ component: string;
23
+ check: string;
24
+ status: ValidationStatus;
25
+ message: string;
26
+ details?: Record<string, unknown>;
27
+ repairAction?: RepairAction;
28
+ }
29
+
30
+ export interface Validator {
31
+ name: string;
32
+ validate(context: ValidationContext): Promise<ValidationResult[]>;
33
+ repair?(result: ValidationResult, context: ValidationContext): Promise<boolean>;
34
+ }
35
+
36
+ export interface ValidationContext {
37
+ projectDir?: string;
38
+ apiKey?: string;
39
+ verbose?: boolean;
40
+ skipNetwork?: boolean;
41
+ }
42
+
43
+ export interface ValidationSummary {
44
+ passed: number;
45
+ warnings: number;
46
+ failed: number;
47
+ results: ValidationResult[];
48
+ }
49
+
50
+ // ============================================================================
51
+ // Version Tracking Types
52
+ // ============================================================================
53
+
54
+ export interface HookVersions {
55
+ sessionStart: string;
56
+ sessionEnd: string;
57
+ syncTodos: string;
58
+ }
59
+
60
+ export interface ComponentVersions {
61
+ mcpProxy: string;
62
+ hooks: HookVersions;
63
+ }
64
+
65
+ export interface ConfigPaths {
66
+ claudeCode: string;
67
+ claudeDesktop?: string;
68
+ apiKey: string;
69
+ globalHooks: string;
70
+ }
71
+
72
+ export interface ProjectInstallation {
73
+ projectId: string;
74
+ configVersion: string;
75
+ hooksVersion: string;
76
+ lastValidated: string;
77
+ }
78
+
79
+ export interface VersionFile {
80
+ installedAt: string;
81
+ installedBy: 'setup' | 'npx' | 'manual';
82
+ components: ComponentVersions;
83
+ configPaths: ConfigPaths;
84
+ projects: Record<string, ProjectInstallation>;
85
+ lastUpdateCheck?: string;
86
+ }
87
+
88
+ // ============================================================================
89
+ // Hook Types
90
+ // ============================================================================
91
+
92
+ export interface HookTemplate {
93
+ name: string;
94
+ version: string;
95
+ filename: string;
96
+ content: (config: HookConfig) => string;
97
+ }
98
+
99
+ export interface HookConfig {
100
+ apiKeyPath: string;
101
+ apiUrl: string;
102
+ generatedAt: string;
103
+ }
104
+
105
+ // ============================================================================
106
+ // Setup Types
107
+ // ============================================================================
108
+
109
+ export interface SetupOptions {
110
+ apiKey: string;
111
+ projectDir: string;
112
+ projectId?: string;
113
+ skipHooks?: boolean;
114
+ force?: boolean;
115
+ globalOnly?: boolean;
116
+ }
117
+
118
+ export interface DoctorOptions {
119
+ projectDir?: string;
120
+ json?: boolean;
121
+ verbose?: boolean;
122
+ component?: string;
123
+ }
124
+
125
+ export interface RepairOptions {
126
+ projectDir?: string;
127
+ force?: boolean;
128
+ component?: string;
129
+ includeWarnings?: boolean;
130
+ }
131
+
132
+ // ============================================================================
133
+ // Console Output Types
134
+ // ============================================================================
135
+
136
+ export interface Colors {
137
+ reset: string;
138
+ red: string;
139
+ green: string;
140
+ yellow: string;
141
+ blue: string;
142
+ cyan: string;
143
+ bold: string;
144
+ dim: string;
145
+ }
146
+
147
+ export const COLORS: Colors = {
148
+ reset: '\x1b[0m',
149
+ red: '\x1b[31m',
150
+ green: '\x1b[32m',
151
+ yellow: '\x1b[33m',
152
+ blue: '\x1b[34m',
153
+ cyan: '\x1b[36m',
154
+ bold: '\x1b[1m',
155
+ dim: '\x1b[2m',
156
+ };
157
+
158
+ export function supportsColor(): boolean {
159
+ return (
160
+ process.stdout.isTTY === true &&
161
+ process.env.FORCE_COLOR !== '0' &&
162
+ (process.platform !== 'win32' ||
163
+ process.env.TERM === 'xterm-256color' ||
164
+ !!process.env.WT_SESSION)
165
+ );
166
+ }
167
+
168
+ export function c(color: keyof Colors, text: string): string {
169
+ return supportsColor() ? `${COLORS[color]}${text}${COLORS.reset}` : text;
170
+ }
171
+
172
+ // ============================================================================
173
+ // Constants
174
+ // ============================================================================
175
+
176
+ export const HOOK_VERSIONS = {
177
+ sessionStart: '1.1.0',
178
+ sessionEnd: '1.1.0',
179
+ syncTodos: '5.1.0', // Fixed: read from stdin instead of TOOL_INPUT env var
180
+ };
181
+
182
+ export const EVERSTATE_API_URL = 'https://www.everstate.ai';
183
+
184
+ export function getEverstateDir(): string {
185
+ return `${process.env.HOME}/.everstate`;
186
+ }
187
+
188
+ export function getClaudeConfigPath(): string {
189
+ return `${process.env.HOME}/.claude.json`;
190
+ }
191
+
192
+ export function getClaudeDesktopConfigPath(): string {
193
+ if (process.platform === 'darwin') {
194
+ return `${process.env.HOME}/Library/Application Support/Claude/claude_desktop_config.json`;
195
+ } else if (process.platform === 'win32') {
196
+ return `${process.env.APPDATA}/Claude/claude_desktop_config.json`;
197
+ }
198
+ return ''; // Linux - no known Claude Desktop location
199
+ }
200
+
201
+ export function getCursorConfigPath(): string {
202
+ return `${process.env.HOME}/.cursor/mcp.json`;
203
+ }
204
+
205
+ export function getWindsurfConfigPath(): string {
206
+ return `${process.env.HOME}/.codeium/windsurf/mcp_config.json`;
207
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * API Key Validator
3
+ *
4
+ * Validates that the API key exists, has correct format, and authenticates.
5
+ */
6
+
7
+ import * as fs from 'fs';
8
+ import * as https from 'https';
9
+ import {
10
+ Validator,
11
+ ValidationResult,
12
+ ValidationContext,
13
+ getEverstateDir,
14
+ EVERSTATE_API_URL,
15
+ } from '../types.js';
16
+
17
+ export const apiKeyValidator: Validator = {
18
+ name: 'API Key',
19
+
20
+ async validate(context: ValidationContext): Promise<ValidationResult[]> {
21
+ const results: ValidationResult[] = [];
22
+ const apiKeyPath = `${getEverstateDir()}/api-key`;
23
+
24
+ // Check 1: API key file exists
25
+ if (!fs.existsSync(apiKeyPath)) {
26
+ results.push({
27
+ component: 'API Key',
28
+ check: 'File exists',
29
+ status: 'fail',
30
+ message: `API key file not found at ${apiKeyPath}`,
31
+ repairAction: {
32
+ type: 'install',
33
+ description: 'Run setup with your API key',
34
+ automatic: false,
35
+ },
36
+ });
37
+ return results; // Can't continue without file
38
+ }
39
+
40
+ results.push({
41
+ component: 'API Key',
42
+ check: 'File exists',
43
+ status: 'pass',
44
+ message: `Found at ${apiKeyPath}`,
45
+ });
46
+
47
+ // Check 2: File is not empty and has correct format
48
+ let apiKey: string;
49
+ try {
50
+ apiKey = fs.readFileSync(apiKeyPath, 'utf8').trim();
51
+ } catch (err) {
52
+ results.push({
53
+ component: 'API Key',
54
+ check: 'Readable',
55
+ status: 'fail',
56
+ message: `Cannot read API key file: ${err instanceof Error ? err.message : 'Unknown error'}`,
57
+ repairAction: {
58
+ type: 'chmod',
59
+ description: 'Fix file permissions',
60
+ automatic: true,
61
+ },
62
+ });
63
+ return results;
64
+ }
65
+
66
+ if (!apiKey) {
67
+ results.push({
68
+ component: 'API Key',
69
+ check: 'Not empty',
70
+ status: 'fail',
71
+ message: 'API key file is empty',
72
+ repairAction: {
73
+ type: 'configure',
74
+ description: 'Run setup with your API key',
75
+ automatic: false,
76
+ },
77
+ });
78
+ return results;
79
+ }
80
+
81
+ // Check 3: Format validation
82
+ if (!apiKey.startsWith('ck_') && !apiKey.startsWith('ckp_')) {
83
+ results.push({
84
+ component: 'API Key',
85
+ check: 'Format',
86
+ status: 'fail',
87
+ message: `Invalid format - must start with 'ck_' or 'ckp_'`,
88
+ details: { keyPrefix: apiKey.substring(0, 4) },
89
+ repairAction: {
90
+ type: 'configure',
91
+ description: 'Replace with valid API key',
92
+ automatic: false,
93
+ },
94
+ });
95
+ return results;
96
+ }
97
+
98
+ if (apiKey.length < 20) {
99
+ results.push({
100
+ component: 'API Key',
101
+ check: 'Format',
102
+ status: 'warn',
103
+ message: 'API key appears unusually short',
104
+ details: { length: apiKey.length },
105
+ });
106
+ } else {
107
+ results.push({
108
+ component: 'API Key',
109
+ check: 'Format',
110
+ status: 'pass',
111
+ message: `Valid format (${apiKey.substring(0, 7)}...)`,
112
+ });
113
+ }
114
+
115
+ // Check 4: Authentication (skip if network checks disabled)
116
+ if (context.skipNetwork) {
117
+ results.push({
118
+ component: 'API Key',
119
+ check: 'Authentication',
120
+ status: 'warn',
121
+ message: 'Skipped (network checks disabled)',
122
+ });
123
+ return results;
124
+ }
125
+
126
+ const authResult = await checkAuthentication(apiKey);
127
+ results.push(authResult);
128
+
129
+ return results;
130
+ },
131
+
132
+ async repair(result: ValidationResult, context: ValidationContext): Promise<boolean> {
133
+ if (result.check === 'Readable' && result.repairAction?.type === 'chmod') {
134
+ const apiKeyPath = `${getEverstateDir()}/api-key`;
135
+ try {
136
+ fs.chmodSync(apiKeyPath, 0o600);
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+ // Other repairs require user input (new API key)
143
+ return false;
144
+ },
145
+ };
146
+
147
+ async function checkAuthentication(apiKey: string): Promise<ValidationResult> {
148
+ return new Promise((resolve) => {
149
+ const url = new URL(`${EVERSTATE_API_URL}/api/projects`);
150
+
151
+ const req = https.request(
152
+ {
153
+ hostname: url.hostname,
154
+ port: 443,
155
+ path: url.pathname,
156
+ method: 'GET',
157
+ headers: {
158
+ Authorization: `Bearer ${apiKey}`,
159
+ 'Content-Type': 'application/json',
160
+ },
161
+ timeout: 10000,
162
+ },
163
+ (res) => {
164
+ if (res.statusCode === 200) {
165
+ resolve({
166
+ component: 'API Key',
167
+ check: 'Authentication',
168
+ status: 'pass',
169
+ message: 'Authenticated successfully',
170
+ });
171
+ } else if (res.statusCode === 401 || res.statusCode === 403) {
172
+ resolve({
173
+ component: 'API Key',
174
+ check: 'Authentication',
175
+ status: 'fail',
176
+ message: 'Authentication failed - invalid or expired key',
177
+ details: { statusCode: res.statusCode },
178
+ repairAction: {
179
+ type: 'configure',
180
+ description: 'Replace with valid API key',
181
+ automatic: false,
182
+ },
183
+ });
184
+ } else {
185
+ resolve({
186
+ component: 'API Key',
187
+ check: 'Authentication',
188
+ status: 'warn',
189
+ message: `Unexpected response: ${res.statusCode}`,
190
+ details: { statusCode: res.statusCode },
191
+ });
192
+ }
193
+ }
194
+ );
195
+
196
+ req.on('error', (err) => {
197
+ resolve({
198
+ component: 'API Key',
199
+ check: 'Authentication',
200
+ status: 'warn',
201
+ message: `Network error: ${err.message}`,
202
+ details: { error: err.message },
203
+ });
204
+ });
205
+
206
+ req.on('timeout', () => {
207
+ req.destroy();
208
+ resolve({
209
+ component: 'API Key',
210
+ check: 'Authentication',
211
+ status: 'warn',
212
+ message: 'Request timed out',
213
+ });
214
+ });
215
+
216
+ req.end();
217
+ });
218
+ }