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.
Files changed (128) hide show
  1. package/AGENTS.md +37 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1013 -0
  4. package/bin/pantheon-init.mjs +183 -0
  5. package/commands/pantheon-audit.md +25 -0
  6. package/commands/pantheon-bg.md +10 -0
  7. package/commands/pantheon-consolidate.md +11 -0
  8. package/commands/pantheon-deepwork.md +128 -0
  9. package/commands/pantheon-doc.md +10 -0
  10. package/commands/pantheon-focus.md +9 -0
  11. package/commands/pantheon-forget.md +58 -0
  12. package/commands/pantheon-hash.md +11 -0
  13. package/commands/pantheon-optimize.md +79 -0
  14. package/commands/pantheon-remember.md +44 -0
  15. package/commands/pantheon-search.md +48 -0
  16. package/commands/pantheon-status.md +71 -0
  17. package/commands/pantheon-todo.md +11 -0
  18. package/commands/pantheon.md +49 -0
  19. package/docs/AGENT-MCP.md +194 -0
  20. package/docs/ARCHITECTURE.md +384 -0
  21. package/docs/BRANCH-PROTECTION.md +142 -0
  22. package/docs/INDEX.md +81 -0
  23. package/docs/INSTALLATION.md +217 -0
  24. package/docs/MCP.md +238 -0
  25. package/docs/MEMORY.md +471 -0
  26. package/docs/MIGRATION-MEMORY-BANK.md +139 -0
  27. package/docs/PLATFORMS.md +5 -0
  28. package/docs/QUICKSTART.md +49 -0
  29. package/docs/README.md +18 -0
  30. package/docs/RELEASING.md +256 -0
  31. package/docs/SETUP.md +5 -0
  32. package/docs/UPGRADING.md +41 -0
  33. package/docs/mcp-recommendations.md +439 -0
  34. package/docs/mcp-tools.md +156 -0
  35. package/docs/mcp-user-guide.md +204 -0
  36. package/docs/persistence-mcp.md +111 -0
  37. package/package.json +72 -0
  38. package/pantheon.schema.json +158 -0
  39. package/scripts/__init__.py +0 -0
  40. package/scripts/_pantheon_paths.py +68 -0
  41. package/scripts/check-version-consistency.sh +23 -0
  42. package/scripts/code_mode_server.py +202 -0
  43. package/scripts/doctor.mjs +763 -0
  44. package/scripts/generate-prompts.sh +222 -0
  45. package/scripts/generate-routing-docs.mjs +104 -0
  46. package/scripts/hash_verify.py +192 -0
  47. package/scripts/init-pantheon-mcp.sh +118 -0
  48. package/scripts/install/agents-md.mjs +214 -0
  49. package/scripts/install/health-check.mjs +196 -0
  50. package/scripts/install/migrate.mjs +209 -0
  51. package/scripts/install/opencode.mjs +645 -0
  52. package/scripts/install/shared.mjs +655 -0
  53. package/scripts/install/venv.mjs +116 -0
  54. package/scripts/install-mcp.mjs +885 -0
  55. package/scripts/install.mjs +26 -0
  56. package/scripts/manifest.mjs +622 -0
  57. package/scripts/mcp_persistence_server.py +459 -0
  58. package/scripts/mcp_resources_server.py +206 -0
  59. package/scripts/memory_cache.py +78 -0
  60. package/scripts/memory_mcp_server.py +605 -0
  61. package/scripts/paths.py +64 -0
  62. package/scripts/prune_context.py +72 -0
  63. package/scripts/release-bundle.mjs +109 -0
  64. package/scripts/scrub-secrets.py +282 -0
  65. package/scripts/scrub_secrets.py +281 -0
  66. package/scripts/test-context-compression.sh +166 -0
  67. package/scripts/themis_heuristic_scan.py +287 -0
  68. package/scripts/todo_enforcer.py +242 -0
  69. package/scripts/uninstall.mjs +1057 -0
  70. package/scripts/validate-routing.mjs +160 -0
  71. package/scripts/validate_agent_frontmatter.py +226 -0
  72. package/scripts/versioning.mjs +254 -0
  73. package/skills-lock.json +16 -0
  74. package/src/agents/aphrodite.md +162 -0
  75. package/src/agents/apollo.md +109 -0
  76. package/src/agents/athena.md +226 -0
  77. package/src/agents/demeter.md +146 -0
  78. package/src/agents/gaia.md +82 -0
  79. package/src/agents/hephaestus.md +105 -0
  80. package/src/agents/hermes.md +302 -0
  81. package/src/agents/iris.md +99 -0
  82. package/src/agents/mnemosyne.md +226 -0
  83. package/src/agents/nyx.md +87 -0
  84. package/src/agents/prometheus.md +199 -0
  85. package/src/agents/talos.md +93 -0
  86. package/src/agents/themis.md +187 -0
  87. package/src/agents/zeus.md +209 -0
  88. package/src/instructions/agent-return-format.instructions.md +26 -0
  89. package/src/instructions/backend-standards.instructions.md +45 -0
  90. package/src/instructions/documentation-standards.instructions.md +53 -0
  91. package/src/instructions/frontend-standards.instructions.md +46 -0
  92. package/src/instructions/memory-protocol.instructions.md +67 -0
  93. package/src/instructions/yagni.instructions.md +21 -0
  94. package/src/instructions/zeus-anti-stall.instructions.md +72 -0
  95. package/src/instructions/zeus-communication-rules.instructions.md +15 -0
  96. package/src/instructions/zeus-council-synthesis.instructions.md +105 -0
  97. package/src/instructions/zeus-timeout-retry.instructions.md +127 -0
  98. package/src/mcp/_pantheon_paths.py +67 -0
  99. package/src/mcp/code_mode_server.py +202 -0
  100. package/src/mcp/init-pantheon-mcp.sh +118 -0
  101. package/src/mcp/install-mcp.mjs +885 -0
  102. package/src/mcp/mcp_persistence_server.py +458 -0
  103. package/src/mcp/mcp_resources_server.py +205 -0
  104. package/src/mcp/memory_mcp_server.py +604 -0
  105. package/src/mcp/requirements-mcp-core.txt +5 -0
  106. package/src/mcp/requirements-mcp.txt +5 -0
  107. package/src/plugin.ts +19 -0
  108. package/src/plugins/tui/dist/tui.js +143 -0
  109. package/src/plugins/tui/dist/tui.tsx +144 -0
  110. package/src/plugins/tui/package.json +32 -0
  111. package/src/plugins/tui/src/index.tsx +144 -0
  112. package/src/plugins/tui/tsdown.config.ts +22 -0
  113. package/src/routing.yml +499 -0
  114. package/src/skills/README.md +230 -0
  115. package/src/skills/agent-coordination/SKILL.md +95 -0
  116. package/src/skills/artifact-management/SKILL.md +118 -0
  117. package/src/skills/auto-continue/SKILL.md +280 -0
  118. package/src/skills/code-review-checklist/SKILL.md +139 -0
  119. package/src/skills/context-compression/SKILL.md +861 -0
  120. package/src/skills/git-workflow-and-versioning/SKILL.md +32 -0
  121. package/src/skills/incremental-implementation/SKILL.md +27 -0
  122. package/src/skills/memory-bank/SKILL.md +165 -0
  123. package/src/skills/orchestration-workflow/SKILL.md +311 -0
  124. package/src/skills/security-hardening/SKILL.md +36 -0
  125. package/src/skills/session-goal/SKILL.md +138 -0
  126. package/src/skills/spec-driven-development/SKILL.md +23 -0
  127. package/src/skills/tdd-with-agents/SKILL.md +170 -0
  128. package/src/skills/visual-review-pipeline/SKILL.md +200 -0
@@ -0,0 +1,459 @@
1
+ # Auto-generated: resolved symlink from ../src/mcp/mcp_persistence_server.py
2
+ #!/usr/bin/env python3
3
+ """Pantheon Persistence MCP Server.
4
+
5
+ Key-Value store with FTS5 full-text search, TTL-based expiration,
6
+ and namespace isolation. Uses SQLite with zero external dependencies.
7
+
8
+ Usage:
9
+ python scripts/mcp_persistence_server.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import re
16
+ import sqlite3
17
+ import sys
18
+ from datetime import UTC, datetime, timedelta
19
+ from pathlib import Path
20
+
21
+ from _pantheon_paths import pantheon_home, pantheon_project
22
+ from mcp.server.fastmcp import FastMCP
23
+
24
+ # ── Schema ──────────────────────────────────────────────────────────────────────
25
+
26
+ CREATE_SQL: str = """
27
+ CREATE TABLE IF NOT EXISTS kv_store (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ namespace TEXT NOT NULL,
30
+ key TEXT NOT NULL,
31
+ value TEXT NOT NULL,
32
+ expires_at TEXT, -- ISO 8601, NULL = forever
33
+ deleted_at TEXT, -- NULL = active, set on TTL purge
34
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
36
+ UNIQUE(namespace, key)
37
+ );
38
+
39
+ CREATE VIRTUAL TABLE IF NOT EXISTS kv_store_fts USING fts5(
40
+ namespace, key, value,
41
+ content='kv_store', content_rowid='id',
42
+ tokenize='porter unicode61'
43
+ );
44
+
45
+ CREATE TRIGGER IF NOT EXISTS kv_store_ai AFTER INSERT ON kv_store BEGIN
46
+ INSERT INTO kv_store_fts(rowid, namespace, key, value)
47
+ VALUES (new.id, new.namespace, new.key, new.value);
48
+ END;
49
+
50
+ CREATE TRIGGER IF NOT EXISTS kv_store_ad AFTER DELETE ON kv_store BEGIN
51
+ INSERT INTO kv_store_fts(kv_store_fts, rowid, namespace, key, value)
52
+ VALUES('delete', old.id, old.namespace, old.key, old.value);
53
+ END;
54
+
55
+ CREATE TRIGGER IF NOT EXISTS kv_store_au AFTER UPDATE ON kv_store BEGIN
56
+ INSERT INTO kv_store_fts(kv_store_fts, rowid, namespace, key, value)
57
+ VALUES('delete', old.id, old.namespace, old.key, old.value);
58
+ INSERT INTO kv_store_fts(rowid, namespace, key, value)
59
+ VALUES (new.id, new.namespace, new.key, new.value);
60
+ END;
61
+ """
62
+
63
+ # ── FastMCP App ────────────────────────────────────────────────────────────────
64
+
65
+ mcp = FastMCP(
66
+ "pantheon-persistence",
67
+ instructions="Key-Value store with FTS5 full-text search, "
68
+ "TTL-based expiration, and namespace isolation.",
69
+ )
70
+
71
+ # ── Database Initialization ─────────────────────────────────────────────────────
72
+
73
+ _global_db: sqlite3.Connection | None = None
74
+ _project_db: sqlite3.Connection | None = None
75
+ _DELETELOG_MAX_BYTES: int = 1_048_576 # 1 MB
76
+ _DELETELOG_KEEP: int = 3
77
+
78
+
79
+ def _init_db(db_path: Path) -> sqlite3.Connection:
80
+ """Initialize a SQLite database with WAL mode and schema."""
81
+ db_path.parent.mkdir(parents=True, exist_ok=True)
82
+ conn = sqlite3.connect(str(db_path), check_same_thread=False)
83
+ conn.execute("PRAGMA journal_mode=WAL")
84
+ conn.execute("PRAGMA busy_timeout=5000")
85
+ conn.execute("PRAGMA foreign_keys=ON")
86
+ # ── FTS5 Availability Check ──────────────────────────────────────────
87
+ # Check BEFORE schema creation. kv_search uses FTS5, which isn't on all systems.
88
+ row = conn.execute("PRAGMA compile_options").fetchall()
89
+ if not any("ENABLE_FTS5" in r[0] for r in row):
90
+ print(
91
+ "WARNING: SQLite FTS5 not available. kv_search will fail.", file=sys.stderr
92
+ )
93
+ import re as _re # noqa: PLC0415
94
+
95
+ _no_fts = _re.sub(
96
+ r"CREATE VIRTUAL TABLE IF NOT EXISTS kv_store_fts.*?;",
97
+ "",
98
+ CREATE_SQL,
99
+ flags=_re.DOTALL,
100
+ )
101
+ conn.executescript(_no_fts)
102
+ else:
103
+ conn.executescript(CREATE_SQL)
104
+ conn.commit()
105
+
106
+ return conn
107
+
108
+
109
+ def _db(scope: str) -> sqlite3.Connection:
110
+ """Resolve the connection for the given scope."""
111
+ if scope == "global":
112
+ if _global_db is None:
113
+ raise RuntimeError("Global database not initialized")
114
+ return _global_db
115
+ if scope == "project":
116
+ if _project_db is None:
117
+ raise ValueError(
118
+ "Project database not available. "
119
+ "Set PANTHEON_PROJECT or use scope='global'"
120
+ )
121
+ return _project_db
122
+ raise ValueError(f"Unknown scope: {scope!r}. Expected 'global' or 'project'.")
123
+
124
+
125
+ # ── Deletelog ────────────────────────────────────────────────────────────────────
126
+
127
+
128
+ def _rotate_deletelog(log_path: Path) -> None:
129
+ """Rotate deletelog at 1MB, keep last 3 rotated files."""
130
+ if not log_path.exists() or log_path.stat().st_size < _DELETELOG_MAX_BYTES:
131
+ return
132
+
133
+ # Shift existing rotated files: .3 → remove, .2 → .3, .1 → .2
134
+ for i in range(_DELETELOG_KEEP, 0, -1):
135
+ older = log_path.with_suffix(f".deletelog.{i}")
136
+ if older.exists():
137
+ if i == _DELETELOG_KEEP:
138
+ older.unlink()
139
+ else:
140
+ older.rename(log_path.with_suffix(f".deletelog.{i + 1}"))
141
+
142
+ # Rename current log to .1
143
+ log_path.rename(log_path.with_suffix(".deletelog.1"))
144
+
145
+
146
+ def _write_deletelog(db_path: Path, count: int, keys: list[str]) -> None:
147
+ """Append a TTL purge entry to the deletelog."""
148
+ log_path = db_path.with_name(db_path.name + ".deletelog")
149
+ _rotate_deletelog(log_path)
150
+
151
+ timestamp = datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
152
+ keys_str = ",".join(keys)
153
+ with open(log_path, "a", encoding="utf-8") as f:
154
+ f.write(f"[{timestamp}] PURGED={count} KEYS=[{keys_str}]\n")
155
+
156
+
157
+ # ── Resolve scope and init databases ────────────────────────────────────────────
158
+
159
+ parser = argparse.ArgumentParser()
160
+ parser.add_argument("--global-db", default=None)
161
+ parser.add_argument("--project-db", default=None)
162
+ args = parser.parse_args()
163
+
164
+ _global_root = (
165
+ Path(args.global_db) if args.global_db else pantheon_home() / "persistence"
166
+ )
167
+ _global_db = _init_db(_global_root / "global.db")
168
+
169
+ _project_db_instance = None
170
+ if args.project_db:
171
+ _project_db_instance = _init_db(Path(args.project_db))
172
+ else:
173
+ _proj = pantheon_project()
174
+ if _proj:
175
+ _project_db_instance = _init_db(
176
+ _proj / ".pantheon" / "persistence" / "project.db"
177
+ )
178
+ _project_db = _project_db_instance
179
+
180
+
181
+ # ── Tools ───────────────────────────────────────────────────────────────────────
182
+
183
+
184
+ @mcp.tool(
185
+ name="kv_store",
186
+ description="Store a key-value pair in a namespace with optional TTL (seconds). "
187
+ "INSERT OR REPLACE on duplicate (namespace, key).",
188
+ )
189
+ async def kv_store(
190
+ namespace: str,
191
+ key: str,
192
+ value: str,
193
+ ttl: int | None = None,
194
+ scope: str = "project",
195
+ ) -> dict:
196
+ """Store a value under namespace+key with optional TTL.
197
+
198
+ Args:
199
+ namespace: Logical grouping for keys.
200
+ key: Unique key within the namespace.
201
+ value: String value to store.
202
+ ttl: Time-to-live in seconds (None = forever).
203
+ scope: 'project' (default) or 'global'.
204
+
205
+ Returns:
206
+ Status dict with namespace and key.
207
+ """
208
+ conn = _db(scope)
209
+ expires_at: str | None = None
210
+ if ttl is not None:
211
+ expires_at = (datetime.now(UTC) + timedelta(seconds=ttl)).isoformat()
212
+
213
+ conn.execute(
214
+ "INSERT INTO kv_store (namespace, key, value, expires_at, "
215
+ "created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
216
+ "ON CONFLICT(namespace, key) DO UPDATE SET "
217
+ " value = excluded.value, "
218
+ " expires_at = excluded.expires_at, "
219
+ " updated_at = datetime('now')",
220
+ (namespace, key, value, expires_at),
221
+ )
222
+ conn.commit()
223
+ return {"status": "stored", "namespace": namespace, "key": key}
224
+
225
+
226
+ @mcp.tool(
227
+ name="kv_get",
228
+ description="Retrieve a value by namespace and key. "
229
+ "Returns None if not found or expired.",
230
+ )
231
+ async def kv_get(
232
+ namespace: str,
233
+ key: str,
234
+ scope: str = "project",
235
+ ) -> str | None:
236
+ """Get a value by namespace and key.
237
+
238
+ Args:
239
+ namespace: Logical grouping for keys.
240
+ key: Unique key within the namespace.
241
+ scope: 'project' (default) or 'global'.
242
+
243
+ Returns:
244
+ Value string or None if not found/expired.
245
+ """
246
+ conn = _db(scope)
247
+ row = conn.execute(
248
+ "SELECT value FROM kv_store "
249
+ "WHERE namespace = ? AND key = ? "
250
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
251
+ "AND deleted_at IS NULL",
252
+ (namespace, key),
253
+ ).fetchone()
254
+ return str(row[0]) if row else None
255
+
256
+
257
+ @mcp.tool(
258
+ name="kv_delete",
259
+ description="Delete a key-value pair by namespace and key. "
260
+ "FTS trigger cascades automatically.",
261
+ )
262
+ async def kv_delete(
263
+ namespace: str,
264
+ key: str,
265
+ scope: str = "project",
266
+ ) -> dict:
267
+ """Delete a key-value pair.
268
+
269
+ Args:
270
+ namespace: Logical grouping for keys.
271
+ key: Unique key within the namespace.
272
+ scope: 'project' (default) or 'global'.
273
+
274
+ Returns:
275
+ Status dict indicating deletion outcome.
276
+ """
277
+ conn = _db(scope)
278
+ cursor = conn.execute(
279
+ "DELETE FROM kv_store WHERE namespace = ? AND key = ?",
280
+ (namespace, key),
281
+ )
282
+ conn.commit()
283
+ if cursor.rowcount > 0:
284
+ return {"status": "deleted"}
285
+ return {"status": "not_found"}
286
+
287
+
288
+ @mcp.tool(
289
+ name="kv_list",
290
+ description="List keys in a namespace with optional prefix filter. "
291
+ "Returns up to 'limit' entries (default 100).",
292
+ )
293
+ async def kv_list(
294
+ namespace: str,
295
+ prefix: str = "",
296
+ scope: str = "project",
297
+ limit: int = 100,
298
+ ) -> list[dict]:
299
+ """List keys in a namespace, filtered by prefix.
300
+
301
+ Args:
302
+ namespace: Logical grouping for keys.
303
+ prefix: Optional key prefix filter.
304
+ scope: 'project' (default) or 'global'.
305
+ limit: Maximum number of results (default 100).
306
+
307
+ Returns:
308
+ List of dicts with key, value, created_at, expires_at.
309
+ """
310
+ conn = _db(scope)
311
+ rows = conn.execute(
312
+ "SELECT key, value, created_at, expires_at FROM kv_store "
313
+ "WHERE namespace = ? AND key LIKE ? "
314
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
315
+ "AND deleted_at IS NULL "
316
+ "ORDER BY key LIMIT ?",
317
+ (namespace, f"{prefix}%", limit),
318
+ ).fetchall()
319
+ return [
320
+ {
321
+ "key": r[0],
322
+ "value": r[1],
323
+ "created_at": r[2],
324
+ "expires_at": r[3],
325
+ }
326
+ for r in rows
327
+ ]
328
+
329
+
330
+ @mcp.tool(
331
+ name="kv_search",
332
+ description="Full-text search across keys and values using FTS5. "
333
+ "Optionally filter by namespace.",
334
+ )
335
+ async def kv_search(
336
+ query: str,
337
+ namespace: str | None = None,
338
+ scope: str = "project",
339
+ limit: int = 20,
340
+ ) -> list[dict]:
341
+ """Full-text search across keys and values.
342
+
343
+ Args:
344
+ query: Free-text search terms (max 10 terms quoted).
345
+ namespace: Optional namespace filter.
346
+ scope: 'project' (default) or 'global'.
347
+ limit: Maximum number of results (default 20).
348
+
349
+ Returns:
350
+ List of dicts with namespace, key, value, created_at, score.
351
+ """
352
+ conn = _db(scope)
353
+
354
+ # Sanitize: extract up to 10 word tokens, wrap each in quotes
355
+ terms = re.findall(r"\w+", query)[:10]
356
+ if not terms:
357
+ return []
358
+ fts_query = " OR ".join(f'"{t}"' for t in terms)
359
+
360
+ sql: str = (
361
+ "SELECT kv_store.namespace, kv_store.key, kv_store.value, kv_store.created_at, "
362
+ "BM25(kv_store_fts) AS score "
363
+ "FROM kv_store_fts "
364
+ "JOIN kv_store ON kv_store_fts.rowid = kv_store.id "
365
+ "WHERE kv_store_fts MATCH ? "
366
+ "AND kv_store.deleted_at IS NULL "
367
+ "AND (kv_store.expires_at IS NULL OR kv_store.expires_at > datetime('now'))"
368
+ )
369
+ params: list[str | int] = [fts_query]
370
+
371
+ if namespace is not None:
372
+ sql += " AND kv_store.namespace = ?"
373
+ params.append(namespace)
374
+
375
+ sql += " ORDER BY score LIMIT ?"
376
+ params.append(limit)
377
+
378
+ rows = conn.execute(sql, params).fetchall()
379
+ return [
380
+ {
381
+ "namespace": r[0],
382
+ "key": r[1],
383
+ "value": r[2],
384
+ "created_at": r[3],
385
+ "score": round(float(r[4]), 4),
386
+ }
387
+ for r in rows
388
+ ]
389
+
390
+
391
+ @mcp.tool(
392
+ name="purge_expired",
393
+ description="Purge expired TTL entries. Optionally dry-run to see count "
394
+ "without deleting. Logs purged keys to deletelog.",
395
+ )
396
+ async def purge_expired(
397
+ scope: str = "project",
398
+ dry_run: bool = False,
399
+ ) -> dict:
400
+ """Purge expired TTL entries from the database.
401
+
402
+ Args:
403
+ scope: 'project' (default) or 'global'.
404
+ dry_run: If True, only report count without purging.
405
+
406
+ Returns:
407
+ Dict with purged count and dry_run flag.
408
+ """
409
+ conn = _db(scope)
410
+
411
+ # Find expired entries first
412
+ expired = conn.execute(
413
+ "SELECT key FROM kv_store "
414
+ "WHERE expires_at IS NOT NULL "
415
+ "AND expires_at < datetime('now') "
416
+ "AND deleted_at IS NULL",
417
+ ).fetchall()
418
+ expired_keys = [r[0] for r in expired]
419
+ count = len(expired_keys)
420
+
421
+ if dry_run:
422
+ return {"purged": count, "dry_run": True}
423
+
424
+ if count == 0:
425
+ return {"purged": 0, "dry_run": False}
426
+
427
+ # Mark as deleted (soft delete)
428
+ conn.execute(
429
+ "UPDATE kv_store SET deleted_at = datetime('now') "
430
+ "WHERE expires_at IS NOT NULL "
431
+ "AND expires_at < datetime('now') "
432
+ "AND deleted_at IS NULL",
433
+ )
434
+ conn.commit()
435
+
436
+ # Write deletelog
437
+ db_path = _resolve_db_path(scope)
438
+ if db_path:
439
+ _write_deletelog(db_path, count, expired_keys)
440
+
441
+ return {"purged": count, "dry_run": False}
442
+
443
+
444
+ def _resolve_db_path(scope: str) -> Path | None:
445
+ """Resolve the database file path for a given scope."""
446
+ if scope == "global":
447
+ return _global_root / "global.db"
448
+ if _project_db_instance:
449
+ # Find the project root from the project DB path
450
+ proj_root = pantheon_project()
451
+ if proj_root:
452
+ return proj_root / ".pantheon" / "persistence" / "project.db"
453
+ return None
454
+
455
+
456
+ # ── Main Entrypoint ─────────────────────────────────────────────────────────────
457
+
458
+ if __name__ == "__main__":
459
+ mcp.run()
@@ -0,0 +1,206 @@
1
+ # Auto-generated: resolved symlink from ../src/mcp/mcp_resources_server.py
2
+ #!/usr/bin/env python3
3
+ """Pantheon MCP Resources Server.
4
+
5
+ Provides MCP resources for the Pantheon agent framework:
6
+ - Static resources: agents list, skills list, routing.yml
7
+ - Template resources: agent by name, skill by name, deepwork
8
+ plans/status, memory-bank files
9
+
10
+ Usage:
11
+ python scripts/mcp_resources_server.py
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from pathlib import Path
18
+ from typing import Any
19
+
20
+ import yaml
21
+ from _pantheon_paths import pantheon_home, pantheon_project
22
+ from mcp.server.fastmcp import FastMCP
23
+
24
+ # ── Path Resolution ──────────────────────────────────────────────────────────
25
+ _PANTHEON_HOME: Path = pantheon_home()
26
+ _PANTHEON_PROJECT: Path | None = pantheon_project()
27
+
28
+ # ── FastMCP App ───────────────────────────────────────────────────────────────
29
+ mcp = FastMCP(
30
+ "Pantheon Resources",
31
+ instructions="MCP Resources for the Pantheon agent framework. "
32
+ "Access agents, skills, routing configuration, deepwork plans, "
33
+ "and memory-bank files via the pantheon:// URI scheme.",
34
+ )
35
+
36
+ # ── Helpers ───────────────────────────────────────────────────────────────────
37
+
38
+
39
+ def _parse_yaml_frontmatter(filepath: Path) -> dict[str, Any]:
40
+ """Parse YAML frontmatter from a markdown file."""
41
+ content = filepath.read_text(encoding="utf-8")
42
+ match = re.match(r"^---\s*\n(.*?)\n---", content, re.DOTALL)
43
+ if not match:
44
+ return {}
45
+ try:
46
+ return dict(yaml.safe_load(match.group(1)) or {})
47
+ except yaml.YAMLError:
48
+ return {}
49
+
50
+
51
+ def _get_role_from_frontmatter(frontmatter: dict[str, Any]) -> str:
52
+ """Extract the role/description string from agent frontmatter."""
53
+ desc = frontmatter.get("description", "")
54
+ if not isinstance(desc, str) or not desc:
55
+ return "Pantheon agent"
56
+ if "—" in desc:
57
+ return desc.split("—")[0].strip()
58
+ return desc
59
+
60
+
61
+ # ── Static Resources ──────────────────────────────────────────────────────────
62
+
63
+
64
+ @mcp.resource(
65
+ "pantheon://agents",
66
+ description="List of all Pantheon agents with roles from YAML frontmatter",
67
+ )
68
+ async def list_agents() -> str:
69
+ """Return a markdown list of all agents with their roles."""
70
+ agents_dir = _PANTHEON_HOME / "agents"
71
+ if not agents_dir.is_dir():
72
+ return "Agents directory not found."
73
+
74
+ agents: list[str] = []
75
+ for f in sorted(agents_dir.iterdir()):
76
+ if f.suffix == ".md" and f.stem.lower() != "readme":
77
+ frontmatter = _parse_yaml_frontmatter(f)
78
+ name = frontmatter.get("name", f.stem)
79
+ role = _get_role_from_frontmatter(frontmatter)
80
+ agents.append(f"- **{name}** — {role}")
81
+
82
+ return "\n".join(agents) if agents else "No agents found."
83
+
84
+
85
+ @mcp.resource(
86
+ "pantheon://skills",
87
+ description="List of all Pantheon skills with descriptions",
88
+ )
89
+ async def list_skills() -> str:
90
+ """Return a markdown list of all skills with descriptions."""
91
+ skills_dir = _PANTHEON_HOME / "skills"
92
+ if not skills_dir.is_dir():
93
+ return "Skills directory not found."
94
+
95
+ skills: list[str] = []
96
+ for f in sorted(skills_dir.iterdir()):
97
+ if f.is_dir():
98
+ skill_file = f / "SKILL.md"
99
+ if skill_file.exists():
100
+ frontmatter = _parse_yaml_frontmatter(skill_file)
101
+ name = frontmatter.get("name", f.name)
102
+ desc = frontmatter.get("description", "No description")
103
+ skills.append(f"- **{name}** — {desc}")
104
+
105
+ return "\n".join(skills) if skills else "No skills found."
106
+
107
+
108
+ @mcp.resource(
109
+ "pantheon://routing",
110
+ description="Full content of routing.yml (canonical routing source)",
111
+ )
112
+ async def get_routing() -> str:
113
+ """Return the full content of routing.yml."""
114
+ routing_file = _PANTHEON_HOME / "routing.yml"
115
+ if not routing_file.exists():
116
+ return "routing.yml not found."
117
+ return routing_file.read_text(encoding="utf-8")
118
+
119
+
120
+ # ── Template Resources ────────────────────────────────────────────────────────
121
+
122
+
123
+ @mcp.resource(
124
+ "pantheon://agents/{agent_name}",
125
+ description="Content of an agent file by name (case-insensitive lookup)",
126
+ )
127
+ async def get_agent(agent_name: str) -> str:
128
+ """Return the full content of an agent file, case-insensitively."""
129
+ agents_dir = _PANTHEON_HOME / "agents"
130
+ name_lower = agent_name.lower()
131
+ for f in agents_dir.iterdir():
132
+ if f.suffix == ".md" and f.stem.lower() == name_lower:
133
+ return f.read_text(encoding="utf-8")
134
+ return f"Agent '{agent_name}' not found."
135
+
136
+
137
+ @mcp.resource(
138
+ "pantheon://deepwork/{slug}",
139
+ description="PLAN.md content for a deepwork task slug",
140
+ )
141
+ async def get_deepwork_plan(slug: str) -> str:
142
+ """Return PLAN.md content for a deepwork slug."""
143
+ if _PANTHEON_PROJECT is None:
144
+ return f"Deepwork '{slug}' not found. (PANTHEON_PROJECT not set)"
145
+ plan_file = _PANTHEON_PROJECT / ".pantheon" / "deepwork" / slug / "PLAN.md"
146
+ if not plan_file.exists():
147
+ return f"Deepwork '{slug}' not found."
148
+ return plan_file.read_text(encoding="utf-8")
149
+
150
+
151
+ @mcp.resource(
152
+ "pantheon://deepwork/{slug}/status",
153
+ description="STATUS.md content for a deepwork task slug "
154
+ "(or default IN_PROGRESS message if missing)",
155
+ )
156
+ async def get_deepwork_status(slug: str) -> str:
157
+ """Return STATUS.md content for a deepwork slug, or a default message."""
158
+ if _PANTHEON_PROJECT is None:
159
+ return "STATUS.md not found. (PANTHEON_PROJECT not set)"
160
+ status_file = _PANTHEON_PROJECT / ".pantheon" / "deepwork" / slug / "STATUS.md"
161
+ if not status_file.exists():
162
+ return "STATUS.md not found. Current state: IN PROGRESS"
163
+ return status_file.read_text(encoding="utf-8")
164
+
165
+
166
+ @mcp.resource(
167
+ "pantheon://skills/{name}",
168
+ description="Content of a skill's SKILL.md file by name",
169
+ )
170
+ async def get_skill(name: str) -> str:
171
+ """Return SKILL.md content for a skill directory."""
172
+ skill_file = _PANTHEON_HOME / "skills" / name / "SKILL.md"
173
+ if not skill_file.exists():
174
+ return f"Skill '{name}' not found."
175
+ return skill_file.read_text(encoding="utf-8")
176
+
177
+
178
+ @mcp.resource(
179
+ "pantheon://memory-bank/{path}",
180
+ description="Content of a .pantheon/memory-bank/ file by relative path "
181
+ "(path traversal blocked).",
182
+ )
183
+ async def get_memory_bank(path: str) -> str:
184
+ """Return content of a memory-bank file.
185
+
186
+ Security: resolves absolute path and verifies it stays within
187
+ .pantheon/memory-bank/ to prevent directory traversal attacks.
188
+ """
189
+ if _PANTHEON_PROJECT is None:
190
+ return "Memory bank not available. (PANTHEON_PROJECT not set)"
191
+ resolved = (_PANTHEON_PROJECT / ".pantheon" / "memory-bank" / path).resolve()
192
+ mb_dir = (_PANTHEON_PROJECT / ".pantheon" / "memory-bank").resolve()
193
+
194
+ if not str(resolved).startswith(str(mb_dir)):
195
+ return "Path traversal blocked: access denied."
196
+
197
+ if not resolved.exists() or not resolved.is_file():
198
+ return f"File '{path}' not found."
199
+
200
+ return resolved.read_text(encoding="utf-8")
201
+
202
+
203
+ # ── Main Entrypoint ───────────────────────────────────────────────────────────
204
+
205
+ if __name__ == "__main__":
206
+ mcp.run()