claude-code-workflow 6.3.4 → 6.3.5

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 (59) hide show
  1. package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
  2. package/.claude/commands/workflow/docs/analyze.md +1467 -0
  3. package/.claude/commands/workflow/docs/copyright.md +1265 -0
  4. package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
  5. package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
  6. package/.claude/skills/_shared/mermaid-utils.md +584 -0
  7. package/.claude/skills/copyright-docs/SKILL.md +132 -0
  8. package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
  9. package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +454 -0
  10. package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
  11. package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
  12. package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
  13. package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
  14. package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
  15. package/.claude/skills/project-analyze/SKILL.md +162 -0
  16. package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
  17. package/.claude/skills/project-analyze/phases/02-project-exploration.md +75 -0
  18. package/.claude/skills/project-analyze/phases/03-deep-analysis.md +640 -0
  19. package/.claude/skills/project-analyze/phases/03.5-consolidation.md +208 -0
  20. package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
  21. package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
  22. package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
  23. package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
  24. package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
  25. package/README.md +11 -1
  26. package/ccw/dist/cli.d.ts.map +1 -1
  27. package/ccw/dist/cli.js +1 -0
  28. package/ccw/dist/cli.js.map +1 -1
  29. package/ccw/dist/commands/cli.d.ts.map +1 -1
  30. package/ccw/dist/commands/cli.js +46 -8
  31. package/ccw/dist/commands/cli.js.map +1 -1
  32. package/ccw/dist/core/routes/cli-routes.js +2 -2
  33. package/ccw/dist/core/routes/cli-routes.js.map +1 -1
  34. package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
  35. package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
  36. package/ccw/dist/tools/claude-cli-tools.js +31 -17
  37. package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
  38. package/ccw/dist/tools/smart-search.d.ts +25 -0
  39. package/ccw/dist/tools/smart-search.d.ts.map +1 -1
  40. package/ccw/dist/tools/smart-search.js +121 -17
  41. package/ccw/dist/tools/smart-search.js.map +1 -1
  42. package/ccw/src/cli.ts +1 -0
  43. package/ccw/src/commands/cli.ts +49 -7
  44. package/ccw/src/core/routes/cli-routes.ts +3 -3
  45. package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
  46. package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
  47. package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
  48. package/ccw/src/templates/dashboard-js/views/history.js +19 -4
  49. package/ccw/src/tools/claude-cli-tools.ts +37 -20
  50. package/ccw/src/tools/smart-search.ts +157 -16
  51. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  52. package/codex-lens/src/codexlens/config.py +5 -0
  53. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  54. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  55. package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
  56. package/codex-lens/src/codexlens/search/ranking.py +267 -1
  57. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  58. package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
  59. package/package.json +2 -2
@@ -15,7 +15,9 @@ async function loadCliHistory(options = {}) {
15
15
  const { limit = cliHistoryLimit, tool = cliHistoryFilter, status = null } = options;
16
16
 
17
17
  // Use history-native endpoint to get native session info
18
- let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}`;
18
+ // Use recursiveQueryEnabled setting (from cli-status.js) to control recursive query
19
+ const recursive = typeof recursiveQueryEnabled !== 'undefined' ? recursiveQueryEnabled : true;
20
+ let url = `/api/cli/history-native?path=${encodeURIComponent(projectPath)}&limit=${limit}&recursive=${recursive}`;
19
21
  if (tool) url += `&tool=${tool}`;
20
22
  if (status) url += `&status=${status}`;
21
23
  if (cliHistorySearch) url += `&search=${encodeURIComponent(cliHistorySearch)}`;
@@ -36,9 +38,12 @@ async function loadCliHistory(options = {}) {
36
38
  async function loadNativeSessionContent(executionId, sourceDir) {
37
39
  try {
38
40
  // If sourceDir provided, use it to build the correct path
39
- const basePath = sourceDir && sourceDir !== '.'
40
- ? projectPath + '/' + sourceDir
41
- : projectPath;
41
+ // Check if sourceDir is absolute path (contains : or starts with /)
42
+ let basePath = projectPath;
43
+ if (sourceDir && sourceDir !== '.') {
44
+ const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
45
+ basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
46
+ }
42
47
  const url = `/api/cli/native-session?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`;
43
48
  const response = await fetch(url);
44
49
  if (!response.ok) return null;
@@ -65,9 +70,12 @@ async function loadEnrichedConversation(executionId) {
65
70
  async function loadExecutionDetail(executionId, sourceDir) {
66
71
  try {
67
72
  // If sourceDir provided, use it to build the correct path
68
- const basePath = sourceDir && sourceDir !== '.'
69
- ? projectPath + '/' + sourceDir
70
- : projectPath;
73
+ // Check if sourceDir is absolute path (contains : or starts with /)
74
+ let basePath = projectPath;
75
+ if (sourceDir && sourceDir !== '.') {
76
+ const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
77
+ basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
78
+ }
71
79
  const url = `/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`;
72
80
  const response = await fetch(url);
73
81
  if (!response.ok) throw new Error('Execution not found');
@@ -137,8 +145,9 @@ function renderCliHistory() {
137
145
  </span>`
138
146
  : '';
139
147
 
140
- // Escape sourceDir for use in onclick
141
- const sourceDirEscaped = exec.sourceDir ? exec.sourceDir.replace(/'/g, "\\'") : '';
148
+ // Normalize and escape sourceDir for use in onclick
149
+ // Convert backslashes to forward slashes to prevent JS escape issues in onclick
150
+ const sourceDirEscaped = exec.sourceDir ? exec.sourceDir.replace(/\\/g, '/').replace(/'/g, "\\'") : '';
142
151
 
143
152
  return `
144
153
  <div class="cli-history-item ${hasNative ? 'has-native' : ''}">
@@ -155,11 +164,14 @@ function renderCliHistory() {
155
164
  <div class="cli-history-meta">
156
165
  <span><i data-lucide="clock" class="w-3 h-3"></i> ${timeAgo}</span>
157
166
  <span><i data-lucide="timer" class="w-3 h-3"></i> ${duration}</span>
158
- <span><i data-lucide="hash" class="w-3 h-3"></i> ${exec.id.split('-')[0]}</span>
167
+ <span title="${exec.id}"><i data-lucide="hash" class="w-3 h-3"></i> ${exec.id.substring(0, 13)}...${exec.id.split('-').pop()}</span>
159
168
  ${turnBadge}
160
169
  </div>
161
170
  </div>
162
171
  <div class="cli-history-actions">
172
+ <button class="btn-icon" onclick="event.stopPropagation(); copyCliExecutionId('${exec.id}')" title="Copy ID">
173
+ <i data-lucide="copy" class="w-3.5 h-3.5"></i>
174
+ </button>
163
175
  ${hasNative ? `
164
176
  <button class="btn-icon" onclick="event.stopPropagation(); showNativeSessionDetail('${exec.id}', '${sourceDirEscaped}')" title="View Native Session">
165
177
  <i data-lucide="file-json" class="w-3.5 h-3.5"></i>
@@ -431,9 +443,12 @@ function confirmDeleteExecution(executionId, sourceDir) {
431
443
  async function deleteExecution(executionId, sourceDir) {
432
444
  try {
433
445
  // Build correct path - use sourceDir if provided for recursive items
434
- const basePath = sourceDir && sourceDir !== '.'
435
- ? projectPath + '/' + sourceDir
436
- : projectPath;
446
+ // Check if sourceDir is absolute path (contains : or starts with /)
447
+ let basePath = projectPath;
448
+ if (sourceDir && sourceDir !== '.') {
449
+ const isAbsolute = sourceDir.includes(':') || sourceDir.startsWith('/');
450
+ basePath = isAbsolute ? sourceDir : projectPath + '/' + sourceDir;
451
+ }
437
452
 
438
453
  const response = await fetch(`/api/cli/execution?path=${encodeURIComponent(basePath)}&id=${encodeURIComponent(executionId)}`, {
439
454
  method: 'DELETE'
@@ -461,6 +476,18 @@ async function deleteExecution(executionId, sourceDir) {
461
476
  }
462
477
 
463
478
  // ========== Copy Functions ==========
479
+ async function copyCliExecutionId(executionId) {
480
+ if (navigator.clipboard) {
481
+ try {
482
+ await navigator.clipboard.writeText(executionId);
483
+ showRefreshToast('ID copied: ' + executionId, 'success');
484
+ } catch (err) {
485
+ console.error('Failed to copy ID:', err);
486
+ showRefreshToast('Failed to copy ID', 'error');
487
+ }
488
+ }
489
+ }
490
+
464
491
  async function copyExecutionPrompt(executionId) {
465
492
  const detail = await loadExecutionDetail(executionId);
466
493
  if (!detail) {
@@ -21,9 +21,24 @@ let nativeResumeEnabled = localStorage.getItem('ccw-native-resume') !== 'false';
21
21
  // Recursive Query settings (for hierarchical storage aggregation)
22
22
  let recursiveQueryEnabled = localStorage.getItem('ccw-recursive-query') !== 'false'; // default true
23
23
 
24
- // Code Index MCP provider (codexlens or ace)
24
+ // Code Index MCP provider (codexlens, ace, or none)
25
25
  let codeIndexMcpProvider = 'codexlens';
26
26
 
27
+ // ========== Helper Functions ==========
28
+ /**
29
+ * Get the context-tools filename based on provider
30
+ */
31
+ function getContextToolsFileName(provider) {
32
+ switch (provider) {
33
+ case 'ace':
34
+ return 'context-tools-ace.md';
35
+ case 'none':
36
+ return 'context-tools-none.md';
37
+ default:
38
+ return 'context-tools.md';
39
+ }
40
+ }
41
+
27
42
  // ========== Initialization ==========
28
43
  function initCliStatus() {
29
44
  // Load all statuses in one call using aggregated endpoint
@@ -637,9 +652,17 @@ function renderCliStatus() {
637
652
  onclick="setCodeIndexMcpProvider('ace')">
638
653
  ACE
639
654
  </button>
655
+ <button class="code-mcp-btn px-3 py-1.5 text-xs font-medium rounded-md transition-all ${codeIndexMcpProvider === 'none' ? 'bg-primary text-primary-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground'}"
656
+ onclick="setCodeIndexMcpProvider('none')">
657
+ None
658
+ </button>
640
659
  </div>
641
660
  </div>
642
661
  <p class="cli-setting-desc">Code search provider (updates CLAUDE.md context-tools reference)</p>
662
+ <p class="cli-setting-desc text-xs text-muted-foreground mt-1">
663
+ <i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>
664
+ Current: <code class="bg-muted px-1 rounded">${getContextToolsFileName(codeIndexMcpProvider)}</code>
665
+ </p>
643
666
  </div>
644
667
  </div>
645
668
  </div>
@@ -775,7 +798,8 @@ async function setCodeIndexMcpProvider(provider) {
775
798
  if (window.claudeCliToolsConfig && window.claudeCliToolsConfig.settings) {
776
799
  window.claudeCliToolsConfig.settings.codeIndexMcp = provider;
777
800
  }
778
- showRefreshToast(`Code Index MCP switched to ${provider === 'ace' ? 'ACE (Augment)' : 'CodexLens'}`, 'success');
801
+ const providerName = provider === 'ace' ? 'ACE (Augment)' : provider === 'none' ? 'None (Built-in only)' : 'CodexLens';
802
+ showRefreshToast(`Code Index MCP switched to ${providerName}`, 'success');
779
803
  // Re-render both CLI status and settings section
780
804
  if (typeof renderCliStatus === 'function') renderCliStatus();
781
805
  if (typeof renderCliSettingsSection === 'function') renderCliSettingsSection();
@@ -996,9 +996,14 @@ function renderCliSettingsSection() {
996
996
  '<select class="cli-setting-select" onchange="setCodeIndexMcpProvider(this.value)">' +
997
997
  '<option value="codexlens"' + (codeIndexMcpProvider === 'codexlens' ? ' selected' : '') + '>CodexLens</option>' +
998
998
  '<option value="ace"' + (codeIndexMcpProvider === 'ace' ? ' selected' : '') + '>ACE (Augment)</option>' +
999
+ '<option value="none"' + (codeIndexMcpProvider === 'none' ? ' selected' : '') + '>None (Built-in)</option>' +
999
1000
  '</select>' +
1000
1001
  '</div>' +
1001
1002
  '<p class="cli-setting-desc">' + t('cli.codeIndexMcpDesc') + '</p>' +
1003
+ '<p class="cli-setting-desc text-xs text-muted-foreground">' +
1004
+ '<i data-lucide="file-text" class="w-3 h-3 inline-block mr-1"></i>' +
1005
+ 'Current: <code class="bg-muted px-1 rounded">' + getContextToolsFileName(codeIndexMcpProvider) + '</code>' +
1006
+ '</p>' +
1002
1007
  '</div>' +
1003
1008
  '</div>';
1004
1009
 
@@ -69,8 +69,10 @@ async function renderCliHistoryView() {
69
69
  '</div>'
70
70
  : '';
71
71
 
72
+ // Normalize sourceDir: convert backslashes to forward slashes for safe onclick handling
73
+ var normalizedSourceDir = (exec.sourceDir || '').replace(/\\/g, '/');
72
74
  historyHtml += '<div class="history-item' + (isSelected ? ' history-item-selected' : '') + '" ' +
73
- 'onclick="' + (isMultiSelectMode ? 'toggleExecutionSelection(\'' + exec.id + '\')' : 'showExecutionDetail(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')') + '">' +
75
+ 'onclick="' + (isMultiSelectMode ? 'toggleExecutionSelection(\'' + exec.id + '\')' : 'showExecutionDetail(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')') + '">' +
74
76
  checkboxHtml +
75
77
  '<div class="history-item-main">' +
76
78
  '<div class="history-item-header">' +
@@ -87,14 +89,17 @@ async function renderCliHistoryView() {
87
89
  '<div class="history-item-meta">' +
88
90
  '<span class="history-time"><i data-lucide="clock" class="w-3 h-3"></i> ' + timeAgo + '</span>' +
89
91
  '<span class="history-duration"><i data-lucide="timer" class="w-3 h-3"></i> ' + duration + '</span>' +
90
- '<span class="history-id"><i data-lucide="hash" class="w-3 h-3"></i> ' + exec.id.split('-')[0] + '</span>' +
92
+ '<span class="history-id" title="' + exec.id + '"><i data-lucide="hash" class="w-3 h-3"></i> ' + exec.id.substring(0, 13) + '...' + exec.id.split('-').pop() + '</span>' +
91
93
  '</div>' +
92
94
  '</div>' +
93
95
  '<div class="history-item-actions">' +
94
- '<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\')" title="View Details">' +
96
+ '<button class="btn-icon" onclick="event.stopPropagation(); copyExecutionId(\'' + exec.id + '\')" title="Copy ID">' +
97
+ '<i data-lucide="copy" class="w-4 h-4"></i>' +
98
+ '</button>' +
99
+ '<button class="btn-icon" onclick="event.stopPropagation(); showExecutionDetail(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')" title="View Details">' +
95
100
  '<i data-lucide="eye" class="w-4 h-4"></i>' +
96
101
  '</button>' +
97
- '<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + (exec.sourceDir ? '\',\'' + escapeHtml(exec.sourceDir) : '') + '\')" title="Delete">' +
102
+ '<button class="btn-icon btn-danger" onclick="event.stopPropagation(); confirmDeleteExecution(\'' + exec.id + '\', \'' + normalizedSourceDir.replace(/'/g, "\\'") + '\')" title="Delete">' +
98
103
  '<i data-lucide="trash-2" class="w-4 h-4"></i>' +
99
104
  '</button>' +
100
105
  '</div>' +
@@ -179,6 +184,16 @@ async function renderCliHistoryView() {
179
184
  }
180
185
 
181
186
  // ========== Actions ==========
187
+ async function copyExecutionId(executionId) {
188
+ try {
189
+ await navigator.clipboard.writeText(executionId);
190
+ showRefreshToast('ID copied: ' + executionId, 'success');
191
+ } catch (err) {
192
+ console.error('Failed to copy ID:', err);
193
+ showRefreshToast('Failed to copy ID', 'error');
194
+ }
195
+ }
196
+
182
197
  async function filterCliHistoryView(tool) {
183
198
  cliHistoryFilter = tool || null;
184
199
  await loadCliHistory();
@@ -42,7 +42,7 @@ export interface ClaudeCliToolsConfig {
42
42
  nativeResume: boolean;
43
43
  recursiveQuery: boolean;
44
44
  cache: ClaudeCacheSettings;
45
- codeIndexMcp: 'codexlens' | 'ace'; // Code Index MCP provider
45
+ codeIndexMcp: 'codexlens' | 'ace' | 'none'; // Code Index MCP provider
46
46
  };
47
47
  }
48
48
 
@@ -308,7 +308,7 @@ export function getClaudeCliToolsInfo(projectDir: string): {
308
308
  */
309
309
  export function updateCodeIndexMcp(
310
310
  projectDir: string,
311
- provider: 'codexlens' | 'ace'
311
+ provider: 'codexlens' | 'ace' | 'none'
312
312
  ): { success: boolean; error?: string; config?: ClaudeCliToolsConfig } {
313
313
  try {
314
314
  // Update config
@@ -319,21 +319,28 @@ export function updateCodeIndexMcp(
319
319
  // Only update global CLAUDE.md (consistent with Chinese response / Windows platform)
320
320
  const globalClaudeMdPath = path.join(os.homedir(), '.claude', 'CLAUDE.md');
321
321
 
322
+ // Define patterns for all formats
323
+ const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
324
+ const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
325
+ const nonePattern = /@~\/\.claude\/workflows\/context-tools-none\.md/g;
326
+
327
+ // Determine target file based on provider
328
+ const targetFile = provider === 'ace'
329
+ ? '@~/.claude/workflows/context-tools-ace.md'
330
+ : provider === 'none'
331
+ ? '@~/.claude/workflows/context-tools-none.md'
332
+ : '@~/.claude/workflows/context-tools.md';
333
+
322
334
  if (!fs.existsSync(globalClaudeMdPath)) {
323
335
  // If global CLAUDE.md doesn't exist, check project-level
324
336
  const projectClaudeMdPath = path.join(projectDir, '.claude', 'CLAUDE.md');
325
337
  if (fs.existsSync(projectClaudeMdPath)) {
326
338
  let content = fs.readFileSync(projectClaudeMdPath, 'utf-8');
327
339
 
328
- // Define patterns for both formats
329
- const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
330
- const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
331
-
332
- if (provider === 'ace') {
333
- content = content.replace(codexlensPattern, '@~/.claude/workflows/context-tools-ace.md');
334
- } else {
335
- content = content.replace(acePattern, '@~/.claude/workflows/context-tools.md');
336
- }
340
+ // Replace any existing pattern with the target
341
+ content = content.replace(codexlensPattern, targetFile);
342
+ content = content.replace(acePattern, targetFile);
343
+ content = content.replace(nonePattern, targetFile);
337
344
 
338
345
  fs.writeFileSync(projectClaudeMdPath, content, 'utf-8');
339
346
  console.log(`[claude-cli-tools] Updated project CLAUDE.md to use ${provider} (no global CLAUDE.md found)`);
@@ -342,14 +349,10 @@ export function updateCodeIndexMcp(
342
349
  // Update global CLAUDE.md (primary target)
343
350
  let content = fs.readFileSync(globalClaudeMdPath, 'utf-8');
344
351
 
345
- const codexlensPattern = /@~\/\.claude\/workflows\/context-tools\.md/g;
346
- const acePattern = /@~\/\.claude\/workflows\/context-tools-ace\.md/g;
347
-
348
- if (provider === 'ace') {
349
- content = content.replace(codexlensPattern, '@~/.claude/workflows/context-tools-ace.md');
350
- } else {
351
- content = content.replace(acePattern, '@~/.claude/workflows/context-tools.md');
352
- }
352
+ // Replace any existing pattern with the target
353
+ content = content.replace(codexlensPattern, targetFile);
354
+ content = content.replace(acePattern, targetFile);
355
+ content = content.replace(nonePattern, targetFile);
353
356
 
354
357
  fs.writeFileSync(globalClaudeMdPath, content, 'utf-8');
355
358
  console.log(`[claude-cli-tools] Updated global CLAUDE.md to use ${provider}`);
@@ -365,7 +368,21 @@ export function updateCodeIndexMcp(
365
368
  /**
366
369
  * Get current Code Index MCP provider
367
370
  */
368
- export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' {
371
+ export function getCodeIndexMcp(projectDir: string): 'codexlens' | 'ace' | 'none' {
369
372
  const config = loadClaudeCliTools(projectDir);
370
373
  return config.settings.codeIndexMcp || 'codexlens';
371
374
  }
375
+
376
+ /**
377
+ * Get the context-tools file path based on provider
378
+ */
379
+ export function getContextToolsPath(provider: 'codexlens' | 'ace' | 'none'): string {
380
+ switch (provider) {
381
+ case 'ace':
382
+ return 'context-tools-ace.md';
383
+ case 'none':
384
+ return 'context-tools-none.md';
385
+ default:
386
+ return 'context-tools.md';
387
+ }
388
+ }
@@ -24,6 +24,39 @@ import {
24
24
  import type { ProgressInfo } from './codex-lens.js';
25
25
  import { getProjectRoot } from '../utils/path-validator.js';
26
26
 
27
+ // Timing utilities for performance analysis
28
+ const TIMING_ENABLED = process.env.SMART_SEARCH_TIMING === '1' || process.env.DEBUG?.includes('timing');
29
+
30
+ interface TimingData {
31
+ [key: string]: number;
32
+ }
33
+
34
+ function createTimer(): { mark: (name: string) => void; getTimings: () => TimingData; log: () => void } {
35
+ const startTime = performance.now();
36
+ const marks: { name: string; time: number }[] = [];
37
+ let lastMark = startTime;
38
+
39
+ return {
40
+ mark(name: string) {
41
+ const now = performance.now();
42
+ marks.push({ name, time: now - lastMark });
43
+ lastMark = now;
44
+ },
45
+ getTimings(): TimingData {
46
+ const timings: TimingData = {};
47
+ marks.forEach(m => { timings[m.name] = Math.round(m.time * 100) / 100; });
48
+ timings['_total'] = Math.round((performance.now() - startTime) * 100) / 100;
49
+ return timings;
50
+ },
51
+ log() {
52
+ if (TIMING_ENABLED) {
53
+ const timings = this.getTimings();
54
+ console.error(`[TIMING] smart-search: ${JSON.stringify(timings)}`);
55
+ }
56
+ }
57
+ };
58
+ }
59
+
27
60
  // Define Zod schema for validation
28
61
  const ParamsSchema = z.object({
29
62
  // Action: search (content), find_files (path/name pattern), init, status
@@ -48,6 +81,9 @@ const ParamsSchema = z.object({
48
81
  regex: z.boolean().default(true), // Use regex pattern matching (default: enabled)
49
82
  caseSensitive: z.boolean().default(true), // Case sensitivity (default: case-sensitive)
50
83
  tokenize: z.boolean().default(true), // Tokenize multi-word queries for OR matching (default: enabled)
84
+ // File type filtering
85
+ excludeExtensions: z.array(z.string()).optional().describe('File extensions to exclude from results (e.g., ["md", "txt"])'),
86
+ codeOnly: z.boolean().default(false).describe('Only return code files (excludes md, txt, json, yaml, xml, etc.)'),
51
87
  // Fuzzy matching is implicit in hybrid mode (RRF fusion)
52
88
  });
53
89
 
@@ -254,6 +290,8 @@ interface SearchMetadata {
254
290
  tokenized?: boolean; // Whether tokenization was applied
255
291
  // Pagination metadata
256
292
  pagination?: PaginationInfo;
293
+ // Performance timing data (when SMART_SEARCH_TIMING=1 or DEBUG includes 'timing')
294
+ timing?: TimingData;
257
295
  // Init action specific
258
296
  action?: string;
259
297
  path?: string;
@@ -1086,7 +1124,8 @@ async function executeCodexLensExactMode(params: Params): Promise<SearchResult>
1086
1124
  * Requires index with embeddings
1087
1125
  */
1088
1126
  async function executeHybridMode(params: Params): Promise<SearchResult> {
1089
- const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false } = params;
1127
+ const timer = createTimer();
1128
+ const { query, path = '.', maxResults = 5, extraFilesCount = 10, maxContentLength = 200, enrich = false, excludeExtensions, codeOnly = false } = params;
1090
1129
 
1091
1130
  if (!query) {
1092
1131
  return {
@@ -1097,6 +1136,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1097
1136
 
1098
1137
  // Check CodexLens availability
1099
1138
  const readyStatus = await ensureCodexLensReady();
1139
+ timer.mark('codexlens_ready_check');
1100
1140
  if (!readyStatus.ready) {
1101
1141
  return {
1102
1142
  success: false,
@@ -1106,6 +1146,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1106
1146
 
1107
1147
  // Check index status
1108
1148
  const indexStatus = await checkIndexStatus(path);
1149
+ timer.mark('index_status_check');
1109
1150
 
1110
1151
  // Request more results to support split (full content + extra files)
1111
1152
  const totalToFetch = maxResults + extraFilesCount;
@@ -1114,8 +1155,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1114
1155
  args.push('--enrich');
1115
1156
  }
1116
1157
  const result = await executeCodexLens(args, { cwd: path });
1158
+ timer.mark('codexlens_search');
1117
1159
 
1118
1160
  if (!result.success) {
1161
+ timer.log();
1119
1162
  return {
1120
1163
  success: false,
1121
1164
  error: result.error,
@@ -1150,6 +1193,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1150
1193
  symbol: item.symbol || null,
1151
1194
  };
1152
1195
  });
1196
+ timer.mark('parse_results');
1153
1197
 
1154
1198
  initialCount = allResults.length;
1155
1199
 
@@ -1159,14 +1203,15 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1159
1203
  allResults = baselineResult.filteredResults;
1160
1204
  baselineInfo = baselineResult.baselineInfo;
1161
1205
 
1162
- // 1. Filter noisy files (coverage, node_modules, etc.)
1163
- allResults = filterNoisyFiles(allResults);
1206
+ // 1. Filter noisy files (coverage, node_modules, etc.) and excluded extensions
1207
+ allResults = filterNoisyFiles(allResults, { excludeExtensions, codeOnly });
1164
1208
  // 2. Boost results containing query keywords
1165
1209
  allResults = applyKeywordBoosting(allResults, query);
1166
1210
  // 3. Enforce score diversity (penalize identical scores)
1167
1211
  allResults = enforceScoreDiversity(allResults);
1168
1212
  // 4. Re-sort by adjusted scores
1169
1213
  allResults.sort((a, b) => b.score - a.score);
1214
+ timer.mark('post_processing');
1170
1215
  } catch {
1171
1216
  return {
1172
1217
  success: true,
@@ -1184,6 +1229,7 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1184
1229
 
1185
1230
  // Split results: first N with full content, rest as file paths only
1186
1231
  const { results, extra_files } = splitResultsWithExtraFiles(allResults, maxResults, extraFilesCount);
1232
+ timer.mark('split_results');
1187
1233
 
1188
1234
  // Build metadata with baseline info if detected
1189
1235
  let note = 'Hybrid mode uses RRF fusion (exact + fuzzy + vector) for best results';
@@ -1191,6 +1237,10 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1191
1237
  note += ` | Filtered ${initialCount - allResults.length} hot-spot results with baseline score ~${baselineInfo.score.toFixed(4)}`;
1192
1238
  }
1193
1239
 
1240
+ // Log timing data
1241
+ timer.log();
1242
+ const timings = timer.getTimings();
1243
+
1194
1244
  return {
1195
1245
  success: true,
1196
1246
  results,
@@ -1203,22 +1253,82 @@ async function executeHybridMode(params: Params): Promise<SearchResult> {
1203
1253
  note,
1204
1254
  warning: indexStatus.warning,
1205
1255
  suggested_weights: getRRFWeights(query),
1256
+ timing: TIMING_ENABLED ? timings : undefined,
1206
1257
  },
1207
1258
  };
1208
1259
  }
1209
1260
 
1210
- const RRF_WEIGHTS = {
1211
- code: { exact: 0.7, fuzzy: 0.2, vector: 0.1 },
1212
- natural: { exact: 0.4, fuzzy: 0.2, vector: 0.4 },
1213
- default: { exact: 0.5, fuzzy: 0.2, vector: 0.3 },
1214
- };
1261
+ /**
1262
+ * Query intent used to adapt RRF weights (Python parity).
1263
+ *
1264
+ * Keep this logic aligned with CodexLens Python hybrid search:
1265
+ * `codex-lens/src/codexlens/search/hybrid_search.py`
1266
+ */
1267
+ export type QueryIntent = 'keyword' | 'semantic' | 'mixed';
1268
+
1269
+ // Python default: vector 60%, exact 30%, fuzzy 10%
1270
+ const DEFAULT_RRF_WEIGHTS = {
1271
+ exact: 0.3,
1272
+ fuzzy: 0.1,
1273
+ vector: 0.6,
1274
+ } as const;
1275
+
1276
+ function normalizeWeights(weights: Record<string, number>): Record<string, number> {
1277
+ const sum = Object.values(weights).reduce((acc, v) => acc + v, 0);
1278
+ if (!Number.isFinite(sum) || sum <= 0) return { ...weights };
1279
+ return Object.fromEntries(Object.entries(weights).map(([k, v]) => [k, v / sum]));
1280
+ }
1281
+
1282
+ /**
1283
+ * Detect query intent using the same heuristic signals as Python:
1284
+ * - Code patterns: `.`, `::`, `->`, CamelCase, snake_case, common code keywords
1285
+ * - Natural language patterns: >5 words, question marks, interrogatives, common verbs
1286
+ */
1287
+ export function detectQueryIntent(query: string): QueryIntent {
1288
+ const trimmed = query.trim();
1289
+ if (!trimmed) return 'mixed';
1290
+
1291
+ const lower = trimmed.toLowerCase();
1292
+ const wordCount = trimmed.split(/\s+/).filter(Boolean).length;
1293
+
1294
+ const hasCodeSignals =
1295
+ /(::|->|\.)/.test(trimmed) ||
1296
+ /[A-Z][a-z]+[A-Z]/.test(trimmed) ||
1297
+ /\b\w+_\w+\b/.test(trimmed) ||
1298
+ /\b(def|class|function|const|let|var|import|from|return|async|await|interface|type)\b/i.test(lower);
1299
+
1300
+ const hasNaturalSignals =
1301
+ wordCount > 5 ||
1302
+ /\?/.test(trimmed) ||
1303
+ /\b(how|what|why|when|where)\b/i.test(trimmed) ||
1304
+ /\b(handle|explain|fix|implement|create|build|use|find|search|convert|parse|generate|support)\b/i.test(trimmed);
1305
+
1306
+ if (hasCodeSignals && hasNaturalSignals) return 'mixed';
1307
+ if (hasCodeSignals) return 'keyword';
1308
+ if (hasNaturalSignals) return 'semantic';
1309
+ return 'mixed';
1310
+ }
1311
+
1312
+ /**
1313
+ * Intent → weights mapping (Python parity).
1314
+ * - keyword: exact-heavy
1315
+ * - semantic: vector-heavy
1316
+ * - mixed: keep defaults
1317
+ */
1318
+ export function adjustWeightsByIntent(
1319
+ intent: QueryIntent,
1320
+ baseWeights: Record<string, number>,
1321
+ ): Record<string, number> {
1322
+ if (intent === 'keyword') return normalizeWeights({ exact: 0.5, fuzzy: 0.1, vector: 0.4 });
1323
+ if (intent === 'semantic') return normalizeWeights({ exact: 0.2, fuzzy: 0.1, vector: 0.7 });
1324
+ return normalizeWeights({ ...baseWeights });
1325
+ }
1215
1326
 
1216
- function getRRFWeights(query: string): Record<string, number> {
1217
- const isCode = looksLikeCodeQuery(query);
1218
- const isNatural = detectNaturalLanguage(query);
1219
- if (isCode) return RRF_WEIGHTS.code;
1220
- if (isNatural) return RRF_WEIGHTS.natural;
1221
- return RRF_WEIGHTS.default;
1327
+ export function getRRFWeights(
1328
+ query: string,
1329
+ baseWeights: Record<string, number> = DEFAULT_RRF_WEIGHTS,
1330
+ ): Record<string, number> {
1331
+ return adjustWeightsByIntent(detectQueryIntent(query), baseWeights);
1222
1332
  }
1223
1333
 
1224
1334
  /**
@@ -1231,7 +1341,29 @@ const FILE_EXCLUDE_REGEXES = [...FILTER_CONFIG.exclude_files].map(pattern =>
1231
1341
  new RegExp('^' + pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\\\*/g, '.*') + '$')
1232
1342
  );
1233
1343
 
1234
- function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
1344
+ // Non-code file extensions (for codeOnly filter)
1345
+ const NON_CODE_EXTENSIONS = new Set([
1346
+ 'md', 'txt', 'json', 'yaml', 'yml', 'xml', 'csv', 'log',
1347
+ 'ini', 'cfg', 'conf', 'toml', 'env', 'properties',
1348
+ 'html', 'htm', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'webp',
1349
+ 'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
1350
+ 'lock', 'sum', 'mod',
1351
+ ]);
1352
+
1353
+ interface FilterOptions {
1354
+ excludeExtensions?: string[];
1355
+ codeOnly?: boolean;
1356
+ }
1357
+
1358
+ function filterNoisyFiles(results: SemanticMatch[], options: FilterOptions = {}): SemanticMatch[] {
1359
+ const { excludeExtensions = [], codeOnly = false } = options;
1360
+
1361
+ // Build extension filter set
1362
+ const excludedExtSet = new Set(excludeExtensions.map(ext => ext.toLowerCase().replace(/^\./, '')));
1363
+ if (codeOnly) {
1364
+ NON_CODE_EXTENSIONS.forEach(ext => excludedExtSet.add(ext));
1365
+ }
1366
+
1235
1367
  return results.filter(r => {
1236
1368
  const filePath = r.file || '';
1237
1369
  if (!filePath) return true;
@@ -1249,6 +1381,14 @@ function filterNoisyFiles(results: SemanticMatch[]): SemanticMatch[] {
1249
1381
  return false;
1250
1382
  }
1251
1383
 
1384
+ // Extension filter check
1385
+ if (excludedExtSet.size > 0) {
1386
+ const ext = filename.split('.').pop()?.toLowerCase() || '';
1387
+ if (excludedExtSet.has(ext)) {
1388
+ return false;
1389
+ }
1390
+ }
1391
+
1252
1392
  return true;
1253
1393
  });
1254
1394
  }
@@ -1396,10 +1536,11 @@ function filterDominantBaselineScores(
1396
1536
  */
1397
1537
  function applyRRFFusion(
1398
1538
  resultsMap: Map<string, any[]>,
1399
- weights: Record<string, number>,
1539
+ weightsOrQuery: Record<string, number> | string,
1400
1540
  limit: number,
1401
1541
  k: number = 60,
1402
1542
  ): any[] {
1543
+ const weights = typeof weightsOrQuery === 'string' ? getRRFWeights(weightsOrQuery) : weightsOrQuery;
1403
1544
  const pathScores = new Map<string, { score: number; result: any; sources: string[] }>();
1404
1545
 
1405
1546
  resultsMap.forEach((results, source) => {
@@ -103,6 +103,11 @@ class Config:
103
103
  # Indexing/search optimizations
104
104
  global_symbol_index_enabled: bool = True # Enable project-wide symbol index fast path
105
105
 
106
+ # Optional search reranking (disabled by default)
107
+ enable_reranking: bool = False
108
+ reranking_top_k: int = 50
109
+ symbol_boost_factor: float = 1.5
110
+
106
111
  # Multi-endpoint configuration for litellm backend
107
112
  embedding_endpoints: List[Dict[str, Any]] = field(default_factory=list)
108
113
  # List of endpoint configs: [{"model": "...", "api_key": "...", "api_base": "...", "weight": 1.0}]