omnemo 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.
omnemo-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Guy Hutchins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
omnemo-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,104 @@
1
+ Metadata-Version: 2.4
2
+ Name: omnemo
3
+ Version: 0.1.0
4
+ Summary: Memory-only MCP server: save, recall, search, forget.
5
+ Author: Guy Hutchins
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/GuyMannDude/omnemo
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: mcp>=2
12
+ Requires-Dist: fastembed>=0.8
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Dynamic: license-file
16
+
17
+ # Omnemo — memory for Omarchy
18
+
19
+ **One shared memory for every agent harness on the box.**
20
+
21
+ Omarchy made coding agents system citizens. Omnemo gives them the one thing
22
+ they still wake up without: memory. Claude Code remembers what Codex did
23
+ yesterday. Your agent knows this app crashed before — and what fixed it.
24
+
25
+ Built just for [Omarchy](https://omarchy.org). Not a fork, not a patch — a
26
+ glove-fit layer made entirely from Omarchy's own extension points (skills,
27
+ user plugins, hooks), so it survives every `omarchy update`.
28
+
29
+ ## What you get
30
+
31
+ - **Shared recall across all harnesses** — one memory store, spoken over MCP,
32
+ registered with every agent CLI Omarchy ships. Save in one, recall in any.
33
+ - **Human-like memory, not a fixed database** — recall blends similarity,
34
+ recency, and importance; memories decay at different rates by kind, the way
35
+ yours do.
36
+ - **A memory skill** dropped into `~/.agents/skills/` — every harness learns
37
+ *when* to remember and *when* to recall, automatically.
38
+ - **Crashes with a past** — Omarchy hands core dumps to your agent; Omnemo
39
+ lets the agent answer "this crashed before, here's what fixed it."
40
+ - **Memory in the bar** — an Omarchy shell plugin showing what your machine
41
+ remembered today.
42
+
43
+ ## Install
44
+
45
+ > Fastest path: open your agent (`omarchy agent`) and say
46
+ > **"Install Omnemo — follow INSTALL.md."** It will do the rest.
47
+
48
+ Manual instructions: [`INSTALL.md`](./INSTALL.md).
49
+
50
+ ## Prove it
51
+
52
+ Memory is durable the instant a save returns — a crashing harness loses
53
+ nothing. Don't take our word for it:
54
+
55
+ ```sh
56
+ python3 examples/crash_demo.py
57
+ ```
58
+
59
+ It opens a real MCP session against `omnemo serve`, saves a memory, SIGKILLs
60
+ the server the moment the save is acknowledged (no shutdown, no flush), then
61
+ recalls the memory from a fresh process.
62
+
63
+ ### Cross-harness recall, live
64
+
65
+ The core promise — save in one harness, recall in any — is a two-command
66
+ demo on a box with two authenticated harnesses:
67
+
68
+ ```sh
69
+ # Ask Claude Code (Anthropic) to remember something
70
+ claude -p 'Use the omnemo save tool to store this memory: the electronics
71
+ repair shop favorite song was "Everything'\''s Broken" by Bob Dylan' \
72
+ --allowedTools "mcp__omnemo__save"
73
+
74
+ # Ask Codex (OpenAI) — a different vendor's harness, fresh process
75
+ codex exec 'Using only the omnemo MCP recall tool: what was the favorite
76
+ song at the electronics repair shop?'
77
+ ```
78
+
79
+ Run live on the reference Omarchy 4.0.1 box:
80
+
81
+ ```
82
+ mcp: omnemo/recall started
83
+ mcp: omnemo/recall (completed)
84
+ codex
85
+ "Everything's Broken" by Bob Dylan.
86
+ ```
87
+
88
+ One memory, written by one vendor's agent, recalled by its competitor.
89
+ The memory belongs to the machine, not the harness.
90
+
91
+ ## Status
92
+
93
+ **v0.1 glove, proven on a clean Omarchy 4.0.1 box.** Fresh install to green
94
+ in minutes: MCP registered with every shipping harness (verified read-back,
95
+ not just name-matched), skill linked, bar widget live — and everything
96
+ survives `omarchy update`, harness upgrades, theme switches, and a shell
97
+ restart with zero re-setup. See [`SPEC.md`](./SPEC.md) for the design.
98
+
99
+ ## Lineage
100
+
101
+ Omnemo is the memory core of [Mnemo Cortex](https://github.com/GuyMannDude/mnemo-cortex)
102
+ — "memory that works more like a human than the others" — rebuilt clean and
103
+ cut down to just memory, then fitted to Omarchy natively. MIT licensed, like
104
+ Omarchy itself.
omnemo-0.1.0/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Omnemo — memory for Omarchy
2
+
3
+ **One shared memory for every agent harness on the box.**
4
+
5
+ Omarchy made coding agents system citizens. Omnemo gives them the one thing
6
+ they still wake up without: memory. Claude Code remembers what Codex did
7
+ yesterday. Your agent knows this app crashed before — and what fixed it.
8
+
9
+ Built just for [Omarchy](https://omarchy.org). Not a fork, not a patch — a
10
+ glove-fit layer made entirely from Omarchy's own extension points (skills,
11
+ user plugins, hooks), so it survives every `omarchy update`.
12
+
13
+ ## What you get
14
+
15
+ - **Shared recall across all harnesses** — one memory store, spoken over MCP,
16
+ registered with every agent CLI Omarchy ships. Save in one, recall in any.
17
+ - **Human-like memory, not a fixed database** — recall blends similarity,
18
+ recency, and importance; memories decay at different rates by kind, the way
19
+ yours do.
20
+ - **A memory skill** dropped into `~/.agents/skills/` — every harness learns
21
+ *when* to remember and *when* to recall, automatically.
22
+ - **Crashes with a past** — Omarchy hands core dumps to your agent; Omnemo
23
+ lets the agent answer "this crashed before, here's what fixed it."
24
+ - **Memory in the bar** — an Omarchy shell plugin showing what your machine
25
+ remembered today.
26
+
27
+ ## Install
28
+
29
+ > Fastest path: open your agent (`omarchy agent`) and say
30
+ > **"Install Omnemo — follow INSTALL.md."** It will do the rest.
31
+
32
+ Manual instructions: [`INSTALL.md`](./INSTALL.md).
33
+
34
+ ## Prove it
35
+
36
+ Memory is durable the instant a save returns — a crashing harness loses
37
+ nothing. Don't take our word for it:
38
+
39
+ ```sh
40
+ python3 examples/crash_demo.py
41
+ ```
42
+
43
+ It opens a real MCP session against `omnemo serve`, saves a memory, SIGKILLs
44
+ the server the moment the save is acknowledged (no shutdown, no flush), then
45
+ recalls the memory from a fresh process.
46
+
47
+ ### Cross-harness recall, live
48
+
49
+ The core promise — save in one harness, recall in any — is a two-command
50
+ demo on a box with two authenticated harnesses:
51
+
52
+ ```sh
53
+ # Ask Claude Code (Anthropic) to remember something
54
+ claude -p 'Use the omnemo save tool to store this memory: the electronics
55
+ repair shop favorite song was "Everything'\''s Broken" by Bob Dylan' \
56
+ --allowedTools "mcp__omnemo__save"
57
+
58
+ # Ask Codex (OpenAI) — a different vendor's harness, fresh process
59
+ codex exec 'Using only the omnemo MCP recall tool: what was the favorite
60
+ song at the electronics repair shop?'
61
+ ```
62
+
63
+ Run live on the reference Omarchy 4.0.1 box:
64
+
65
+ ```
66
+ mcp: omnemo/recall started
67
+ mcp: omnemo/recall (completed)
68
+ codex
69
+ "Everything's Broken" by Bob Dylan.
70
+ ```
71
+
72
+ One memory, written by one vendor's agent, recalled by its competitor.
73
+ The memory belongs to the machine, not the harness.
74
+
75
+ ## Status
76
+
77
+ **v0.1 glove, proven on a clean Omarchy 4.0.1 box.** Fresh install to green
78
+ in minutes: MCP registered with every shipping harness (verified read-back,
79
+ not just name-matched), skill linked, bar widget live — and everything
80
+ survives `omarchy update`, harness upgrades, theme switches, and a shell
81
+ restart with zero re-setup. See [`SPEC.md`](./SPEC.md) for the design.
82
+
83
+ ## Lineage
84
+
85
+ Omnemo is the memory core of [Mnemo Cortex](https://github.com/GuyMannDude/mnemo-cortex)
86
+ — "memory that works more like a human than the others" — rebuilt clean and
87
+ cut down to just memory, then fitted to Omarchy natively. MIT licensed, like
88
+ Omarchy itself.
@@ -0,0 +1,3 @@
1
+ """Omnemo — memory-only MCP server: save, recall, search, forget."""
2
+
3
+ __version__ = "0.1.0.dev0"
@@ -0,0 +1,175 @@
1
+ """CLI: `omnemo serve` runs the MCP server; save/recall/search/forget are
2
+ direct verbs for testing; `omnemo stats` is the stats-only digest source.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import sys
9
+ from datetime import datetime
10
+
11
+ from . import __version__
12
+ from .config import ConfigError, load_config, store_path
13
+ from .embedder import make_embedder
14
+ from .store import CorruptStoreError, EmbedderMismatchError, Store
15
+
16
+ # Anything the store/config layer reports as a clean, named error.
17
+ _KNOWN_ERRORS = (ConfigError, CorruptStoreError, EmbedderMismatchError)
18
+
19
+
20
+ def _open_store() -> Store:
21
+ config = load_config()
22
+ embedder = make_embedder(config.embedder)
23
+ return Store(store_path(), embedder, config)
24
+
25
+
26
+ def _fmt_time(ts: float | None) -> str:
27
+ if ts is None:
28
+ return "never"
29
+ return datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = argparse.ArgumentParser(
34
+ prog="omnemo", description="Memory-only MCP server."
35
+ )
36
+ parser.add_argument("--version", action="version", version=__version__)
37
+ sub = parser.add_subparsers(dest="command", required=True)
38
+
39
+ sub.add_parser("serve", help="run the MCP server on stdio")
40
+
41
+ p_save = sub.add_parser("save", help="save one memory")
42
+ p_save.add_argument("text")
43
+ p_save.add_argument("--category", "-c", default=None)
44
+
45
+ p_recall = sub.add_parser("recall", help="recall memories by meaning")
46
+ p_recall.add_argument("query")
47
+ p_recall.add_argument("--limit", "-n", type=int, default=None)
48
+
49
+ p_search = sub.add_parser("search", help="search memories by substring")
50
+ p_search.add_argument("query")
51
+ p_search.add_argument("--limit", "-n", type=int, default=None)
52
+
53
+ p_forget = sub.add_parser("forget", help="permanently delete a memory by id")
54
+ p_forget.add_argument("id", type=int)
55
+
56
+ p_stats = sub.add_parser(
57
+ "stats", help="memory count, learned today, last recall"
58
+ )
59
+ p_stats.add_argument("--json", action="store_true", dest="as_json")
60
+
61
+ sub.add_parser("warm", help="pre-load the embedder (boot warm-up)")
62
+
63
+ sub.add_parser(
64
+ "setup",
65
+ help="install the Omarchy glove: skill, bar plugin, warm-up, connect",
66
+ )
67
+ sub.add_parser("connect", help="register the MCP server with every harness")
68
+ p_disc = sub.add_parser("disconnect", help="unregister from every harness")
69
+ p_disc.add_argument(
70
+ "--all-pieces", action="store_true",
71
+ help="also remove skill links, bar plugin, and warm-up unit",
72
+ )
73
+
74
+ args = parser.parse_args(argv)
75
+
76
+ if args.command in ("setup", "connect", "disconnect"):
77
+ from .omarchy import glove
78
+ from .omarchy.harnesses import connect_all, disconnect_all
79
+
80
+ if args.command == "setup":
81
+ lines, results = glove.setup()
82
+ elif args.command == "connect":
83
+ lines, results = [], connect_all()
84
+ elif getattr(args, "all_pieces", False):
85
+ lines, results = glove.teardown()
86
+ else:
87
+ lines, results = [], disconnect_all()
88
+ for line in lines:
89
+ print(line)
90
+ failed = False
91
+ for r in results:
92
+ mark = "ok " if r.ok else "FAIL"
93
+ detail = f" — {r.detail}" if r.detail else ""
94
+ print(f"[{mark}] {r.harness}: {r.action}{detail}")
95
+ failed = failed or not r.ok
96
+ return 1 if failed else 0
97
+
98
+ if args.command == "warm":
99
+ try:
100
+ config = load_config()
101
+ embedder = make_embedder(config.embedder)
102
+ embedder.embed(["omnemo warm-up"])
103
+ except _KNOWN_ERRORS as e:
104
+ print(f"error: {e}", file=sys.stderr)
105
+ return 1
106
+ print("embedder warm")
107
+ return 0
108
+
109
+ if args.command == "serve":
110
+ from .server import serve
111
+
112
+ try:
113
+ serve()
114
+ except _KNOWN_ERRORS as e:
115
+ print(f"error: {e}", file=sys.stderr)
116
+ return 1
117
+ return 0
118
+
119
+ try:
120
+ store = _open_store()
121
+ except _KNOWN_ERRORS as e:
122
+ print(f"error: {e}", file=sys.stderr)
123
+ return 1
124
+
125
+ try:
126
+ if args.command == "save":
127
+ try:
128
+ memory = store.save(args.text, args.category)
129
+ except ValueError as e:
130
+ print(f"error: {e}", file=sys.stderr)
131
+ return 1
132
+ print(f"saved #{memory.id} [{memory.category}]")
133
+
134
+ elif args.command == "recall":
135
+ results = store.recall(args.query, args.limit)
136
+ if not results:
137
+ print("no memories recalled")
138
+ for r in results:
139
+ print(
140
+ f"#{r.memory.id} [{r.memory.category}] "
141
+ f"score={r.score:.3f} sim={r.similarity:.3f} {r.memory.text}"
142
+ )
143
+
144
+ elif args.command == "search":
145
+ memories = store.search(args.query, args.limit)
146
+ if not memories:
147
+ print("no matches")
148
+ for m in memories:
149
+ print(f"#{m.id} [{m.category}] {m.text}")
150
+
151
+ elif args.command == "forget":
152
+ if store.forget(args.id):
153
+ print(f"forgot #{args.id}")
154
+ else:
155
+ print(f"error: no memory #{args.id}", file=sys.stderr)
156
+ return 1
157
+
158
+ elif args.command == "stats":
159
+ s = store.stats()
160
+ if args.as_json:
161
+ import json
162
+
163
+ print(json.dumps(s))
164
+ else:
165
+ print(f"memories: {s['memory_count']}")
166
+ print(f"learned today: {s['learned_today']}")
167
+ print(f"last recall: {_fmt_time(s['last_recall_at'])}")
168
+ finally:
169
+ store.close()
170
+
171
+ return 0
172
+
173
+
174
+ if __name__ == "__main__":
175
+ sys.exit(main())
@@ -0,0 +1,208 @@
1
+ """Configuration: exposed defaults, optionally overridden by a TOML file.
2
+
3
+ Every ranking weight, threshold, and category parameter lives here — never
4
+ hard-coded at the point of use — so recall can be tuned on real data
5
+ without touching code.
6
+
7
+ Config file: $XDG_CONFIG_HOME/omnemo/config.toml (default ~/.config/omnemo/).
8
+ Missing file or missing keys fall back to the shipped defaults below.
9
+ Invalid values fail at load time with a ConfigError naming the key —
10
+ hand-edited TOML is the supported tuning surface, so it must never crash
11
+ a verb with a raw traceback later.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import tomllib
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+
22
+ class ConfigError(Exception):
23
+ """config.toml contains an invalid value."""
24
+
25
+
26
+ def config_path() -> Path:
27
+ base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
28
+ return Path(base) / "omnemo" / "config.toml"
29
+
30
+
31
+ def data_dir() -> Path:
32
+ base = os.environ.get("XDG_DATA_HOME") or str(Path.home() / ".local" / "share")
33
+ return Path(base) / "omnemo"
34
+
35
+
36
+ def store_path() -> Path:
37
+ return data_dir() / "store.db"
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class Category:
42
+ """Per-category ranking parameters.
43
+
44
+ importance: additive weight in the composite recall score (0..1).
45
+ half_life_multiplier: scales the base recency half-life — memories in
46
+ a category with multiplier 2.0 fade half as fast as the base rate.
47
+ """
48
+
49
+ importance: float
50
+ half_life_multiplier: float
51
+
52
+
53
+ DEFAULT_CATEGORIES: dict[str, Category] = {
54
+ "fact": Category(importance=0.6, half_life_multiplier=4.0),
55
+ "decision": Category(importance=1.0, half_life_multiplier=6.0),
56
+ "preference": Category(importance=0.8, half_life_multiplier=8.0),
57
+ "incident": Category(importance=0.9, half_life_multiplier=2.0),
58
+ "transient": Category(importance=0.2, half_life_multiplier=0.25),
59
+ }
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Config:
64
+ # Embedder spec: "fastembed:<model-name>".
65
+ embedder: str = "fastembed:BAAI/bge-small-en-v1.5"
66
+
67
+ # Recall ranking — composite score:
68
+ # score = weight_similarity * cosine_similarity
69
+ # + weight_recency * 0.5 ** (age_days / (base_half_life_days * category.half_life_multiplier))
70
+ # + weight_importance * category.importance
71
+ # + weight_recall_count * min(recall_count, recall_count_cap) / recall_count_cap
72
+ # Candidates below min_similarity are dropped before scoring.
73
+ min_similarity: float = 0.35
74
+ weight_similarity: float = 1.0
75
+ weight_recency: float = 0.25
76
+ weight_importance: float = 0.15
77
+ weight_recall_count: float = 0.10
78
+ base_half_life_days: float = 30.0
79
+ recall_count_cap: int = 10
80
+
81
+ default_category: str = "fact"
82
+ recall_limit: int = 5
83
+ search_limit: int = 20
84
+
85
+ # Parameters used for memories whose saved category is no longer in
86
+ # the config (removed or renamed): recall degrades, never crashes.
87
+ fallback_importance: float = 0.5
88
+ fallback_half_life_multiplier: float = 1.0
89
+
90
+ categories: dict[str, Category] = field(
91
+ default_factory=lambda: dict(DEFAULT_CATEGORIES)
92
+ )
93
+
94
+
95
+ _SCALAR_TYPES: dict[str, type] = {
96
+ "embedder": str,
97
+ "min_similarity": float,
98
+ "weight_similarity": float,
99
+ "weight_recency": float,
100
+ "weight_importance": float,
101
+ "weight_recall_count": float,
102
+ "base_half_life_days": float,
103
+ "recall_count_cap": int,
104
+ "default_category": str,
105
+ "recall_limit": int,
106
+ "search_limit": int,
107
+ "fallback_importance": float,
108
+ "fallback_half_life_multiplier": float,
109
+ }
110
+
111
+
112
+ def _scalar(key: str, value: object) -> object:
113
+ expected = _SCALAR_TYPES[key]
114
+ is_bool = isinstance(value, bool)
115
+ if expected is float and isinstance(value, (int, float)) and not is_bool:
116
+ return float(value)
117
+ if expected is int and isinstance(value, int) and not is_bool:
118
+ return value
119
+ if expected is str and isinstance(value, str):
120
+ return value
121
+ kind = {float: "number", int: "integer", str: "string"}[expected]
122
+ raise ConfigError(f"{key} must be a {kind} (got {value!r})")
123
+
124
+
125
+ def _number(name: str, value: object) -> float:
126
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
127
+ return float(value)
128
+ raise ConfigError(f"{name} must be a number (got {value!r})")
129
+
130
+
131
+ def load_config(path: Path | None = None) -> Config:
132
+ """Load config from TOML, merging over the shipped defaults.
133
+
134
+ Raises ConfigError with the offending key named for any invalid value.
135
+ """
136
+ path = path if path is not None else config_path()
137
+ if not path.exists():
138
+ return Config()
139
+
140
+ with open(path, "rb") as f:
141
+ raw = tomllib.load(f)
142
+
143
+ kwargs: dict = {k: _scalar(k, raw[k]) for k in _SCALAR_TYPES if k in raw}
144
+
145
+ defaults = Config()
146
+ # Base for categories new to this file that omit a field.
147
+ new_base = Category(
148
+ importance=kwargs.get("fallback_importance", defaults.fallback_importance),
149
+ half_life_multiplier=kwargs.get(
150
+ "fallback_half_life_multiplier", defaults.fallback_half_life_multiplier
151
+ ),
152
+ )
153
+
154
+ raw_categories = raw.get("categories", {})
155
+ if not isinstance(raw_categories, dict):
156
+ raise ConfigError(f"categories must be a table (got {raw_categories!r})")
157
+ categories = dict(DEFAULT_CATEGORIES)
158
+ for name, params in raw_categories.items():
159
+ if not isinstance(params, dict):
160
+ raise ConfigError(f"categories.{name} must be a table (got {params!r})")
161
+ base = categories.get(name, new_base)
162
+ categories[name] = Category(
163
+ importance=_number(
164
+ f"categories.{name}.importance",
165
+ params.get("importance", base.importance),
166
+ ),
167
+ half_life_multiplier=_number(
168
+ f"categories.{name}.half_life_multiplier",
169
+ params.get("half_life_multiplier", base.half_life_multiplier),
170
+ ),
171
+ )
172
+ kwargs["categories"] = categories
173
+
174
+ config = Config(**kwargs)
175
+ _validate(config)
176
+ return config
177
+
178
+
179
+ def _validate(config: Config) -> None:
180
+ if config.base_half_life_days <= 0:
181
+ raise ConfigError(
182
+ f"base_half_life_days must be positive (got {config.base_half_life_days})"
183
+ )
184
+ if config.fallback_half_life_multiplier <= 0:
185
+ raise ConfigError(
186
+ "fallback_half_life_multiplier must be positive "
187
+ f"(got {config.fallback_half_life_multiplier})"
188
+ )
189
+ for name, cat in config.categories.items():
190
+ if cat.half_life_multiplier <= 0:
191
+ raise ConfigError(
192
+ f"categories.{name}.half_life_multiplier must be positive "
193
+ f"(got {cat.half_life_multiplier})"
194
+ )
195
+ if config.recall_count_cap < 1:
196
+ raise ConfigError(
197
+ f"recall_count_cap must be >= 1 (got {config.recall_count_cap})"
198
+ )
199
+ if config.recall_limit < 1:
200
+ raise ConfigError(f"recall_limit must be >= 1 (got {config.recall_limit})")
201
+ if config.search_limit < 1:
202
+ raise ConfigError(f"search_limit must be >= 1 (got {config.search_limit})")
203
+ if config.default_category not in config.categories:
204
+ known = ", ".join(sorted(config.categories))
205
+ raise ConfigError(
206
+ f"default_category {config.default_category!r} is not a "
207
+ f"configured category (known: {known})"
208
+ )
@@ -0,0 +1,83 @@
1
+ """Embedders: local only, one per store.
2
+
3
+ The store records which embedder produced its vectors and refuses to open
4
+ under a different one — mixing embedders fragments the vector space and
5
+ silently breaks recall.
6
+
7
+ No provider API keys are ever read from the environment; embedding never
8
+ leaves the machine (fastembed's one-time model download excepted).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Protocol
14
+
15
+
16
+ class Embedder(Protocol):
17
+ """A text embedder. `name` identifies it in store metadata; `dim` is the
18
+ vector length. Both must be stable for the life of a store."""
19
+
20
+ @property
21
+ def name(self) -> str: ...
22
+
23
+ @property
24
+ def dim(self) -> int: ...
25
+
26
+ def warm_up(self) -> None:
27
+ """Load the model and run one embedding so first real use is fast."""
28
+ ...
29
+
30
+ def embed(self, texts: list[str]) -> list[list[float]]: ...
31
+
32
+
33
+ class FastEmbedEmbedder:
34
+ """Local ONNX embedder via the fastembed package.
35
+
36
+ The model is downloaded once to fastembed's cache on first use; after
37
+ that everything runs offline.
38
+ """
39
+
40
+ def __init__(self, model_name: str = "BAAI/bge-small-en-v1.5") -> None:
41
+ self._model_name = model_name
42
+ self._model = None
43
+ self._dim: int | None = None
44
+
45
+ @property
46
+ def name(self) -> str:
47
+ return f"fastembed:{self._model_name}"
48
+
49
+ @property
50
+ def dim(self) -> int:
51
+ if self._dim is None:
52
+ # Known models: read the dimension from fastembed's registry so
53
+ # opening a store (which checks it) never forces a model load.
54
+ from fastembed import TextEmbedding
55
+
56
+ for entry in TextEmbedding.list_supported_models():
57
+ if entry["model"] == self._model_name:
58
+ self._dim = int(entry["dim"])
59
+ break
60
+ else:
61
+ self.warm_up()
62
+ assert self._dim is not None
63
+ return self._dim
64
+
65
+ def warm_up(self) -> None:
66
+ if self._model is None:
67
+ from fastembed import TextEmbedding # deferred: heavy import
68
+
69
+ self._model = TextEmbedding(model_name=self._model_name)
70
+ self._dim = len(next(iter(self._model.embed(["warm up"]))))
71
+
72
+ def embed(self, texts: list[str]) -> list[list[float]]:
73
+ self.warm_up()
74
+ assert self._model is not None
75
+ return [[float(x) for x in vec] for vec in self._model.embed(texts)]
76
+
77
+
78
+ def make_embedder(spec: str) -> Embedder:
79
+ """Build an embedder from a config spec string:
80
+ "fastembed:<model>" — local ONNX model (the default)."""
81
+ if spec.startswith("fastembed:"):
82
+ return FastEmbedEmbedder(spec.removeprefix("fastembed:"))
83
+ raise ValueError(f"unknown embedder spec: {spec!r}")
@@ -0,0 +1 @@
1
+ """The Omarchy glove: harness registration, skill links, bar plugin, warm-up."""