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.
- omni_memory/__init__.py +6 -0
- omni_memory/__main__.py +6 -0
- omni_memory/artifacts.py +57 -0
- omni_memory/branch.py +46 -0
- omni_memory/cleanup.py +80 -0
- omni_memory/cli.py +341 -0
- omni_memory/context.py +70 -0
- omni_memory/digest.py +62 -0
- omni_memory/gitmeta.py +137 -0
- omni_memory/graph/__init__.py +4 -0
- omni_memory/graphbuild.py +37 -0
- omni_memory/inject.py +50 -0
- omni_memory/install.py +88 -0
- omni_memory/llm.py +132 -0
- omni_memory/rank.py +130 -0
- omni_memory/serve.py +90 -0
- omni_memory/session_memory.py +141 -0
- omni_memory/skills/omni-memory/SKILL.md +60 -0
- omni_memory/staleness.py +69 -0
- omni_memory/static/index.html +224 -0
- omni_memory/store.py +244 -0
- omni_memory_agent-0.1.0.dist-info/METADATA +104 -0
- omni_memory_agent-0.1.0.dist-info/RECORD +27 -0
- omni_memory_agent-0.1.0.dist-info/WHEEL +5 -0
- omni_memory_agent-0.1.0.dist-info/entry_points.txt +2 -0
- omni_memory_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- omni_memory_agent-0.1.0.dist-info/top_level.txt +1 -0
omni_memory/__init__.py
ADDED
omni_memory/__main__.py
ADDED
omni_memory/artifacts.py
ADDED
|
@@ -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
|