claude-code-workflow 6.2.7 → 6.3.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.
Files changed (208) hide show
  1. package/.claude/CLAUDE.md +16 -1
  2. package/.claude/workflows/cli-templates/protocols/analysis-protocol.md +11 -4
  3. package/.claude/workflows/cli-templates/protocols/write-protocol.md +10 -75
  4. package/.claude/workflows/cli-tools-usage.md +14 -24
  5. package/.codex/AGENTS.md +51 -1
  6. package/.codex/prompts/compact.md +378 -0
  7. package/.gemini/GEMINI.md +57 -20
  8. package/ccw/dist/cli.d.ts.map +1 -1
  9. package/ccw/dist/cli.js +21 -8
  10. package/ccw/dist/cli.js.map +1 -1
  11. package/ccw/dist/commands/cli.d.ts +2 -0
  12. package/ccw/dist/commands/cli.d.ts.map +1 -1
  13. package/ccw/dist/commands/cli.js +129 -8
  14. package/ccw/dist/commands/cli.js.map +1 -1
  15. package/ccw/dist/commands/hook.d.ts.map +1 -1
  16. package/ccw/dist/commands/hook.js +3 -2
  17. package/ccw/dist/commands/hook.js.map +1 -1
  18. package/ccw/dist/config/litellm-api-config-manager.d.ts +180 -0
  19. package/ccw/dist/config/litellm-api-config-manager.d.ts.map +1 -0
  20. package/ccw/dist/config/litellm-api-config-manager.js +770 -0
  21. package/ccw/dist/config/litellm-api-config-manager.js.map +1 -0
  22. package/ccw/dist/config/provider-models.d.ts +73 -0
  23. package/ccw/dist/config/provider-models.d.ts.map +1 -0
  24. package/ccw/dist/config/provider-models.js +172 -0
  25. package/ccw/dist/config/provider-models.js.map +1 -0
  26. package/ccw/dist/core/cache-manager.d.ts.map +1 -1
  27. package/ccw/dist/core/cache-manager.js +3 -5
  28. package/ccw/dist/core/cache-manager.js.map +1 -1
  29. package/ccw/dist/core/dashboard-generator.d.ts.map +1 -1
  30. package/ccw/dist/core/dashboard-generator.js +3 -1
  31. package/ccw/dist/core/dashboard-generator.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.d.ts.map +1 -1
  33. package/ccw/dist/core/routes/cli-routes.js +169 -0
  34. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  35. package/ccw/dist/core/routes/codexlens-routes.d.ts.map +1 -1
  36. package/ccw/dist/core/routes/codexlens-routes.js +234 -18
  37. package/ccw/dist/core/routes/codexlens-routes.js.map +1 -1
  38. package/ccw/dist/core/routes/hooks-routes.d.ts.map +1 -1
  39. package/ccw/dist/core/routes/hooks-routes.js +30 -32
  40. package/ccw/dist/core/routes/hooks-routes.js.map +1 -1
  41. package/ccw/dist/core/routes/litellm-api-routes.d.ts +21 -0
  42. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -0
  43. package/ccw/dist/core/routes/litellm-api-routes.js +780 -0
  44. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -0
  45. package/ccw/dist/core/routes/litellm-routes.d.ts +20 -0
  46. package/ccw/dist/core/routes/litellm-routes.d.ts.map +1 -0
  47. package/ccw/dist/core/routes/litellm-routes.js +85 -0
  48. package/ccw/dist/core/routes/litellm-routes.js.map +1 -0
  49. package/ccw/dist/core/routes/mcp-routes.js +2 -2
  50. package/ccw/dist/core/routes/mcp-routes.js.map +1 -1
  51. package/ccw/dist/core/routes/status-routes.d.ts.map +1 -1
  52. package/ccw/dist/core/routes/status-routes.js +39 -0
  53. package/ccw/dist/core/routes/status-routes.js.map +1 -1
  54. package/ccw/dist/core/routes/system-routes.js +1 -1
  55. package/ccw/dist/core/routes/system-routes.js.map +1 -1
  56. package/ccw/dist/core/server.d.ts.map +1 -1
  57. package/ccw/dist/core/server.js +15 -1
  58. package/ccw/dist/core/server.js.map +1 -1
  59. package/ccw/dist/mcp-server/index.js +1 -1
  60. package/ccw/dist/mcp-server/index.js.map +1 -1
  61. package/ccw/dist/tools/claude-cli-tools.d.ts +82 -0
  62. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -0
  63. package/ccw/dist/tools/claude-cli-tools.js +216 -0
  64. package/ccw/dist/tools/claude-cli-tools.js.map +1 -0
  65. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  66. package/ccw/dist/tools/cli-executor.js +76 -14
  67. package/ccw/dist/tools/cli-executor.js.map +1 -1
  68. package/ccw/dist/tools/codex-lens.d.ts +9 -2
  69. package/ccw/dist/tools/codex-lens.d.ts.map +1 -1
  70. package/ccw/dist/tools/codex-lens.js +114 -9
  71. package/ccw/dist/tools/codex-lens.js.map +1 -1
  72. package/ccw/dist/tools/context-cache-store.d.ts +136 -0
  73. package/ccw/dist/tools/context-cache-store.d.ts.map +1 -0
  74. package/ccw/dist/tools/context-cache-store.js +256 -0
  75. package/ccw/dist/tools/context-cache-store.js.map +1 -0
  76. package/ccw/dist/tools/context-cache.d.ts +56 -0
  77. package/ccw/dist/tools/context-cache.d.ts.map +1 -0
  78. package/ccw/dist/tools/context-cache.js +294 -0
  79. package/ccw/dist/tools/context-cache.js.map +1 -0
  80. package/ccw/dist/tools/core-memory.d.ts.map +1 -1
  81. package/ccw/dist/tools/core-memory.js +33 -19
  82. package/ccw/dist/tools/core-memory.js.map +1 -1
  83. package/ccw/dist/tools/index.d.ts.map +1 -1
  84. package/ccw/dist/tools/index.js +2 -0
  85. package/ccw/dist/tools/index.js.map +1 -1
  86. package/ccw/dist/tools/litellm-client.d.ts +85 -0
  87. package/ccw/dist/tools/litellm-client.d.ts.map +1 -0
  88. package/ccw/dist/tools/litellm-client.js +188 -0
  89. package/ccw/dist/tools/litellm-client.js.map +1 -0
  90. package/ccw/dist/tools/litellm-executor.d.ts +34 -0
  91. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -0
  92. package/ccw/dist/tools/litellm-executor.js +192 -0
  93. package/ccw/dist/tools/litellm-executor.js.map +1 -0
  94. package/ccw/dist/tools/pattern-parser.d.ts +55 -0
  95. package/ccw/dist/tools/pattern-parser.d.ts.map +1 -0
  96. package/ccw/dist/tools/pattern-parser.js +237 -0
  97. package/ccw/dist/tools/pattern-parser.js.map +1 -0
  98. package/ccw/dist/tools/smart-search.d.ts +1 -0
  99. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  100. package/ccw/dist/tools/smart-search.js +117 -41
  101. package/ccw/dist/tools/smart-search.js.map +1 -1
  102. package/ccw/dist/types/litellm-api-config.d.ts +294 -0
  103. package/ccw/dist/types/litellm-api-config.d.ts.map +1 -0
  104. package/ccw/dist/types/litellm-api-config.js +8 -0
  105. package/ccw/dist/types/litellm-api-config.js.map +1 -0
  106. package/ccw/src/cli.ts +258 -244
  107. package/ccw/src/commands/cli.ts +153 -9
  108. package/ccw/src/commands/hook.ts +3 -2
  109. package/ccw/src/config/.litellm-api-config-manager.ts.2025-12-23T11-57-43-727Z.bak +441 -0
  110. package/ccw/src/config/litellm-api-config-manager.ts +1012 -0
  111. package/ccw/src/config/provider-models.ts +222 -0
  112. package/ccw/src/core/cache-manager.ts +292 -294
  113. package/ccw/src/core/dashboard-generator.ts +3 -1
  114. package/ccw/src/core/routes/cli-routes.ts +192 -0
  115. package/ccw/src/core/routes/codexlens-routes.ts +241 -19
  116. package/ccw/src/core/routes/hooks-routes.ts +399 -405
  117. package/ccw/src/core/routes/litellm-api-routes.ts +930 -0
  118. package/ccw/src/core/routes/litellm-routes.ts +107 -0
  119. package/ccw/src/core/routes/mcp-routes.ts +1271 -1271
  120. package/ccw/src/core/routes/status-routes.ts +51 -0
  121. package/ccw/src/core/routes/system-routes.ts +1 -1
  122. package/ccw/src/core/server.ts +15 -1
  123. package/ccw/src/mcp-server/index.ts +1 -1
  124. package/ccw/src/templates/dashboard-css/12-cli-legacy.css +44 -0
  125. package/ccw/src/templates/dashboard-css/31-api-settings.css +2265 -0
  126. package/ccw/src/templates/dashboard-js/components/cli-history.js +15 -8
  127. package/ccw/src/templates/dashboard-js/components/cli-status.js +323 -9
  128. package/ccw/src/templates/dashboard-js/components/navigation.js +329 -313
  129. package/ccw/src/templates/dashboard-js/i18n.js +583 -1
  130. package/ccw/src/templates/dashboard-js/views/api-settings.js +3362 -0
  131. package/ccw/src/templates/dashboard-js/views/cli-manager.js +199 -24
  132. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +1265 -27
  133. package/ccw/src/templates/dashboard.html +840 -831
  134. package/ccw/src/tools/claude-cli-tools.ts +300 -0
  135. package/ccw/src/tools/cli-executor.ts +83 -14
  136. package/ccw/src/tools/codex-lens.ts +146 -9
  137. package/ccw/src/tools/context-cache-store.ts +368 -0
  138. package/ccw/src/tools/context-cache.ts +393 -0
  139. package/ccw/src/tools/core-memory.ts +33 -19
  140. package/ccw/src/tools/index.ts +2 -0
  141. package/ccw/src/tools/litellm-client.ts +246 -0
  142. package/ccw/src/tools/litellm-executor.ts +241 -0
  143. package/ccw/src/tools/pattern-parser.ts +329 -0
  144. package/ccw/src/tools/smart-search.ts +142 -41
  145. package/ccw/src/types/litellm-api-config.ts +402 -0
  146. package/ccw-litellm/README.md +180 -0
  147. package/ccw-litellm/pyproject.toml +35 -0
  148. package/ccw-litellm/src/ccw_litellm/__init__.py +47 -0
  149. package/ccw-litellm/src/ccw_litellm/__pycache__/__init__.cpython-313.pyc +0 -0
  150. package/ccw-litellm/src/ccw_litellm/__pycache__/cli.cpython-313.pyc +0 -0
  151. package/ccw-litellm/src/ccw_litellm/cli.py +108 -0
  152. package/ccw-litellm/src/ccw_litellm/clients/__init__.py +12 -0
  153. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/__init__.cpython-313.pyc +0 -0
  154. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  155. package/ccw-litellm/src/ccw_litellm/clients/__pycache__/litellm_llm.cpython-313.pyc +0 -0
  156. package/ccw-litellm/src/ccw_litellm/clients/litellm_embedder.py +251 -0
  157. package/ccw-litellm/src/ccw_litellm/clients/litellm_llm.py +165 -0
  158. package/ccw-litellm/src/ccw_litellm/config/__init__.py +22 -0
  159. package/ccw-litellm/src/ccw_litellm/config/__pycache__/__init__.cpython-313.pyc +0 -0
  160. package/ccw-litellm/src/ccw_litellm/config/__pycache__/loader.cpython-313.pyc +0 -0
  161. package/ccw-litellm/src/ccw_litellm/config/__pycache__/models.cpython-313.pyc +0 -0
  162. package/ccw-litellm/src/ccw_litellm/config/loader.py +316 -0
  163. package/ccw-litellm/src/ccw_litellm/config/models.py +130 -0
  164. package/ccw-litellm/src/ccw_litellm/interfaces/__init__.py +14 -0
  165. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/__init__.cpython-313.pyc +0 -0
  166. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/embedder.cpython-313.pyc +0 -0
  167. package/ccw-litellm/src/ccw_litellm/interfaces/__pycache__/llm.cpython-313.pyc +0 -0
  168. package/ccw-litellm/src/ccw_litellm/interfaces/embedder.py +52 -0
  169. package/ccw-litellm/src/ccw_litellm/interfaces/llm.py +45 -0
  170. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  171. package/codex-lens/src/codexlens/cli/__pycache__/commands.cpython-313.pyc +0 -0
  172. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  173. package/codex-lens/src/codexlens/cli/__pycache__/model_manager.cpython-313.pyc +0 -0
  174. package/codex-lens/src/codexlens/cli/__pycache__/output.cpython-313.pyc +0 -0
  175. package/codex-lens/src/codexlens/cli/commands.py +378 -23
  176. package/codex-lens/src/codexlens/cli/embedding_manager.py +660 -56
  177. package/codex-lens/src/codexlens/cli/model_manager.py +31 -18
  178. package/codex-lens/src/codexlens/cli/output.py +12 -1
  179. package/codex-lens/src/codexlens/config.py +93 -0
  180. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  181. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  182. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  183. package/codex-lens/src/codexlens/search/chain_search.py +6 -2
  184. package/codex-lens/src/codexlens/search/hybrid_search.py +44 -21
  185. package/codex-lens/src/codexlens/search/ranking.py +1 -1
  186. package/codex-lens/src/codexlens/semantic/__init__.py +42 -0
  187. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  188. package/codex-lens/src/codexlens/semantic/__pycache__/base.cpython-313.pyc +0 -0
  189. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  190. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  191. package/codex-lens/src/codexlens/semantic/__pycache__/factory.cpython-313.pyc +0 -0
  192. package/codex-lens/src/codexlens/semantic/__pycache__/gpu_support.cpython-313.pyc +0 -0
  193. package/codex-lens/src/codexlens/semantic/__pycache__/litellm_embedder.cpython-313.pyc +0 -0
  194. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  195. package/codex-lens/src/codexlens/semantic/base.py +61 -0
  196. package/codex-lens/src/codexlens/semantic/chunker.py +43 -20
  197. package/codex-lens/src/codexlens/semantic/embedder.py +60 -13
  198. package/codex-lens/src/codexlens/semantic/factory.py +98 -0
  199. package/codex-lens/src/codexlens/semantic/gpu_support.py +225 -3
  200. package/codex-lens/src/codexlens/semantic/litellm_embedder.py +144 -0
  201. package/codex-lens/src/codexlens/semantic/rotational_embedder.py +434 -0
  202. package/codex-lens/src/codexlens/semantic/vector_store.py +33 -8
  203. package/codex-lens/src/codexlens/storage/__pycache__/path_mapper.cpython-313.pyc +0 -0
  204. package/codex-lens/src/codexlens/storage/migrations/__pycache__/migration_004_dual_fts.cpython-313.pyc +0 -0
  205. package/codex-lens/src/codexlens/storage/path_mapper.py +27 -1
  206. package/package.json +15 -5
  207. package/.codex/prompts.zip +0 -0
  208. package/ccw/package.json +0 -65
@@ -79,36 +79,38 @@ def get_cache_dir() -> Path:
79
79
  """Get fastembed cache directory.
80
80
 
81
81
  Returns:
82
- Path to cache directory (usually ~/.cache/fastembed or %LOCALAPPDATA%\\Temp\\fastembed_cache)
82
+ Path to cache directory (~/.cache/huggingface or custom path)
83
83
  """
84
84
  # Check HF_HOME environment variable first
85
85
  if "HF_HOME" in os.environ:
86
86
  return Path(os.environ["HF_HOME"])
87
87
 
88
- # Default cache locations
89
- if os.name == "nt": # Windows
90
- cache_dir = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local")) / "Temp" / "fastembed_cache"
91
- else: # Unix-like
92
- cache_dir = Path.home() / ".cache" / "fastembed"
93
-
94
- return cache_dir
88
+ # fastembed 0.7.4+ uses HuggingFace cache when cache_dir is specified
89
+ # Models are stored directly under the cache directory
90
+ return Path.home() / ".cache" / "huggingface"
95
91
 
96
92
 
97
93
  def _get_model_cache_path(cache_dir: Path, info: Dict) -> Path:
98
94
  """Get the actual cache path for a model.
99
95
 
100
- fastembed uses ONNX versions of models with different names than the original.
101
- This function returns the correct path based on the cache_name field.
96
+ fastembed 0.7.4+ uses HuggingFace Hub's naming convention:
97
+ - Prefix: 'models--'
98
+ - Replace '/' with '--' in model name
99
+ Example: jinaai/jina-embeddings-v2-base-code
100
+ -> models--jinaai--jina-embeddings-v2-base-code
102
101
 
103
102
  Args:
104
- cache_dir: The fastembed cache directory
103
+ cache_dir: The fastembed cache directory (HuggingFace hub path)
105
104
  info: Model profile info dictionary
106
105
 
107
106
  Returns:
108
107
  Path to the model cache directory
109
108
  """
110
- cache_name = info.get("cache_name", info["model_name"])
111
- return cache_dir / f"models--{cache_name.replace('/', '--')}"
109
+ # HuggingFace Hub naming: models--{org}--{model}
110
+ # Use cache_name if available (for mapped ONNX models), else model_name
111
+ target_name = info.get("cache_name", info["model_name"])
112
+ sanitized_name = f"models--{target_name.replace('/', '--')}"
113
+ return cache_dir / sanitized_name
112
114
 
113
115
 
114
116
  def list_models() -> Dict[str, any]:
@@ -194,18 +196,29 @@ def download_model(profile: str, progress_callback: Optional[callable] = None) -
194
196
  model_name = info["model_name"]
195
197
 
196
198
  try:
197
- # Download model by instantiating TextEmbedding
198
- # This will automatically download to cache if not present
199
+ # Get cache directory
200
+ cache_dir = get_cache_dir()
201
+
202
+ # Download model by instantiating TextEmbedding with explicit cache_dir
203
+ # This ensures fastembed uses the correct HuggingFace Hub cache location
199
204
  if progress_callback:
200
205
  progress_callback(f"Downloading {model_name}...")
201
206
 
202
- embedder = TextEmbedding(model_name=model_name)
207
+ # CRITICAL: Must specify cache_dir to use HuggingFace cache
208
+ # and call embed() to trigger actual download
209
+ embedder = TextEmbedding(model_name=model_name, cache_dir=str(cache_dir))
210
+
211
+ # Trigger actual download by calling embed
212
+ # TextEmbedding.__init__ alone doesn't download files
213
+ if progress_callback:
214
+ progress_callback(f"Initializing {model_name}...")
215
+
216
+ list(embedder.embed(["test"])) # Trigger download
203
217
 
204
218
  if progress_callback:
205
219
  progress_callback(f"Model {model_name} downloaded successfully")
206
220
 
207
- # Get cache info using correct cache_name
208
- cache_dir = get_cache_dir()
221
+ # Get cache info using correct HuggingFace Hub path
209
222
  model_cache_path = _get_model_cache_path(cache_dir, info)
210
223
 
211
224
  cache_size = 0
@@ -35,12 +35,23 @@ def _to_jsonable(value: Any) -> Any:
35
35
  return value
36
36
 
37
37
 
38
- def print_json(*, success: bool, result: Any = None, error: str | None = None) -> None:
38
+ def print_json(*, success: bool, result: Any = None, error: str | None = None, **kwargs: Any) -> None:
39
+ """Print JSON output with optional additional fields.
40
+
41
+ Args:
42
+ success: Whether the operation succeeded
43
+ result: Result data (used when success=True)
44
+ error: Error message (used when success=False)
45
+ **kwargs: Additional fields to include in the payload (e.g., code, details)
46
+ """
39
47
  payload: dict[str, Any] = {"success": success}
40
48
  if success:
41
49
  payload["result"] = _to_jsonable(result)
42
50
  else:
43
51
  payload["error"] = error or "Unknown error"
52
+ # Include additional error details if provided
53
+ for key, value in kwargs.items():
54
+ payload[key] = _to_jsonable(value)
44
55
  console.print_json(json.dumps(payload, ensure_ascii=False))
45
56
 
46
57
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
5
6
  import os
6
7
  from dataclasses import dataclass, field
7
8
  from functools import cached_property
@@ -14,6 +15,9 @@ from .errors import ConfigError
14
15
  # Workspace-local directory name
15
16
  WORKSPACE_DIR_NAME = ".codexlens"
16
17
 
18
+ # Settings file name
19
+ SETTINGS_FILE_NAME = "settings.json"
20
+
17
21
 
18
22
  def _default_global_dir() -> Path:
19
23
  """Get global CodexLens data directory."""
@@ -89,6 +93,19 @@ class Config:
89
93
  # Hybrid chunker configuration
90
94
  hybrid_max_chunk_size: int = 2000 # Max characters per chunk before LLM refinement
91
95
  hybrid_llm_refinement: bool = False # Enable LLM-based semantic boundary refinement
96
+
97
+ # Embedding configuration
98
+ embedding_backend: str = "fastembed" # "fastembed" (local) or "litellm" (API)
99
+ embedding_model: str = "code" # For fastembed: profile (fast/code/multilingual/balanced)
100
+ # For litellm: model name from config (e.g., "qwen3-embedding")
101
+ embedding_use_gpu: bool = True # For fastembed: whether to use GPU acceleration
102
+
103
+ # Multi-endpoint configuration for litellm backend
104
+ embedding_endpoints: List[Dict[str, Any]] = field(default_factory=list)
105
+ # List of endpoint configs: [{"model": "...", "api_key": "...", "api_base": "...", "weight": 1.0}]
106
+ embedding_strategy: str = "latency_aware" # round_robin, latency_aware, weighted_random
107
+ embedding_cooldown: float = 60.0 # Default cooldown seconds for rate-limited endpoints
108
+
92
109
  def __post_init__(self) -> None:
93
110
  try:
94
111
  self.data_dir = self.data_dir.expanduser().resolve()
@@ -133,6 +150,82 @@ class Config:
133
150
  """Get parsing rules for a specific language, falling back to defaults."""
134
151
  return {**self.parsing_rules.get("default", {}), **self.parsing_rules.get(language_id, {})}
135
152
 
153
+ @cached_property
154
+ def settings_path(self) -> Path:
155
+ """Path to the settings file."""
156
+ return self.data_dir / SETTINGS_FILE_NAME
157
+
158
+ def save_settings(self) -> None:
159
+ """Save embedding and other settings to file."""
160
+ embedding_config = {
161
+ "backend": self.embedding_backend,
162
+ "model": self.embedding_model,
163
+ "use_gpu": self.embedding_use_gpu,
164
+ }
165
+ # Include multi-endpoint config if present
166
+ if self.embedding_endpoints:
167
+ embedding_config["endpoints"] = self.embedding_endpoints
168
+ embedding_config["strategy"] = self.embedding_strategy
169
+ embedding_config["cooldown"] = self.embedding_cooldown
170
+
171
+ settings = {
172
+ "embedding": embedding_config,
173
+ "llm": {
174
+ "enabled": self.llm_enabled,
175
+ "tool": self.llm_tool,
176
+ "timeout_ms": self.llm_timeout_ms,
177
+ "batch_size": self.llm_batch_size,
178
+ },
179
+ }
180
+ with open(self.settings_path, "w", encoding="utf-8") as f:
181
+ json.dump(settings, f, indent=2)
182
+
183
+ def load_settings(self) -> None:
184
+ """Load settings from file if exists."""
185
+ if not self.settings_path.exists():
186
+ return
187
+
188
+ try:
189
+ with open(self.settings_path, "r", encoding="utf-8") as f:
190
+ settings = json.load(f)
191
+
192
+ # Load embedding settings
193
+ embedding = settings.get("embedding", {})
194
+ if "backend" in embedding:
195
+ self.embedding_backend = embedding["backend"]
196
+ if "model" in embedding:
197
+ self.embedding_model = embedding["model"]
198
+ if "use_gpu" in embedding:
199
+ self.embedding_use_gpu = embedding["use_gpu"]
200
+
201
+ # Load multi-endpoint configuration
202
+ if "endpoints" in embedding:
203
+ self.embedding_endpoints = embedding["endpoints"]
204
+ if "strategy" in embedding:
205
+ self.embedding_strategy = embedding["strategy"]
206
+ if "cooldown" in embedding:
207
+ self.embedding_cooldown = embedding["cooldown"]
208
+
209
+ # Load LLM settings
210
+ llm = settings.get("llm", {})
211
+ if "enabled" in llm:
212
+ self.llm_enabled = llm["enabled"]
213
+ if "tool" in llm:
214
+ self.llm_tool = llm["tool"]
215
+ if "timeout_ms" in llm:
216
+ self.llm_timeout_ms = llm["timeout_ms"]
217
+ if "batch_size" in llm:
218
+ self.llm_batch_size = llm["batch_size"]
219
+ except Exception:
220
+ pass # Silently ignore errors
221
+
222
+ @classmethod
223
+ def load(cls) -> "Config":
224
+ """Load config with settings from file."""
225
+ config = cls()
226
+ config.load_settings()
227
+ return config
228
+
136
229
 
137
230
  @dataclass
138
231
  class WorkspaceConfig:
@@ -494,9 +494,13 @@ class ChainSearchEngine:
494
494
  else:
495
495
  # Use fuzzy FTS if enable_fuzzy=True (mode="fuzzy"), otherwise exact FTS
496
496
  if enable_fuzzy:
497
- fts_results = store.search_fts_fuzzy(query, limit=limit)
497
+ fts_results = store.search_fts_fuzzy(
498
+ query, limit=limit, return_full_content=True
499
+ )
498
500
  else:
499
- fts_results = store.search_fts(query, limit=limit)
501
+ fts_results = store.search_fts_exact(
502
+ query, limit=limit, return_full_content=True
503
+ )
500
504
 
501
505
  # Optionally add semantic keyword results
502
506
  if include_semantic:
@@ -27,11 +27,11 @@ class HybridSearchEngine:
27
27
  default_weights: Default RRF weights for each source
28
28
  """
29
29
 
30
- # Default RRF weights (exact: 40%, fuzzy: 30%, vector: 30%)
30
+ # Default RRF weights (vector: 60%, exact: 30%, fuzzy: 10%)
31
31
  DEFAULT_WEIGHTS = {
32
- "exact": 0.4,
33
- "fuzzy": 0.3,
34
- "vector": 0.3,
32
+ "exact": 0.3,
33
+ "fuzzy": 0.1,
34
+ "vector": 0.6,
35
35
  }
36
36
 
37
37
  def __init__(self, weights: Optional[Dict[str, float]] = None):
@@ -200,7 +200,9 @@ class HybridSearchEngine:
200
200
  """
201
201
  try:
202
202
  with DirIndexStore(index_path) as store:
203
- return store.search_fts_exact(query, limit=limit)
203
+ return store.search_fts_exact(
204
+ query, limit=limit, return_full_content=True
205
+ )
204
206
  except Exception as exc:
205
207
  self.logger.debug("Exact search error: %s", exc)
206
208
  return []
@@ -220,7 +222,9 @@ class HybridSearchEngine:
220
222
  """
221
223
  try:
222
224
  with DirIndexStore(index_path) as store:
223
- return store.search_fts_fuzzy(query, limit=limit)
225
+ return store.search_fts_fuzzy(
226
+ query, limit=limit, return_full_content=True
227
+ )
224
228
  except Exception as exc:
225
229
  self.logger.debug("Fuzzy search error: %s", exc)
226
230
  return []
@@ -260,7 +264,7 @@ class HybridSearchEngine:
260
264
  return []
261
265
 
262
266
  # Initialize embedder and vector store
263
- from codexlens.semantic.embedder import get_embedder
267
+ from codexlens.semantic.factory import get_embedder
264
268
  from codexlens.semantic.vector_store import VectorStore
265
269
 
266
270
  vector_store = VectorStore(index_path)
@@ -277,32 +281,51 @@ class HybridSearchEngine:
277
281
  # Get stored model configuration (preferred) or auto-detect from dimension
278
282
  model_config = vector_store.get_model_config()
279
283
  if model_config:
280
- profile = model_config["model_profile"]
284
+ backend = model_config.get("backend", "fastembed")
285
+ model_name = model_config["model_name"]
286
+ model_profile = model_config["model_profile"]
281
287
  self.logger.debug(
282
- "Using stored model config: %s (%s, %dd)",
283
- profile, model_config["model_name"], model_config["embedding_dim"]
288
+ "Using stored model config: %s backend, %s (%s, %dd)",
289
+ backend, model_profile, model_name, model_config["embedding_dim"]
284
290
  )
291
+
292
+ # Get embedder based on backend
293
+ if backend == "litellm":
294
+ embedder = get_embedder(backend="litellm", model=model_name)
295
+ else:
296
+ embedder = get_embedder(backend="fastembed", profile=model_profile)
285
297
  else:
286
298
  # Fallback: auto-detect from embedding dimension
287
299
  detected_dim = vector_store.dimension
288
300
  if detected_dim is None:
289
301
  self.logger.info("Vector store dimension unknown, using default profile")
290
- profile = "code" # Default fallback
302
+ embedder = get_embedder(backend="fastembed", profile="code")
291
303
  elif detected_dim == 384:
292
- profile = "fast"
304
+ embedder = get_embedder(backend="fastembed", profile="fast")
293
305
  elif detected_dim == 768:
294
- profile = "code"
306
+ embedder = get_embedder(backend="fastembed", profile="code")
295
307
  elif detected_dim == 1024:
296
- profile = "multilingual" # or balanced, both are 1024
308
+ embedder = get_embedder(backend="fastembed", profile="multilingual")
309
+ elif detected_dim == 1536:
310
+ # Likely OpenAI text-embedding-3-small or ada-002
311
+ self.logger.info(
312
+ "Detected 1536-dim embeddings (likely OpenAI), using litellm backend with text-embedding-3-small"
313
+ )
314
+ embedder = get_embedder(backend="litellm", model="text-embedding-3-small")
315
+ elif detected_dim == 3072:
316
+ # Likely OpenAI text-embedding-3-large
317
+ self.logger.info(
318
+ "Detected 3072-dim embeddings (likely OpenAI), using litellm backend with text-embedding-3-large"
319
+ )
320
+ embedder = get_embedder(backend="litellm", model="text-embedding-3-large")
297
321
  else:
298
- profile = "code" # Default fallback
299
- self.logger.debug(
300
- "No stored model config, auto-detected profile '%s' from dimension %s",
301
- profile, detected_dim
302
- )
322
+ self.logger.debug(
323
+ "Unknown dimension %s, using default fastembed profile 'code'",
324
+ detected_dim
325
+ )
326
+ embedder = get_embedder(backend="fastembed", profile="code")
327
+
303
328
 
304
- # Use cached embedder (singleton) for performance
305
- embedder = get_embedder(profile=profile)
306
329
 
307
330
  # Generate query embedding
308
331
  query_embedding = embedder.embed_single(query)
@@ -25,7 +25,7 @@ def reciprocal_rank_fusion(
25
25
  results_map: Dictionary mapping source name to list of SearchResult objects
26
26
  Sources: 'exact', 'fuzzy', 'vector'
27
27
  weights: Dictionary mapping source name to weight (default: equal weights)
28
- Example: {'exact': 0.4, 'fuzzy': 0.3, 'vector': 0.3}
28
+ Example: {'exact': 0.3, 'fuzzy': 0.1, 'vector': 0.6}
29
29
  k: Constant to avoid division by zero and control rank influence (default 60)
30
30
 
31
31
  Returns:
@@ -14,6 +14,7 @@ from __future__ import annotations
14
14
  SEMANTIC_AVAILABLE = False
15
15
  SEMANTIC_BACKEND: str | None = None
16
16
  GPU_AVAILABLE = False
17
+ LITELLM_AVAILABLE = False
17
18
  _import_error: str | None = None
18
19
 
19
20
 
@@ -67,10 +68,51 @@ def check_gpu_available() -> tuple[bool, str]:
67
68
  return False, "GPU support module not available"
68
69
 
69
70
 
71
+ # Export embedder components
72
+ # BaseEmbedder is always available (abstract base class)
73
+ from .base import BaseEmbedder
74
+
75
+ # Factory function for creating embedders
76
+ from .factory import get_embedder as get_embedder_factory
77
+
78
+ # Optional: LiteLLMEmbedderWrapper (only if ccw-litellm is installed)
79
+ try:
80
+ import ccw_litellm # noqa: F401
81
+ from .litellm_embedder import LiteLLMEmbedderWrapper
82
+ LITELLM_AVAILABLE = True
83
+ except ImportError:
84
+ LiteLLMEmbedderWrapper = None
85
+ LITELLM_AVAILABLE = False
86
+
87
+
88
+ def is_embedding_backend_available(backend: str) -> tuple[bool, str | None]:
89
+ """Check whether a specific embedding backend can be used.
90
+
91
+ Notes:
92
+ - "fastembed" requires the optional semantic deps (pip install codexlens[semantic]).
93
+ - "litellm" requires ccw-litellm to be installed in the same environment.
94
+ """
95
+ backend = (backend or "").strip().lower()
96
+ if backend == "fastembed":
97
+ if SEMANTIC_AVAILABLE:
98
+ return True, None
99
+ return False, _import_error or "fastembed not available. Install with: pip install codexlens[semantic]"
100
+ if backend == "litellm":
101
+ if LITELLM_AVAILABLE:
102
+ return True, None
103
+ return False, "ccw-litellm not available. Install with: pip install ccw-litellm"
104
+ return False, f"Invalid embedding backend: {backend}. Must be 'fastembed' or 'litellm'."
105
+
106
+
70
107
  __all__ = [
71
108
  "SEMANTIC_AVAILABLE",
72
109
  "SEMANTIC_BACKEND",
73
110
  "GPU_AVAILABLE",
111
+ "LITELLM_AVAILABLE",
74
112
  "check_semantic_available",
113
+ "is_embedding_backend_available",
75
114
  "check_gpu_available",
115
+ "BaseEmbedder",
116
+ "get_embedder_factory",
117
+ "LiteLLMEmbedderWrapper",
76
118
  ]
@@ -0,0 +1,61 @@
1
+ """Base class for embedders.
2
+
3
+ Defines the interface that all embedders must implement.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import Iterable
10
+
11
+ import numpy as np
12
+
13
+
14
+ class BaseEmbedder(ABC):
15
+ """Base class for all embedders.
16
+
17
+ All embedder implementations must inherit from this class and implement
18
+ the abstract methods to ensure a consistent interface.
19
+ """
20
+
21
+ @property
22
+ @abstractmethod
23
+ def embedding_dim(self) -> int:
24
+ """Return embedding dimensions.
25
+
26
+ Returns:
27
+ int: Dimension of the embedding vectors.
28
+ """
29
+ ...
30
+
31
+ @property
32
+ @abstractmethod
33
+ def model_name(self) -> str:
34
+ """Return model name.
35
+
36
+ Returns:
37
+ str: Name or identifier of the underlying model.
38
+ """
39
+ ...
40
+
41
+ @property
42
+ def max_tokens(self) -> int:
43
+ """Return maximum token limit for embeddings.
44
+
45
+ Returns:
46
+ int: Maximum number of tokens that can be embedded at once.
47
+ Default is 8192 if not overridden by implementation.
48
+ """
49
+ return 8192
50
+
51
+ @abstractmethod
52
+ def embed_to_numpy(self, texts: str | Iterable[str]) -> np.ndarray:
53
+ """Embed texts to numpy array.
54
+
55
+ Args:
56
+ texts: Single text or iterable of texts to embed.
57
+
58
+ Returns:
59
+ numpy.ndarray: Array of shape (n_texts, embedding_dim) containing embeddings.
60
+ """
61
+ ...
@@ -39,7 +39,7 @@ from codexlens.parsers.tokenizer import get_default_tokenizer
39
39
  class ChunkConfig:
40
40
  """Configuration for chunking strategies."""
41
41
  max_chunk_size: int = 1000 # Max characters per chunk
42
- overlap: int = 100 # Overlap for sliding window
42
+ overlap: int = 200 # Overlap for sliding window (increased from 100 for better context)
43
43
  strategy: str = "auto" # Chunking strategy: auto, symbol, sliding_window, hybrid
44
44
  min_chunk_size: int = 50 # Minimum chunk size
45
45
  skip_token_count: bool = False # Skip expensive token counting (use char/4 estimate)
@@ -80,6 +80,7 @@ class Chunker:
80
80
  """Chunk code by extracted symbols (functions, classes).
81
81
 
82
82
  Each symbol becomes one chunk with its full content.
83
+ Large symbols exceeding max_chunk_size are recursively split using sliding window.
83
84
 
84
85
  Args:
85
86
  content: Source code content
@@ -101,27 +102,49 @@ class Chunker:
101
102
  if len(chunk_content.strip()) < self.config.min_chunk_size:
102
103
  continue
103
104
 
104
- # Calculate token count if not provided
105
- token_count = None
106
- if symbol_token_counts and symbol.name in symbol_token_counts:
107
- token_count = symbol_token_counts[symbol.name]
105
+ # Check if symbol content exceeds max_chunk_size
106
+ if len(chunk_content) > self.config.max_chunk_size:
107
+ # Create line mapping for correct line number tracking
108
+ line_mapping = list(range(start_line, end_line + 1))
109
+
110
+ # Use sliding window to split large symbol
111
+ sub_chunks = self.chunk_sliding_window(
112
+ chunk_content,
113
+ file_path=file_path,
114
+ language=language,
115
+ line_mapping=line_mapping
116
+ )
117
+
118
+ # Update sub_chunks with parent symbol metadata
119
+ for sub_chunk in sub_chunks:
120
+ sub_chunk.metadata["symbol_name"] = symbol.name
121
+ sub_chunk.metadata["symbol_kind"] = symbol.kind
122
+ sub_chunk.metadata["strategy"] = "symbol_split"
123
+ sub_chunk.metadata["parent_symbol_range"] = (start_line, end_line)
124
+
125
+ chunks.extend(sub_chunks)
108
126
  else:
109
- token_count = self._estimate_token_count(chunk_content)
127
+ # Calculate token count if not provided
128
+ token_count = None
129
+ if symbol_token_counts and symbol.name in symbol_token_counts:
130
+ token_count = symbol_token_counts[symbol.name]
131
+ else:
132
+ token_count = self._estimate_token_count(chunk_content)
110
133
 
111
- chunks.append(SemanticChunk(
112
- content=chunk_content,
113
- embedding=None,
114
- metadata={
115
- "file": str(file_path),
116
- "language": language,
117
- "symbol_name": symbol.name,
118
- "symbol_kind": symbol.kind,
119
- "start_line": start_line,
120
- "end_line": end_line,
121
- "strategy": "symbol",
122
- "token_count": token_count,
123
- }
124
- ))
134
+ chunks.append(SemanticChunk(
135
+ content=chunk_content,
136
+ embedding=None,
137
+ metadata={
138
+ "file": str(file_path),
139
+ "language": language,
140
+ "symbol_name": symbol.name,
141
+ "symbol_kind": symbol.kind,
142
+ "start_line": start_line,
143
+ "end_line": end_line,
144
+ "strategy": "symbol",
145
+ "token_count": token_count,
146
+ }
147
+ ))
125
148
 
126
149
  return chunks
127
150