@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.
- package/README.md +42 -6
- package/dist/commands/api-client.d.ts +35 -0
- package/dist/commands/api-client.d.ts.map +1 -0
- package/dist/commands/api-client.js +138 -0
- package/dist/commands/api-client.js.map +1 -0
- package/dist/commands/done.d.ts +7 -0
- package/dist/commands/done.d.ts.map +1 -0
- package/dist/commands/done.js +57 -0
- package/dist/commands/done.js.map +1 -0
- package/dist/commands/recall.d.ts +7 -0
- package/dist/commands/recall.d.ts.map +1 -0
- package/dist/commands/recall.js +36 -0
- package/dist/commands/recall.js.map +1 -0
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +33 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.d.ts +7 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +34 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +68 -5
- package/dist/index.js.map +1 -1
- package/dist/setup/auto-update.d.ts +20 -0
- package/dist/setup/auto-update.d.ts.map +1 -0
- package/dist/setup/auto-update.js +295 -0
- package/dist/setup/auto-update.js.map +1 -0
- package/dist/setup/commands/doctor.d.ts +15 -0
- package/dist/setup/commands/doctor.d.ts.map +1 -0
- package/dist/setup/commands/doctor.js +264 -0
- package/dist/setup/commands/doctor.js.map +1 -0
- package/dist/setup/commands/repair.d.ts +14 -0
- package/dist/setup/commands/repair.d.ts.map +1 -0
- package/dist/setup/commands/repair.js +252 -0
- package/dist/setup/commands/repair.js.map +1 -0
- package/dist/setup/environments.d.ts +48 -0
- package/dist/setup/environments.d.ts.map +1 -0
- package/dist/setup/environments.js +222 -0
- package/dist/setup/environments.js.map +1 -0
- package/dist/setup/hooks/templates.d.ts +30 -0
- package/dist/setup/hooks/templates.d.ts.map +1 -0
- package/dist/setup/hooks/templates.js +253 -0
- package/dist/setup/hooks/templates.js.map +1 -0
- package/dist/setup/types.d.ts +122 -0
- package/dist/setup/types.d.ts.map +1 -0
- package/dist/setup/types.js +66 -0
- package/dist/setup/types.js.map +1 -0
- package/dist/setup/validators/api-key.d.ts +8 -0
- package/dist/setup/validators/api-key.d.ts.map +1 -0
- package/dist/setup/validators/api-key.js +233 -0
- package/dist/setup/validators/api-key.js.map +1 -0
- package/dist/setup/validators/connectivity.d.ts +8 -0
- package/dist/setup/validators/connectivity.d.ts.map +1 -0
- package/dist/setup/validators/connectivity.js +150 -0
- package/dist/setup/validators/connectivity.js.map +1 -0
- package/dist/setup/validators/hooks.d.ts +8 -0
- package/dist/setup/validators/hooks.d.ts.map +1 -0
- package/dist/setup/validators/hooks.js +431 -0
- package/dist/setup/validators/hooks.js.map +1 -0
- package/dist/setup/validators/index.d.ts +18 -0
- package/dist/setup/validators/index.d.ts.map +1 -0
- package/dist/setup/validators/index.js +123 -0
- package/dist/setup/validators/index.js.map +1 -0
- package/dist/setup/validators/mcp-config.d.ts +9 -0
- package/dist/setup/validators/mcp-config.d.ts.map +1 -0
- package/dist/setup/validators/mcp-config.js +302 -0
- package/dist/setup/validators/mcp-config.js.map +1 -0
- package/dist/setup/validators/project.d.ts +8 -0
- package/dist/setup/validators/project.d.ts.map +1 -0
- package/dist/setup/validators/project.js +202 -0
- package/dist/setup/validators/project.js.map +1 -0
- package/dist/setup/version.d.ts +58 -0
- package/dist/setup/version.d.ts.map +1 -0
- package/dist/setup/version.js +262 -0
- package/dist/setup/version.js.map +1 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +397 -214
- package/dist/setup.js.map +1 -1
- package/package.json +1 -1
- package/src/commands/api-client.ts +117 -0
- package/src/commands/done.ts +63 -0
- package/src/commands/recall.ts +40 -0
- package/src/commands/status.ts +36 -0
- package/src/commands/sync.ts +37 -0
- package/src/index.ts +64 -5
- package/src/setup/auto-update.ts +328 -0
- package/src/setup/commands/doctor.ts +266 -0
- package/src/setup/commands/repair.ts +260 -0
- package/src/setup/environments.ts +225 -0
- package/src/setup/hooks/templates.ts +255 -0
- package/src/setup/types.ts +207 -0
- package/src/setup/validators/api-key.ts +218 -0
- package/src/setup/validators/connectivity.ts +176 -0
- package/src/setup/validators/hooks.ts +447 -0
- package/src/setup/validators/index.ts +137 -0
- package/src/setup/validators/mcp-config.ts +288 -0
- package/src/setup/validators/project.ts +179 -0
- package/src/setup/version.ts +267 -0
- 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
|
+
}
|