codevira 1.6.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 (58) hide show
  1. codevira-1.6.0.dist-info/LICENSE +21 -0
  2. codevira-1.6.0.dist-info/METADATA +477 -0
  3. codevira-1.6.0.dist-info/RECORD +58 -0
  4. codevira-1.6.0.dist-info/WHEEL +5 -0
  5. codevira-1.6.0.dist-info/entry_points.txt +2 -0
  6. codevira-1.6.0.dist-info/top_level.txt +2 -0
  7. indexer/__init__.py +1 -0
  8. indexer/chunker.py +428 -0
  9. indexer/global_db.py +197 -0
  10. indexer/graph_generator.py +380 -0
  11. indexer/index_codebase.py +588 -0
  12. indexer/outcome_tracker.py +172 -0
  13. indexer/rule_learner.py +186 -0
  14. indexer/sqlite_graph.py +640 -0
  15. indexer/treesitter_parser.py +423 -0
  16. mcp_server/__init__.py +1 -0
  17. mcp_server/__main__.py +20 -0
  18. mcp_server/auto_init.py +257 -0
  19. mcp_server/cli.py +622 -0
  20. mcp_server/crash_logger.py +236 -0
  21. mcp_server/data/__init__.py +1 -0
  22. mcp_server/data/agents/builder.md +84 -0
  23. mcp_server/data/agents/developer.md +111 -0
  24. mcp_server/data/agents/documenter.md +138 -0
  25. mcp_server/data/agents/orchestrator.md +96 -0
  26. mcp_server/data/agents/planner.md +106 -0
  27. mcp_server/data/agents/reviewer.md +82 -0
  28. mcp_server/data/agents/tester.md +83 -0
  29. mcp_server/data/config.example.yaml +33 -0
  30. mcp_server/data/rules/coding-standards.md +48 -0
  31. mcp_server/data/rules/engineering-excellence.md +28 -0
  32. mcp_server/data/rules/git-cicd-governance.md +32 -0
  33. mcp_server/data/rules/git_commits.md +130 -0
  34. mcp_server/data/rules/incremental-updates.md +5 -0
  35. mcp_server/data/rules/master_rule.md +187 -0
  36. mcp_server/data/rules/multi-language.md +19 -0
  37. mcp_server/data/rules/persistence.md +21 -0
  38. mcp_server/data/rules/resilience-observability.md +17 -0
  39. mcp_server/data/rules/smoke-testing.md +48 -0
  40. mcp_server/data/rules/testing-standards.md +23 -0
  41. mcp_server/detect.py +284 -0
  42. mcp_server/gitignore.py +284 -0
  43. mcp_server/global_sync.py +187 -0
  44. mcp_server/http_server.py +341 -0
  45. mcp_server/ide_inject.py +444 -0
  46. mcp_server/launchd.py +156 -0
  47. mcp_server/migrate.py +215 -0
  48. mcp_server/paths.py +256 -0
  49. mcp_server/prompts.py +136 -0
  50. mcp_server/server.py +1049 -0
  51. mcp_server/tools/__init__.py +0 -0
  52. mcp_server/tools/changesets.py +223 -0
  53. mcp_server/tools/code_reader.py +335 -0
  54. mcp_server/tools/graph.py +637 -0
  55. mcp_server/tools/learning.py +238 -0
  56. mcp_server/tools/playbook.py +89 -0
  57. mcp_server/tools/roadmap.py +599 -0
  58. mcp_server/tools/search.py +145 -0
mcp_server/cli.py ADDED
@@ -0,0 +1,622 @@
1
+ """
2
+ cli.py — Entry point for the `codevira` command.
3
+
4
+ Dispatches subcommands:
5
+ codevira → start MCP server (default)
6
+ codevira init → initialize project in centralized storage
7
+ codevira register → one-time global IDE registration (v1.6)
8
+ codevira index → run incremental index update
9
+ codevira index --full → full index rebuild
10
+ codevira status → show index health and stats
11
+ codevira report → show recent crash logs
12
+ codevira report --clear → clear the crash log
13
+ codevira serve → start MCP HTTP server
14
+ codevira serve --install-service → install macOS launchd auto-start
15
+ codevira serve --uninstall-service → remove macOS launchd service
16
+
17
+ Global flags:
18
+ --project-dir <path> → override project directory (for Google Antigravity,
19
+ which doesn't support `cwd` in its MCP config)
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import sys
25
+ from pathlib import Path
26
+
27
+
28
+ def _set_project_dir_early(args: list[str]) -> Path | None:
29
+ """
30
+ Parse --project-dir before any subcommand handling so that
31
+ paths.set_project_dir() is called before any module-level path resolution.
32
+ """
33
+ for i, arg in enumerate(args):
34
+ if arg == "--project-dir" and i + 1 < len(args):
35
+ return Path(args[i + 1]).resolve()
36
+ if arg.startswith("--project-dir="):
37
+ return Path(arg.split("=", 1)[1]).resolve()
38
+ return None
39
+
40
+
41
+ def _detect_project_root_markers(path: Path) -> bool:
42
+ """Return True if the given path looks like a project root."""
43
+ markers = [
44
+ ".git", "pyproject.toml", "setup.py", "setup.cfg",
45
+ "package.json", "Cargo.toml", "go.mod", "Makefile",
46
+ "pom.xml", "build.gradle",
47
+ ]
48
+ return any((path / m).exists() for m in markers)
49
+
50
+
51
+ def cmd_init() -> None:
52
+ """Initialize Codevira in the current project."""
53
+ from mcp_server.paths import get_project_root, get_data_dir, get_package_data_dir
54
+ import shutil
55
+ import yaml
56
+
57
+ cwd = get_project_root()
58
+ data_dir = get_data_dir()
59
+
60
+ print()
61
+ print(" Codevira — Project Initialization")
62
+ print(" " + "─" * 40)
63
+ print()
64
+
65
+ # Step 1: Validate project root
66
+ if not _detect_project_root_markers(cwd):
67
+ parent = cwd.parent
68
+ if _detect_project_root_markers(parent):
69
+ print(f" Warning: It looks like you may be in a subdirectory.")
70
+ print(f" Project markers found in: {parent}")
71
+ print(f" Current directory: {cwd}")
72
+ print()
73
+ answer = input(" Continue initializing here anyway? [y/N] ").strip().lower()
74
+ if answer != "y":
75
+ print(" Aborted. Run `codevira init` from your project root.")
76
+ sys.exit(0)
77
+ print()
78
+
79
+ # Step 2a: Auto-migrate legacy .codevira/ if present
80
+ git_dir = cwd / ".git"
81
+ from mcp_server.migrate import detect_migration_needed, migrate_to_centralized
82
+ if detect_migration_needed(cwd):
83
+ print(f" Migrating legacy .codevira/ to centralized storage ...", end="", flush=True)
84
+ try:
85
+ result = migrate_to_centralized(cwd)
86
+ if result.get("migrated"):
87
+ print(f" done ({result.get('files_copied', 0)} files → {result.get('new_path', '')})")
88
+ # Re-evaluate data_dir after migration — now points to centralized path
89
+ data_dir = get_data_dir()
90
+ else:
91
+ print(f" skipped ({result.get('reason', '')})")
92
+ except Exception as e:
93
+ print(f" failed ({e})")
94
+
95
+ # Step 2b: Create centralized directory structure
96
+ is_centralized = str(data_dir).startswith(str(Path.home() / ".codevira" / "projects"))
97
+ if is_centralized:
98
+ print(f" Creating centralized data dir ...")
99
+ print(f" {data_dir}")
100
+ else:
101
+ print(f" Creating .codevira/ in {cwd} ...")
102
+ for subdir in ["graph/changesets", "codeindex", "logs"]:
103
+ (data_dir / subdir).mkdir(parents=True, exist_ok=True)
104
+ print(f" Data directory ready ... done")
105
+
106
+ # Step 3: For new centralized projects, no .gitignore entry needed.
107
+ # For legacy mode (in-project), add .codevira/ to .gitignore.
108
+ if not is_centralized and git_dir.exists():
109
+ gitignore = cwd / ".gitignore"
110
+ entry = ".codevira/"
111
+ needs_add = True
112
+ if gitignore.exists():
113
+ content = gitignore.read_text()
114
+ if ".codevira" in content:
115
+ needs_add = False
116
+ if needs_add:
117
+ print(f" Adding .codevira/ to .gitignore ... ", end="", flush=True)
118
+ with open(gitignore, "a") as f:
119
+ if gitignore.exists() and gitignore.stat().st_size > 0:
120
+ existing = gitignore.read_text()
121
+ if not existing.endswith("\n"):
122
+ f.write("\n")
123
+ f.write(f"\n# Codevira — auto-generated, do not commit\n{entry}\n")
124
+ print("done")
125
+
126
+ # Step 4: Zero-config auto-detection (no interactive prompts)
127
+ print()
128
+ from mcp_server.detect import auto_detect_project
129
+ detected = auto_detect_project(cwd)
130
+
131
+ # Apply CLI overrides if provided (parsed from args later)
132
+ if hasattr(cmd_init, '_overrides'):
133
+ overrides = cmd_init._overrides
134
+ if overrides.get("name"): detected["name"] = overrides["name"]
135
+ if overrides.get("language"): detected["language"] = overrides["language"]
136
+ if overrides.get("dirs"): detected["watched_dirs"] = [d.strip() for d in overrides["dirs"].split(",")]
137
+ if overrides.get("ext"): detected["file_extensions"] = [e.strip() for e in overrides["ext"].split(",")]
138
+
139
+ print(f" Auto-detected:")
140
+ print(f" Project: {detected['name']}")
141
+ print(f" Language: {detected['language']}")
142
+ print(f" Source dirs: {', '.join(detected['watched_dirs'])}")
143
+ print(f" Extensions: {', '.join(detected['file_extensions'])}")
144
+
145
+ # Write config.yaml
146
+ config = {
147
+ "project": {
148
+ "name": detected["name"],
149
+ "language": detected["language"],
150
+ "collection_name": detected["collection_name"],
151
+ "watched_dirs": detected["watched_dirs"],
152
+ "file_extensions": detected["file_extensions"],
153
+ }
154
+ }
155
+
156
+ # Try to copy example config as base, then merge project settings
157
+ pkg_config_example = get_package_data_dir() / "config.example.yaml"
158
+ config_path = data_dir / "config.yaml"
159
+ if pkg_config_example.exists():
160
+ shutil.copy(pkg_config_example, config_path)
161
+ # Merge project section on top
162
+ with open(config_path) as f:
163
+ base = yaml.safe_load(f) or {}
164
+ base.update(config)
165
+ with open(config_path, "w") as f:
166
+ yaml.dump(base, f, default_flow_style=False, sort_keys=False)
167
+ else:
168
+ with open(config_path, "w") as f:
169
+ yaml.dump(config, f, default_flow_style=False, sort_keys=False)
170
+
171
+ print()
172
+
173
+ # Step 5: Run full index build — let rich progress bars render directly.
174
+ # Suppress noisy HuggingFace/transformers output via env vars.
175
+ import os as _os
176
+ import contextlib, io
177
+ print(" Building code index ...")
178
+ try:
179
+ from indexer.index_codebase import cmd_full_rebuild
180
+ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
181
+ _os.environ.setdefault("HF_HUB_VERBOSITY", "error")
182
+ # Suppress stderr noise (BertModel LOAD REPORT, HF_TOKEN warnings)
183
+ with contextlib.redirect_stderr(io.StringIO()):
184
+ cmd_full_rebuild()
185
+ except Exception as e:
186
+ print(f" skipped ({e})")
187
+ try:
188
+ from mcp_server.crash_logger import log_crash
189
+ log_crash(e, context="codevira init: index build", project_path=str(cwd))
190
+ except Exception: pass
191
+
192
+ # Step 6: Generate graph stubs
193
+ print(" Generating graph stubs ... ", end="", flush=True)
194
+ try:
195
+ from indexer.index_codebase import cmd_generate_graph
196
+ buf = io.StringIO()
197
+ with contextlib.redirect_stdout(buf):
198
+ cmd_generate_graph()
199
+ output = buf.getvalue()
200
+ nodes = "0"
201
+ for line in output.splitlines():
202
+ if "Nodes added:" in line:
203
+ nodes = line.split(":")[-1].strip()
204
+ break
205
+ print(f"done ({nodes} nodes)")
206
+ except Exception as e:
207
+ print(f"skipped ({e})")
208
+ try:
209
+ from mcp_server.crash_logger import log_crash
210
+ log_crash(e, context="codevira init: graph stubs", project_path=str(cwd))
211
+ except Exception: pass
212
+
213
+ # Step 7: Bootstrap roadmap
214
+ print(" Bootstrapping roadmap ... ", end="", flush=True)
215
+ try:
216
+ from indexer.index_codebase import cmd_bootstrap_roadmap
217
+ import io
218
+ import contextlib
219
+
220
+ buf = io.StringIO()
221
+ with contextlib.redirect_stdout(buf):
222
+ cmd_bootstrap_roadmap()
223
+ print("done")
224
+ except Exception as e:
225
+ print(f"skipped ({e})")
226
+ try:
227
+ from mcp_server.crash_logger import log_crash
228
+ log_crash(e, context="codevira init: roadmap bootstrap", project_path=str(cwd))
229
+ except Exception: pass
230
+
231
+ # Step 8: Install git hook
232
+ if git_dir.exists():
233
+ print(" Installing git hook ... ", end="", flush=True)
234
+ try:
235
+ hooks_dir = git_dir / "hooks"
236
+ hooks_dir.mkdir(exist_ok=True)
237
+ hook_path = hooks_dir / "post-commit"
238
+
239
+ # Find codevira executable path
240
+ import shutil as _shutil
241
+ cmd_path = _shutil.which("codevira") or "codevira"
242
+
243
+ hook_content = (
244
+ "#!/bin/sh\n"
245
+ "# Codevira post-commit hook — auto-reindex changed files\n"
246
+ f'"{cmd_path}" index --quiet 2>/dev/null || true\n'
247
+ )
248
+
249
+ # Backup existing hook if it exists and is not ours
250
+ if hook_path.exists():
251
+ existing = hook_path.read_text()
252
+ if "codevira" not in existing.lower():
253
+ hook_path.rename(hook_path.with_suffix(".bak"))
254
+
255
+ hook_path.write_text(hook_content)
256
+ hook_path.chmod(0o755)
257
+ print("done")
258
+ except Exception as e:
259
+ print(f"skipped ({e})")
260
+ try:
261
+ from mcp_server.crash_logger import log_crash
262
+ log_crash(e, context="codevira init: git hook", project_path=str(cwd))
263
+ except Exception: pass
264
+
265
+ # Step 9: Auto-inject IDE configurations
266
+ print()
267
+ print(" " + "─" * 60)
268
+ print(f" ✓ Codevira initialized in {data_dir}")
269
+ print()
270
+
271
+ no_inject = getattr(cmd_init, '_no_inject', False)
272
+ if not no_inject:
273
+ print(" Configuring AI tools ... ", end="", flush=True)
274
+ try:
275
+ from mcp_server.ide_inject import inject_ide_config
276
+ results = inject_ide_config(cwd, project_name=detected["name"])
277
+ if results:
278
+ print("done")
279
+ for ide_name, config_path in results.items():
280
+ print(f" ✓ {ide_name}: {config_path}")
281
+ else:
282
+ print("no AI tools detected")
283
+ except Exception as e:
284
+ print(f"skipped ({e})")
285
+ try:
286
+ from mcp_server.crash_logger import log_crash
287
+ log_crash(e, context="codevira init: IDE inject", project_path=str(cwd))
288
+ except Exception: pass
289
+
290
+ # Step 10: Register in global memory (with git_remote for rename-resilient lookup)
291
+ try:
292
+ from mcp_server.global_sync import import_global_to_project
293
+ from mcp_server.paths import get_global_db_path, _get_git_remote_url
294
+ from indexer.global_db import GlobalDB
295
+ from mcp_server.auto_init import _write_metadata
296
+
297
+ git_remote = _get_git_remote_url(cwd)
298
+ gdb = GlobalDB(get_global_db_path())
299
+ gdb.register_project(str(data_dir), detected["name"], detected["language"], git_remote=git_remote)
300
+ proj_count = gdb.get_project_count()
301
+ gdb.close()
302
+ if proj_count > 1:
303
+ print(f" Registered in global memory ({proj_count} projects)")
304
+
305
+ # Write metadata.json for centralized storage marker
306
+ _write_metadata(data_dir, cwd)
307
+ except Exception as e:
308
+ print(f" Global memory registration skipped ({e})")
309
+ try:
310
+ from mcp_server.crash_logger import log_crash
311
+ log_crash(e, context="codevira init: global memory register", project_path=str(cwd))
312
+ except Exception: pass
313
+
314
+ # Print fallback for undetected tools
315
+ import shutil as _shutil
316
+ import sys as _sys
317
+ python_exe = _sys.executable
318
+ project_path = str(cwd)
319
+
320
+ print()
321
+ print(" For other AI tools, add this to their MCP config:")
322
+ print()
323
+ print(' {')
324
+ print(' "mcpServers": {')
325
+ print(' "codevira": {')
326
+ print(f' "command": "{python_exe}",')
327
+ print(f' "args": ["-m", "mcp_server", "--project-dir", "{project_path}"]')
328
+ print(' }')
329
+ print(' }')
330
+ print(' }')
331
+ print()
332
+ print(" Verify: ask your agent to call get_roadmap()")
333
+ print()
334
+
335
+
336
+ def cmd_index(full: bool = False, quiet: bool = False) -> None:
337
+ """Run the indexer (incremental by default, or --full for complete rebuild)."""
338
+ from indexer.index_codebase import cmd_full_rebuild, cmd_incremental
339
+
340
+ if full:
341
+ cmd_full_rebuild()
342
+ else:
343
+ cmd_incremental(quiet=quiet)
344
+
345
+
346
+ def cmd_status() -> None:
347
+ """Show index health and statistics."""
348
+ from indexer.index_codebase import cmd_status as _cmd_status
349
+ _cmd_status()
350
+
351
+
352
+ def cmd_report(limit: int = 20, clear: bool = False) -> None:
353
+ """Show recent crash logs."""
354
+ from mcp_server.crash_logger import read_recent_crashes, get_crash_log_path
355
+
356
+ if clear:
357
+ log_path = get_crash_log_path()
358
+ if log_path.exists():
359
+ log_path.unlink()
360
+ print(" Crash log cleared.")
361
+ else:
362
+ print(" No crash log to clear.")
363
+ return
364
+
365
+ print()
366
+ print(" Codevira — Crash Report")
367
+ print(" " + "-" * 40)
368
+ print()
369
+ print(read_recent_crashes(limit=limit))
370
+ print()
371
+
372
+
373
+ def cmd_server(project_dir: Path | None = None) -> None:
374
+ """Start the MCP server (stdio transport)."""
375
+ from mcp_server.server import main as server_main
376
+ server_main()
377
+
378
+
379
+ def cmd_serve(
380
+ host: str = "127.0.0.1",
381
+ port: int = 7007,
382
+ use_https: bool = False,
383
+ project_dir: Path | None = None,
384
+ install_service: bool = False,
385
+ uninstall_service: bool = False,
386
+ ) -> None:
387
+ """Start the MCP HTTP server (Streamable HTTP transport)."""
388
+ if install_service:
389
+ from mcp_server.launchd import install_launchd
390
+ try:
391
+ plist = install_launchd(port=port, use_https=use_https, host=host, project_dir=project_dir)
392
+ print(f" Launchd service installed: {plist}")
393
+ print(" Codevira MCP server will start automatically on login.")
394
+ except RuntimeError as e:
395
+ print(f" Error: {e}")
396
+ sys.exit(1)
397
+ return
398
+
399
+ if uninstall_service:
400
+ from mcp_server.launchd import uninstall_launchd
401
+ try:
402
+ removed = uninstall_launchd()
403
+ if removed:
404
+ print(" Launchd service removed.")
405
+ else:
406
+ print(" No launchd service was installed.")
407
+ except RuntimeError as e:
408
+ print(f" Error: {e}")
409
+ sys.exit(1)
410
+ return
411
+
412
+ from mcp_server.http_server import run_http_server
413
+ run_http_server(host=host, port=port, use_https=use_https, project_dir=project_dir)
414
+
415
+
416
+ def cmd_register(
417
+ global_mode: bool = True,
418
+ claude_desktop: bool = False,
419
+ http_url: str | None = None,
420
+ ) -> None:
421
+ """One-time global IDE registration (v1.6).
422
+
423
+ Injects codevira into all detected AI tools' global configs so that
424
+ every project the developer opens automatically has Codevira memory.
425
+ """
426
+ from mcp_server.paths import get_project_root
427
+ from mcp_server.ide_inject import (
428
+ _resolve_command, detect_installed_ides,
429
+ inject_global_claude_code, inject_global_cursor, inject_global_windsurf,
430
+ _inject_claude_desktop, inject_claude_http_url,
431
+ )
432
+
433
+ project_root = get_project_root()
434
+ cmd_path, python_exe = _resolve_command()
435
+
436
+ print()
437
+ print(" Codevira — Global IDE Registration (v1.6)")
438
+ print(" " + "─" * 44)
439
+ print()
440
+
441
+ if http_url:
442
+ path = inject_claude_http_url(http_url)
443
+ print(f" ✓ Claude Code (HTTP URL): {path}")
444
+ print()
445
+ return
446
+
447
+ if claude_desktop:
448
+ path = _inject_claude_desktop(project_root, cmd_path, python_exe)
449
+ print(f" ✓ Claude Desktop: {path}")
450
+ print(" Note: Claude Desktop uses stdio — restart it to pick up changes.")
451
+ print()
452
+ return
453
+
454
+ # Global mode: inject into all detected IDEs
455
+ ides = detect_installed_ides(project_root)
456
+ results: dict[str, str] = {}
457
+
458
+ for ide in ides:
459
+ try:
460
+ if ide == "claude":
461
+ path = inject_global_claude_code(cmd_path, python_exe)
462
+ if path:
463
+ results["Claude Code (global)"] = path
464
+ elif ide == "cursor":
465
+ path = inject_global_cursor(cmd_path, python_exe)
466
+ if path:
467
+ results["Cursor (global)"] = path
468
+ elif ide == "windsurf":
469
+ path = inject_global_windsurf(cmd_path, python_exe)
470
+ if path:
471
+ results["Windsurf (global)"] = path
472
+ elif ide == "claude_desktop":
473
+ path = _inject_claude_desktop(project_root, cmd_path, python_exe)
474
+ if path:
475
+ results["Claude Desktop"] = path
476
+ except Exception as e:
477
+ print(f" Warning: could not configure {ide}: {e}")
478
+
479
+ if results:
480
+ for ide_name, config_path in results.items():
481
+ print(f" ✓ {ide_name}: {config_path}")
482
+ print()
483
+ print(" Restart your AI tools to pick up the new configuration.")
484
+ print(" Every project you open will now have Codevira memory automatically.")
485
+ else:
486
+ print(" No AI tools detected. Install Claude Code, Cursor, or Windsurf first.")
487
+ print()
488
+
489
+
490
+ def main() -> None:
491
+ # Pre-parse --project-dir before argparse so we can initialize paths early.
492
+ raw_args = sys.argv[1:]
493
+ project_dir = _set_project_dir_early(raw_args)
494
+
495
+ if project_dir is not None:
496
+ from mcp_server.paths import set_project_dir
497
+ set_project_dir(project_dir)
498
+
499
+ parser = argparse.ArgumentParser(
500
+ prog="codevira",
501
+ description="Codevira — AI context layer for your codebase",
502
+ )
503
+ parser.add_argument(
504
+ "--project-dir",
505
+ metavar="PATH",
506
+ help="Project directory (alternative to cwd; useful for Google Antigravity)",
507
+ )
508
+
509
+ subparsers = parser.add_subparsers(dest="command")
510
+
511
+ # init
512
+ init_parser = subparsers.add_parser("init", help="Initialize Codevira in the current project")
513
+ init_parser.add_argument("--name", help="Override project name")
514
+ init_parser.add_argument("--language", help="Override detected language")
515
+ init_parser.add_argument("--dirs", help="Override source directories (comma-separated)")
516
+ init_parser.add_argument("--ext", help="Override file extensions (comma-separated)")
517
+ init_parser.add_argument("--no-inject", action="store_true", help="Skip auto-injecting IDE configs")
518
+
519
+ # index
520
+ index_parser = subparsers.add_parser("index", help="Run the code indexer")
521
+ index_parser.add_argument("--full", action="store_true", help="Full rebuild from scratch")
522
+ index_parser.add_argument("--quiet", action="store_true", help="Suppress output (used by git hook)")
523
+
524
+ # status
525
+ subparsers.add_parser("status", help="Show index health and statistics")
526
+
527
+ # report
528
+ report_parser = subparsers.add_parser("report", help="Show recent crash logs")
529
+ report_parser.add_argument("--limit", type=int, default=20, help="Number of recent crashes to show (default: 20)")
530
+ report_parser.add_argument("--clear", action="store_true", help="Clear the crash log")
531
+
532
+ # serve
533
+ serve_parser = subparsers.add_parser(
534
+ "serve",
535
+ help="Start MCP HTTP server (Streamable HTTP transport — register via URL)",
536
+ )
537
+ serve_parser.add_argument(
538
+ "--port", type=int, default=7007,
539
+ help="TCP port to listen on (default: 7007)",
540
+ )
541
+ serve_parser.add_argument(
542
+ "--host", default="127.0.0.1",
543
+ help="Bind address (default: 127.0.0.1; use 0.0.0.0 for LAN access)",
544
+ )
545
+ serve_parser.add_argument(
546
+ "--https", action="store_true",
547
+ help="Enable HTTPS using mkcert certs from ~/.codevira/certs/",
548
+ )
549
+ serve_parser.add_argument(
550
+ "--install-service", action="store_true",
551
+ help="Install macOS launchd service so the server starts automatically on login",
552
+ )
553
+ serve_parser.add_argument(
554
+ "--uninstall-service", action="store_true",
555
+ help="Remove the macOS launchd service",
556
+ )
557
+ serve_parser.add_argument(
558
+ "--project-dir",
559
+ metavar="PATH",
560
+ help="Project directory override (same as the global --project-dir flag)",
561
+ )
562
+
563
+ # register (v1.6: one-time global IDE registration)
564
+ register_parser = subparsers.add_parser(
565
+ "register",
566
+ help="One-time global IDE registration — inject Codevira into all detected AI tools",
567
+ )
568
+ register_parser.add_argument(
569
+ "--claude-desktop", action="store_true",
570
+ help="Only configure Claude Desktop (stdio mode)",
571
+ )
572
+ register_parser.add_argument(
573
+ "--http-url",
574
+ metavar="URL",
575
+ help="Inject an HTTP URL config into Claude Code (e.g. https://localhost:7443/mcp)",
576
+ )
577
+
578
+ args = parser.parse_args(raw_args)
579
+
580
+ if args.command == "init":
581
+ # Pass overrides via function attribute (avoids changing signature)
582
+ cmd_init._overrides = {
583
+ "name": getattr(args, "name", None),
584
+ "language": getattr(args, "language", None),
585
+ "dirs": getattr(args, "dirs", None),
586
+ "ext": getattr(args, "ext", None),
587
+ }
588
+ cmd_init._no_inject = getattr(args, "no_inject", False)
589
+ cmd_init()
590
+ elif args.command == "index":
591
+ cmd_index(full=args.full, quiet=args.quiet)
592
+ elif args.command == "status":
593
+ cmd_status()
594
+ elif args.command == "report":
595
+ cmd_report(limit=args.limit, clear=args.clear)
596
+ elif args.command == "serve":
597
+ # --project-dir may appear after "serve" — merge with pre-parsed value
598
+ sub_project_dir = getattr(args, "project_dir", None)
599
+ if sub_project_dir and project_dir is None:
600
+ from mcp_server.paths import set_project_dir
601
+ project_dir = Path(sub_project_dir).resolve()
602
+ set_project_dir(project_dir)
603
+ cmd_serve(
604
+ host=args.host,
605
+ port=args.port,
606
+ use_https=args.https,
607
+ project_dir=project_dir,
608
+ install_service=getattr(args, "install_service", False),
609
+ uninstall_service=getattr(args, "uninstall_service", False),
610
+ )
611
+ elif args.command == "register":
612
+ cmd_register(
613
+ claude_desktop=getattr(args, "claude_desktop", False),
614
+ http_url=getattr(args, "http_url", None),
615
+ )
616
+ else:
617
+ # No subcommand → start MCP server (stdio transport)
618
+ cmd_server()
619
+
620
+
621
+ if __name__ == "__main__":
622
+ main()