crabcode 0.1.6__py3-none-win_amd64.whl

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 (205) hide show
  1. crabcode-0.1.6.dist-info/METADATA +63 -0
  2. crabcode-0.1.6.dist-info/RECORD +205 -0
  3. crabcode-0.1.6.dist-info/WHEEL +4 -0
  4. crabcode-0.1.6.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +7 -0
  6. crabcode_cli/__main__.py +4 -0
  7. crabcode_cli/_native/crabcode-computer-use-host.exe +0 -0
  8. crabcode_cli/_native/runtime.json +6 -0
  9. crabcode_cli/app.py +776 -0
  10. crabcode_cli/banner.py +39 -0
  11. crabcode_cli/computer_use.py +286 -0
  12. crabcode_cli/model_manager.py +649 -0
  13. crabcode_cli/model_picker.py +168 -0
  14. crabcode_cli/pipe.py +148 -0
  15. crabcode_cli/render/__init__.py +0 -0
  16. crabcode_cli/render/messages.py +19 -0
  17. crabcode_cli/repl.py +4841 -0
  18. crabcode_cli/session_picker.py +239 -0
  19. crabcode_core/__init__.py +7 -0
  20. crabcode_core/_version.py +3 -0
  21. crabcode_core/_windows_process.py +68 -0
  22. crabcode_core/agent_manager.py +1731 -0
  23. crabcode_core/api/__init__.py +20 -0
  24. crabcode_core/api/anthropic_adapter.py +648 -0
  25. crabcode_core/api/azure_adapter.py +69 -0
  26. crabcode_core/api/base.py +233 -0
  27. crabcode_core/api/codex_adapter.py +1327 -0
  28. crabcode_core/api/error_logging.py +132 -0
  29. crabcode_core/api/gemini_adapter.py +26 -0
  30. crabcode_core/api/model_info.py +257 -0
  31. crabcode_core/api/network.py +62 -0
  32. crabcode_core/api/ollama_adapter.py +27 -0
  33. crabcode_core/api/openai_adapter.py +323 -0
  34. crabcode_core/api/registry.py +58 -0
  35. crabcode_core/command_line.py +18 -0
  36. crabcode_core/compact/__init__.py +17 -0
  37. crabcode_core/compact/compact.py +514 -0
  38. crabcode_core/compact/context_tokens.py +165 -0
  39. crabcode_core/computer_use_mode.py +38 -0
  40. crabcode_core/computer_use_observation.py +144 -0
  41. crabcode_core/config/__init__.py +3 -0
  42. crabcode_core/config/claudemd.py +61 -0
  43. crabcode_core/config/manager.py +237 -0
  44. crabcode_core/config/model_catalog.py +256 -0
  45. crabcode_core/events.py +4896 -0
  46. crabcode_core/file_lock.py +81 -0
  47. crabcode_core/filesystem.py +22 -0
  48. crabcode_core/goal.py +149 -0
  49. crabcode_core/hooks/__init__.py +5 -0
  50. crabcode_core/hooks/manager.py +290 -0
  51. crabcode_core/image_io.py +111 -0
  52. crabcode_core/io_worker.py +211 -0
  53. crabcode_core/logging_utils.py +168 -0
  54. crabcode_core/lsp/__init__.py +6 -0
  55. crabcode_core/lsp/client.py +786 -0
  56. crabcode_core/lsp/diagnostics.py +150 -0
  57. crabcode_core/lsp/manager.py +583 -0
  58. crabcode_core/mcp/__init__.py +4 -0
  59. crabcode_core/mcp/client.py +169 -0
  60. crabcode_core/mcp/config.py +57 -0
  61. crabcode_core/path_validation.py +34 -0
  62. crabcode_core/paths.py +24 -0
  63. crabcode_core/peer/__init__.py +10 -0
  64. crabcode_core/peer/runtime.py +518 -0
  65. crabcode_core/permissions/__init__.py +7 -0
  66. crabcode_core/permissions/ai_reviewer.py +184 -0
  67. crabcode_core/permissions/manager.py +187 -0
  68. crabcode_core/plan/__init__.py +5 -0
  69. crabcode_core/plan/executor.py +270 -0
  70. crabcode_core/plan/types.py +169 -0
  71. crabcode_core/prompts/__init__.py +0 -0
  72. crabcode_core/prompts/blocks.py +10 -0
  73. crabcode_core/prompts/context.py +229 -0
  74. crabcode_core/prompts/library.py +92 -0
  75. crabcode_core/prompts/profile.py +79 -0
  76. crabcode_core/prompts/system.py +356 -0
  77. crabcode_core/prompts/templates.py +104 -0
  78. crabcode_core/query/__init__.py +0 -0
  79. crabcode_core/query/computer_use_history.py +83 -0
  80. crabcode_core/query/loop.py +2446 -0
  81. crabcode_core/query/retry.py +116 -0
  82. crabcode_core/schedule/__init__.py +13 -0
  83. crabcode_core/schedule/manager.py +631 -0
  84. crabcode_core/schedule/models.py +204 -0
  85. crabcode_core/schedule/store.py +558 -0
  86. crabcode_core/schedule/timing.py +287 -0
  87. crabcode_core/session/__init__.py +12 -0
  88. crabcode_core/session/export.py +141 -0
  89. crabcode_core/session/meta_db.py +684 -0
  90. crabcode_core/session/storage.py +2032 -0
  91. crabcode_core/session/title_gen.py +52 -0
  92. crabcode_core/skills/__init__.py +6 -0
  93. crabcode_core/skills/context.py +23 -0
  94. crabcode_core/skills/loader.py +214 -0
  95. crabcode_core/skills/matcher.py +203 -0
  96. crabcode_core/snapshot/__init__.py +21 -0
  97. crabcode_core/snapshot/snapshot.py +436 -0
  98. crabcode_core/snapshot/tracker.py +359 -0
  99. crabcode_core/subprocess_utils.py +260 -0
  100. crabcode_core/team/__init__.py +27 -0
  101. crabcode_core/team/bridge.py +66 -0
  102. crabcode_core/team/inbox.py +237 -0
  103. crabcode_core/team/manager.py +1660 -0
  104. crabcode_core/team/message_bus.py +821 -0
  105. crabcode_core/team/models.py +191 -0
  106. crabcode_core/team/recovery.py +192 -0
  107. crabcode_core/text_io.py +52 -0
  108. crabcode_core/tools/__init__.py +112 -0
  109. crabcode_core/tools/_input_helpers.py +39 -0
  110. crabcode_core/tools/_input_schema.py +130 -0
  111. crabcode_core/tools/agent.py +413 -0
  112. crabcode_core/tools/apply_patch.py +587 -0
  113. crabcode_core/tools/ask_user.py +122 -0
  114. crabcode_core/tools/bash.py +154 -0
  115. crabcode_core/tools/browser.py +678 -0
  116. crabcode_core/tools/checklist.py +241 -0
  117. crabcode_core/tools/checkpoint.py +78 -0
  118. crabcode_core/tools/computer_use.py +599 -0
  119. crabcode_core/tools/diff_utils.py +123 -0
  120. crabcode_core/tools/file_edit.py +240 -0
  121. crabcode_core/tools/file_read.py +187 -0
  122. crabcode_core/tools/file_write.py +154 -0
  123. crabcode_core/tools/glob.py +123 -0
  124. crabcode_core/tools/goal.py +134 -0
  125. crabcode_core/tools/grep.py +247 -0
  126. crabcode_core/tools/image.py +173 -0
  127. crabcode_core/tools/image_generate.py +115 -0
  128. crabcode_core/tools/lint.py +388 -0
  129. crabcode_core/tools/loading.py +187 -0
  130. crabcode_core/tools/memory.py +456 -0
  131. crabcode_core/tools/monitor.py +677 -0
  132. crabcode_core/tools/peer.py +103 -0
  133. crabcode_core/tools/revert.py +90 -0
  134. crabcode_core/tools/schedule.py +683 -0
  135. crabcode_core/tools/skill.py +108 -0
  136. crabcode_core/tools/switch_mode.py +213 -0
  137. crabcode_core/tools/team.py +329 -0
  138. crabcode_core/tools/tool_search.py +64 -0
  139. crabcode_core/tools/web_search.py +444 -0
  140. crabcode_core/turn_details.py +64 -0
  141. crabcode_core/types/__init__.py +95 -0
  142. crabcode_core/types/config.py +434 -0
  143. crabcode_core/types/event.py +334 -0
  144. crabcode_core/types/message.py +263 -0
  145. crabcode_core/types/tool.py +208 -0
  146. crabcode_core/usage.py +289 -0
  147. crabcode_core/utf8_sanitize.py +29 -0
  148. crabcode_debugger/__init__.py +5 -0
  149. crabcode_debugger/adapters.py +332 -0
  150. crabcode_debugger/dap.py +304 -0
  151. crabcode_debugger/memory.py +1806 -0
  152. crabcode_debugger/process.py +707 -0
  153. crabcode_debugger/runtime.py +58 -0
  154. crabcode_debugger/sessions.py +387 -0
  155. crabcode_debugger/tool.py +731 -0
  156. crabcode_gateway/__init__.py +18 -0
  157. crabcode_gateway/acp/__init__.py +13 -0
  158. crabcode_gateway/acp/agent.py +1581 -0
  159. crabcode_gateway/acp/session.py +114 -0
  160. crabcode_gateway/acp/transport.py +39 -0
  161. crabcode_gateway/acp/types.py +90 -0
  162. crabcode_gateway/adapter.py +30 -0
  163. crabcode_gateway/auth.py +139 -0
  164. crabcode_gateway/computer_use.py +364 -0
  165. crabcode_gateway/document_engine.py +637 -0
  166. crabcode_gateway/document_worker.py +301 -0
  167. crabcode_gateway/event_bus.py +655 -0
  168. crabcode_gateway/grpc/__init__.py +0 -0
  169. crabcode_gateway/grpc/crabcode_pb2.py +70 -0
  170. crabcode_gateway/grpc/crabcode_pb2_grpc.py +474 -0
  171. crabcode_gateway/grpc/proto/crabcode.proto +160 -0
  172. crabcode_gateway/grpc/server.py +765 -0
  173. crabcode_gateway/middleware.py +344 -0
  174. crabcode_gateway/protocol.py +17 -0
  175. crabcode_gateway/routes/__init__.py +0 -0
  176. crabcode_gateway/routes/agent.py +392 -0
  177. crabcode_gateway/routes/auth.py +150 -0
  178. crabcode_gateway/routes/computer_use.py +101 -0
  179. crabcode_gateway/routes/config.py +1698 -0
  180. crabcode_gateway/routes/document.py +1810 -0
  181. crabcode_gateway/routes/event.py +3801 -0
  182. crabcode_gateway/routes/git_review.py +232 -0
  183. crabcode_gateway/routes/health.py +14 -0
  184. crabcode_gateway/routes/peer.py +78 -0
  185. crabcode_gateway/routes/permission.py +121 -0
  186. crabcode_gateway/routes/schedule.py +300 -0
  187. crabcode_gateway/routes/session.py +1496 -0
  188. crabcode_gateway/routes/snapshot.py +211 -0
  189. crabcode_gateway/routes/tasks.py +318 -0
  190. crabcode_gateway/routes/team.py +392 -0
  191. crabcode_gateway/routes/usage.py +49 -0
  192. crabcode_gateway/routes/workspace.py +299 -0
  193. crabcode_gateway/safe_file.py +63 -0
  194. crabcode_gateway/schemas.py +1820 -0
  195. crabcode_gateway/server.py +514 -0
  196. crabcode_gateway/session_registry.py +36 -0
  197. crabcode_gateway/task_registry.py +708 -0
  198. crabcode_gateway/turn_details.py +60 -0
  199. crabcode_search/__init__.py +11 -0
  200. crabcode_search/background.py +368 -0
  201. crabcode_search/chunker.py +517 -0
  202. crabcode_search/embedder.py +497 -0
  203. crabcode_search/indexer.py +215 -0
  204. crabcode_search/store.py +222 -0
  205. crabcode_search/tool.py +268 -0
@@ -0,0 +1,517 @@
1
+ """Code chunker — splits source files into semantic chunks.
2
+
3
+ Uses tree-sitter AST parsing when available (pip install crabcode-search[ast]),
4
+ falling back to regex-based boundary detection otherwise.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ try:
15
+ from tree_sitter_language_pack import get_parser as _ts_get_parser
16
+
17
+ _HAS_TREE_SITTER = True
18
+ except ImportError:
19
+ _HAS_TREE_SITTER = False
20
+
21
+ MAX_CHUNK_LINES = 80
22
+ MIN_CHUNK_LINES = 3
23
+
24
+
25
+ @dataclass
26
+ class ChunkMeta:
27
+ file_path: str
28
+ start_line: int
29
+ end_line: int
30
+ signature: str
31
+ content: str
32
+
33
+ @property
34
+ def display(self) -> str:
35
+ return f"{self.file_path}:{self.start_line}-{self.end_line}"
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Public API
40
+ # ---------------------------------------------------------------------------
41
+
42
+ def chunk_file(file_path: str, content: str) -> list[ChunkMeta]:
43
+ """Split a single file into semantic chunks.
44
+
45
+ Tries tree-sitter AST parsing first; falls back to regex if unavailable.
46
+ """
47
+ if not content.strip():
48
+ return []
49
+
50
+ if _HAS_TREE_SITTER:
51
+ lang = _ext_to_ts_language(file_path)
52
+ if lang:
53
+ try:
54
+ return _chunk_file_treesitter(file_path, content, lang)
55
+ except Exception:
56
+ pass
57
+
58
+ return _chunk_file_regex(file_path, content)
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Tree-sitter AST chunking
63
+ # ---------------------------------------------------------------------------
64
+
65
+ _EXT_TO_LANGUAGE: dict[str, str] = {
66
+ ".py": "python",
67
+ ".js": "javascript",
68
+ ".jsx": "javascript",
69
+ ".ts": "typescript",
70
+ ".tsx": "tsx",
71
+ ".rs": "rust",
72
+ ".go": "go",
73
+ ".java": "java",
74
+ ".c": "c",
75
+ ".h": "c",
76
+ ".cpp": "cpp",
77
+ ".cc": "cpp",
78
+ ".cxx": "cpp",
79
+ ".hpp": "cpp",
80
+ ".hxx": "cpp",
81
+ ".cs": "c_sharp",
82
+ ".rb": "ruby",
83
+ ".swift": "swift",
84
+ ".kt": "kotlin",
85
+ ".kts": "kotlin",
86
+ ".scala": "scala",
87
+ ".lua": "lua",
88
+ ".php": "php",
89
+ ".sh": "bash",
90
+ ".bash": "bash",
91
+ ".zig": "zig",
92
+ ".ex": "elixir",
93
+ ".exs": "elixir",
94
+ ".erl": "erlang",
95
+ ".hs": "haskell",
96
+ ".ml": "ocaml",
97
+ ".mli": "ocaml",
98
+ ".jl": "julia",
99
+ ".dart": "dart",
100
+ ".r": "r",
101
+ ".sql": "sql",
102
+ ".html": "html",
103
+ ".css": "css",
104
+ ".scss": "scss",
105
+ }
106
+
107
+ _DEFINITION_TYPES: dict[str, frozenset[str]] = {
108
+ "python": frozenset({
109
+ "function_definition", "class_definition", "decorated_definition",
110
+ }),
111
+ "javascript": frozenset({
112
+ "function_declaration", "class_declaration", "export_statement",
113
+ "lexical_declaration", "variable_declaration",
114
+ }),
115
+ "typescript": frozenset({
116
+ "function_declaration", "class_declaration", "export_statement",
117
+ "lexical_declaration", "interface_declaration",
118
+ "type_alias_declaration", "enum_declaration",
119
+ }),
120
+ "tsx": frozenset({
121
+ "function_declaration", "class_declaration", "export_statement",
122
+ "lexical_declaration", "interface_declaration",
123
+ "type_alias_declaration", "enum_declaration",
124
+ }),
125
+ "rust": frozenset({
126
+ "function_item", "impl_item", "struct_item", "enum_item",
127
+ "trait_item", "mod_item", "const_item", "static_item",
128
+ "type_item", "macro_definition",
129
+ }),
130
+ "go": frozenset({
131
+ "function_declaration", "method_declaration", "type_declaration",
132
+ }),
133
+ "java": frozenset({
134
+ "class_declaration", "interface_declaration", "method_declaration",
135
+ "enum_declaration", "record_declaration",
136
+ }),
137
+ "c": frozenset({
138
+ "function_definition", "struct_specifier", "enum_specifier",
139
+ "type_definition",
140
+ }),
141
+ "cpp": frozenset({
142
+ "function_definition", "class_specifier", "struct_specifier",
143
+ "enum_specifier", "namespace_definition", "template_declaration",
144
+ }),
145
+ "c_sharp": frozenset({
146
+ "class_declaration", "interface_declaration", "method_declaration",
147
+ "enum_declaration", "struct_declaration", "namespace_declaration",
148
+ }),
149
+ "ruby": frozenset({
150
+ "method", "class", "module", "singleton_method",
151
+ }),
152
+ "swift": frozenset({
153
+ "function_declaration", "class_declaration", "struct_declaration",
154
+ "enum_declaration", "protocol_declaration",
155
+ }),
156
+ "kotlin": frozenset({
157
+ "function_declaration", "class_declaration", "object_declaration",
158
+ }),
159
+ "scala": frozenset({
160
+ "function_definition", "class_definition", "object_definition",
161
+ "trait_definition",
162
+ }),
163
+ "php": frozenset({
164
+ "function_definition", "class_declaration", "method_declaration",
165
+ }),
166
+ "lua": frozenset({
167
+ "function_declaration", "local_function",
168
+ }),
169
+ "elixir": frozenset({
170
+ "call",
171
+ }),
172
+ "haskell": frozenset({
173
+ "function", "type_class_declaration", "data_declaration",
174
+ }),
175
+ }
176
+
177
+ _CONTAINER_TYPES: dict[str, frozenset[str]] = {
178
+ "python": frozenset({"class_definition", "decorated_definition"}),
179
+ "javascript": frozenset({"class_declaration", "class_body"}),
180
+ "typescript": frozenset({"class_declaration", "class_body"}),
181
+ "tsx": frozenset({"class_declaration", "class_body"}),
182
+ "rust": frozenset({"impl_item", "trait_item", "mod_item"}),
183
+ "go": frozenset(),
184
+ "java": frozenset({"class_declaration", "class_body", "interface_declaration"}),
185
+ "c": frozenset(),
186
+ "cpp": frozenset({"class_specifier", "struct_specifier", "namespace_definition"}),
187
+ "c_sharp": frozenset({"class_declaration", "struct_declaration", "namespace_declaration"}),
188
+ "ruby": frozenset({"class", "module"}),
189
+ "swift": frozenset({"class_declaration", "struct_declaration"}),
190
+ "kotlin": frozenset({"class_declaration", "object_declaration"}),
191
+ "scala": frozenset({"class_definition", "object_definition", "trait_definition"}),
192
+ "php": frozenset({"class_declaration"}),
193
+ }
194
+
195
+ _METHOD_TYPES: dict[str, frozenset[str]] = {
196
+ "python": frozenset({"function_definition", "decorated_definition"}),
197
+ "javascript": frozenset({"method_definition"}),
198
+ "typescript": frozenset({"method_definition", "public_field_definition"}),
199
+ "tsx": frozenset({"method_definition", "public_field_definition"}),
200
+ "rust": frozenset({"function_item"}),
201
+ "java": frozenset({"method_declaration", "constructor_declaration"}),
202
+ "cpp": frozenset({"function_definition"}),
203
+ "c_sharp": frozenset({"method_declaration", "constructor_declaration"}),
204
+ "ruby": frozenset({"method", "singleton_method"}),
205
+ "swift": frozenset({"function_declaration"}),
206
+ "kotlin": frozenset({"function_declaration"}),
207
+ "scala": frozenset({"function_definition"}),
208
+ "php": frozenset({"method_declaration"}),
209
+ }
210
+
211
+
212
+ def _ext_to_ts_language(file_path: str) -> str | None:
213
+ ext = Path(file_path).suffix.lower()
214
+ return _EXT_TO_LANGUAGE.get(ext)
215
+
216
+
217
+ def _chunk_file_treesitter(
218
+ file_path: str, content: str, language: str
219
+ ) -> list[ChunkMeta]:
220
+ parser = _ts_get_parser(language)
221
+ tree = parser.parse(content.encode("utf-8", errors="replace"))
222
+ root = tree.root_node
223
+
224
+ lines = content.splitlines(keepends=True)
225
+ definition_types = _DEFINITION_TYPES.get(language, frozenset())
226
+ container_types = _CONTAINER_TYPES.get(language, frozenset())
227
+ method_types = _METHOD_TYPES.get(language, frozenset())
228
+
229
+ chunks: list[ChunkMeta] = []
230
+ header_nodes: list[Any] = []
231
+
232
+ for child in root.children:
233
+ if child.type in definition_types:
234
+ if header_nodes:
235
+ _flush_header(file_path, lines, header_nodes, chunks)
236
+ header_nodes = []
237
+
238
+ node_lines = child.end_point[0] - child.start_point[0] + 1
239
+
240
+ if (
241
+ node_lines > MAX_CHUNK_LINES
242
+ and child.type in container_types
243
+ and method_types
244
+ ):
245
+ _chunk_container_node(
246
+ file_path, lines, child, language,
247
+ container_types, method_types, chunks,
248
+ )
249
+ else:
250
+ _add_node_chunk(file_path, lines, child, chunks)
251
+ else:
252
+ header_nodes.append(child)
253
+
254
+ if header_nodes:
255
+ _flush_header(file_path, lines, header_nodes, chunks)
256
+
257
+ return chunks
258
+
259
+
260
+ def _add_node_chunk(
261
+ file_path: str,
262
+ lines: list[str],
263
+ node: Any,
264
+ chunks: list[ChunkMeta],
265
+ ) -> None:
266
+ start = node.start_point[0]
267
+ end = min(node.end_point[0] + 1, len(lines))
268
+
269
+ if end - start > MAX_CHUNK_LINES:
270
+ end = start + MAX_CHUNK_LINES
271
+
272
+ chunk_lines = lines[start:end]
273
+ text = "".join(chunk_lines).strip()
274
+ if not text or len(chunk_lines) < MIN_CHUNK_LINES:
275
+ return
276
+
277
+ sig = _node_signature(node, lines)
278
+ chunks.append(ChunkMeta(
279
+ file_path=file_path,
280
+ start_line=start + 1,
281
+ end_line=end,
282
+ signature=sig,
283
+ content=text,
284
+ ))
285
+
286
+
287
+ _BODY_BLOCK_TYPES = frozenset({
288
+ "block", "body", "class_body", "enum_body", "impl_body",
289
+ "declaration_list", "field_declaration_list", "statement_block",
290
+ "compound_statement",
291
+ })
292
+
293
+
294
+ def _chunk_container_node(
295
+ file_path: str,
296
+ lines: list[str],
297
+ node: Any,
298
+ language: str,
299
+ container_types: frozenset[str],
300
+ method_types: frozenset[str],
301
+ chunks: list[ChunkMeta],
302
+ ) -> None:
303
+ """Split a large class/impl/trait into header + individual methods."""
304
+ children_defs: list[Any] = []
305
+
306
+ def _find_methods(n: Any) -> None:
307
+ for child in n.children:
308
+ if child.type in method_types:
309
+ children_defs.append(child)
310
+ elif (
311
+ child.type in container_types
312
+ or child.type in _BODY_BLOCK_TYPES
313
+ or child.type.endswith("_body")
314
+ ):
315
+ _find_methods(child)
316
+
317
+ _find_methods(node)
318
+
319
+ if not children_defs:
320
+ _add_node_chunk(file_path, lines, node, chunks)
321
+ return
322
+
323
+ header_start = node.start_point[0]
324
+ first_method_start = children_defs[0].start_point[0]
325
+
326
+ if first_method_start > header_start:
327
+ header_end = min(first_method_start, header_start + MAX_CHUNK_LINES)
328
+ header_text = "".join(lines[header_start:header_end]).strip()
329
+ if header_text and (header_end - header_start) >= MIN_CHUNK_LINES:
330
+ sig = _node_signature(node, lines)
331
+ chunks.append(ChunkMeta(
332
+ file_path=file_path,
333
+ start_line=header_start + 1,
334
+ end_line=header_end,
335
+ signature=sig,
336
+ content=header_text,
337
+ ))
338
+
339
+ for method_node in children_defs:
340
+ _add_node_chunk(file_path, lines, method_node, chunks)
341
+
342
+
343
+ def _flush_header(
344
+ file_path: str,
345
+ lines: list[str],
346
+ nodes: list[Any],
347
+ chunks: list[ChunkMeta],
348
+ ) -> None:
349
+ if not nodes:
350
+ return
351
+ start = nodes[0].start_point[0]
352
+ end = nodes[-1].end_point[0] + 1
353
+ if end - start < MIN_CHUNK_LINES:
354
+ return
355
+
356
+ text = "".join(lines[start:end]).strip()
357
+ if not text:
358
+ return
359
+
360
+ if end - start > MAX_CHUNK_LINES:
361
+ end = start + MAX_CHUNK_LINES
362
+ text = "".join(lines[start:end]).strip()
363
+
364
+ chunks.append(ChunkMeta(
365
+ file_path=file_path,
366
+ start_line=start + 1,
367
+ end_line=end,
368
+ signature="(module header)",
369
+ content=text,
370
+ ))
371
+
372
+
373
+ def _node_signature(node: Any, lines: list[str]) -> str:
374
+ first_line = lines[node.start_point[0]].strip() if node.start_point[0] < len(lines) else ""
375
+ if len(first_line) > 120:
376
+ first_line = first_line[:117] + "..."
377
+ return first_line
378
+
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # Regex fallback chunking
382
+ # ---------------------------------------------------------------------------
383
+
384
+ _FUNC_CLASS_RE = re.compile(
385
+ r"^(?:"
386
+ r"(?:export\s+)?(?:async\s+)?(?:def|class|function|fn|func|pub\s+fn|pub\s+async\s+fn)"
387
+ r"|(?:export\s+)?(?:const|let|var)\s+\w+\s*="
388
+ r"|(?:public|private|protected|static|\s)*(?:class|interface|enum|record)\s"
389
+ r"|(?:public|private|protected|static|\s)*\w[\w<>\[\],\s]*\s+\w+\s*\("
390
+ r")\s*",
391
+ re.MULTILINE,
392
+ )
393
+
394
+
395
+ def _chunk_file_regex(file_path: str, content: str) -> list[ChunkMeta]:
396
+ """Regex-based chunking — used when tree-sitter is not available."""
397
+ lines = content.splitlines(keepends=True)
398
+ if not lines:
399
+ return []
400
+
401
+ boundaries: list[int] = []
402
+ for i, line in enumerate(lines):
403
+ if _FUNC_CLASS_RE.match(line):
404
+ boundaries.append(i)
405
+
406
+ if boundaries:
407
+ return _chunk_by_boundaries(file_path, lines, boundaries)
408
+ return _chunk_by_paragraphs(file_path, lines)
409
+
410
+
411
+ def _chunk_by_boundaries(
412
+ file_path: str,
413
+ lines: list[str],
414
+ boundaries: list[int],
415
+ ) -> list[ChunkMeta]:
416
+ chunks: list[ChunkMeta] = []
417
+
418
+ if boundaries[0] > 0:
419
+ header_lines = lines[: boundaries[0]]
420
+ header_text = "".join(header_lines).strip()
421
+ if header_text and len(header_lines) >= MIN_CHUNK_LINES:
422
+ chunks.append(ChunkMeta(
423
+ file_path=file_path,
424
+ start_line=1,
425
+ end_line=boundaries[0],
426
+ signature="(module header)",
427
+ content=header_text,
428
+ ))
429
+
430
+ for idx, start in enumerate(boundaries):
431
+ end = boundaries[idx + 1] if idx + 1 < len(boundaries) else len(lines)
432
+
433
+ if end - start > MAX_CHUNK_LINES:
434
+ end = start + MAX_CHUNK_LINES
435
+
436
+ chunk_lines = lines[start:end]
437
+ text = "".join(chunk_lines).strip()
438
+ if not text or len(chunk_lines) < MIN_CHUNK_LINES:
439
+ continue
440
+
441
+ sig = lines[start].strip()
442
+ if len(sig) > 120:
443
+ sig = sig[:117] + "..."
444
+
445
+ chunks.append(ChunkMeta(
446
+ file_path=file_path,
447
+ start_line=start + 1,
448
+ end_line=end,
449
+ signature=sig,
450
+ content=text,
451
+ ))
452
+
453
+ return chunks
454
+
455
+
456
+ def _chunk_by_paragraphs(
457
+ file_path: str,
458
+ lines: list[str],
459
+ ) -> list[ChunkMeta]:
460
+ """Fallback: split by blank-line paragraphs, capped at MAX_CHUNK_LINES."""
461
+ chunks: list[ChunkMeta] = []
462
+ current_start = 0
463
+ current_lines: list[str] = []
464
+
465
+ def _flush() -> None:
466
+ if not current_lines:
467
+ return
468
+ text = "".join(current_lines).strip()
469
+ if text and len(current_lines) >= MIN_CHUNK_LINES:
470
+ sig = current_lines[0].strip()
471
+ if len(sig) > 120:
472
+ sig = sig[:117] + "..."
473
+ chunks.append(ChunkMeta(
474
+ file_path=file_path,
475
+ start_line=current_start + 1,
476
+ end_line=current_start + len(current_lines),
477
+ signature=sig,
478
+ content=text,
479
+ ))
480
+
481
+ for i, line in enumerate(lines):
482
+ if line.strip() == "" and len(current_lines) >= MIN_CHUNK_LINES:
483
+ _flush()
484
+ current_start = i + 1
485
+ current_lines = []
486
+ else:
487
+ current_lines.append(line)
488
+ if len(current_lines) >= MAX_CHUNK_LINES:
489
+ _flush()
490
+ current_start = i + 1
491
+ current_lines = []
492
+
493
+ _flush()
494
+ return chunks
495
+
496
+
497
+ # ---------------------------------------------------------------------------
498
+ # File type detection
499
+ # ---------------------------------------------------------------------------
500
+
501
+ _TEXT_EXTENSIONS = frozenset({
502
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".rs", ".go",
503
+ ".java", ".c", ".cpp", ".cc", ".cxx", ".h", ".hpp", ".hxx",
504
+ ".cs", ".rb", ".swift", ".kt", ".kts", ".scala", ".lua",
505
+ ".php", ".sh", ".bash", ".zsh", ".fish", ".r", ".m", ".mm",
506
+ ".toml", ".yaml", ".yml", ".json", ".xml", ".html",
507
+ ".css", ".scss", ".less", ".sql", ".graphql", ".proto",
508
+ ".md", ".txt", ".rst", ".tex", ".el", ".vim",
509
+ ".zig", ".nim", ".ex", ".exs", ".erl", ".hrl",
510
+ ".clj", ".cljs", ".hs", ".ml", ".mli", ".f90",
511
+ ".jl", ".dart", ".v", ".sv",
512
+ })
513
+
514
+
515
+ def is_indexable(path: Path) -> bool:
516
+ """Check if a file should be indexed."""
517
+ return path.suffix.lower() in _TEXT_EXTENSIONS