claude-self-reflect 3.2.4 → 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.
- package/.claude/agents/claude-self-reflect-test.md +992 -510
- package/.claude/agents/reflection-specialist.md +59 -3
- package/README.md +14 -5
- 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 +73 -5
- package/mcp-server/src/app_context.py +64 -0
- package/mcp-server/src/config.py +57 -0
- package/mcp-server/src/connection_pool.py +286 -0
- package/mcp-server/src/decay_manager.py +106 -0
- package/mcp-server/src/embedding_manager.py +64 -40
- package/mcp-server/src/embeddings_old.py +141 -0
- package/mcp-server/src/models.py +64 -0
- package/mcp-server/src/parallel_search.py +305 -0
- package/mcp-server/src/project_resolver.py +5 -0
- package/mcp-server/src/reflection_tools.py +211 -0
- package/mcp-server/src/rich_formatting.py +196 -0
- package/mcp-server/src/search_tools.py +874 -0
- package/mcp-server/src/server.py +127 -1720
- package/mcp-server/src/temporal_design.py +132 -0
- package/mcp-server/src/temporal_tools.py +604 -0
- package/mcp-server/src/temporal_utils.py +384 -0
- package/mcp-server/src/utils.py +150 -67
- package/package.json +15 -1
- package/scripts/add-timestamp-indexes.py +134 -0
- package/scripts/ast_grep_final_analyzer.py +325 -0
- package/scripts/ast_grep_unified_registry.py +556 -0
- package/scripts/check-collections.py +29 -0
- package/scripts/csr-status +366 -0
- package/scripts/debug-august-parsing.py +76 -0
- package/scripts/debug-import-single.py +91 -0
- package/scripts/debug-project-resolver.py +82 -0
- package/scripts/debug-temporal-tools.py +135 -0
- package/scripts/delta-metadata-update.py +547 -0
- package/scripts/import-conversations-unified.py +157 -25
- package/scripts/precompact-hook.sh +33 -0
- package/scripts/session_quality_tracker.py +481 -0
- package/scripts/streaming-watcher.py +1578 -0
- package/scripts/update_patterns.py +334 -0
- package/scripts/utils.py +39 -0
|
@@ -13,11 +13,23 @@ import ast
|
|
|
13
13
|
import re
|
|
14
14
|
import fcntl
|
|
15
15
|
import time
|
|
16
|
+
import argparse
|
|
16
17
|
from pathlib import Path
|
|
17
18
|
from datetime import datetime
|
|
18
19
|
from typing import List, Dict, Any, Optional, Set
|
|
19
20
|
import logging
|
|
20
21
|
|
|
22
|
+
# Load .env file if it exists
|
|
23
|
+
try:
|
|
24
|
+
from dotenv import load_dotenv
|
|
25
|
+
# Load from project root
|
|
26
|
+
env_path = Path(__file__).parent.parent / '.env'
|
|
27
|
+
if env_path.exists():
|
|
28
|
+
load_dotenv(env_path)
|
|
29
|
+
print(f"Loaded .env from {env_path}")
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass # dotenv not available, use system environment
|
|
32
|
+
|
|
21
33
|
# Add the scripts directory to the Python path for utils import
|
|
22
34
|
scripts_dir = Path(__file__).parent
|
|
23
35
|
sys.path.insert(0, str(scripts_dir))
|
|
@@ -133,8 +145,17 @@ def ensure_collection(collection_name: str):
|
|
|
133
145
|
|
|
134
146
|
def generate_embeddings(texts: List[str]) -> List[List[float]]:
|
|
135
147
|
"""Generate embeddings for texts."""
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
# Use the global embedding_provider which gets updated by command-line args
|
|
149
|
+
if PREFER_LOCAL_EMBEDDINGS:
|
|
150
|
+
# FastEmbed uses 'embed' method, not 'passage_embed'
|
|
151
|
+
# Try 'embed' first, fall back to 'passage_embed' for compatibility
|
|
152
|
+
if hasattr(embedding_provider, 'embed'):
|
|
153
|
+
embeddings = list(embedding_provider.embed(texts))
|
|
154
|
+
elif hasattr(embedding_provider, 'passage_embed'):
|
|
155
|
+
# Fallback for older versions (shouldn't exist but kept for safety)
|
|
156
|
+
embeddings = list(embedding_provider.passage_embed(texts))
|
|
157
|
+
else:
|
|
158
|
+
raise AttributeError("FastEmbed provider has neither 'embed' nor 'passage_embed' method")
|
|
138
159
|
return [emb.tolist() if hasattr(emb, 'tolist') else emb for emb in embeddings]
|
|
139
160
|
else:
|
|
140
161
|
response = embedding_provider.embed(texts, model="voyage-3")
|
|
@@ -355,7 +376,8 @@ def extract_metadata_single_pass(file_path: str) -> tuple[Dict[str, Any], str, i
|
|
|
355
376
|
# Extract code for AST analysis with bounds checking
|
|
356
377
|
if len(metadata['ast_elements']) < MAX_AST_ELEMENTS:
|
|
357
378
|
# Fix: More permissive regex to handle various fence formats
|
|
358
|
-
|
|
379
|
+
# Handles both ```\n and ```python\n cases, with optional newline
|
|
380
|
+
code_blocks = re.findall(r'```[^`\n]*\n?(.*?)```', item.get('text', ''), re.DOTALL)
|
|
359
381
|
for code_block in code_blocks[:MAX_CODE_BLOCKS]: # Use defined constant
|
|
360
382
|
if len(metadata['ast_elements']) >= MAX_AST_ELEMENTS:
|
|
361
383
|
break
|
|
@@ -363,7 +385,11 @@ def extract_metadata_single_pass(file_path: str) -> tuple[Dict[str, Any], str, i
|
|
|
363
385
|
for elem in list(ast_elems)[:MAX_ELEMENTS_PER_BLOCK]: # Use defined constant
|
|
364
386
|
if elem not in metadata['ast_elements'] and len(metadata['ast_elements']) < MAX_AST_ELEMENTS:
|
|
365
387
|
metadata['ast_elements'].append(elem)
|
|
366
|
-
|
|
388
|
+
|
|
389
|
+
elif item.get('type') == 'thinking':
|
|
390
|
+
# Also include thinking content in metadata extraction
|
|
391
|
+
text_content += item.get('thinking', '')
|
|
392
|
+
|
|
367
393
|
elif item.get('type') == 'tool_use':
|
|
368
394
|
tool_name = item.get('name', '')
|
|
369
395
|
if tool_name and tool_name not in metadata['tools_used']:
|
|
@@ -410,39 +436,77 @@ def extract_metadata_single_pass(file_path: str) -> tuple[Dict[str, Any], str, i
|
|
|
410
436
|
if all_text:
|
|
411
437
|
combined_text = ' '.join(all_text[:MAX_CONCEPT_MESSAGES]) # Limit messages for concept extraction
|
|
412
438
|
metadata['concepts'] = extract_concepts(combined_text)
|
|
413
|
-
|
|
439
|
+
|
|
440
|
+
# MANDATORY: AST-GREP Pattern Analysis
|
|
441
|
+
# Analyze code quality for files mentioned in conversation
|
|
442
|
+
pattern_quality = {}
|
|
443
|
+
avg_quality_score = 0.0
|
|
444
|
+
|
|
445
|
+
try:
|
|
446
|
+
# Update patterns first (uses 24h cache, <100ms)
|
|
447
|
+
from update_patterns import check_and_update_patterns
|
|
448
|
+
check_and_update_patterns()
|
|
449
|
+
|
|
450
|
+
# Import analyzer
|
|
451
|
+
from ast_grep_final_analyzer import FinalASTGrepAnalyzer
|
|
452
|
+
analyzer = FinalASTGrepAnalyzer()
|
|
453
|
+
|
|
454
|
+
# Analyze edited and analyzed files
|
|
455
|
+
files_to_analyze = list(set(metadata['files_edited'] + metadata['files_analyzed'][:10]))
|
|
456
|
+
quality_scores = []
|
|
457
|
+
|
|
458
|
+
for file_path in files_to_analyze:
|
|
459
|
+
# Only analyze code files
|
|
460
|
+
if file_path and any(file_path.endswith(ext) for ext in ['.py', '.ts', '.js', '.tsx', '.jsx']):
|
|
461
|
+
try:
|
|
462
|
+
# Check if file exists and is accessible
|
|
463
|
+
if os.path.exists(file_path):
|
|
464
|
+
result = analyzer.analyze_file(file_path)
|
|
465
|
+
metrics = result['quality_metrics']
|
|
466
|
+
pattern_quality[file_path] = {
|
|
467
|
+
'score': metrics['quality_score'],
|
|
468
|
+
'good_patterns': metrics['good_patterns_found'],
|
|
469
|
+
'bad_patterns': metrics['bad_patterns_found'],
|
|
470
|
+
'issues': metrics['total_issues']
|
|
471
|
+
}
|
|
472
|
+
quality_scores.append(metrics['quality_score'])
|
|
473
|
+
except Exception as e:
|
|
474
|
+
logger.debug(f"Could not analyze {file_path}: {e}")
|
|
475
|
+
|
|
476
|
+
# Calculate average quality
|
|
477
|
+
if quality_scores:
|
|
478
|
+
avg_quality_score = sum(quality_scores) / len(quality_scores)
|
|
479
|
+
|
|
480
|
+
except Exception as e:
|
|
481
|
+
logger.debug(f"AST analysis not available: {e}")
|
|
482
|
+
|
|
483
|
+
# Add pattern analysis to metadata
|
|
484
|
+
metadata['pattern_analysis'] = pattern_quality
|
|
485
|
+
metadata['avg_quality_score'] = round(avg_quality_score, 3)
|
|
486
|
+
|
|
414
487
|
# Set total messages
|
|
415
488
|
metadata['total_messages'] = message_count
|
|
416
|
-
|
|
489
|
+
|
|
417
490
|
# Limit arrays
|
|
418
491
|
metadata['files_analyzed'] = metadata['files_analyzed'][:MAX_FILES_ANALYZED]
|
|
419
492
|
metadata['files_edited'] = metadata['files_edited'][:MAX_FILES_EDITED]
|
|
420
493
|
metadata['tools_used'] = metadata['tools_used'][:MAX_TOOLS_USED]
|
|
421
494
|
metadata['ast_elements'] = metadata['ast_elements'][:MAX_AST_ELEMENTS]
|
|
422
|
-
|
|
495
|
+
|
|
423
496
|
return metadata, first_timestamp or datetime.now().isoformat(), message_count
|
|
424
497
|
|
|
425
498
|
def stream_import_file(jsonl_file: Path, collection_name: str, project_path: Path) -> int:
|
|
426
499
|
"""Stream import a single JSONL file without loading it into memory."""
|
|
427
500
|
logger.info(f"Streaming import of {jsonl_file.name}")
|
|
428
|
-
|
|
429
|
-
#
|
|
501
|
+
|
|
502
|
+
# Extract conversation ID
|
|
430
503
|
conversation_id = jsonl_file.stem
|
|
431
|
-
|
|
432
|
-
from qdrant_client.models import Filter, FieldCondition, MatchValue
|
|
433
|
-
client.delete(
|
|
434
|
-
collection_name=collection_name,
|
|
435
|
-
points_selector=Filter(
|
|
436
|
-
must=[FieldCondition(key="conversation_id", match=MatchValue(value=conversation_id))]
|
|
437
|
-
),
|
|
438
|
-
wait=True
|
|
439
|
-
)
|
|
440
|
-
logger.info(f"Deleted existing points for conversation {conversation_id}")
|
|
441
|
-
except Exception as e:
|
|
442
|
-
logger.warning(f"Could not delete existing points for {conversation_id}: {e}")
|
|
443
|
-
|
|
504
|
+
|
|
444
505
|
# Extract metadata in first pass (lightweight)
|
|
445
506
|
metadata, created_at, total_messages = extract_metadata_single_pass(str(jsonl_file))
|
|
507
|
+
|
|
508
|
+
# Track whether we should delete old points (only after successful import)
|
|
509
|
+
should_delete_old = False
|
|
446
510
|
|
|
447
511
|
# Reset counters for each conversation (critical for correct indexing)
|
|
448
512
|
current_message_index = 0 # Must be reset before processing each conversation
|
|
@@ -480,6 +544,11 @@ def stream_import_file(jsonl_file: Path, collection_name: str, project_path: Pat
|
|
|
480
544
|
item_type = item.get('type', '')
|
|
481
545
|
if item_type == 'text':
|
|
482
546
|
text_parts.append(item.get('text', ''))
|
|
547
|
+
elif item_type == 'thinking':
|
|
548
|
+
# Include thinking content (from Claude's thinking blocks)
|
|
549
|
+
thinking_content = item.get('thinking', '')
|
|
550
|
+
if thinking_content:
|
|
551
|
+
text_parts.append(f"[Thinking] {thinking_content[:1000]}") # Limit size
|
|
483
552
|
elif item_type == 'tool_use':
|
|
484
553
|
# Include tool use information
|
|
485
554
|
tool_name = item.get('name', 'unknown')
|
|
@@ -581,10 +650,35 @@ def stream_import_file(jsonl_file: Path, collection_name: str, project_path: Pat
|
|
|
581
650
|
created_at, metadata, collection_name, project_path, total_messages
|
|
582
651
|
)
|
|
583
652
|
total_chunks += chunks
|
|
584
|
-
|
|
653
|
+
|
|
654
|
+
# Only delete old points after successful import verification
|
|
655
|
+
if total_chunks > 0:
|
|
656
|
+
try:
|
|
657
|
+
from qdrant_client.models import Filter, FieldCondition, MatchValue
|
|
658
|
+
# Count old points before deletion for verification
|
|
659
|
+
old_count_filter = Filter(
|
|
660
|
+
must=[FieldCondition(key="conversation_id", match=MatchValue(value=conversation_id))]
|
|
661
|
+
)
|
|
662
|
+
old_points = client.scroll(
|
|
663
|
+
collection_name=collection_name,
|
|
664
|
+
scroll_filter=old_count_filter,
|
|
665
|
+
limit=1
|
|
666
|
+
)[0]
|
|
667
|
+
|
|
668
|
+
if len(old_points) > total_chunks + 5: # Allow some tolerance
|
|
669
|
+
# Only delete if we have significantly more old points than new
|
|
670
|
+
client.delete(
|
|
671
|
+
collection_name=collection_name,
|
|
672
|
+
points_selector=old_count_filter,
|
|
673
|
+
wait=True
|
|
674
|
+
)
|
|
675
|
+
logger.info(f"Deleted old points for conversation {conversation_id} after verifying new import")
|
|
676
|
+
except Exception as e:
|
|
677
|
+
logger.warning(f"Could not clean up old points for {conversation_id}: {e}")
|
|
678
|
+
|
|
585
679
|
logger.info(f"Imported {total_chunks} chunks from {jsonl_file.name}")
|
|
586
680
|
return total_chunks
|
|
587
|
-
|
|
681
|
+
|
|
588
682
|
except Exception as e:
|
|
589
683
|
logger.error(f"Failed to import {jsonl_file}: {e}")
|
|
590
684
|
return 0
|
|
@@ -673,6 +767,32 @@ def update_file_state(file_path: Path, state: dict, chunks: int):
|
|
|
673
767
|
|
|
674
768
|
def main():
|
|
675
769
|
"""Main import function."""
|
|
770
|
+
# Parse command-line arguments
|
|
771
|
+
parser = argparse.ArgumentParser(description='Import conversations with unified embeddings support')
|
|
772
|
+
parser.add_argument('--prefer-voyage', action='store_true',
|
|
773
|
+
help='Use Voyage AI embeddings instead of local FastEmbed')
|
|
774
|
+
parser.add_argument('--limit', type=int,
|
|
775
|
+
help='Limit number of files to import')
|
|
776
|
+
parser.add_argument('--max-files-per-cycle', type=int,
|
|
777
|
+
help='Maximum files to process per cycle')
|
|
778
|
+
args = parser.parse_args()
|
|
779
|
+
|
|
780
|
+
# Override environment variable if --prefer-voyage is specified
|
|
781
|
+
global PREFER_LOCAL_EMBEDDINGS, embedding_provider, embedding_dimension, collection_suffix
|
|
782
|
+
if args.prefer_voyage:
|
|
783
|
+
if not VOYAGE_API_KEY:
|
|
784
|
+
logger.error("--prefer-voyage specified but VOYAGE_KEY environment variable not set")
|
|
785
|
+
sys.exit(1)
|
|
786
|
+
logger.info("Command-line flag --prefer-voyage detected, switching to Voyage AI embeddings")
|
|
787
|
+
PREFER_LOCAL_EMBEDDINGS = False
|
|
788
|
+
|
|
789
|
+
# Re-initialize embedding provider with Voyage
|
|
790
|
+
import voyageai
|
|
791
|
+
embedding_provider = voyageai.Client(api_key=VOYAGE_API_KEY)
|
|
792
|
+
embedding_dimension = 1024
|
|
793
|
+
collection_suffix = "voyage"
|
|
794
|
+
logger.info("Switched to Voyage AI embeddings (dimension: 1024)")
|
|
795
|
+
|
|
676
796
|
# Load state
|
|
677
797
|
state = load_state()
|
|
678
798
|
logger.info(f"Loaded state with {len(state.get('imported_files', {}))} previously imported files")
|
|
@@ -695,6 +815,7 @@ def main():
|
|
|
695
815
|
logger.info(f"Found {len(project_dirs)} projects to import")
|
|
696
816
|
|
|
697
817
|
total_imported = 0
|
|
818
|
+
files_processed = 0
|
|
698
819
|
|
|
699
820
|
for project_dir in project_dirs:
|
|
700
821
|
# Get collection name
|
|
@@ -707,13 +828,24 @@ def main():
|
|
|
707
828
|
# Find JSONL files
|
|
708
829
|
jsonl_files = sorted(project_dir.glob("*.jsonl"))
|
|
709
830
|
|
|
831
|
+
# Apply limit from command line if specified
|
|
832
|
+
if args.limit and files_processed >= args.limit:
|
|
833
|
+
logger.info(f"Reached limit of {args.limit} files, stopping import")
|
|
834
|
+
break
|
|
835
|
+
|
|
710
836
|
# Limit files per cycle if specified
|
|
711
|
-
max_files = int(os.getenv("MAX_FILES_PER_CYCLE", "1000"))
|
|
837
|
+
max_files = args.max_files_per_cycle or int(os.getenv("MAX_FILES_PER_CYCLE", "1000"))
|
|
712
838
|
jsonl_files = jsonl_files[:max_files]
|
|
713
839
|
|
|
714
840
|
for jsonl_file in jsonl_files:
|
|
841
|
+
# Check limit again per file
|
|
842
|
+
if args.limit and files_processed >= args.limit:
|
|
843
|
+
logger.info(f"Reached limit of {args.limit} files, stopping import")
|
|
844
|
+
break
|
|
845
|
+
|
|
715
846
|
if should_import_file(jsonl_file, state):
|
|
716
847
|
chunks = stream_import_file(jsonl_file, collection_name, project_dir)
|
|
848
|
+
files_processed += 1
|
|
717
849
|
if chunks > 0:
|
|
718
850
|
# Verify data is actually in Qdrant before marking as imported
|
|
719
851
|
from qdrant_client.models import Filter, FieldCondition, MatchValue
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# PreCompact hook for Claude Self-Reflect
|
|
3
|
+
# Place this in ~/.claude/hooks/precompact or source it from there
|
|
4
|
+
|
|
5
|
+
# Configuration
|
|
6
|
+
CLAUDE_REFLECT_DIR="${CLAUDE_REFLECT_DIR:-$HOME/claude-self-reflect}"
|
|
7
|
+
VENV_PATH="${VENV_PATH:-$CLAUDE_REFLECT_DIR/.venv}"
|
|
8
|
+
IMPORT_TIMEOUT="${IMPORT_TIMEOUT:-30}"
|
|
9
|
+
|
|
10
|
+
# Check if Claude Self-Reflect is installed
|
|
11
|
+
if [ ! -d "$CLAUDE_REFLECT_DIR" ]; then
|
|
12
|
+
echo "Claude Self-Reflect not found at $CLAUDE_REFLECT_DIR" >&2
|
|
13
|
+
exit 0 # Exit gracefully
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Check if virtual environment exists
|
|
17
|
+
if [ ! -d "$VENV_PATH" ]; then
|
|
18
|
+
echo "Virtual environment not found at $VENV_PATH" >&2
|
|
19
|
+
exit 0 # Exit gracefully
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Run quick import with timeout
|
|
23
|
+
echo "Updating conversation memory..." >&2
|
|
24
|
+
timeout $IMPORT_TIMEOUT bash -c "
|
|
25
|
+
source '$VENV_PATH/bin/activate' 2>/dev/null
|
|
26
|
+
python '$CLAUDE_REFLECT_DIR/scripts/import-latest.py' 2>&1 | \
|
|
27
|
+
grep -E '(Quick import completed|Imported|Warning)' >&2
|
|
28
|
+
" || {
|
|
29
|
+
echo "Quick import timed out after ${IMPORT_TIMEOUT}s" >&2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Always exit successfully to not block compacting
|
|
33
|
+
exit 0
|