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
@@ -36,10 +36,12 @@ const ParamsSchema = z.object({
36
36
  path: z.string().optional(),
37
37
  paths: z.array(z.string()).default([]),
38
38
  contextLines: z.number().default(0),
39
- maxResults: z.number().default(20), // Increased default
39
+ maxResults: z.number().default(5), // Default 5 with full content
40
40
  includeHidden: z.boolean().default(false),
41
41
  languages: z.array(z.string()).optional(),
42
- limit: z.number().default(20), // Increased default
42
+ limit: z.number().default(5), // Default 5 with full content
43
+ extraFilesCount: z.number().default(10), // Additional file-only results
44
+ maxContentLength: z.number().default(200), // Max content length for truncation (50-2000)
43
45
  offset: z.number().default(0), // NEW: Pagination offset (start_index)
44
46
  enrich: z.boolean().default(false),
45
47
  // Search modifiers for ripgrep mode
@@ -268,6 +270,7 @@ interface SearchMetadata {
268
270
  interface SearchResult {
269
271
  success: boolean;
270
272
  results?: ExactMatch[] | SemanticMatch[] | GraphMatch[] | FileMatch[] | unknown;
273
+ extra_files?: string[]; // Additional file paths without content
271
274
  output?: string;
272
275
  metadata?: SearchMetadata;
273
276
  error?: string;
@@ -275,11 +278,22 @@ interface SearchResult {
275
278
  message?: string;
276
279
  }
277
280
 
281
+ interface ModelInfo {
282
+ model_profile?: string;
283
+ model_name?: string;
284
+ embedding_dim?: number;
285
+ backend?: string;
286
+ created_at?: string;
287
+ updated_at?: string;
288
+ }
289
+
278
290
  interface IndexStatus {
279
291
  indexed: boolean;
280
292
  has_embeddings: boolean;
281
293
  file_count?: number;
282
294
  embeddings_coverage_percent?: number;
295
+ total_chunks?: number;
296
+ model_info?: ModelInfo | null;
283
297
  warning?: string;
284
298
  }
285
299
 
@@ -290,6 +304,42 @@ function stripAnsi(str: string): string {
290
304
  return str.replace(/\x1b\[[0-9;]*m/g, '');
291
305
  }
292
306
 
307
+ /** Default maximum content length to return (avoid excessive output) */
308
+ const DEFAULT_MAX_CONTENT_LENGTH = 200;
309
+
310
+ /**
311
+ * Truncate content to specified length with ellipsis
312
+ * @param content - The content to truncate
313
+ * @param maxLength - Maximum length (default: 200)
314
+ */
315
+ function truncateContent(content: string | null | undefined, maxLength: number = DEFAULT_MAX_CONTENT_LENGTH): string {
316
+ if (!content) return '';
317
+ if (content.length <= maxLength) return content;
318
+ return content.slice(0, maxLength) + '...';
319
+ }
320
+
321
+ /**
322
+ * Split results into full content results and extra file-only results
323
+ * Generic function supporting both SemanticMatch and ExactMatch types
324
+ * @param allResults - All search results (must have 'file' property)
325
+ * @param fullContentLimit - Number of results with full content (default: 5)
326
+ * @param extraFilesCount - Number of additional file-only results (default: 10)
327
+ */
328
+ function splitResultsWithExtraFiles<T extends { file: string }>(
329
+ allResults: T[],
330
+ fullContentLimit: number = 5,
331
+ extraFilesCount: number = 10
332
+ ): { results: T[]; extra_files: string[] } {
333
+ // First N results with full content
334
+ const results = allResults.slice(0, fullContentLimit);
335
+
336
+ // Next M results as file paths only (deduplicated)
337
+ const extraResults = allResults.slice(fullContentLimit, fullContentLimit + extraFilesCount);
338
+ const extra_files = [...new Set(extraResults.map(r => r.file))];
339
+
340
+ return { results, extra_files };
341
+ }
342
+
293
343
  /**
294
344
  * Check if CodexLens index exists for current directory
295
345
  * @param path - Directory path to check
@@ -320,6 +370,18 @@ async function checkIndexStatus(path: string = '.'): Promise<IndexStatus> {
320
370
  const embeddingsData = status.embeddings || {};
321
371
  const embeddingsCoverage = embeddingsData.coverage_percent || 0;
322
372
  const has_embeddings = embeddingsCoverage >= 50; // Threshold: 50%
373
+ const totalChunks = embeddingsData.total_chunks || 0;
374
+
375
+ // Extract model info if available
376
+ const modelInfoData = embeddingsData.model_info;
377
+ const modelInfo: ModelInfo | undefined = modelInfoData ? {
378
+ model_profile: modelInfoData.model_profile,
379
+ model_name: modelInfoData.model_name,
380
+ embedding_dim: modelInfoData.embedding_dim,
381
+ backend: modelInfoData.backend,
382
+ created_at: modelInfoData.created_at,
383
+ updated_at: modelInfoData.updated_at,
384
+ } : undefined;
323
385
 
324
386
  let warning: string | undefined;
325
387
  if (!indexed) {
@@ -335,6 +397,9 @@ async function checkIndexStatus(path: string = '.'): Promise<IndexStatus> {
335
397
  has_embeddings,
336
398
  file_count: status.total_files,
337
399
  embeddings_coverage_percent: embeddingsCoverage,
400
+ total_chunks: totalChunks,
401
+ // Ensure model_info is null instead of undefined so it's included in JSON
402
+ model_info: modelInfo ?? null,
338
403
  warning,
339
404
  };
340
405
  } catch {
@@ -688,7 +753,7 @@ async function executeAutoMode(params: Params): Promise<SearchResult> {
688
753
  * Supports tokenized multi-word queries with OR matching and result ranking
689
754
  */
690
755
  async function executeRipgrepMode(params: Params): Promise<SearchResult> {
691
- const { query, paths = [], contextLines = 0, maxResults = 10, includeHidden = false, path = '.', regex = true, caseSensitive = true, tokenize = true } = params;
756
+ const { query, paths = [], contextLines = 0, maxResults = 5, extraFilesCount = 10, maxContentLength = 200, includeHidden = false, path = '.', regex = true, caseSensitive = true, tokenize = true } = params;
692
757
 
693
758
  if (!query) {
694
759
  return {
@@ -700,6 +765,9 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
700
765
  // Check if ripgrep is available
701
766
  const hasRipgrep = checkToolAvailability('rg');
702
767
 
768
+ // Calculate total to fetch for split (full content + extra files)
769
+ const totalToFetch = maxResults + extraFilesCount;
770
+
703
771
  // If ripgrep not available, fall back to CodexLens exact mode
704
772
  if (!hasRipgrep) {
705
773
  const readyStatus = await ensureCodexLensReady();
@@ -711,7 +779,7 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
711
779
  }
712
780
 
713
781
  // Use CodexLens exact mode as fallback
714
- const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
782
+ const args = ['search', query, '--limit', totalToFetch.toString(), '--mode', 'exact', '--json'];
715
783
  const result = await executeCodexLens(args, { cwd: path });
716
784
 
717
785
  if (!result.success) {
@@ -728,23 +796,27 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
728
796
  }
729
797
 
730
798
  // Parse results
731
- let results: SemanticMatch[] = [];
799
+ let allResults: SemanticMatch[] = [];
732
800
  try {
733
801
  const parsed = JSON.parse(stripAnsi(result.output || '{}'));
734
802
  const data = parsed.result?.results || parsed.results || parsed;
735
- results = (Array.isArray(data) ? data : []).map((item: any) => ({
803
+ allResults = (Array.isArray(data) ? data : []).map((item: any) => ({
736
804
  file: item.path || item.file,
737
805
  score: item.score || 0,
738
- content: item.excerpt || item.content || '',
806
+ content: truncateContent(item.content || item.excerpt, maxContentLength),
739
807
  symbol: item.symbol || null,
740
808
  }));
741
809
  } catch {
742
810
  // Keep empty results
743
811
  }
744
812
 
813
+ // Split results: first N with full content, rest as file paths only
814
+ const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
815
+
745
816
  return {
746
817
  success: true,
747
818
  results,
819
+ extra_files: extra_files.length > 0 ? extra_files : undefined,
748
820
  metadata: {
749
821
  mode: 'ripgrep',
750
822
  backend: 'codexlens-fallback',
@@ -755,12 +827,12 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
755
827
  };
756
828
  }
757
829
 
758
- // Use ripgrep
830
+ // Use ripgrep - request more results to support split
759
831
  const { command, args, tokens } = buildRipgrepCommand({
760
832
  query,
761
833
  paths: paths.length > 0 ? paths : [path],
762
834
  contextLines,
763
- maxResults,
835
+ maxResults: totalToFetch, // Fetch more to support split
764
836
  includeHidden,
765
837
  regex,
766
838
  caseSensitive,
@@ -786,14 +858,14 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
786
858
  });
787
859
 
788
860
  child.on('close', (code) => {
789
- const results: ExactMatch[] = [];
861
+ const allResults: ExactMatch[] = [];
790
862
  const lines = stdout.split('\n').filter((line) => line.trim());
791
863
  // Limit total results to prevent memory overflow (--max-count only limits per-file)
792
- const effectiveLimit = maxResults > 0 ? maxResults : 500;
864
+ const effectiveLimit = totalToFetch > 0 ? totalToFetch : 500;
793
865
 
794
866
  for (const line of lines) {
795
867
  // Stop collecting if we've reached the limit
796
- if (results.length >= effectiveLimit) {
868
+ if (allResults.length >= effectiveLimit) {
797
869
  resultLimitReached = true;
798
870
  break;
799
871
  }
@@ -811,7 +883,7 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
811
883
  : 1,
812
884
  content: item.data.lines.text.trim(),
813
885
  };
814
- results.push(match);
886
+ allResults.push(match);
815
887
  }
816
888
  } catch {
817
889
  continue;
@@ -824,9 +896,12 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
824
896
 
825
897
  // Apply token-based scoring and sorting for multi-word queries
826
898
  // Results matching more tokens are ranked higher (exact matches first)
827
- const scoredResults = tokens.length > 1 ? scoreByTokenMatch(results, tokens) : results;
899
+ const scoredResults = tokens.length > 1 ? scoreByTokenMatch(allResults, tokens) : allResults;
828
900
 
829
901
  if (code === 0 || code === 1 || (isWindowsDeviceError && scoredResults.length > 0)) {
902
+ // Split results: first N with full content, rest as file paths only
903
+ const { results, extra_files } = splitResultsWithExtraFiles(scoredResults, maxResults, extraFilesCount);
904
+
830
905
  // Build warning message for various conditions
831
906
  const warnings: string[] = [];
832
907
  if (resultLimitReached) {
@@ -838,18 +913,19 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
838
913
 
839
914
  resolve({
840
915
  success: true,
841
- results: scoredResults,
916
+ results,
917
+ extra_files: extra_files.length > 0 ? extra_files : undefined,
842
918
  metadata: {
843
919
  mode: 'ripgrep',
844
920
  backend: 'ripgrep',
845
- count: scoredResults.length,
921
+ count: results.length,
846
922
  query,
847
923
  tokens: tokens.length > 1 ? tokens : undefined, // Include tokens in metadata for debugging
848
924
  tokenized: tokens.length > 1,
849
925
  ...(warnings.length > 0 && { warning: warnings.join('; ') }),
850
926
  },
851
927
  });
852
- } else if (isWindowsDeviceError && results.length === 0) {
928
+ } else if (isWindowsDeviceError && allResults.length === 0) {
853
929
  // Windows device error but no results - might be the only issue
854
930
  resolve({
855
931
  success: true,
@@ -886,7 +962,7 @@ async function executeRipgrepMode(params: Params): Promise<SearchResult> {
886
962
  * Requires index
887
963
  */
888
964
  async function executeCodexLensExactMode(params: Params): Promise<SearchResult> {
889
- const { query, path = '.', maxResults = 10, enrich = false } = params;
965
+ const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false } = params;
890
966
 
891
967
  if (!query) {
892
968
  return {
@@ -907,7 +983,9 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
907
983
  // Check index status
908
984
  const indexStatus = await checkIndexStatus(path);
909
985
 
910
- const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'exact', '--json'];
986
+ // Request more results to support split (full content + extra files)
987
+ const totalToFetch = maxResults + extraFilesCount;
988
+ const args = ['search', query, '--limit', totalToFetch.toString(), '--mode', 'exact', '--json'];
911
989
  if (enrich) {
912
990
  args.push('--enrich');
913
991
  }
@@ -928,14 +1006,14 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
928
1006
  }
929
1007
 
930
1008
  // Parse results
931
- let results: SemanticMatch[] = [];
1009
+ let allResults: SemanticMatch[] = [];
932
1010
  try {
933
1011
  const parsed = JSON.parse(stripAnsi(result.output || '{}'));
934
1012
  const data = parsed.result?.results || parsed.results || parsed;
935
- results = (Array.isArray(data) ? data : []).map((item: any) => ({
1013
+ allResults = (Array.isArray(data) ? data : []).map((item: any) => ({
936
1014
  file: item.path || item.file,
937
1015
  score: item.score || 0,
938
- content: item.excerpt || item.content || '',
1016
+ content: truncateContent(item.content || item.excerpt, maxContentLength),
939
1017
  symbol: item.symbol || null,
940
1018
  }));
941
1019
  } catch {
@@ -943,8 +1021,8 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
943
1021
  }
944
1022
 
945
1023
  // Fallback to fuzzy mode if exact returns no results
946
- if (results.length === 0) {
947
- const fuzzyArgs = ['search', query, '--limit', maxResults.toString(), '--mode', 'fuzzy', '--json'];
1024
+ if (allResults.length === 0) {
1025
+ const fuzzyArgs = ['search', query, '--limit', totalToFetch.toString(), '--mode', 'fuzzy', '--json'];
948
1026
  if (enrich) {
949
1027
  fuzzyArgs.push('--enrich');
950
1028
  }
@@ -954,20 +1032,23 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
954
1032
  try {
955
1033
  const parsed = JSON.parse(stripAnsi(fuzzyResult.output || '{}'));
956
1034
  const data = parsed.result?.results || parsed.results || parsed;
957
- results = (Array.isArray(data) ? data : []).map((item: any) => ({
1035
+ allResults = (Array.isArray(data) ? data : []).map((item: any) => ({
958
1036
  file: item.path || item.file,
959
1037
  score: item.score || 0,
960
- content: item.excerpt || item.content || '',
1038
+ content: truncateContent(item.content || item.excerpt, maxContentLength),
961
1039
  symbol: item.symbol || null,
962
1040
  }));
963
1041
  } catch {
964
1042
  // Keep empty results
965
1043
  }
966
1044
 
967
- if (results.length > 0) {
1045
+ if (allResults.length > 0) {
1046
+ // Split results: first N with full content, rest as file paths only
1047
+ const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
968
1048
  return {
969
1049
  success: true,
970
1050
  results,
1051
+ extra_files: extra_files.length > 0 ? extra_files : undefined,
971
1052
  metadata: {
972
1053
  mode: 'exact',
973
1054
  backend: 'codexlens',
@@ -982,9 +1063,13 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
982
1063
  }
983
1064
  }
984
1065
 
1066
+ // Split results: first N with full content, rest as file paths only
1067
+ const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
1068
+
985
1069
  return {
986
1070
  success: true,
987
1071
  results,
1072
+ extra_files: extra_files.length > 0 ? extra_files : undefined,
988
1073
  metadata: {
989
1074
  mode: 'exact',
990
1075
  backend: 'codexlens',
@@ -1001,7 +1086,7 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
1001
1086
  * Requires index with embeddings
1002
1087
  */
1003
1088
  async function executeHybridMode(params: Params): Promise<SearchResult> {
1004
- const { query, path = '.', maxResults = 10, enrich = false } = params;
1089
+ const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false } = params;
1005
1090
 
1006
1091
  if (!query) {
1007
1092
  return {
@@ -1022,7 +1107,9 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1022
1107
  // Check index status
1023
1108
  const indexStatus = await checkIndexStatus(path);
1024
1109
 
1025
- const args = ['search', query, '--limit', maxResults.toString(), '--mode', 'hybrid', '--json'];
1110
+ // Request more results to support split (full content + extra files)
1111
+ const totalToFetch = maxResults + extraFilesCount;
1112
+ const args = ['search', query, '--limit', totalToFetch.toString(), '--mode', 'hybrid', '--json'];
1026
1113
  if (enrich) {
1027
1114
  args.push('--enrich');
1028
1115
  }
@@ -1043,14 +1130,14 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1043
1130
  }
1044
1131
 
1045
1132
  // Parse results
1046
- let results: SemanticMatch[] = [];
1133
+ let allResults: SemanticMatch[] = [];
1047
1134
  let baselineInfo: { score: number; count: number } | null = null;
1048
1135
  let initialCount = 0;
1049
1136
 
1050
1137
  try {
1051
1138
  const parsed = JSON.parse(stripAnsi(result.output || '{}'));
1052
1139
  const data = parsed.result?.results || parsed.results || parsed;
1053
- results = (Array.isArray(data) ? data : []).map((item: any) => {
1140
+ allResults = (Array.isArray(data) ? data : []).map((item: any) => {
1054
1141
  const rawScore = item.score || 0;
1055
1142
  // Hybrid mode returns distance scores (lower is better).
1056
1143
  // Convert to similarity scores (higher is better) for consistency.
@@ -1059,27 +1146,27 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1059
1146
  return {
1060
1147
  file: item.path || item.file,
1061
1148
  score: similarityScore,
1062
- content: item.excerpt || item.content || '',
1149
+ content: truncateContent(item.content || item.excerpt, maxContentLength),
1063
1150
  symbol: item.symbol || null,
1064
1151
  };
1065
1152
  });
1066
1153
 
1067
- initialCount = results.length;
1154
+ initialCount = allResults.length;
1068
1155
 
1069
1156
  // Post-processing pipeline to improve semantic search quality
1070
1157
  // 0. Filter dominant baseline scores (hot spot detection)
1071
- const baselineResult = filterDominantBaselineScores(results);
1072
- results = baselineResult.filteredResults;
1158
+ const baselineResult = filterDominantBaselineScores(allResults);
1159
+ allResults = baselineResult.filteredResults;
1073
1160
  baselineInfo = baselineResult.baselineInfo;
1074
1161
 
1075
1162
  // 1. Filter noisy files (coverage, node_modules, etc.)
1076
- results = filterNoisyFiles(results);
1163
+ allResults = filterNoisyFiles(allResults);
1077
1164
  // 2. Boost results containing query keywords
1078
- results = applyKeywordBoosting(results, query);
1165
+ allResults = applyKeywordBoosting(allResults, query);
1079
1166
  // 3. Enforce score diversity (penalize identical scores)
1080
- results = enforceScoreDiversity(results);
1167
+ allResults = enforceScoreDiversity(allResults);
1081
1168
  // 4. Re-sort by adjusted scores
1082
- results.sort((a, b) => b.score - a.score);
1169
+ allResults.sort((a, b) => b.score - a.score);
1083
1170
  } catch {
1084
1171
  return {
1085
1172
  success: true,
@@ -1095,15 +1182,19 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1095
1182
  };
1096
1183
  }
1097
1184
 
1185
+ // Split results: first N with full content, rest as file paths only
1186
+ const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
1187
+
1098
1188
  // Build metadata with baseline info if detected
1099
1189
  let note = 'Hybrid mode uses RRF fusion (exact + fuzzy + vector) for best results';
1100
1190
  if (baselineInfo) {
1101
- note += ` | Filtered ${initialCount - results.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
1191
+ note += ` | Filtered ${initialCount - allResults.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
1102
1192
  }
1103
1193
 
1104
1194
  return {
1105
1195
  success: true,
1106
1196
  results,
1197
+ extra_files: extra_files.length > 0 ? extra_files : undefined,
1107
1198
  metadata: {
1108
1199
  mode: 'hybrid',
1109
1200
  backend: 'codexlens',
@@ -1514,7 +1605,7 @@ export const schema: ToolSchema = {
1514
1605
  mode: {
1515
1606
  type: 'string',
1516
1607
  enum: SEARCH_MODES,
1517
- description: 'Search mode: auto (default), hybrid (best quality), exact (CodexLens FTS), ripgrep (fast, no index), priority (fallback: hybrid->exact->ripgrep)',
1608
+ description: 'Search mode: auto, hybrid (best quality), exact (CodexLens FTS), ripgrep (fast, no index), priority (fallback chain)',
1518
1609
  default: 'auto',
1519
1610
  },
1520
1611
  output_mode: {
@@ -1550,6 +1641,16 @@ export const schema: ToolSchema = {
1550
1641
  description: 'Alias for maxResults (default: 20)',
1551
1642
  default: 20,
1552
1643
  },
1644
+ extraFilesCount: {
1645
+ type: 'number',
1646
+ description: 'Number of additional file-only results (paths without content)',
1647
+ default: 10,
1648
+ },
1649
+ maxContentLength: {
1650
+ type: 'number',
1651
+ description: 'Maximum content length for truncation (50-2000)',
1652
+ default: 200,
1653
+ },
1553
1654
  offset: {
1554
1655
  type: 'number',
1555
1656
  description: 'Pagination offset - skip first N results (default: 0)',