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.
- package/.claude/commands/{clean.md → workflow/clean.md} +5 -5
- package/.claude/commands/workflow/docs/analyze.md +1467 -0
- package/.claude/commands/workflow/docs/copyright.md +1265 -0
- package/.claude/commands/workflow/tools/conflict-resolution.md +76 -240
- package/.claude/commands/workflow/tools/task-generate-agent.md +81 -8
- package/.claude/skills/_shared/mermaid-utils.md +584 -0
- package/.claude/skills/copyright-docs/SKILL.md +132 -0
- package/.claude/skills/copyright-docs/phases/01-metadata-collection.md +78 -0
- package/.claude/skills/copyright-docs/phases/02-deep-analysis.md +454 -0
- package/.claude/skills/copyright-docs/phases/02.5-consolidation.md +192 -0
- package/.claude/skills/copyright-docs/phases/04-document-assembly.md +261 -0
- package/.claude/skills/copyright-docs/phases/05-compliance-refinement.md +192 -0
- package/.claude/skills/copyright-docs/specs/cpcc-requirements.md +121 -0
- package/.claude/skills/copyright-docs/templates/agent-base.md +200 -0
- package/.claude/skills/project-analyze/SKILL.md +162 -0
- package/.claude/skills/project-analyze/phases/01-requirements-discovery.md +79 -0
- package/.claude/skills/project-analyze/phases/02-project-exploration.md +75 -0
- package/.claude/skills/project-analyze/phases/03-deep-analysis.md +640 -0
- package/.claude/skills/project-analyze/phases/03.5-consolidation.md +208 -0
- package/.claude/skills/project-analyze/phases/04-report-generation.md +217 -0
- package/.claude/skills/project-analyze/phases/05-iterative-refinement.md +124 -0
- package/.claude/skills/project-analyze/specs/quality-standards.md +115 -0
- package/.claude/skills/project-analyze/specs/writing-style.md +152 -0
- package/.claude/workflows/cli-templates/schemas/conflict-resolution-schema.json +79 -65
- package/README.md +11 -1
- package/ccw/dist/cli.d.ts.map +1 -1
- package/ccw/dist/cli.js +1 -0
- package/ccw/dist/cli.js.map +1 -1
- package/ccw/dist/commands/cli.d.ts.map +1 -1
- package/ccw/dist/commands/cli.js +46 -8
- package/ccw/dist/commands/cli.js.map +1 -1
- package/ccw/dist/core/routes/cli-routes.js +2 -2
- package/ccw/dist/core/routes/cli-routes.js.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.d.ts +7 -3
- package/ccw/dist/tools/claude-cli-tools.d.ts.map +1 -1
- package/ccw/dist/tools/claude-cli-tools.js +31 -17
- package/ccw/dist/tools/claude-cli-tools.js.map +1 -1
- package/ccw/dist/tools/smart-search.d.ts +25 -0
- package/ccw/dist/tools/smart-search.d.ts.map +1 -1
- package/ccw/dist/tools/smart-search.js +121 -17
- package/ccw/dist/tools/smart-search.js.map +1 -1
- package/ccw/src/cli.ts +1 -0
- package/ccw/src/commands/cli.ts +49 -7
- package/ccw/src/core/routes/cli-routes.ts +3 -3
- package/ccw/src/templates/dashboard-js/components/cli-history.js +40 -13
- package/ccw/src/templates/dashboard-js/components/cli-status.js +26 -2
- package/ccw/src/templates/dashboard-js/views/cli-manager.js +5 -0
- package/ccw/src/templates/dashboard-js/views/history.js +19 -4
- package/ccw/src/tools/claude-cli-tools.ts +37 -20
- package/ccw/src/tools/smart-search.ts +157 -16
- package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/config.py +5 -0
- package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/search/hybrid_search.py +144 -11
- package/codex-lens/src/codexlens/search/ranking.py +267 -1
- package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
- package/codex-lens/src/codexlens/semantic/chunker.py +55 -10
- 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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
141
|
-
|
|
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('-')
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
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
|
-
|
|
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 +
|
|
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('-')
|
|
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();
|
|
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 +
|
|
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
|
-
//
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
|
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
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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(
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) => {
|
|
Binary file
|
|
@@ -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}]
|
|
Binary file
|
|
Binary file
|