claude-self-reflect 3.3.0 → 4.0.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.
- package/.claude/agents/claude-self-reflect-test.md +525 -11
- package/.claude/agents/quality-fixer.md +314 -0
- package/.claude/agents/reflection-specialist.md +40 -1
- package/installer/cli.js +16 -0
- package/installer/postinstall.js +14 -0
- package/installer/statusline-setup.js +289 -0
- package/mcp-server/run-mcp.sh +45 -7
- package/mcp-server/src/code_reload_tool.py +271 -0
- package/mcp-server/src/embedding_manager.py +60 -26
- package/mcp-server/src/enhanced_tool_registry.py +407 -0
- package/mcp-server/src/mode_switch_tool.py +181 -0
- package/mcp-server/src/parallel_search.py +24 -85
- package/mcp-server/src/project_resolver.py +20 -2
- package/mcp-server/src/reflection_tools.py +60 -13
- package/mcp-server/src/rich_formatting.py +103 -0
- package/mcp-server/src/search_tools.py +180 -79
- package/mcp-server/src/security_patches.py +555 -0
- package/mcp-server/src/server.py +318 -240
- package/mcp-server/src/status.py +13 -8
- package/mcp-server/src/temporal_tools.py +10 -3
- package/mcp-server/src/test_quality.py +153 -0
- package/package.json +6 -1
- package/scripts/ast_grep_final_analyzer.py +328 -0
- package/scripts/ast_grep_unified_registry.py +710 -0
- package/scripts/csr-status +511 -0
- package/scripts/import-conversations-unified.py +114 -28
- package/scripts/session_quality_tracker.py +661 -0
- package/scripts/streaming-watcher.py +140 -5
- package/scripts/update_patterns.py +334 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Claude Code Statusline Integration Setup
|
|
4
|
+
* Automatically configures CC statusline to show CSR metrics
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
class StatuslineSetup {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.homeDir = os.homedir();
|
|
19
|
+
this.claudeDir = path.join(this.homeDir, '.claude');
|
|
20
|
+
this.csrScript = path.join(path.dirname(__dirname), 'scripts', 'csr-status');
|
|
21
|
+
this.globalBin = '/usr/local/bin/csr-status';
|
|
22
|
+
this.statuslineWrapper = path.join(this.claudeDir, 'statusline-wrapper.sh');
|
|
23
|
+
this.statuslineBackup = path.join(this.claudeDir, 'statusline-wrapper.sh.backup');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
log(message, type = 'info') {
|
|
27
|
+
const colors = {
|
|
28
|
+
info: '\x1b[36m',
|
|
29
|
+
success: '\x1b[32m',
|
|
30
|
+
warning: '\x1b[33m',
|
|
31
|
+
error: '\x1b[31m'
|
|
32
|
+
};
|
|
33
|
+
console.log(`${colors[type]}${message}\x1b[0m`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
checkPrerequisites() {
|
|
37
|
+
// Check if Claude Code directory exists
|
|
38
|
+
if (!fs.existsSync(this.claudeDir)) {
|
|
39
|
+
this.log('Claude Code directory not found. Please ensure Claude Code is installed.', 'warning');
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check if csr-status script exists
|
|
44
|
+
if (!fs.existsSync(this.csrScript)) {
|
|
45
|
+
this.log('CSR status script not found. Please ensure the package is installed correctly.', 'error');
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
installGlobalCommand() {
|
|
53
|
+
try {
|
|
54
|
+
// Check if we need sudo
|
|
55
|
+
const needsSudo = !this.canWriteTo('/usr/local/bin');
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(this.globalBin)) {
|
|
58
|
+
// Check if it's already pointing to our script
|
|
59
|
+
try {
|
|
60
|
+
const target = fs.readlinkSync(this.globalBin);
|
|
61
|
+
if (target === this.csrScript) {
|
|
62
|
+
this.log('Global csr-status command already installed', 'success');
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
} catch (e) {
|
|
66
|
+
// Not a symlink or can't read, will replace
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create symlink
|
|
71
|
+
const cmd = needsSudo
|
|
72
|
+
? `sudo ln -sf "${this.csrScript}" "${this.globalBin}"`
|
|
73
|
+
: `ln -sf "${this.csrScript}" "${this.globalBin}"`;
|
|
74
|
+
|
|
75
|
+
if (needsSudo) {
|
|
76
|
+
this.log('Installing global csr-status command (may require password)...', 'info');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
80
|
+
|
|
81
|
+
// Make executable
|
|
82
|
+
const chmodCmd = needsSudo
|
|
83
|
+
? `sudo chmod +x "${this.globalBin}"`
|
|
84
|
+
: `chmod +x "${this.globalBin}"`;
|
|
85
|
+
execSync(chmodCmd);
|
|
86
|
+
|
|
87
|
+
this.log('Global csr-status command installed successfully', 'success');
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.log(`Failed to install global command: ${error.message}`, 'warning');
|
|
91
|
+
this.log('You can manually install by running:', 'info');
|
|
92
|
+
this.log(` sudo ln -sf "${this.csrScript}" "${this.globalBin}"`, 'info');
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
canWriteTo(dir) {
|
|
98
|
+
try {
|
|
99
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
patchStatuslineWrapper() {
|
|
107
|
+
if (!fs.existsSync(this.statuslineWrapper)) {
|
|
108
|
+
this.log('Claude Code statusline wrapper not found. Statusline integration skipped.', 'warning');
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Read current wrapper
|
|
114
|
+
let content = fs.readFileSync(this.statuslineWrapper, 'utf8');
|
|
115
|
+
|
|
116
|
+
// Check if already patched with new approach
|
|
117
|
+
if (content.includes('CSR compact status instead of MCP bar')) {
|
|
118
|
+
this.log('Statusline wrapper already patched', 'success');
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Create backup
|
|
123
|
+
if (!fs.existsSync(this.statuslineBackup)) {
|
|
124
|
+
fs.copyFileSync(this.statuslineWrapper, this.statuslineBackup);
|
|
125
|
+
this.log(`Backup created: ${this.statuslineBackup}`, 'info');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Find and replace the MCP bar generation section
|
|
129
|
+
const barPattern = /# Create mini progress bar[\s\S]*?MCP_COLOR="\\033\[1;90m" # Gray/;
|
|
130
|
+
|
|
131
|
+
if (content.match(barPattern)) {
|
|
132
|
+
// Replace the entire bar generation section
|
|
133
|
+
const replacement = `# Use CSR compact status instead of MCP bar
|
|
134
|
+
# This shows both import percentage and code quality in format: [100% <time>][🟢:A+]
|
|
135
|
+
CSR_COMPACT=$(csr-status --compact 2>/dev/null || echo "")
|
|
136
|
+
|
|
137
|
+
if [[ -n "$CSR_COMPACT" ]]; then
|
|
138
|
+
MCP_STATUS="$CSR_COMPACT"
|
|
139
|
+
|
|
140
|
+
# Color based on content
|
|
141
|
+
if [[ "$CSR_COMPACT" == *"100%"* ]]; then
|
|
142
|
+
MCP_COLOR="\\033[1;32m" # Green for complete
|
|
143
|
+
elif [[ "$CSR_COMPACT" == *"[🟢:"* ]]; then
|
|
144
|
+
MCP_COLOR="\\033[1;32m" # Green for good quality
|
|
145
|
+
elif [[ "$CSR_COMPACT" == *"[🟡:"* ]]; then
|
|
146
|
+
MCP_COLOR="\\033[1;33m" # Yellow for medium quality
|
|
147
|
+
elif [[ "$CSR_COMPACT" == *"[🔴:"* ]]; then
|
|
148
|
+
MCP_COLOR="\\033[1;31m" # Red for poor quality
|
|
149
|
+
else
|
|
150
|
+
MCP_COLOR="\\033[1;36m" # Cyan default
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
MCP_STATUS=""
|
|
154
|
+
MCP_COLOR="\\033[1;90m" # Gray`;
|
|
155
|
+
|
|
156
|
+
content = content.replace(barPattern, replacement);
|
|
157
|
+
|
|
158
|
+
// Write back
|
|
159
|
+
fs.writeFileSync(this.statuslineWrapper, content);
|
|
160
|
+
this.log('Statusline wrapper patched successfully (replaced MCP bar)', 'success');
|
|
161
|
+
return true;
|
|
162
|
+
} else {
|
|
163
|
+
// Fallback: Look for the PERCENTAGE check
|
|
164
|
+
const altPattern = /if \[\[ "\$PERCENTAGE" != "null"[\s\S]*?MCP_COLOR="\\033\[1;90m" # Gray/;
|
|
165
|
+
|
|
166
|
+
if (content.match(altPattern)) {
|
|
167
|
+
const replacement = `# Use CSR compact status instead of MCP bar
|
|
168
|
+
# This shows both import percentage and code quality in format: [100% <time>][🟢:A+]
|
|
169
|
+
CSR_COMPACT=$(csr-status --compact 2>/dev/null || echo "")
|
|
170
|
+
|
|
171
|
+
if [[ -n "$CSR_COMPACT" ]]; then
|
|
172
|
+
MCP_STATUS="$CSR_COMPACT"
|
|
173
|
+
|
|
174
|
+
# Color based on content
|
|
175
|
+
if [[ "$CSR_COMPACT" == *"100%"* ]]; then
|
|
176
|
+
MCP_COLOR="\\033[1;32m" # Green for complete
|
|
177
|
+
elif [[ "$CSR_COMPACT" == *"[🟢:"* ]]; then
|
|
178
|
+
MCP_COLOR="\\033[1;32m" # Green for good quality
|
|
179
|
+
elif [[ "$CSR_COMPACT" == *"[🟡:"* ]]; then
|
|
180
|
+
MCP_COLOR="\\033[1;33m" # Yellow for medium quality
|
|
181
|
+
elif [[ "$CSR_COMPACT" == *"[🔴:"* ]]; then
|
|
182
|
+
MCP_COLOR="\\033[1;31m" # Red for poor quality
|
|
183
|
+
else
|
|
184
|
+
MCP_COLOR="\\033[1;36m" # Cyan default
|
|
185
|
+
fi
|
|
186
|
+
else
|
|
187
|
+
MCP_STATUS=""
|
|
188
|
+
MCP_COLOR="\\033[1;90m" # Gray`;
|
|
189
|
+
|
|
190
|
+
content = content.replace(altPattern, replacement);
|
|
191
|
+
|
|
192
|
+
// Write back
|
|
193
|
+
fs.writeFileSync(this.statuslineWrapper, content);
|
|
194
|
+
this.log('Statusline wrapper patched successfully (replaced PERCENTAGE section)', 'success');
|
|
195
|
+
return true;
|
|
196
|
+
} else {
|
|
197
|
+
this.log('Could not find MCP bar section to replace', 'warning');
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
this.log(`Failed to patch statusline wrapper: ${error.message}`, 'error');
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
validateIntegration() {
|
|
208
|
+
try {
|
|
209
|
+
// Test csr-status command
|
|
210
|
+
const output = execSync('csr-status --compact', { encoding: 'utf8' });
|
|
211
|
+
if (output) {
|
|
212
|
+
this.log(`CSR status output: ${output.trim()}`, 'success');
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
this.log('CSR status command not working properly', 'warning');
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async run() {
|
|
223
|
+
this.log('🚀 Setting up Claude Code Statusline Integration...', 'info');
|
|
224
|
+
|
|
225
|
+
if (!this.checkPrerequisites()) {
|
|
226
|
+
this.log('Prerequisites check failed', 'error');
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const steps = [
|
|
231
|
+
{ name: 'Install global command', fn: () => this.installGlobalCommand() },
|
|
232
|
+
{ name: 'Patch statusline wrapper', fn: () => this.patchStatuslineWrapper() },
|
|
233
|
+
{ name: 'Validate integration', fn: () => this.validateIntegration() }
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
let success = true;
|
|
237
|
+
for (const step of steps) {
|
|
238
|
+
this.log(`\n📋 ${step.name}...`, 'info');
|
|
239
|
+
if (!step.fn()) {
|
|
240
|
+
success = false;
|
|
241
|
+
this.log(`❌ ${step.name} failed`, 'error');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (success) {
|
|
246
|
+
this.log('\n✅ Statusline integration completed successfully!', 'success');
|
|
247
|
+
this.log('The CSR status will now appear in your Claude Code statusline.', 'info');
|
|
248
|
+
this.log('Format: [import%][🟢:grade] for compact quality metrics', 'info');
|
|
249
|
+
} else {
|
|
250
|
+
this.log('\n⚠️ Statusline integration completed with warnings', 'warning');
|
|
251
|
+
this.log('Some features may need manual configuration.', 'warning');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return success;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Restore original statusline if needed
|
|
258
|
+
restore() {
|
|
259
|
+
if (fs.existsSync(this.statuslineBackup)) {
|
|
260
|
+
try {
|
|
261
|
+
fs.copyFileSync(this.statuslineBackup, this.statuslineWrapper);
|
|
262
|
+
this.log('Statusline wrapper restored from backup', 'success');
|
|
263
|
+
return true;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.log(`Failed to restore: ${error.message}`, 'error');
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
this.log('No backup found to restore', 'warning');
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Run if called directly
|
|
276
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
277
|
+
const setup = new StatuslineSetup();
|
|
278
|
+
|
|
279
|
+
if (process.argv[2] === '--restore') {
|
|
280
|
+
setup.restore();
|
|
281
|
+
} else {
|
|
282
|
+
setup.run().catch(error => {
|
|
283
|
+
console.error('Setup failed:', error);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export default StatuslineSetup;
|
package/mcp-server/run-mcp.sh
CHANGED
|
@@ -11,8 +11,24 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
|
|
11
11
|
# Navigate to the mcp-server directory
|
|
12
12
|
cd "$SCRIPT_DIR"
|
|
13
13
|
|
|
14
|
-
# CRITICAL:
|
|
15
|
-
#
|
|
14
|
+
# CRITICAL: Environment variables priority:
|
|
15
|
+
# 1. Command-line args from Claude Code (already in environment)
|
|
16
|
+
# 2. .env file (only for missing values)
|
|
17
|
+
# 3. Defaults (as fallback)
|
|
18
|
+
|
|
19
|
+
# Store any command-line provided values BEFORE loading .env
|
|
20
|
+
CMDLINE_VOYAGE_KEY="${VOYAGE_KEY:-}"
|
|
21
|
+
CMDLINE_PREFER_LOCAL="${PREFER_LOCAL_EMBEDDINGS:-}"
|
|
22
|
+
CMDLINE_QDRANT_URL="${QDRANT_URL:-}"
|
|
23
|
+
|
|
24
|
+
# CRITICAL: If local mode is explicitly requested, skip VOYAGE_KEY from .env
|
|
25
|
+
if [ "$CMDLINE_PREFER_LOCAL" = "true" ]; then
|
|
26
|
+
echo "[DEBUG] Local mode explicitly requested - will skip VOYAGE_KEY from .env" >&2
|
|
27
|
+
# Save current VOYAGE_KEY state
|
|
28
|
+
SAVED_VOYAGE_KEY="${VOYAGE_KEY:-}"
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Load .env file for any missing values
|
|
16
32
|
if [ -f "../.env" ]; then
|
|
17
33
|
echo "[DEBUG] Loading .env file from project root" >&2
|
|
18
34
|
set -a # Export all variables
|
|
@@ -22,8 +38,31 @@ else
|
|
|
22
38
|
echo "[DEBUG] No .env file found, using defaults" >&2
|
|
23
39
|
fi
|
|
24
40
|
|
|
25
|
-
#
|
|
26
|
-
|
|
41
|
+
# CRITICAL: Handle local mode by clearing VOYAGE_KEY if local was explicitly requested
|
|
42
|
+
if [ "$CMDLINE_PREFER_LOCAL" = "true" ]; then
|
|
43
|
+
unset VOYAGE_KEY
|
|
44
|
+
echo "[DEBUG] Local mode: VOYAGE_KEY cleared to force local embeddings" >&2
|
|
45
|
+
elif [ "${CMDLINE_VOYAGE_KEY+x}" ]; then
|
|
46
|
+
# Restore command-line VOYAGE_KEY if it was explicitly set
|
|
47
|
+
export VOYAGE_KEY="$CMDLINE_VOYAGE_KEY"
|
|
48
|
+
if [ -z "$VOYAGE_KEY" ]; then
|
|
49
|
+
echo "[DEBUG] VOYAGE_KEY explicitly set to empty (forcing local mode)" >&2
|
|
50
|
+
else
|
|
51
|
+
echo "[DEBUG] Using command-line VOYAGE_KEY" >&2
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
if [ ! -z "$CMDLINE_PREFER_LOCAL" ]; then
|
|
56
|
+
export PREFER_LOCAL_EMBEDDINGS="$CMDLINE_PREFER_LOCAL"
|
|
57
|
+
echo "[DEBUG] Using command-line PREFER_LOCAL_EMBEDDINGS: $PREFER_LOCAL_EMBEDDINGS" >&2
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if [ ! -z "$CMDLINE_QDRANT_URL" ]; then
|
|
61
|
+
export QDRANT_URL="$CMDLINE_QDRANT_URL"
|
|
62
|
+
echo "[DEBUG] Using command-line QDRANT_URL: $QDRANT_URL" >&2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Set smart defaults ONLY if still not set
|
|
27
66
|
if [ -z "$QDRANT_URL" ]; then
|
|
28
67
|
export QDRANT_URL="http://localhost:6333"
|
|
29
68
|
echo "[DEBUG] Using default QDRANT_URL: $QDRANT_URL" >&2
|
|
@@ -52,9 +91,8 @@ fi
|
|
|
52
91
|
# CRITICAL FIX: Pass through environment variables from Claude Code
|
|
53
92
|
# These environment variables are set by `claude mcp add -e KEY=value`
|
|
54
93
|
# Export them so the Python process can access them
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
fi
|
|
94
|
+
# BUT: Don't export VOYAGE_KEY if we're in local mode
|
|
95
|
+
# Note: VOYAGE_KEY might have been unset earlier for local mode, so skip this entirely
|
|
58
96
|
|
|
59
97
|
if [ ! -z "$VOYAGE_KEY_2" ]; then
|
|
60
98
|
export VOYAGE_KEY_2="$VOYAGE_KEY_2"
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Runtime code reloading tool for MCP server development."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import importlib
|
|
6
|
+
import logging
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Literal
|
|
9
|
+
from fastmcp import Context
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CodeReloader:
|
|
18
|
+
"""Handles runtime code reloading for the MCP server."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
"""Initialize the code reloader."""
|
|
22
|
+
self.module_hashes: Dict[str, str] = {}
|
|
23
|
+
self.reload_history: List[Dict] = []
|
|
24
|
+
self.cache_dir = Path.home() / '.claude-self-reflect' / 'reload_cache'
|
|
25
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
# Test comment: Hot reload test at 2025-09-15
|
|
27
|
+
logger.info("CodeReloader initialized with hot reload support")
|
|
28
|
+
|
|
29
|
+
def _get_file_hash(self, filepath: Path) -> str:
|
|
30
|
+
"""Get SHA256 hash of a file."""
|
|
31
|
+
with open(filepath, 'rb') as f:
|
|
32
|
+
return hashlib.sha256(f.read()).hexdigest()
|
|
33
|
+
|
|
34
|
+
def _get_changed_modules(self) -> List[str]:
|
|
35
|
+
"""Detect which modules have changed since last check."""
|
|
36
|
+
changed = []
|
|
37
|
+
src_dir = Path(__file__).parent
|
|
38
|
+
|
|
39
|
+
for py_file in src_dir.glob("*.py"):
|
|
40
|
+
if py_file.name == "__pycache__":
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
module_name = f"src.{py_file.stem}"
|
|
44
|
+
current_hash = self._get_file_hash(py_file)
|
|
45
|
+
|
|
46
|
+
if module_name in self.module_hashes:
|
|
47
|
+
if self.module_hashes[module_name] != current_hash:
|
|
48
|
+
changed.append(module_name)
|
|
49
|
+
|
|
50
|
+
self.module_hashes[module_name] = current_hash
|
|
51
|
+
|
|
52
|
+
return changed
|
|
53
|
+
|
|
54
|
+
async def reload_modules(
|
|
55
|
+
self,
|
|
56
|
+
ctx: Context,
|
|
57
|
+
modules: Optional[List[str]] = None,
|
|
58
|
+
auto_detect: bool = True
|
|
59
|
+
) -> str:
|
|
60
|
+
"""Reload Python modules at runtime without restarting the MCP server."""
|
|
61
|
+
|
|
62
|
+
await ctx.debug("Starting code reload process...")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
# Track what we're reloading
|
|
66
|
+
reload_targets = []
|
|
67
|
+
|
|
68
|
+
if auto_detect:
|
|
69
|
+
# Detect changed modules
|
|
70
|
+
changed = self._get_changed_modules()
|
|
71
|
+
if changed:
|
|
72
|
+
reload_targets.extend(changed)
|
|
73
|
+
await ctx.debug(f"Auto-detected changes in: {changed}")
|
|
74
|
+
|
|
75
|
+
if modules:
|
|
76
|
+
# Add explicitly requested modules
|
|
77
|
+
reload_targets.extend(modules)
|
|
78
|
+
|
|
79
|
+
if not reload_targets:
|
|
80
|
+
return "📊 No modules to reload. All code is up to date!"
|
|
81
|
+
|
|
82
|
+
# Perform the reload
|
|
83
|
+
reloaded = []
|
|
84
|
+
failed = []
|
|
85
|
+
|
|
86
|
+
for module_name in reload_targets:
|
|
87
|
+
try:
|
|
88
|
+
# SECURITY FIX: Validate module is in whitelist
|
|
89
|
+
from .security_patches import ModuleWhitelist
|
|
90
|
+
if not ModuleWhitelist.is_allowed_module(module_name):
|
|
91
|
+
logger.warning(f"Module not in whitelist, skipping: {module_name}")
|
|
92
|
+
failed.append((module_name, "Module not in whitelist"))
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
if module_name in sys.modules:
|
|
96
|
+
# Store old module reference for rollback
|
|
97
|
+
old_module = sys.modules[module_name]
|
|
98
|
+
|
|
99
|
+
# Reload the module
|
|
100
|
+
logger.info(f"Reloading module: {module_name}")
|
|
101
|
+
reloaded_module = importlib.reload(sys.modules[module_name])
|
|
102
|
+
|
|
103
|
+
# Update any global references if needed
|
|
104
|
+
self._update_global_references(module_name, reloaded_module)
|
|
105
|
+
|
|
106
|
+
reloaded.append(module_name)
|
|
107
|
+
await ctx.debug(f"✅ Reloaded: {module_name}")
|
|
108
|
+
else:
|
|
109
|
+
# Module not loaded yet, import it
|
|
110
|
+
importlib.import_module(module_name)
|
|
111
|
+
reloaded.append(module_name)
|
|
112
|
+
await ctx.debug(f"✅ Imported: {module_name}")
|
|
113
|
+
|
|
114
|
+
except Exception as e:
|
|
115
|
+
logger.error(f"Failed to reload {module_name}: {e}", exc_info=True)
|
|
116
|
+
failed.append((module_name, str(e)))
|
|
117
|
+
await ctx.debug(f"❌ Failed: {module_name} - {e}")
|
|
118
|
+
|
|
119
|
+
# Record reload history
|
|
120
|
+
self.reload_history.append({
|
|
121
|
+
"timestamp": os.environ.get('MCP_REQUEST_ID', 'unknown'),
|
|
122
|
+
"reloaded": reloaded,
|
|
123
|
+
"failed": failed
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Build response
|
|
127
|
+
response = "🔄 **Code Reload Results**\n\n"
|
|
128
|
+
|
|
129
|
+
if reloaded:
|
|
130
|
+
response += f"**Successfully Reloaded ({len(reloaded)}):**\n"
|
|
131
|
+
for module in reloaded:
|
|
132
|
+
response += f"- ✅ {module}\n"
|
|
133
|
+
response += "\n"
|
|
134
|
+
|
|
135
|
+
if failed:
|
|
136
|
+
response += f"**Failed to Reload ({len(failed)}):**\n"
|
|
137
|
+
for module, error in failed:
|
|
138
|
+
response += f"- ❌ {module}: {error}\n"
|
|
139
|
+
response += "\n"
|
|
140
|
+
|
|
141
|
+
response += "**Important Notes:**\n"
|
|
142
|
+
response += "- Class instances created before reload keep old code\n"
|
|
143
|
+
response += "- New requests will use the reloaded code\n"
|
|
144
|
+
response += "- Some changes may require full restart (e.g., new tools)\n"
|
|
145
|
+
|
|
146
|
+
return response
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Code reload failed: {e}", exc_info=True)
|
|
150
|
+
return f"❌ Code reload failed: {str(e)}"
|
|
151
|
+
|
|
152
|
+
def _update_global_references(self, module_name: str, new_module):
|
|
153
|
+
"""Update global references after module reload."""
|
|
154
|
+
# This is where we'd update any global singleton references
|
|
155
|
+
# For example, if we reload embedding_manager, we might need to
|
|
156
|
+
# update the global embedding manager instance
|
|
157
|
+
|
|
158
|
+
if module_name == "src.embedding_manager":
|
|
159
|
+
# Update the global embedding manager if it exists
|
|
160
|
+
if hasattr(new_module, 'get_embedding_manager'):
|
|
161
|
+
# The singleton pattern should handle this automatically
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
elif module_name == "src.search_tools":
|
|
165
|
+
# Search tools might need to refresh their references
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
# Add more specific updates as needed
|
|
169
|
+
|
|
170
|
+
async def get_reload_status(self, ctx: Context) -> str:
|
|
171
|
+
"""Get the current reload status and history."""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Check for changed files
|
|
175
|
+
changed = self._get_changed_modules()
|
|
176
|
+
|
|
177
|
+
response = "📊 **Code Reload Status**\n\n"
|
|
178
|
+
|
|
179
|
+
response += "**Module Status:**\n"
|
|
180
|
+
if changed:
|
|
181
|
+
response += f"⚠️ {len(changed)} modules have pending changes:\n"
|
|
182
|
+
for module in changed:
|
|
183
|
+
response += f" - {module}\n"
|
|
184
|
+
else:
|
|
185
|
+
response += "✅ All modules are up to date\n"
|
|
186
|
+
|
|
187
|
+
response += f"\n**Tracked Modules:** {len(self.module_hashes)}\n"
|
|
188
|
+
|
|
189
|
+
if self.reload_history:
|
|
190
|
+
response += f"\n**Recent Reloads:**\n"
|
|
191
|
+
for entry in self.reload_history[-5:]: # Last 5 reloads
|
|
192
|
+
response += f"- {entry['timestamp']}: "
|
|
193
|
+
response += f"{len(entry['reloaded'])} success, "
|
|
194
|
+
response += f"{len(entry['failed'])} failed\n"
|
|
195
|
+
|
|
196
|
+
return response
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error(f"Failed to get reload status: {e}", exc_info=True)
|
|
200
|
+
return f"❌ Failed to get reload status: {str(e)}"
|
|
201
|
+
|
|
202
|
+
async def clear_python_cache(self, ctx: Context) -> str:
|
|
203
|
+
"""Clear Python's module cache and bytecode."""
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
await ctx.debug("Clearing Python cache...")
|
|
207
|
+
|
|
208
|
+
# Clear __pycache__ directories
|
|
209
|
+
src_dir = Path(__file__).parent
|
|
210
|
+
pycache_dirs = list(src_dir.rglob("__pycache__"))
|
|
211
|
+
|
|
212
|
+
for pycache in pycache_dirs:
|
|
213
|
+
if pycache.is_dir():
|
|
214
|
+
import shutil
|
|
215
|
+
shutil.rmtree(pycache)
|
|
216
|
+
await ctx.debug(f"Removed: {pycache}")
|
|
217
|
+
|
|
218
|
+
# Clear import cache
|
|
219
|
+
importlib.invalidate_caches()
|
|
220
|
+
|
|
221
|
+
return f"✅ Cleared {len(pycache_dirs)} __pycache__ directories and invalidated import caches"
|
|
222
|
+
|
|
223
|
+
except Exception as e:
|
|
224
|
+
logger.error(f"Failed to clear cache: {e}", exc_info=True)
|
|
225
|
+
return f"❌ Failed to clear cache: {str(e)}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def register_code_reload_tool(mcp, get_embedding_manager):
|
|
229
|
+
"""Register the code reloading tool with the MCP server."""
|
|
230
|
+
|
|
231
|
+
reloader = CodeReloader()
|
|
232
|
+
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
async def reload_code(
|
|
235
|
+
ctx: Context,
|
|
236
|
+
modules: Optional[List[str]] = Field(
|
|
237
|
+
default=None,
|
|
238
|
+
description="Specific modules to reload (e.g., ['src.search_tools', 'src.embedding_manager'])"
|
|
239
|
+
),
|
|
240
|
+
auto_detect: bool = Field(
|
|
241
|
+
default=True,
|
|
242
|
+
description="Automatically detect and reload changed modules"
|
|
243
|
+
)
|
|
244
|
+
) -> str:
|
|
245
|
+
"""Reload Python code at runtime without restarting the MCP server.
|
|
246
|
+
|
|
247
|
+
This allows hot-reloading of code changes during development, similar to
|
|
248
|
+
the mode switching capability. Changes take effect for new requests.
|
|
249
|
+
|
|
250
|
+
Note: Some changes (new tools, startup configuration) still require restart.
|
|
251
|
+
"""
|
|
252
|
+
return await reloader.reload_modules(ctx, modules, auto_detect)
|
|
253
|
+
|
|
254
|
+
@mcp.tool()
|
|
255
|
+
async def reload_status(ctx: Context) -> str:
|
|
256
|
+
"""Check which modules have pending changes and reload history.
|
|
257
|
+
|
|
258
|
+
Shows which files have been modified since last reload and
|
|
259
|
+
the history of recent reload operations.
|
|
260
|
+
"""
|
|
261
|
+
return await reloader.get_reload_status(ctx)
|
|
262
|
+
|
|
263
|
+
@mcp.tool()
|
|
264
|
+
async def clear_module_cache(ctx: Context) -> str:
|
|
265
|
+
"""Clear Python's module cache and __pycache__ directories.
|
|
266
|
+
|
|
267
|
+
Useful when reload isn't working due to cached bytecode.
|
|
268
|
+
"""
|
|
269
|
+
return await reloader.clear_python_cache(ctx)
|
|
270
|
+
|
|
271
|
+
logger.info("Code reload tools registered successfully")
|