devmemory 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.
@@ -0,0 +1,71 @@
1
+ import pathlib
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from devmemory.core.config import DevMemoryConfig
7
+ from devmemory.core.sync_state import SyncState
8
+ from devmemory.core.git_ai_parser import get_repo_root, get_git_ai_version, is_git_ai_installed
9
+ from devmemory.core.ams_client import AMSClient
10
+ from devmemory import __version__
11
+
12
+ console = Console()
13
+
14
+
15
+ def run_status():
16
+ config = DevMemoryConfig.load()
17
+
18
+ table = Table(title="DevMemory Status", show_header=False, border_style="dim")
19
+ table.add_column("Key", style="bold")
20
+ table.add_column("Value")
21
+
22
+ table.add_row("DevMemory version", __version__)
23
+
24
+ git_ai_ok = is_git_ai_installed()
25
+ git_ai_ver = get_git_ai_version() if git_ai_ok else "not installed"
26
+ table.add_row("Git AI", f"[green]{git_ai_ver}[/green]" if git_ai_ok else "[red]not installed[/red]")
27
+
28
+ repo_root = get_repo_root()
29
+ table.add_row("Git repo", repo_root or "[red]not in a git repo[/red]")
30
+
31
+ client = AMSClient(base_url=config.ams_endpoint)
32
+ try:
33
+ health = client.health_check()
34
+ ams_status = f"[green]healthy[/green] (endpoint: {config.ams_endpoint})"
35
+ except Exception:
36
+ health = None
37
+ ams_status = f"[red]unreachable[/red] (endpoint: {config.ams_endpoint})"
38
+ table.add_row("AMS server", ams_status)
39
+
40
+ if health:
41
+ count = client.get_memory_count(namespace=config.namespace)
42
+ table.add_row("Memories stored", str(count) if count >= 0 else "[yellow]unknown[/yellow]")
43
+ else:
44
+ table.add_row("Memories stored", "[dim]N/A[/dim]")
45
+
46
+ table.add_row("Namespace", config.namespace)
47
+ table.add_row("User ID", config.user_id or "[dim]auto (from git)[/dim]")
48
+
49
+ if repo_root:
50
+ state = SyncState.load(repo_root)
51
+ if state.last_synced_sha:
52
+ table.add_row("Last synced commit", state.last_synced_sha[:12])
53
+ table.add_row("Last synced at", state.last_synced_at)
54
+ table.add_row("Total synced", str(state.total_synced))
55
+ else:
56
+ table.add_row("Sync state", "[yellow]never synced[/yellow]")
57
+
58
+ if repo_root:
59
+ hook_path = pathlib.Path(repo_root) / ".git" / "hooks" / "post-commit"
60
+ if hook_path.exists() and "devmemory" in hook_path.read_text():
61
+ table.add_row("Post-commit hook", "[green]installed[/green]")
62
+ else:
63
+ table.add_row("Post-commit hook", "[yellow]not installed[/yellow] (run: devmemory install)")
64
+
65
+ mcp_config = pathlib.Path.home() / ".cursor" / "mcp.json"
66
+ if mcp_config.exists() and "agent-memory" in mcp_config.read_text():
67
+ table.add_row("Cursor MCP config", "[green]configured[/green]")
68
+ else:
69
+ table.add_row("Cursor MCP config", "[yellow]not configured[/yellow] (run: devmemory install)")
70
+
71
+ console.print(table)
@@ -0,0 +1,125 @@
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+
5
+ from devmemory.core.config import DevMemoryConfig
6
+ from devmemory.core.sync_state import SyncState
7
+ from devmemory.core.git_ai_parser import (
8
+ get_repo_root,
9
+ get_ai_notes_since,
10
+ get_latest_commit_note,
11
+ )
12
+ from devmemory.core.memory_formatter import format_commit_as_memories, format_commit_without_ai
13
+ from devmemory.core.ams_client import AMSClient
14
+
15
+ console = Console()
16
+
17
+
18
+ def run_sync(
19
+ latest: bool = False,
20
+ all_commits: bool = False,
21
+ ai_only: bool = True,
22
+ dry_run: bool = False,
23
+ limit: int = 50,
24
+ quiet: bool = False,
25
+ ):
26
+ repo_root = get_repo_root()
27
+ if not repo_root:
28
+ if not quiet:
29
+ console.print("[red]Not inside a git repository.[/red]")
30
+ raise typer.Exit(1)
31
+
32
+ config = DevMemoryConfig.load()
33
+ state = SyncState.load(repo_root)
34
+
35
+ if latest:
36
+ note = get_latest_commit_note()
37
+ if not note:
38
+ if not quiet:
39
+ console.print("[yellow]No commits found.[/yellow]")
40
+ raise typer.Exit(0)
41
+ notes = [note]
42
+ else:
43
+ since_sha = None if all_commits else (state.last_synced_sha or None)
44
+ notes = get_ai_notes_since(since_sha, limit=limit)
45
+
46
+ if not notes:
47
+ if not quiet:
48
+ console.print("[green]Already up to date. No new commits to sync.[/green]")
49
+ raise typer.Exit(0)
50
+
51
+ if ai_only:
52
+ ai_notes = [n for n in notes if n.has_ai_note]
53
+ skipped = len(notes) - len(ai_notes)
54
+ if skipped > 0 and not quiet:
55
+ console.print(f"[dim]Skipping {skipped} commit(s) without AI notes.[/dim]")
56
+ notes_to_sync = ai_notes
57
+ else:
58
+ notes_to_sync = notes
59
+
60
+ if not notes_to_sync:
61
+ if not quiet:
62
+ console.print("[green]No commits with AI notes to sync.[/green]")
63
+ if notes:
64
+ state.mark_synced(notes[0].sha, count=0)
65
+ raise typer.Exit(0)
66
+
67
+ if not quiet:
68
+ table = Table(title=f"Commits to sync ({len(notes_to_sync)})")
69
+ table.add_column("SHA", style="cyan", width=12)
70
+ table.add_column("Subject", style="white")
71
+ table.add_column("AI Files", style="green", justify="right")
72
+ table.add_column("Prompts", style="magenta", justify="right")
73
+ table.add_column("Author", style="dim")
74
+
75
+ for n in notes_to_sync:
76
+ table.add_row(
77
+ n.sha[:12],
78
+ n.subject[:60],
79
+ str(len(n.files)),
80
+ str(len(n.prompts)),
81
+ n.author_name,
82
+ )
83
+ console.print(table)
84
+
85
+ if dry_run:
86
+ if not quiet:
87
+ console.print("[yellow]Dry run -- no memories sent.[/yellow]")
88
+ raise typer.Exit(0)
89
+
90
+ client = AMSClient(base_url=config.ams_endpoint)
91
+
92
+ try:
93
+ client.health_check()
94
+ except Exception as e:
95
+ if not quiet:
96
+ console.print(f"[red]Cannot reach AMS at {config.ams_endpoint}: {e}[/red]")
97
+ console.print("[dim]Is the Docker stack running? Try: make up[/dim]")
98
+ raise typer.Exit(1)
99
+
100
+ total_memories = 0
101
+ for n in notes_to_sync:
102
+ if n.has_ai_note:
103
+ memories = format_commit_as_memories(n, namespace=config.namespace, user_id=config.user_id)
104
+ else:
105
+ memories = format_commit_without_ai(n, namespace=config.namespace, user_id=config.user_id)
106
+
107
+ if memories:
108
+ try:
109
+ client.create_memories(memories)
110
+ total_memories += len(memories)
111
+ if not quiet:
112
+ console.print(f" [green]✓[/green] {n.sha[:12]} → {len(memories)} memory(s)")
113
+ except Exception as e:
114
+ if not quiet:
115
+ console.print(f" [red]✗[/red] {n.sha[:12]} → error: {e}")
116
+
117
+ newest_sha = notes_to_sync[0].sha
118
+ state.mark_synced(newest_sha, count=total_memories)
119
+
120
+ if quiet:
121
+ if total_memories > 0:
122
+ print(f"devmemory: synced {total_memories} memory(s) from {len(notes_to_sync)} commit(s)")
123
+ else:
124
+ console.print(f"\n[green]Synced {total_memories} memories from {len(notes_to_sync)} commit(s).[/green]")
125
+ console.print(f"[dim]Last synced: {newest_sha[:12]}[/dim]")
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+ from dataclasses import dataclass
5
+
6
+
7
+ @dataclass
8
+ class MemoryResult:
9
+ id: str
10
+ text: str
11
+ score: float
12
+ topics: list[str]
13
+ entities: list[str]
14
+ memory_type: str
15
+ created_at: str
16
+
17
+
18
+ class AMSClient:
19
+ def __init__(self, base_url: str = "http://localhost:8000", timeout: float = 30.0):
20
+ self.base_url = base_url.rstrip("/")
21
+ self.timeout = timeout
22
+
23
+ def _client(self) -> httpx.Client:
24
+ return httpx.Client(base_url=self.base_url, timeout=self.timeout)
25
+
26
+ def health_check(self) -> dict:
27
+ with self._client() as client:
28
+ resp = client.get("/v1/health")
29
+ resp.raise_for_status()
30
+ return resp.json()
31
+
32
+ def create_memories(
33
+ self,
34
+ memories: list[dict],
35
+ deduplicate: bool = True,
36
+ ) -> dict:
37
+ payload = {
38
+ "memories": memories,
39
+ "deduplicate": deduplicate,
40
+ }
41
+ with self._client() as client:
42
+ resp = client.post("/v1/long-term-memory/", json=payload)
43
+ resp.raise_for_status()
44
+ return resp.json()
45
+
46
+ def search_memories(
47
+ self,
48
+ text: str,
49
+ limit: int = 10,
50
+ namespace: str | None = None,
51
+ user_id: str | None = None,
52
+ topics: list[str] | None = None,
53
+ memory_type: str | None = None,
54
+ ) -> list[MemoryResult]:
55
+ payload: dict = {
56
+ "text": text,
57
+ "limit": limit,
58
+ }
59
+ if namespace:
60
+ payload["namespace"] = {"eq": namespace}
61
+ if user_id:
62
+ payload["user_id"] = {"eq": user_id}
63
+ if topics:
64
+ payload["topics"] = {"any": topics}
65
+ if memory_type:
66
+ payload["memory_type"] = {"eq": memory_type}
67
+
68
+ with self._client() as client:
69
+ resp = client.post("/v1/long-term-memory/search", json=payload)
70
+ resp.raise_for_status()
71
+ data = resp.json()
72
+
73
+ results = []
74
+ for m in data.get("memories", []):
75
+ results.append(MemoryResult(
76
+ id=m.get("id", ""),
77
+ text=m.get("text", ""),
78
+ score=m.get("dist", 0.0),
79
+ topics=m.get("topics") or [],
80
+ entities=m.get("entities") or [],
81
+ memory_type=m.get("memory_type", ""),
82
+ created_at=m.get("created_at", ""),
83
+ ))
84
+ return results
85
+
86
+ def get_memory_count(self, namespace: str | None = None) -> int:
87
+ try:
88
+ total = 0
89
+ offset = 0
90
+ with self._client() as client:
91
+ while True:
92
+ payload: dict = {"text": "", "limit": 100, "offset": offset}
93
+ if namespace:
94
+ payload["namespace"] = {"eq": namespace}
95
+ resp = client.post("/v1/long-term-memory/search", json=payload)
96
+ resp.raise_for_status()
97
+ data = resp.json()
98
+ total += len(data.get("memories", []))
99
+ if data.get("next_offset") is None:
100
+ break
101
+ offset = data["next_offset"]
102
+ return total
103
+ except Exception:
104
+ return -1
105
+
106
+ def list_sessions(self, namespace: str | None = None, limit: int = 50) -> list[str]:
107
+ params: dict = {"limit": limit}
108
+ if namespace:
109
+ params["namespace"] = namespace
110
+ with self._client() as client:
111
+ resp = client.get("/v1/working-memory/", params=params)
112
+ resp.raise_for_status()
113
+ return resp.json().get("sessions", [])
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from dataclasses import dataclass, field, asdict
6
+
7
+
8
+ CONFIG_DIR = Path.home() / ".devmemory"
9
+ CONFIG_FILE = CONFIG_DIR / "config.json"
10
+
11
+ DEFAULTS = {
12
+ "ams_endpoint": "http://localhost:8000",
13
+ "mcp_endpoint": "http://localhost:9050",
14
+ "namespace": "default",
15
+ "user_id": "",
16
+ }
17
+
18
+
19
+ @dataclass
20
+ class DevMemoryConfig:
21
+ ams_endpoint: str = DEFAULTS["ams_endpoint"]
22
+ mcp_endpoint: str = DEFAULTS["mcp_endpoint"]
23
+ namespace: str = DEFAULTS["namespace"]
24
+ user_id: str = DEFAULTS["user_id"]
25
+
26
+ @classmethod
27
+ def load(cls) -> DevMemoryConfig:
28
+ if CONFIG_FILE.exists():
29
+ raw = json.loads(CONFIG_FILE.read_text())
30
+ merged = {**DEFAULTS, **raw}
31
+ return cls(**{k: v for k, v in merged.items() if k in cls.__dataclass_fields__})
32
+ return cls()
33
+
34
+ def save(self) -> None:
35
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
36
+ CONFIG_FILE.write_text(json.dumps(asdict(self), indent=2) + "\n")
37
+
38
+ def set_value(self, key: str, value: str) -> None:
39
+ if key not in self.__dataclass_fields__:
40
+ raise KeyError(f"Unknown config key: {key}. Valid keys: {list(self.__dataclass_fields__)}")
41
+ setattr(self, key, value)
42
+ self.save()