synap-git 2.1.4__tar.gz → 2.1.6__tar.gz

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 (57) hide show
  1. {synap_git-2.1.4 → synap_git-2.1.6}/CHANGELOG.md +19 -0
  2. {synap_git-2.1.4 → synap_git-2.1.6}/PKG-INFO +17 -13
  3. {synap_git-2.1.4 → synap_git-2.1.6}/README.md +12 -8
  4. {synap_git-2.1.4 → synap_git-2.1.6}/pyproject.toml +4 -4
  5. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/__init__.py +1 -1
  6. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/app.py +1 -1
  7. synap_git-2.1.6/src/synap_git/cli/checkpoint.py +125 -0
  8. synap_git-2.1.6/src/synap_git/cli/lessons.py +132 -0
  9. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/main.py +34 -10
  10. synap_git-2.1.6/src/synap_git/cli/mcp.py +137 -0
  11. synap_git-2.1.6/src/synap_git/cli/memory.py +114 -0
  12. synap_git-2.1.6/src/synap_git/cli/usage.py +88 -0
  13. synap_git-2.1.6/src/synap_git/cli/wiki.py +86 -0
  14. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/config.py +7 -0
  15. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/tracing.py +2 -4
  16. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/git/state.py +1 -1
  17. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/daemon.py +15 -14
  18. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/engine.py +78 -79
  19. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/scanner.py +33 -3
  20. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/wiki.py +4 -4
  21. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/mcp/server.py +2 -6
  22. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/parser/registry.py +33 -36
  23. synap_git-2.1.6/src/synap_git/provider/__init__.py +1 -0
  24. synap_git-2.1.6/src/synap_git/provider/factory.py +164 -0
  25. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/engine.py +11 -8
  26. synap_git-2.1.6/src/synap_git/security/__init__.py +5 -0
  27. synap_git-2.1.6/src/synap_git/security/redactor.py +56 -0
  28. synap_git-2.1.6/src/synap_git/security/sanitizer.py +29 -0
  29. synap_git-2.1.6/src/synap_git/security/validator.py +33 -0
  30. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/storage/sqlite.py +17 -4
  31. synap_git-2.1.4/src/synap_git/provider/factory.py +0 -107
  32. {synap_git-2.1.4 → synap_git-2.1.6}/.gitignore +0 -0
  33. {synap_git-2.1.4 → synap_git-2.1.6}/.synap.example/README.md +0 -0
  34. {synap_git-2.1.4 → synap_git-2.1.6}/LICENSE.md +0 -0
  35. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/__init__.py +0 -0
  36. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/static/index.html +0 -0
  37. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/__init__.py +0 -0
  38. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/__main__.py +0 -0
  39. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/__init__.py +0 -0
  40. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/logger.py +0 -0
  41. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/embeddings/__init__.py +0 -0
  42. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/git/__init__.py +0 -0
  43. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/__init__.py +0 -0
  44. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/mcp/__init__.py +0 -0
  45. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/parser/__init__.py +0 -0
  46. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/anthropic.py +0 -0
  47. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/base.py +0 -0
  48. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/gemini.py +0 -0
  49. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/ollama.py +0 -0
  50. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/openai.py +0 -0
  51. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/openrouter.py +0 -0
  52. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/py.typed +0 -0
  53. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/__init__.py +0 -0
  54. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/memory.py +0 -0
  55. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/storage/__init__.py +0 -0
  56. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/utils/__init__.py +0 -0
  57. {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/utils/serialization.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.5] - 2026-06-11
9
+
10
+ ## [2.1.6] - 2026-06-29
11
+
12
+ ### Added
13
+ - **Universal Language Expansion**: Expanded tree-sitter AST parsing support from 4 hardcoded languages to dynamically supporting over ~50 languages using `tree-sitter-languages`. Also added dynamic shebang detection (`#!`) to parse extensionless scripts as python, ruby, node, and bash scripts.
14
+ - **API Rate Limiting**: Implemented a global `RateLimitedProvider` to throttle LLM and embedding calls to 3 requests per second to mitigate resource exhaustion on the diagnostic API.
15
+
16
+ ### Fixed
17
+ - **Broad Exception Handling**: Eradicated remaining `except Exception:` blocks across diagnostics, daemon execution, indexer scanning, and module resolution, and replaced them with `OSError`, `sqlite3.Error`, and specific errors to prevent silent data corruption.
18
+ - **Async Sleep Compatibility**: Fixed thread-blocking `time.sleep()` in LLM retries by delegating to `anyio.from_thread.run(anyio.sleep)` if invoked inside FastMCP worker threads.
19
+ - **Edge Resolution & Source Paths**: Added support for resolving Javascript, Java, C++, Ruby, and more during import/edge resolution.
20
+ - **Module Path Trimming**: Improved module path mapping by trimming `internal/`, `source/`, `packages/`, `main/`, `java/`, `kotlin/`, `scala/` from module keys.
21
+ - **Wipe Confirmation**: Added a `--yes` confirmation bypass flag for `synap wipe` and enforced safety confirmation in non-interactive terminals.
22
+ - **Metadata Fixes**: Corrected repository URLs in `pyproject.toml` to correctly point to `synapse` instead of `synap-git`.
23
+
24
+ ### Refactored
25
+ - **Daemon Teardown**: Refactored the forceful SIGTERM daemon teardown logic to safely toggle via `force_exit=True` rather than a blanket catch-all. Makes programmatic `daemon.stop()` safer without blocking terminal UI interactions.
26
+
8
27
  ## [2.1.4] - 2026-06-11
9
28
 
10
29
  ### Fixed
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 2.1.4
3
+ Version: 2.1.6
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
- Project-URL: Homepage, https://github.com/saahilpal/synap-git
6
- Project-URL: Repository, https://github.com/saahilpal/synap-git
7
- Project-URL: Issues, https://github.com/saahilpal/synap-git/issues
8
- Project-URL: Changelog, https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md
5
+ Project-URL: Homepage, https://github.com/saahilpal/synapse
6
+ Project-URL: Repository, https://github.com/saahilpal/synapse
7
+ Project-URL: Issues, https://github.com/saahilpal/synapse/issues
8
+ Project-URL: Changelog, https://github.com/saahilpal/synapse/blob/main/CHANGELOG.md
9
9
  Author: Synap contributors
10
10
  License: Apache-2.0
11
11
  License-File: LICENSE.md
@@ -443,15 +443,19 @@ synap mcp config .
443
443
 
444
444
  ## Supported Languages
445
445
 
446
- Synap processes syntax for the following formats:
446
+ Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
447
+
447
448
  * **Python** (`.py`)
448
- * **JavaScript / JSX** (`.js`, `.jsx`)
449
- * **TypeScript / TSX** (`.ts`, `.tsx`)
450
- * **Go** (`.go`)
451
- * **Rust** (`.rs`)
452
- * **Java** (`.java`)
453
- * **C / C++** (`.cpp`, `.cc`, `.cxx`, `.hpp`, `.h`)
454
- * **Ruby** (`.rb`)
449
+ * **JavaScript / TypeScript** (`.js`, `.jsx`, `.ts`, `.tsx`)
450
+ * **C / C++ / C#** (`.c`, `.cpp`, `.h`, `.hpp`, `.cs`)
451
+ * **Java / Kotlin / Scala** (`.java`, `.kt`, `.kts`, `.scala`)
452
+ * **Go / Rust / Swift** (`.go`, `.rs`, `.swift`)
453
+ * **Ruby / PHP / Perl** (`.rb`, `.php`, `.pl`)
454
+ * **Shell Scripts** (`.sh`, `.bash`, or via `#!` shebang detection)
455
+ * **Data / Config** (`.sql`, `.json`, `.yaml`, `.toml`, `.xml`)
456
+ * **Web** (`.html`, `.css`, `.scss`)
457
+
458
+ Even extensionless scripts are gracefully detected via shebang parsing. Missing grammars automatically degrade gracefully to plaintext indexing.
455
459
 
456
460
  ---
457
461
 
@@ -393,15 +393,19 @@ synap mcp config .
393
393
 
394
394
  ## Supported Languages
395
395
 
396
- Synap processes syntax for the following formats:
396
+ Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
397
+
397
398
  * **Python** (`.py`)
398
- * **JavaScript / JSX** (`.js`, `.jsx`)
399
- * **TypeScript / TSX** (`.ts`, `.tsx`)
400
- * **Go** (`.go`)
401
- * **Rust** (`.rs`)
402
- * **Java** (`.java`)
403
- * **C / C++** (`.cpp`, `.cc`, `.cxx`, `.hpp`, `.h`)
404
- * **Ruby** (`.rb`)
399
+ * **JavaScript / TypeScript** (`.js`, `.jsx`, `.ts`, `.tsx`)
400
+ * **C / C++ / C#** (`.c`, `.cpp`, `.h`, `.hpp`, `.cs`)
401
+ * **Java / Kotlin / Scala** (`.java`, `.kt`, `.kts`, `.scala`)
402
+ * **Go / Rust / Swift** (`.go`, `.rs`, `.swift`)
403
+ * **Ruby / PHP / Perl** (`.rb`, `.php`, `.pl`)
404
+ * **Shell Scripts** (`.sh`, `.bash`, or via `#!` shebang detection)
405
+ * **Data / Config** (`.sql`, `.json`, `.yaml`, `.toml`, `.xml`)
406
+ * **Web** (`.html`, `.css`, `.scss`)
407
+
408
+ Even extensionless scripts are gracefully detected via shebang parsing. Missing grammars automatically degrade gracefully to plaintext indexing.
405
409
 
406
410
  ---
407
411
 
@@ -40,10 +40,10 @@ dependencies = [
40
40
  ]
41
41
 
42
42
  [project.urls]
43
- Homepage = "https://github.com/saahilpal/synap-git"
44
- Repository = "https://github.com/saahilpal/synap-git"
45
- Issues = "https://github.com/saahilpal/synap-git/issues"
46
- Changelog = "https://github.com/saahilpal/synap-git/blob/main/CHANGELOG.md"
43
+ Homepage = "https://github.com/saahilpal/synapse"
44
+ Repository = "https://github.com/saahilpal/synapse"
45
+ Issues = "https://github.com/saahilpal/synapse/issues"
46
+ Changelog = "https://github.com/saahilpal/synapse/blob/main/CHANGELOG.md"
47
47
 
48
48
 
49
49
  [project.optional-dependencies]
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "2.1.4"
5
+ __version__ = "2.1.6"
@@ -104,7 +104,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
104
104
  except Exception as e:
105
105
  import structlog
106
106
 
107
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
107
+ structlog.get_logger().error("suppressed_error_caught", exc_info=True)
108
108
 
109
109
  wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
110
110
  exists = await asyncio.to_thread(wiki_path.exists)
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import uuid
5
+ from datetime import datetime
6
+ from typing import Annotated
7
+
8
+ import typer
9
+ from rich.table import Table
10
+
11
+ from synap_git.cli.main import (
12
+ _settings,
13
+ console,
14
+ )
15
+ from synap_git.indexer.engine import SynapRuntime
16
+
17
+ app = typer.Typer(help="Checkpoint commands.", no_args_is_help=True)
18
+
19
+
20
+ @app.command("create")
21
+ def checkpoint_create(
22
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
23
+ doing: Annotated[
24
+ str, typer.Option(help="What the agent is currently doing.")
25
+ ] = "Manual snapshot",
26
+ files: Annotated[str, typer.Option(help="Comma-separated list of changed files.")] = "",
27
+ next_step: Annotated[str, typer.Option(help="The next step to be taken.")] = "",
28
+ blockers: Annotated[str, typer.Option(help="Current blockers or obstacles.")] = "",
29
+ ) -> None:
30
+ """Create a new context checkpoint."""
31
+ runtime = SynapRuntime(_settings(path))
32
+
33
+ checkpoint_id = str(uuid.uuid4())
34
+ status = runtime.status()
35
+ branch = status.branch
36
+ commit = status.git_commit or "unknown"
37
+
38
+ file_list = [f.strip() for f in files.split(",") if f.strip()]
39
+
40
+ runtime.store.put_checkpoint(
41
+ checkpoint_id=checkpoint_id,
42
+ branch=branch,
43
+ commit_hash=commit,
44
+ doing=doing,
45
+ changed_files=json.dumps(file_list),
46
+ next_step=next_step,
47
+ blockers=blockers,
48
+ )
49
+ console.print(f"[green]✓ Checkpoint {checkpoint_id} created for branch '{branch}'.[/green]")
50
+
51
+
52
+ @app.command("list")
53
+ def checkpoint_list(
54
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
55
+ ) -> None:
56
+ """List checkpoints for the active branch."""
57
+ runtime = SynapRuntime(_settings(path))
58
+ status = runtime.status()
59
+ branch = status.branch
60
+ cps = runtime.store.get_checkpoints(branch)
61
+
62
+ if not cps:
63
+ console.print(f"[yellow]No checkpoints found for branch '{branch}'.[/yellow]")
64
+ return
65
+
66
+ table = Table(
67
+ title=f"Checkpoints for branch '{branch}'", show_header=True, header_style="bold cyan"
68
+ )
69
+ table.add_column("Checkpoint ID")
70
+ table.add_column("Commit Hash")
71
+ table.add_column("Doing")
72
+ table.add_column("Changed Files")
73
+ table.add_column("Created At")
74
+
75
+ for cp in cps:
76
+ created = datetime.fromtimestamp(cp["created_at"]).isoformat()
77
+ try:
78
+ ch_files = ", ".join(json.loads(cp["changed_files"]))
79
+ except Exception:
80
+ ch_files = cp["changed_files"]
81
+ table.add_row(
82
+ cp["checkpoint_id"][:16] + "…",
83
+ cp["commit_hash"][:7],
84
+ cp["doing"],
85
+ ch_files if ch_files else "None",
86
+ created,
87
+ )
88
+ console.print(table)
89
+
90
+
91
+ @app.command("restore")
92
+ def checkpoint_restore(
93
+ checkpoint_id: Annotated[
94
+ str, typer.Argument(help="The ID of the checkpoint to restore (defaults to latest).")
95
+ ] = "latest",
96
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
97
+ ) -> None:
98
+ """Restore and show details of a checkpoint."""
99
+ runtime = SynapRuntime(_settings(path))
100
+ status = runtime.status()
101
+ branch = status.branch
102
+
103
+ if checkpoint_id == "latest":
104
+ cp = runtime.store.get_latest_checkpoint(branch)
105
+ else:
106
+ cp = runtime.store.get_checkpoint(checkpoint_id)
107
+
108
+ if not cp:
109
+ console.print(f"[red]✗ Checkpoint '{checkpoint_id}' not found.[/red]")
110
+ raise typer.Exit(1)
111
+
112
+ console.print(f"[bold green]✓ Checkpoint '{cp['checkpoint_id']}' details:[/bold green]")
113
+ console.print(f" [bold]Branch:[/bold] {cp['branch']}")
114
+ console.print(f" [bold]Commit:[/bold] {cp['commit_hash']}")
115
+ console.print(f" [bold]Doing:[/bold] {cp['doing']}")
116
+ try:
117
+ ch_files = ", ".join(json.loads(cp["changed_files"]))
118
+ except Exception:
119
+ ch_files = cp["changed_files"]
120
+ console.print(f" [bold]Changed Files:[/bold] {ch_files if ch_files else 'None'}")
121
+ console.print(f" [bold]Next Step:[/bold] {cp['next_step'] or 'None'}")
122
+ console.print(f" [bold]Blockers:[/bold] {cp['blockers'] or 'None'}")
123
+ console.print(
124
+ f" [bold]Created At:[/bold] {datetime.fromtimestamp(cp['created_at']).isoformat()}"
125
+ )
@@ -0,0 +1,132 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import questionary
6
+ import typer
7
+ from rich.panel import Panel
8
+
9
+ from synap_git.cli.main import (
10
+ _settings,
11
+ console,
12
+ )
13
+ from synap_git.indexer.engine import SynapRuntime
14
+
15
+ app = typer.Typer(help="Lessons commands.", no_args_is_help=True)
16
+
17
+
18
+ @app.command("approve")
19
+ def lessons_approve(
20
+ lesson_id: Annotated[str, typer.Argument(help="The ID of the lesson to approve.")],
21
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
22
+ ) -> None:
23
+ """Approve a pending lesson."""
24
+ runtime = SynapRuntime(_settings(path))
25
+ # Fetch to ensure it exists and is pending
26
+ pending = runtime.store.get_lessons("pending")
27
+ target = next((lesson for lesson in pending if lesson["lesson_id"] == lesson_id), None)
28
+
29
+ if not target:
30
+ console.print(f"[red]✗ Pending lesson {lesson_id} not found.[/red]")
31
+ raise typer.Exit(1)
32
+
33
+ runtime.store.update_lesson(lesson_id, target["why_failed"], "approved", actor="cli_user")
34
+ console.print(f"[green]✓ Lesson {lesson_id} approved. Memory updated.[/green]")
35
+
36
+
37
+ @app.command("review")
38
+ def lessons_review(
39
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
40
+ ) -> None:
41
+ """Interactively review and manage pending lessons."""
42
+
43
+ runtime = SynapRuntime(_settings(path))
44
+ pending = runtime.store.get_lessons("pending")
45
+
46
+ if not pending:
47
+ console.print("[yellow]No pending lessons to review.[/yellow]")
48
+ return
49
+
50
+ console.print(f"[bold cyan]Reviewing {len(pending)} pending lessons...[/bold cyan]\n")
51
+
52
+ approved_count = 0
53
+ rejected_count = 0
54
+ skipped_count = 0
55
+
56
+ for lesson in pending:
57
+ lesson_id = lesson["lesson_id"]
58
+
59
+ # Build detail view
60
+ details = (
61
+ f"[bold]Revert Commit:[/bold] {lesson['revert_commit'][:8]}\n"
62
+ f"[bold]Reverted From:[/bold] {lesson['reverted_from'][:8]}\n"
63
+ f"[bold]Files Affected:[/bold] {lesson['files_affected']}\n"
64
+ f"[bold]What Failed:[/bold] {lesson['what_failed']}\n"
65
+ f"[bold]Proposed Lesson:[/bold] {lesson['why_failed']}"
66
+ )
67
+
68
+ console.print(Panel(details, title=f"Lesson {lesson_id[:8]}", expand=False))
69
+
70
+ choice = questionary.select(
71
+ "Action:",
72
+ choices=[
73
+ questionary.Choice("Approve", value="approve"),
74
+ questionary.Choice("Edit & Approve", value="edit"),
75
+ questionary.Choice("Reject", value="reject"),
76
+ questionary.Choice("Skip", value="skip"),
77
+ ],
78
+ ).ask()
79
+
80
+ if choice == "approve":
81
+ runtime.store.update_lesson(
82
+ lesson_id, lesson["why_failed"], "approved", actor="cli_user"
83
+ )
84
+ console.print("[green]✓ Approved.[/green]\n")
85
+ approved_count += 1
86
+ elif choice == "edit":
87
+ edited = questionary.text("Edit lesson text:", default=lesson["why_failed"]).ask()
88
+ if edited:
89
+ runtime.store.update_lesson(lesson_id, edited, "approved", actor="cli_user")
90
+ console.print("[green]✓ Edited and Approved.[/green]\n")
91
+ approved_count += 1
92
+ else:
93
+ console.print("[yellow]Skipping edit.[/yellow]\n")
94
+ skipped_count += 1
95
+ elif choice == "reject":
96
+ runtime.store.update_lesson(
97
+ lesson_id, lesson["why_failed"], "rejected", actor="cli_user"
98
+ )
99
+ console.print("[red]✗ Rejected.[/red]\n")
100
+ rejected_count += 1
101
+ elif choice == "skip":
102
+ console.print("[yellow]Skipped.[/yellow]\n")
103
+ skipped_count += 1
104
+ else:
105
+ console.print("[yellow]Aborting review.[/yellow]")
106
+ break
107
+
108
+ console.print("[bold cyan]Review Summary:[/bold cyan]")
109
+ console.print(f" [green]Approved:[/green] {approved_count}")
110
+ console.print(f" [red]Rejected:[/red] {rejected_count}")
111
+ console.print(f" [yellow]Skipped:[/yellow] {skipped_count}")
112
+
113
+
114
+ @app.command("reject")
115
+ def lessons_reject(
116
+ lesson_id: Annotated[str, typer.Argument(help="The ID of the lesson to reject.")],
117
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
118
+ ) -> None:
119
+ """Reject a pending or approved lesson."""
120
+ runtime = SynapRuntime(_settings(path))
121
+ pending = runtime.store.get_lessons("pending")
122
+ approved = runtime.store.get_lessons("approved")
123
+ target = next(
124
+ (lesson for lesson in pending + approved if lesson["lesson_id"] == lesson_id), None
125
+ )
126
+
127
+ if not target:
128
+ console.print(f"[red]✗ Lesson {lesson_id} not found in pending or approved queues.[/red]")
129
+ raise typer.Exit(1)
130
+
131
+ runtime.store.update_lesson(lesson_id, target["why_failed"], "rejected", actor="cli_user")
132
+ console.print(f"[yellow]✓ Lesson {lesson_id} rejected. Memory updated.[/yellow]")
@@ -143,7 +143,13 @@ app.add_typer(usage_app, name="usage")
143
143
  def setup(
144
144
  path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
145
145
  ) -> None:
146
- """Interactive first-run setup and onboarding."""
146
+ """
147
+ Interactive first-run setup and onboarding.
148
+
149
+ Examples:
150
+ synap setup
151
+ synap setup /path/to/repo
152
+ """
147
153
  import sys
148
154
 
149
155
  import httpx
@@ -516,7 +522,13 @@ def init(
516
522
  quiet: Annotated[bool, typer.Option("--quiet", help="Suppress output.")] = False,
517
523
  json_output: Annotated[bool, JSON_OPTION] = False,
518
524
  ) -> None:
519
- """Initialize local Synap state and perform first scan."""
525
+ """
526
+ Initialize local Synap state and perform first scan.
527
+
528
+ Examples:
529
+ synap init
530
+ synap init --force --skip-llm
531
+ """
520
532
  settings = _settings(path, json_output=json_output)
521
533
  if skip_llm:
522
534
  settings.llm_provider = None
@@ -547,9 +559,14 @@ def init(
547
559
  @app.command()
548
560
  def wipe(
549
561
  path: Annotated[str, typer.Argument(help="Repository path to wipe.")] = ".",
562
+ yes: Annotated[
563
+ bool, typer.Option("--yes", "-y", help="Skip confirmation prompt and force wipe")
564
+ ] = False,
550
565
  ) -> None:
551
566
  """Completely purge the local index for a fresh rebuild."""
552
- if not typer.confirm("This will delete all indexed symbols and embeddings. Continue?"):
567
+ if not yes and not typer.confirm(
568
+ "This will delete all indexed symbols and embeddings. Continue?"
569
+ ):
553
570
  raise typer.Abort()
554
571
  runtime = SynapRuntime(_settings(path))
555
572
  runtime.wipe_index()
@@ -651,7 +668,7 @@ def start(
651
668
  except Exception as e:
652
669
  import structlog
653
670
 
654
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
671
+ structlog.get_logger().error("suppressed_error_caught", exc_info=True)
655
672
 
656
673
  # 2. Spawn detached daemon process
657
674
  cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
@@ -718,8 +735,9 @@ def daemon_run(
718
735
  daemon = RuntimeDaemon(settings)
719
736
  asyncio.run(daemon.start())
720
737
 
721
- # Forceful process exit to prevent atexit thread-join blocking
722
- os._exit(0)
738
+ import sys
739
+
740
+ sys.exit(0)
723
741
 
724
742
 
725
743
  @app.command()
@@ -802,7 +820,7 @@ def stop(
802
820
  except Exception as e:
803
821
  import structlog
804
822
 
805
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
823
+ structlog.get_logger().error("suppressed_error_caught", exc_info=True)
806
824
  success = not _is_process_running(pid)
807
825
 
808
826
  # Clean up lockfiles
@@ -968,7 +986,13 @@ def search(
968
986
  int, typer.Option("--max-tokens", help="Maximum context window tokens.")
969
987
  ] = 4000,
970
988
  ) -> None:
971
- """Execute a hybrid structural search across the repository."""
989
+ """
990
+ Execute a hybrid structural search across the repository.
991
+
992
+ Examples:
993
+ synap search "database connection logic"
994
+ synap search "authentication middleware" --path /path/to/repo --max-tokens 8000
995
+ """
972
996
  settings = _settings(path)
973
997
  runtime = SynapRuntime(settings)
974
998
 
@@ -1247,7 +1271,7 @@ def rollback(
1247
1271
  except Exception as e:
1248
1272
  import structlog
1249
1273
 
1250
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
1274
+ structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1251
1275
 
1252
1276
  with console.status(
1253
1277
  f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
@@ -1322,7 +1346,7 @@ def repair(
1322
1346
  except Exception as e:
1323
1347
  import structlog
1324
1348
 
1325
- structlog.get_logger().error("suppressed_error_caught", error=str(e), exc_info=True)
1349
+ structlog.get_logger().error("suppressed_error_caught", exc_info=True)
1326
1350
 
1327
1351
  status_info = runtime.status()
1328
1352
  console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
@@ -0,0 +1,137 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import sys
6
+ import time
7
+ import uuid
8
+ from pathlib import Path
9
+ from typing import Annotated, Any
10
+
11
+ import typer
12
+ from rich.table import Table
13
+
14
+ from synap_git.cli.main import (
15
+ _settings,
16
+ console,
17
+ )
18
+ from synap_git.indexer.engine import SynapRuntime
19
+ from synap_git.mcp.server import SynapMCPFacade, SynapMCPServer
20
+
21
+ app = typer.Typer(help="Mcp commands.", no_args_is_help=True)
22
+
23
+
24
+ @app.command("start")
25
+ def mcp_start(
26
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
27
+ ) -> None:
28
+ """Start the MCP server."""
29
+ settings = _settings(path, json_output=True)
30
+ runtime = SynapRuntime(settings)
31
+ runtime.bootstrap()
32
+
33
+ server = SynapMCPServer(runtime)
34
+ asyncio.run(server.run())
35
+
36
+
37
+ @app.command("config")
38
+ def mcp_config(
39
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
40
+ ) -> None:
41
+ """Output the MCP server configuration for manual IDE setup."""
42
+ abs_path = Path(path).resolve().as_posix()
43
+
44
+ config = {
45
+ "mcpServers": {
46
+ "synap": {
47
+ "command": sys.executable,
48
+ "args": ["-m", "synap_git.cli", "mcp", "start", abs_path],
49
+ "autoConnect": True,
50
+ }
51
+ }
52
+ }
53
+ typer.echo(json.dumps(config, indent=2))
54
+
55
+
56
+ @app.command("verify")
57
+ def mcp_verify(
58
+ path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
59
+ ) -> None:
60
+ """Verify MCP tools, schemas, and transport stability."""
61
+
62
+ console.print("[bold cyan]Synap MCP Verification[/bold cyan]\n")
63
+
64
+ settings = _settings(path)
65
+ runtime = SynapRuntime(settings)
66
+ runtime.bootstrap()
67
+
68
+ facade = SynapMCPFacade(runtime)
69
+
70
+ table = Table(show_header=True, header_style="bold magenta")
71
+ table.add_column("Stage")
72
+ table.add_column("Status")
73
+ table.add_column("Latency (ms)")
74
+ table.add_column("Details")
75
+
76
+ with console.status(
77
+ "[bold yellow]Verifying MCP transport and tools...[/bold yellow]"
78
+ ) as status:
79
+
80
+ def _simulate_mcp_call(tool_name: str, *args: Any, **kwargs: Any) -> bool:
81
+ status.update(
82
+ f"[bold yellow]Verifying stage: [white]{tool_name}[/white]...[/bold yellow]"
83
+ )
84
+ start = time.monotonic()
85
+ try:
86
+ status_info = facade.runtime.status()
87
+ dirty = status_info.is_dirty
88
+ warnings = ["Working tree is dirty. Index may be stale."] if dirty else []
89
+
90
+ method = getattr(facade, tool_name)
91
+ data = method(*args, **kwargs)
92
+
93
+ response = {
94
+ "ok": True,
95
+ "data": data,
96
+ "warnings": warnings,
97
+ "trace_id": str(uuid.uuid4()),
98
+ "dirty_tree": dirty,
99
+ }
100
+
101
+ # Verify strict schema
102
+ assert "ok" in response
103
+ assert "data" in response
104
+ assert "warnings" in response
105
+ assert "trace_id" in response
106
+ assert "dirty_tree" in response
107
+
108
+ latency = (time.monotonic() - start) * 1000
109
+
110
+ if tool_name == "search":
111
+ # Check trace payload exists
112
+ assert "trace" in data, "Trace payload missing in search data"
113
+
114
+ details = f"keys: {list(data.keys())}"
115
+ table.add_row(tool_name, "[green]PASS[/green]", f"{latency:.1f}", details)
116
+ time.sleep(0.1) # Visual breathing room
117
+ return True
118
+ except Exception as e:
119
+ latency = (time.monotonic() - start) * 1000
120
+ table.add_row(tool_name, "[red]FAIL[/red]", f"{latency:.1f}", str(e))
121
+ return False
122
+
123
+ results = [
124
+ _simulate_mcp_call("get_status"),
125
+ _simulate_mcp_call("verify_system"),
126
+ _simulate_mcp_call("search", "User"),
127
+ _simulate_mcp_call("create_checkpoint", "Testing MCP", ["test.py"], "Next", "None"),
128
+ _simulate_mcp_call("restore_checkpoint", "latest"),
129
+ ]
130
+
131
+ console.print(table)
132
+
133
+ if all(results):
134
+ console.print("\n[bold green]✓ MCP Protocol Verified. All contracts passed.[/bold green]")
135
+ else:
136
+ console.print("\n[bold red]✗ MCP Protocol Verification Failed.[/bold red]")
137
+ raise typer.Exit(1)