synap-git 2.1.6__tar.gz → 2.1.7__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.1.7}/.gitignore +4 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/CHANGELOG.md +8 -1
- {synap_git-2.1.6 → synap_git-2.1.7}/PKG-INFO +4 -6
- {synap_git-2.1.6 → synap_git-2.1.7}/README.md +3 -5
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/__init__.py +1 -1
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/app.py +1 -1
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/main.py +96 -77
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/config.py +0 -6
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/logger.py +4 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/git/state.py +1 -1
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/daemon.py +7 -64
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/engine.py +88 -59
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/scanner.py +2 -2
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/wiki.py +4 -4
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/mcp/server.py +2 -2
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/parser/registry.py +1 -1
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/factory.py +4 -3
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/engine.py +8 -6
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/storage/sqlite.py +24 -1
- synap_git-2.1.7/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.1.7}/.synap.example/README.md +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/LICENSE.md +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/pyproject.toml +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/api/static/index.html +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/__main__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/checkpoint.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/lessons.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/memory.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/usage.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/cli/wiki.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/diagnostics/tracing.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/embeddings/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/git/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/indexer/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/mcp/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/parser/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/anthropic.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/base.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/gemini.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/ollama.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/openai.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/provider/openrouter.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/py.typed +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/retrieval/memory.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/redactor.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/sanitizer.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/security/validator.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/storage/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/utils/__init__.py +0 -0
- {synap_git-2.1.6 → synap_git-2.1.7}/src/synap_git/utils/serialization.py +0 -0
|
@@ -5,7 +5,14 @@ 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.
|
|
8
|
+
## [2.1.7] - 2026-07-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **AI Engineer Rules**: Created standardized agent rulesets across `AGENTS.md`, `.cursorrules`, `.windsurfrules`, and `.github/copilot-instructions.md`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Daemon Branch Switching Test Stability**: Resolved timing race condition in daemon branch checkout verification.
|
|
15
|
+
- **CLI Setup Connectivity**: Fixed setup connectivity verification and non-TTY prompt fallback handling.
|
|
9
16
|
|
|
10
17
|
## [2.1.6] - 2026-06-29
|
|
11
18
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: synap-git
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.7
|
|
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
|
|
@@ -339,8 +339,8 @@ Initialize the local SQLite database index:
|
|
|
339
339
|
synap init .
|
|
340
340
|
```
|
|
341
341
|
|
|
342
|
-
### 3.
|
|
343
|
-
|
|
342
|
+
### 3. Connect IDE / Start MCP
|
|
343
|
+
Launch the unified background daemon and MCP stdio server:
|
|
344
344
|
```bash
|
|
345
345
|
synap start .
|
|
346
346
|
```
|
|
@@ -372,7 +372,6 @@ All commands support target path arguments:
|
|
|
372
372
|
* `synap doctor [PATH]` — Verifies database integrity and parsers.
|
|
373
373
|
* `synap run [PATH]` — Runs daemon in foreground.
|
|
374
374
|
* `synap ui [PATH]` — Launches HTML dashboard.
|
|
375
|
-
* `synap mcp start [PATH]` — Hosts MCP stdio server.
|
|
376
375
|
* `synap mcp config [PATH]` — Outputs Cursor connection blocks.
|
|
377
376
|
* `synap mcp verify [PATH]` — Asserts MCP server stability.
|
|
378
377
|
* `synap memory status [PATH]` — Lists lesson counts.
|
|
@@ -432,8 +431,7 @@ synap mcp config .
|
|
|
432
431
|
"mcpServers": {
|
|
433
432
|
"synap": {
|
|
434
433
|
"command": "/usr/local/bin/python",
|
|
435
|
-
"args": ["-m", "synap_git.cli", "
|
|
436
|
-
"autoConnect": true
|
|
434
|
+
"args": ["-m", "synap_git.cli", "start", "/Users/username/repo"]
|
|
437
435
|
}
|
|
438
436
|
}
|
|
439
437
|
}
|
|
@@ -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)
|
|
@@ -646,14 +654,19 @@ def start(
|
|
|
646
654
|
path: Annotated[str, typer.Argument(help="Repository path to watch.")] = ".",
|
|
647
655
|
) -> None:
|
|
648
656
|
"""Start the Synap daemon in the background."""
|
|
657
|
+
_verify_is_git(path)
|
|
658
|
+
import asyncio
|
|
659
|
+
import os
|
|
649
660
|
import subprocess
|
|
650
661
|
import sys
|
|
651
662
|
import time
|
|
652
663
|
|
|
664
|
+
from synap_git.mcp.server import SynapMCPServer
|
|
665
|
+
|
|
653
666
|
abs_path = Path(path).resolve()
|
|
654
667
|
pid_file = abs_path / ".synap" / "daemon.pid"
|
|
655
668
|
|
|
656
|
-
|
|
669
|
+
is_running = False
|
|
657
670
|
if pid_file.exists():
|
|
658
671
|
try:
|
|
659
672
|
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
@@ -662,59 +675,78 @@ def start(
|
|
|
662
675
|
hb = _read_daemon_heartbeat(abs_path)
|
|
663
676
|
if hb and "port" in hb:
|
|
664
677
|
console.print(f"[green]✓ UI available at http://127.0.0.1:{hb['port']}[/green]")
|
|
665
|
-
|
|
678
|
+
is_running = True
|
|
666
679
|
else:
|
|
667
680
|
pid_file.unlink()
|
|
668
681
|
except Exception as e:
|
|
669
682
|
import structlog
|
|
670
683
|
|
|
671
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
684
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
672
685
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
686
|
+
if not is_running:
|
|
687
|
+
# 2. Spawn detached daemon process
|
|
688
|
+
cmd = [sys.executable, "-m", "synap_git.cli", "daemon-run", abs_path.as_posix()]
|
|
689
|
+
try:
|
|
690
|
+
if sys.platform == "win32":
|
|
691
|
+
subprocess.Popen(
|
|
692
|
+
cmd,
|
|
693
|
+
stdout=subprocess.DEVNULL,
|
|
694
|
+
stderr=subprocess.DEVNULL,
|
|
695
|
+
stdin=subprocess.DEVNULL,
|
|
696
|
+
creationflags=0x00000008, # DETACHED_PROCESS
|
|
697
|
+
)
|
|
698
|
+
else:
|
|
699
|
+
subprocess.Popen(
|
|
700
|
+
cmd,
|
|
701
|
+
stdout=subprocess.DEVNULL,
|
|
702
|
+
stderr=subprocess.DEVNULL,
|
|
703
|
+
stdin=subprocess.DEVNULL,
|
|
704
|
+
start_new_session=True,
|
|
705
|
+
)
|
|
706
|
+
except Exception as e:
|
|
707
|
+
console.print(f"[bold red]✗ Failed to start daemon:[/bold red] {e}")
|
|
708
|
+
raise typer.Exit(1)
|
|
695
709
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
success = False
|
|
711
|
+
with console.status(
|
|
712
|
+
"[yellow]Starting Synap daemon and awaiting health check...[/yellow]", spinner="dots"
|
|
713
|
+
):
|
|
714
|
+
for _ in range(25): # wait up to 5 seconds for initial heartbeat
|
|
715
|
+
time.sleep(0.2)
|
|
716
|
+
if pid_file.exists():
|
|
717
|
+
hb = _read_daemon_heartbeat(abs_path)
|
|
718
|
+
if hb:
|
|
719
|
+
status = hb.get("status")
|
|
720
|
+
if status in ("healthy", "degraded", "starting"):
|
|
721
|
+
success = True
|
|
722
|
+
port = hb.get("port", 9876)
|
|
723
|
+
pid = int(hb.get("pid", 0))
|
|
724
|
+
console.print(f"[green]✓ Synap daemon started (PID {pid})[/green]")
|
|
725
|
+
if status == "starting":
|
|
726
|
+
console.print(
|
|
727
|
+
"[yellow]ℹ Daemon is currently indexing in the background...[/yellow]"
|
|
728
|
+
)
|
|
729
|
+
else:
|
|
730
|
+
console.print("[green]✓ Runtime healthy[/green]")
|
|
731
|
+
console.print(
|
|
732
|
+
f"[green]✓ UI available at http://127.0.0.1:{port}[/green]"
|
|
733
|
+
)
|
|
734
|
+
break
|
|
712
735
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
736
|
+
if not success:
|
|
737
|
+
console.print(
|
|
738
|
+
"[red]✗ Daemon started but did not report healthy status. Check logs in ~/.config/synap/logs/[/red]"
|
|
739
|
+
)
|
|
740
|
+
raise typer.Exit(1)
|
|
741
|
+
|
|
742
|
+
# 3. Start MCP Server in the foreground
|
|
743
|
+
settings = _settings(path, json_output=True)
|
|
744
|
+
runtime = SynapRuntime(settings)
|
|
745
|
+
server = SynapMCPServer(runtime)
|
|
746
|
+
try:
|
|
747
|
+
asyncio.run(server.run())
|
|
748
|
+
except KeyboardInterrupt:
|
|
749
|
+
os._exit(0)
|
|
718
750
|
|
|
719
751
|
|
|
720
752
|
@app.command("daemon-run", hidden=True)
|
|
@@ -820,7 +852,7 @@ def stop(
|
|
|
820
852
|
except Exception as e:
|
|
821
853
|
import structlog
|
|
822
854
|
|
|
823
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
855
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
824
856
|
success = not _is_process_running(pid)
|
|
825
857
|
|
|
826
858
|
# Clean up lockfiles
|
|
@@ -1271,7 +1303,7 @@ def rollback(
|
|
|
1271
1303
|
except Exception as e:
|
|
1272
1304
|
import structlog
|
|
1273
1305
|
|
|
1274
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
1306
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
1275
1307
|
|
|
1276
1308
|
with console.status(
|
|
1277
1309
|
f"[yellow]Reindexing repository to match {selected_commit}...[/yellow]", spinner="dots"
|
|
@@ -1346,7 +1378,7 @@ def repair(
|
|
|
1346
1378
|
except Exception as e:
|
|
1347
1379
|
import structlog
|
|
1348
1380
|
|
|
1349
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
1381
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
1350
1382
|
|
|
1351
1383
|
status_info = runtime.status()
|
|
1352
1384
|
console.print(f"\n[green]✓ Recovery complete. {status_info.files} files restored.[/green]")
|
|
@@ -1475,6 +1507,12 @@ def doctor(
|
|
|
1475
1507
|
completed=1,
|
|
1476
1508
|
description=f"[green]✓ Daemon active and healthy (PID {daemon_info['pid']}, uptime {daemon_info['uptime_seconds']}s)[/green]",
|
|
1477
1509
|
)
|
|
1510
|
+
elif status_str == "STARTING":
|
|
1511
|
+
progress.update(
|
|
1512
|
+
task_daemon,
|
|
1513
|
+
completed=1,
|
|
1514
|
+
description=f"[green]✓ Daemon is active and currently starting/indexing (PID {daemon_info['pid']})[/green]",
|
|
1515
|
+
)
|
|
1478
1516
|
elif status_str == "STALE":
|
|
1479
1517
|
progress.update(
|
|
1480
1518
|
task_daemon,
|
|
@@ -1509,21 +1547,6 @@ def run(
|
|
|
1509
1547
|
start(path)
|
|
1510
1548
|
|
|
1511
1549
|
|
|
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
1550
|
@mcp_app.command("config")
|
|
1528
1551
|
def mcp_config(
|
|
1529
1552
|
path: Annotated[str, typer.Argument(help="Repository path.")] = ".",
|
|
@@ -1534,11 +1557,7 @@ def mcp_config(
|
|
|
1534
1557
|
|
|
1535
1558
|
config = {
|
|
1536
1559
|
"mcpServers": {
|
|
1537
|
-
"synap": {
|
|
1538
|
-
"command": sys.executable,
|
|
1539
|
-
"args": ["-m", "synap_git.cli", "mcp", "start", abs_path],
|
|
1540
|
-
"autoConnect": True,
|
|
1541
|
-
}
|
|
1560
|
+
"synap": {"command": sys.executable, "args": ["-m", "synap_git.cli", "start", abs_path]}
|
|
1542
1561
|
}
|
|
1543
1562
|
}
|
|
1544
1563
|
typer.echo(json.dumps(config, indent=2))
|
|
@@ -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)
|
|
@@ -75,6 +75,7 @@ class RuntimeDaemon:
|
|
|
75
75
|
self._ui_server.capture_signals = dummy_capture_signals # type: ignore[method-assign]
|
|
76
76
|
|
|
77
77
|
# Initial bootstrap
|
|
78
|
+
self._write_heartbeat(status="starting")
|
|
78
79
|
await asyncio.to_thread(self.runtime.bootstrap)
|
|
79
80
|
|
|
80
81
|
with self.runtime.store.connect() as conn:
|
|
@@ -128,18 +129,6 @@ class RuntimeDaemon:
|
|
|
128
129
|
self.logger.info("daemon_stop_requested")
|
|
129
130
|
self._stop_event.set()
|
|
130
131
|
|
|
131
|
-
# Force immediate cleanup and exit to avoid thread-join blocking
|
|
132
|
-
if force_exit:
|
|
133
|
-
try:
|
|
134
|
-
self.runtime.shutdown()
|
|
135
|
-
self._delete_heartbeat()
|
|
136
|
-
except Exception:
|
|
137
|
-
pass
|
|
138
|
-
|
|
139
|
-
import sys
|
|
140
|
-
|
|
141
|
-
sys.exit(0)
|
|
142
|
-
|
|
143
132
|
def health(self) -> DaemonHealth:
|
|
144
133
|
status = self.runtime.status()
|
|
145
134
|
return DaemonHealth(
|
|
@@ -246,7 +235,7 @@ class RuntimeDaemon:
|
|
|
246
235
|
}
|
|
247
236
|
heartbeat_file.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
248
237
|
except Exception as e:
|
|
249
|
-
self.logger.error("daemon_write_heartbeat_error",
|
|
238
|
+
self.logger.error("daemon_write_heartbeat_error", error=str(e))
|
|
250
239
|
|
|
251
240
|
def _delete_heartbeat(self) -> None:
|
|
252
241
|
heartbeat_file = self.settings.repository_path / ".synap" / "daemon_heartbeat.json"
|
|
@@ -256,7 +245,7 @@ class RuntimeDaemon:
|
|
|
256
245
|
except Exception as e:
|
|
257
246
|
import structlog
|
|
258
247
|
|
|
259
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
248
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
260
249
|
|
|
261
250
|
# Cleanup process PID lockfile as well
|
|
262
251
|
pid_file = self.settings.repository_path / ".synap" / "daemon.pid"
|
|
@@ -266,7 +255,7 @@ class RuntimeDaemon:
|
|
|
266
255
|
except Exception as e:
|
|
267
256
|
import structlog
|
|
268
257
|
|
|
269
|
-
structlog.get_logger().error("suppressed_error_caught",
|
|
258
|
+
structlog.get_logger().error("suppressed_error_caught", error=str(e))
|
|
270
259
|
|
|
271
260
|
async def _poll_git_loop(self) -> None:
|
|
272
261
|
while not self._stop_event.is_set():
|
|
@@ -302,7 +291,7 @@ class RuntimeDaemon:
|
|
|
302
291
|
|
|
303
292
|
self._write_heartbeat(status="healthy")
|
|
304
293
|
except Exception as e:
|
|
305
|
-
self.logger.error("daemon_loop_error",
|
|
294
|
+
self.logger.error("daemon_loop_error", error=str(e))
|
|
306
295
|
self._recovery_attempts += 1
|
|
307
296
|
self._write_heartbeat(status="degraded", last_error=str(e))
|
|
308
297
|
|
|
@@ -334,11 +323,6 @@ class RuntimeDaemon:
|
|
|
334
323
|
|
|
335
324
|
async def _wiki_worker_loop(self) -> None:
|
|
336
325
|
self.logger.info("wiki_worker_started")
|
|
337
|
-
from rich.console import Console
|
|
338
|
-
|
|
339
|
-
from synap_git.config import LoggingMode
|
|
340
|
-
|
|
341
|
-
console = Console()
|
|
342
326
|
|
|
343
327
|
while not self._stop_event.is_set():
|
|
344
328
|
try:
|
|
@@ -364,39 +348,7 @@ class RuntimeDaemon:
|
|
|
364
348
|
|
|
365
349
|
start_time = time.time()
|
|
366
350
|
try:
|
|
367
|
-
|
|
368
|
-
with console.status(
|
|
369
|
-
f"[bold cyan][Wiki][/bold cyan] Generating: [white]{file_path}[/white] (0.0s) ({total_pending} remaining)"
|
|
370
|
-
) as status:
|
|
371
|
-
# Update status periodically with elapsed time
|
|
372
|
-
async def _update_status(
|
|
373
|
-
st: float = start_time,
|
|
374
|
-
fp: str = file_path,
|
|
375
|
-
tp: int = total_pending,
|
|
376
|
-
) -> None:
|
|
377
|
-
try:
|
|
378
|
-
while True:
|
|
379
|
-
await asyncio.sleep(0.1)
|
|
380
|
-
elapsed = time.time() - st
|
|
381
|
-
status.update(
|
|
382
|
-
f"[bold cyan][Wiki][/bold cyan] Generating: [white]{fp}[/white] [dim]({elapsed:.1f}s)[/dim] ({tp} remaining)"
|
|
383
|
-
)
|
|
384
|
-
except asyncio.CancelledError:
|
|
385
|
-
pass
|
|
386
|
-
|
|
387
|
-
status_task = asyncio.create_task(_update_status())
|
|
388
|
-
try:
|
|
389
|
-
await asyncio.to_thread(
|
|
390
|
-
self.runtime.wiki.ensure_wiki_page, file_path
|
|
391
|
-
)
|
|
392
|
-
finally:
|
|
393
|
-
status_task.cancel()
|
|
394
|
-
try:
|
|
395
|
-
await status_task
|
|
396
|
-
except asyncio.CancelledError:
|
|
397
|
-
pass
|
|
398
|
-
else:
|
|
399
|
-
await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
|
|
351
|
+
await asyncio.to_thread(self.runtime.wiki.ensure_wiki_page, file_path)
|
|
400
352
|
|
|
401
353
|
await asyncio.to_thread(
|
|
402
354
|
self.runtime.store.update_wiki_queue_status,
|
|
@@ -408,11 +360,6 @@ class RuntimeDaemon:
|
|
|
408
360
|
self.logger.info(
|
|
409
361
|
"wiki_worker_completed_task", path=file_path, elapsed=elapsed
|
|
410
362
|
)
|
|
411
|
-
|
|
412
|
-
if self.settings.logging_mode == LoggingMode.HUMAN:
|
|
413
|
-
print(
|
|
414
|
-
f"[bold green]✓[/bold green] Wiki generated: [white]{file_path}[/white] [dim]({elapsed:.1f}s)[/dim]"
|
|
415
|
-
)
|
|
416
363
|
except Exception as ex:
|
|
417
364
|
self.logger.error("wiki_worker_task_failed", path=file_path, error=str(ex))
|
|
418
365
|
if attempts + 1 >= 3:
|
|
@@ -422,10 +369,6 @@ class RuntimeDaemon:
|
|
|
422
369
|
"failed",
|
|
423
370
|
attempts + 1,
|
|
424
371
|
)
|
|
425
|
-
if self.settings.logging_mode == LoggingMode.HUMAN:
|
|
426
|
-
print(
|
|
427
|
-
f"[bold red]✗[/bold red] Wiki failed: [white]{file_path}[/white] [dim]({str(ex)})[/dim]"
|
|
428
|
-
)
|
|
429
372
|
else:
|
|
430
373
|
await asyncio.to_thread(
|
|
431
374
|
self.runtime.store.update_wiki_queue_status,
|
|
@@ -438,6 +381,6 @@ class RuntimeDaemon:
|
|
|
438
381
|
except asyncio.CancelledError:
|
|
439
382
|
break
|
|
440
383
|
except Exception as e:
|
|
441
|
-
self.logger.error("wiki_worker_loop_error",
|
|
384
|
+
self.logger.error("wiki_worker_loop_error", error=str(e))
|
|
442
385
|
await asyncio.sleep(5.0)
|
|
443
386
|
self.logger.info("wiki_worker_stopped")
|