pantheon-opencode 1.1.3-beta.0 → 1.2.0-beta.9.7bef03e

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 (38) hide show
  1. package/README.md +74 -1
  2. package/bin/pantheon-init.mjs +79 -0
  3. package/commands/pantheon-vision.md +67 -0
  4. package/opencode.json +12 -41
  5. package/package.json +5 -1
  6. package/scripts/doctor.mjs +4 -2
  7. package/scripts/install/health-check.mjs +101 -1
  8. package/scripts/install/model-picker.mjs +118 -0
  9. package/scripts/install/opencode.mjs +98 -17
  10. package/scripts/install-mcp.mjs +98 -13
  11. package/scripts/mcp_persistence_server.py +331 -1
  12. package/scripts/secret-scan.mjs +76 -0
  13. package/scripts/validate-routing.mjs +15 -0
  14. package/src/agents/aphrodite.md +7 -0
  15. package/src/agents/demeter.md +7 -0
  16. package/src/agents/hephaestus.md +7 -0
  17. package/src/agents/hermes.md +7 -0
  18. package/src/agents/zeus.md +31 -18
  19. package/src/instructions/agent-return-format.instructions.md +1 -0
  20. package/src/instructions/zeus-anti-stall.instructions.md +43 -5
  21. package/src/instructions/zeus-timeout-retry.instructions.md +47 -10
  22. package/src/mcp/install-mcp.mjs +6 -913
  23. package/src/mcp/mcp_persistence_server.py +332 -0
  24. package/src/mcp/pantheon_vision_server.py +495 -0
  25. package/src/mcp/requirements-vision.txt +3 -0
  26. package/src/pantheon/auto-wake.ts +91 -0
  27. package/src/pantheon/background-job-board.ts +568 -0
  28. package/src/pantheon/file-persistence.ts +95 -0
  29. package/src/pantheon/presets.d.mts +99 -0
  30. package/src/pantheon/presets.mjs +491 -0
  31. package/src/pantheon/vision.ts +1340 -0
  32. package/src/plugin.ts +40 -25
  33. package/src/plugins/tui/dist/tui.d.ts.map +1 -1
  34. package/src/plugins/tui/dist/tui.js +806 -222
  35. package/src/plugins/tui/dist/tui.tsx +866 -121
  36. package/src/plugins/tui/package.json +2 -2
  37. package/src/plugins/tui/src/index.tsx +866 -121
  38. package/src/routing.yml +179 -0
@@ -14,6 +14,7 @@ import argparse
14
14
  import re
15
15
  import sqlite3
16
16
  import sys
17
+ import uuid
17
18
  from datetime import datetime, timedelta, timezone
18
19
  from pathlib import Path
19
20
 
@@ -35,6 +36,9 @@ CREATE TABLE IF NOT EXISTS kv_store (
35
36
  UNIQUE(namespace, key)
36
37
  );
37
38
 
39
+ CREATE INDEX IF NOT EXISTS idx_kv_namespace_expires
40
+ ON kv_store(namespace, expires_at);
41
+
38
42
  CREATE VIRTUAL TABLE IF NOT EXISTS kv_store_fts USING fts5(
39
43
  namespace, key, value,
40
44
  content='kv_store', content_rowid='id',
@@ -51,6 +55,14 @@ CREATE TRIGGER IF NOT EXISTS kv_store_ad AFTER DELETE ON kv_store BEGIN
51
55
  VALUES('delete', old.id, old.namespace, old.key, old.value);
52
56
  END;
53
57
 
58
+ CREATE TRIGGER IF NOT EXISTS kv_store_softdelete_ad
59
+ AFTER UPDATE OF deleted_at ON kv_store
60
+ WHEN new.deleted_at IS NOT NULL AND old.deleted_at IS NULL
61
+ BEGIN
62
+ INSERT INTO kv_store_fts(kv_store_fts, rowid)
63
+ VALUES('delete', old.id);
64
+ END;
65
+
54
66
  CREATE TRIGGER IF NOT EXISTS kv_store_au AFTER UPDATE ON kv_store BEGIN
55
67
  INSERT INTO kv_store_fts(kv_store_fts, rowid, namespace, key, value)
56
68
  VALUES('delete', old.id, old.namespace, old.key, old.value);
@@ -219,6 +231,10 @@ async def kv_store(
219
231
  (namespace, key, value, expires_at),
220
232
  )
221
233
  conn.commit()
234
+
235
+ # Opportunistic auto-purge: if namespace has >500 entries, purge expired
236
+ _opportunistic_auto_purge(conn, namespace)
237
+
222
238
  return {"status": "stored", "namespace": namespace, "key": key}
223
239
 
224
240
 
@@ -253,6 +269,52 @@ async def kv_get(
253
269
  return str(row[0]) if row else None
254
270
 
255
271
 
272
+ @mcp.tool(
273
+ name="kv_stats",
274
+ description="Return storage statistics: total entries, expired count, per-namespace breakdown, DB file size.",
275
+ )
276
+ async def kv_stats(
277
+ scope: str = "project",
278
+ ) -> dict:
279
+ """Return storage statistics for the given scope.
280
+
281
+ Args:
282
+ scope: 'project' (default) or 'global'.
283
+
284
+ Returns:
285
+ Dict with total_entries, expired_entries, namespaces breakdown, db_size_bytes.
286
+ """
287
+ conn = _db(scope)
288
+
289
+ total = conn.execute("SELECT COUNT(*) FROM kv_store WHERE deleted_at IS NULL").fetchone()[0]
290
+ expired = conn.execute(
291
+ "SELECT COUNT(*) FROM kv_store "
292
+ "WHERE expires_at IS NOT NULL AND expires_at < datetime('now') "
293
+ "AND deleted_at IS NULL"
294
+ ).fetchone()[0]
295
+
296
+ ns_rows = conn.execute(
297
+ "SELECT namespace, COUNT(*) as cnt, "
298
+ "SUM(CASE WHEN expires_at IS NOT NULL AND expires_at < datetime('now') THEN 1 ELSE 0 END) as expired_count "
299
+ "FROM kv_store WHERE deleted_at IS NULL GROUP BY namespace ORDER BY cnt DESC"
300
+ ).fetchall()
301
+
302
+ namespaces = {
303
+ r[0]: {"count": r[1], "expired": r[2]}
304
+ for r in ns_rows
305
+ }
306
+
307
+ db_path = _resolve_db_path(scope)
308
+ db_size = db_path.stat().st_size if db_path and db_path.exists() else 0
309
+
310
+ return {
311
+ "scope": scope,
312
+ "total_entries": total,
313
+ "expired_entries": expired,
314
+ "namespaces": namespaces,
315
+ "db_size_bytes": db_size,
316
+ }
317
+
256
318
  @mcp.tool(
257
319
  name="kv_delete",
258
320
  description="Delete a key-value pair by namespace and key. "
@@ -352,6 +414,7 @@ async def kv_search(
352
414
 
353
415
  # Sanitize: extract up to 10 word tokens, wrap each in quotes
354
416
  terms = re.findall(r"\w+", query)[:10]
417
+ terms = [re.escape(t) for t in terms]
355
418
  if not terms:
356
419
  return []
357
420
  fts_query = " OR ".join(f'"{t}"' for t in terms)
@@ -440,6 +503,275 @@ async def purge_expired(
440
503
  return {"purged": count, "dry_run": False}
441
504
 
442
505
 
506
+
507
+ def _opportunistic_auto_purge(conn: sqlite3.Connection, namespace: str, threshold: int = 500) -> None:
508
+ """Lightweight auto-purge: if namespace exceeds threshold, soft-delete expired entries."""
509
+ count = conn.execute(
510
+ "SELECT COUNT(*) FROM kv_store WHERE namespace = ? AND deleted_at IS NULL",
511
+ (namespace,),
512
+ ).fetchone()[0]
513
+ if count <= threshold:
514
+ return
515
+
516
+ conn.execute(
517
+ "UPDATE kv_store SET deleted_at = datetime('now') "
518
+ "WHERE namespace = ? AND expires_at IS NOT NULL "
519
+ "AND expires_at < datetime('now') AND deleted_at IS NULL",
520
+ (namespace,),
521
+ )
522
+ conn.commit()
523
+
524
+
525
+ @mcp.tool(
526
+ name="kv_delete_namespace",
527
+ description="Delete all entries in a namespace. Optionally limit to entries older than N days.",
528
+ )
529
+ async def kv_delete_namespace(
530
+ namespace: str,
531
+ scope: str = "project",
532
+ older_than_days: int | None = None,
533
+ ) -> dict:
534
+ """Delete all entries in a namespace, optionally old entries only.
535
+
536
+ Args:
537
+ namespace: Namespace to clear.
538
+ scope: 'project' (default) or 'global'.
539
+ older_than_days: If set, only delete entries older than this many days.
540
+
541
+ Returns:
542
+ Dict with deleted count.
543
+ """
544
+ conn = _db(scope)
545
+
546
+ if older_than_days is not None:
547
+ cursor = conn.execute(
548
+ "DELETE FROM kv_store WHERE namespace = ? "
549
+ "AND datetime(created_at) < datetime('now', ? || ' days')",
550
+ (namespace, f"-{older_than_days}"),
551
+ )
552
+ else:
553
+ cursor = conn.execute(
554
+ "DELETE FROM kv_store WHERE namespace = ?",
555
+ (namespace,),
556
+ )
557
+ conn.commit()
558
+
559
+ return {"deleted": cursor.rowcount}
560
+
561
+
562
+ # ── Context Checkpoint Tools ────────────────────────────────────────────────────
563
+ # Session-scoped context storage for deepwork checkpoints, phase state,
564
+ # tool call history, and reasoning chains. All entries use namespace
565
+ # "checkpoint:{slug}" with auto-TTL of 4 hours (session duration).
566
+
567
+ DEFAULT_CONTEXT_TTL: int = 14400 # 4 hours
568
+
569
+
570
+ @mcp.tool(
571
+ name="context_save",
572
+ description="Save a context checkpoint for a session/phase. "
573
+ "Stores structured JSON in persistence KV with auto-TTL of 4h.",
574
+ )
575
+ async def context_save(
576
+ slug: str,
577
+ key: str,
578
+ content: str,
579
+ session_id: str | None = None,
580
+ ttl: int | None = None,
581
+ scope: str = "project",
582
+ ) -> dict:
583
+ """Save a context checkpoint with session-level isolation.
584
+
585
+ If session_id is provided, namespace becomes "checkpoint:{slug}:{session_id}"
586
+ preventing cross-session reads. If omitted, a random UUID is auto-generated
587
+ and returned so the caller can pass it to subsequent calls.
588
+
589
+ Args:
590
+ slug: Session identifier (e.g. "auth-refactor").
591
+ key: Checkpoint key (e.g. "phase:3", "latest", "heartbeat").
592
+ content: JSON-serializable string or any text content.
593
+ session_id: Unique session ID for namespace isolation (auto-generated if None).
594
+ ttl: TTL in seconds (default 4h / 14400). None = session duration.
595
+ scope: 'project' (default) or 'global'.
596
+
597
+ Returns:
598
+ Status dict with namespace, key, and session_id for subsequent calls.
599
+ """
600
+ actual_session = session_id or uuid.uuid4().hex[:12]
601
+ ns = f"checkpoint:{slug}:{actual_session}"
602
+ actual_ttl = ttl if ttl is not None else DEFAULT_CONTEXT_TTL
603
+
604
+ conn = _db(scope)
605
+ expires_at = (datetime.now(timezone.utc) + timedelta(seconds=actual_ttl)).isoformat()
606
+
607
+ conn.execute(
608
+ "INSERT INTO kv_store (namespace, key, value, expires_at, "
609
+ "created_at, updated_at) VALUES (?, ?, ?, ?, datetime('now'), datetime('now'))"
610
+ "ON CONFLICT(namespace, key) DO UPDATE SET "
611
+ " value = excluded.value, "
612
+ " expires_at = excluded.expires_at, "
613
+ " updated_at = datetime('now')",
614
+ (ns, key, content, expires_at),
615
+ )
616
+ conn.commit()
617
+
618
+ # Also always update "latest" pointer
619
+ if key != "latest":
620
+ # Use a larger TTL for "latest" to outlive individual phase entries
621
+ conn.execute(
622
+ "INSERT INTO kv_store (namespace, key, value, expires_at, "
623
+ "created_at, updated_at) VALUES (?, 'latest', ?, ?, datetime('now'), datetime('now'))"
624
+ "ON CONFLICT(namespace, key) DO UPDATE SET "
625
+ " value = excluded.value, "
626
+ " expires_at = excluded.expires_at, "
627
+ " updated_at = datetime('now')",
628
+ (ns, content, expires_at),
629
+ )
630
+ conn.commit()
631
+
632
+ return {"status": "stored", "namespace": ns, "key": key, "session_id": actual_session, "ttl": actual_ttl}
633
+
634
+
635
+ @mcp.tool(
636
+ name="context_get",
637
+ description="Retrieve a context checkpoint by session slug and key. "
638
+ "Returns the raw content string or null if expired/not found.",
639
+ )
640
+ async def context_get(
641
+ slug: str,
642
+ key: str = "latest",
643
+ session_id: str | None = None,
644
+ scope: str = "project",
645
+ ) -> str | None:
646
+ """Get a context checkpoint with session isolation.
647
+
648
+ Args:
649
+ slug: Session identifier.
650
+ key: Checkpoint key (default "latest").
651
+ session_id: Required for isolation. If None, searches without session scope.
652
+ scope: 'project' (default) or 'global'.
653
+
654
+ Returns:
655
+ Content string or None if not found/expired.
656
+ """
657
+ conn = _db(scope)
658
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
659
+ row = conn.execute(
660
+ "SELECT value FROM kv_store "
661
+ "WHERE namespace = ? AND key = ? "
662
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
663
+ "AND deleted_at IS NULL",
664
+ (ns, key),
665
+ ).fetchone()
666
+ return row[0] if row else None
667
+
668
+
669
+ @mcp.tool(
670
+ name="context_list",
671
+ description="List all context checkpoints for a session slug. "
672
+ "Returns keys, timestamps, and TTL info.",
673
+ )
674
+ async def context_list(
675
+ slug: str,
676
+ session_id: str | None = None,
677
+ scope: str = "project",
678
+ ) -> list[dict]:
679
+ """List all checkpoints for a session.
680
+
681
+ Args:
682
+ slug: Session identifier.
683
+ session_id: Optional. If provided, narrows to specific session.
684
+ scope: 'project' (default) or 'global'.
685
+
686
+ Returns:
687
+ List of dicts with key, created_at, expires_at.
688
+ """
689
+ conn = _db(scope)
690
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
691
+ rows = conn.execute(
692
+ "SELECT key, created_at, expires_at FROM kv_store "
693
+ "WHERE namespace = ? "
694
+ "AND (expires_at IS NULL OR expires_at > datetime('now')) "
695
+ "AND deleted_at IS NULL "
696
+ "ORDER BY created_at DESC LIMIT 50",
697
+ (ns,),
698
+ ).fetchall()
699
+ return [
700
+ {
701
+ "key": r[0],
702
+ "created_at": r[1],
703
+ "expires_at": r[2],
704
+ }
705
+ for r in rows
706
+ ]
707
+
708
+
709
+ @mcp.tool(
710
+ name="context_stats",
711
+ description="Return session context statistics: "
712
+ "checkpoint count, TTL remaining, total size per slug.",
713
+ )
714
+ async def context_stats(
715
+ slug: str,
716
+ session_id: str | None = None,
717
+ scope: str = "project",
718
+ ) -> dict:
719
+ """Return storage statistics for a session's context.
720
+
721
+ Args:
722
+ slug: Session identifier.
723
+ session_id: Optional. If provided, narrows to specific session.
724
+ scope: 'project' (default) or 'global'.
725
+
726
+ Returns:
727
+ Dict with entry_count, total_bytes, ttl_remaining.
728
+ """
729
+ conn = _db(scope)
730
+ ns = f"checkpoint:{slug}:{session_id}" if session_id else f"checkpoint:{slug}"
731
+
732
+ count = conn.execute(
733
+ "SELECT COUNT(*) FROM kv_store "
734
+ "WHERE namespace = ? AND deleted_at IS NULL",
735
+ (ns,),
736
+ ).fetchone()[0]
737
+
738
+ expired = conn.execute(
739
+ "SELECT COUNT(*) FROM kv_store "
740
+ "WHERE namespace = ? AND expires_at < datetime('now') "
741
+ "AND deleted_at IS NULL",
742
+ (ns,),
743
+ ).fetchone()[0]
744
+
745
+ size = conn.execute(
746
+ "SELECT COALESCE(SUM(LENGTH(value)), 0) FROM kv_store "
747
+ "WHERE namespace = ? AND deleted_at IS NULL",
748
+ (ns,),
749
+ ).fetchone()[0]
750
+
751
+ # Check TTL of latest entry
752
+ remaining: int | None = None
753
+ row = conn.execute(
754
+ "SELECT expires_at FROM kv_store "
755
+ "WHERE namespace = ? AND key = 'latest' AND deleted_at IS NULL",
756
+ (ns,),
757
+ ).fetchone()
758
+ if row and row[0]:
759
+ try:
760
+ exp = datetime.fromisoformat(row[0])
761
+ now = datetime.now(timezone.utc)
762
+ remaining = max(0, int((exp - now).total_seconds()))
763
+ except (ValueError, TypeError):
764
+ pass
765
+
766
+ return {
767
+ "slug": slug,
768
+ "namespace": ns,
769
+ "entry_count": count,
770
+ "expired_entries": expired,
771
+ "total_bytes": size,
772
+ "ttl_remaining_seconds": remaining,
773
+ }
774
+
443
775
  def _resolve_db_path(scope: str) -> Path | None:
444
776
  """Resolve the database file path for a given scope."""
445
777
  if scope == "global":