pantheon-opencode 1.0.0
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.
- package/AGENTS.md +37 -0
- package/LICENSE +21 -0
- package/README.md +1013 -0
- package/bin/pantheon-init.mjs +183 -0
- package/commands/pantheon-audit.md +25 -0
- package/commands/pantheon-bg.md +10 -0
- package/commands/pantheon-consolidate.md +11 -0
- package/commands/pantheon-deepwork.md +128 -0
- package/commands/pantheon-doc.md +10 -0
- package/commands/pantheon-focus.md +9 -0
- package/commands/pantheon-forget.md +58 -0
- package/commands/pantheon-hash.md +11 -0
- package/commands/pantheon-optimize.md +79 -0
- package/commands/pantheon-remember.md +44 -0
- package/commands/pantheon-search.md +48 -0
- package/commands/pantheon-status.md +71 -0
- package/commands/pantheon-todo.md +11 -0
- package/commands/pantheon.md +49 -0
- package/docs/AGENT-MCP.md +194 -0
- package/docs/ARCHITECTURE.md +384 -0
- package/docs/BRANCH-PROTECTION.md +142 -0
- package/docs/INDEX.md +81 -0
- package/docs/INSTALLATION.md +217 -0
- package/docs/MCP.md +238 -0
- package/docs/MEMORY.md +471 -0
- package/docs/MIGRATION-MEMORY-BANK.md +139 -0
- package/docs/PLATFORMS.md +5 -0
- package/docs/QUICKSTART.md +49 -0
- package/docs/README.md +18 -0
- package/docs/RELEASING.md +256 -0
- package/docs/SETUP.md +5 -0
- package/docs/UPGRADING.md +41 -0
- package/docs/mcp-recommendations.md +439 -0
- package/docs/mcp-tools.md +156 -0
- package/docs/mcp-user-guide.md +204 -0
- package/docs/persistence-mcp.md +111 -0
- package/package.json +72 -0
- package/pantheon.schema.json +158 -0
- package/scripts/__init__.py +0 -0
- package/scripts/_pantheon_paths.py +68 -0
- package/scripts/check-version-consistency.sh +23 -0
- package/scripts/code_mode_server.py +202 -0
- package/scripts/doctor.mjs +763 -0
- package/scripts/generate-prompts.sh +222 -0
- package/scripts/generate-routing-docs.mjs +104 -0
- package/scripts/hash_verify.py +192 -0
- package/scripts/init-pantheon-mcp.sh +118 -0
- package/scripts/install/agents-md.mjs +214 -0
- package/scripts/install/health-check.mjs +196 -0
- package/scripts/install/migrate.mjs +209 -0
- package/scripts/install/opencode.mjs +645 -0
- package/scripts/install/shared.mjs +655 -0
- package/scripts/install/venv.mjs +116 -0
- package/scripts/install-mcp.mjs +885 -0
- package/scripts/install.mjs +26 -0
- package/scripts/manifest.mjs +622 -0
- package/scripts/mcp_persistence_server.py +459 -0
- package/scripts/mcp_resources_server.py +206 -0
- package/scripts/memory_cache.py +78 -0
- package/scripts/memory_mcp_server.py +605 -0
- package/scripts/paths.py +64 -0
- package/scripts/prune_context.py +72 -0
- package/scripts/release-bundle.mjs +109 -0
- package/scripts/scrub-secrets.py +282 -0
- package/scripts/scrub_secrets.py +281 -0
- package/scripts/test-context-compression.sh +166 -0
- package/scripts/themis_heuristic_scan.py +287 -0
- package/scripts/todo_enforcer.py +242 -0
- package/scripts/uninstall.mjs +1057 -0
- package/scripts/validate-routing.mjs +160 -0
- package/scripts/validate_agent_frontmatter.py +226 -0
- package/scripts/versioning.mjs +254 -0
- package/skills-lock.json +16 -0
- package/src/agents/aphrodite.md +162 -0
- package/src/agents/apollo.md +109 -0
- package/src/agents/athena.md +226 -0
- package/src/agents/demeter.md +146 -0
- package/src/agents/gaia.md +82 -0
- package/src/agents/hephaestus.md +105 -0
- package/src/agents/hermes.md +302 -0
- package/src/agents/iris.md +99 -0
- package/src/agents/mnemosyne.md +226 -0
- package/src/agents/nyx.md +87 -0
- package/src/agents/prometheus.md +199 -0
- package/src/agents/talos.md +93 -0
- package/src/agents/themis.md +187 -0
- package/src/agents/zeus.md +209 -0
- package/src/instructions/agent-return-format.instructions.md +26 -0
- package/src/instructions/backend-standards.instructions.md +45 -0
- package/src/instructions/documentation-standards.instructions.md +53 -0
- package/src/instructions/frontend-standards.instructions.md +46 -0
- package/src/instructions/memory-protocol.instructions.md +67 -0
- package/src/instructions/yagni.instructions.md +21 -0
- package/src/instructions/zeus-anti-stall.instructions.md +72 -0
- package/src/instructions/zeus-communication-rules.instructions.md +15 -0
- package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
- package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
- package/src/mcp/_pantheon_paths.py +67 -0
- package/src/mcp/code_mode_server.py +202 -0
- package/src/mcp/init-pantheon-mcp.sh +118 -0
- package/src/mcp/install-mcp.mjs +885 -0
- package/src/mcp/mcp_persistence_server.py +458 -0
- package/src/mcp/mcp_resources_server.py +205 -0
- package/src/mcp/memory_mcp_server.py +604 -0
- package/src/mcp/requirements-mcp-core.txt +5 -0
- package/src/mcp/requirements-mcp.txt +5 -0
- package/src/plugin.ts +19 -0
- package/src/plugins/tui/dist/tui.js +143 -0
- package/src/plugins/tui/dist/tui.tsx +144 -0
- package/src/plugins/tui/package.json +32 -0
- package/src/plugins/tui/src/index.tsx +144 -0
- package/src/plugins/tui/tsdown.config.ts +22 -0
- package/src/routing.yml +499 -0
- package/src/skills/README.md +230 -0
- package/src/skills/agent-coordination/SKILL.md +95 -0
- package/src/skills/artifact-management/SKILL.md +118 -0
- package/src/skills/auto-continue/SKILL.md +280 -0
- package/src/skills/code-review-checklist/SKILL.md +139 -0
- package/src/skills/context-compression/SKILL.md +861 -0
- package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
- package/src/skills/incremental-implementation/SKILL.md +27 -0
- package/src/skills/memory-bank/SKILL.md +165 -0
- package/src/skills/orchestration-workflow/SKILL.md +311 -0
- package/src/skills/security-hardening/SKILL.md +36 -0
- package/src/skills/session-goal/SKILL.md +138 -0
- package/src/skills/spec-driven-development/SKILL.md +23 -0
- package/src/skills/tdd-with-agents/SKILL.md +170 -0
- package/src/skills/visual-review-pipeline/SKILL.md +200 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Pantheon Memory MCP Server — lightweight, zero heavy deps.
|
|
3
|
+
|
|
4
|
+
Uses sqlite-vec (vector extension) + fastembed (ONNX, no PyTorch)
|
|
5
|
+
for semantic memory with hybrid search (vector cosine + FTS5 BM25).
|
|
6
|
+
|
|
7
|
+
Total footprint: ~50MB (sqlite-vec + fastembed) vs ~1.4GB (old chromadb).
|
|
8
|
+
|
|
9
|
+
Tools:
|
|
10
|
+
memory_store — Store with automatic embedding
|
|
11
|
+
memory_search — Hybrid vector + FTS5 keyword search
|
|
12
|
+
memory_recall — Exact recall by key
|
|
13
|
+
memory_forget — Delete by ID or key
|
|
14
|
+
memory_list — Chronological listing
|
|
15
|
+
memory_stats — DB statistics
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
python scripts/memory_mcp_server.py
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import functools
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sqlite3
|
|
27
|
+
import time
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import sqlite_vec
|
|
32
|
+
from _pantheon_paths import pantheon_home
|
|
33
|
+
from fastembed import TextEmbedding
|
|
34
|
+
from mcp.server.fastmcp import FastMCP
|
|
35
|
+
|
|
36
|
+
# ── Paths ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
DB_PATH = pantheon_home() / "memory" / "memory.db"
|
|
39
|
+
EMBED_CACHE = Path.home() / ".cache" / "fastembed"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_BYTE_UNIT = 1024
|
|
43
|
+
|
|
44
|
+
def _set_memory_dir(path: str | Path) -> None:
|
|
45
|
+
"""Override the memory db path for testing."""
|
|
46
|
+
global DB_PATH # noqa: PLW0603
|
|
47
|
+
DB_PATH = Path(path) / "memory.db"
|
|
48
|
+
_reset_test_state()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _reset_test_state() -> None:
|
|
52
|
+
"""Reset cached state for test isolation."""
|
|
53
|
+
_get_db.cache_clear()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
SCHEMA_SQL = """
|
|
57
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
namespace TEXT NOT NULL DEFAULT 'default',
|
|
60
|
+
key TEXT,
|
|
61
|
+
value TEXT NOT NULL,
|
|
62
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
63
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
64
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_memories_ns_key
|
|
68
|
+
ON memories(namespace, key);
|
|
69
|
+
|
|
70
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS vec_memories USING vec0(
|
|
71
|
+
id INTEGER PRIMARY KEY,
|
|
72
|
+
embedding float[384]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
76
|
+
namespace, key, value, metadata,
|
|
77
|
+
content='memories', content_rowid='id',
|
|
78
|
+
tokenize='porter unicode61'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
-- FTS sync: INSERT
|
|
82
|
+
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
83
|
+
INSERT INTO memories_fts(rowid, namespace, key, value, metadata)
|
|
84
|
+
VALUES (new.id, new.namespace, new.key, new.value, new.metadata);
|
|
85
|
+
END;
|
|
86
|
+
|
|
87
|
+
-- FTS sync: DELETE
|
|
88
|
+
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
89
|
+
INSERT INTO memories_fts(memories_fts, rowid, namespace, key, value, metadata)
|
|
90
|
+
VALUES ('delete', old.id, old.namespace, old.key, old.value, old.metadata);
|
|
91
|
+
END;
|
|
92
|
+
|
|
93
|
+
-- FTS sync: UPDATE
|
|
94
|
+
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
95
|
+
INSERT INTO memories_fts(memories_fts, rowid, namespace, key, value, metadata)
|
|
96
|
+
VALUES ('delete', old.id, old.namespace, old.key, old.value, old.metadata);
|
|
97
|
+
INSERT INTO memories_fts(rowid, namespace, key, value, metadata)
|
|
98
|
+
VALUES (new.id, new.namespace, new.key, new.value, new.metadata);
|
|
99
|
+
END;
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
# ── FastMCP App ───────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
mcp = FastMCP(
|
|
105
|
+
"pantheon-memory",
|
|
106
|
+
instructions="Lightweight semantic memory with sqlite-vec + fastembed",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
# ── Database ──────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@functools.cache
|
|
114
|
+
def _get_db() -> sqlite3.Connection:
|
|
115
|
+
"""Get or create the SQLite connection singleton.
|
|
116
|
+
|
|
117
|
+
Creates DB directory, applies WAL/performance pragmas, registers
|
|
118
|
+
the sqlite-vec extension, and runs schema init.
|
|
119
|
+
"""
|
|
120
|
+
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
conn = sqlite3.connect(str(DB_PATH), check_same_thread=False)
|
|
122
|
+
conn.row_factory = sqlite3.Row
|
|
123
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
124
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
125
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
126
|
+
# Register sqlite-vec extension
|
|
127
|
+
|
|
128
|
+
sqlite_vec.load(conn)
|
|
129
|
+
conn.executescript(SCHEMA_SQL)
|
|
130
|
+
return conn
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_conn() -> sqlite3.Connection:
|
|
134
|
+
"""Alias for _get_db — shorthand for tool use."""
|
|
135
|
+
return _get_db()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ── Embedding ─────────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@functools.cache
|
|
142
|
+
def _get_embedder() -> TextEmbedding:
|
|
143
|
+
"""Lazy-load fastembed model (auto-downloads on first call, ~30MB).
|
|
144
|
+
|
|
145
|
+
Uses BAAI/bge-small-en-v1.5 (384-dim, CPU, ONNX) — no PyTorch needed.
|
|
146
|
+
Model is cached in ~/.cache/fastembed/.
|
|
147
|
+
"""
|
|
148
|
+
os.environ.setdefault("TQDM_DISABLE", "1")
|
|
149
|
+
return TextEmbedding(
|
|
150
|
+
model_name="BAAI/bge-small-en-v1.5",
|
|
151
|
+
cache_dir=str(EMBED_CACHE),
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _embed(text: str) -> list[float]:
|
|
156
|
+
"""Generate a 384-dim embedding vector for the given text."""
|
|
157
|
+
return next(iter(_get_embedder().embed(text))).tolist()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _dict_from_row(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
|
164
|
+
"""Convert a sqlite3.Row to a plain dict, or return None."""
|
|
165
|
+
if row is None:
|
|
166
|
+
return None
|
|
167
|
+
return dict(row)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _now_iso() -> str:
|
|
171
|
+
"""Return current UTC time as ISO 8601 string."""
|
|
172
|
+
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_metadata(row: dict[str, Any]) -> dict[str, Any]:
|
|
176
|
+
"""Parse the metadata JSON field of a memory row."""
|
|
177
|
+
try:
|
|
178
|
+
row["metadata"] = json.loads(row["metadata"])
|
|
179
|
+
except (json.JSONDecodeError, TypeError):
|
|
180
|
+
row["metadata"] = {}
|
|
181
|
+
return row
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# ── RRF Fusion ────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
_RRF_CONST = 60
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _rrf_fuse(
|
|
190
|
+
vec_ids: list[int],
|
|
191
|
+
fts_ids: list[int],
|
|
192
|
+
top_k: int,
|
|
193
|
+
) -> list[tuple[int, float]]:
|
|
194
|
+
"""Reciprocal Rank Fusion of vector and FTS5 result ID lists.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
vec_ids: Ordered list of IDs from vector search (most relevant first).
|
|
198
|
+
fts_ids: Ordered list of IDs from FTS5 search (most relevant first).
|
|
199
|
+
top_k: Maximum number of fused results to return.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of (id, rrf_score) tuples sorted by descending score.
|
|
203
|
+
"""
|
|
204
|
+
scores: dict[int, float] = {}
|
|
205
|
+
for rank, doc_id in enumerate(vec_ids, start=1):
|
|
206
|
+
scores[doc_id] = 1.0 / (_RRF_CONST + rank)
|
|
207
|
+
for rank, doc_id in enumerate(fts_ids, start=1):
|
|
208
|
+
scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (_RRF_CONST + rank)
|
|
209
|
+
ranked = sorted(scores.items(), key=lambda x: (-x[1], x[0]))
|
|
210
|
+
return ranked[:top_k]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# ── Tools ─────────────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@mcp.tool(
|
|
217
|
+
description="Store a memory entry with automatic embedding generation. "
|
|
218
|
+
"Returns the entry ID and status.",
|
|
219
|
+
)
|
|
220
|
+
def memory_store(
|
|
221
|
+
value: str = "",
|
|
222
|
+
namespace: str = "default",
|
|
223
|
+
key: str | None = None,
|
|
224
|
+
metadata: str = "{}",
|
|
225
|
+
) -> dict[str, Any]:
|
|
226
|
+
"""Store a value with its embedding vector for semantic search.
|
|
227
|
+
|
|
228
|
+
Generates a 384-dim embedding via fastembed (BAAI/bge-small-en-v1.5)
|
|
229
|
+
and stores it in the vec_memories virtual table alongside the text
|
|
230
|
+
content in the memories table.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
value: Text content to store.
|
|
234
|
+
namespace: Namespace for isolation (default: "default").
|
|
235
|
+
key: Optional unique key within namespace.
|
|
236
|
+
metadata: Optional JSON metadata string.
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dict with id, namespace, key, status.
|
|
240
|
+
"""
|
|
241
|
+
if not value or not value.strip():
|
|
242
|
+
return {"error": "value cannot be empty"}
|
|
243
|
+
|
|
244
|
+
value = value.strip()
|
|
245
|
+
db = _get_conn()
|
|
246
|
+
now = _now_iso()
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# Validate metadata is valid JSON
|
|
250
|
+
md_obj: dict[str, Any] = (
|
|
251
|
+
json.loads(metadata) if metadata and metadata != "{}" else {}
|
|
252
|
+
)
|
|
253
|
+
metadata_str = json.dumps(md_obj)
|
|
254
|
+
|
|
255
|
+
cur = db.execute(
|
|
256
|
+
"""INSERT INTO memories (namespace, key, value, metadata,
|
|
257
|
+
created_at, updated_at)
|
|
258
|
+
VALUES (?, ?, ?, ?, ?, ?)""",
|
|
259
|
+
[namespace, key, value, metadata_str, now, now],
|
|
260
|
+
)
|
|
261
|
+
entry_id = cur.lastrowid
|
|
262
|
+
|
|
263
|
+
# Generate embedding
|
|
264
|
+
embedding = _embed(value)
|
|
265
|
+
embedding_json = json.dumps(embedding)
|
|
266
|
+
|
|
267
|
+
db.execute(
|
|
268
|
+
"INSERT INTO vec_memories(id, embedding) VALUES (?, ?)",
|
|
269
|
+
[entry_id, embedding_json],
|
|
270
|
+
)
|
|
271
|
+
db.commit()
|
|
272
|
+
except sqlite3.IntegrityError as e:
|
|
273
|
+
db.rollback()
|
|
274
|
+
return {"error": f"Duplicate key or constraint violation: {e}"}
|
|
275
|
+
except Exception as e:
|
|
276
|
+
db.rollback()
|
|
277
|
+
return {"error": f"Failed to store memory: {e}"}
|
|
278
|
+
|
|
279
|
+
return {"id": entry_id, "namespace": namespace, "key": key, "status": "stored"}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@mcp.tool(
|
|
283
|
+
description="Hybrid semantic search across memories. "
|
|
284
|
+
"Combines vector cosine similarity and FTS5 BM25 keyword search "
|
|
285
|
+
"via Reciprocal Rank Fusion (RRF).",
|
|
286
|
+
)
|
|
287
|
+
def memory_search( # noqa: PLR0912
|
|
288
|
+
query: str,
|
|
289
|
+
namespace: str | None = None,
|
|
290
|
+
top_k: int = 5,
|
|
291
|
+
) -> list[dict[str, Any]]:
|
|
292
|
+
"""Hybrid search: vector + keyword fused via RRF.
|
|
293
|
+
|
|
294
|
+
For short or keyword-heavy queries, FTS5 BM25 dominates.
|
|
295
|
+
For conceptual or long queries, vector cosine dominates.
|
|
296
|
+
RRF combines both into a single ranked list.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
query: Search query text.
|
|
300
|
+
namespace: Optional namespace filter.
|
|
301
|
+
top_k: Maximum results (default 5, max 50).
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
List of memory entries with score and metadata.
|
|
305
|
+
"""
|
|
306
|
+
if not query or not query.strip():
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
top_k = max(1, min(50, int(top_k)))
|
|
310
|
+
db = _get_conn()
|
|
311
|
+
query_stripped = query.strip()
|
|
312
|
+
|
|
313
|
+
# 1. Generate query embedding
|
|
314
|
+
try:
|
|
315
|
+
query_vec = json.dumps(_embed(query_stripped))
|
|
316
|
+
except Exception:
|
|
317
|
+
query_vec = None
|
|
318
|
+
|
|
319
|
+
vec_ids: list[int] = []
|
|
320
|
+
fts_ids: list[int] = []
|
|
321
|
+
|
|
322
|
+
# 2. Vector search
|
|
323
|
+
if query_vec is not None:
|
|
324
|
+
nvec = top_k * 2
|
|
325
|
+
try:
|
|
326
|
+
if namespace:
|
|
327
|
+
vec_rows = db.execute(
|
|
328
|
+
"""SELECT v.id FROM vec_memories v
|
|
329
|
+
JOIN memories m ON m.id = v.id
|
|
330
|
+
WHERE m.namespace = ?
|
|
331
|
+
ORDER BY v.embedding MATCH ? LIMIT ?""",
|
|
332
|
+
[namespace, query_vec, nvec],
|
|
333
|
+
).fetchall()
|
|
334
|
+
else:
|
|
335
|
+
vec_rows = db.execute(
|
|
336
|
+
"""SELECT v.id FROM vec_memories v
|
|
337
|
+
ORDER BY v.embedding MATCH ? LIMIT ?""",
|
|
338
|
+
[query_vec, nvec],
|
|
339
|
+
).fetchall()
|
|
340
|
+
vec_ids = [r["id"] for r in vec_rows]
|
|
341
|
+
except Exception:
|
|
342
|
+
vec_ids = []
|
|
343
|
+
|
|
344
|
+
# 3. FTS5 keyword search
|
|
345
|
+
try:
|
|
346
|
+
# Build FTS query: escape special chars, use prefix matching
|
|
347
|
+
fts_query = " OR ".join(f'"{word}"*' for word in query_stripped.split() if word)
|
|
348
|
+
if namespace:
|
|
349
|
+
fts_rows = db.execute(
|
|
350
|
+
"""SELECT rowid FROM memories_fts
|
|
351
|
+
WHERE memories_fts MATCH ?
|
|
352
|
+
AND namespace = ?
|
|
353
|
+
ORDER BY rank LIMIT ?""",
|
|
354
|
+
[fts_query, namespace, top_k * 2],
|
|
355
|
+
).fetchall()
|
|
356
|
+
else:
|
|
357
|
+
fts_rows = db.execute(
|
|
358
|
+
"""SELECT rowid FROM memories_fts
|
|
359
|
+
WHERE memories_fts MATCH ?
|
|
360
|
+
ORDER BY rank LIMIT ?""",
|
|
361
|
+
[fts_query, top_k * 2],
|
|
362
|
+
).fetchall()
|
|
363
|
+
fts_ids = [r["rowid"] for r in fts_rows]
|
|
364
|
+
except Exception:
|
|
365
|
+
fts_ids = []
|
|
366
|
+
|
|
367
|
+
# 4. RRF fusion
|
|
368
|
+
fused = _rrf_fuse(vec_ids, fts_ids, top_k)
|
|
369
|
+
|
|
370
|
+
if not fused:
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
# 5. Fetch full entries
|
|
374
|
+
id_list = [doc_id for doc_id, _ in fused]
|
|
375
|
+
score_map = {doc_id: score for doc_id, score in fused}
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
placeholders = ",".join("?" * len(id_list))
|
|
379
|
+
rows = db.execute(
|
|
380
|
+
f"""SELECT id, namespace, key, value, metadata, created_at
|
|
381
|
+
FROM memories WHERE id IN ({placeholders})""",
|
|
382
|
+
id_list,
|
|
383
|
+
).fetchall()
|
|
384
|
+
except Exception:
|
|
385
|
+
return []
|
|
386
|
+
|
|
387
|
+
# Preserve RRF ranking order
|
|
388
|
+
row_map = {r["id"]: r for r in rows}
|
|
389
|
+
results: list[dict[str, Any]] = []
|
|
390
|
+
for doc_id in id_list:
|
|
391
|
+
row = row_map.get(doc_id)
|
|
392
|
+
if row is None:
|
|
393
|
+
continue
|
|
394
|
+
entry = dict(row)
|
|
395
|
+
entry = _parse_metadata(entry)
|
|
396
|
+
entry["score"] = round(score_map.get(doc_id, 0.0), 4)
|
|
397
|
+
results.append(entry)
|
|
398
|
+
|
|
399
|
+
return results
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@mcp.tool(
|
|
403
|
+
description="Recall a specific memory entry by its key within a namespace. "
|
|
404
|
+
"Returns the full entry including parsed metadata.",
|
|
405
|
+
)
|
|
406
|
+
def memory_recall(
|
|
407
|
+
key: str,
|
|
408
|
+
namespace: str = "default",
|
|
409
|
+
) -> dict[str, Any] | None:
|
|
410
|
+
"""Exact-match lookup by namespace + key.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
key: The unique key of the entry.
|
|
414
|
+
namespace: Namespace scope (default: "default").
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
The memory entry dict, or None if not found.
|
|
418
|
+
"""
|
|
419
|
+
if not key or not key.strip():
|
|
420
|
+
return None
|
|
421
|
+
|
|
422
|
+
db = _get_conn()
|
|
423
|
+
row = db.execute(
|
|
424
|
+
"SELECT id, namespace, key, value, metadata, created_at, updated_at "
|
|
425
|
+
"FROM memories WHERE namespace = ? AND key = ?",
|
|
426
|
+
[namespace, key.strip()],
|
|
427
|
+
).fetchone()
|
|
428
|
+
|
|
429
|
+
if row is None:
|
|
430
|
+
return None
|
|
431
|
+
|
|
432
|
+
entry = _dict_from_row(row)
|
|
433
|
+
return _parse_metadata(entry)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@mcp.tool(
|
|
437
|
+
description="Delete a memory entry by ID or by key. "
|
|
438
|
+
"Vector embedding and FTS index entries are removed automatically "
|
|
439
|
+
"via CASCADE / triggers.",
|
|
440
|
+
)
|
|
441
|
+
def memory_forget(
|
|
442
|
+
id: int | None = None,
|
|
443
|
+
key: str | None = None,
|
|
444
|
+
namespace: str = "default",
|
|
445
|
+
) -> dict[str, Any]:
|
|
446
|
+
"""Delete a memory entry and its associated vector embedding.
|
|
447
|
+
|
|
448
|
+
Provide either ``id`` (exact rowid) or ``key`` (within namespace)
|
|
449
|
+
to identify the entry. Foreign key + FTS triggers handle cleanup.
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
id: Exact row ID of the entry to delete.
|
|
453
|
+
key: Key of the entry to delete (requires namespace).
|
|
454
|
+
namespace: Namespace scope when using key (default: "default").
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
Dict with deleted status and entry identifier.
|
|
458
|
+
"""
|
|
459
|
+
if id is not None:
|
|
460
|
+
identifier = id
|
|
461
|
+
col = "id"
|
|
462
|
+
params: list[Any] = [id]
|
|
463
|
+
elif key and key.strip():
|
|
464
|
+
identifier = key
|
|
465
|
+
col = "key"
|
|
466
|
+
params = [key.strip(), namespace]
|
|
467
|
+
sql = "DELETE FROM memories WHERE key = ? AND namespace = ?"
|
|
468
|
+
else:
|
|
469
|
+
return {"error": "Provide either id or key"}
|
|
470
|
+
|
|
471
|
+
db = _get_conn()
|
|
472
|
+
try:
|
|
473
|
+
if col == "id":
|
|
474
|
+
# Vec table cascade
|
|
475
|
+
db.execute("DELETE FROM vec_memories WHERE id = ?", [id])
|
|
476
|
+
cur = db.execute("DELETE FROM memories WHERE id = ?", [id])
|
|
477
|
+
else:
|
|
478
|
+
cur = db.execute(sql, params)
|
|
479
|
+
db.commit()
|
|
480
|
+
|
|
481
|
+
if cur.rowcount == 0:
|
|
482
|
+
return {"deleted": False, "error": "Entry not found"}
|
|
483
|
+
|
|
484
|
+
return {"deleted": True, col: identifier, "namespace": namespace}
|
|
485
|
+
except Exception as e:
|
|
486
|
+
db.rollback()
|
|
487
|
+
return {"error": f"Failed to delete: {e}"}
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@mcp.tool(
|
|
491
|
+
description="List memory entries chronologically with optional "
|
|
492
|
+
"namespace and key-prefix filters.",
|
|
493
|
+
)
|
|
494
|
+
def memory_list(
|
|
495
|
+
namespace: str | None = None,
|
|
496
|
+
prefix: str = "",
|
|
497
|
+
limit: int = 50,
|
|
498
|
+
) -> list[dict[str, Any]]:
|
|
499
|
+
"""List memory entries, newest first.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
namespace: Optional namespace filter.
|
|
503
|
+
prefix: Optional key prefix filter.
|
|
504
|
+
limit: Maximum entries (default 50, max 500).
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
List of memory entries sorted by created_at descending.
|
|
508
|
+
"""
|
|
509
|
+
limit = max(1, min(500, int(limit)))
|
|
510
|
+
db = _get_conn()
|
|
511
|
+
|
|
512
|
+
conditions: list[str] = []
|
|
513
|
+
params: list[Any] = []
|
|
514
|
+
|
|
515
|
+
if namespace:
|
|
516
|
+
conditions.append("namespace = ?")
|
|
517
|
+
params.append(namespace)
|
|
518
|
+
|
|
519
|
+
if prefix:
|
|
520
|
+
conditions.append("key LIKE ?")
|
|
521
|
+
params.append(f"{prefix}%")
|
|
522
|
+
|
|
523
|
+
where = " AND ".join(conditions) if conditions else "1"
|
|
524
|
+
|
|
525
|
+
try:
|
|
526
|
+
rows = db.execute(
|
|
527
|
+
f"SELECT id, namespace, key, value, metadata, created_at "
|
|
528
|
+
f"FROM memories WHERE {where} ORDER BY created_at DESC LIMIT ?",
|
|
529
|
+
[*params, limit],
|
|
530
|
+
).fetchall()
|
|
531
|
+
except Exception as e:
|
|
532
|
+
return [{"error": f"List failed: {e}"}]
|
|
533
|
+
|
|
534
|
+
results = []
|
|
535
|
+
for row in rows:
|
|
536
|
+
entry = _dict_from_row(row)
|
|
537
|
+
results.append(_parse_metadata(entry))
|
|
538
|
+
|
|
539
|
+
return results
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@mcp.tool(
|
|
543
|
+
description="Memory database statistics: total entries, per-namespace "
|
|
544
|
+
"breakdown, DB file size, and FTS/vector table sizes.",
|
|
545
|
+
)
|
|
546
|
+
def memory_stats() -> dict[str, Any]:
|
|
547
|
+
"""Return aggregate statistics about the memory database.
|
|
548
|
+
|
|
549
|
+
Includes total count, namespace breakdown, FTS entry count,
|
|
550
|
+
vector entry count, and file size on disk.
|
|
551
|
+
|
|
552
|
+
Returns:
|
|
553
|
+
Dict with count, namespaces, and storage info.
|
|
554
|
+
"""
|
|
555
|
+
db = _get_conn()
|
|
556
|
+
|
|
557
|
+
stats: dict[str, Any] = {"status": "ok"}
|
|
558
|
+
|
|
559
|
+
try:
|
|
560
|
+
stats["total_entries"] = db.execute(
|
|
561
|
+
"SELECT COUNT(*) AS c FROM memories"
|
|
562
|
+
).fetchone()["c"]
|
|
563
|
+
except Exception:
|
|
564
|
+
stats["total_entries"] = 0
|
|
565
|
+
|
|
566
|
+
try:
|
|
567
|
+
ns_rows = db.execute(
|
|
568
|
+
"SELECT namespace, COUNT(*) AS c FROM memories GROUP BY namespace "
|
|
569
|
+
"ORDER BY c DESC"
|
|
570
|
+
).fetchall()
|
|
571
|
+
stats["namespaces"] = [
|
|
572
|
+
{"namespace": r["namespace"], "count": r["c"]} for r in ns_rows
|
|
573
|
+
]
|
|
574
|
+
except Exception:
|
|
575
|
+
stats["namespaces"] = []
|
|
576
|
+
|
|
577
|
+
try:
|
|
578
|
+
stats["vector_entries"] = db.execute(
|
|
579
|
+
"SELECT COUNT(*) AS c FROM vec_memories"
|
|
580
|
+
).fetchone()["c"]
|
|
581
|
+
except Exception:
|
|
582
|
+
stats["vector_entries"] = 0
|
|
583
|
+
|
|
584
|
+
try:
|
|
585
|
+
db_size = DB_PATH.stat().st_size if DB_PATH.exists() else 0
|
|
586
|
+
stats["db_size_bytes"] = db_size
|
|
587
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
588
|
+
if db_size < _BYTE_UNIT:
|
|
589
|
+
stats["db_size_human"] = f"{db_size:.2f} {unit}"
|
|
590
|
+
break
|
|
591
|
+
db_size /= 1024.0
|
|
592
|
+
else:
|
|
593
|
+
stats["db_size_human"] = f"{db_size:.2f} TB"
|
|
594
|
+
except Exception:
|
|
595
|
+
stats["db_size_bytes"] = 0
|
|
596
|
+
stats["db_size_human"] = "unknown"
|
|
597
|
+
|
|
598
|
+
return stats
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# ── Main Entrypoint ───────────────────────────────────────────────────────────
|
|
602
|
+
|
|
603
|
+
if __name__ == "__main__":
|
|
604
|
+
mcp.run()
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PluginConfig } from 'opencode'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'pantheon',
|
|
5
|
+
version: '5.0.0',
|
|
6
|
+
description: 'Pantheon multi-agent orchestration platform',
|
|
7
|
+
|
|
8
|
+
hooks: {
|
|
9
|
+
config: async (config: PluginConfig) => {
|
|
10
|
+
config.agentsPath = config.agentsPath ?? []
|
|
11
|
+
config.agentsPath.push(new URL('./agents', import.meta.url).pathname)
|
|
12
|
+
|
|
13
|
+
config.skillsPaths = config.skillsPaths ?? []
|
|
14
|
+
config.skillsPaths.push(new URL('./skills', import.meta.url).pathname)
|
|
15
|
+
|
|
16
|
+
return config
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
} satisfies PluginConfig
|