claude-code-workflow 6.3.13 → 6.3.15

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 (69) hide show
  1. package/.claude/agents/issue-plan-agent.md +57 -103
  2. package/.claude/agents/issue-queue-agent.md +69 -120
  3. package/.claude/commands/issue/new.md +217 -473
  4. package/.claude/commands/issue/plan.md +76 -154
  5. package/.claude/commands/issue/queue.md +208 -259
  6. package/.claude/skills/issue-manage/SKILL.md +63 -22
  7. package/.claude/workflows/cli-templates/schemas/discovery-finding-schema.json +3 -3
  8. package/.claude/workflows/cli-templates/schemas/issues-jsonl-schema.json +3 -3
  9. package/.claude/workflows/cli-templates/schemas/queue-schema.json +0 -5
  10. package/.codex/prompts/issue-plan.md +16 -19
  11. package/.codex/prompts/issue-queue.md +0 -1
  12. package/README.md +1 -0
  13. package/ccw/dist/cli.d.ts.map +1 -1
  14. package/ccw/dist/cli.js +3 -1
  15. package/ccw/dist/cli.js.map +1 -1
  16. package/ccw/dist/commands/cli.d.ts.map +1 -1
  17. package/ccw/dist/commands/cli.js +45 -3
  18. package/ccw/dist/commands/cli.js.map +1 -1
  19. package/ccw/dist/commands/issue.d.ts +3 -1
  20. package/ccw/dist/commands/issue.d.ts.map +1 -1
  21. package/ccw/dist/commands/issue.js +383 -30
  22. package/ccw/dist/commands/issue.js.map +1 -1
  23. package/ccw/dist/core/routes/issue-routes.d.ts.map +1 -1
  24. package/ccw/dist/core/routes/issue-routes.js +77 -16
  25. package/ccw/dist/core/routes/issue-routes.js.map +1 -1
  26. package/ccw/dist/tools/cli-executor.d.ts.map +1 -1
  27. package/ccw/dist/tools/cli-executor.js +117 -4
  28. package/ccw/dist/tools/cli-executor.js.map +1 -1
  29. package/ccw/dist/tools/litellm-executor.d.ts +4 -0
  30. package/ccw/dist/tools/litellm-executor.d.ts.map +1 -1
  31. package/ccw/dist/tools/litellm-executor.js +54 -1
  32. package/ccw/dist/tools/litellm-executor.js.map +1 -1
  33. package/ccw/dist/tools/ui-generate-preview.d.ts +18 -0
  34. package/ccw/dist/tools/ui-generate-preview.d.ts.map +1 -1
  35. package/ccw/dist/tools/ui-generate-preview.js +26 -10
  36. package/ccw/dist/tools/ui-generate-preview.js.map +1 -1
  37. package/ccw/src/cli.ts +3 -1
  38. package/ccw/src/commands/cli.ts +47 -3
  39. package/ccw/src/commands/issue.ts +442 -34
  40. package/ccw/src/core/routes/issue-routes.ts +82 -16
  41. package/ccw/src/tools/cli-executor.ts +125 -4
  42. package/ccw/src/tools/litellm-executor.ts +107 -24
  43. package/ccw/src/tools/ui-generate-preview.js +60 -37
  44. package/codex-lens/src/codexlens/__pycache__/config.cpython-313.pyc +0 -0
  45. package/codex-lens/src/codexlens/__pycache__/entities.cpython-313.pyc +0 -0
  46. package/codex-lens/src/codexlens/config.py +25 -2
  47. package/codex-lens/src/codexlens/entities.py +5 -1
  48. package/codex-lens/src/codexlens/indexing/__pycache__/symbol_extractor.cpython-313.pyc +0 -0
  49. package/codex-lens/src/codexlens/indexing/symbol_extractor.py +243 -243
  50. package/codex-lens/src/codexlens/parsers/__pycache__/factory.cpython-313.pyc +0 -0
  51. package/codex-lens/src/codexlens/parsers/__pycache__/treesitter_parser.cpython-313.pyc +0 -0
  52. package/codex-lens/src/codexlens/parsers/factory.py +256 -256
  53. package/codex-lens/src/codexlens/parsers/treesitter_parser.py +335 -335
  54. package/codex-lens/src/codexlens/search/__pycache__/chain_search.cpython-313.pyc +0 -0
  55. package/codex-lens/src/codexlens/search/__pycache__/hybrid_search.cpython-313.pyc +0 -0
  56. package/codex-lens/src/codexlens/search/__pycache__/ranking.cpython-313.pyc +0 -0
  57. package/codex-lens/src/codexlens/search/chain_search.py +30 -1
  58. package/codex-lens/src/codexlens/semantic/__pycache__/__init__.cpython-313.pyc +0 -0
  59. package/codex-lens/src/codexlens/semantic/__pycache__/embedder.cpython-313.pyc +0 -0
  60. package/codex-lens/src/codexlens/semantic/__pycache__/reranker.cpython-313.pyc +0 -0
  61. package/codex-lens/src/codexlens/semantic/__pycache__/vector_store.cpython-313.pyc +0 -0
  62. package/codex-lens/src/codexlens/semantic/embedder.py +6 -9
  63. package/codex-lens/src/codexlens/semantic/vector_store.py +271 -200
  64. package/codex-lens/src/codexlens/storage/__pycache__/dir_index.cpython-313.pyc +0 -0
  65. package/codex-lens/src/codexlens/storage/__pycache__/index_tree.cpython-313.pyc +0 -0
  66. package/codex-lens/src/codexlens/storage/__pycache__/sqlite_store.cpython-313.pyc +0 -0
  67. package/codex-lens/src/codexlens/storage/sqlite_store.py +184 -108
  68. package/package.json +6 -1
  69. package/.claude/commands/issue/manage.md +0 -113
@@ -1,335 +1,335 @@
1
- """Tree-sitter based parser for CodexLens.
2
-
3
- Provides precise AST-level parsing with fallback to regex-based parsing.
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- from pathlib import Path
9
- from typing import List, Optional
10
-
11
- try:
12
- from tree_sitter import Language as TreeSitterLanguage
13
- from tree_sitter import Node as TreeSitterNode
14
- from tree_sitter import Parser as TreeSitterParser
15
- TREE_SITTER_AVAILABLE = True
16
- except ImportError:
17
- TreeSitterLanguage = None # type: ignore[assignment]
18
- TreeSitterNode = None # type: ignore[assignment]
19
- TreeSitterParser = None # type: ignore[assignment]
20
- TREE_SITTER_AVAILABLE = False
21
-
22
- from codexlens.entities import IndexedFile, Symbol
23
- from codexlens.parsers.tokenizer import get_default_tokenizer
24
-
25
-
26
- class TreeSitterSymbolParser:
27
- """Parser using tree-sitter for AST-level symbol extraction."""
28
-
29
- def __init__(self, language_id: str, path: Optional[Path] = None) -> None:
30
- """Initialize tree-sitter parser for a language.
31
-
32
- Args:
33
- language_id: Language identifier (python, javascript, typescript, etc.)
34
- path: Optional file path for language variant detection (e.g., .tsx)
35
- """
36
- self.language_id = language_id
37
- self.path = path
38
- self._parser: Optional[object] = None
39
- self._language: Optional[TreeSitterLanguage] = None
40
- self._tokenizer = get_default_tokenizer()
41
-
42
- if TREE_SITTER_AVAILABLE:
43
- self._initialize_parser()
44
-
45
- def _initialize_parser(self) -> None:
46
- """Initialize tree-sitter parser and language."""
47
- if TreeSitterParser is None or TreeSitterLanguage is None:
48
- return
49
-
50
- try:
51
- # Load language grammar
52
- if self.language_id == "python":
53
- import tree_sitter_python
54
- self._language = TreeSitterLanguage(tree_sitter_python.language())
55
- elif self.language_id == "javascript":
56
- import tree_sitter_javascript
57
- self._language = TreeSitterLanguage(tree_sitter_javascript.language())
58
- elif self.language_id == "typescript":
59
- import tree_sitter_typescript
60
- # Detect TSX files by extension
61
- if self.path is not None and self.path.suffix.lower() == ".tsx":
62
- self._language = TreeSitterLanguage(tree_sitter_typescript.language_tsx())
63
- else:
64
- self._language = TreeSitterLanguage(tree_sitter_typescript.language_typescript())
65
- else:
66
- return
67
-
68
- # Create parser
69
- self._parser = TreeSitterParser()
70
- if hasattr(self._parser, "set_language"):
71
- self._parser.set_language(self._language) # type: ignore[attr-defined]
72
- else:
73
- self._parser.language = self._language # type: ignore[assignment]
74
-
75
- except Exception:
76
- # Gracefully handle missing language bindings
77
- self._parser = None
78
- self._language = None
79
-
80
- def is_available(self) -> bool:
81
- """Check if tree-sitter parser is available.
82
-
83
- Returns:
84
- True if parser is initialized and ready
85
- """
86
- return self._parser is not None and self._language is not None
87
-
88
-
89
- def parse_symbols(self, text: str) -> Optional[List[Symbol]]:
90
- """Parse source code and extract symbols without creating IndexedFile.
91
-
92
- Args:
93
- text: Source code text
94
-
95
- Returns:
96
- List of symbols if parsing succeeds, None if tree-sitter unavailable
97
- """
98
- if not self.is_available() or self._parser is None:
99
- return None
100
-
101
- try:
102
- source_bytes = text.encode("utf8")
103
- tree = self._parser.parse(source_bytes) # type: ignore[attr-defined]
104
- root = tree.root_node
105
-
106
- return self._extract_symbols(source_bytes, root)
107
- except Exception:
108
- # Gracefully handle parsing errors
109
- return None
110
-
111
- def parse(self, text: str, path: Path) -> Optional[IndexedFile]:
112
- """Parse source code and extract symbols.
113
-
114
- Args:
115
- text: Source code text
116
- path: File path
117
-
118
- Returns:
119
- IndexedFile if parsing succeeds, None if tree-sitter unavailable
120
- """
121
- if not self.is_available() or self._parser is None:
122
- return None
123
-
124
- try:
125
- symbols = self.parse_symbols(text)
126
- if symbols is None:
127
- return None
128
-
129
- return IndexedFile(
130
- path=str(path.resolve()),
131
- language=self.language_id,
132
- symbols=symbols,
133
- chunks=[],
134
- )
135
- except Exception:
136
- # Gracefully handle parsing errors
137
- return None
138
-
139
- def _extract_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
140
- """Extract symbols from AST.
141
-
142
- Args:
143
- source_bytes: Source code as bytes
144
- root: Root AST node
145
-
146
- Returns:
147
- List of extracted symbols
148
- """
149
- if self.language_id == "python":
150
- return self._extract_python_symbols(source_bytes, root)
151
- elif self.language_id in {"javascript", "typescript"}:
152
- return self._extract_js_ts_symbols(source_bytes, root)
153
- else:
154
- return []
155
-
156
- def _extract_python_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
157
- """Extract Python symbols from AST.
158
-
159
- Args:
160
- source_bytes: Source code as bytes
161
- root: Root AST node
162
-
163
- Returns:
164
- List of Python symbols (classes, functions, methods)
165
- """
166
- symbols: List[Symbol] = []
167
-
168
- for node in self._iter_nodes(root):
169
- if node.type == "class_definition":
170
- name_node = node.child_by_field_name("name")
171
- if name_node is None:
172
- continue
173
- symbols.append(Symbol(
174
- name=self._node_text(source_bytes, name_node),
175
- kind="class",
176
- range=self._node_range(node),
177
- ))
178
- elif node.type in {"function_definition", "async_function_definition"}:
179
- name_node = node.child_by_field_name("name")
180
- if name_node is None:
181
- continue
182
- symbols.append(Symbol(
183
- name=self._node_text(source_bytes, name_node),
184
- kind=self._python_function_kind(node),
185
- range=self._node_range(node),
186
- ))
187
-
188
- return symbols
189
-
190
- def _extract_js_ts_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
191
- """Extract JavaScript/TypeScript symbols from AST.
192
-
193
- Args:
194
- source_bytes: Source code as bytes
195
- root: Root AST node
196
-
197
- Returns:
198
- List of JS/TS symbols (classes, functions, methods)
199
- """
200
- symbols: List[Symbol] = []
201
-
202
- for node in self._iter_nodes(root):
203
- if node.type in {"class_declaration", "class"}:
204
- name_node = node.child_by_field_name("name")
205
- if name_node is None:
206
- continue
207
- symbols.append(Symbol(
208
- name=self._node_text(source_bytes, name_node),
209
- kind="class",
210
- range=self._node_range(node),
211
- ))
212
- elif node.type in {"function_declaration", "generator_function_declaration"}:
213
- name_node = node.child_by_field_name("name")
214
- if name_node is None:
215
- continue
216
- symbols.append(Symbol(
217
- name=self._node_text(source_bytes, name_node),
218
- kind="function",
219
- range=self._node_range(node),
220
- ))
221
- elif node.type == "variable_declarator":
222
- name_node = node.child_by_field_name("name")
223
- value_node = node.child_by_field_name("value")
224
- if (
225
- name_node is None
226
- or value_node is None
227
- or name_node.type not in {"identifier", "property_identifier"}
228
- or value_node.type != "arrow_function"
229
- ):
230
- continue
231
- symbols.append(Symbol(
232
- name=self._node_text(source_bytes, name_node),
233
- kind="function",
234
- range=self._node_range(node),
235
- ))
236
- elif node.type == "method_definition" and self._has_class_ancestor(node):
237
- name_node = node.child_by_field_name("name")
238
- if name_node is None:
239
- continue
240
- name = self._node_text(source_bytes, name_node)
241
- if name == "constructor":
242
- continue
243
- symbols.append(Symbol(
244
- name=name,
245
- kind="method",
246
- range=self._node_range(node),
247
- ))
248
-
249
- return symbols
250
-
251
- def _python_function_kind(self, node: TreeSitterNode) -> str:
252
- """Determine if Python function is a method or standalone function.
253
-
254
- Args:
255
- node: Function definition node
256
-
257
- Returns:
258
- 'method' if inside a class, 'function' otherwise
259
- """
260
- parent = node.parent
261
- while parent is not None:
262
- if parent.type in {"function_definition", "async_function_definition"}:
263
- return "function"
264
- if parent.type == "class_definition":
265
- return "method"
266
- parent = parent.parent
267
- return "function"
268
-
269
- def _has_class_ancestor(self, node: TreeSitterNode) -> bool:
270
- """Check if node has a class ancestor.
271
-
272
- Args:
273
- node: AST node to check
274
-
275
- Returns:
276
- True if node is inside a class
277
- """
278
- parent = node.parent
279
- while parent is not None:
280
- if parent.type in {"class_declaration", "class"}:
281
- return True
282
- parent = parent.parent
283
- return False
284
-
285
- def _iter_nodes(self, root: TreeSitterNode):
286
- """Iterate over all nodes in AST.
287
-
288
- Args:
289
- root: Root node to start iteration
290
-
291
- Yields:
292
- AST nodes in depth-first order
293
- """
294
- stack = [root]
295
- while stack:
296
- node = stack.pop()
297
- yield node
298
- for child in reversed(node.children):
299
- stack.append(child)
300
-
301
- def _node_text(self, source_bytes: bytes, node: TreeSitterNode) -> str:
302
- """Extract text for a node.
303
-
304
- Args:
305
- source_bytes: Source code as bytes
306
- node: AST node
307
-
308
- Returns:
309
- Text content of node
310
- """
311
- return source_bytes[node.start_byte:node.end_byte].decode("utf8")
312
-
313
- def _node_range(self, node: TreeSitterNode) -> tuple[int, int]:
314
- """Get line range for a node.
315
-
316
- Args:
317
- node: AST node
318
-
319
- Returns:
320
- (start_line, end_line) tuple, 1-based inclusive
321
- """
322
- start_line = node.start_point[0] + 1
323
- end_line = node.end_point[0] + 1
324
- return (start_line, max(start_line, end_line))
325
-
326
- def count_tokens(self, text: str) -> int:
327
- """Count tokens in text.
328
-
329
- Args:
330
- text: Text to count tokens for
331
-
332
- Returns:
333
- Token count
334
- """
335
- return self._tokenizer.count_tokens(text)
1
+ """Tree-sitter based parser for CodexLens.
2
+
3
+ Provides precise AST-level parsing with fallback to regex-based parsing.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import List, Optional
10
+
11
+ try:
12
+ from tree_sitter import Language as TreeSitterLanguage
13
+ from tree_sitter import Node as TreeSitterNode
14
+ from tree_sitter import Parser as TreeSitterParser
15
+ TREE_SITTER_AVAILABLE = True
16
+ except ImportError:
17
+ TreeSitterLanguage = None # type: ignore[assignment]
18
+ TreeSitterNode = None # type: ignore[assignment]
19
+ TreeSitterParser = None # type: ignore[assignment]
20
+ TREE_SITTER_AVAILABLE = False
21
+
22
+ from codexlens.entities import IndexedFile, Symbol
23
+ from codexlens.parsers.tokenizer import get_default_tokenizer
24
+
25
+
26
+ class TreeSitterSymbolParser:
27
+ """Parser using tree-sitter for AST-level symbol extraction."""
28
+
29
+ def __init__(self, language_id: str, path: Optional[Path] = None) -> None:
30
+ """Initialize tree-sitter parser for a language.
31
+
32
+ Args:
33
+ language_id: Language identifier (python, javascript, typescript, etc.)
34
+ path: Optional file path for language variant detection (e.g., .tsx)
35
+ """
36
+ self.language_id = language_id
37
+ self.path = path
38
+ self._parser: Optional[object] = None
39
+ self._language: Optional[TreeSitterLanguage] = None
40
+ self._tokenizer = get_default_tokenizer()
41
+
42
+ if TREE_SITTER_AVAILABLE:
43
+ self._initialize_parser()
44
+
45
+ def _initialize_parser(self) -> None:
46
+ """Initialize tree-sitter parser and language."""
47
+ if TreeSitterParser is None or TreeSitterLanguage is None:
48
+ return
49
+
50
+ try:
51
+ # Load language grammar
52
+ if self.language_id == "python":
53
+ import tree_sitter_python
54
+ self._language = TreeSitterLanguage(tree_sitter_python.language())
55
+ elif self.language_id == "javascript":
56
+ import tree_sitter_javascript
57
+ self._language = TreeSitterLanguage(tree_sitter_javascript.language())
58
+ elif self.language_id == "typescript":
59
+ import tree_sitter_typescript
60
+ # Detect TSX files by extension
61
+ if self.path is not None and self.path.suffix.lower() == ".tsx":
62
+ self._language = TreeSitterLanguage(tree_sitter_typescript.language_tsx())
63
+ else:
64
+ self._language = TreeSitterLanguage(tree_sitter_typescript.language_typescript())
65
+ else:
66
+ return
67
+
68
+ # Create parser
69
+ self._parser = TreeSitterParser()
70
+ if hasattr(self._parser, "set_language"):
71
+ self._parser.set_language(self._language) # type: ignore[attr-defined]
72
+ else:
73
+ self._parser.language = self._language # type: ignore[assignment]
74
+
75
+ except Exception:
76
+ # Gracefully handle missing language bindings
77
+ self._parser = None
78
+ self._language = None
79
+
80
+ def is_available(self) -> bool:
81
+ """Check if tree-sitter parser is available.
82
+
83
+ Returns:
84
+ True if parser is initialized and ready
85
+ """
86
+ return self._parser is not None and self._language is not None
87
+
88
+
89
+ def parse_symbols(self, text: str) -> Optional[List[Symbol]]:
90
+ """Parse source code and extract symbols without creating IndexedFile.
91
+
92
+ Args:
93
+ text: Source code text
94
+
95
+ Returns:
96
+ List of symbols if parsing succeeds, None if tree-sitter unavailable
97
+ """
98
+ if not self.is_available() or self._parser is None:
99
+ return None
100
+
101
+ try:
102
+ source_bytes = text.encode("utf8")
103
+ tree = self._parser.parse(source_bytes) # type: ignore[attr-defined]
104
+ root = tree.root_node
105
+
106
+ return self._extract_symbols(source_bytes, root)
107
+ except Exception:
108
+ # Gracefully handle parsing errors
109
+ return None
110
+
111
+ def parse(self, text: str, path: Path) -> Optional[IndexedFile]:
112
+ """Parse source code and extract symbols.
113
+
114
+ Args:
115
+ text: Source code text
116
+ path: File path
117
+
118
+ Returns:
119
+ IndexedFile if parsing succeeds, None if tree-sitter unavailable
120
+ """
121
+ if not self.is_available() or self._parser is None:
122
+ return None
123
+
124
+ try:
125
+ symbols = self.parse_symbols(text)
126
+ if symbols is None:
127
+ return None
128
+
129
+ return IndexedFile(
130
+ path=str(path.resolve()),
131
+ language=self.language_id,
132
+ symbols=symbols,
133
+ chunks=[],
134
+ )
135
+ except Exception:
136
+ # Gracefully handle parsing errors
137
+ return None
138
+
139
+ def _extract_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
140
+ """Extract symbols from AST.
141
+
142
+ Args:
143
+ source_bytes: Source code as bytes
144
+ root: Root AST node
145
+
146
+ Returns:
147
+ List of extracted symbols
148
+ """
149
+ if self.language_id == "python":
150
+ return self._extract_python_symbols(source_bytes, root)
151
+ elif self.language_id in {"javascript", "typescript"}:
152
+ return self._extract_js_ts_symbols(source_bytes, root)
153
+ else:
154
+ return []
155
+
156
+ def _extract_python_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
157
+ """Extract Python symbols from AST.
158
+
159
+ Args:
160
+ source_bytes: Source code as bytes
161
+ root: Root AST node
162
+
163
+ Returns:
164
+ List of Python symbols (classes, functions, methods)
165
+ """
166
+ symbols: List[Symbol] = []
167
+
168
+ for node in self._iter_nodes(root):
169
+ if node.type == "class_definition":
170
+ name_node = node.child_by_field_name("name")
171
+ if name_node is None:
172
+ continue
173
+ symbols.append(Symbol(
174
+ name=self._node_text(source_bytes, name_node),
175
+ kind="class",
176
+ range=self._node_range(node),
177
+ ))
178
+ elif node.type in {"function_definition", "async_function_definition"}:
179
+ name_node = node.child_by_field_name("name")
180
+ if name_node is None:
181
+ continue
182
+ symbols.append(Symbol(
183
+ name=self._node_text(source_bytes, name_node),
184
+ kind=self._python_function_kind(node),
185
+ range=self._node_range(node),
186
+ ))
187
+
188
+ return symbols
189
+
190
+ def _extract_js_ts_symbols(self, source_bytes: bytes, root: TreeSitterNode) -> List[Symbol]:
191
+ """Extract JavaScript/TypeScript symbols from AST.
192
+
193
+ Args:
194
+ source_bytes: Source code as bytes
195
+ root: Root AST node
196
+
197
+ Returns:
198
+ List of JS/TS symbols (classes, functions, methods)
199
+ """
200
+ symbols: List[Symbol] = []
201
+
202
+ for node in self._iter_nodes(root):
203
+ if node.type in {"class_declaration", "class"}:
204
+ name_node = node.child_by_field_name("name")
205
+ if name_node is None:
206
+ continue
207
+ symbols.append(Symbol(
208
+ name=self._node_text(source_bytes, name_node),
209
+ kind="class",
210
+ range=self._node_range(node),
211
+ ))
212
+ elif node.type in {"function_declaration", "generator_function_declaration"}:
213
+ name_node = node.child_by_field_name("name")
214
+ if name_node is None:
215
+ continue
216
+ symbols.append(Symbol(
217
+ name=self._node_text(source_bytes, name_node),
218
+ kind="function",
219
+ range=self._node_range(node),
220
+ ))
221
+ elif node.type == "variable_declarator":
222
+ name_node = node.child_by_field_name("name")
223
+ value_node = node.child_by_field_name("value")
224
+ if (
225
+ name_node is None
226
+ or value_node is None
227
+ or name_node.type not in {"identifier", "property_identifier"}
228
+ or value_node.type != "arrow_function"
229
+ ):
230
+ continue
231
+ symbols.append(Symbol(
232
+ name=self._node_text(source_bytes, name_node),
233
+ kind="function",
234
+ range=self._node_range(node),
235
+ ))
236
+ elif node.type == "method_definition" and self._has_class_ancestor(node):
237
+ name_node = node.child_by_field_name("name")
238
+ if name_node is None:
239
+ continue
240
+ name = self._node_text(source_bytes, name_node)
241
+ if name == "constructor":
242
+ continue
243
+ symbols.append(Symbol(
244
+ name=name,
245
+ kind="method",
246
+ range=self._node_range(node),
247
+ ))
248
+
249
+ return symbols
250
+
251
+ def _python_function_kind(self, node: TreeSitterNode) -> str:
252
+ """Determine if Python function is a method or standalone function.
253
+
254
+ Args:
255
+ node: Function definition node
256
+
257
+ Returns:
258
+ 'method' if inside a class, 'function' otherwise
259
+ """
260
+ parent = node.parent
261
+ while parent is not None:
262
+ if parent.type in {"function_definition", "async_function_definition"}:
263
+ return "function"
264
+ if parent.type == "class_definition":
265
+ return "method"
266
+ parent = parent.parent
267
+ return "function"
268
+
269
+ def _has_class_ancestor(self, node: TreeSitterNode) -> bool:
270
+ """Check if node has a class ancestor.
271
+
272
+ Args:
273
+ node: AST node to check
274
+
275
+ Returns:
276
+ True if node is inside a class
277
+ """
278
+ parent = node.parent
279
+ while parent is not None:
280
+ if parent.type in {"class_declaration", "class"}:
281
+ return True
282
+ parent = parent.parent
283
+ return False
284
+
285
+ def _iter_nodes(self, root: TreeSitterNode):
286
+ """Iterate over all nodes in AST.
287
+
288
+ Args:
289
+ root: Root node to start iteration
290
+
291
+ Yields:
292
+ AST nodes in depth-first order
293
+ """
294
+ stack = [root]
295
+ while stack:
296
+ node = stack.pop()
297
+ yield node
298
+ for child in reversed(node.children):
299
+ stack.append(child)
300
+
301
+ def _node_text(self, source_bytes: bytes, node: TreeSitterNode) -> str:
302
+ """Extract text for a node.
303
+
304
+ Args:
305
+ source_bytes: Source code as bytes
306
+ node: AST node
307
+
308
+ Returns:
309
+ Text content of node
310
+ """
311
+ return source_bytes[node.start_byte:node.end_byte].decode("utf8")
312
+
313
+ def _node_range(self, node: TreeSitterNode) -> tuple[int, int]:
314
+ """Get line range for a node.
315
+
316
+ Args:
317
+ node: AST node
318
+
319
+ Returns:
320
+ (start_line, end_line) tuple, 1-based inclusive
321
+ """
322
+ start_line = node.start_point[0] + 1
323
+ end_line = node.end_point[0] + 1
324
+ return (start_line, max(start_line, end_line))
325
+
326
+ def count_tokens(self, text: str) -> int:
327
+ """Count tokens in text.
328
+
329
+ Args:
330
+ text: Text to count tokens for
331
+
332
+ Returns:
333
+ Token count
334
+ """
335
+ return self._tokenizer.count_tokens(text)