codemesh 0.1.1__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 (52) hide show
  1. codemesh/__init__.py +5 -0
  2. codemesh/__main__.py +8 -0
  3. codemesh/cli/__init__.py +3 -0
  4. codemesh/cli/init.py +208 -0
  5. codemesh/cli/install_cmd.py +208 -0
  6. codemesh/cli/main.py +469 -0
  7. codemesh/context/__init__.py +3 -0
  8. codemesh/context/builder.py +388 -0
  9. codemesh/db/__init__.py +3 -0
  10. codemesh/db/connection.py +66 -0
  11. codemesh/db/queries.py +696 -0
  12. codemesh/db/schema.py +125 -0
  13. codemesh/embedding/__init__.py +3 -0
  14. codemesh/extraction/__init__.py +7 -0
  15. codemesh/extraction/languages/__init__.py +95 -0
  16. codemesh/extraction/languages/c_family.py +614 -0
  17. codemesh/extraction/languages/go.py +397 -0
  18. codemesh/extraction/languages/java.py +603 -0
  19. codemesh/extraction/languages/python.py +718 -0
  20. codemesh/extraction/languages/rust.py +435 -0
  21. codemesh/extraction/languages/swift.py +464 -0
  22. codemesh/extraction/languages/typescript.py +1222 -0
  23. codemesh/extraction/orchestrator.py +218 -0
  24. codemesh/graph/__init__.py +8 -0
  25. codemesh/graph/query_manager.py +117 -0
  26. codemesh/graph/traverser.py +107 -0
  27. codemesh/indexer.py +240 -0
  28. codemesh/mcp/__init__.py +3 -0
  29. codemesh/mcp/server.py +60 -0
  30. codemesh/mcp/tools.py +605 -0
  31. codemesh/querier.py +269 -0
  32. codemesh/resolution/__init__.py +7 -0
  33. codemesh/resolution/frameworks/__init__.py +15 -0
  34. codemesh/resolution/frameworks/django.py +30 -0
  35. codemesh/resolution/frameworks/fastapi.py +23 -0
  36. codemesh/resolution/import_resolver.py +69 -0
  37. codemesh/resolution/name_matcher.py +30 -0
  38. codemesh/resolution/resolver.py +268 -0
  39. codemesh/retrieval/__init__.py +7 -0
  40. codemesh/search/__init__.py +3 -0
  41. codemesh/sync/__init__.py +3 -0
  42. codemesh/sync/watcher.py +135 -0
  43. codemesh/types.py +148 -0
  44. codemesh/viz/__init__.py +0 -0
  45. codemesh/viz/graph_builder.py +162 -0
  46. codemesh/viz/server.py +122 -0
  47. codemesh/viz/templates/index.html +359 -0
  48. codemesh-0.1.1.dist-info/METADATA +337 -0
  49. codemesh-0.1.1.dist-info/RECORD +52 -0
  50. codemesh-0.1.1.dist-info/WHEEL +4 -0
  51. codemesh-0.1.1.dist-info/entry_points.txt +2 -0
  52. codemesh-0.1.1.dist-info/licenses/LICENSE +21 -0
codemesh/cli/main.py ADDED
@@ -0,0 +1,469 @@
1
+ # mypy: ignore-errors
2
+ """CodeMesh CLI entry point."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ app = typer.Typer(
11
+ name="codemesh",
12
+ help="BM25 keyword search + graph walk for code intelligence",
13
+ no_args_is_help=True,
14
+ )
15
+
16
+
17
+ @app.command()
18
+ def init(
19
+ path: str = typer.Argument(".", help="Path to the project to initialize"),
20
+ interactive: bool = typer.Option(
21
+ False, "-i", "--interactive", help="Interactive mode — prompts before overwriting files"
22
+ ),
23
+ index_project: bool = typer.Option(
24
+ False, "--index", help="Also index the project after initialization"
25
+ ),
26
+ ) -> None:
27
+ """Initialize CodeMesh in a project.
28
+
29
+ Creates .codemesh/ directory and writes agent instruction files
30
+ (CLAUDE.md, .cursor/rules/codemesh.mdc, AGENTS.md).
31
+ """
32
+ from codemesh.cli.init import init_project
33
+
34
+ root = Path(path).resolve()
35
+ if not root.exists():
36
+ typer.echo(f"Error: {root} does not exist", err=True)
37
+ raise typer.Exit(1)
38
+
39
+ created = init_project(root, interactive=interactive)
40
+ typer.echo(f"CodeMesh initialized in {root}")
41
+ for key, val in created.items():
42
+ typer.echo(f" {key}: {val}")
43
+
44
+ if index_project:
45
+ from codemesh.indexer import index_project as do_index
46
+
47
+ typer.echo(f"\nIndexing {root}...")
48
+ stats = do_index(root, quiet=True)
49
+ typer.echo(
50
+ f"Done! {stats['nodes']} nodes, {stats['edges']} edges "
51
+ f"indexed in {stats.get('time_seconds', 0):.1f}s."
52
+ )
53
+
54
+
55
+ @app.command()
56
+ def install(
57
+ target: str = typer.Option(
58
+ "auto",
59
+ "--target",
60
+ "-t",
61
+ help="Agent(s) to configure: auto, all, claude, cursor, codex, or comma-separated list",
62
+ ),
63
+ global_config: bool = typer.Option(
64
+ True, "--global/--local", help="Write global config (default) or project-local"
65
+ ),
66
+ yes: bool = typer.Option(False, "--yes", "-y", help="Non-interactive mode"),
67
+ path: str = typer.Option(".", "--path", "-p", help="Project path for local config"),
68
+ ) -> None:
69
+ """Install CodeMesh MCP server configuration for AI coding agents.
70
+
71
+ Auto-detects installed agents and writes MCP server config + permissions.
72
+ Supports Claude Code, Cursor, and Codex CLI.
73
+ """
74
+ from codemesh.cli.install_cmd import (
75
+ detect_agents,
76
+ install_claude,
77
+ install_codex,
78
+ install_cursor,
79
+ )
80
+
81
+ root = Path(path).resolve()
82
+ targets = target.lower().split(",") if target not in ("auto", "all") else [target]
83
+
84
+ if "auto" in targets:
85
+ detected = detect_agents()
86
+ if not detected:
87
+ typer.echo("No AI coding agents detected. Use --target to specify manually.")
88
+ raise typer.Exit(1)
89
+ targets = detected
90
+ if not yes:
91
+ typer.echo(f"Detected agents: {', '.join(targets)}")
92
+ typer.confirm("Configure these agents?", abort=True)
93
+
94
+ if "all" in targets:
95
+ targets = ["claude", "cursor", "codex"]
96
+
97
+ results = {}
98
+ for agent in targets:
99
+ agent = agent.strip()
100
+ if agent == "claude":
101
+ r = install_claude(root, global_config=global_config)
102
+ results["claude"] = r
103
+ elif agent == "cursor":
104
+ r = install_cursor(root)
105
+ results["cursor"] = r
106
+ elif agent == "codex":
107
+ r = install_codex(root)
108
+ results["codex"] = r
109
+ else:
110
+ typer.echo(f"Unknown agent: {agent}", err=True)
111
+
112
+ typer.echo("CodeMesh MCP server configured:")
113
+ for agent, r in results.items():
114
+ for key, val in r.items():
115
+ if val:
116
+ typer.echo(f" {agent}/{key}: {val}")
117
+
118
+ # Also init the project if not already
119
+ codemesh_dir = root / ".codemesh"
120
+ if not codemesh_dir.exists():
121
+ from codemesh.cli.init import init_project
122
+
123
+ init_project(root)
124
+ typer.echo(f"\nInitialized .codemesh/ in {root}")
125
+
126
+ typer.echo("\nRestart your agent(s) for the MCP server to load.")
127
+
128
+
129
+ @app.command()
130
+ def index(
131
+ path: str = typer.Argument(".", help="Path to the codebase to index"),
132
+ workers: int | None = typer.Option(None, "--workers", "-w", help="Number of parallel workers"),
133
+ force: bool = typer.Option(
134
+ False, "--force", "-f", help="Force re-index even if already indexed"
135
+ ),
136
+ quiet: bool = typer.Option(False, "--quiet", "-q", help="Minimal output"),
137
+ ) -> None:
138
+ """Index a codebase for BM25 search."""
139
+ from codemesh.indexer import index_project
140
+
141
+ root = Path(path).resolve()
142
+ if not root.exists():
143
+ typer.echo(f"Error: {root} does not exist", err=True)
144
+ raise typer.Exit(1)
145
+
146
+ typer.echo(f"Indexing {root}...")
147
+ stats = index_project(root, max_workers=workers, quiet=quiet)
148
+ if quiet:
149
+ typer.echo(
150
+ f"Done! {stats['nodes']} nodes, {stats['edges']} edges "
151
+ f"indexed in {stats.get('time_seconds', 0):.1f}s."
152
+ )
153
+ # When not quiet, the progress bar already shows completion
154
+
155
+
156
+ @app.command()
157
+ def sync(
158
+ path: str = typer.Argument(".", help="Path to watch for changes"),
159
+ debounce: float = typer.Option(1.0, "--debounce", "-d", help="Debounce delay in seconds"),
160
+ ) -> None:
161
+ """Watch for file changes and auto-sync the index.
162
+
163
+ Uses native OS file events (FSEvents/inotify) with debounced auto-sync.
164
+ The graph stays current as you code.
165
+ """
166
+ from codemesh.indexer import sync_project
167
+
168
+ root = Path(path).resolve()
169
+ typer.echo(f"Watching {root} for changes... (Ctrl+C to stop)")
170
+ sync_project(root, debounce_delay=debounce)
171
+
172
+
173
+ @app.command()
174
+ def query(
175
+ q: str = typer.Argument(..., help="Query string"),
176
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
177
+ limit: int = typer.Option(10, "--limit", "-l", help="Max results"),
178
+ fmt: str = typer.Option(
179
+ "xml", "--format", "-f", help="Output format: xml, markdown, structured, or json"
180
+ ),
181
+ ) -> None:
182
+ """Query the indexed codebase."""
183
+ from codemesh.querier import query_codebase
184
+
185
+ root = Path(path).resolve()
186
+ result = query_codebase(root, q, limit=limit, fmt=fmt)
187
+ typer.echo(result)
188
+
189
+
190
+ @app.command()
191
+ def callers(
192
+ symbol: str = typer.Argument(..., help="Symbol to find callers for"),
193
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
194
+ ) -> None:
195
+ """Find all functions/methods that call a specific symbol."""
196
+ from codemesh.db.connection import get_connection, get_db_path
197
+ from codemesh.db.schema import init_db
198
+ from codemesh.graph.query_manager import QueryManager
199
+
200
+ root = Path(path).resolve()
201
+ init_db(get_db_path(root))
202
+ with get_connection(get_db_path(root)) as conn:
203
+ qm = QueryManager(conn)
204
+ callers = qm.find_callers(symbol)
205
+ if not callers:
206
+ typer.echo(f'No callers found for "{symbol}"')
207
+ return
208
+ typer.echo(f'Callers of "{symbol}" ({len(callers)}):')
209
+ typer.echo("")
210
+ for n in callers:
211
+ sig = f" {n.qualified_name} ({n.kind.value}) - {n.file_path}:{n.start_line}"
212
+ typer.echo(sig)
213
+
214
+
215
+ @app.command()
216
+ def callees(
217
+ symbol: str = typer.Argument(..., help="Symbol to find callees for"),
218
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
219
+ ) -> None:
220
+ """Find all functions/methods that a specific symbol calls."""
221
+ from codemesh.db.connection import get_connection, get_db_path
222
+ from codemesh.db.schema import init_db
223
+ from codemesh.graph.query_manager import QueryManager
224
+
225
+ root = Path(path).resolve()
226
+ init_db(get_db_path(root))
227
+ with get_connection(get_db_path(root)) as conn:
228
+ qm = QueryManager(conn)
229
+ callees = qm.find_callees(symbol)
230
+ if not callees:
231
+ typer.echo(f'No callees found for "{symbol}"')
232
+ return
233
+ typer.echo(f'Callees of "{symbol}" ({len(callees)}):')
234
+ typer.echo("")
235
+ for n in callees:
236
+ sig = f" {n.qualified_name} ({n.kind.value}) - {n.file_path}:{n.start_line}"
237
+ typer.echo(sig)
238
+
239
+
240
+ @app.command()
241
+ def impact(
242
+ symbol: str = typer.Argument(..., help="Symbol to analyze impact for"),
243
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
244
+ depth: int = typer.Option(3, "--depth", "-d", help="Max traversal depth"),
245
+ ) -> None:
246
+ """Analyze what code is affected by changing a symbol."""
247
+ from codemesh.db.connection import get_connection, get_db_path
248
+ from codemesh.db.queries import get_node
249
+ from codemesh.db.schema import init_db
250
+ from codemesh.graph.query_manager import QueryManager
251
+
252
+ root = Path(path).resolve()
253
+ init_db(get_db_path(root))
254
+ with get_connection(get_db_path(root)) as conn:
255
+ qm = QueryManager(conn)
256
+ subgraph = qm.what_breaks_if_changed(symbol)
257
+ affected = [n for nid in subgraph.nodes if (n := get_node(conn, nid)) is not None]
258
+ if not affected:
259
+ typer.echo(f'No dependents found for "{symbol}"')
260
+ return
261
+ typer.echo(f'Impact of changing "{symbol}" — {len(affected)} affected symbols:')
262
+ typer.echo("")
263
+ # Group by file
264
+ by_file: dict[str, list] = {}
265
+ for n in affected:
266
+ fp = str(n.file_path)
267
+ by_file.setdefault(fp, []).append(n)
268
+ for fp, nodes in sorted(by_file.items()):
269
+ typer.echo(fp)
270
+ for n in nodes:
271
+ typer.echo(f" {n.kind.value:10s} {n.name}:{n.start_line}")
272
+ typer.echo("")
273
+
274
+
275
+ @app.command()
276
+ def context(
277
+ symbol: str = typer.Argument(..., help="Symbol to get context for"),
278
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
279
+ tokens: int = typer.Option(8000, "--tokens", "-t", help="Token budget"),
280
+ fmt: str = typer.Option(
281
+ "xml", "--format", "-f", help="Output format: xml, markdown, or structured"
282
+ ),
283
+ max_nodes: int = typer.Option(50, "--max-nodes", "-n", help="Max nodes to include"),
284
+ max_code: int = typer.Option(10, "--max-code", "-c", help="Max code blocks"),
285
+ no_code: bool = typer.Option(False, "--no-code", help="Exclude code blocks"),
286
+ ) -> None:
287
+ """Get context for a symbol (or general task).
288
+
289
+ Builds structured context with Entry Points, Related Symbols, and Code.
290
+ Similar to a context command for code intelligence.
291
+ """
292
+ from codemesh.querier import get_context
293
+
294
+ root = Path(path).resolve()
295
+
296
+ # If format is structured, we need to handle it differently
297
+ if fmt == "structured":
298
+ from codemesh.context.builder import ContextBuilder, ContextFormat, ContextOptions
299
+ from codemesh.db.connection import get_connection, get_db_path
300
+ from codemesh.db.queries import get_node, search_nodes_fts
301
+ from codemesh.db.schema import init_db
302
+ from codemesh.graph.traverser import GraphTraverser
303
+
304
+ init_db(get_db_path(root))
305
+ with get_connection(get_db_path(root)) as conn:
306
+ # Search for the symbol
307
+ results = search_nodes_fts(conn, symbol, limit=max_nodes)
308
+ if not results:
309
+ typer.echo(f"No results for: {symbol}")
310
+ return
311
+
312
+ # Separate entry points (top results) from related (graph expansion)
313
+ traverser = GraphTraverser()
314
+ bm25_ids = {n.id for n, _ in results[:10]}
315
+ expanded = list(results[:10])
316
+
317
+ for node, _score in results[:5]:
318
+ subgraph = traverser.traverse(conn, [node.id], max_depth=1, max_nodes=20)
319
+ for nid, tr in subgraph.nodes.items():
320
+ if nid not in bm25_ids and len(expanded) < max_nodes:
321
+ bm25_ids.add(nid)
322
+ n = get_node(conn, nid)
323
+ if n is not None:
324
+ expanded.append((n, tr.score))
325
+
326
+ entry_points = expanded[:5]
327
+ related = expanded[5:max_nodes]
328
+
329
+ builder = ContextBuilder(conn, root)
330
+ context = builder.build(
331
+ expanded[:max_code] if not no_code else [],
332
+ symbol,
333
+ ContextOptions(
334
+ max_snippets=max_code if not no_code else 0,
335
+ max_tokens=tokens * 4,
336
+ format=ContextFormat.STRUCTURED,
337
+ ),
338
+ entry_points=entry_points,
339
+ related=related,
340
+ )
341
+ typer.echo(context)
342
+ return
343
+
344
+ result = get_context(root, symbol, max_tokens=tokens)
345
+ typer.echo(result)
346
+
347
+
348
+ @app.command()
349
+ def serve(
350
+ transport: str = typer.Option("stdio", "--transport", help="Transport: stdio or sse"),
351
+ port: int = typer.Option(3000, "--port", "-p", help="Port for SSE transport"),
352
+ ) -> None:
353
+ """Start the MCP server."""
354
+ from codemesh.mcp.server import run_server
355
+
356
+ run_server(transport=transport, port=port)
357
+
358
+
359
+ @app.command()
360
+ def graph(
361
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
362
+ port: int = typer.Option(8765, "--port", help="Port for the visualization server"),
363
+ symbol: str | None = typer.Option(None, "--symbol", "-s", help="Focus on a specific symbol"),
364
+ kind: str | None = typer.Option(None, "--kind", "-k", help="Filter by node kind"),
365
+ depth: int = typer.Option(3, "--depth", "-d", help="BFS depth for symbol focus"),
366
+ export_json: str | None = typer.Option(None, "--json", help="Export graph as JSON to file"),
367
+ ) -> None:
368
+ """Open interactive graph visualization in browser."""
369
+ import json as json_mod
370
+ from pathlib import Path as Path2
371
+
372
+ from codemesh.viz.graph_builder import build_graph
373
+
374
+ root = Path(path).resolve()
375
+
376
+ if export_json:
377
+ g = build_graph(
378
+ root, kind_filter=[kind] if kind else None, symbol_focus=symbol, depth=depth
379
+ )
380
+ Path2(export_json).write_text(json_mod.dumps(g, indent=2, default=str))
381
+ typer.echo(
382
+ f"Graph exported to {export_json} ({len(g['nodes'])} nodes, {len(g['edges'])} edges)"
383
+ )
384
+ return
385
+
386
+ from codemesh.viz.server import run_server
387
+
388
+ run_server(root=root, port=port, open_browser=True)
389
+
390
+
391
+ @app.command()
392
+ def files(
393
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
394
+ ) -> None:
395
+ """Show project file structure from the index."""
396
+ from codemesh.db.connection import get_connection, get_db_path
397
+ from codemesh.db.schema import init_db
398
+
399
+ root = Path(path).resolve()
400
+ init_db(get_db_path(root))
401
+ with get_connection(get_db_path(root)) as conn:
402
+ # Get file nodes
403
+ file_rows = conn.execute(
404
+ "SELECT DISTINCT file_path, language FROM nodes WHERE kind = 'file' ORDER BY file_path"
405
+ ).fetchall()
406
+ if not file_rows:
407
+ typer.echo("No files indexed. Run 'codemesh index' first.")
408
+ return
409
+
410
+ # Count nodes per file
411
+ counts = conn.execute(
412
+ "SELECT file_path, kind, COUNT(*) as cnt FROM nodes GROUP BY file_path, kind ORDER BY file_path"
413
+ ).fetchall()
414
+
415
+ by_file: dict[str, dict[str, int]] = {}
416
+ for row in counts:
417
+ fp = row["file_path"]
418
+ by_file.setdefault(fp, {})[row["kind"]] = row["cnt"]
419
+
420
+ typer.echo(f"Indexed files: {len(file_rows)}")
421
+ typer.echo("")
422
+ for row in file_rows:
423
+ fp = row["file_path"]
424
+ lang = row["language"]
425
+ kinds = by_file.get(fp, {})
426
+ total = sum(kinds.values())
427
+ kind_str = ", ".join(f"{k}={v}" for k, v in sorted(kinds.items()) if k != "file")
428
+ typer.echo(f" {fp} ({lang}, {total} nodes: {kind_str})")
429
+
430
+
431
+ @app.command()
432
+ def status(
433
+ path: str = typer.Option(".", "--path", "-p", help="Path to the indexed codebase"),
434
+ ) -> None:
435
+ """Show index status and statistics."""
436
+ from codemesh.db.connection import get_connection, get_db_path
437
+ from codemesh.db.schema import init_db
438
+
439
+ root = Path(path).resolve()
440
+ init_db(get_db_path(root))
441
+ with get_connection(get_db_path(root)) as conn:
442
+ node_count = conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0]
443
+ edge_count = conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0]
444
+ file_count = conn.execute(
445
+ "SELECT COUNT(DISTINCT file_path) FROM nodes WHERE kind = 'file'"
446
+ ).fetchone()[0]
447
+
448
+ # Node kinds breakdown
449
+ kinds = conn.execute(
450
+ "SELECT kind, COUNT(*) as cnt FROM nodes GROUP BY kind ORDER BY cnt DESC"
451
+ ).fetchall()
452
+
453
+ # Edge kinds breakdown
454
+ edge_kinds = conn.execute(
455
+ "SELECT kind, COUNT(*) as cnt FROM edges GROUP BY kind ORDER BY cnt DESC"
456
+ ).fetchall()
457
+
458
+ typer.echo("CodeMesh Index Status")
459
+ typer.echo("=" * 40)
460
+ typer.echo(f" Files: {file_count}")
461
+ typer.echo(f" Nodes: {node_count}")
462
+ typer.echo(f" Edges: {edge_count}")
463
+ typer.echo("")
464
+ typer.echo(" Node kinds:")
465
+ for row in kinds:
466
+ typer.echo(f" {row['kind']:12s} {row['cnt']}")
467
+ typer.echo(" Edge kinds:")
468
+ for row in edge_kinds:
469
+ typer.echo(f" {row['kind']:12s} {row['cnt']}")
@@ -0,0 +1,3 @@
1
+ """Context building for LLM prompts."""
2
+
3
+ from __future__ import annotations