synap-git 2.1.6__tar.gz → 2.2.0__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.6 → synap_git-2.2.0}/.gitignore +7 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/CHANGELOG.md +24 -1
- {synap_git-2.1.6 → synap_git-2.2.0}/PKG-INFO +6 -7
- {synap_git-2.1.6 → synap_git-2.2.0}/README.md +3 -5
- {synap_git-2.1.6 → synap_git-2.2.0}/pyproject.toml +1 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/app.py +1 -1
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/main.py +149 -94
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/config.py +0 -6
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/logger.py +4 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/git/state.py +1 -1
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/daemon.py +107 -107
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/engine.py +89 -59
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/scanner.py +2 -2
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/wiki.py +4 -4
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/mcp/server.py +2 -2
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/parser/registry.py +47 -1
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/anthropic.py +6 -5
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/factory.py +4 -3
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/openrouter.py +6 -2
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/engine.py +17 -10
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/storage/sqlite.py +38 -1
- synap_git-2.2.0/website/README.md +36 -0
- synap_git-2.1.6/src/synap_git/cli/mcp.py +0 -137
- {synap_git-2.1.6 → synap_git-2.2.0}/.synap.example/README.md +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/LICENSE.md +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/checkpoint.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/lessons.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/memory.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/usage.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/cli/wiki.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/redactor.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/sanitizer.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/security/validator.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.2.0}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,7 +5,30 @@ 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.
|
|
8
|
+
## [2.2.0] - 2026-08-03
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **AST Symbol Parser Expansion (AUD-02)**: Implemented Tree-sitter AST symbol extraction handlers for C#, PHP, Kotlin, Scala, and C.
|
|
12
|
+
- **Event-Driven File Watcher (AUD-03)**: Replaced continuous 2s subprocess polling with `watchdog` filesystem event tracking, reducing idle CPU usage to near-zero.
|
|
13
|
+
- **Daemon REST API Diagnostic Checks (AUD-06)**: Added daemon port availability and REST API endpoint status checks to `synap doctor`.
|
|
14
|
+
- **Wiki Task Requeue Subcommand (AUD-08)**: Added `synap wiki retry` CLI subcommand and `retry_failed_wiki_queue` storage method to requeue permanently failed wiki tasks back to pending state.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- **CLI `synap start` Control Flow (AUD-01)**: Fixed `synap start` so it exits cleanly when a daemon is already running unless `--foreground` is explicitly passed.
|
|
18
|
+
- **SQLite WAL Performance Rationale (AUD-07)**: Documented `PRAGMA synchronous=NORMAL` in WAL mode as an intentional architectural tradeoff balancing write throughput and self-healing index recovery.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- **Graceful Embedding Provider Fallback (AUD-04)**: Configured Anthropic and OpenRouter providers to log a warning and return empty embedding vectors instead of raising unhandled `NotImplementedError`.
|
|
22
|
+
- **Symbol Preservation on File Renames (AUD-05)**: Added `-M` rename detection to `git diff-tree` in incremental indexing to preserve file symbol history and edges across renames.
|
|
23
|
+
|
|
24
|
+
## [2.1.7] - 2026-07-29
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- **AI Engineer Rules**: Created standardized agent rulesets across `AGENTS.md`, `.cursorrules`, `.windsurfrules`, and `.github/copilot-instructions.md`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- **Daemon Branch Switching Test Stability**: Resolved timing race condition in daemon branch checkout verification.
|
|
31
|
+
- **CLI Setup Connectivity**: Fixed setup connectivity verification and non-TTY prompt fallback handling.
|
|
9
32
|
|
|
10
33
|
## [2.1.6] - 2026-06-29
|
|
11
34
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Persistent structural context infrastructure for AI coding agents.
|
|
5
5
|
Project-URL: Homepage, https://github.com/saahilpal/synapse
|
|
6
6
|
Project-URL: Repository, https://github.com/saahilpal/synapse
|
|
@@ -37,6 +37,7 @@ Requires-Dist: tree-sitter-languages==1.10.2
|
|
|
37
37
|
Requires-Dist: tree-sitter==0.20.4
|
|
38
38
|
Requires-Dist: typer==0.15.1
|
|
39
39
|
Requires-Dist: uvicorn==0.34.0
|
|
40
|
+
Requires-Dist: watchdog>=6.0.0
|
|
40
41
|
Provides-Extra: dev
|
|
41
42
|
Requires-Dist: bandit==1.8.3; extra == 'dev'
|
|
42
43
|
Requires-Dist: mypy==1.15.0; extra == 'dev'
|
|
@@ -339,8 +340,8 @@ Initialize the local SQLite database index:
|
|
|
339
340
|
synap init .
|
|
340
341
|
```
|
|
341
342
|
|
|
342
|
-
### 3.
|
|
343
|
-
|
|
343
|
+
### 3. Connect IDE / Start MCP
|
|
344
|
+
Launch the unified background daemon and MCP stdio server:
|
|
344
345
|
```bash
|
|
345
346
|
synap start .
|
|
346
347
|
```
|
|
@@ -372,7 +373,6 @@ All commands support target path arguments:
|
|
|
372
373
|
* `synap doctor [PATH]` — Verifies database integrity and parsers.
|
|
373
374
|
* `synap run [PATH]` — Runs daemon in foreground.
|
|
374
375
|
* `synap ui [PATH]` — Launches HTML dashboard.
|
|
375
|
-
* `synap mcp start [PATH]` — Hosts MCP stdio server.
|
|
376
376
|
* `synap mcp config [PATH]` — Outputs Cursor connection blocks.
|
|
377
377
|
* `synap mcp verify [PATH]` — Asserts MCP server stability.
|
|
378
378
|
* `synap memory status [PATH]` — Lists lesson counts.
|
|
@@ -432,8 +432,7 @@ synap mcp config .
|
|
|
432
432
|
"mcpServers": {
|
|
433
433
|
"synap": {
|
|
434
434
|
"command": "/usr/local/bin/python",
|
|
435
|
-
"args": ["-m", "synap_git.cli", "
|
|
436
|
-
"autoConnect": true
|
|
435
|
+
"args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
|
|
437
436
|
}
|
|
438
437
|
}
|
|
439
438
|
}
|
|
@@ -289,8 +289,8 @@ Initialize the local SQLite database index:
|
|
|
289
289
|
synap init .
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
-
### 3.
|
|
293
|
-
|
|
292
|
+
### 3. Connect IDE / Start MCP
|
|
293
|
+
Launch the unified background daemon and MCP stdio server:
|
|
294
294
|
```bash
|
|
295
295
|
synap start .
|
|
296
296
|
```
|
|
@@ -322,7 +322,6 @@ All commands support target path arguments:
|
|
|
322
322
|
* `synap doctor [PATH]` — Verifies database integrity and parsers.
|
|
323
323
|
* `synap run [PATH]` — Runs daemon in foreground.
|
|
324
324
|
* `synap ui [PATH]` — Launches HTML dashboard.
|
|
325
|
-
* `synap mcp start [PATH]` — Hosts MCP stdio server.
|
|
326
325
|
* `synap mcp config [PATH]` — Outputs Cursor connection blocks.
|
|
327
326
|
* `synap mcp verify [PATH]` — Asserts MCP server stability.
|
|
328
327
|
* `synap memory status [PATH]` — Lists lesson counts.
|
|
@@ -382,8 +381,7 @@ synap mcp config .
|
|
|
382
381
|
"mcpServers": {
|
|
383
382
|
"synap": {
|
|
384
383
|
"command": "/usr/local/bin/python",
|
|
385
|
-
"args": ["-m", "synap_git.cli", "
|
|
386
|
-
"autoConnect": true
|
|
384
|
+
"args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
|
|
387
385
|
}
|
|
388
386
|
}
|
|
389
387
|
}
|
|
@@ -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", error=str(e))
|
|
108
108
|
|
|
109
109
|
wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
|
|
110
110
|
exists = await asyncio.to_thread(wiki_path.exists)
|
|
@@ -57,7 +57,7 @@ def _custom_call(*args: Any, **kwargs: Any) -> Any:
|
|
|
57
57
|
app.__call__ = _custom_call # type: ignore[method-assign]
|
|
58
58
|
|
|
59
59
|
JSON_OPTION = typer.Option("--json", help="Emit machine-readable JSON.")
|
|
60
|
-
console = Console()
|
|
60
|
+
console = Console(stderr=True)
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def _settings(
|
|
@@ -88,9 +88,19 @@ def _settings(
|
|
|
88
88
|
return settings
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def _verify_is_git(path: str) -> None:
|
|
92
|
+
from synap_git.git.state import GitRepository
|
|
93
|
+
|
|
94
|
+
if not GitRepository(Path(path)).state().is_repository:
|
|
95
|
+
console.print(f"[bold red]Error:[/bold red] '{path}' is not a valid Git repository.")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
|
|
98
|
+
|
|
91
99
|
def _emit(value: Any, *, json_output: bool) -> None:
|
|
92
100
|
if json_output:
|
|
93
|
-
|
|
101
|
+
import sys
|
|
102
|
+
|
|
103
|
+
print(json.dumps(_jsonable(value), indent=2, sort_keys=True), file=sys.stdout)
|
|
94
104
|
return
|
|
95
105
|
if isinstance(value, str):
|
|
96
106
|
console.print(value)
|
|
@@ -529,6 +539,7 @@ def init(
|
|
|
529
539
|
synap init
|
|
530
540
|
synap init --force --skip-llm
|
|
531
541
|
"""
|
|
542
|
+
_verify_is_git(path)
|
|
532
543
|
settings = _settings(path, json_output=json_output)
|
|
533
544
|
if skip_llm:
|
|
534
545
|
settings.llm_provider = None
|
|
@@ -537,12 +548,9 @@ def init(
|
|
|
537
548
|
|
|
538
549
|
runtime = SynapRuntime(settings)
|
|
539
550
|
if not quiet and not json_output:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
commit = runtime.bootstrap(force=force)
|
|
544
|
-
else:
|
|
545
|
-
commit = runtime.bootstrap(force=force)
|
|
551
|
+
console.print("[yellow]Initializing Synap runtime and scanning repository...[/yellow]")
|
|
552
|
+
|
|
553
|
+
commit = runtime.bootstrap(force=force)
|
|
546
554
|
|
|
547
555
|
if json_output:
|
|
548
556
|
_emit({"active_commit": commit, "state": "initialized"}, json_output=True)
|
|
@@ -644,16 +652,27 @@ def _detect_install_method() -> str:
|
|
|
644
652
|
@app.command()
|
|
645
653
|
def start(
|
|
646
654
|
path: Annotated[str, typer.Argument(help="Repository path to watch.")] = ".",
|
|
655
|
+
foreground: Annotated[
|
|
656
|
+
bool,
|
|
657
|
+
typer.Option(
|
|
658
|
+
"--foreground", help="Run FastMCP stdio server in foreground after starting daemon."
|
|
659
|
+
),
|
|
660
|
+
] = False,
|
|
647
661
|
) -> None:
|
|
648
662
|
"""Start the Synap daemon in the background."""
|
|
663
|
+
_verify_is_git(path)
|
|
664
|
+
import asyncio
|
|
665
|
+
import os
|
|
649
666
|
import subprocess
|
|
650
667
|
import sys
|
|
651
668
|
import time
|
|
652
669
|
|
|
670
|
+
from synap_git.mcp.server import SynapMCPServer
|
|
671
|
+
|
|
653
672
|
abs_path = Path(path).resolve()
|
|
654
673
|
pid_file = abs_path / ".synap" / "daemon.pid"
|
|
655
674
|
|
|
656
|
-
|
|
675
|
+
is_running = False
|
|
657
676
|
if pid_file.exists():
|
|
658
677
|
try:
|
|
659
678
|
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
@@ -662,59 +681,83 @@ def start(
|
|
|
662
681
|
hb = _read_daemon_heartbeat(abs_path)
|
|
663
682
|
if hb and "port" in hb:
|
|
664
683
|
console.print(f"[green]✓ UI available at http://127.0.0.1:{hb['port']}[/green]")
|
|
665
|
-
|
|
684
|
+
is_running = True
|
|
685
|
+
if not foreground:
|
|
686
|
+
return
|
|
666
687
|
else:
|
|
667
688
|
pid_file.unlink()
|
|
668
689
|
except Exception as e:
|
|
669
690
|
import structlog
|
|
670
691
|
|
|
671
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
692
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
672
693
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
694
|
+
if not is_running:
|
|
695
|
+
# 2. Spawn detached daemon process
|
|
696
|
+
cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
|
|
697
|
+
try:
|
|
698
|
+
if sys.platform == "win32":
|
|
699
|
+
subprocess.Popen(
|
|
700
|
+
cmd,
|
|
701
|
+
stdout=subprocess.DEVNULL,
|
|
702
|
+
stderr=subprocess.DEVNULL,
|
|
703
|
+
stdin=subprocess.DEVNULL,
|
|
704
|
+
creationflags=0x00000008, # DETACHED_PROCESS
|
|
705
|
+
)
|
|
706
|
+
else:
|
|
707
|
+
subprocess.Popen(
|
|
708
|
+
cmd,
|
|
709
|
+
stdout=subprocess.DEVNULL,
|
|
710
|
+
stderr=subprocess.DEVNULL,
|
|
711
|
+
stdin=subprocess.DEVNULL,
|
|
712
|
+
start_new_session=True,
|
|
713
|
+
)
|
|
714
|
+
except Exception as e:
|
|
715
|
+
console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
|
|
716
|
+
raise typer.Exit(1)
|
|
717
|
+
|
|
718
|
+
success = False
|
|
719
|
+
with console.status(
|
|
720
|
+
"[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
|
|
721
|
+
):
|
|
722
|
+
for _ in range(25): # wait up to 5 seconds for initial heartbeat
|
|
723
|
+
time.sleep(0.2)
|
|
724
|
+
if pid_file.exists():
|
|
725
|
+
hb = _read_daemon_heartbeat(abs_path)
|
|
726
|
+
if hb:
|
|
727
|
+
status = hb.get("status")
|
|
728
|
+
if status in ("healthy", "degraded", "starting"):
|
|
729
|
+
success = True
|
|
730
|
+
port = hb.get("port", 9876)
|
|
731
|
+
pid = int(hb.get("pid", 0))
|
|
732
|
+
console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
|
|
733
|
+
if status == "starting":
|
|
734
|
+
console.print(
|
|
735
|
+
"[yellow]ℹ Daemon is currently indexing in the background...[/yellow]"
|
|
736
|
+
)
|
|
737
|
+
else:
|
|
738
|
+
console.print("[green]✓ Runtime healthy[/green]")
|
|
739
|
+
console.print(
|
|
740
|
+
f"[green]✓ UI available at http://127.0.0.1:{port}[/green]"
|
|
741
|
+
)
|
|
742
|
+
break
|
|
743
|
+
|
|
744
|
+
if not success:
|
|
745
|
+
console.print(
|
|
746
|
+
"[red]✗ Daemon started but did not report healthy status. Check logs in ~/.config/synap/logs/[/red]"
|
|
691
747
|
)
|
|
692
|
-
|
|
693
|
-
console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
|
|
694
|
-
raise typer.Exit(1)
|
|
748
|
+
raise typer.Exit(1)
|
|
695
749
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
"[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
|
|
699
|
-
):
|
|
700
|
-
for _ in range(15): # wait up to 3 seconds
|
|
701
|
-
time.sleep(0.2)
|
|
702
|
-
if pid_file.exists():
|
|
703
|
-
hb = _read_daemon_heartbeat(abs_path)
|
|
704
|
-
if hb and hb.get("status") in ("healthy", "degraded"):
|
|
705
|
-
success = True
|
|
706
|
-
port = hb.get("port", 9876)
|
|
707
|
-
pid = int(hb.get("pid", 0))
|
|
708
|
-
console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
|
|
709
|
-
console.print("[green]✓ Runtime healthy[/green]")
|
|
710
|
-
console.print(f"[green]✓ UI available at http://127.0.0.1:{port}[/green]")
|
|
711
|
-
break
|
|
750
|
+
if not foreground:
|
|
751
|
+
return
|
|
712
752
|
|
|
713
|
-
if
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
753
|
+
# 3. Start MCP Server in the foreground if --foreground flag passed
|
|
754
|
+
settings = _settings(path, json_output=True)
|
|
755
|
+
runtime = SynapRuntime(settings)
|
|
756
|
+
server = SynapMCPServer(runtime)
|
|
757
|
+
try:
|
|
758
|
+
asyncio.run(server.run())
|
|
759
|
+
except KeyboardInterrupt:
|
|
760
|
+
os._exit(0)
|
|
718
761
|
|
|
719
762
|
|
|
720
763
|
@app.command("daemon-run", hidden=True)
|
|
@@ -820,7 +863,7 @@ def stop(
|
|
|
820
863
|
except Exception as e:
|
|
821
864
|
import structlog
|
|
822
865
|
|
|
823
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
866
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
824
867
|
success = not _is_process_running(pid)
|
|
825
868
|
|
|
826
869
|
# Clean up lockfiles
|
|
@@ -1271,7 +1314,7 @@ def rollback(
|
|
|
1271
1314
|
except Exception as e:
|
|
1272
1315
|
import structlog
|
|
1273
1316
|
|
|
1274
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
1317
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
1275
1318
|
|
|
1276
1319
|
with console.status(
|
|
1277
1320
|
f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
|
|
@@ -1346,7 +1389,7 @@ def repair(
|
|
|
1346
1389
|
except Exception as e:
|
|
1347
1390
|
import structlog
|
|
1348
1391
|
|
|
1349
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
1392
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
1350
1393
|
|
|
1351
1394
|
status_info = runtime.status()
|
|
1352
1395
|
console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
|
|
@@ -1464,34 +1507,52 @@ def doctor(
|
|
|
1464
1507
|
task_prov, completed=1, description=f"[red]✗ Unexpected error: {e}[/red]"
|
|
1465
1508
|
)
|
|
1466
1509
|
|
|
1467
|
-
task_daemon = progress.add_task("Checking Daemon Status...", total=1)
|
|
1510
|
+
task_daemon = progress.add_task("Checking Daemon & REST API Status...", total=1)
|
|
1468
1511
|
try:
|
|
1512
|
+
import socket
|
|
1513
|
+
|
|
1514
|
+
import httpx
|
|
1515
|
+
|
|
1469
1516
|
daemon_info = _read_daemon_heartbeat(settings.repository_path)
|
|
1470
|
-
if daemon_info
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1517
|
+
port = daemon_info.get("port", 9876) if daemon_info else 9876
|
|
1518
|
+
|
|
1519
|
+
# Test port availability
|
|
1520
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
1521
|
+
s.settimeout(0.5)
|
|
1522
|
+
port_in_use = s.connect_ex(("127.0.0.1", port)) == 0
|
|
1523
|
+
|
|
1524
|
+
if daemon_info and port_in_use:
|
|
1525
|
+
try:
|
|
1526
|
+
r = httpx.get(f"http://127.0.0.1:{port}/api/status", timeout=2.0)
|
|
1527
|
+
if r.status_code == 200:
|
|
1528
|
+
progress.update(
|
|
1529
|
+
task_daemon,
|
|
1530
|
+
completed=1,
|
|
1531
|
+
description=f"[green]✓ Daemon active & REST API healthy (PID {daemon_info['pid']}, port {port})[/green]",
|
|
1532
|
+
)
|
|
1533
|
+
else:
|
|
1534
|
+
progress.update(
|
|
1535
|
+
task_daemon,
|
|
1536
|
+
completed=1,
|
|
1537
|
+
description=f"[yellow]⚠ Daemon running on port {port} but REST API returned HTTP {r.status_code}[/yellow]",
|
|
1538
|
+
)
|
|
1539
|
+
except Exception as ex:
|
|
1485
1540
|
progress.update(
|
|
1486
1541
|
task_daemon,
|
|
1487
1542
|
completed=1,
|
|
1488
|
-
description=f"[
|
|
1543
|
+
description=f"[yellow]⚠ Daemon running on port {port} but API check failed: {ex}[/yellow]",
|
|
1489
1544
|
)
|
|
1545
|
+
elif daemon_info:
|
|
1546
|
+
progress.update(
|
|
1547
|
+
task_daemon,
|
|
1548
|
+
completed=1,
|
|
1549
|
+
description=f"[yellow]⚠ Heartbeat found for PID {daemon_info['pid']} but port {port} is not listening[/yellow]",
|
|
1550
|
+
)
|
|
1490
1551
|
else:
|
|
1491
1552
|
progress.update(
|
|
1492
1553
|
task_daemon,
|
|
1493
1554
|
completed=1,
|
|
1494
|
-
description="[yellow]⚠ Daemon inactive (
|
|
1555
|
+
description=f"[yellow]⚠ Daemon inactive (port {port} available). Start using `synap start`[/yellow]",
|
|
1495
1556
|
)
|
|
1496
1557
|
except Exception as e:
|
|
1497
1558
|
progress.update(
|
|
@@ -1509,21 +1570,6 @@ def run(
|
|
|
1509
1570
|
start(path)
|
|
1510
1571
|
|
|
1511
1572
|
|
|
1512
|
-
@mcp_app.command("start")
|
|
1513
|
-
def mcp_start(
|
|
1514
|
-
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
1515
|
-
) -> None:
|
|
1516
|
-
"""Start the MCP server."""
|
|
1517
|
-
settings = _settings(path, json_output=True)
|
|
1518
|
-
runtime = SynapRuntime(settings)
|
|
1519
|
-
runtime.bootstrap()
|
|
1520
|
-
|
|
1521
|
-
from synap_git.mcp.server import SynapMCPServer
|
|
1522
|
-
|
|
1523
|
-
server = SynapMCPServer(runtime)
|
|
1524
|
-
asyncio.run(server.run())
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
1573
|
@mcp_app.command("config")
|
|
1528
1574
|
def mcp_config(
|
|
1529
1575
|
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
@@ -1534,11 +1580,7 @@ def mcp_config(
|
|
|
1534
1580
|
|
|
1535
1581
|
config = {
|
|
1536
1582
|
"mcpServers": {
|
|
1537
|
-
"synap": {
|
|
1538
|
-
"command": sys.executable,
|
|
1539
|
-
"args": ["-m", "synap_git.cli", "mcp", "start", abs_path],
|
|
1540
|
-
"autoConnect": True,
|
|
1541
|
-
}
|
|
1583
|
+
"synap": {"command": sys.executable, "args": ["-m", "synap_git.cli", "start", abs_path]}
|
|
1542
1584
|
}
|
|
1543
1585
|
}
|
|
1544
1586
|
typer.echo(json.dumps(config, indent=2))
|
|
@@ -2154,3 +2196,16 @@ def wiki_show(
|
|
|
2154
2196
|
|
|
2155
2197
|
content = wiki_path.read_text(encoding="utf-8")
|
|
2156
2198
|
console.print(Markdown(content))
|
|
2199
|
+
|
|
2200
|
+
|
|
2201
|
+
@wiki_app.command("retry")
|
|
2202
|
+
def wiki_retry(
|
|
2203
|
+
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
2204
|
+
) -> None:
|
|
2205
|
+
"""Requeue all permanently failed wiki tasks back to pending state."""
|
|
2206
|
+
runtime = SynapRuntime(_settings(path))
|
|
2207
|
+
count = runtime.store.retry_failed_wiki_queue()
|
|
2208
|
+
if count > 0:
|
|
2209
|
+
console.print(f"[green]✓ Requeued {count} failed wiki tasks to pending state.[/green]")
|
|
2210
|
+
else:
|
|
2211
|
+
console.print("[yellow]No failed wiki tasks found in queue.[/yellow]")
|
|
@@ -50,7 +50,6 @@ class SynapSettings(BaseSettings):
|
|
|
50
50
|
repository_path: Path = Field(default=Path(), description="Repository root.")
|
|
51
51
|
state_path: Path = Field(default=Path(".synap"), description="Local runtime state.")
|
|
52
52
|
sqlite_path: Path | None = None
|
|
53
|
-
object_path: Path | None = None
|
|
54
53
|
log_path: Path | None = None
|
|
55
54
|
|
|
56
55
|
logging_mode: LoggingMode = LoggingMode.HUMAN
|
|
@@ -150,7 +149,6 @@ class SynapSettings(BaseSettings):
|
|
|
150
149
|
self.repository_path = repository_path
|
|
151
150
|
self.state_path = state_path.resolve()
|
|
152
151
|
self.sqlite_path = self._resolve_optional_path(self.sqlite_path, "synap.db")
|
|
153
|
-
self.object_path = self._resolve_optional_path(self.object_path, "objects")
|
|
154
152
|
self.log_path = self._resolve_optional_path(self.log_path, "logs")
|
|
155
153
|
|
|
156
154
|
if self.profile is RuntimeProfile.TEST:
|
|
@@ -166,10 +164,6 @@ class SynapSettings(BaseSettings):
|
|
|
166
164
|
|
|
167
165
|
def ensure_directories(self) -> None:
|
|
168
166
|
self.state_path.mkdir(parents=True, exist_ok=True)
|
|
169
|
-
if self.object_path:
|
|
170
|
-
self.object_path.mkdir(parents=True, exist_ok=True)
|
|
171
|
-
if self.log_path:
|
|
172
|
-
self.log_path.mkdir(parents=True, exist_ok=True)
|
|
173
167
|
|
|
174
168
|
def validate_configuration(self) -> list[str]:
|
|
175
169
|
errors = []
|
|
@@ -56,6 +56,10 @@ def configure_logging(settings: SynapSettings) -> None:
|
|
|
56
56
|
# Set up stream handler (stderr)
|
|
57
57
|
stream_handler = logging.StreamHandler(sys.stderr)
|
|
58
58
|
stream_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
59
|
+
if settings.log_level.upper() == "DEBUG":
|
|
60
|
+
stream_handler.setLevel(logging.DEBUG)
|
|
61
|
+
else:
|
|
62
|
+
stream_handler.setLevel(logging.WARNING)
|
|
59
63
|
root_logger.addHandler(stream_handler)
|
|
60
64
|
|
|
61
65
|
root_logger.setLevel(getattr(logging, settings.log_level.upper(), logging.INFO))
|
|
@@ -131,7 +131,7 @@ class GitRepository:
|
|
|
131
131
|
except Exception as e:
|
|
132
132
|
import structlog
|
|
133
133
|
|
|
134
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
134
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
135
135
|
|
|
136
136
|
if len(current.commit_parent_hashes) > 1:
|
|
137
137
|
return GitChange(kind=GitChangeKind.MERGE, previous=previous, current=current)
|