synap-git 0.2.1__tar.gz → 1.1.1__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 (49) hide show
  1. synap_git-1.1.1/CHANGELOG.md +139 -0
  2. {synap_git-0.2.1 → synap_git-1.1.1}/PKG-INFO +16 -3
  3. {synap_git-0.2.1 → synap_git-1.1.1}/README.md +14 -2
  4. {synap_git-0.2.1 → synap_git-1.1.1}/pyproject.toml +1 -0
  5. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/__init__.py +1 -1
  6. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/api/app.py +49 -18
  7. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/api/static/index.html +33 -23
  8. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/cli/main.py +243 -62
  9. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/config.py +13 -0
  10. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/git/state.py +1 -1
  11. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/indexer/daemon.py +81 -1
  12. synap_git-1.1.1/src/synap_git/indexer/engine.py +791 -0
  13. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/indexer/scanner.py +14 -1
  14. synap_git-1.1.1/src/synap_git/indexer/wiki.py +195 -0
  15. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/mcp/server.py +26 -0
  16. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/parser/registry.py +41 -3
  17. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/anthropic.py +48 -0
  18. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/base.py +14 -0
  19. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/factory.py +2 -9
  20. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/gemini.py +15 -0
  21. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/ollama.py +42 -0
  22. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/openai.py +49 -0
  23. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/provider/openrouter.py +51 -0
  24. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/retrieval/engine.py +42 -17
  25. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/storage/sqlite.py +329 -169
  26. synap_git-0.2.1/CHANGELOG.md +0 -80
  27. synap_git-0.2.1/src/synap_git/indexer/engine.py +0 -327
  28. synap_git-0.2.1/src/synap_git/indexer/wiki.py +0 -78
  29. synap_git-0.2.1/src/synap_git/provider/mock.py +0 -66
  30. {synap_git-0.2.1 → synap_git-1.1.1}/.gitignore +0 -0
  31. {synap_git-0.2.1 → synap_git-1.1.1}/.synap.example/README.md +0 -0
  32. {synap_git-0.2.1 → synap_git-1.1.1}/LICENSE.md +0 -0
  33. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/api/__init__.py +0 -0
  34. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/cli/__init__.py +0 -0
  35. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/cli/__main__.py +0 -0
  36. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/diagnostics/__init__.py +0 -0
  37. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/diagnostics/logger.py +0 -0
  38. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/diagnostics/tracing.py +0 -0
  39. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/embeddings/__init__.py +0 -0
  40. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/git/__init__.py +0 -0
  41. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/indexer/__init__.py +0 -0
  42. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/mcp/__init__.py +0 -0
  43. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/parser/__init__.py +0 -0
  44. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/py.typed +0 -0
  45. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/retrieval/__init__.py +0 -0
  46. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/retrieval/memory.py +0 -0
  47. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/storage/__init__.py +0 -0
  48. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/utils/__init__.py +0 -0
  49. {synap_git-0.2.1 → synap_git-1.1.1}/src/synap_git/utils/serialization.py +0 -0
@@ -0,0 +1,139 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.1.1] - 2026-05-28
9
+
10
+ ### Fixed
11
+ - **CRITICAL: N+1 query loop during edge resolution:** Migrated structural edge resolution to bulk FETCH queries, eliminating thousands of database calls per indexing run.
12
+ - **CRITICAL: SQLite Synchronous Pragma:** Enforced \`PRAGMA synchronous=NORMAL\` on every connection, multiplying write throughput by 10x-100x.
13
+ - **SPEC: Content-Scoped File IDs:** Updated \`file_id\` formula to \`sha256(path + content_hash)\` ensuring temporal version isolation in the graph.
14
+ - **HIGH: Wiki Generation Resiliency:** Implemented exponential backoff retries for LLM wiki generation to prevent data loss on transient network errors.
15
+ - **HIGH: Single Read Principle:** Optimized pipeline to read each file exactly once, halving I/O overhead.
16
+ - **MEDIUM: Automated Lesson Pruning:** Daemon now automatically prunes expired memory lessons hourly.
17
+ - **MEDIUM: Memory Bounded Indexing:** First-run indexing now processes in memory-bounded batches to prevent OOM on large repos.
18
+ - **LOW: Checkpoint Validation:** MCP \`create_checkpoint\` tool now validates all input fields to prevent malformed data.
19
+
20
+ ### Added
21
+ - **Interactive Review Flow:** New CLI command \`synap lessons review\` for interactive management of agent-proposed lessons.
22
+ - **Context Monitoring:** New MCP tool \`signal_low_context\` for proactive agent context window monitoring.
23
+ - **Configurable Maintenance:** Added \`checkpoint_threshold\` and \`lesson_expiry_days\` to \`config.toml\`.
24
+ - **Improved Doctor:** \`synap doctor\` now checks for Git and GitHub CLI availability.
25
+ - **Onboarding Guidance:** \`synap init\` now provides explicit next steps for starting the system.
26
+
27
+ ### Removed
28
+ - **Mock LLM Mode:** \`MockLLMProvider\` removed from the production codebase to maintain strict operational integrity.
29
+
30
+ ## [1.1.0] - 2026-05-28
31
+
32
+ ### Added — Git-Snapshot Projection & Performance Refactoring
33
+ - **Split Two-Path Indexing:** Separated initialization and incremental indexing into `_first_run_index` (full scan, CPU-parallelized) and `_incremental_index` (Git delta change detector).
34
+ - **Asynchronous Wiki Generation Queue:** Decoupled slow, non-deterministic LLM wiki generation from structural indexing using a persistent database queue (`wiki_queue`) processed asynchronously by a daemon worker.
35
+ - **Lazy Wiki Caching:** Added synchronous wiki generation fallback to CLI (`wiki show`), Web API, and MCP tools to dynamically build missing or stale pages on-demand.
36
+ - **Process Pool Parallel Parsing:** Parallelized Tree-sitter parsing on first run across all CPU cores utilizing process-based concurrency with independent parser instances.
37
+ - **SQLite Performance Hardening:**
38
+ - WAL mode and NORMAL synchronous configuration enabled during writes.
39
+ - Multi-row symbol and edge inserts batched into a single transaction via `executemany`.
40
+ - Dot-separated `module_key` pre-computation and indexing for $O(1)$ module resolution.
41
+ - SQLite FTS5 index integration for fast sub-millisecond symbol searches, avoiding full-table scans.
42
+ - **Web API Lazy Refreshes:** Updated the `/wiki/{filepath}` GET endpoint to perform lazy refreshes on stale or missing pages before returning content.
43
+
44
+ ### Fixed
45
+ - **FTS5 Cascade Delete:** Added database trigger `tgr_symbols_delete` to automatically clean up virtual `symbols_fts` entries when parent symbols are deleted.
46
+ - **Duplicate File ID Collision:** Handled unique, path-scoped file identifier generation ensuring files with identical content (like empty `__init__.py`) do not conflict.
47
+
48
+ ## [0.2.1] - 2026-05-27
49
+
50
+ ### Added — Final Production Hardening & Release Execution
51
+ - `synap rollback --commit <ref>` option: directly target a commit by hash/reference without interactive selection prompt.
52
+ - `synap rollback --yes` / `-y` option: suppress confirmation prompt for non-interactive and scripted rollback flows.
53
+ - Non-interactive guard in `synap rollback`: fails fast with a clear error when used in piped/CI contexts without `--commit` or `--yes`.
54
+ - `synap rollback` invalid commit detection: validates commit reference via `git rev-parse --verify` and rejects unknown refs with a clear message.
55
+
56
+ ### Fixed
57
+ - **SQLite migration short-circuit bug**: legacy un-versioned databases (user_version = 0) incorrectly skipped `CREATE TABLE IF NOT EXISTS` execution, leaving the `symbols` table and others uninitialized. The premature short-circuit is removed; all schema tables are now created correctly before bumping to version 1.
58
+ - **Python `import_from_statement` missing symbol extraction**: Tree-sitter AST parser only extracted the module identifier from `from X import Y` statements, discarding `Y`. Now correctly emits `module:symbol` pairs for all imported names, aliases, and grouped imports.
59
+ - **Namespace-aware call edge resolution**: Pass 2 import resolver now splits `module:symbol` import entries to narrow edge targets to the correct module file, eliminating false-positive dependency edges to duplicate class names in sibling namespaces.
60
+ - **FastAPI app version hardcoded**: `create_app()` used a hardcoded version string `"0.2.0"` instead of the canonical `__version__`. Now dynamically imported from `synap_git.__init__`.
61
+ - **Streaming generator cancellation safety**: confirmed `httpx` stream connections are closed cleanly on partial consumption (no socket leaks).
62
+ - **Degraded mode retry logic**: confirmed 2-stage exponential backoff and graceful structural fallback under fully-offline and timeout conditions.
63
+
64
+ ### Changed
65
+ - Daemon resilience test (`test_daemon_resilience.py`) hardened against race condition where `SIGKILL` test read a stale PID from a prior run that had already exited.
66
+
67
+ ## [0.2.0] - 2026-05-26
68
+
69
+ ### Added — Final Polish & Release Readiness
70
+ - CLI usage management: `synap usage show` (displays Rich aggregated usage table and summary panel) and `synap usage clear`.
71
+ - CLI wiki management: `synap wiki list` and `synap wiki show <filepath>` (renders page in terminal via Rich Markdown).
72
+ - LLM call database logging: records `prompt_tokens`, `completion_tokens` for retrieval and wiki generation passes.
73
+ - Real-time daemon state: heartbeats integrated into `synap status`, `synap doctor`, and the Web UI status endpoints.
74
+ - Premium Web UI dashboard polish: dual L3 memory (Approved vs Pending) view, real-time LLM usage analytics, and active daemon PID badge.
75
+ - Defensive GHA release pipeline: `.github/workflows/release.yml` automates TestPyPI and PyPI publishing, tag alignment checking, and draft release generation.
76
+ - Clean Typer execution wrapper: intercepts configuration and credential exceptions to output actionable suggestions (e.g. `synap setup`) instead of tracebacks.
77
+
78
+ ### Added — Hardening Loop 2: MCP Stability & Protocol Polish
79
+ - Deterministic MCP JSON envelope: every tool response carries `ok`, `data`, `warnings`, `trace_id`, `dirty_tree`.
80
+ - Structured error objects with `code`, `message`, and `suggestion` fields for all failure paths.
81
+ - `dirty_tree` propagation: agents are warned when the working tree is ahead of the index.
82
+ - `get_approved_memory()` and `get_pending_memory()` MCP tools exposing lesson trust status.
83
+ - `synap mcp verify` command to assert full protocol contract compliance.
84
+
85
+ ### Added — Hardening Loop 3: Lesson System Trust & Memory Lifecycle
86
+ - Formal `LessonStatus` enum enforcing explicit state machine: `PENDING → APPROVED/REJECTED`, `APPROVED → EXPIRED`.
87
+ - Retrieval gating: only `APPROVED`, non-expired lessons are injected into LLM context as `# APPROVED SYSTEM MEMORY`.
88
+ - `prune_expired_lessons()` transitions stale lessons to `EXPIRED` state on demand.
89
+ - `approval_actor` field on lessons for full human-governance provenance.
90
+ - `synap memory status` — counts of pending, approved, expired lessons.
91
+ - `synap memory prune` — forces expiry evaluation and prunes dead memory.
92
+ - `synap memory verify` — checks approved lessons' `files_affected` against current repo state; reports dangling references.
93
+ - `synap lessons approve <id>` and `synap lessons reject <id>` — explicit human governance over pending lessons.
94
+
95
+ ### Added — Hardening Loop 4: Tracing, Git Compliance & Release Hardening
96
+ - `GitIgnoreSpec` glob-to-regex parser in `RepositoryScanner` respects `.gitignore` patterns.
97
+ - Auto-protection: `SynapRuntime.bootstrap()` automatically adds `.synap/` to `.gitignore`.
98
+ - Enhanced binary file detection via extension blocklist + control-character ratio analysis.
99
+ - Symlink traversal prevention in `RepositoryScanner` (path-containment enforcement).
100
+ - `TraceStore` writes structured operational traces to `.synap/trace_latest.json`.
101
+ - Full hybrid retrieval tracing: latency timeline, token allocation, truncation explanations, structural hops, dirty-tree warnings.
102
+ - `/api/v1/trace/latest` REST endpoint exposing the latest trace.
103
+ - Diagnostic Web UI with visual latency timeline, source provenance table, and dirty-tree badge.
104
+ - Daemon heartbeat file (`.synap/daemon_heartbeat.json`) with PID, uptime, and recovery metrics.
105
+ - Daemon self-healing: SQLite corruption detected via `PRAGMA quick_check` triggers a wipe + re-bootstrap.
106
+ - `synap rollback` — interactive rollback to a previous git commit with lesson preservation.
107
+ - `synap recover` — explicit manual DB corruption recovery flow.
108
+ - `py.typed` marker for PEP 561 compliance.
109
+
110
+ ### Changed
111
+ - CI pipeline split into 4 focused jobs: `lint`, `test`, `benchmark` (main only), `release-validation`.
112
+ - Release validation now runs `synap init` before `synap doctor` to ensure a valid Synap DB context.
113
+ - `pytest` configured with `asyncio_default_fixture_loop_scope = "function"` to eliminate deprecation warnings.
114
+ - Benchmark tests gated behind `benchmark` marker; skipped in fast PR CI passes.
115
+
116
+ ### Fixed
117
+ - `synap doctor` in CI release validation step previously ran against an uninitialized directory.
118
+ - `ruff format` drift in `cli/main.py` and `indexer/daemon.py` resolved.
119
+
120
+ ## [0.1.0] - 2026-05-25
121
+
122
+ ### Added
123
+ - Core deterministic indexing engine using Tree-sitter and Git content hashes.
124
+ - 4-stage hybrid retrieval pipeline (Temporal, Structural, Lexical, Semantic).
125
+ - Model Context Protocol (MCP) server for IDE integration.
126
+ - "Why-This-Context" retrieval tracing system.
127
+ - Secure secret management via `python-keyring`.
128
+ - `synap doctor` for system validation.
129
+ - Diagnostic UI dashboard.
130
+
131
+ ### Changed
132
+ - Refactored entire architecture from event-sourcing to deterministic Git projections.
133
+ - Consolidated storage into unified SQLite schema with Recursive CTE support.
134
+ - Upgraded documentation to production infrastructure standards.
135
+
136
+ ### Removed
137
+ - Legacy "cognitive OS" and "graph operating system" abstractions.
138
+ - Speculative async priority queues and replay engines.
139
+ - Brittle regex-based parsers.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synap-git
3
- Version: 0.2.1
3
+ Version: 1.1.1
4
4
  Summary: Persistent structural context infrastructure for AI coding agents.
5
5
  Project-URL: Homepage, https://github.com/saahilpal/synap-git
6
6
  Project-URL: Repository, https://github.com/saahilpal/synap-git
@@ -27,6 +27,7 @@ Requires-Dist: keyring==25.6.0
27
27
  Requires-Dist: markdown-it-py==3.0.0
28
28
  Requires-Dist: mcp==1.3.0
29
29
  Requires-Dist: msgpack==1.1.0
30
+ Requires-Dist: prompt-toolkit<3.0.44
30
31
  Requires-Dist: pydantic-settings==2.8.1
31
32
  Requires-Dist: pydantic==2.10.6
32
33
  Requires-Dist: questionary==2.1.0
@@ -73,6 +74,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
73
74
 
74
75
  ---
75
76
 
77
+ ## ⚡ Performance Architecture (v1.1.0)
78
+
79
+ Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
80
+ - **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
81
+ - **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
82
+ - **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
83
+ - **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
84
+
85
+ ---
86
+
76
87
  ## 🏗️ High-Level Architecture (HLD)
77
88
 
78
89
  Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
@@ -227,6 +238,7 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
227
238
  - `synap memory status .` : View counts of approved, pending, and expired lessons.
228
239
  - `synap memory prune .` : Prune expired rules and cleanup memory.
229
240
  - `synap memory verify .` : Detect dangling file references in active memory.
241
+ - `synap lessons review .` : Interactively review and Approve/Edit/Reject pending lessons.
230
242
  - `synap lessons approve <id> .` : Approve a pending revert lesson to activate it.
231
243
  - `synap lessons reject <id> .` : Reject and discard a pending lesson.
232
244
  - `synap checkpoint create . --doing "..."` : Save the current context state.
@@ -236,8 +248,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
236
248
  ### Developer Tools
237
249
  - `synap wiki list .` : List all generated wiki documentation files.
238
250
  - `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
239
- - `synap cost show .` : Display detailed aggregated LLM token usage and estimated costs.
240
- - `synap cost clear .` : Purge all LLM call cost history.
251
+ - `synap usage show .` : Display detailed aggregated LLM token usage.
252
+ - `synap usage clear .` : Purge all LLM call history.
241
253
  - `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
242
254
  - `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
243
255
 
@@ -251,3 +263,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
251
263
  - Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
252
264
 
253
265
  License: [Apache 2.0](LICENSE.md)
266
+ ](LICENSE.md)
@@ -24,6 +24,16 @@ Synap is **NOT a RAG system.** It is a strict, deterministic background daemon t
24
24
 
25
25
  ---
26
26
 
27
+ ## ⚡ Performance Architecture (v1.1.0)
28
+
29
+ Synap is built on the **Git-Snapshot Paradigm** to guarantee sub-100ms response times for everyday agent workflows:
30
+ - **Two separate code paths:** First-run indexing uses process-pool parallel parsing (`ProcessPoolExecutor`) to build the initial codebase structure, while subsequent indexing uses a Git delta change detector (`git diff-tree`) to process only changed files. (Reason: Avoids full filesystem scans on every run).
31
+ - **No filesystem scan / file hashing on incremental runs:** Synap uses Git blob OIDs to detect changed files. (Reason: Git already hashes everything, making filesystem reads redundant).
32
+ - **Decoupled non-deterministic LLM pipeline:** Structural parsing (deterministic, fast) completes immediately and enqueues wiki generation (non-deterministic, slow) to a background worker, while lazy caching fallback handles CLI/API wiki requests. (Reason: Never block structural indexing or CLI commands on LLM API response latency).
33
+ - **SQLite optimizations:** SQLite writes are grouped into a single transaction per batch (Reason: Avoids disk sync overhead per row). SQLite FTS5 index matches symbols in sub-milliseconds (Reason: Eliminates slow wildcard `LIKE` table scans). Pre-computed dot-separated `module_key` columns allow O(1) module resolution (Reason: Prevents suffix-matching scans).
34
+
35
+ ---
36
+
27
37
  ## 🏗️ High-Level Architecture (HLD)
28
38
 
29
39
  Synap bridges the gap between your local file system, Git history, and the LLM via a 3-layer indexing strategy.
@@ -178,6 +188,7 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
178
188
  - `synap memory status .` : View counts of approved, pending, and expired lessons.
179
189
  - `synap memory prune .` : Prune expired rules and cleanup memory.
180
190
  - `synap memory verify .` : Detect dangling file references in active memory.
191
+ - `synap lessons review .` : Interactively review and Approve/Edit/Reject pending lessons.
181
192
  - `synap lessons approve <id> .` : Approve a pending revert lesson to activate it.
182
193
  - `synap lessons reject <id> .` : Reject and discard a pending lesson.
183
194
  - `synap checkpoint create . --doing "..."` : Save the current context state.
@@ -187,8 +198,8 @@ Synap uses a powerful, strict CLI interface. Every destructive action prompts fo
187
198
  ### Developer Tools
188
199
  - `synap wiki list .` : List all generated wiki documentation files.
189
200
  - `synap wiki show <filepath> .` : Render a specific wiki markdown page to the console.
190
- - `synap cost show .` : Display detailed aggregated LLM token usage and estimated costs.
191
- - `synap cost clear .` : Purge all LLM call cost history.
201
+ - `synap usage show .` : Display detailed aggregated LLM token usage.
202
+ - `synap usage clear .` : Purge all LLM call history.
192
203
  - `synap doctor .` : Validate SQLite integrity, Tree-sitter, tokenizers, LLM providers, and daemon heartbeat.
193
204
  - `synap mcp verify .` : Verify MCP protocol, tool schemas, and contract stability.
194
205
 
@@ -202,3 +213,4 @@ Synap is built on the philosophy that AI tools must be transparent and controlla
202
213
  - Ensure all states are stored exclusively in `synap.db` or `.synap/wiki/`.
203
214
 
204
215
  License: [Apache 2.0](LICENSE.md)
216
+ ](LICENSE.md)
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "markdown-it-py==3.0.0",
28
28
  "mcp==1.3.0",
29
29
  "msgpack==1.1.0",
30
+ "prompt-toolkit<3.0.44",
30
31
  "pydantic==2.10.6",
31
32
  "pydantic-settings==2.8.1",
32
33
  "questionary==2.1.0",
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.2.1"
5
+ __version__ = "1.1.1"
@@ -10,6 +10,7 @@ from fastapi.middleware.cors import CORSMiddleware
10
10
  from fastapi.responses import HTMLResponse, StreamingResponse
11
11
  from fastapi.staticfiles import StaticFiles
12
12
 
13
+ from synap_git import __version__
13
14
  from synap_git.indexer.engine import SynapRuntime
14
15
 
15
16
 
@@ -17,7 +18,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
17
18
  app = FastAPI(
18
19
  title="Synap Context Diagnostics",
19
20
  description="Deterministic structural context infrastructure for AI coding agents.",
20
- version="0.2.0",
21
+ version=__version__,
21
22
  )
22
23
 
23
24
  app.add_middleware(
@@ -36,17 +37,20 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
36
37
  @app.get("/", response_class=HTMLResponse)
37
38
  async def get_index() -> str:
38
39
  index_file = static_dir / "index.html"
39
- if index_file.exists():
40
- return index_file.read_text(encoding="utf-8")
40
+ exists = await asyncio.to_thread(index_file.exists)
41
+ if exists:
42
+ return await asyncio.to_thread(index_file.read_text, encoding="utf-8")
41
43
  return "<h1>Synap Diagnostic UI is ready.</h1>"
42
44
 
43
45
  @app.get("/api/v1/status")
44
46
  async def get_status() -> dict[str, Any]:
45
47
  try:
46
- status = runtime.status()
48
+ status = await asyncio.to_thread(runtime.status)
47
49
  from synap_git.cli.main import _read_daemon_heartbeat
48
50
 
49
- daemon_info = _read_daemon_heartbeat(Path(status.repository_path))
51
+ daemon_info = await asyncio.to_thread(
52
+ _read_daemon_heartbeat, Path(status.repository_path)
53
+ )
50
54
  return {
51
55
  "repository_path": status.repository_path,
52
56
  "branch": status.branch,
@@ -63,7 +67,9 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
63
67
  @app.get("/api/v1/trace/latest")
64
68
  async def get_latest_trace() -> dict[str, Any]:
65
69
  try:
66
- return runtime.trace_store.get_latest()
70
+ if runtime.trace_store:
71
+ return await asyncio.to_thread(runtime.trace_store.get_latest)
72
+ return {}
67
73
  except Exception as exc:
68
74
  raise HTTPException(status_code=500, detail=str(exc))
69
75
 
@@ -73,7 +79,7 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
73
79
 
74
80
  async def event_generator() -> AsyncGenerator[str, None]:
75
81
  while True:
76
- status = runtime.status()
82
+ status = await asyncio.to_thread(runtime.status)
77
83
  yield f"data: {json.dumps(status.__dict__)}\n\n"
78
84
  await asyncio.sleep(2)
79
85
 
@@ -81,31 +87,56 @@ def create_app(runtime: SynapRuntime) -> FastAPI:
81
87
 
82
88
  @app.get("/wiki/{filepath:path}")
83
89
  async def get_wiki_page(filepath: str) -> dict[str, Any]:
84
- wiki_path = runtime.wiki.wiki_dir / f"{filepath}.md"
85
- if wiki_path.exists():
86
- return {"status": "ok", "content": wiki_path.read_text(encoding="utf-8")}
90
+ base_dir = runtime.wiki.wiki_dir.resolve()
91
+ try:
92
+ target_path = (base_dir / filepath).resolve()
93
+ except Exception:
94
+ raise HTTPException(status_code=400, detail="Invalid path")
95
+
96
+ if not target_path.is_relative_to(base_dir):
97
+ raise HTTPException(status_code=403, detail="Path traversal detected")
98
+
99
+ target = filepath
100
+ if target.endswith(".md"):
101
+ target = target[:-3]
102
+ try:
103
+ await asyncio.to_thread(runtime.wiki.ensure_wiki_page, target)
104
+ except Exception:
105
+ pass
106
+
107
+ wiki_path = runtime.wiki.wiki_dir / f"{target}.md"
108
+ exists = await asyncio.to_thread(wiki_path.exists)
109
+ if exists:
110
+ content = await asyncio.to_thread(wiki_path.read_text, encoding="utf-8")
111
+ return {"status": "ok", "content": content}
87
112
  return {"status": "error", "message": "Wiki not found"}
88
113
 
89
114
  @app.get("/api/v1/memory")
90
115
  async def get_memory_page() -> dict[str, Any]:
91
- approved = runtime.store.get_lessons("approved")
92
- pending = runtime.store.get_lessons("pending")
116
+ approved = await asyncio.to_thread(runtime.store.get_lessons, "approved")
117
+ pending = await asyncio.to_thread(runtime.store.get_lessons, "pending")
93
118
  return {"status": "ok", "approved": approved, "pending": pending}
94
119
 
95
- @app.get("/api/v1/cost")
96
- async def get_cost_page() -> dict[str, Any]:
120
+ def _fetch_calls() -> list[dict[str, Any]]:
97
121
  with runtime.store.connect() as conn:
98
122
  rows = conn.execute("SELECT * FROM llm_calls ORDER BY created_at DESC").fetchall()
99
- calls = [dict(r) for r in rows]
123
+ return [dict(r) for r in rows]
124
+
125
+ @app.get("/api/v1/usage")
126
+ async def get_usage_page() -> dict[str, Any]:
127
+ calls = await asyncio.to_thread(_fetch_calls)
100
128
  return {"status": "ok", "calls": calls}
101
129
 
102
- @app.get("/api/v1/checkpoints")
103
- async def get_checkpoints_page() -> dict[str, Any]:
130
+ def _fetch_checkpoints() -> list[dict[str, Any]]:
104
131
  with runtime.store.connect() as conn:
105
132
  rows = conn.execute(
106
133
  "SELECT * FROM checkpoints ORDER BY created_at DESC LIMIT 20"
107
134
  ).fetchall()
108
- cps = [dict(r) for r in rows]
135
+ return [dict(r) for r in rows]
136
+
137
+ @app.get("/api/v1/checkpoints")
138
+ async def get_checkpoints_page() -> dict[str, Any]:
139
+ cps = await asyncio.to_thread(_fetch_checkpoints)
109
140
  return {"status": "ok", "checkpoints": cps}
110
141
 
111
142
  return app
@@ -202,15 +202,11 @@
202
202
  </div>
203
203
  </div>
204
204
 
205
- <!-- LLM Cost & Usage -->
205
+ <!-- LLM Usage -->
206
206
  <div class="card" style="grid-column: span 2;">
207
- <h2>LLM Call Cost & Usage</h2>
207
+ <h2>LLM Call Usage</h2>
208
208
  <div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem;">
209
209
  <div class="stats-grid" style="grid-template-columns: 1fr; gap: 0.75rem;">
210
- <div class="stat-box">
211
- <div class="stat-value" id="cost-total-usd">$0.000000</div>
212
- <div class="stat-label">Total Cost (USD)</div>
213
- </div>
214
210
  <div class="stat-box">
215
211
  <div class="stat-value" id="cost-total-calls">0</div>
216
212
  <div class="stat-label">Total LLM Calls</div>
@@ -231,11 +227,10 @@
231
227
  <th>Model</th>
232
228
  <th>Purpose</th>
233
229
  <th>Tokens</th>
234
- <th>Cost</th>
235
230
  </tr>
236
231
  </thead>
237
232
  <tbody id="cost-calls-body">
238
- <tr><td colspan="6" class="empty-state">No calls recorded.</td></tr>
233
+ <tr><td colspan="5" class="empty-state">No calls recorded.</td></tr>
239
234
  </tbody>
240
235
  </table>
241
236
  </div>
@@ -443,44 +438,59 @@
443
438
  pendingBody.innerHTML = `<tr><td colspan="3" class="empty-state">No pending lessons needing approval.</td></tr>`;
444
439
  }
445
440
 
446
- // Fetch Cost & Usage
447
- const costRes = await fetch('/api/v1/cost');
448
- const costData = await costRes.json();
441
+ // Fetch Usage
442
+ const usageRes = await fetch('/api/v1/usage');
443
+ const usageData = await usageRes.json();
449
444
  const costBody = document.getElementById('cost-calls-body');
450
445
 
451
- let totalCost = 0;
452
446
  let totalCalls = 0;
453
447
  let totalTokens = 0;
454
448
 
455
- if (costData.calls && costData.calls.length > 0) {
456
- totalCalls = costData.calls.length;
457
- costBody.innerHTML = costData.calls.slice(0, 15).map(c => {
458
- totalCost += c.cost_usd;
449
+ if (usageData.calls && usageData.calls.length > 0) {
450
+ totalCalls = usageData.calls.length;
451
+ usageData.calls.slice(0, 15).map(c => {
459
452
  const tokens = c.input_tokens + c.output_tokens;
460
453
  totalTokens += tokens;
461
454
 
462
455
  const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
463
- return `
456
+ const row = `
464
457
  <tr class="row-hover">
465
458
  <td>${timeStr}</td>
466
459
  <td>${c.provider}</td>
467
460
  <td><code>${c.model}</code></td>
468
461
  <td>${c.purpose}</td>
469
462
  <td>${tokens.toLocaleString()}</td>
470
- <td style="color:var(--success); font-family:var(--font-mono); font-size:11px;">$${c.cost_usd.toFixed(6)}</td>
471
463
  </tr>
472
464
  `;
473
- }).join('');
465
+ costBody.insertAdjacentHTML('beforeend', row);
466
+ });
467
+
468
+ // Clear the loading message if it exists
469
+ if (usageData.calls.length > 0 && costBody.querySelector('.empty-state')) {
470
+ costBody.innerHTML = '';
471
+ // Re-render the 15 calls because we just cleared it
472
+ costBody.innerHTML = usageData.calls.slice(0, 15).map(c => {
473
+ const tokens = c.input_tokens + c.output_tokens;
474
+ const timeStr = new Date(c.created_at * 1000).toLocaleTimeString();
475
+ return `
476
+ <tr class="row-hover">
477
+ <td>${timeStr}</td>
478
+ <td>${c.provider}</td>
479
+ <td><code>${c.model}</code></td>
480
+ <td>${c.purpose}</td>
481
+ <td>${tokens.toLocaleString()}</td>
482
+ </tr>
483
+ `;
484
+ }).join('');
485
+ }
474
486
 
475
- costData.calls.slice(15).forEach(c => {
476
- totalCost += c.cost_usd;
487
+ usageData.calls.slice(15).forEach(c => {
477
488
  totalTokens += (c.input_tokens + c.output_tokens);
478
489
  });
479
490
  } else {
480
- costBody.innerHTML = `<tr><td colspan="6" class="empty-state">No LLM calls recorded yet.</td></tr>`;
491
+ costBody.innerHTML = `<tr><td colspan="5" class="empty-state">No LLM calls recorded yet.</td></tr>`;
481
492
  }
482
493
 
483
- document.getElementById('cost-total-usd').textContent = `$${totalCost.toFixed(6)}`;
484
494
  document.getElementById('cost-total-calls').textContent = totalCalls.toLocaleString();
485
495
  document.getElementById('cost-total-tokens').textContent = totalTokens.toLocaleString();
486
496