crabcode 0.1.0__py3-none-any.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 (64) hide show
  1. crabcode-0.1.0.dist-info/METADATA +23 -0
  2. crabcode-0.1.0.dist-info/RECORD +64 -0
  3. crabcode-0.1.0.dist-info/WHEEL +4 -0
  4. crabcode-0.1.0.dist-info/entry_points.txt +2 -0
  5. crabcode_cli/__init__.py +3 -0
  6. crabcode_cli/app.py +142 -0
  7. crabcode_cli/banner.py +42 -0
  8. crabcode_cli/pipe.py +58 -0
  9. crabcode_cli/render/__init__.py +0 -0
  10. crabcode_cli/render/messages.py +19 -0
  11. crabcode_cli/repl.py +1263 -0
  12. crabcode_core/__init__.py +3 -0
  13. crabcode_core/api/__init__.py +6 -0
  14. crabcode_core/api/anthropic_adapter.py +250 -0
  15. crabcode_core/api/base.py +66 -0
  16. crabcode_core/api/codex_adapter.py +390 -0
  17. crabcode_core/api/openai_adapter.py +221 -0
  18. crabcode_core/api/registry.py +40 -0
  19. crabcode_core/compact/__init__.py +7 -0
  20. crabcode_core/compact/compact.py +150 -0
  21. crabcode_core/config/__init__.py +3 -0
  22. crabcode_core/config/claudemd.py +53 -0
  23. crabcode_core/config/manager.py +152 -0
  24. crabcode_core/events.py +392 -0
  25. crabcode_core/mcp/__init__.py +4 -0
  26. crabcode_core/mcp/client.py +163 -0
  27. crabcode_core/mcp/config.py +52 -0
  28. crabcode_core/permissions/__init__.py +3 -0
  29. crabcode_core/permissions/manager.py +106 -0
  30. crabcode_core/prompts/__init__.py +0 -0
  31. crabcode_core/prompts/context.py +209 -0
  32. crabcode_core/prompts/profile.py +66 -0
  33. crabcode_core/prompts/system.py +405 -0
  34. crabcode_core/prompts/templates.py +60 -0
  35. crabcode_core/query/__init__.py +0 -0
  36. crabcode_core/query/loop.py +442 -0
  37. crabcode_core/session/__init__.py +3 -0
  38. crabcode_core/session/storage.py +153 -0
  39. crabcode_core/skills/__init__.py +5 -0
  40. crabcode_core/skills/loader.py +147 -0
  41. crabcode_core/tools/__init__.py +29 -0
  42. crabcode_core/tools/agent.py +160 -0
  43. crabcode_core/tools/bash.py +127 -0
  44. crabcode_core/tools/diff_utils.py +120 -0
  45. crabcode_core/tools/file_edit.py +145 -0
  46. crabcode_core/tools/file_read.py +123 -0
  47. crabcode_core/tools/file_write.py +102 -0
  48. crabcode_core/tools/glob.py +109 -0
  49. crabcode_core/tools/grep.py +130 -0
  50. crabcode_core/tools/lint.py +368 -0
  51. crabcode_core/tools/memory.py +236 -0
  52. crabcode_core/tools/skill.py +112 -0
  53. crabcode_core/types/__init__.py +73 -0
  54. crabcode_core/types/config.py +103 -0
  55. crabcode_core/types/event.py +105 -0
  56. crabcode_core/types/message.py +177 -0
  57. crabcode_core/types/tool.py +138 -0
  58. crabcode_search/__init__.py +11 -0
  59. crabcode_search/background.py +371 -0
  60. crabcode_search/chunker.py +517 -0
  61. crabcode_search/embedder.py +447 -0
  62. crabcode_search/indexer.py +212 -0
  63. crabcode_search/store.py +222 -0
  64. crabcode_search/tool.py +232 -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