claude-code-workflow 6.3.19 → 6.3.20

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 (51) hide show
  1. package/.claude/agents/issue-plan-agent.md +31 -2
  2. package/.claude/commands/issue/new.md +92 -2
  3. package/.claude/commands/issue/plan.md +3 -2
  4. package/.codex/prompts/issue-execute.md +5 -0
  5. package/ccw/dist/core/routes/litellm-api-routes.d.ts.map +1 -1
  6. package/ccw/dist/core/routes/litellm-api-routes.js +8 -0
  7. package/ccw/dist/core/routes/litellm-api-routes.js.map +1 -1
  8. package/ccw/dist/core/server.d.ts.map +1 -1
  9. package/ccw/dist/core/server.js +5 -0
  10. package/ccw/dist/core/server.js.map +1 -1
  11. package/ccw/dist/core/services/api-key-tester.d.ts +11 -0
  12. package/ccw/dist/core/services/api-key-tester.d.ts.map +1 -1
  13. package/ccw/dist/core/services/api-key-tester.js +30 -10
  14. package/ccw/dist/core/services/api-key-tester.js.map +1 -1
  15. package/ccw/dist/core/services/health-check-service.d.ts +6 -0
  16. package/ccw/dist/core/services/health-check-service.d.ts.map +1 -1
  17. package/ccw/dist/core/services/health-check-service.js +22 -0
  18. package/ccw/dist/core/services/health-check-service.js.map +1 -1
  19. package/ccw/src/core/routes/litellm-api-routes.ts +8 -0
  20. package/ccw/src/core/server.ts +6 -0
  21. package/ccw/src/core/services/api-key-tester.ts +33 -10
  22. package/ccw/src/core/services/health-check-service.ts +26 -0
  23. package/ccw/src/templates/dashboard-js/i18n.js +10 -0
  24. package/ccw/src/templates/dashboard-js/views/codexlens-manager.js +10 -0
  25. package/codex-lens/src/codexlens/__pycache__/config.cpython-312.pyc +0 -0
  26. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  27. package/codex-lens/src/codexlens/__pycache__/env_config.cpython-312.pyc +0 -0
  28. package/codex-lens/src/codexlens/__pycache__/env_config.cpython-313.pyc +0 -0
  29. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-312.pyc +0 -0
  30. package/codex-lens/src/codexlens/cli/__pycache__/embedding_manager.cpython-313.pyc +0 -0
  31. package/codex-lens/src/codexlens/cli/embedding_manager.py +13 -4
  32. package/codex-lens/src/codexlens/config.py +35 -0
  33. package/codex-lens/src/codexlens/env_config.py +6 -0
  34. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-312.pyc +0 -0
  35. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  36. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-312.pyc +0 -0
  37. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  38. package/codex-lens/src/codexlens/search/chain_search.py +10 -0
  39. package/codex-lens/src/codexlens/search/ranking.py +50 -0
  40. package/codex-lens/src/codexlens/semantic/__pycache__/chunker.cpython-313.pyc +0 -0
  41. package/codex-lens/src/codexlens/semantic/chunker.py +328 -23
  42. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/__init__.cpython-312.pyc +0 -0
  43. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/api_reranker.cpython-312.pyc +0 -0
  44. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/base.cpython-312.pyc +0 -0
  45. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/factory.cpython-312.pyc +0 -0
  46. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/fastembed_reranker.cpython-312.pyc +0 -0
  47. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/legacy.cpython-312.pyc +0 -0
  48. package/codex-lens/src/codexlens/semantic/reranker/__pycache__/onnx_reranker.cpython-312.pyc +0 -0
  49. package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
  50. package/codex-lens/src/codexlens/storage/index_tree.py +46 -2
  51. package/package.json +1 -1
@@ -43,6 +43,250 @@ class ChunkConfig:
43
43
  strategy: str = "auto" # Chunking strategy: auto, symbol, sliding_window, hybrid
44
44
  min_chunk_size: int = 50 # Minimum chunk size
45
45
  skip_token_count: bool = False # Skip expensive token counting (use char/4 estimate)
46
+ strip_comments: bool = True # Remove comments from chunk content for embedding
47
+ strip_docstrings: bool = True # Remove docstrings from chunk content for embedding
48
+ preserve_original: bool = True # Store original content in metadata when stripping
49
+
50
+
51
+ class CommentStripper:
52
+ """Remove comments from source code while preserving structure."""
53
+
54
+ @staticmethod
55
+ def strip_python_comments(content: str) -> str:
56
+ """Strip Python comments (# style) but preserve docstrings.
57
+
58
+ Args:
59
+ content: Python source code
60
+
61
+ Returns:
62
+ Code with comments removed
63
+ """
64
+ lines = content.splitlines(keepends=True)
65
+ result_lines: List[str] = []
66
+ in_string = False
67
+ string_char = None
68
+
69
+ for line in lines:
70
+ new_line = []
71
+ i = 0
72
+ while i < len(line):
73
+ char = line[i]
74
+
75
+ # Handle string literals
76
+ if char in ('"', "'") and not in_string:
77
+ # Check for triple quotes
78
+ if line[i:i+3] in ('"""', "'''"):
79
+ in_string = True
80
+ string_char = line[i:i+3]
81
+ new_line.append(line[i:i+3])
82
+ i += 3
83
+ continue
84
+ else:
85
+ in_string = True
86
+ string_char = char
87
+ elif in_string:
88
+ if string_char and len(string_char) == 3:
89
+ if line[i:i+3] == string_char:
90
+ in_string = False
91
+ new_line.append(line[i:i+3])
92
+ i += 3
93
+ string_char = None
94
+ continue
95
+ elif char == string_char:
96
+ # Check for escape
97
+ if i > 0 and line[i-1] != '\\':
98
+ in_string = False
99
+ string_char = None
100
+
101
+ # Handle comments (only outside strings)
102
+ if char == '#' and not in_string:
103
+ # Rest of line is comment, skip it
104
+ new_line.append('\n' if line.endswith('\n') else '')
105
+ break
106
+
107
+ new_line.append(char)
108
+ i += 1
109
+
110
+ result_lines.append(''.join(new_line))
111
+
112
+ return ''.join(result_lines)
113
+
114
+ @staticmethod
115
+ def strip_c_style_comments(content: str) -> str:
116
+ """Strip C-style comments (// and /* */) from code.
117
+
118
+ Args:
119
+ content: Source code with C-style comments
120
+
121
+ Returns:
122
+ Code with comments removed
123
+ """
124
+ result = []
125
+ i = 0
126
+ in_string = False
127
+ string_char = None
128
+ in_multiline_comment = False
129
+
130
+ while i < len(content):
131
+ # Handle multi-line comment end
132
+ if in_multiline_comment:
133
+ if content[i:i+2] == '*/':
134
+ in_multiline_comment = False
135
+ i += 2
136
+ continue
137
+ i += 1
138
+ continue
139
+
140
+ char = content[i]
141
+
142
+ # Handle string literals
143
+ if char in ('"', "'", '`') and not in_string:
144
+ in_string = True
145
+ string_char = char
146
+ result.append(char)
147
+ i += 1
148
+ continue
149
+ elif in_string:
150
+ result.append(char)
151
+ if char == string_char and (i == 0 or content[i-1] != '\\'):
152
+ in_string = False
153
+ string_char = None
154
+ i += 1
155
+ continue
156
+
157
+ # Handle comments
158
+ if content[i:i+2] == '//':
159
+ # Single line comment - skip to end of line
160
+ while i < len(content) and content[i] != '\n':
161
+ i += 1
162
+ if i < len(content):
163
+ result.append('\n')
164
+ i += 1
165
+ continue
166
+
167
+ if content[i:i+2] == '/*':
168
+ in_multiline_comment = True
169
+ i += 2
170
+ continue
171
+
172
+ result.append(char)
173
+ i += 1
174
+
175
+ return ''.join(result)
176
+
177
+ @classmethod
178
+ def strip_comments(cls, content: str, language: str) -> str:
179
+ """Strip comments based on language.
180
+
181
+ Args:
182
+ content: Source code content
183
+ language: Programming language
184
+
185
+ Returns:
186
+ Code with comments removed
187
+ """
188
+ if language == "python":
189
+ return cls.strip_python_comments(content)
190
+ elif language in {"javascript", "typescript", "java", "c", "cpp", "go", "rust"}:
191
+ return cls.strip_c_style_comments(content)
192
+ return content
193
+
194
+
195
+ class DocstringStripper:
196
+ """Remove docstrings from source code."""
197
+
198
+ @staticmethod
199
+ def strip_python_docstrings(content: str) -> str:
200
+ """Strip Python docstrings (triple-quoted strings at module/class/function level).
201
+
202
+ Args:
203
+ content: Python source code
204
+
205
+ Returns:
206
+ Code with docstrings removed
207
+ """
208
+ lines = content.splitlines(keepends=True)
209
+ result_lines: List[str] = []
210
+ i = 0
211
+
212
+ while i < len(lines):
213
+ line = lines[i]
214
+ stripped = line.strip()
215
+
216
+ # Check for docstring start
217
+ if stripped.startswith('"""') or stripped.startswith("'''"):
218
+ quote_type = '"""' if stripped.startswith('"""') else "'''"
219
+
220
+ # Single line docstring
221
+ if stripped.count(quote_type) >= 2:
222
+ # Skip this line (docstring)
223
+ i += 1
224
+ continue
225
+
226
+ # Multi-line docstring - skip until closing
227
+ i += 1
228
+ while i < len(lines):
229
+ if quote_type in lines[i]:
230
+ i += 1
231
+ break
232
+ i += 1
233
+ continue
234
+
235
+ result_lines.append(line)
236
+ i += 1
237
+
238
+ return ''.join(result_lines)
239
+
240
+ @staticmethod
241
+ def strip_jsdoc_comments(content: str) -> str:
242
+ """Strip JSDoc comments (/** ... */) from code.
243
+
244
+ Args:
245
+ content: JavaScript/TypeScript source code
246
+
247
+ Returns:
248
+ Code with JSDoc comments removed
249
+ """
250
+ result = []
251
+ i = 0
252
+ in_jsdoc = False
253
+
254
+ while i < len(content):
255
+ if in_jsdoc:
256
+ if content[i:i+2] == '*/':
257
+ in_jsdoc = False
258
+ i += 2
259
+ continue
260
+ i += 1
261
+ continue
262
+
263
+ # Check for JSDoc start (/** but not /*)
264
+ if content[i:i+3] == '/**':
265
+ in_jsdoc = True
266
+ i += 3
267
+ continue
268
+
269
+ result.append(content[i])
270
+ i += 1
271
+
272
+ return ''.join(result)
273
+
274
+ @classmethod
275
+ def strip_docstrings(cls, content: str, language: str) -> str:
276
+ """Strip docstrings based on language.
277
+
278
+ Args:
279
+ content: Source code content
280
+ language: Programming language
281
+
282
+ Returns:
283
+ Code with docstrings removed
284
+ """
285
+ if language == "python":
286
+ return cls.strip_python_docstrings(content)
287
+ elif language in {"javascript", "typescript"}:
288
+ return cls.strip_jsdoc_comments(content)
289
+ return content
46
290
 
47
291
 
48
292
  class Chunker:
@@ -51,6 +295,33 @@ class Chunker:
51
295
  def __init__(self, config: ChunkConfig | None = None) -> None:
52
296
  self.config = config or ChunkConfig()
53
297
  self._tokenizer = get_default_tokenizer()
298
+ self._comment_stripper = CommentStripper()
299
+ self._docstring_stripper = DocstringStripper()
300
+
301
+ def _process_content(self, content: str, language: str) -> Tuple[str, Optional[str]]:
302
+ """Process chunk content by stripping comments/docstrings if configured.
303
+
304
+ Args:
305
+ content: Original chunk content
306
+ language: Programming language
307
+
308
+ Returns:
309
+ Tuple of (processed_content, original_content_if_preserved)
310
+ """
311
+ original = content if self.config.preserve_original else None
312
+ processed = content
313
+
314
+ if self.config.strip_comments:
315
+ processed = self._comment_stripper.strip_comments(processed, language)
316
+
317
+ if self.config.strip_docstrings:
318
+ processed = self._docstring_stripper.strip_docstrings(processed, language)
319
+
320
+ # If nothing changed, don't store original
321
+ if processed == content:
322
+ original = None
323
+
324
+ return processed, original
54
325
 
55
326
  def _estimate_token_count(self, text: str) -> int:
56
327
  """Estimate token count based on config.
@@ -120,30 +391,45 @@ class Chunker:
120
391
  sub_chunk.metadata["symbol_name"] = symbol.name
121
392
  sub_chunk.metadata["symbol_kind"] = symbol.kind
122
393
  sub_chunk.metadata["strategy"] = "symbol_split"
394
+ sub_chunk.metadata["chunk_type"] = "code"
123
395
  sub_chunk.metadata["parent_symbol_range"] = (start_line, end_line)
124
396
 
125
397
  chunks.extend(sub_chunks)
126
398
  else:
399
+ # Process content (strip comments/docstrings if configured)
400
+ processed_content, original_content = self._process_content(chunk_content, language)
401
+
402
+ # Skip if processed content is too small
403
+ if len(processed_content.strip()) < self.config.min_chunk_size:
404
+ continue
405
+
127
406
  # Calculate token count if not provided
128
407
  token_count = None
129
408
  if symbol_token_counts and symbol.name in symbol_token_counts:
130
409
  token_count = symbol_token_counts[symbol.name]
131
410
  else:
132
- token_count = self._estimate_token_count(chunk_content)
411
+ token_count = self._estimate_token_count(processed_content)
412
+
413
+ metadata = {
414
+ "file": str(file_path),
415
+ "language": language,
416
+ "symbol_name": symbol.name,
417
+ "symbol_kind": symbol.kind,
418
+ "start_line": start_line,
419
+ "end_line": end_line,
420
+ "strategy": "symbol",
421
+ "chunk_type": "code",
422
+ "token_count": token_count,
423
+ }
424
+
425
+ # Store original content if it was modified
426
+ if original_content is not None:
427
+ metadata["original_content"] = original_content
133
428
 
134
429
  chunks.append(SemanticChunk(
135
- content=chunk_content,
430
+ content=processed_content,
136
431
  embedding=None,
137
- metadata={
138
- "file": str(file_path),
139
- "language": language,
140
- "symbol_name": symbol.name,
141
- "symbol_kind": symbol.kind,
142
- "start_line": start_line,
143
- "end_line": end_line,
144
- "strategy": "symbol",
145
- "token_count": token_count,
146
- }
432
+ metadata=metadata
147
433
  ))
148
434
 
149
435
  return chunks
@@ -188,7 +474,19 @@ class Chunker:
188
474
  chunk_content = "".join(lines[start:end])
189
475
 
190
476
  if len(chunk_content.strip()) >= self.config.min_chunk_size:
191
- token_count = self._estimate_token_count(chunk_content)
477
+ # Process content (strip comments/docstrings if configured)
478
+ processed_content, original_content = self._process_content(chunk_content, language)
479
+
480
+ # Skip if processed content is too small
481
+ if len(processed_content.strip()) < self.config.min_chunk_size:
482
+ # Move window forward
483
+ step = lines_per_chunk - overlap_lines
484
+ if step <= 0:
485
+ step = 1
486
+ start += step
487
+ continue
488
+
489
+ token_count = self._estimate_token_count(processed_content)
192
490
 
193
491
  # Calculate correct line numbers
194
492
  if line_mapping:
@@ -200,18 +498,25 @@ class Chunker:
200
498
  start_line = start + 1
201
499
  end_line = end
202
500
 
501
+ metadata = {
502
+ "file": str(file_path),
503
+ "language": language,
504
+ "chunk_index": chunk_idx,
505
+ "start_line": start_line,
506
+ "end_line": end_line,
507
+ "strategy": "sliding_window",
508
+ "chunk_type": "code",
509
+ "token_count": token_count,
510
+ }
511
+
512
+ # Store original content if it was modified
513
+ if original_content is not None:
514
+ metadata["original_content"] = original_content
515
+
203
516
  chunks.append(SemanticChunk(
204
- content=chunk_content,
517
+ content=processed_content,
205
518
  embedding=None,
206
- metadata={
207
- "file": str(file_path),
208
- "language": language,
209
- "chunk_index": chunk_idx,
210
- "start_line": start_line,
211
- "end_line": end_line,
212
- "strategy": "sliding_window",
213
- "token_count": token_count,
214
- }
519
+ metadata=metadata
215
520
  ))
216
521
  chunk_idx += 1
217
522
 
@@ -412,7 +412,8 @@ class IndexTreeBuilder:
412
412
  A directory is indexed if:
413
413
  1. It's not in IGNORE_DIRS
414
414
  2. It doesn't start with '.'
415
- 3. It contains at least one supported language file
415
+ 3. It contains at least one supported language file, OR
416
+ 4. It has subdirectories that contain supported files (transitive)
416
417
 
417
418
  Args:
418
419
  dir_path: Directory to check
@@ -427,7 +428,50 @@ class IndexTreeBuilder:
427
428
 
428
429
  # Check for supported files in this directory
429
430
  source_files = self._iter_source_files(dir_path, languages)
430
- return len(source_files) > 0
431
+ if len(source_files) > 0:
432
+ return True
433
+
434
+ # Check if any subdirectory has indexable files (transitive)
435
+ # This handles cases like 'src' which has no direct files but has 'src/codexlens'
436
+ for item in dir_path.iterdir():
437
+ if not item.is_dir():
438
+ continue
439
+ if item.name in self.IGNORE_DIRS or item.name.startswith("."):
440
+ continue
441
+ # Recursively check subdirectories
442
+ if self._has_indexable_files_recursive(item, languages):
443
+ return True
444
+
445
+ return False
446
+
447
+ def _has_indexable_files_recursive(self, dir_path: Path, languages: List[str] = None) -> bool:
448
+ """Check if directory or any subdirectory has indexable files.
449
+
450
+ Args:
451
+ dir_path: Directory to check
452
+ languages: Optional language filter
453
+
454
+ Returns:
455
+ True if directory tree contains indexable files
456
+ """
457
+ # Check for supported files in this directory
458
+ source_files = self._iter_source_files(dir_path, languages)
459
+ if len(source_files) > 0:
460
+ return True
461
+
462
+ # Check subdirectories
463
+ try:
464
+ for item in dir_path.iterdir():
465
+ if not item.is_dir():
466
+ continue
467
+ if item.name in self.IGNORE_DIRS or item.name.startswith("."):
468
+ continue
469
+ if self._has_indexable_files_recursive(item, languages):
470
+ return True
471
+ except PermissionError:
472
+ pass
473
+
474
+ return False
431
475
 
432
476
  def _build_level_parallel(
433
477
  self,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-workflow",
3
- "version": "6.3.19",
3
+ "version": "6.3.20",
4
4
  "description": "JSON-driven multi-agent development framework with intelligent CLI orchestration (Gemini/Qwen/Codex), context-first architecture, and automated workflow execution",
5
5
  "type": "module",
6
6
  "main": "ccw/src/index.js",