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.
Files changed (41) hide show
  1. package/.claude/agents/claude-self-reflect-test.md +992 -510
  2. package/.claude/agents/reflection-specialist.md +59 -3
  3. package/README.md +14 -5
  4. package/installer/cli.js +16 -0
  5. package/installer/postinstall.js +14 -0
  6. package/installer/statusline-setup.js +289 -0
  7. package/mcp-server/run-mcp.sh +73 -5
  8. package/mcp-server/src/app_context.py +64 -0
  9. package/mcp-server/src/config.py +57 -0
  10. package/mcp-server/src/connection_pool.py +286 -0
  11. package/mcp-server/src/decay_manager.py +106 -0
  12. package/mcp-server/src/embedding_manager.py +64 -40
  13. package/mcp-server/src/embeddings_old.py +141 -0
  14. package/mcp-server/src/models.py +64 -0
  15. package/mcp-server/src/parallel_search.py +305 -0
  16. package/mcp-server/src/project_resolver.py +5 -0
  17. package/mcp-server/src/reflection_tools.py +211 -0
  18. package/mcp-server/src/rich_formatting.py +196 -0
  19. package/mcp-server/src/search_tools.py +874 -0
  20. package/mcp-server/src/server.py +127 -1720
  21. package/mcp-server/src/temporal_design.py +132 -0
  22. package/mcp-server/src/temporal_tools.py +604 -0
  23. package/mcp-server/src/temporal_utils.py +384 -0
  24. package/mcp-server/src/utils.py +150 -67
  25. package/package.json +15 -1
  26. package/scripts/add-timestamp-indexes.py +134 -0
  27. package/scripts/ast_grep_final_analyzer.py +325 -0
  28. package/scripts/ast_grep_unified_registry.py +556 -0
  29. package/scripts/check-collections.py +29 -0
  30. package/scripts/csr-status +366 -0
  31. package/scripts/debug-august-parsing.py +76 -0
  32. package/scripts/debug-import-single.py +91 -0
  33. package/scripts/debug-project-resolver.py +82 -0
  34. package/scripts/debug-temporal-tools.py +135 -0
  35. package/scripts/delta-metadata-update.py +547 -0
  36. package/scripts/import-conversations-unified.py +157 -25
  37. package/scripts/precompact-hook.sh +33 -0
  38. package/scripts/session_quality_tracker.py +481 -0
  39. package/scripts/streaming-watcher.py +1578 -0
  40. package/scripts/update_patterns.py +334 -0
  41. 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
- if PREFER_LOCAL_EMBEDDINGS or not VOYAGE_API_KEY:
137
- embeddings = list(embedding_provider.passage_embed(texts))
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
- code_blocks = re.findall(r'```[^`]*?\n(.*?)```', item.get('text', ''), re.DOTALL)
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
- # Delete existing points for this conversation to prevent stale data
501
+
502
+ # Extract conversation ID
430
503
  conversation_id = jsonl_file.stem
431
- try:
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