dreaming-memory 0.2.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,18 @@
1
+ """Agent-agnostic memory extension — Postgres-backed agent memory."""
2
+
3
+ from dreaming_memory.agent_memory import AgentMemory
4
+ from dreaming_memory.config import FleetConfig
5
+ from dreaming_memory.session import SessionContext
6
+ from dreaming_memory.types import MemoryRecord, MemorySource, MemoryType, SessionType
7
+
8
+ __all__ = [
9
+ "AgentMemory",
10
+ "FleetConfig",
11
+ "MemoryRecord",
12
+ "MemorySource",
13
+ "MemoryType",
14
+ "SessionContext",
15
+ "SessionType",
16
+ ]
17
+
18
+ __version__ = "0.2.0"
@@ -0,0 +1,161 @@
1
+ """High-level facade for agents — Postgres SSOT + optional semantic + integrations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from uuid import UUID
7
+
8
+ from dreaming_memory.config import FleetConfig
9
+ from dreaming_memory.integrations.cloudflare import CloudflareR2
10
+ from dreaming_memory.integrations.linear import LinearMemoryBridge
11
+ from dreaming_memory.integrations.notion import NotionMemoryBridge
12
+ from dreaming_memory.observability import sentry
13
+ from dreaming_memory.semantic.lancedb import SemanticMemoryStore
14
+ from dreaming_memory.session import SessionContext
15
+ from dreaming_memory.store.postgres import AgentMemoryStore
16
+ from dreaming_memory.types import MemoryRecord, MemorySource, MemoryType, SessionType
17
+
18
+
19
+ class AgentMemory:
20
+ """
21
+ Main entry point for agent memory within cursor-dreaming-sdk.
22
+
23
+ Usage:
24
+ memory = AgentMemory()
25
+ memory.ensure_schema()
26
+ ctx = SessionContext.for_dream_eval("2026-06-15T09-00-00Z")
27
+ memory.remember(ctx, MemoryType.OBSERVATION, {"note": "faithfulness 0.63"}, source=MemorySource.SDK)
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ store: AgentMemoryStore | None = None,
33
+ semantic: SemanticMemoryStore | None = None,
34
+ linear: LinearMemoryBridge | None = None,
35
+ notion: NotionMemoryBridge | None = None,
36
+ config: FleetConfig | None = None,
37
+ enable_sentry: bool = True,
38
+ ) -> None:
39
+ self.config = config or FleetConfig.load()
40
+ self.store = store or AgentMemoryStore(self.config.database_url)
41
+ self.semantic = semantic
42
+ self._linear = linear
43
+ self._notion = notion
44
+ self._cloudflare: CloudflareR2 | None = None
45
+ if enable_sentry:
46
+ sentry.init_sentry(self.config)
47
+
48
+ def close(self) -> None:
49
+ self.store.close()
50
+
51
+ def __enter__(self) -> AgentMemory:
52
+ return self
53
+
54
+ def __exit__(self, *exc: Any) -> None:
55
+ self.close()
56
+
57
+ @property
58
+ def cloudflare(self) -> CloudflareR2:
59
+ if self._cloudflare is None:
60
+ self._cloudflare = CloudflareR2(self.config)
61
+ return self._cloudflare
62
+
63
+ def ensure_schema(self) -> None:
64
+ self.store.ensure_schema()
65
+
66
+ @property
67
+ def linear(self) -> LinearMemoryBridge:
68
+ if self._linear is None:
69
+ self._linear = LinearMemoryBridge(self.store)
70
+ return self._linear
71
+
72
+ @property
73
+ def notion(self) -> NotionMemoryBridge:
74
+ if self._notion is None:
75
+ self._notion = NotionMemoryBridge(self.store)
76
+ return self._notion
77
+
78
+ def remember(
79
+ self,
80
+ session: SessionContext,
81
+ memory_type: MemoryType,
82
+ content: dict[str, Any],
83
+ *,
84
+ source: MemorySource = MemorySource.SDK,
85
+ metadata: dict[str, Any] | None = None,
86
+ ) -> MemoryRecord:
87
+ record = MemoryRecord(
88
+ agent_id=session.agent_id,
89
+ session_id=session.session_id,
90
+ session_type=session.session_type,
91
+ memory_type=memory_type,
92
+ content=content,
93
+ source=source,
94
+ metadata={**(session.metadata or {}), **(metadata or {})},
95
+ )
96
+ sentry.breadcrumb(
97
+ f"remember {memory_type.value}",
98
+ data={"session_type": session.session_type.value, "source": source.value},
99
+ )
100
+ return self.store.write(record)
101
+
102
+ def recall(
103
+ self,
104
+ *,
105
+ agent_id: str | None = None,
106
+ session_id: str | None = None,
107
+ session_type: SessionType | None = None,
108
+ memory_type: MemoryType | None = None,
109
+ source: MemorySource | None = None,
110
+ limit: int = 50,
111
+ ) -> list[MemoryRecord]:
112
+ return self.store.query(
113
+ agent_id=agent_id,
114
+ session_id=session_id,
115
+ session_type=session_type,
116
+ memory_type=memory_type,
117
+ source=source,
118
+ limit=limit,
119
+ )
120
+
121
+ def recall_session(self, session: SessionContext, limit: int = 50) -> list[MemoryRecord]:
122
+ return self.recall(
123
+ agent_id=session.agent_id,
124
+ session_id=session.session_id,
125
+ session_type=session.session_type,
126
+ limit=limit,
127
+ )
128
+
129
+ def index_semantic(
130
+ self,
131
+ record: MemoryRecord,
132
+ text: str,
133
+ vector: list[float],
134
+ ) -> MemoryRecord | None:
135
+ if self.semantic is None:
136
+ raise RuntimeError("semantic store not configured — pass SemanticMemoryStore() to AgentMemory")
137
+ if not self.semantic.enabled:
138
+ return None
139
+ if record.id is None:
140
+ raise ValueError("record must have an id before indexing into semantic store")
141
+ self.semantic.index_memory(
142
+ record.id, text, vector, metadata={"source": record.source.value}
143
+ )
144
+ ref = MemoryRecord(
145
+ agent_id=record.agent_id,
146
+ session_id=record.session_id,
147
+ session_type=record.session_type,
148
+ memory_type=MemoryType.EMBEDDING_REF,
149
+ source=MemorySource.SDK,
150
+ content={"text_preview": text[:500], "parent_id": str(record.id)},
151
+ metadata={"lance_table": self.semantic.table_name},
152
+ )
153
+ return self.store.write(ref)
154
+
155
+ def search_semantic(self, vector: list[float], limit: int = 10) -> list[dict[str, Any]]:
156
+ if self.semantic is None or not self.semantic.enabled:
157
+ return []
158
+ return self.semantic.search(vector, limit=limit)
159
+
160
+ def get(self, memory_id: UUID) -> MemoryRecord | None:
161
+ return self.store.get(memory_id)
dreaming_memory/cli.py ADDED
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env python3
2
+ """CLI for agent memory — schema init, query, Linear/Notion ingest."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import sys
9
+
10
+ from dreaming_memory import AgentMemory, FleetConfig, SessionContext
11
+ from dreaming_memory.types import MemoryRecord, MemorySource, MemoryType, SessionType
12
+
13
+
14
+ def _print_records(records: list[MemoryRecord]) -> None:
15
+ for r in records:
16
+ print(json.dumps(r.model_dump(mode="json"), default=str))
17
+
18
+
19
+ def _enum_value(value: object) -> object:
20
+ """Return the underlying value for StrEnum members, pass others through."""
21
+ return value.value if hasattr(value, "value") else value
22
+
23
+
24
+ def render_export_markdown(session_id: str, records: list[MemoryRecord]) -> str:
25
+ """Render memory records for a session as Markdown (pure, DB-free)."""
26
+ lines: list[str] = [f"# Memory export — {session_id}", ""]
27
+ if not records:
28
+ lines.append("_No memory records found._")
29
+ return "\n".join(lines) + "\n"
30
+ for r in records:
31
+ memory_type = _enum_value(r.memory_type)
32
+ source = _enum_value(r.source)
33
+ created = r.created_at.isoformat() if r.created_at else "unknown"
34
+ lines.append(f"## {memory_type} ({created})")
35
+ lines.append(f"**Source:** {source}")
36
+ lines.append("")
37
+ lines.append("```json")
38
+ lines.append(json.dumps(r.content, indent=2, default=str, ensure_ascii=False))
39
+ lines.append("```")
40
+ lines.append("")
41
+ return "\n".join(lines).rstrip() + "\n"
42
+
43
+
44
+ def _doctor() -> None:
45
+ """Print config presence + Postgres/Linear/Notion connectivity (no secret values)."""
46
+ import httpx
47
+ import psycopg
48
+
49
+ config = FleetConfig.load()
50
+ status = config.status()
51
+ print(json.dumps({"config": status, "redacted": config.redacted()}, indent=2))
52
+ try:
53
+ memory = AgentMemory(config=config, enable_sentry=False)
54
+ with memory.store._pool.connection() as conn:
55
+ conn.execute("SELECT 1")
56
+ memory.store.close()
57
+ print('{"postgres": "ok"}')
58
+ except psycopg.Error as exc:
59
+ print(json.dumps({"postgres": "error", "detail": str(exc)}))
60
+
61
+ if config.linear_api_key:
62
+ try:
63
+ from dreaming_memory.integrations.linear import LinearClient
64
+
65
+ client = LinearClient(api_key=config.linear_api_key)
66
+ client.gql("query { viewer { id } }")
67
+ print('{"linear": "ok"}')
68
+ except httpx.HTTPError as exc:
69
+ print(json.dumps({"linear": "error", "detail": str(exc)}))
70
+ else:
71
+ print('{"linear": "missing_api_key"}')
72
+
73
+ if config.notion_api_key:
74
+ try:
75
+ from dreaming_memory.integrations.notion import NotionMemoryBridge
76
+ from dreaming_memory.store.postgres import AgentMemoryStore
77
+
78
+ NotionMemoryBridge(AgentMemoryStore(config.database_url))
79
+ print('{"notion": "wired"}')
80
+ except httpx.HTTPError as exc:
81
+ print(json.dumps({"notion": "error", "detail": str(exc)}))
82
+ else:
83
+ print('{"notion": "missing_api_key"}')
84
+
85
+
86
+ def main() -> None:
87
+ parser = argparse.ArgumentParser(description="cursor-dreaming-sdk agent memory CLI")
88
+ sub = parser.add_subparsers(dest="command", required=True)
89
+
90
+ sub.add_parser("init", help="Apply Postgres schema")
91
+ sub.add_parser("doctor", help="Show config presence + DB connectivity")
92
+
93
+ p_metrics = sub.add_parser("metrics", help="Print aggregated metrics as JSON")
94
+ p_metrics.add_argument("--days", type=int, default=14)
95
+
96
+ p_triage = sub.add_parser("triage", help="Auto-triage untriaged Linear issues")
97
+ p_triage.add_argument("--state", default=None, help="Filter by workflow state name")
98
+ p_triage.add_argument("--limit", type=int, default=50)
99
+ p_triage.add_argument("--apply", action="store_true", help="Write priority/labels to Linear")
100
+ p_triage.add_argument("--comment", action="store_true", help="Post rationale comment")
101
+
102
+ p_serve = sub.add_parser("serve", help="Run the metrics dashboard")
103
+ p_serve.add_argument("--host", default="0.0.0.0")
104
+ p_serve.add_argument("--port", type=int, default=8787)
105
+
106
+ p_remember = sub.add_parser("remember", help="Write a memory record")
107
+ p_remember.add_argument("--agent", default="default")
108
+ p_remember.add_argument("--session-id", required=True)
109
+ p_remember.add_argument("--session-type", default="generic")
110
+ p_remember.add_argument("--type", dest="memory_type", default="observation")
111
+ p_remember.add_argument("--source", default="sdk")
112
+ p_remember.add_argument("--content", required=True, help="JSON object")
113
+
114
+ p_recall = sub.add_parser("recall", help="Query memory records")
115
+ p_recall.add_argument("--agent")
116
+ p_recall.add_argument("--session-id")
117
+ p_recall.add_argument("--session-type")
118
+ p_recall.add_argument("--type", dest="memory_type")
119
+ p_recall.add_argument("--source")
120
+ p_recall.add_argument("--limit", type=int, default=20)
121
+
122
+ p_linear = sub.add_parser("linear-ingest", help="Ingest Linear issue into memory")
123
+ p_linear.add_argument("issue_id")
124
+ p_linear.add_argument("--session-id", default="linear-sync")
125
+ p_linear.add_argument("--session-type", default="cursor")
126
+
127
+ p_notion = sub.add_parser("notion-ingest", help="Ingest Notion page into memory")
128
+ p_notion.add_argument("page_id")
129
+ p_notion.add_argument("--session-id", default="notion-sync")
130
+ p_notion.add_argument("--session-type", default="cursor")
131
+
132
+ p_export = sub.add_parser("export", help="Export a session's memory as Markdown")
133
+ p_export.add_argument("--session-id", required=True)
134
+ p_export.add_argument("--agent", default=None)
135
+ p_export.add_argument("--output", default=None, help="Write Markdown to this file")
136
+
137
+ p_slack = sub.add_parser("slack-report", help="Post an eval report to Slack")
138
+ p_slack.add_argument("--run-id", default=None)
139
+ p_slack.add_argument("--metrics-json", default=None, help="Path to metrics JSON file")
140
+
141
+ args = parser.parse_args()
142
+
143
+ if args.command == "doctor":
144
+ _doctor()
145
+ return
146
+
147
+ if args.command == "metrics":
148
+ from dreaming_memory.store.postgres import AgentMemoryStore
149
+
150
+ with AgentMemoryStore() as store:
151
+ print(json.dumps(store.metrics(days=args.days), indent=2, default=str))
152
+ return
153
+
154
+ if args.command == "serve":
155
+ from dreaming_memory.dashboard import serve
156
+
157
+ serve(host=args.host, port=args.port)
158
+ return
159
+
160
+ if args.command == "triage":
161
+ from dreaming_memory.triage import AutoTriage
162
+
163
+ with AutoTriage() as triage:
164
+ results = triage.run(
165
+ state=args.state, limit=args.limit, apply=args.apply, comment=args.comment
166
+ )
167
+ for r in results:
168
+ print(json.dumps(r.to_content(), default=str))
169
+ print(f"# triaged {len(results)} issue(s); apply={args.apply}")
170
+ return
171
+
172
+ if args.command == "slack-report":
173
+ from dreaming_memory.integrations.slack import SlackClient
174
+
175
+ metrics: dict = {}
176
+ if args.metrics_json:
177
+ with open(args.metrics_json, encoding="utf-8") as fh:
178
+ metrics = json.load(fh)
179
+ sent = SlackClient().report_eval_result(metrics, run_id=args.run_id)
180
+ if sent:
181
+ print(json.dumps({"slack": "sent", "run_id": args.run_id}))
182
+ else:
183
+ print(json.dumps({"slack": "skipped", "reason": "no SLACK_WEBHOOK_URL configured"}))
184
+ return
185
+
186
+ if args.command == "export":
187
+ # Initialize the store/memory BEFORE use; do not rely on the shared
188
+ # `memory = AgentMemory()` defined later in this function.
189
+ with AgentMemory() as memory:
190
+ records = memory.recall(
191
+ session_id=args.session_id,
192
+ agent_id=args.agent,
193
+ limit=100,
194
+ )
195
+ markdown = render_export_markdown(args.session_id, records)
196
+ if args.output:
197
+ with open(args.output, "w", encoding="utf-8") as fh:
198
+ fh.write(markdown)
199
+ print(json.dumps({
200
+ "export": "written", "path": args.output, "records": len(records)
201
+ }))
202
+ else:
203
+ print(markdown)
204
+ return
205
+
206
+ with AgentMemory() as memory:
207
+ if args.command == "init":
208
+ memory.ensure_schema()
209
+ print("Schema applied.")
210
+ return
211
+
212
+ if args.command == "remember":
213
+ ctx = SessionContext(
214
+ session_id=args.session_id,
215
+ session_type=SessionType(args.session_type),
216
+ agent_id=args.agent,
217
+ )
218
+ record = memory.remember(
219
+ ctx,
220
+ MemoryType(args.memory_type),
221
+ json.loads(args.content),
222
+ source=MemorySource(args.source),
223
+ )
224
+ _print_records([record])
225
+ return
226
+
227
+ if args.command == "recall":
228
+ records = memory.recall(
229
+ agent_id=args.agent,
230
+ session_id=args.session_id,
231
+ session_type=SessionType(args.session_type) if args.session_type else None,
232
+ memory_type=MemoryType(args.memory_type) if args.memory_type else None,
233
+ source=MemorySource(args.source) if args.source else None,
234
+ limit=args.limit,
235
+ )
236
+ _print_records(records)
237
+ return
238
+
239
+ if args.command == "linear-ingest":
240
+ ctx = SessionContext(
241
+ session_id=args.session_id,
242
+ session_type=SessionType(args.session_type),
243
+ )
244
+ record = memory.linear.ingest_issue(args.issue_id, ctx)
245
+ _print_records([record])
246
+ return
247
+
248
+ if args.command == "notion-ingest":
249
+ ctx = SessionContext(
250
+ session_id=args.session_id,
251
+ session_type=SessionType(args.session_type),
252
+ )
253
+ record = memory.notion.ingest_page(args.page_id, ctx)
254
+ _print_records([record])
255
+ return
256
+
257
+ parser.print_help()
258
+ sys.exit(2)
259
+
260
+
261
+ if __name__ == "__main__":
262
+ main()
@@ -0,0 +1,130 @@
1
+ """Central config + secrets loader for the agent memory fleet.
2
+
3
+ Resolution order (first match wins):
4
+ 1. Process environment (os.environ)
5
+ 2. Fleet env file (~/.openclaude/.env — `export KEY="val"` lines, never committed)
6
+ 3. Per-host fallback (~/.config/agent-memory/.env)
7
+
8
+ Only read at runtime; secrets are never written to the repo.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import re
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+
18
+ _EXPORT_RE = re.compile(r'^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$')
19
+
20
+ _DEFAULT_SECRET_FILES = (
21
+ Path(os.environ.get("FLEET_ENV_FILE", "")) if os.environ.get("FLEET_ENV_FILE") else None,
22
+ Path.home() / ".openclaude" / ".env",
23
+ Path.home() / ".config" / "agent-memory" / ".env",
24
+ )
25
+
26
+
27
+ def _parse_env_file(path: Path | None) -> dict[str, str]:
28
+ if not path or not path.exists():
29
+ return {}
30
+ out: dict[str, str] = {}
31
+ for line in path.read_text(encoding="utf-8").splitlines():
32
+ line = line.strip()
33
+ if not line or line.startswith("#"):
34
+ continue
35
+ m = _EXPORT_RE.match(line)
36
+ if not m:
37
+ continue
38
+ key, val = m.group(1), m.group(2)
39
+ out[key] = val.strip().strip('"').strip("'")
40
+ return out
41
+
42
+
43
+ def load_fleet_secrets(extra_files: tuple[Path, ...] = ()) -> dict[str, str]:
44
+ """Load fleet env files into os.environ without overwriting existing vars."""
45
+ merged: dict[str, str] = {}
46
+ for path in (*_DEFAULT_SECRET_FILES, *extra_files):
47
+ merged.update(_parse_env_file(path))
48
+ for key, val in merged.items():
49
+ os.environ.setdefault(key, val)
50
+ return merged
51
+
52
+
53
+ def get_secret(key: str, default: str | None = None) -> str | None:
54
+ if key not in os.environ:
55
+ load_fleet_secrets()
56
+ return os.environ.get(key, default)
57
+
58
+
59
+ @dataclass
60
+ class FleetConfig:
61
+ """Resolved secrets/config for all integrations across the fleet."""
62
+
63
+ database_url: str | None = None
64
+ linear_api_key: str | None = None
65
+ linear_team_id: str | None = None
66
+ notion_api_key: str | None = None
67
+ cloudflare_api_token: str | None = None
68
+ cloudflare_account_id: str | None = None
69
+ sentry_dsn: str | None = None
70
+ sentry_environment: str = "development"
71
+ upstash_redis_url: str | None = None
72
+ upstash_redis_token: str | None = None
73
+ lance_db_uri: str | None = None
74
+ r2_bucket: str | None = None
75
+ r2_access_key_id: str | None = None
76
+ r2_secret_access_key: str | None = None
77
+
78
+ @classmethod
79
+ def load(cls) -> FleetConfig:
80
+ load_fleet_secrets()
81
+ return cls(
82
+ database_url=(
83
+ os.environ.get("AGENT_MEMORY_DATABASE_URL")
84
+ or os.environ.get("DATABASE_URL")
85
+ ),
86
+ linear_api_key=os.environ.get("LINEAR_API_KEY"),
87
+ linear_team_id=os.environ.get("LINEAR_TEAM_ID"),
88
+ notion_api_key=os.environ.get("NOTION_API_KEY") or os.environ.get("NOTION_TOKEN"),
89
+ cloudflare_api_token=os.environ.get("CLOUDFLARE_API_TOKEN"),
90
+ cloudflare_account_id=os.environ.get("CLOUDFLARE_ACCOUNT_ID"),
91
+ sentry_dsn=os.environ.get("SENTRY_DSN"),
92
+ sentry_environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
93
+ upstash_redis_url=os.environ.get("UPSTASH_REDIS_REST_URL"),
94
+ upstash_redis_token=os.environ.get("UPSTASH_REDIS_REST_TOKEN"),
95
+ lance_db_uri=os.environ.get("LANCE_DB_URI"),
96
+ r2_bucket=os.environ.get("R2_BUCKET")
97
+ or os.environ.get("AGENT_MEMORY_R2_BUCKET")
98
+ or "agent-memory",
99
+ r2_access_key_id=os.environ.get("R2_ACCESS_KEY_ID"),
100
+ r2_secret_access_key=os.environ.get("R2_SECRET_ACCESS_KEY"),
101
+ )
102
+
103
+ def status(self) -> dict[str, bool]:
104
+ """Boolean presence map — safe to log (no secret values)."""
105
+ return {
106
+ "database_url": bool(self.database_url),
107
+ "linear": bool(self.linear_api_key),
108
+ "notion": bool(self.notion_api_key),
109
+ "cloudflare": bool(self.cloudflare_api_token and self.cloudflare_account_id),
110
+ "sentry": bool(self.sentry_dsn),
111
+ "upstash": bool(self.upstash_redis_url and self.upstash_redis_token),
112
+ }
113
+
114
+ def redacted(self) -> dict[str, str]:
115
+ """Masked summary for logging."""
116
+
117
+ def mask(v: str | None) -> str:
118
+ if not v:
119
+ return "(unset)"
120
+ return v[:4] + "…" + v[-2:] if len(v) > 8 else "set"
121
+
122
+ return {
123
+ "database_url": re.sub(r":[^:@/]+@", ":***@", self.database_url or "(unset)"),
124
+ "linear_api_key": mask(self.linear_api_key),
125
+ "notion_api_key": mask(self.notion_api_key),
126
+ "cloudflare_api_token": mask(self.cloudflare_api_token),
127
+ "cloudflare_account_id": mask(self.cloudflare_account_id),
128
+ "sentry_dsn": "set" if self.sentry_dsn else "(unset)",
129
+ "lance_db_uri": self.lance_db_uri or "(unset)",
130
+ }