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
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
import time
|
|
7
|
+
import html
|
|
7
8
|
from typing import Optional, List, Dict, Any
|
|
8
9
|
from datetime import datetime, timezone
|
|
9
10
|
from pathlib import Path
|
|
@@ -82,10 +83,20 @@ class SearchTools:
|
|
|
82
83
|
# Generate embedding for query
|
|
83
84
|
embedding_manager = self.get_embedding_manager()
|
|
84
85
|
|
|
85
|
-
# Determine embedding type based on collection name
|
|
86
|
-
|
|
86
|
+
# Determine embedding type based on collection name (v3 and v4 compatible)
|
|
87
|
+
# v4 format: csr_project_mode_dims (e.g., csr_project_cloud_1024d)
|
|
88
|
+
# v3 format: project_suffix (e.g., project_voyage)
|
|
89
|
+
if '_cloud_' in collection_name or collection_name.endswith('_1024d') or collection_name.endswith('_voyage'):
|
|
90
|
+
embedding_type = 'voyage'
|
|
91
|
+
else:
|
|
92
|
+
embedding_type = 'local'
|
|
87
93
|
query_embedding = await embedding_manager.generate_embedding(query, force_type=embedding_type)
|
|
88
|
-
|
|
94
|
+
|
|
95
|
+
# FIX: Validate embedding before search
|
|
96
|
+
if query_embedding is None:
|
|
97
|
+
logger.warning(f"Embedding generation failed for query in {collection_name}")
|
|
98
|
+
return []
|
|
99
|
+
|
|
89
100
|
# Search the collection
|
|
90
101
|
search_results = await self.qdrant_client.search(
|
|
91
102
|
collection_name=collection_name,
|
|
@@ -131,9 +142,9 @@ class SearchTools:
|
|
|
131
142
|
# Apply exponential decay
|
|
132
143
|
decay_factor = pow(2, -age / self.decay_scale_days)
|
|
133
144
|
|
|
134
|
-
# Adjust score
|
|
145
|
+
# Adjust score - FIX: Maintain comparable scale
|
|
135
146
|
original_score = result['score']
|
|
136
|
-
result['score'] = original_score * (1 - self.decay_weight) +
|
|
147
|
+
result['score'] = original_score * ((1 - self.decay_weight) + self.decay_weight * decay_factor)
|
|
137
148
|
result['original_score'] = original_score
|
|
138
149
|
result['decay_factor'] = decay_factor
|
|
139
150
|
|
|
@@ -168,20 +179,23 @@ class SearchTools:
|
|
|
168
179
|
if include_raw:
|
|
169
180
|
output += f"**Raw Payload:**\n```json\n{json.dumps(result.get('payload', {}), indent=2)}\n```\n\n"
|
|
170
181
|
else:
|
|
171
|
-
# XML format (default)
|
|
172
|
-
|
|
182
|
+
# XML format (default) with proper escaping
|
|
183
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
184
|
+
|
|
185
|
+
output = f"<search_results>\n<query>{_esc(query)}</query>\n<count>{len(results)}</count>\n"
|
|
173
186
|
for i, result in enumerate(results, 1):
|
|
174
187
|
output += f"<result index=\"{i}\">\n"
|
|
175
188
|
output += f" <score>{result['score']:.3f}</score>\n"
|
|
176
|
-
output += f" <timestamp>{result.get('timestamp', 'N/A')}</timestamp>\n"
|
|
177
|
-
output += f" <conversation_id>{result.get('conversation_id', 'N/A')}</conversation_id>\n"
|
|
189
|
+
output += f" <timestamp>{_esc(result.get('timestamp', 'N/A'))}</timestamp>\n"
|
|
190
|
+
output += f" <conversation_id>{_esc(result.get('conversation_id', 'N/A'))}</conversation_id>\n"
|
|
178
191
|
if not brief:
|
|
179
192
|
# Handle both 'content' and 'excerpt' fields
|
|
180
|
-
content = result.get('content', result.get('excerpt', ''))
|
|
193
|
+
content = result.get('content', result.get('excerpt', result.get('text', '')))
|
|
181
194
|
truncated = content[:500] + ('...' if len(content) > 500 else '')
|
|
182
195
|
output += f" <content><![CDATA[{truncated}]]></content>\n"
|
|
183
196
|
if include_raw:
|
|
184
|
-
|
|
197
|
+
# Use CDATA for large JSON payloads
|
|
198
|
+
output += f" <raw_payload><![CDATA[{json.dumps(result.get('payload', {}), ensure_ascii=False)}]]></raw_payload>\n"
|
|
185
199
|
output += "</result>\n"
|
|
186
200
|
output += "</search_results>"
|
|
187
201
|
|
|
@@ -238,12 +252,14 @@ class SearchTools:
|
|
|
238
252
|
]
|
|
239
253
|
await ctx.debug(f"Filtered to {len(filtered_collections)} collections from {len(all_collections)} total")
|
|
240
254
|
else:
|
|
241
|
-
# Use all collections
|
|
255
|
+
# Use all collections INCLUDING reflections (with decay)
|
|
242
256
|
collections_response = await self.qdrant_client.get_collections()
|
|
243
257
|
collections = collections_response.collections
|
|
258
|
+
# Include both conversation collections and reflection collections
|
|
244
259
|
filtered_collections = [
|
|
245
|
-
c for c in collections
|
|
246
|
-
if
|
|
260
|
+
c for c in collections
|
|
261
|
+
if (c.name.endswith('_local') or c.name.endswith('_voyage') or
|
|
262
|
+
c.name.startswith('reflections'))
|
|
247
263
|
]
|
|
248
264
|
await ctx.debug(f"Searching across {len(filtered_collections)} collections")
|
|
249
265
|
|
|
@@ -358,12 +374,14 @@ class SearchTools:
|
|
|
358
374
|
if c.name in collection_names
|
|
359
375
|
]
|
|
360
376
|
else:
|
|
361
|
-
# Use all collections
|
|
377
|
+
# Use all collections INCLUDING reflections (with decay)
|
|
362
378
|
collections_response = await self.qdrant_client.get_collections()
|
|
363
379
|
collections = collections_response.collections
|
|
380
|
+
# Include both conversation collections and reflection collections
|
|
364
381
|
filtered_collections = [
|
|
365
|
-
c for c in collections
|
|
366
|
-
if
|
|
382
|
+
c for c in collections
|
|
383
|
+
if (c.name.endswith('_local') or c.name.endswith('_voyage') or
|
|
384
|
+
c.name.startswith('reflections'))
|
|
367
385
|
]
|
|
368
386
|
|
|
369
387
|
# Quick PARALLEL count across collections
|
|
@@ -394,22 +412,29 @@ class SearchTools:
|
|
|
394
412
|
top_result = max(all_results, key=lambda x: x.get('score', 0)) if all_results else None
|
|
395
413
|
top_score = top_result.get('score', 0) if top_result else 0
|
|
396
414
|
|
|
397
|
-
# Format quick search response
|
|
415
|
+
# Format quick search response with proper XML escaping
|
|
416
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
417
|
+
|
|
398
418
|
if not top_result:
|
|
399
419
|
return "<quick_search><count>0</count><message>No matches found</message></quick_search>"
|
|
400
|
-
|
|
420
|
+
|
|
421
|
+
# Get preview text and ensure we have content fallbacks
|
|
422
|
+
preview_text = top_result.get('excerpt', top_result.get('content', top_result.get('text', '')))[:200]
|
|
423
|
+
|
|
401
424
|
return f"""<quick_search>
|
|
402
|
-
<count>{collections_with_matches}
|
|
425
|
+
<count>{collections_with_matches}</count>
|
|
426
|
+
<collections_with_matches>{collections_with_matches}</collections_with_matches>
|
|
403
427
|
<top_result>
|
|
404
428
|
<score>{top_result['score']:.3f}</score>
|
|
405
|
-
<timestamp>{top_result.get('timestamp', 'N/A')}</timestamp>
|
|
406
|
-
<preview
|
|
429
|
+
<timestamp>{_esc(top_result.get('timestamp', 'N/A'))}</timestamp>
|
|
430
|
+
<preview><![CDATA[{preview_text}...]]></preview>
|
|
407
431
|
</top_result>
|
|
408
432
|
</quick_search>"""
|
|
409
|
-
|
|
433
|
+
|
|
410
434
|
except Exception as e:
|
|
411
435
|
logger.error(f"Quick search failed: {e}", exc_info=True)
|
|
412
|
-
|
|
436
|
+
def _esc(x): return html.escape(str(x), quote=False)
|
|
437
|
+
return f"<quick_search><error>Quick search failed: {_esc(str(e))}</error></quick_search>"
|
|
413
438
|
|
|
414
439
|
async def search_summary(
|
|
415
440
|
self,
|
|
@@ -439,12 +464,14 @@ class SearchTools:
|
|
|
439
464
|
if c.name in collection_names
|
|
440
465
|
]
|
|
441
466
|
else:
|
|
442
|
-
# Use all collections
|
|
467
|
+
# Use all collections INCLUDING reflections (with decay)
|
|
443
468
|
collections_response = await self.qdrant_client.get_collections()
|
|
444
469
|
collections = collections_response.collections
|
|
470
|
+
# Include both conversation collections and reflection collections
|
|
445
471
|
filtered_collections = [
|
|
446
|
-
c for c in collections
|
|
447
|
-
if
|
|
472
|
+
c for c in collections
|
|
473
|
+
if (c.name.endswith('_local') or c.name.endswith('_voyage') or
|
|
474
|
+
c.name.startswith('reflections'))
|
|
448
475
|
]
|
|
449
476
|
|
|
450
477
|
# Gather results for summary using PARALLEL search
|
|
@@ -534,12 +561,14 @@ class SearchTools:
|
|
|
534
561
|
if c.name in collection_names
|
|
535
562
|
]
|
|
536
563
|
else:
|
|
537
|
-
# Use all collections
|
|
564
|
+
# Use all collections INCLUDING reflections (with decay)
|
|
538
565
|
collections_response = await self.qdrant_client.get_collections()
|
|
539
566
|
collections = collections_response.collections
|
|
567
|
+
# Include both conversation collections and reflection collections
|
|
540
568
|
filtered_collections = [
|
|
541
|
-
c for c in collections
|
|
542
|
-
if
|
|
569
|
+
c for c in collections
|
|
570
|
+
if (c.name.endswith('_local') or c.name.endswith('_voyage') or
|
|
571
|
+
c.name.startswith('reflections'))
|
|
543
572
|
]
|
|
544
573
|
|
|
545
574
|
# Gather all results using PARALLEL search
|
|
@@ -606,61 +635,102 @@ class SearchTools:
|
|
|
606
635
|
project: Optional[str] = None
|
|
607
636
|
) -> str:
|
|
608
637
|
"""Search for conversations that analyzed a specific file."""
|
|
609
|
-
|
|
638
|
+
|
|
610
639
|
await ctx.debug(f"Searching for file: {file_path}, project={project}")
|
|
611
|
-
|
|
640
|
+
|
|
612
641
|
try:
|
|
613
|
-
#
|
|
614
|
-
|
|
615
|
-
|
|
642
|
+
# Create multiple path variants to match how paths are stored
|
|
643
|
+
# Import uses normalize_file_path which replaces /Users/ with ~/
|
|
644
|
+
path_variants = set()
|
|
645
|
+
|
|
646
|
+
# Original path
|
|
647
|
+
path_variants.add(file_path)
|
|
648
|
+
|
|
649
|
+
# Basename only
|
|
650
|
+
path_variants.add(os.path.basename(file_path))
|
|
651
|
+
|
|
652
|
+
# Try to resolve if it's a valid path
|
|
653
|
+
try:
|
|
654
|
+
resolved_path = str(Path(file_path).resolve())
|
|
655
|
+
path_variants.add(resolved_path)
|
|
656
|
+
|
|
657
|
+
# Convert resolved path to ~/ format (matching how import stores it)
|
|
658
|
+
home_dir = str(Path.home())
|
|
659
|
+
if resolved_path.startswith(home_dir):
|
|
660
|
+
tilde_path = resolved_path.replace(home_dir, '~', 1)
|
|
661
|
+
path_variants.add(tilde_path)
|
|
662
|
+
|
|
663
|
+
# Also try with /Users/ replaced by ~/
|
|
664
|
+
if '/Users/' in resolved_path:
|
|
665
|
+
path_variants.add(resolved_path.replace('/Users/', '~/', 1))
|
|
666
|
+
except:
|
|
667
|
+
pass
|
|
668
|
+
|
|
669
|
+
# If path starts with ~, also try expanded version
|
|
670
|
+
if file_path.startswith('~'):
|
|
671
|
+
expanded = os.path.expanduser(file_path)
|
|
672
|
+
path_variants.add(expanded)
|
|
673
|
+
|
|
674
|
+
# Convert all to forward slashes for consistency
|
|
675
|
+
path_variants = {p.replace('\\', '/') for p in path_variants if p}
|
|
676
|
+
|
|
677
|
+
await ctx.debug(f"Searching with path variants: {list(path_variants)}")
|
|
678
|
+
|
|
616
679
|
# Search for file mentions in metadata
|
|
617
680
|
collections_response = await self.qdrant_client.get_collections()
|
|
618
681
|
collections = collections_response.collections
|
|
619
|
-
|
|
620
|
-
# Define async function to search a single collection
|
|
682
|
+
|
|
683
|
+
# Define async function to search a single collection using scroll
|
|
621
684
|
async def search_collection(collection_name: str):
|
|
622
685
|
try:
|
|
623
|
-
|
|
624
|
-
|
|
686
|
+
from qdrant_client import models
|
|
687
|
+
|
|
688
|
+
# Use scroll with proper filter for metadata-only search
|
|
689
|
+
results, _ = await self.qdrant_client.scroll(
|
|
625
690
|
collection_name=collection_name,
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
691
|
+
scroll_filter=models.Filter(
|
|
692
|
+
should=[
|
|
693
|
+
models.FieldCondition(
|
|
694
|
+
key="files_analyzed",
|
|
695
|
+
match=models.MatchValue(value=path_variant)
|
|
696
|
+
)
|
|
697
|
+
for path_variant in path_variants
|
|
634
698
|
]
|
|
635
|
-
|
|
699
|
+
),
|
|
700
|
+
limit=limit,
|
|
701
|
+
with_payload=True
|
|
636
702
|
)
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
for
|
|
640
|
-
|
|
641
|
-
'conversation_id':
|
|
642
|
-
'timestamp':
|
|
643
|
-
'content':
|
|
644
|
-
'files_analyzed':
|
|
645
|
-
'score':
|
|
703
|
+
|
|
704
|
+
formatted_results = []
|
|
705
|
+
for point in results:
|
|
706
|
+
formatted_results.append({
|
|
707
|
+
'conversation_id': point.payload.get('conversation_id'),
|
|
708
|
+
'timestamp': point.payload.get('timestamp'),
|
|
709
|
+
'content': point.payload.get('content', point.payload.get('text', '')),
|
|
710
|
+
'files_analyzed': point.payload.get('files_analyzed', []),
|
|
711
|
+
'score': 1.0 # No score in scroll, use 1.0 for found items
|
|
646
712
|
})
|
|
647
|
-
return
|
|
648
|
-
|
|
713
|
+
return formatted_results
|
|
714
|
+
|
|
649
715
|
except Exception as e:
|
|
650
716
|
await ctx.debug(f"Error searching {collection_name}: {e}")
|
|
651
717
|
return []
|
|
652
718
|
|
|
653
|
-
#
|
|
719
|
+
# SECURITY FIX: Use proper concurrency limiting
|
|
654
720
|
import asyncio
|
|
721
|
+
from .security_patches import ConcurrencyLimiter
|
|
722
|
+
|
|
655
723
|
search_tasks = [search_collection(c.name) for c in collections]
|
|
656
|
-
|
|
657
|
-
#
|
|
658
|
-
batch_size = 20
|
|
724
|
+
|
|
725
|
+
# Use semaphore-based limiting instead of batching
|
|
659
726
|
all_results = []
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
727
|
+
batch_results = await ConcurrencyLimiter.limited_gather(search_tasks, limit=10)
|
|
728
|
+
for results in batch_results:
|
|
729
|
+
if isinstance(results, Exception):
|
|
730
|
+
logger.error(f"Search task failed: {type(results).__name__}: {results}")
|
|
731
|
+
await ctx.debug(f"Search task error: {results}")
|
|
732
|
+
continue
|
|
733
|
+
if results:
|
|
664
734
|
all_results.extend(results)
|
|
665
735
|
|
|
666
736
|
# Format results
|
|
@@ -743,7 +813,7 @@ def register_search_tools(
|
|
|
743
813
|
project_resolver # Pass the resolver
|
|
744
814
|
)
|
|
745
815
|
|
|
746
|
-
@mcp.tool()
|
|
816
|
+
@mcp.tool(name="csr_reflect_on_past")
|
|
747
817
|
async def reflect_on_past(
|
|
748
818
|
ctx: Context,
|
|
749
819
|
query: str = Field(description="The search query to find semantically similar conversations"),
|
|
@@ -756,29 +826,45 @@ def register_search_tools(
|
|
|
756
826
|
include_raw: bool = Field(default=False, description="Include raw Qdrant payload data for debugging (increases response size)"),
|
|
757
827
|
response_format: str = Field(default="xml", description="Response format: 'xml' or 'markdown'")
|
|
758
828
|
) -> str:
|
|
759
|
-
"""Search
|
|
829
|
+
"""Search past Claude conversations semantically to find relevant context.
|
|
830
|
+
|
|
831
|
+
WHEN TO USE: User asks 'what did we discuss about X?', 'find conversations about Y',
|
|
832
|
+
mentions 'remember when' or 'last time', debugging issues that may have been solved before,
|
|
833
|
+
or finding implementation patterns used in the project.
|
|
834
|
+
|
|
835
|
+
This is the PRIMARY tool for conversation memory - use it liberally!"""
|
|
760
836
|
return await tools.reflect_on_past(ctx, query, limit, min_score, use_decay, project, mode, brief, include_raw, response_format)
|
|
761
837
|
|
|
762
|
-
@mcp.tool()
|
|
838
|
+
@mcp.tool(name="csr_quick_check")
|
|
763
839
|
async def quick_search(
|
|
764
840
|
ctx: Context,
|
|
765
841
|
query: str = Field(description="The search query to find semantically similar conversations"),
|
|
766
842
|
min_score: float = Field(default=0.3, description="Minimum similarity score (0-1)"),
|
|
767
843
|
project: Optional[str] = Field(default=None, description="Search specific project only. If not provided, searches current project based on working directory. Use 'all' to search across all projects.")
|
|
768
844
|
) -> str:
|
|
769
|
-
"""Quick
|
|
845
|
+
"""Quick check if a topic was discussed before (returns count + top match only).
|
|
846
|
+
|
|
847
|
+
WHEN TO USE: User asks 'have we discussed X?' or 'is there anything about Y?',
|
|
848
|
+
need a yes/no answer about topic existence, checking if a problem was encountered before.
|
|
849
|
+
|
|
850
|
+
Much faster than full search - use for existence checks!"""
|
|
770
851
|
return await tools.quick_search(ctx, query, min_score, project)
|
|
771
852
|
|
|
772
|
-
@mcp.tool()
|
|
853
|
+
@mcp.tool(name="csr_search_insights")
|
|
773
854
|
async def search_summary(
|
|
774
855
|
ctx: Context,
|
|
775
856
|
query: str = Field(description="The search query to find semantically similar conversations"),
|
|
776
857
|
project: Optional[str] = Field(default=None, description="Search specific project only. If not provided, searches current project based on working directory. Use 'all' to search across all projects.")
|
|
777
858
|
) -> str:
|
|
778
|
-
"""Get aggregated insights from search results
|
|
859
|
+
"""Get aggregated insights and patterns from search results.
|
|
860
|
+
|
|
861
|
+
WHEN TO USE: User wants patterns or trends, analyzing topic evolution,
|
|
862
|
+
understanding common themes, getting high-level view without details.
|
|
863
|
+
|
|
864
|
+
Provides analysis, not just search results!"""
|
|
779
865
|
return await tools.search_summary(ctx, query, project)
|
|
780
866
|
|
|
781
|
-
@mcp.tool()
|
|
867
|
+
@mcp.tool(name="csr_get_more")
|
|
782
868
|
async def get_more_results(
|
|
783
869
|
ctx: Context,
|
|
784
870
|
query: str = Field(description="The original search query"),
|
|
@@ -787,20 +873,30 @@ def register_search_tools(
|
|
|
787
873
|
min_score: float = Field(default=0.3, description="Minimum similarity score (0-1)"),
|
|
788
874
|
project: Optional[str] = Field(default=None, description="Search specific project only")
|
|
789
875
|
) -> str:
|
|
790
|
-
"""Get additional search results
|
|
876
|
+
"""Get additional search results for paginated exploration.
|
|
877
|
+
|
|
878
|
+
WHEN TO USE: User says 'show me more' after a search, initial results weren't sufficient,
|
|
879
|
+
deep diving into a topic, user wants comprehensive coverage.
|
|
880
|
+
|
|
881
|
+
Use after initial search when more context is needed!"""
|
|
791
882
|
return await tools.get_more_results(ctx, query, offset, limit, min_score, project)
|
|
792
883
|
|
|
793
|
-
@mcp.tool()
|
|
884
|
+
@mcp.tool(name="csr_search_by_file")
|
|
794
885
|
async def search_by_file(
|
|
795
886
|
ctx: Context,
|
|
796
887
|
file_path: str = Field(description="The file path to search for in conversations"),
|
|
797
888
|
limit: int = Field(default=10, description="Maximum number of results to return"),
|
|
798
889
|
project: Optional[str] = Field(default=None, description="Search specific project only. Use 'all' to search across all projects.")
|
|
799
890
|
) -> str:
|
|
800
|
-
"""
|
|
891
|
+
"""Find all conversations that analyzed or modified a specific file.
|
|
892
|
+
|
|
893
|
+
WHEN TO USE: User asks 'when did we modify X file?', investigating file history,
|
|
894
|
+
understanding why changes were made, finding discussions about specific code files.
|
|
895
|
+
|
|
896
|
+
Perfect for code archaeology and understanding file evolution!"""
|
|
801
897
|
return await tools.search_by_file(ctx, file_path, limit, project)
|
|
802
898
|
|
|
803
|
-
@mcp.tool()
|
|
899
|
+
@mcp.tool(name="csr_search_by_concept")
|
|
804
900
|
async def search_by_concept(
|
|
805
901
|
ctx: Context,
|
|
806
902
|
concept: str = Field(description="The concept to search for (e.g., 'security', 'docker', 'testing')"),
|
|
@@ -808,7 +904,12 @@ def register_search_tools(
|
|
|
808
904
|
project: Optional[str] = Field(default=None, description="Search specific project only. Use 'all' to search across all projects."),
|
|
809
905
|
include_files: bool = Field(default=True, description="Include file information in results")
|
|
810
906
|
) -> str:
|
|
811
|
-
"""Search for conversations about
|
|
907
|
+
"""Search for conversations about specific development concepts or themes.
|
|
908
|
+
|
|
909
|
+
WHEN TO USE: User asks about broad topics like 'security', 'testing', 'performance',
|
|
910
|
+
looking for all discussions on a technical theme, gathering knowledge about a concept.
|
|
911
|
+
|
|
912
|
+
Ideal for thematic analysis and knowledge gathering!"""
|
|
812
913
|
return await tools.search_by_concept(ctx, concept, limit, project, include_files)
|
|
813
914
|
|
|
814
915
|
@mcp.tool()
|