PRODUCTAGENTharrisonhjohnson/productagent · /harnesses/todo/bot/integrations/karma_client.py
000%

karma_client.py

view on github ↗148 lines · python
"""
Karma integration for Navi.

Writes seeds as markdown files directly to karma's seeds directory.
Queries knowledge via Claude CLI RAG over those files.
No karma Python import required — pure file I/O + subprocess.
"""
import json
import logging
import os
import subprocess
from datetime import datetime, timezone
from pathlib import Path

logger = logging.getLogger(__name__)

# Resolved at call time via expanduser (not at import time) so the running
# process always sees the correct home dir regardless of LaunchAgent timing.
def _seeds_dir() -> Path:
    return Path(os.path.expanduser("~/.karma/seeds"))

def _points_file() -> Path:
    return Path(os.path.expanduser("~/.karma/points.json"))


def _slugify(text: str) -> str:
    slug = text.lower()[:60]
    slug = "".join(c if c.isalnum() else "-" for c in slug)
    while "--" in slug:
        slug = slug.replace("--", "-")
    return slug.strip("-")


def write_seed(title: str, body: str) -> str:
    """Write a markdown seed to karma's seeds dir. Returns the slug."""
    seeds_dir = _seeds_dir()
    seeds_dir.mkdir(parents=True, exist_ok=True)

    slug = _slugify(title)
    filepath = seeds_dir / f"{slug}.md"

    if filepath.exists():
        ts = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
        slug = f"{slug}-{ts}"
        filepath = seeds_dir / f"{slug}.md"

    now = datetime.now(timezone.utc).isoformat()
    content = f"---\ntitle: {title}\ncreated_at: {now}\ntags: []\n---\n\n{body}"
    filepath.write_text(content, encoding="utf-8")
    logger.info(f"Karma seed written: {filepath.name}")
    return slug


def query(question: str, claude_path: str = None) -> str:
    """RAG query over all karma seeds via Claude CLI."""
    seeds_dir = _seeds_dir()
    if not seeds_dir.exists():
        return "No karma seeds yet."

    seed_files = sorted(seeds_dir.glob("*.md"))
    if not seed_files:
        return "No karma seeds yet."

    parts = []
    for sf in seed_files:
        try:
            text = sf.read_text(encoding="utf-8")
            title = sf.stem
            body = text
            if text.startswith("---"):
                chunks = text.split("---", maxsplit=2)
                if len(chunks) >= 3:
                    body = chunks[2].strip()
                    for line in chunks[1].splitlines():
                        if line.startswith("title:"):
                            title = line.split(":", 1)[1].strip()
                            break
            if body:
                parts.append(f"[{title}]\n{body}")
        except OSError:
            continue

    if not parts:
        return "No readable karma seeds."

    seeds_context = "\n\n".join(parts)
    prompt = (
        f"Personal knowledge seeds:\n\n{seeds_context}\n\n"
        f"Question: {question}\n\n"
        f"Answer concisely using only what's in the seeds. "
        f"If the answer isn't there, say so plainly."
    )

    if not claude_path:
        claude_path = os.path.expanduser("~/.local/bin/claude")

    try:
        result = subprocess.run(
            [claude_path, "-p", prompt, "--output-format", "text"],
            capture_output=True, text=True, timeout=90
        )
        if result.returncode == 0 and result.stdout.strip():
            return result.stdout.strip()
        logger.error(f"Karma query failed: {result.stderr[:200]}")
        return "Query failed — check logs."
    except subprocess.TimeoutExpired:
        return "Query timed out."
    except Exception as e:
        logger.error(f"Karma query error: {e}")
        return f"Query error: {e}"


def get_recent_seeds(n: int = 3) -> list:
    """Return titles of the n most recently written seeds."""
    seeds_dir = _seeds_dir()
    if not seeds_dir.exists():
        return []
    files = sorted(seeds_dir.glob("*.md"), key=lambda f: f.stat().st_mtime, reverse=True)
    titles = []
    for sf in files[:n]:
        title = sf.stem
        try:
            text = sf.read_text(encoding="utf-8")
            if text.startswith("---"):
                chunks = text.split("---", maxsplit=2)
                for line in chunks[1].splitlines():
                    if line.startswith("title:"):
                        title = line.split(":", 1)[1].strip()
                        break
        except OSError:
            pass
        titles.append(title)
    return titles


def get_status() -> dict:
    """Return seed count and karma points total."""
    seeds_dir = _seeds_dir()
    seed_count = len(list(seeds_dir.glob("*.md"))) if seeds_dir.exists() else 0
    points = 0
    points_file = _points_file()
    if points_file.exists():
        try:
            data = json.loads(points_file.read_text(encoding="utf-8"))
            points = data.get("total", 0)
        except (json.JSONDecodeError, OSError):
            pass
    return {"seeds": seed_count, "points": points}
flow-designglobal
SKILL.md74 lines
SKILL.md162 lines
pm-strategistglobal
pm-strategist.md51 lines
todosystem
bot31 items
integrations02 items
__init__.py1 lines
karma_client.py148 lines
agent_executor.py344 lines
behavior_state.py106 lines
BOT.md175 lines
config.py76 lines
git_sync.py87 lines
hyrule_server.py284 lines
manage_bot.sh47 lines
mcp_server.py324 lines
mobile_mode.py229 lines
navi_tab.py95 lines
prework_engine.py426 lines
run_bot.sh18 lines
run_hyrule.sh38 lines
scheduler.py958 lines
session.py264 lines
telegram_bot.py3328 lines
todo_manager.py1130 lines
ventures_signal.py314 lines
voice_handler.py203 lines
VOICE_SETUP.md112 lines
warroom_manager.py1004 lines
README.md44 lines
night-orderssystem
README.md84 lines
loops-spec.md212 lines
settings.json116 lines
README.md59 lines