omnidb-server 0.1.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.
Files changed (45) hide show
  1. omnidb_server-0.1.0/PKG-INFO +157 -0
  2. omnidb_server-0.1.0/README.md +137 -0
  3. omnidb_server-0.1.0/omnidb_server/__init__.py +8 -0
  4. omnidb_server-0.1.0/omnidb_server/__main__.py +6 -0
  5. omnidb_server-0.1.0/omnidb_server/cli.py +88 -0
  6. omnidb_server-0.1.0/omnidb_server/config.py +105 -0
  7. omnidb_server-0.1.0/omnidb_server/consolidate.py +205 -0
  8. omnidb_server-0.1.0/omnidb_server/db.py +114 -0
  9. omnidb_server-0.1.0/omnidb_server/embeddings.py +93 -0
  10. omnidb_server-0.1.0/omnidb_server/fusion.py +26 -0
  11. omnidb_server-0.1.0/omnidb_server/portability.py +135 -0
  12. omnidb_server-0.1.0/omnidb_server/rerank.py +74 -0
  13. omnidb_server-0.1.0/omnidb_server/scoring.py +29 -0
  14. omnidb_server-0.1.0/omnidb_server/server.py +121 -0
  15. omnidb_server-0.1.0/omnidb_server/store.py +732 -0
  16. omnidb_server-0.1.0/omnidb_server/vector_index.py +249 -0
  17. omnidb_server-0.1.0/omnidb_server.egg-info/PKG-INFO +157 -0
  18. omnidb_server-0.1.0/omnidb_server.egg-info/SOURCES.txt +43 -0
  19. omnidb_server-0.1.0/omnidb_server.egg-info/dependency_links.txt +1 -0
  20. omnidb_server-0.1.0/omnidb_server.egg-info/entry_points.txt +2 -0
  21. omnidb_server-0.1.0/omnidb_server.egg-info/requires.txt +15 -0
  22. omnidb_server-0.1.0/omnidb_server.egg-info/top_level.txt +1 -0
  23. omnidb_server-0.1.0/pyproject.toml +41 -0
  24. omnidb_server-0.1.0/setup.cfg +4 -0
  25. omnidb_server-0.1.0/tests/test_agent_flow.py +61 -0
  26. omnidb_server-0.1.0/tests/test_chaos.py +119 -0
  27. omnidb_server-0.1.0/tests/test_cli_ops.py +105 -0
  28. omnidb_server-0.1.0/tests/test_consolidate.py +86 -0
  29. omnidb_server-0.1.0/tests/test_db.py +84 -0
  30. omnidb_server-0.1.0/tests/test_foundation_v2.py +226 -0
  31. omnidb_server-0.1.0/tests/test_graph_fuzz.py +72 -0
  32. omnidb_server-0.1.0/tests/test_graph_search.py +35 -0
  33. omnidb_server-0.1.0/tests/test_hardening.py +104 -0
  34. omnidb_server-0.1.0/tests/test_issue_fixes.py +268 -0
  35. omnidb_server-0.1.0/tests/test_memory.py +112 -0
  36. omnidb_server-0.1.0/tests/test_perf_smoke.py +93 -0
  37. omnidb_server-0.1.0/tests/test_portability.py +126 -0
  38. omnidb_server-0.1.0/tests/test_reconcile.py +55 -0
  39. omnidb_server-0.1.0/tests/test_reranker.py +199 -0
  40. omnidb_server-0.1.0/tests/test_scoring_properties.py +69 -0
  41. omnidb_server-0.1.0/tests/test_security_matrix.py +126 -0
  42. omnidb_server-0.1.0/tests/test_semantic.py +40 -0
  43. omnidb_server-0.1.0/tests/test_stdio_conformance.py +75 -0
  44. omnidb_server-0.1.0/tests/test_supersession.py +76 -0
  45. omnidb_server-0.1.0/tests/test_tools_v2.py +39 -0
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnidb-server
3
+ Version: 0.1.0
4
+ Summary: MCP-native agent memory & retrieval server over SQLite (WAL + FTS5) with an hnswlib vector sidecar.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.11
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: mcp<2,>=1.9
9
+ Requires-Dist: numpy>=1.26
10
+ Requires-Dist: httpx>=0.27
11
+ Provides-Extra: semantic
12
+ Requires-Dist: fastembed>=0.3; extra == "semantic"
13
+ Provides-Extra: hnsw
14
+ Requires-Dist: hnswlib>=0.8; extra == "hnsw"
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-cov>=5; extra == "dev"
18
+ Requires-Dist: ruff>=0.6; extra == "dev"
19
+ Requires-Dist: hypothesis>=6.100; extra == "dev"
20
+
21
+ # OmniDB — MCP-native agent memory & retrieval server
22
+
23
+ A local-first, single-binary-feel **agent memory and retrieval server over SQLite**, exposed to AI
24
+ agents through the Model Context Protocol. This is the revised v1 that came out of the August 2026
25
+ research + adversarial review (see `../OmniDB Vault/`): an MCP memory/retrieval server, not a database.
26
+
27
+ - **Substrate**: SQLite in WAL mode (`synchronous=NORMAL`, `busy_timeout=5000`) — one directory on disk,
28
+ copyable as a unit (`omnidb.db` + WAL sidecars + vector index file). The single-file mandate was dropped.
29
+ - **Retrieval**: BM25 (FTS5, porter tokenization) fused with dense vector search via Reciprocal Rank Fusion,
30
+ then a gentle recency/frequency rerank. Hybrid retrieval runs in-process; SQLite table-valued functions
31
+ (`VECTOR_SIM` / `MEMORY_RECALL`) need a native extension and are the documented upgrade path for the
32
+ future Go/Rust port.
33
+ - **Vectors**: hnswlib (M=16, efC=200) inner-product on L2-normalized vectors == cosine, persisted as a
34
+ sidecar file; exact brute-force fallback always available. Deletions are mark-deleted.
35
+ - **Memory model**: plain relational tables (`episodic | semantic | procedural`) — no bespoke LSM, no fixed
36
+ decay religion (weights are config). Consolidation distills episodic → semantic via a configurable LLM,
37
+ with an offline stub when none is set.
38
+
39
+ ## Quickstart
40
+
41
+ ```bash
42
+ pip install -e ".[hnsw,semantic,dev]" # hnsw/semantic extras optional; falls back gracefully
43
+ pytest
44
+ omnidb-server serve # MCP stdio server on $OMNIDB_HOME (default ~/.omnidb)
45
+ python -m omnidb_server serve # equivalent
46
+ python -m omnidb_server stats
47
+ ```
48
+
49
+ ### Claude Desktop / any MCP client
50
+
51
+ See [`examples/claude-desktop-config.example.json`](examples/claude-desktop-config.example.json)
52
+ — includes the env vars for real (fastembed) semantics and a working `HF_HOME`
53
+ override. Minimal version:
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "omnidb": {
59
+ "command": "python",
60
+ "args": ["-m", "omnidb_server", "serve"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ ## Tools
67
+
68
+ | Tool | Purpose |
69
+ |---|---|
70
+ | `remember(content, kind, metadata, links)` | persist a memory (episodic/semantic/procedural), optional graph links |
71
+ | `recall(query, k, kind, k_fetch)` | hybrid BM25+dense recall with recency/frequency rerank; pool floors at `k` |
72
+ | `update_memory(memory_id, content, metadata)` | edit content (re-embeds + reindexes via FTS trigger) and/or merge metadata |
73
+ | `search(sql)` | read-only SELECT/WITH escape hatch against SQLite |
74
+ | `related(memory_id, max_hops, label)` | undirected graph walk (recursive CTE, cycle-safe, hops clamped 1-8) |
75
+ | `forget(memory_id)` / `forget_many(ids)` | soft delete one / many |
76
+ | `consolidate(hours)` | episodic → semantic distillation |
77
+ | `stats()` | counts, edges, embedder/index info, dead-ratio, all file sizes |
78
+
79
+ Error messages carry `OMN-*` codes from the original spec's taxonomy (`[OMN-020]` read-only violation, `[OMN-021]` not found, `[OMN-006]` dimension mismatch, `[OMN-011]` timeout, ...).
80
+
81
+ ## Configuration
82
+
83
+ Precedence: **CLI flags > environment > `omnidb.toml` > defaults**. The TOML file is looked up in
84
+ the CWD and the data directory (field names mirror the config dataclass):
85
+
86
+ ```toml
87
+ half_life_hours = 168
88
+ rrf_k = 60
89
+ k_fetch = 50
90
+ persist_every_ops = 64 # vector-sidecar fsync debouncing
91
+ dead_compact_ratio = 0.2 # hnsw tombstone ratio triggering rebuild-on-open
92
+ ```
93
+
94
+ ## Operations
95
+
96
+ ```bash
97
+ omnidb-server compact --home D:/path/to/data # force full vector-index rebuild
98
+ python -m omnidb_server stats # includes pending_compaction, reconciled_at, WAL size
99
+ omnidb-server check # FTS integrity, index drift, orphan edges, WAL size
100
+ omnidb-server purge [--vacuum] # hard-delete soft-deleted rows; --vacuum reclaims space
101
+ omnidb-server export D:/backup/memories.jsonl # dump all memories + edges to JSONL
102
+ omnidb-server import D:/backup/memories.jsonl # restore from a JSONL dump
103
+ ```
104
+
105
+ ## Configuration (environment)
106
+
107
+ | Variable | Default | Meaning |
108
+ |---|---|---|
109
+ | `OMNIDB_HOME` | `~/.omnidb` | data directory |
110
+ | `OMNIDB_EMBEDDER` | `auto` | `auto`/`hashing` are offline-safe (never download); `fastembed` opts into real semantics (`[semantic]` extra, ~100MB model on first run). Embedder identity is pinned per database. |
111
+ | `OMNIDB_INDEX` | `auto` | `auto` \| `hnsw` \| `brute` |
112
+ | `OMNIDB_HALF_LIFE_HOURS` | `168` | recency half-life for rerank |
113
+ | `OMNIDB_RECENCY_WEIGHT` / `OMNIDB_FREQ_WEIGHT` | `0.15` / `0.05` | rerank weights (RRF base is weight-free) |
114
+ | `OMNIDB_K_FETCH` | `50` | candidates fetched per leg before fusion |
115
+ | `OMNIDB_RRF_K` | `60` | RRF smoothing constant |
116
+ | `OMNIDB_MAX_SEARCH_ROWS` | `500` | row cap for the `search` tool |
117
+ | `OMNIDB_LLM_BASE_URL` / `_MODEL` / `_API_KEY` | unset | OpenAI-compatible endpoint used by `consolidate`; stub when unset. With one configured, a failed call consumes nothing (episodes stay retryable) |
118
+
119
+ ## Benchmarking
120
+
121
+ ```bash
122
+ python bench/baseline.py # writes bench/results.md
123
+ ```
124
+
125
+ Measures hnswlib (M=16, efC=200) recall@10 vs exact brute force across an ef sweep at
126
+ 384/768 dims on clustered synthetic data (1k/10k/100k), plus the SQ8 experiment.
127
+ Findings so far: at 768-dim/10k, SQ8+rerank (0.947) beats raw HNSW at ef=128 (0.837);
128
+ at 100k the clustered-synthetic distribution is strongly pessimistic for graph ANN
129
+ (tight clusters create hub structure), where quantized scoring + f32 rerank stays
130
+ robust (0.90) but at full-scan cost (~120ms). Treat as a floor, not a forecast —
131
+ re-run against your real corpus before drawing conclusions.
132
+
133
+ ## Encryption at rest
134
+
135
+ OmniDB is local-first and writes only to `OMNIDB_HOME`; it phones home to no one. For
136
+ data-at-rest protection we recommend OS-level disk encryption (BitLocker / FileVault /
137
+ LUKS) — that covers the stolen-disk threat model without adding a dependency or a key
138
+ you must manage.
139
+
140
+ A SQLCipher swap is documented but deliberately not bundled: encrypting the SQLite file
141
+ would require `pysqlcipher3`, whose Windows wheels for Python 3.14 are effectively
142
+ unobtainable, so shipping it as an extra would break installs rather than protect them.
143
+ If your threat model needs per-file encryption beyond full-disk, compile SQLCipher and
144
+ point this package's connection at it — the store layer is plain `sqlite3` DB-API and
145
+ needs no code changes.
146
+
147
+ ## Limitations (v1, deliberate)
148
+
149
+ - Single-writer process per home directory (SQLite WAL semantics). The vector sidecar is
150
+ last-writer-wins: run one server per home; startup reconciliation after a crash between the DB and
151
+ the index file is deferred.
152
+ - The Python `sqlite3` module cannot register virtual tables, so SQL++-style TVFs arrive with the
153
+ native port; until then hybrid queries go through the tools.
154
+ - Hashing embedder matches surface forms, not paraphrases — install `fastembed` for real semantics.
155
+ Embedder identity is pinned per database; switching requires a fresh home.
156
+ - No QPS targets: this serves one agent session locally. Gates are interactive latency, cold start,
157
+ and RAM at scale (re-baselined empirically at true embedding dimensions, not SIFT-128 folklore).
@@ -0,0 +1,137 @@
1
+ # OmniDB — MCP-native agent memory & retrieval server
2
+
3
+ A local-first, single-binary-feel **agent memory and retrieval server over SQLite**, exposed to AI
4
+ agents through the Model Context Protocol. This is the revised v1 that came out of the August 2026
5
+ research + adversarial review (see `../OmniDB Vault/`): an MCP memory/retrieval server, not a database.
6
+
7
+ - **Substrate**: SQLite in WAL mode (`synchronous=NORMAL`, `busy_timeout=5000`) — one directory on disk,
8
+ copyable as a unit (`omnidb.db` + WAL sidecars + vector index file). The single-file mandate was dropped.
9
+ - **Retrieval**: BM25 (FTS5, porter tokenization) fused with dense vector search via Reciprocal Rank Fusion,
10
+ then a gentle recency/frequency rerank. Hybrid retrieval runs in-process; SQLite table-valued functions
11
+ (`VECTOR_SIM` / `MEMORY_RECALL`) need a native extension and are the documented upgrade path for the
12
+ future Go/Rust port.
13
+ - **Vectors**: hnswlib (M=16, efC=200) inner-product on L2-normalized vectors == cosine, persisted as a
14
+ sidecar file; exact brute-force fallback always available. Deletions are mark-deleted.
15
+ - **Memory model**: plain relational tables (`episodic | semantic | procedural`) — no bespoke LSM, no fixed
16
+ decay religion (weights are config). Consolidation distills episodic → semantic via a configurable LLM,
17
+ with an offline stub when none is set.
18
+
19
+ ## Quickstart
20
+
21
+ ```bash
22
+ pip install -e ".[hnsw,semantic,dev]" # hnsw/semantic extras optional; falls back gracefully
23
+ pytest
24
+ omnidb-server serve # MCP stdio server on $OMNIDB_HOME (default ~/.omnidb)
25
+ python -m omnidb_server serve # equivalent
26
+ python -m omnidb_server stats
27
+ ```
28
+
29
+ ### Claude Desktop / any MCP client
30
+
31
+ See [`examples/claude-desktop-config.example.json`](examples/claude-desktop-config.example.json)
32
+ — includes the env vars for real (fastembed) semantics and a working `HF_HOME`
33
+ override. Minimal version:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "omnidb": {
39
+ "command": "python",
40
+ "args": ["-m", "omnidb_server", "serve"]
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ ## Tools
47
+
48
+ | Tool | Purpose |
49
+ |---|---|
50
+ | `remember(content, kind, metadata, links)` | persist a memory (episodic/semantic/procedural), optional graph links |
51
+ | `recall(query, k, kind, k_fetch)` | hybrid BM25+dense recall with recency/frequency rerank; pool floors at `k` |
52
+ | `update_memory(memory_id, content, metadata)` | edit content (re-embeds + reindexes via FTS trigger) and/or merge metadata |
53
+ | `search(sql)` | read-only SELECT/WITH escape hatch against SQLite |
54
+ | `related(memory_id, max_hops, label)` | undirected graph walk (recursive CTE, cycle-safe, hops clamped 1-8) |
55
+ | `forget(memory_id)` / `forget_many(ids)` | soft delete one / many |
56
+ | `consolidate(hours)` | episodic → semantic distillation |
57
+ | `stats()` | counts, edges, embedder/index info, dead-ratio, all file sizes |
58
+
59
+ Error messages carry `OMN-*` codes from the original spec's taxonomy (`[OMN-020]` read-only violation, `[OMN-021]` not found, `[OMN-006]` dimension mismatch, `[OMN-011]` timeout, ...).
60
+
61
+ ## Configuration
62
+
63
+ Precedence: **CLI flags > environment > `omnidb.toml` > defaults**. The TOML file is looked up in
64
+ the CWD and the data directory (field names mirror the config dataclass):
65
+
66
+ ```toml
67
+ half_life_hours = 168
68
+ rrf_k = 60
69
+ k_fetch = 50
70
+ persist_every_ops = 64 # vector-sidecar fsync debouncing
71
+ dead_compact_ratio = 0.2 # hnsw tombstone ratio triggering rebuild-on-open
72
+ ```
73
+
74
+ ## Operations
75
+
76
+ ```bash
77
+ omnidb-server compact --home D:/path/to/data # force full vector-index rebuild
78
+ python -m omnidb_server stats # includes pending_compaction, reconciled_at, WAL size
79
+ omnidb-server check # FTS integrity, index drift, orphan edges, WAL size
80
+ omnidb-server purge [--vacuum] # hard-delete soft-deleted rows; --vacuum reclaims space
81
+ omnidb-server export D:/backup/memories.jsonl # dump all memories + edges to JSONL
82
+ omnidb-server import D:/backup/memories.jsonl # restore from a JSONL dump
83
+ ```
84
+
85
+ ## Configuration (environment)
86
+
87
+ | Variable | Default | Meaning |
88
+ |---|---|---|
89
+ | `OMNIDB_HOME` | `~/.omnidb` | data directory |
90
+ | `OMNIDB_EMBEDDER` | `auto` | `auto`/`hashing` are offline-safe (never download); `fastembed` opts into real semantics (`[semantic]` extra, ~100MB model on first run). Embedder identity is pinned per database. |
91
+ | `OMNIDB_INDEX` | `auto` | `auto` \| `hnsw` \| `brute` |
92
+ | `OMNIDB_HALF_LIFE_HOURS` | `168` | recency half-life for rerank |
93
+ | `OMNIDB_RECENCY_WEIGHT` / `OMNIDB_FREQ_WEIGHT` | `0.15` / `0.05` | rerank weights (RRF base is weight-free) |
94
+ | `OMNIDB_K_FETCH` | `50` | candidates fetched per leg before fusion |
95
+ | `OMNIDB_RRF_K` | `60` | RRF smoothing constant |
96
+ | `OMNIDB_MAX_SEARCH_ROWS` | `500` | row cap for the `search` tool |
97
+ | `OMNIDB_LLM_BASE_URL` / `_MODEL` / `_API_KEY` | unset | OpenAI-compatible endpoint used by `consolidate`; stub when unset. With one configured, a failed call consumes nothing (episodes stay retryable) |
98
+
99
+ ## Benchmarking
100
+
101
+ ```bash
102
+ python bench/baseline.py # writes bench/results.md
103
+ ```
104
+
105
+ Measures hnswlib (M=16, efC=200) recall@10 vs exact brute force across an ef sweep at
106
+ 384/768 dims on clustered synthetic data (1k/10k/100k), plus the SQ8 experiment.
107
+ Findings so far: at 768-dim/10k, SQ8+rerank (0.947) beats raw HNSW at ef=128 (0.837);
108
+ at 100k the clustered-synthetic distribution is strongly pessimistic for graph ANN
109
+ (tight clusters create hub structure), where quantized scoring + f32 rerank stays
110
+ robust (0.90) but at full-scan cost (~120ms). Treat as a floor, not a forecast —
111
+ re-run against your real corpus before drawing conclusions.
112
+
113
+ ## Encryption at rest
114
+
115
+ OmniDB is local-first and writes only to `OMNIDB_HOME`; it phones home to no one. For
116
+ data-at-rest protection we recommend OS-level disk encryption (BitLocker / FileVault /
117
+ LUKS) — that covers the stolen-disk threat model without adding a dependency or a key
118
+ you must manage.
119
+
120
+ A SQLCipher swap is documented but deliberately not bundled: encrypting the SQLite file
121
+ would require `pysqlcipher3`, whose Windows wheels for Python 3.14 are effectively
122
+ unobtainable, so shipping it as an extra would break installs rather than protect them.
123
+ If your threat model needs per-file encryption beyond full-disk, compile SQLCipher and
124
+ point this package's connection at it — the store layer is plain `sqlite3` DB-API and
125
+ needs no code changes.
126
+
127
+ ## Limitations (v1, deliberate)
128
+
129
+ - Single-writer process per home directory (SQLite WAL semantics). The vector sidecar is
130
+ last-writer-wins: run one server per home; startup reconciliation after a crash between the DB and
131
+ the index file is deferred.
132
+ - The Python `sqlite3` module cannot register virtual tables, so SQL++-style TVFs arrive with the
133
+ native port; until then hybrid queries go through the tools.
134
+ - Hashing embedder matches surface forms, not paraphrases — install `fastembed` for real semantics.
135
+ Embedder identity is pinned per database; switching requires a fresh home.
136
+ - No QPS targets: this serves one agent session locally. Gates are interactive latency, cold start,
137
+ and RAM at scale (re-baselined empirically at true embedding dimensions, not SIFT-128 folklore).
@@ -0,0 +1,8 @@
1
+ """OmniDB: an MCP-native agent memory & retrieval server over SQLite."""
2
+
3
+ from .config import Config
4
+ from .store import MemoryStore, StoreError
5
+
6
+ __version__ = "0.1.0"
7
+
8
+ __all__ = ["Config", "MemoryStore", "StoreError", "__version__"]
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ sys.exit(main())
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from .config import Config
8
+ from .consolidate import run_consolidation
9
+ from .server import build_stack
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = argparse.ArgumentParser(prog="omnidb-server",
14
+ description="MCP-native agent memory & retrieval server over SQLite")
15
+ parser.add_argument("--home", help="data directory (default $OMNIDB_HOME or ~/.omnidb)")
16
+ # accept --home after the subcommand too; SUPPRESS keeps the subparser
17
+ # from overwriting a top-level value with its own default
18
+ common = argparse.ArgumentParser(add_help=False)
19
+ common.add_argument("--home", default=argparse.SUPPRESS)
20
+ sub = parser.add_subparsers(dest="command", required=True)
21
+ sub.add_parser("serve", parents=[common], help="run the MCP stdio server")
22
+ sub.add_parser("stats", parents=[common], help="print database statistics as JSON")
23
+ sub.add_parser("compact", parents=[common],
24
+ help="force a full vector-index rebuild (compaction / repair)")
25
+ p_consol = sub.add_parser("consolidate", parents=[common],
26
+ help="distill episodic memories into semantic ones")
27
+ p_consol.add_argument("--hours", type=float, default=24.0)
28
+ sub.add_parser("check", parents=[common],
29
+ help="run integrity checks (FTS, index drift, orphan edges)")
30
+ p_purge = sub.add_parser("purge", parents=[common],
31
+ help="hard-delete soft-deleted memories")
32
+ p_purge.add_argument("--vacuum", action="store_true",
33
+ help="reclaim disk space with VACUUM afterwards")
34
+ p_export = sub.add_parser("export", parents=[common],
35
+ help="dump live memories and edges to a JSONL file")
36
+ p_export.add_argument("path")
37
+ p_import = sub.add_parser("import", parents=[common],
38
+ help="restore a JSONL export into an empty home")
39
+ p_import.add_argument("path")
40
+
41
+ args = parser.parse_args(argv)
42
+ home = getattr(args, "home", None)
43
+
44
+ try:
45
+ if args.command == "serve":
46
+ from .server import serve
47
+ serve(home)
48
+ return 0
49
+
50
+ cfg = Config.load(home)
51
+ store = build_stack(cfg)
52
+ try:
53
+ if args.command == "stats":
54
+ print(json.dumps(store.stats(), indent=2))
55
+ elif args.command == "consolidate":
56
+ print(json.dumps(run_consolidation(store, args.hours), indent=2))
57
+ elif args.command == "compact":
58
+ print(json.dumps(store.rebuild_index(reason="manual"), indent=2))
59
+ elif args.command == "check":
60
+ print(json.dumps(store.check(), indent=2))
61
+ elif args.command == "purge":
62
+ print(json.dumps(store.purge(do_vacuum=args.vacuum), indent=2))
63
+ elif args.command == "export":
64
+ from .portability import export_jsonl
65
+
66
+ print(json.dumps(export_jsonl(store, args.path), indent=2))
67
+ elif args.command == "import":
68
+ from .portability import import_jsonl
69
+
70
+ print(json.dumps(import_jsonl(store, args.path), indent=2))
71
+ finally:
72
+ store.close()
73
+ except Exception as exc:
74
+ from .config import ConfigError
75
+ from .db import DbError
76
+ from .embeddings import EmbeddingError
77
+ from .store import StoreError
78
+ from .vector_index import VectorIndexError
79
+ if isinstance(exc, (StoreError, DbError, EmbeddingError,
80
+ VectorIndexError, ConfigError)):
81
+ print(f"omnidb-server: error: {exc}", file=sys.stderr)
82
+ return 2
83
+ raise
84
+ return 0
85
+
86
+
87
+ if __name__ == "__main__":
88
+ sys.exit(main())
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tomllib
5
+ from dataclasses import MISSING, dataclass, fields
6
+ from pathlib import Path
7
+
8
+ _TOML_NAME = "omnidb.toml"
9
+
10
+
11
+ class ConfigError(RuntimeError):
12
+ pass
13
+
14
+
15
+ def _env(name: str) -> str | None:
16
+ value = os.environ.get(name)
17
+ return value if value not in (None, "") else None
18
+
19
+
20
+ def _coerce(raw, template):
21
+ """Coerce a TOML/env scalar to the field's declared type."""
22
+ if template is None or isinstance(template, str):
23
+ return raw if isinstance(raw, str) else str(raw)
24
+ if isinstance(template, bool):
25
+ return str(raw).strip().lower() in ("1", "true", "yes", "on")
26
+ if isinstance(template, int):
27
+ return int(float(raw))
28
+ if isinstance(template, float):
29
+ return float(raw)
30
+ return raw
31
+
32
+
33
+ def _default_of(field):
34
+ return None if field.default is MISSING else field.default
35
+
36
+
37
+ def _read_toml(path: Path) -> dict:
38
+ with path.open("rb") as handle:
39
+ return tomllib.load(handle)
40
+
41
+
42
+ @dataclass
43
+ class Config:
44
+ home: Path
45
+ embedder: str = "auto" # auto | hashing | fastembed
46
+ vector_index: str = "auto" # auto | hnsw | brute
47
+ half_life_hours: float = 168.0
48
+ recency_weight: float = 0.15
49
+ freq_weight: float = 0.05
50
+ rrf_k: int = 60
51
+ k_fetch: int = 50
52
+ max_search_rows: int = 500
53
+ persist_every_ops: int = 64
54
+ persist_every_s: float = 0.25
55
+ dead_compact_ratio: float = 0.2
56
+
57
+ llm_base_url: str | None = None
58
+ llm_model: str | None = None
59
+ llm_api_key: str | None = None
60
+
61
+ @staticmethod
62
+ def load(home: str | Path | None = None) -> "Config":
63
+ """Precedence: CLI arg > environment > omnidb.toml > built-in defaults.
64
+
65
+ omnidb.toml is looked up in the CWD and in the resolved home directory
66
+ (the home copy wins). Field names in the file mirror the dataclass.
67
+ """
68
+ arg_home = Path(home) if home is not None else None
69
+ env_home_value = _env("OMNIDB_HOME")
70
+ env_home = Path(env_home_value) if env_home_value else None
71
+ resolved_home = arg_home or env_home or Path.home() / ".omnidb"
72
+
73
+ merged: dict = {}
74
+ cwd_toml = Path.cwd() / _TOML_NAME
75
+ if cwd_toml.is_file():
76
+ merged.update(_read_toml(cwd_toml))
77
+ # a bare "home" key in the CWD file can redirect the home directory
78
+ # when neither a CLI arg nor the environment pinned one
79
+ if not arg_home and not env_home and isinstance(merged.get("home"), str):
80
+ resolved_home = Path(merged["home"])
81
+ home_toml = resolved_home / _TOML_NAME
82
+ if home_toml != cwd_toml and home_toml.is_file():
83
+ merged.update(_read_toml(home_toml))
84
+
85
+ known = {f.name for f in fields(Config)}
86
+ unknown = set(merged) - known
87
+ if unknown:
88
+ raise ConfigError(f"unknown keys in {_TOML_NAME}: {sorted(unknown)}")
89
+
90
+ values: dict = {}
91
+ for field in fields(Config):
92
+ if field.name == "home":
93
+ continue
94
+ default = _default_of(field)
95
+ if field.name in merged:
96
+ values[field.name] = _coerce(merged[field.name], default)
97
+ env_value = _env("OMNIDB_" + field.name.upper())
98
+ if env_value is not None:
99
+ values[field.name] = _coerce(env_value, default)
100
+
101
+ values["home"] = arg_home or env_home or resolved_home
102
+ return Config(**values)
103
+
104
+ # backward-compatible alias used across tests/CLI
105
+ from_env = load