claude-self-reflect 3.3.0 → 3.3.1

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,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;
@@ -11,8 +11,17 @@ 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: Load .env file from project root if it exists
15
- # This ensures the MCP server uses the same settings as other scripts
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
+ # Load .env file for any missing values
16
25
  if [ -f "../.env" ]; then
17
26
  echo "[DEBUG] Loading .env file from project root" >&2
18
27
  set -a # Export all variables
@@ -22,8 +31,23 @@ else
22
31
  echo "[DEBUG] No .env file found, using defaults" >&2
23
32
  fi
24
33
 
25
- # Set smart defaults if not already set
26
- # These match what the CLI setup wizard uses
34
+ # Restore command-line values (they take precedence)
35
+ if [ ! -z "$CMDLINE_VOYAGE_KEY" ]; then
36
+ export VOYAGE_KEY="$CMDLINE_VOYAGE_KEY"
37
+ echo "[DEBUG] Using command-line VOYAGE_KEY" >&2
38
+ fi
39
+
40
+ if [ ! -z "$CMDLINE_PREFER_LOCAL" ]; then
41
+ export PREFER_LOCAL_EMBEDDINGS="$CMDLINE_PREFER_LOCAL"
42
+ echo "[DEBUG] Using command-line PREFER_LOCAL_EMBEDDINGS: $PREFER_LOCAL_EMBEDDINGS" >&2
43
+ fi
44
+
45
+ if [ ! -z "$CMDLINE_QDRANT_URL" ]; then
46
+ export QDRANT_URL="$CMDLINE_QDRANT_URL"
47
+ echo "[DEBUG] Using command-line QDRANT_URL: $QDRANT_URL" >&2
48
+ fi
49
+
50
+ # Set smart defaults ONLY if still not set
27
51
  if [ -z "$QDRANT_URL" ]; then
28
52
  export QDRANT_URL="http://localhost:6333"
29
53
  echo "[DEBUG] Using default QDRANT_URL: $QDRANT_URL" >&2
@@ -64,84 +64,11 @@ async def search_single_collection(
64
64
  DECAY_SCALE_DAYS = constants.get('DECAY_SCALE_DAYS', 90)
65
65
  DECAY_WEIGHT = constants.get('DECAY_WEIGHT', 0.3)
66
66
 
67
- if should_use_decay and USE_NATIVE_DECAY and NATIVE_DECAY_AVAILABLE:
68
- # Use native Qdrant decay with newer API
69
- await ctx.debug(f"Using NATIVE Qdrant decay (new API) for {collection_name}")
70
-
71
- half_life_seconds = DECAY_SCALE_DAYS * 24 * 60 * 60
72
-
73
- # Build query using Qdrant's Fusion and RankFusion
74
- fusion_query = models.Fusion(
75
- fusion=models.RankFusion.RRF,
76
- queries=[
77
- # Semantic similarity query
78
- models.NearestQuery(
79
- nearest=query_embedding,
80
- score_threshold=min_score
81
- ),
82
- # Time decay query using context pair
83
- models.ContextQuery(
84
- context=[
85
- models.ContextPair(
86
- positive=models.DiscoverQuery(
87
- target=query_embedding,
88
- context=[
89
- models.ContextPair(
90
- positive=models.DatetimeRange(
91
- gt=datetime.now().isoformat(),
92
- lt=(datetime.now().timestamp() + half_life_seconds)
93
- )
94
- )
95
- ]
96
- )
97
- )
98
- ]
99
- )
100
- ]
101
- )
102
-
103
- # Execute search with native decay
104
- search_results = await qdrant_client.query_points(
105
- collection_name=collection_name,
106
- query=fusion_query,
107
- limit=limit,
108
- with_payload=True
109
- )
110
-
111
- # Process results
112
- for point in search_results.points:
113
- # Process each point and add to results
114
- raw_timestamp = point.payload.get('timestamp', datetime.now().isoformat())
115
- clean_timestamp = raw_timestamp.replace('Z', '+00:00') if raw_timestamp.endswith('Z') else raw_timestamp
116
-
117
- point_project = point.payload.get('project', collection_name.replace('conv_', '').replace('_voyage', '').replace('_local', ''))
118
-
119
- # Apply project filtering
120
- if target_project != 'all' and not is_reflection_collection:
121
- if point_project != target_project:
122
- normalized_target = target_project.replace('-', '_')
123
- normalized_point = point_project.replace('-', '_')
124
- if not (normalized_point == normalized_target or
125
- point_project.endswith(f"/{target_project}") or
126
- point_project.endswith(f"-{target_project}") or
127
- normalized_point.endswith(f"_{normalized_target}") or
128
- normalized_point.endswith(f"/{normalized_target}")):
129
- continue
130
-
131
- # Create SearchResult
132
- search_result = {
133
- 'id': str(point.id),
134
- 'score': point.score,
135
- 'timestamp': clean_timestamp,
136
- 'role': point.payload.get('start_role', point.payload.get('role', 'unknown')),
137
- 'excerpt': (point.payload.get('text', '')[:350] + '...'
138
- if len(point.payload.get('text', '')) > 350
139
- else point.payload.get('text', '')),
140
- 'project_name': point_project,
141
- 'payload': point.payload
142
- }
143
- results.append(search_result)
144
-
67
+ # NOTE: Native decay API is not available in current Qdrant, fall back to client-side
68
+ # The Fusion/RankFusion API was experimental and removed, always use client-side decay
69
+ if should_use_decay and False: # Disabled until Qdrant provides stable decay API
70
+ # This code path is intentionally disabled
71
+ pass
145
72
  else:
146
73
  # Standard search without native decay or client-side decay
147
74
  search_results = await qdrant_client.search(
@@ -220,17 +147,24 @@ async def search_single_collection(
220
147
  continue
221
148
  logger.debug(f"Keeping point: project '{point_project}' matches target '{target_project}'")
222
149
 
223
- # Create SearchResult
150
+ # Create SearchResult with consistent structure
224
151
  search_result = {
225
152
  'id': str(point.id),
226
153
  'score': adjusted_score,
227
154
  'timestamp': clean_timestamp,
228
155
  'role': point.payload.get('start_role', point.payload.get('role', 'unknown')),
229
- 'excerpt': (point.payload.get('text', '')[:350] + '...'
230
- if len(point.payload.get('text', '')) > 350
156
+ 'excerpt': (point.payload.get('text', '')[:350] + '...'
157
+ if len(point.payload.get('text', '')) > 350
231
158
  else point.payload.get('text', '')),
232
159
  'project_name': point_project,
233
- 'payload': point.payload
160
+ 'conversation_id': point.payload.get('conversation_id'),
161
+ 'base_conversation_id': point.payload.get('base_conversation_id'),
162
+ 'collection_name': collection_name,
163
+ 'raw_payload': point.payload, # Renamed from 'payload' for consistency
164
+ 'code_patterns': point.payload.get('code_patterns'),
165
+ 'files_analyzed': point.payload.get('files_analyzed'),
166
+ 'tools_used': list(point.payload.get('tools_used', [])) if isinstance(point.payload.get('tools_used'), set) else point.payload.get('tools_used'),
167
+ 'concepts': point.payload.get('concepts')
234
168
  }
235
169
  results.append(search_result)
236
170
  else:
@@ -44,9 +44,10 @@ class ReflectionTools:
44
44
  await ctx.debug(f"Storing reflection with {len(tags)} tags")
45
45
 
46
46
  try:
47
- # Determine collection name based on embedding type
47
+ # Determine collection name based on active model type, not prefer_local
48
48
  embedding_manager = self.get_embedding_manager()
49
- embedding_type = "local" if embedding_manager.prefer_local else "voyage"
49
+ # Use actual model_type to ensure consistency
50
+ embedding_type = embedding_manager.model_type or ("voyage" if embedding_manager.voyage_client else "local")
50
51
  collection_name = f"reflections_{embedding_type}"
51
52
 
52
53
  # Ensure reflections collection exists
@@ -57,8 +58,8 @@ class ReflectionTools:
57
58
  # Collection doesn't exist, create it
58
59
  await ctx.debug(f"Creating {collection_name} collection")
59
60
 
60
- # Determine embedding dimensions
61
- embedding_dim = embedding_manager.get_vector_dimension()
61
+ # Get embedding dimensions for the specific type
62
+ embedding_dim = embedding_manager.get_vector_dimension(force_type=embedding_type)
62
63
 
63
64
  await self.qdrant_client.create_collection(
64
65
  collection_name=collection_name,
@@ -67,10 +68,14 @@ class ReflectionTools:
67
68
  distance=Distance.COSINE
68
69
  )
69
70
  )
70
-
71
- # Generate embedding for the reflection
72
- embedding_manager = self.get_embedding_manager()
73
- embedding = await embedding_manager.generate_embedding(content)
71
+
72
+ # Generate embedding with the same forced type for consistency
73
+ embedding = await embedding_manager.generate_embedding(content, force_type=embedding_type)
74
+
75
+ # Guard against failed embeddings
76
+ if not embedding:
77
+ await ctx.debug("Failed to generate embedding for reflection")
78
+ return "Failed to store reflection: embedding generation failed"
74
79
 
75
80
  # Create unique ID
76
81
  reflection_id = hashlib.md5(f"{content}{datetime.now().isoformat()}".encode()).hexdigest()