@claudetools/tools 0.1.0

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.
@@ -0,0 +1,174 @@
1
+ // =============================================================================
2
+ // Configuration and Project ID Resolution
3
+ // =============================================================================
4
+ import { mcpLogger } from '../logger.js';
5
+ import * as fs from 'fs';
6
+ import { getConfig, getConfigDir, getProjectsPath, } from './config-manager.js';
7
+ import { getOrRegisterProject } from './project-registration.js';
8
+ // Configuration - now loaded from config manager
9
+ const config = getConfig();
10
+ export const API_BASE_URL = config.apiUrl;
11
+ export const CLAUDETOOLS_DIR = getConfigDir();
12
+ export const PROJECTS_FILE = getProjectsPath();
13
+ // Cache for resolved project ID
14
+ let resolvedProjectId = null;
15
+ /**
16
+ * Validates that a project ID is a proper UUID format
17
+ */
18
+ export function isValidProjectId(id) {
19
+ return /^proj_[a-f0-9]{20}$/.test(id);
20
+ }
21
+ /**
22
+ * Finds a project binding in the cache by directory path
23
+ */
24
+ export function findProjectBinding(dirPath) {
25
+ try {
26
+ if (!fs.existsSync(PROJECTS_FILE)) {
27
+ return null;
28
+ }
29
+ const content = fs.readFileSync(PROJECTS_FILE, 'utf-8');
30
+ const cache = JSON.parse(content);
31
+ if (!cache?.bindings?.length) {
32
+ return null;
33
+ }
34
+ // Try exact match first
35
+ const exactMatch = cache.bindings.find(b => b.local_path === dirPath);
36
+ if (exactMatch) {
37
+ mcpLogger.debug('MEMORY', `UUID lookup exact match: ${exactMatch.project_id} for ${dirPath}`);
38
+ return exactMatch;
39
+ }
40
+ // Try prefix match (for subdirectories)
41
+ const prefixMatches = cache.bindings
42
+ .filter(b => dirPath.startsWith(b.local_path + '/') || dirPath === b.local_path)
43
+ .sort((a, b) => b.local_path.length - a.local_path.length);
44
+ if (prefixMatches.length > 0) {
45
+ mcpLogger.debug('MEMORY', `UUID lookup prefix match: ${prefixMatches[0].project_id} for ${dirPath}`);
46
+ return prefixMatches[0];
47
+ }
48
+ return null;
49
+ }
50
+ catch (error) {
51
+ mcpLogger.debug('MEMORY', `Error reading projects cache: ${error}`);
52
+ return null;
53
+ }
54
+ }
55
+ /**
56
+ * Look up project ID from local cache based on current working directory
57
+ * Synchronous version - throws if not already resolved
58
+ */
59
+ export function resolveProjectId() {
60
+ // Return cached result if available
61
+ if (resolvedProjectId) {
62
+ return resolvedProjectId;
63
+ }
64
+ throw new Error('Project ID not yet resolved. Server must call resolveProjectIdAsync() during startup.');
65
+ }
66
+ /**
67
+ * Async version that auto-registers if needed
68
+ * Should be called during server startup
69
+ */
70
+ export async function resolveProjectIdAsync() {
71
+ // Return cached result if available
72
+ if (resolvedProjectId) {
73
+ return resolvedProjectId;
74
+ }
75
+ // Check environment variable first
76
+ if (process.env.CLAUDETOOLS_PROJECT_ID) {
77
+ const envProjectId = process.env.CLAUDETOOLS_PROJECT_ID;
78
+ // Validate UUID format
79
+ if (!isValidProjectId(envProjectId)) {
80
+ throw new Error(`Invalid project ID format in CLAUDETOOLS_PROJECT_ID: ${envProjectId}. ` +
81
+ `Expected format: proj_xxxxxxxxxxxxxxxxxxxx (20 hex chars)`);
82
+ }
83
+ resolvedProjectId = envProjectId;
84
+ mcpLogger.debug('MEMORY', `Using project ID from env: ${resolvedProjectId}`);
85
+ return resolvedProjectId;
86
+ }
87
+ const cwd = process.cwd();
88
+ // Check projects.json cache
89
+ const binding = findProjectBinding(cwd);
90
+ if (binding) {
91
+ // Validate UUID format
92
+ if (!isValidProjectId(binding.project_id)) {
93
+ throw new Error(`Invalid project ID format in cache: ${binding.project_id}. ` +
94
+ `Cache may be corrupted. Delete ${PROJECTS_FILE} and restart.`);
95
+ }
96
+ resolvedProjectId = binding.project_id;
97
+ mcpLogger.info('MEMORY', `Project resolved from cache: ${resolvedProjectId}`);
98
+ return resolvedProjectId;
99
+ }
100
+ // No binding found - auto-register if enabled
101
+ if (config.autoRegister) {
102
+ mcpLogger.info('MEMORY', `No project binding found for ${cwd}, auto-registering...`);
103
+ try {
104
+ resolvedProjectId = await getOrRegisterProject(cwd);
105
+ mcpLogger.info('MEMORY', `Project auto-registered: ${resolvedProjectId}`);
106
+ return resolvedProjectId;
107
+ }
108
+ catch (error) {
109
+ mcpLogger.error('MEMORY', `Auto-registration failed: ${error}`);
110
+ throw new Error(`Failed to auto-register project for ${cwd}: ${error}\n\n` +
111
+ 'Please check:\n' +
112
+ '1. CLAUDETOOLS_API_KEY or MEMORY_API_KEY is set\n' +
113
+ '2. API URL is correct in ~/.claudetools/config.json\n' +
114
+ '3. Network connectivity to ClaudeTools API');
115
+ }
116
+ }
117
+ // Auto-register disabled - throw error
118
+ throw new Error(`No project binding found for ${cwd} and auto-registration is disabled.\n` +
119
+ `To register this project:\n` +
120
+ ` 1. Set CLAUDETOOLS_PROJECT_ID environment variable with a valid proj_* UUID\n` +
121
+ ` 2. Enable autoRegister in ~/.claudetools/config.json\n` +
122
+ ` 3. Or manually register via API`);
123
+ }
124
+ // Lazy-loaded DEFAULT_PROJECT_ID to avoid startup errors
125
+ let _defaultProjectId = null;
126
+ /**
127
+ * Gets the default project ID (synchronous version)
128
+ * Throws if not already resolved - call resolveProjectIdAsync() first
129
+ */
130
+ export function getDefaultProjectId() {
131
+ if (_defaultProjectId) {
132
+ return _defaultProjectId;
133
+ }
134
+ // Try config first
135
+ if (config.defaultProjectId) {
136
+ if (!isValidProjectId(config.defaultProjectId)) {
137
+ throw new Error(`Invalid defaultProjectId in config: ${config.defaultProjectId}. ` +
138
+ `Expected format: proj_xxxxxxxxxxxxxxxxxxxx (20 hex chars)`);
139
+ }
140
+ _defaultProjectId = config.defaultProjectId;
141
+ return _defaultProjectId;
142
+ }
143
+ // Fall back to resolveProjectId (throws if not resolved)
144
+ _defaultProjectId = resolveProjectId();
145
+ return _defaultProjectId;
146
+ }
147
+ /**
148
+ * Async version for server startup
149
+ */
150
+ export async function getDefaultProjectIdAsync() {
151
+ if (_defaultProjectId) {
152
+ return _defaultProjectId;
153
+ }
154
+ // Try config first
155
+ if (config.defaultProjectId) {
156
+ if (!isValidProjectId(config.defaultProjectId)) {
157
+ throw new Error(`Invalid defaultProjectId in config: ${config.defaultProjectId}. ` +
158
+ `Expected format: proj_xxxxxxxxxxxxxxxxxxxx (20 hex chars)`);
159
+ }
160
+ _defaultProjectId = config.defaultProjectId;
161
+ return _defaultProjectId;
162
+ }
163
+ // Fall back to async resolve (may auto-register)
164
+ _defaultProjectId = await resolveProjectIdAsync();
165
+ return _defaultProjectId;
166
+ }
167
+ // Keep backward compatibility - but this will throw on access if no project bound
168
+ export const DEFAULT_USER_ID = config.defaultUserId || 'default-user';
169
+ export const AUTO_INJECT_CONTEXT = config.autoInjectContext;
170
+ // Store last context for explain_memory tool
171
+ export let lastContextUsed = null;
172
+ export function setLastContextUsed(context) {
173
+ lastContextUsed = context;
174
+ }
@@ -0,0 +1,30 @@
1
+ export interface ImpactResult {
2
+ function: string;
3
+ analysisType: string;
4
+ directCallers: {
5
+ function: string;
6
+ risk: string;
7
+ }[];
8
+ indirectCallers: {
9
+ function: string;
10
+ depth: number;
11
+ path: string[];
12
+ }[];
13
+ totalAffected: number;
14
+ riskLevel: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
15
+ recommendations: string[];
16
+ }
17
+ export declare function queryDependencies(projectId: string, functionName: string, direction: 'forward' | 'reverse' | 'both', depth?: number): Promise<{
18
+ function: string;
19
+ forward: {
20
+ function: string;
21
+ context: string;
22
+ fileName?: string;
23
+ }[];
24
+ reverse: {
25
+ function: string;
26
+ context: string;
27
+ fileName?: string;
28
+ }[];
29
+ }>;
30
+ export declare function analyzeImpact(projectId: string, functionName: string, analysisType?: 'change' | 'delete' | 'signature', maxDepth?: number): Promise<ImpactResult>;
@@ -0,0 +1,87 @@
1
+ // =============================================================================
2
+ // Dependency Analysis and Impact Assessment
3
+ // =============================================================================
4
+ import { searchMemory } from './api-client.js';
5
+ export async function queryDependencies(projectId, functionName, direction, depth = 1) {
6
+ const searchQuery = `${functionName} CALLS`;
7
+ const context = await searchMemory(projectId, searchQuery, 100);
8
+ const forward = [];
9
+ const reverse = [];
10
+ for (const fact of context.relevant_facts) {
11
+ if (fact.relation_type === 'CALLS') {
12
+ const caller = fact.source_name || '';
13
+ const called = fact.target_name || '';
14
+ if (caller === functionName && (direction === 'forward' || direction === 'both')) {
15
+ forward.push({ function: called, context: fact.fact });
16
+ }
17
+ if (called === functionName && (direction === 'reverse' || direction === 'both')) {
18
+ reverse.push({ function: caller, context: fact.fact });
19
+ }
20
+ }
21
+ }
22
+ return { function: functionName, forward, reverse };
23
+ }
24
+ export async function analyzeImpact(projectId, functionName, analysisType = 'change', maxDepth = 3) {
25
+ const visited = new Set();
26
+ const directCallers = [];
27
+ const indirectCallers = [];
28
+ const directResult = await queryDependencies(projectId, functionName, 'reverse', 1);
29
+ for (const caller of directResult.reverse) {
30
+ visited.add(caller.function);
31
+ let risk = 'MEDIUM';
32
+ if (caller.function.includes('main') || caller.function.includes('init') || caller.function.includes('core')) {
33
+ risk = 'HIGH';
34
+ }
35
+ else if (caller.function.includes('test') || caller.function.includes('mock')) {
36
+ risk = 'LOW';
37
+ }
38
+ directCallers.push({ function: caller.function, risk });
39
+ }
40
+ if (maxDepth > 1) {
41
+ const queue = directCallers.map(c => ({ func: c.function, depth: 1, path: [functionName, c.function] }));
42
+ while (queue.length > 0) {
43
+ const current = queue.shift();
44
+ if (current.depth >= maxDepth)
45
+ continue;
46
+ const indirectResult = await queryDependencies(projectId, current.func, 'reverse', 1);
47
+ for (const caller of indirectResult.reverse) {
48
+ if (!visited.has(caller.function)) {
49
+ visited.add(caller.function);
50
+ const newPath = [...current.path, caller.function];
51
+ indirectCallers.push({
52
+ function: caller.function,
53
+ depth: current.depth + 1,
54
+ path: newPath,
55
+ });
56
+ queue.push({ func: caller.function, depth: current.depth + 1, path: newPath });
57
+ }
58
+ }
59
+ }
60
+ }
61
+ const totalAffected = directCallers.length + indirectCallers.length;
62
+ let riskLevel = 'LOW';
63
+ if (totalAffected > 20 || directCallers.some(c => c.risk === 'HIGH'))
64
+ riskLevel = 'CRITICAL';
65
+ else if (totalAffected > 10)
66
+ riskLevel = 'HIGH';
67
+ else if (totalAffected > 5)
68
+ riskLevel = 'MEDIUM';
69
+ const recommendations = [];
70
+ if (riskLevel === 'CRITICAL') {
71
+ recommendations.push('Create comprehensive test coverage before making changes');
72
+ recommendations.push('Consider deprecation strategy instead of immediate changes');
73
+ }
74
+ if (analysisType === 'signature') {
75
+ recommendations.push('Update all call sites simultaneously');
76
+ recommendations.push('Use TypeScript compiler to catch signature mismatches');
77
+ }
78
+ return {
79
+ function: functionName,
80
+ analysisType,
81
+ directCallers,
82
+ indirectCallers,
83
+ totalAffected,
84
+ riskLevel,
85
+ recommendations,
86
+ };
87
+ }
@@ -0,0 +1,2 @@
1
+ import type { MemoryContext } from './api-client.js';
2
+ export declare function formatContextForClaude(context: MemoryContext): string;
@@ -0,0 +1,24 @@
1
+ // =============================================================================
2
+ // Context Formatting for Claude
3
+ // =============================================================================
4
+ export function formatContextForClaude(context) {
5
+ if (!context.relevant_facts.length && !context.relevant_entities.length) {
6
+ return '';
7
+ }
8
+ const parts = [];
9
+ if (context.relevant_entities.length > 0) {
10
+ parts.push('# Known Entities\n');
11
+ for (const entity of context.relevant_entities) {
12
+ const labels = entity.labels.join(', ');
13
+ parts.push(`- ${entity.name} (${labels})${entity.summary ? ': ' + entity.summary : ''}`);
14
+ }
15
+ parts.push('');
16
+ }
17
+ if (context.relevant_facts.length > 0) {
18
+ parts.push('# Relevant Facts\n');
19
+ for (const fact of context.relevant_facts) {
20
+ parts.push(`- ${fact.fact}`);
21
+ }
22
+ }
23
+ return parts.join('\n');
24
+ }
@@ -0,0 +1,15 @@
1
+ export interface PatternWarning {
2
+ type: 'security' | 'performance';
3
+ severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
4
+ pattern: string;
5
+ description: string;
6
+ line?: number;
7
+ recommendation: string;
8
+ }
9
+ export interface PatternResult {
10
+ warnings: PatternWarning[];
11
+ securityScore: number;
12
+ performanceScore: number;
13
+ overallRisk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
14
+ }
15
+ export declare function checkPatterns(code: string, checkTypes?: ('security' | 'performance' | 'all')[]): PatternResult;
@@ -0,0 +1,118 @@
1
+ // =============================================================================
2
+ // Pattern Detection (Security and Performance)
3
+ // =============================================================================
4
+ // Security patterns to detect
5
+ const SECURITY_PATTERNS = [
6
+ {
7
+ pattern: /\beval\s*\(/i,
8
+ name: 'eval() usage',
9
+ description: 'eval() can execute arbitrary code and is a security risk',
10
+ severity: 'CRITICAL',
11
+ recommendation: 'Avoid eval(). Use JSON.parse() for JSON, or Function constructor for dynamic code',
12
+ },
13
+ {
14
+ pattern: /innerHTML\s*=/i,
15
+ name: 'innerHTML assignment',
16
+ description: 'Direct innerHTML assignment can lead to XSS vulnerabilities',
17
+ severity: 'HIGH',
18
+ recommendation: 'Use textContent for text, or sanitize HTML with DOMPurify',
19
+ },
20
+ {
21
+ pattern: /document\.write\s*\(/i,
22
+ name: 'document.write()',
23
+ description: 'document.write() can overwrite the entire document and enable XSS',
24
+ severity: 'HIGH',
25
+ recommendation: 'Use DOM methods like createElement() and appendChild()',
26
+ },
27
+ {
28
+ pattern: /\bexec\s*\(/i,
29
+ name: 'exec() or shell command execution',
30
+ description: 'Executing shell commands can lead to command injection vulnerabilities',
31
+ severity: 'CRITICAL',
32
+ recommendation: 'Use safer alternatives, validate and sanitize all inputs',
33
+ },
34
+ {
35
+ pattern: /process\.env\[/i,
36
+ name: 'Dynamic environment variable access',
37
+ description: 'Dynamic access to environment variables can expose sensitive data',
38
+ severity: 'MEDIUM',
39
+ recommendation: 'Use static environment variable names, validate values',
40
+ },
41
+ ];
42
+ const PERFORMANCE_PATTERNS = [
43
+ {
44
+ pattern: /for\s*\([^;]*;[^;]*;[^)]*\)\s*{[^}]*for\s*\(/i,
45
+ name: 'Nested loops',
46
+ description: 'Nested loops can cause O(n²) or worse performance',
47
+ severity: 'MEDIUM',
48
+ recommendation: 'Consider using maps, sets, or other data structures to reduce complexity',
49
+ },
50
+ {
51
+ pattern: /\.push\(.*\)/g,
52
+ name: 'Array.push in loop',
53
+ description: 'Repeatedly pushing to arrays can cause memory reallocation',
54
+ severity: 'LOW',
55
+ recommendation: 'Pre-allocate arrays when size is known',
56
+ },
57
+ ];
58
+ export function checkPatterns(code, checkTypes = ['all']) {
59
+ const warnings = [];
60
+ const lines = code.split('\n');
61
+ const shouldCheckSecurity = checkTypes.includes('all') || checkTypes.includes('security');
62
+ const shouldCheckPerformance = checkTypes.includes('all') || checkTypes.includes('performance');
63
+ if (shouldCheckSecurity) {
64
+ for (const pattern of SECURITY_PATTERNS) {
65
+ lines.forEach((line, index) => {
66
+ if (pattern.pattern.test(line)) {
67
+ warnings.push({
68
+ type: 'security',
69
+ severity: pattern.severity,
70
+ pattern: pattern.name,
71
+ description: pattern.description,
72
+ line: index + 1,
73
+ recommendation: pattern.recommendation,
74
+ });
75
+ }
76
+ });
77
+ }
78
+ }
79
+ if (shouldCheckPerformance) {
80
+ for (const pattern of PERFORMANCE_PATTERNS) {
81
+ lines.forEach((line, index) => {
82
+ if (pattern.pattern.test(line)) {
83
+ warnings.push({
84
+ type: 'performance',
85
+ severity: pattern.severity,
86
+ pattern: pattern.name,
87
+ description: pattern.description,
88
+ line: index + 1,
89
+ recommendation: pattern.recommendation,
90
+ });
91
+ }
92
+ });
93
+ }
94
+ }
95
+ const securityWarnings = warnings.filter(w => w.type === 'security');
96
+ const performanceWarnings = warnings.filter(w => w.type === 'performance');
97
+ const securityScore = Math.max(0, 100 - (securityWarnings.length * 20));
98
+ const performanceScore = Math.max(0, 100 - (performanceWarnings.length * 10));
99
+ let overallRisk = 'LOW';
100
+ if (warnings.some(w => w.severity === 'CRITICAL')) {
101
+ overallRisk = 'CRITICAL';
102
+ }
103
+ else if (warnings.some(w => w.severity === 'HIGH')) {
104
+ overallRisk = 'HIGH';
105
+ }
106
+ else if (warnings.length > 5) {
107
+ overallRisk = 'HIGH';
108
+ }
109
+ else if (warnings.length > 2) {
110
+ overallRisk = 'MEDIUM';
111
+ }
112
+ return {
113
+ warnings,
114
+ securityScore,
115
+ performanceScore,
116
+ overallRisk,
117
+ };
118
+ }
@@ -0,0 +1,27 @@
1
+ export interface ProjectBinding {
2
+ binding_id: string;
3
+ project_id: string;
4
+ system_id: string;
5
+ local_path: string;
6
+ git_remote?: string;
7
+ project_name: string;
8
+ org_id: string;
9
+ cached_at: string;
10
+ }
11
+ export interface ProjectsCache {
12
+ bindings: ProjectBinding[];
13
+ last_sync: string;
14
+ system_id?: string;
15
+ }
16
+ /**
17
+ * Get or register project for a local path
18
+ * This is the main function called by config.ts
19
+ *
20
+ * Returns the project_id (UUID) for the given path
21
+ */
22
+ export declare function getOrRegisterProject(localPath: string): Promise<string>;
23
+ /**
24
+ * Sync projects from API (for future use)
25
+ * This would fetch all bindings from the API and update local cache
26
+ */
27
+ export declare function syncProjectsFromAPI(): Promise<void>;