omni-memory-agent 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,6 @@
1
+ """OmniMemory — the memory & context layer for coding agents.
2
+
3
+ Local-first, branch-aware, git-anchored, enforced.
4
+ """
5
+
6
+ __version__ = "0.1.0"
@@ -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,57 @@
1
+ """AI-written doc artifacts — api-map.md and linkup.md.
2
+
3
+ Modeled on the hand-written work/docs kb: an endpoint→service→repo API map and a
4
+ master cross-reference (Mermaid dependency graph + controller/service/repo maps +
5
+ Kafka contracts + shared DBs/code). These regenerate from the live codebase so
6
+ they never drift.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from . import context, llm
13
+ from .store import Store
14
+
15
+ APIMAP_PROMPT = """\
16
+ You are OmniMemory generating `api-map.md` for this repository. Output ONLY
17
+ GitHub-flavored Markdown (no preamble). Structure:
18
+ - A short header: base path / auth headers if evident.
19
+ - One section per controller/route group, each with a table:
20
+ | Method | Path | Request (DTO/params) | Flow: Controller -> Service -> Repository/downstream |
21
+ Be specific: real controller/service/repo class names, real paths, real DTOs,
22
+ real downstreams (DB, Kafka topic, external API). Note Kafka publishes as
23
+ `-> Kafka <topic>`. If a detail is inferred, append `(inferred)`. Base it strictly
24
+ on the code below — do not invent endpoints.
25
+ """
26
+
27
+ LINKUP_PROMPT = """\
28
+ You are OmniMemory generating `linkup.md`, the master cross-reference. Output ONLY
29
+ Markdown. Include, in order:
30
+ 1. `## Dependency graph` — a Mermaid ```mermaid graph LR``` of repos/services/DBs/
31
+ queues and how they call each other (REST, Kafka topics, shared DB).
32
+ 2. `## Controller -> Service -> Repository` — a table of the main call chains.
33
+ 3. `## Event contracts` — Kafka topics with producer(s) and consumer(s).
34
+ 4. `## Shared data & code` — shared DBs/collections and shared utility classes.
35
+ Use real names from the code below. Mark inferred items `(inferred)`. No preamble.
36
+ """
37
+
38
+
39
+ def _generate(root: Path, prompt: str) -> str:
40
+ ctx = context.gather(root)
41
+ return llm.complete(prompt, ctx).strip()
42
+
43
+
44
+ def generate_apimap(store: Store, root: Path) -> Path:
45
+ out = store.dir / "api-map.md"
46
+ out.write_text(_generate(root, APIMAP_PROMPT) + "\n")
47
+ return out
48
+
49
+
50
+ def generate_linkup(store: Store, root: Path) -> Path:
51
+ out = store.dir / "linkup.md"
52
+ out.write_text(_generate(root, LINKUP_PROMPT) + "\n")
53
+ return out
54
+
55
+
56
+ def generate_all(store: Store, root: Path) -> list[Path]:
57
+ return [generate_apimap(store, root), generate_linkup(store, root)]
omni_memory/branch.py ADDED
@@ -0,0 +1,46 @@
1
+ """Branch-aware memory: sync git topology into the store, resolve scope."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ from . import gitmeta
8
+ from .store import Store
9
+
10
+
11
+ def sync_git(store: Store, root: Path) -> dict:
12
+ """Pull the current git topology into the store. Returns the snapshot."""
13
+ snap = gitmeta.snapshot(root)
14
+ for b in snap["branches"]:
15
+ store.upsert_branch(**b)
16
+ for c in snap["commits"]:
17
+ store.upsert_commit(c["sha"], c["branch"], c["author"], c["date"],
18
+ c["message"], c["files"])
19
+ store.set_meta("default_branch", snap["default"])
20
+ from . import staleness
21
+ staleness.recompute(store, root)
22
+ return snap
23
+
24
+
25
+ def scope(store: Store, root: Path) -> tuple[str, Optional[str]]:
26
+ """(current_branch, base_branch|None) honoring the branch-aware toggle.
27
+
28
+ branch-aware ON → memory for current branch + its base.
29
+ branch-aware OFF → all branches (base returned as None, current as '*').
30
+ """
31
+ if not store.get_meta("branch_aware", True):
32
+ return "*", None
33
+ cur = gitmeta.current_branch(root)
34
+ base = store.get_meta("default_branch") or gitmeta.default_branch(root)
35
+ return cur, (base if base != cur else None)
36
+
37
+
38
+ def on_branch_change(store: Store, root: Path) -> None:
39
+ """Re-sync topology; flip merged branches' memories to 'merged' status."""
40
+ snap = sync_git(store, root)
41
+ for b in snap["branches"]:
42
+ if b["status"] == "merged":
43
+ store.db.execute(
44
+ "UPDATE memory SET status='merged' "
45
+ "WHERE branch=? AND status='active'", (b["name"],))
46
+ store.db.commit()
omni_memory/cleanup.py ADDED
@@ -0,0 +1,80 @@
1
+ """Extraction-noise filter — drop junk before it becomes a memory.
2
+
3
+ An LLM (or the doc/heuristic scanner) happily emits aspirational prose, headings,
4
+ and doc boilerplate that pollute the graph and the inject block. A *durable*
5
+ project memory names something concrete — a file, symbol, path, endpoint, table,
6
+ CamelCase type, or quoted term. Generic prose with no such anchor is noise.
7
+
8
+ Applied in `remember_many` for every capture path. Filtering is source-aware:
9
+ - ALL sources → length bounds + structural (markdown/heading/question) reject
10
+ - doc/heuristic → additionally require a concrete anchor (these scanners are
11
+ the noisy ones; the AI extractor is trusted for prose).
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import re
16
+
17
+ _MIN_CHARS = 12
18
+ _MAX_CHARS = 280
19
+ _PROSE_WORDS = 8 # a label with >= this many words reads like prose, not a name
20
+
21
+ # Sources whose output is untrusted prose and must carry a concrete anchor.
22
+ _STRICT_SOURCES = {"doc", "heuristic"}
23
+
24
+ # A memory is "anchored" if it points at something concrete. Any of:
25
+ _PATH = re.compile(r"[\w-]+/[\w./-]+") # src/foo/bar.py
26
+ _DOTTED = re.compile(r"\b\w+\.\w+") # module.func, Foo.bar
27
+ _SNAKE = re.compile(r"\b[a-z0-9]+_[a-z0-9_]+\b") # snake_case
28
+ _CAMEL = re.compile(r"\b[a-z]+[A-Z]\w+\b") # camelCase
29
+ _TYPE = re.compile(r"\b[A-Z][a-z0-9]+[A-Z]\w+\b") # CamelCase / PascalCase
30
+ _ALLCAPS = re.compile(r"\b[A-Z]{2,}\b") # KAFKA, HTTP, DB, API
31
+ _ENDPOINT = re.compile(r"(/api/|\b(GET|POST|PUT|PATCH|DELETE)\b|:\d{2,5}\b)")
32
+ _QUOTED = re.compile(r"[\"'`][^\"'`]{2,}[\"'`]") # "quoted" term
33
+ # A capitalized proper noun that is NOT at the start of the sentence.
34
+ _MIDCAP = re.compile(r"\S\s+[A-Z][a-zA-Z0-9]{2,}")
35
+
36
+ _ANCHORS = (_PATH, _DOTTED, _SNAKE, _CAMEL, _TYPE, _ALLCAPS, _ENDPOINT, _QUOTED,
37
+ _MIDCAP)
38
+
39
+ # Boilerplate / doc-scaffolding fragments that are never durable facts.
40
+ _BOILERPLATE = re.compile(
41
+ r"^(see |for more|e\.?g\.?|i\.?e\.?|note:|todo:?$|tbd|n/a|coming soon"
42
+ r"|as follows|for example|in this|this (section|doc|file|guide))",
43
+ re.I,
44
+ )
45
+
46
+
47
+ def _has_anchor(text: str) -> bool:
48
+ return any(rx.search(text) for rx in _ANCHORS)
49
+
50
+
51
+ def is_noise(text: str, files=None, symbols=None, source: str = "session") -> bool:
52
+ """True if this candidate memory should be dropped as extraction noise."""
53
+ t = (text or "").strip()
54
+ if len(t) < _MIN_CHARS or len(t) > _MAX_CHARS:
55
+ return True
56
+ # structural markdown / doc scaffolding
57
+ if t[0] in "#>|=" or t.startswith(("```", "---", "* ", "- ")) and len(t.split()) < 3:
58
+ return True
59
+ if t.endswith("?"): # questions aren't durable facts
60
+ return True
61
+ if _BOILERPLATE.match(t):
62
+ return True
63
+ if source not in _STRICT_SOURCES:
64
+ return False # trust AI/manual/session prose
65
+ # strict path: doc/heuristic prose must name something concrete
66
+ if files or symbols:
67
+ return False
68
+ return not _has_anchor(t)
69
+
70
+
71
+ def filter_items(items: list[dict], source: str = "session") -> tuple[list[dict], int]:
72
+ """Return (kept_items, dropped_count) after removing extraction noise."""
73
+ kept, dropped = [], 0
74
+ for it in items:
75
+ text = (it.get("text") or "").strip()
76
+ if is_noise(text, it.get("files"), it.get("symbols"), source):
77
+ dropped += 1
78
+ continue
79
+ kept.append(it)
80
+ return kept, dropped
omni_memory/cli.py ADDED
@@ -0,0 +1,341 @@
1
+ """`omni-memory` / `/omni-memory` command dispatch."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from . import __version__, branch as branchmod, digest, gitmeta, graphbuild, inject
9
+ from . import session_memory as sm
10
+ from .store import Store, find_project_root
11
+
12
+
13
+ def _store():
14
+ root = find_project_root()
15
+ return Store(root), root
16
+
17
+
18
+ def cmd_status(args):
19
+ s, root = _store()
20
+ branchmod.sync_git(s, root)
21
+ c = s.counts()
22
+ on = s.get_meta("enabled", True)
23
+ ba = s.get_meta("branch_aware", True)
24
+ cur = gitmeta.current_branch(root)
25
+ from . import llm
26
+ ai = llm.provider() or "none (set GEMINI_API_KEY)"
27
+ print(f"OmniMemory {__version__} · project: {root.name}")
28
+ print(f" layer: {'ON' if on else 'OFF'} branch-aware: {'ON' if ba else 'OFF'} AI: {ai}")
29
+ stale = s.db.execute(
30
+ "SELECT COUNT(*) n FROM memory WHERE status='active' AND stale=1").fetchone()["n"]
31
+ stale_note = f" ⚠ {stale} stale (run: omni-memory check)" if stale else ""
32
+ print(f" branch: {cur} memories: {c.get('active',0)} active, "
33
+ f"{c.get('merged',0)} merged, {c.get('superseded',0)} superseded")
34
+ print(f" store: {s.dir}{stale_note}")
35
+ return 0
36
+
37
+
38
+ def cmd_toggle(args):
39
+ s, _ = _store()
40
+ s.set_meta("enabled", args.cmd == "on")
41
+ print(f"OmniMemory {'enabled' if args.cmd == 'on' else 'disabled'}.")
42
+ return 0
43
+
44
+
45
+ def cmd_branch_aware(args):
46
+ s, _ = _store()
47
+ new = not s.get_meta("branch_aware", True)
48
+ s.set_meta("branch_aware", new)
49
+ print(f"branch-aware: {'ON' if new else 'OFF'}")
50
+ return 0
51
+
52
+
53
+ def cmd_remember(args):
54
+ s, root = _store()
55
+ m = sm.remember(s, root, " ".join(args.text), kind=args.kind, source="manual")
56
+ digest.write_digest(s)
57
+ print(f"[+] remembered [{m.id}] ({m.kind}, branch {m.branch}): {m.text}")
58
+ return 0
59
+
60
+
61
+ def cmd_capture(args):
62
+ """Ingest the agent's extraction JSON (stdin) → memories."""
63
+ s, root = _store()
64
+ raw = sys.stdin.read()
65
+ n = sm.capture_from_json(s, root, raw, source="session")
66
+ digest.write_digest(s)
67
+ print(f"[+] captured {n} mem (branch {gitmeta.current_branch(root)}); digest updated")
68
+ return 0
69
+
70
+
71
+ def cmd_digest(args):
72
+ s, _ = _store()
73
+ out = digest.write_digest(s)
74
+ print(f"[+] knowledge base → {out}")
75
+ return 0
76
+
77
+
78
+ def cmd_build(args):
79
+ """One-time bootstrap: AI-written facts from the repo + docs + graph."""
80
+ from . import context, llm
81
+ s, root = _store()
82
+ print(f"[*] building OmniMemory for {root.name} …")
83
+ branchmod.sync_git(s, root)
84
+ g = graphbuild.build_graph(s, root)
85
+ (s.dir / "graph.json").write_text(json.dumps(g, indent=2))
86
+
87
+ ai_added = 0
88
+ use_ai = llm.available() and not args.no_ai
89
+ if use_ai:
90
+ print(f"[*] AI pass via {llm.provider()} — reading the codebase …")
91
+ try:
92
+ ctx = context.gather(root)
93
+ items = llm.extract_memories(sm.BUILD_PROMPT, ctx)
94
+ ai_added = sm.remember_many(s, root, items, source="ai-build")
95
+ print(f"[+] AI wrote {ai_added} facts from the codebase")
96
+ from . import artifacts
97
+ for p in artifacts.generate_all(s, root):
98
+ print(f"[+] artifact → {p}")
99
+ except Exception as e: # noqa: BLE001
100
+ print(f"[!] AI pass failed ({e}); falling back to docs.")
101
+ use_ai = False
102
+
103
+ doc_added, scanned = (0, 0)
104
+ if not args.no_docs:
105
+ doc_added, scanned = sm.ingest_docs(s, root)
106
+ digest.write_digest(s)
107
+
108
+ print(f"[+] graph: {len(g['nodes'])} nodes · AI facts: {ai_added} · "
109
+ f"docs: {doc_added} from {scanned} file(s)")
110
+ print(f"[+] knowledge base → {s.dir / 'MEMORY.md'}")
111
+ if not use_ai and not args.no_ai:
112
+ print("\n[i] No model key set → only heuristic/doc facts. For real AI facts:")
113
+ print(" export GEMINI_API_KEY=... (then re-run: omni-memory build)")
114
+ return 0
115
+
116
+
117
+ def cmd_prompt(args):
118
+ print(sm.BUILD_PROMPT if args.which == "build" else sm.EXTRACTION_PROMPT)
119
+ return 0
120
+
121
+
122
+ def cmd_artifact(args):
123
+ """Generate the AI-written docs (api-map / linkup)."""
124
+ from . import artifacts, llm
125
+ if not llm.available():
126
+ print("[i] needs a model key. export GEMINI_API_KEY=... then retry.")
127
+ return 1
128
+ s, root = _store()
129
+ which = args.which
130
+ print(f"[*] generating {which} via {llm.provider()} …")
131
+ try:
132
+ if which in ("apimap", "all"):
133
+ print(f"[+] {artifacts.generate_apimap(s, root)}")
134
+ if which in ("linkup", "all"):
135
+ print(f"[+] {artifacts.generate_linkup(s, root)}")
136
+ except Exception as e: # noqa: BLE001
137
+ print(f"[!] failed: {e}")
138
+ return 1
139
+ return 0
140
+
141
+
142
+ def cmd_inject(args):
143
+ s, root = _store()
144
+ if not s.get_meta("enabled", True):
145
+ return 0
146
+ block = inject.build_block(s, root, query=" ".join(args.query or []),
147
+ files=args.file or None)
148
+ if block:
149
+ print(block)
150
+ return 0
151
+
152
+
153
+ def cmd_recall(args):
154
+ s, root = _store()
155
+ cur, base = branchmod.scope(s, root)
156
+ mems = s.memories(branch=None if cur == "*" else cur, base=base,
157
+ query=" ".join(args.query), limit=30)
158
+ if not mems:
159
+ print("not in memory.")
160
+ return 0
161
+ for m in mems:
162
+ where = (" · " + ", ".join(m["files"][:3])) if m["files"] else ""
163
+ stale = " ⚠STALE" if m.get("stale") else ""
164
+ print(f"[{m['id']}] {m['kind']} ({m['branch']}){stale}: {m['text']}{where}")
165
+ return 0
166
+
167
+
168
+ def cmd_branches(args):
169
+ s, root = _store()
170
+ branchmod.sync_git(s, root)
171
+ for b in s.branches():
172
+ star = "*" if b["name"] == gitmeta.current_branch(root) else " "
173
+ merged = f" → merged into {b['into_branch']}" if b["status"] == "merged" else ""
174
+ who = f" by {b['creator']}" if b.get("creator") else ""
175
+ print(f"{star} {b['name']:24} {b['status']}{merged}{who}")
176
+ return 0
177
+
178
+
179
+ def cmd_check(args):
180
+ """Re-anchor memories against git: flag any whose files changed since they
181
+ were written as ⚠ stale (they keep their content — re-verify, don't trust)."""
182
+ from . import staleness
183
+ s, root = _store()
184
+ r = staleness.recompute(s, root)
185
+ print(f"[+] checked {r['checked']} anchored memory · "
186
+ f"{r['stale']} stale · {r['cleared']} cleared")
187
+ if r["stale"]:
188
+ rows = s.db.execute(
189
+ "SELECT id, kind, text FROM memory "
190
+ "WHERE status='active' AND stale=1 ORDER BY stale_since DESC LIMIT 20"
191
+ ).fetchall()
192
+ for row in rows:
193
+ print(f" ⚠ [{row['id']}] {row['kind']}: {row['text'][:80]}")
194
+ return 0
195
+
196
+
197
+ def cmd_forget(args):
198
+ s, _ = _store()
199
+ ok = s.forget(args.id)
200
+ print("forgotten." if ok else "no such memory.")
201
+ return 0
202
+
203
+
204
+ def cmd_map(args):
205
+ s, root = _store()
206
+ branchmod.sync_git(s, root)
207
+ g = graphbuild.build_graph(s, root)
208
+ out = s.dir / "graph.json"
209
+ out.write_text(json.dumps(g, indent=2))
210
+ print(f"[+] graph: {len(g['nodes'])} nodes, {len(g['edges'])} edges → {out}")
211
+ print(" (next: augment with a tree-sitter AST code graph)")
212
+ return 0
213
+
214
+
215
+ def _read_transcript(path):
216
+ """Claude Code transcript is JSONL; flatten to 'role: text' for extraction."""
217
+ from pathlib import Path
218
+ if not path or not Path(path).exists():
219
+ return ""
220
+ out = []
221
+ for line in Path(path).read_text(errors="ignore").splitlines():
222
+ try:
223
+ obj = json.loads(line)
224
+ except Exception: # noqa: BLE001
225
+ continue
226
+ msg = obj.get("message") or obj
227
+ role = msg.get("role") or obj.get("type", "")
228
+ content = msg.get("content", "")
229
+ if isinstance(content, list):
230
+ content = " ".join(b.get("text", "") for b in content
231
+ if isinstance(b, dict) and b.get("type") == "text")
232
+ if isinstance(content, str) and content.strip():
233
+ out.append(f"{role}: {content.strip()}")
234
+ return "\n".join(out)[-140_000:]
235
+
236
+
237
+ def cmd_hook(args):
238
+ """Claude Code hook entrypoint (reads the event JSON on stdin)."""
239
+ try:
240
+ data = json.load(sys.stdin)
241
+ except Exception: # noqa: BLE001
242
+ data = {}
243
+ s, root = _store()
244
+ if not s.get_meta("enabled", True):
245
+ return 0
246
+ if args.event == "inject": # UserPromptSubmit → add memory to context
247
+ block = inject.build_block(s, root, query=data.get("prompt", ""))
248
+ if block:
249
+ print(block) # stdout is added to the prompt context
250
+ return 0
251
+ if args.event == "capture": # SessionEnd/Stop → extract + store
252
+ text = _read_transcript(data.get("transcript_path"))
253
+ if text:
254
+ n = sm.capture_from_json(s, root, text, source="session")
255
+ digest.write_digest(s)
256
+ print(f"omni-memory: captured {n} memories", file=sys.stderr)
257
+ return 0
258
+ return 0
259
+
260
+
261
+ def cmd_key(args):
262
+ """Store an API key securely (~/.omni-memory/credentials.json, chmod 600)."""
263
+ import getpass
264
+ import json as _json
265
+ import os as _os
266
+ from . import llm
267
+ env = {"gemini": "GEMINI_API_KEY", "anthropic": "ANTHROPIC_API_KEY",
268
+ "openai": "OPENAI_API_KEY"}[args.provider]
269
+ val = getpass.getpass(f"Paste {args.provider} API key (hidden): ").strip()
270
+ if not val:
271
+ print("nothing entered.")
272
+ return 1
273
+ llm.CREDS.parent.mkdir(parents=True, exist_ok=True)
274
+ creds = {}
275
+ if llm.CREDS.exists():
276
+ try:
277
+ creds = _json.loads(llm.CREDS.read_text())
278
+ except Exception: # noqa: BLE001
279
+ creds = {}
280
+ creds[env] = val
281
+ llm.CREDS.write_text(_json.dumps(creds, indent=2))
282
+ _os.chmod(llm.CREDS, 0o600)
283
+ print(f"[+] saved {args.provider} key → {llm.CREDS} (chmod 600, gitignored)")
284
+ return 0
285
+
286
+
287
+ def cmd_ui(args):
288
+ from . import serve
289
+ return serve.run_ui(port=args.port)
290
+
291
+
292
+ def cmd_install(args):
293
+ from . import install
294
+ return install.install(platform=args.platform)
295
+
296
+
297
+ def main(argv=None):
298
+ import argparse
299
+ p = argparse.ArgumentParser(prog="omni-memory",
300
+ description="Memory & context layer for coding agents.")
301
+ p.add_argument("--version", action="version", version=f"omni-memory {__version__}")
302
+ sub = p.add_subparsers(dest="cmd")
303
+
304
+ sub.add_parser("status")
305
+ sub.add_parser("on"); sub.add_parser("off")
306
+ sub.add_parser("branch-aware")
307
+ r = sub.add_parser("remember"); r.add_argument("--kind", default="fact"); r.add_argument("text", nargs="+")
308
+ sub.add_parser("capture")
309
+ ij = sub.add_parser("inject"); ij.add_argument("query", nargs="*"); ij.add_argument("--file", action="append")
310
+ rc = sub.add_parser("recall"); rc.add_argument("query", nargs="+")
311
+ sub.add_parser("branches")
312
+ fg = sub.add_parser("forget"); fg.add_argument("id")
313
+ sub.add_parser("map")
314
+ sub.add_parser("check")
315
+ sub.add_parser("digest")
316
+ bd = sub.add_parser("build")
317
+ bd.add_argument("--no-docs", action="store_true")
318
+ bd.add_argument("--no-ai", action="store_true", help="skip the model pass")
319
+ pr = sub.add_parser("prompt"); pr.add_argument("which", nargs="?", default="build",
320
+ choices=["build", "session"])
321
+ ar = sub.add_parser("artifact"); ar.add_argument("which", nargs="?", default="all",
322
+ choices=["apimap", "linkup", "all"])
323
+ hk = sub.add_parser("hook"); hk.add_argument("event", choices=["inject", "capture"])
324
+ ky = sub.add_parser("key"); ky.add_argument("provider", choices=["gemini", "anthropic", "openai"])
325
+ ui = sub.add_parser("ui"); ui.add_argument("--port", type=int, default=7777)
326
+ ins = sub.add_parser("install"); ins.add_argument("--platform", default="claude-code")
327
+
328
+ args = p.parse_args(argv)
329
+ dispatch = {
330
+ None: cmd_status, "status": cmd_status, "on": cmd_toggle, "off": cmd_toggle,
331
+ "branch-aware": cmd_branch_aware, "remember": cmd_remember, "capture": cmd_capture,
332
+ "inject": cmd_inject, "recall": cmd_recall, "branches": cmd_branches,
333
+ "forget": cmd_forget, "map": cmd_map, "check": cmd_check, "digest": cmd_digest,
334
+ "build": cmd_build, "prompt": cmd_prompt, "artifact": cmd_artifact,
335
+ "key": cmd_key, "hook": cmd_hook, "ui": cmd_ui, "install": cmd_install,
336
+ }
337
+ return dispatch[args.cmd](args)
338
+
339
+
340
+ if __name__ == "__main__":
341
+ sys.exit(main())
omni_memory/context.py ADDED
@@ -0,0 +1,70 @@
1
+ """Assemble a compact, high-signal repo snapshot to feed the model on `build`.
2
+
3
+ Prioritizes the files that reveal architecture: manifests, docs, entrypoints,
4
+ controllers/routes/services/models/config. Truncated to a char budget (a later
5
+ phase adds per-area chunking via the AST code graph for big monorepos).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ from pathlib import Path
11
+
12
+ _PRIORITY = (
13
+ "readme", "package.json", "pom.xml", "build.gradle", "requirements",
14
+ "pyproject", "go.mod", "cargo.toml", "dockerfile", "docker-compose",
15
+ "application.yml", "application.properties", ".env.example",
16
+ )
17
+ _DIR_HINTS = ("controller", "route", "service", "handler", "model", "entity",
18
+ "repository", "schema", "config", "kafka", "consumer", "producer",
19
+ "api", "endpoint", "main", "app", "index")
20
+ _CODE_EXT = (".py", ".ts", ".js", ".java", ".go", ".rb", ".kt", ".rs", ".md",
21
+ ".yml", ".yaml", ".sql")
22
+
23
+
24
+ def _tracked_files(root: Path) -> list[str]:
25
+ try:
26
+ r = subprocess.run(["git", "-C", str(root), "ls-files"],
27
+ capture_output=True, text=True, timeout=20)
28
+ if r.returncode == 0:
29
+ return [f for f in r.stdout.splitlines() if f]
30
+ except Exception: # noqa: BLE001
31
+ pass
32
+ return [str(p.relative_to(root)) for p in root.rglob("*")
33
+ if p.is_file() and ".git" not in p.parts][:5000]
34
+
35
+
36
+ def _score(path: str) -> int:
37
+ low = path.lower()
38
+ s = 0
39
+ if any(k in low for k in _PRIORITY):
40
+ s += 100
41
+ if low.endswith(".md"):
42
+ s += 40
43
+ if any(h in low for h in _DIR_HINTS):
44
+ s += 20
45
+ if low.endswith(_CODE_EXT):
46
+ s += 5
47
+ s -= low.count("/") * 2 # prefer shallower files
48
+ return s
49
+
50
+
51
+ def gather(root: Path, max_chars: int = 140_000, per_file: int = 6_000) -> str:
52
+ files = _tracked_files(root)
53
+ tree = "\n".join(files[:1000])
54
+ parts = [f"# REPO: {root.name}\n# FILE TREE ({len(files)} files)\n{tree}\n"]
55
+ budget = max_chars - len(parts[0])
56
+ for f in sorted(files, key=_score, reverse=True):
57
+ if budget <= 0:
58
+ break
59
+ if not f.lower().endswith(_CODE_EXT):
60
+ continue
61
+ try:
62
+ text = (root / f).read_text(errors="ignore")[:per_file]
63
+ except Exception: # noqa: BLE001
64
+ continue
65
+ chunk = f"\n\n# FILE: {f}\n{text}"
66
+ if len(chunk) > budget:
67
+ chunk = chunk[:budget]
68
+ parts.append(chunk)
69
+ budget -= len(chunk)
70
+ return "".join(parts)
omni_memory/digest.py ADDED
@@ -0,0 +1,62 @@
1
+ """Digest — render the store into a human-readable MEMORY.md "knowledge base".
2
+
3
+ This is the persistent artifact: always-loadable docs for the agent (Antigravity
4
+ artifact / Claude Code @-reference) and browsable by humans. Regenerated on every
5
+ capture so it never drifts from the store.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from pathlib import Path
11
+
12
+ from .store import Store
13
+
14
+ DIGEST_NAME = "MEMORY.md"
15
+ # Section order + headings modeled on work/docs README reading-order taxonomy.
16
+ _ORDER = ["decision", "concept", "flow", "event", "endpoint", "db",
17
+ "component", "gotcha", "assumption", "fact", "todo"]
18
+ _HEADINGS = {
19
+ "decision": "Architecture & Decisions", "concept": "Concepts / Glossary",
20
+ "flow": "Flows", "event": "Events (Kafka contracts)",
21
+ "endpoint": "API map (endpoint → service → repo)", "db": "Database",
22
+ "component": "Reusable components", "gotcha": "Gotchas / Known issues",
23
+ "assumption": "Assumptions (verify)", "fact": "Facts", "todo": "TODOs",
24
+ }
25
+
26
+
27
+ def write_digest(store: Store) -> Path:
28
+ mems = store.memories(status="active", limit=2000)
29
+ by_branch: dict[str, dict[str, list[dict]]] = {}
30
+ for m in mems:
31
+ by_branch.setdefault(m["branch"], {}).setdefault(m["kind"], []).append(m)
32
+
33
+ lines = [
34
+ "# Project Memory (OmniMemory)",
35
+ "",
36
+ f"_Auto-generated {time.strftime('%Y-%m-%d %H:%M')} · {len(mems)} active "
37
+ "memories · do not edit by hand; use `omni-memory remember/forget`._",
38
+ "",
39
+ "> Agents: treat this as verified project truth. Cite `[id]`s you use. "
40
+ "If something isn't here or in the code, say \"not in memory\" — don't invent.",
41
+ "",
42
+ ]
43
+ default = store.get_meta("default_branch", "main")
44
+ for branch in sorted(by_branch, key=lambda b: (b != default, b)):
45
+ merged = store.db.execute(
46
+ "SELECT status FROM branches WHERE name=?", (branch,)).fetchone()
47
+ tag = f" _(merged)_" if merged and merged["status"] == "merged" else ""
48
+ lines.append(f"## `{branch}`{tag}")
49
+ for kind in _ORDER:
50
+ items = by_branch[branch].get(kind)
51
+ if not items:
52
+ continue
53
+ lines.append(f"\n### {_HEADINGS.get(kind, kind.title())}")
54
+ for m in items:
55
+ where = f" — `{'`, `'.join(m['files'][:3])}`" if m["files"] else ""
56
+ flag = " _(inferred)_" if m.get("source") == "inferred" else ""
57
+ lines.append(f"- {m['text']}{flag} `[{m['id']}]`{where}")
58
+ lines.append("")
59
+ text = "\n".join(lines)
60
+ out = store.dir / DIGEST_NAME
61
+ out.write_text(text)
62
+ return out