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.
- {synap_git-2.1.4 → synap_git-2.1.6}/CHANGELOG.md +19 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/PKG-INFO +17 -13
- {synap_git-2.1.4 → synap_git-2.1.6}/README.md +12 -8
- {synap_git-2.1.4 → synap_git-2.1.6}/pyproject.toml +4 -4
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/app.py +1 -1
- synap_git-2.1.6/src/synap_git/cli/checkpoint.py +125 -0
- synap_git-2.1.6/src/synap_git/cli/lessons.py +132 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/main.py +34 -10
- synap_git-2.1.6/src/synap_git/cli/mcp.py +137 -0
- synap_git-2.1.6/src/synap_git/cli/memory.py +114 -0
- synap_git-2.1.6/src/synap_git/cli/usage.py +88 -0
- synap_git-2.1.6/src/synap_git/cli/wiki.py +86 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/config.py +7 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/tracing.py +2 -4
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/git/state.py +1 -1
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/daemon.py +15 -14
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/engine.py +78 -79
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/scanner.py +33 -3
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/wiki.py +4 -4
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/mcp/server.py +2 -6
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/parser/registry.py +33 -36
- synap_git-2.1.6/src/synap_git/provider/__init__.py +1 -0
- synap_git-2.1.6/src/synap_git/provider/factory.py +164 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/engine.py +11 -8
- synap_git-2.1.6/src/synap_git/security/__init__.py +5 -0
- synap_git-2.1.6/src/synap_git/security/redactor.py +56 -0
- synap_git-2.1.6/src/synap_git/security/sanitizer.py +29 -0
- synap_git-2.1.6/src/synap_git/security/validator.py +33 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/storage/sqlite.py +17 -4
- synap_git-2.1.4/src/synap_git/provider/factory.py +0 -107
- {synap_git-2.1.4 → synap_git-2.1.6}/.gitignore +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/.synap.example/README.md +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/LICENSE.md +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/diagnostics/logger.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/anthropic.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/provider/openrouter.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.4 → synap_git-2.1.6}/src/synap_git/utils/__init__.py +0 -0
- {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.
|
|
3
|
+
Version: 2.1.6
|
|
4
4
|
Summary: Persistent structural context infrastructure for AI coding agents.
|
|
5
|
-
Project-URL: Homepage, https://github.com/saahilpal/
|
|
6
|
-
Project-URL: Repository, https://github.com/saahilpal/
|
|
7
|
-
Project-URL: Issues, https://github.com/saahilpal/
|
|
8
|
-
Project-URL: Changelog, https://github.com/saahilpal/
|
|
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
|
|
446
|
+
Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
|
|
447
|
+
|
|
447
448
|
* **Python** (`.py`)
|
|
448
|
-
* **JavaScript /
|
|
449
|
-
* **
|
|
450
|
-
* **
|
|
451
|
-
* **Rust** (`.rs`)
|
|
452
|
-
* **
|
|
453
|
-
* **
|
|
454
|
-
* **
|
|
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
|
|
396
|
+
Synap now processes syntax dynamically for **over 50 programming languages** via `tree-sitter-languages`, including:
|
|
397
|
+
|
|
397
398
|
* **Python** (`.py`)
|
|
398
|
-
* **JavaScript /
|
|
399
|
-
* **
|
|
400
|
-
* **
|
|
401
|
-
* **Rust** (`.rs`)
|
|
402
|
-
* **
|
|
403
|
-
* **
|
|
404
|
-
* **
|
|
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/
|
|
44
|
-
Repository = "https://github.com/saahilpal/
|
|
45
|
-
Issues = "https://github.com/saahilpal/
|
|
46
|
-
Changelog = "https://github.com/saahilpal/
|
|
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]
|
|
@@ -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",
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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(
|
|
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",
|
|
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
|
-
|
|
722
|
-
|
|
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",
|
|
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
|
-
"""
|
|
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",
|
|
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",
|
|
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)
|