am-memory 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,4 @@
1
+ from .store import MemoryStore
2
+
3
+ __version__ = "0.1.0"
4
+ __all__ = ["MemoryStore"]
agent_memory/cli.py ADDED
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env python3
2
+ """CLI entry point for am-memory.
3
+
4
+ Usage:
5
+ am init # one-time setup (MCP + hooks + CLAUDE.md)
6
+ am mcp # start MCP server (stdio)
7
+ am doc save --title T --content C [--priority P1] [--source hook]
8
+ am search --query Q [--max-tokens 1500] [--format inject|json]
9
+ am state set --key K --value V
10
+ am state get --key K
11
+ am session start --project P --topic T # prints session_id
12
+ am session end --session-id S [--summary "..."]
13
+ am session message --session-id S --role R --content C
14
+ am session resume --session-id S [--max-tokens 2000]
15
+ """
16
+ import sys
17
+ import json
18
+ import argparse
19
+ from agent_memory.store import MemoryStore
20
+
21
+ _store = None
22
+
23
+
24
+ def get_store() -> MemoryStore:
25
+ global _store
26
+ if _store is None:
27
+ _store = MemoryStore()
28
+ return _store
29
+
30
+
31
+ def cmd_doc(args):
32
+ if args.action == "save":
33
+ content = args.content or sys.stdin.read()
34
+ doc_id = get_store().save(
35
+ title=args.title,
36
+ content=content,
37
+ priority=getattr(args, "priority", "P1"),
38
+ source=getattr(args, "source", "hook"),
39
+ file_path=getattr(args, "file_path", None) or None,
40
+ )
41
+ print(doc_id)
42
+
43
+ elif args.action == "prune":
44
+ deleted = get_store().prune_expired()
45
+ print(f"Pruned {deleted} expired document(s)")
46
+ return
47
+
48
+ elif args.action == "enhance":
49
+ import sqlite3
50
+ import json as _json
51
+ from agent_memory.db import DB_PATH
52
+ from agent_memory.llm_extract import llm_extract
53
+ from agent_memory.vector import embed_doc, vec_to_blob
54
+
55
+ source_filter = getattr(args, "source", None) or None
56
+ force = getattr(args, "force", False)
57
+
58
+ conn = sqlite3.connect(str(DB_PATH))
59
+ conn.row_factory = sqlite3.Row
60
+
61
+ q = "SELECT doc_id, title, raw_content, generator FROM documents WHERE raw_content IS NOT NULL"
62
+ params = []
63
+ if not force:
64
+ q += " AND generator != 'llm'"
65
+ if source_filter:
66
+ q += " AND source = ?"
67
+ params.append(source_filter)
68
+
69
+ docs = conn.execute(q, params).fetchall()
70
+ total = len(docs)
71
+ print(f"Enhancing {total} docs", flush=True)
72
+
73
+ ok = fb = 0
74
+ for i, row in enumerate(docs, 1):
75
+ content = row["raw_content"] or ""
76
+ title = row["title"] or ""
77
+ doc_id = row["doc_id"]
78
+ llm_fields = llm_extract(content, title_hint=title)
79
+ if llm_fields:
80
+ conn.execute(
81
+ "UPDATE documents SET title=?, summary=?, key_facts=?, decisions=?, generator='llm' WHERE doc_id=?",
82
+ (llm_fields["title"] or title, llm_fields["summary"],
83
+ _json.dumps(llm_fields["key_facts"]), _json.dumps(llm_fields["decisions"]), doc_id),
84
+ )
85
+ use = llm_fields
86
+ ok += 1
87
+ else:
88
+ use = {"title": title, "summary": "", "key_facts": []}
89
+ fb += 1
90
+ vec = embed_doc(use.get("title") or title, use.get("summary") or "", use.get("key_facts") or [])
91
+ if vec:
92
+ conn.execute("UPDATE documents SET embedding=? WHERE doc_id=?", (vec_to_blob(vec), doc_id))
93
+ conn.commit()
94
+ print(f"[{i}/{total}] {'llm' if llm_fields else 'rule'} {title[:55]}", flush=True)
95
+
96
+ print(f"Done: {ok} llm {fb} fallback")
97
+ conn.close()
98
+
99
+
100
+ def cmd_search(args):
101
+ results = get_store().search(args.query, max_results=5)
102
+ fmt = getattr(args, "format", "inject")
103
+ if fmt == "json":
104
+ print(json.dumps([{
105
+ "id": r.id, "type": r.type,
106
+ "l1": r.l1, "l2": r.l2,
107
+ "score": r.score, "priority": r.priority,
108
+ } for r in results]))
109
+ else:
110
+ # max_tokens governs inject() token budget, not search()
111
+ print(get_store().inject(results,
112
+ max_tokens=getattr(args, "max_tokens", 3000)))
113
+
114
+
115
+ def cmd_state(args):
116
+ s = get_store()
117
+ if args.action == "set":
118
+ try:
119
+ value = json.loads(args.value)
120
+ except (json.JSONDecodeError, TypeError):
121
+ value = args.value
122
+ s.state.set(args.key, value)
123
+ elif args.action == "get":
124
+ val = s.state.get(args.key)
125
+ if val is None:
126
+ sys.exit(1)
127
+ print(json.dumps(val) if not isinstance(val, str) else val)
128
+
129
+
130
+ def cmd_session(args):
131
+ s = get_store()
132
+ if args.action == "start":
133
+ sid = s.session.start(
134
+ project=getattr(args, "project", ""),
135
+ topic=getattr(args, "topic", ""),
136
+ source=getattr(args, "source", ""),
137
+ )
138
+ print(sid)
139
+ elif args.action == "latest":
140
+ sid = s.session.get_latest_session_id(
141
+ source=getattr(args, "source", None) or None,
142
+ project=getattr(args, "project", None) or None,
143
+ )
144
+ if sid:
145
+ print(sid)
146
+ else:
147
+ sys.exit(1)
148
+ elif args.action == "end":
149
+ s.session.end(
150
+ session_id=args.session_id,
151
+ summary=getattr(args, "summary", None),
152
+ )
153
+ elif args.action == "message":
154
+ s.session.save_message(args.session_id, args.role, args.content)
155
+ elif args.action == "resume":
156
+ ctx = s.session.get_resume_context(
157
+ args.session_id,
158
+ max_tokens=getattr(args, "max_tokens", 2000),
159
+ )
160
+ print(json.dumps(ctx))
161
+ elif args.action == "delete":
162
+ s.session.delete(args.session_id)
163
+ elif args.action == "checkpoint":
164
+ result = s.session.checkpoint(args.session_id)
165
+ print(json.dumps(result))
166
+ elif args.action == "list":
167
+ rows = s.session.list_for_dashboard(
168
+ limit=getattr(args, "limit", 100),
169
+ include_cli=getattr(args, "include_cli", False),
170
+ )
171
+ print(json.dumps(rows))
172
+
173
+
174
+ def cmd_namespace(args):
175
+ s = get_store()
176
+ if args.action == "list":
177
+ namespaces = s.namespace_list()
178
+ if getattr(args, "format", "text") == "json":
179
+ print(json.dumps(namespaces))
180
+ else:
181
+ for ns in namespaces:
182
+ proj = ns["project"]
183
+ count = ns["doc_count"]
184
+ print(f" {proj:30s} {count:4d} docs")
185
+ elif args.action == "stats":
186
+ name = getattr(args, "name", None)
187
+ if not name:
188
+ print("Error: --name required for stats", file=sys.stderr)
189
+ sys.exit(1)
190
+ stats = s.namespace_stats(name)
191
+ print(f"Project: {stats['project']}")
192
+ print(f" Documents: {stats['doc_count']}")
193
+ print(f" P0: {stats['p0_count']} P1: {stats['p1_count']} P2: {stats['p2_count']}")
194
+
195
+
196
+ def cmd_config(args):
197
+ if args.action == "set":
198
+ key = args.key
199
+ value = args.value
200
+ if key == "embedding_provider":
201
+ from agent_memory.embedding import set_provider, _PROVIDERS
202
+ if value not in _PROVIDERS:
203
+ print(f"Error: unknown provider '{value}'. Available: {', '.join(_PROVIDERS)}", file=sys.stderr)
204
+ sys.exit(1)
205
+ set_provider(value)
206
+ print(f"Embedding provider set to: {value}")
207
+ old_dims = get_store() # trigger dimension check warning
208
+ else:
209
+ print(f"Error: unknown config key '{key}'", file=sys.stderr)
210
+ sys.exit(1)
211
+ elif args.action == "get":
212
+ from agent_memory.embedding import _load_config
213
+ config = _load_config()
214
+ key = args.key
215
+ if key:
216
+ val = config.get(key, "(not set)")
217
+ print(f"{key}: {val}")
218
+ else:
219
+ print(json.dumps(config, indent=2))
220
+
221
+
222
+ def cmd_dream(args):
223
+ from agent_memory.dream import Dreamer, DreamLock
224
+ conn = get_store()._conn
225
+ force = getattr(args, "force", False)
226
+ min_hours = getattr(args, "min_hours", 24)
227
+ min_sessions = getattr(args, "min_sessions", 5)
228
+
229
+ dry_run = getattr(args, "dry_run", False)
230
+ dreamer = Dreamer(
231
+ conn=conn,
232
+ min_hours=0 if force else min_hours,
233
+ min_sessions=0 if force else min_sessions,
234
+ )
235
+
236
+ if getattr(args, "status", False):
237
+ lock = DreamLock()
238
+ hours = lock.hours_since_last()
239
+ sessions_since = dreamer._count_sessions_since(lock.last_dream_at())
240
+ print(f"Last dream: {'never' if hours == float('inf') else f'{hours:.1f}h ago'}")
241
+ print(f"Sessions since: {sessions_since}")
242
+ print(f"Gate: {'PASS' if hours >= min_hours and sessions_since >= min_sessions else 'BLOCKED'}")
243
+ return
244
+
245
+ result = dreamer.run(force=force, dry_run=dry_run)
246
+
247
+ if result.success:
248
+ prefix = "[DRY RUN] " if dry_run else ""
249
+ print(f"{prefix}Dream complete ({result.duration_ms}ms)")
250
+ print(f" Sessions reviewed: {result.sessions_reviewed}")
251
+ print(f" Patterns found: {result.patterns_found}")
252
+ print(f" Contradictions resolved: {result.contradictions_resolved}")
253
+ print(f" Documents created: {result.documents_created}")
254
+ print(f" Documents updated: {result.documents_updated}")
255
+ print(f" Documents pruned: {result.documents_pruned}")
256
+ print(f" Stale detected: {result.stale_detected}")
257
+ print(f" Cross-contradictions resolved: {result.cross_contradictions_resolved}")
258
+ print(f" Redundant merged: {result.redundant_merged}")
259
+ if result.planned_actions:
260
+ print(" Planned actions:")
261
+ for action in result.planned_actions:
262
+ atype = action.get("type", "?")
263
+ detail = action.get("project") or action.get("title") or action.get("topic", "")
264
+ print(f" {atype:20s} {str(detail):40s}")
265
+ else:
266
+ print(f"Dream skipped: {result.reason or ', '.join(result.errors)}")
267
+
268
+
269
+ def cmd_mcp(_args):
270
+ from agent_memory.mcp_server import run
271
+ run()
272
+
273
+
274
+ def cmd_serve(args):
275
+ from agent_memory.mcp_server import run
276
+ transport = getattr(args, "transport", "sse")
277
+ port = getattr(args, "port", 3333)
278
+ read_only = getattr(args, "read_only", False)
279
+ run(transport=transport, port=port, read_only=read_only)
280
+
281
+
282
+ def cmd_init(_args):
283
+ from agent_memory.init_cmd import run
284
+ run()
285
+
286
+
287
+ def cmd_dashboard(args):
288
+ from agent_memory.dashboard import start
289
+ port = getattr(args, "port", 8420)
290
+ allow_edits = getattr(args, "allow_edits", False)
291
+ start(port=port, allow_edits=allow_edits)
292
+
293
+
294
+ def cmd_status(args):
295
+ from agent_memory.watch import status_line
296
+ status_line(
297
+ event=getattr(args, "event", "idle"),
298
+ detail=getattr(args, "detail", ""),
299
+ )
300
+
301
+
302
+ def main():
303
+ from agent_memory import __version__
304
+ p = argparse.ArgumentParser(prog="am")
305
+ p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
306
+ sub = p.add_subparsers(dest="cmd")
307
+
308
+ # doc save
309
+ doc_p = sub.add_parser("doc")
310
+ doc_p.add_argument("action", choices=["save", "enhance", "prune"])
311
+ doc_p.add_argument("--title", default="")
312
+ doc_p.add_argument("--content", default=None)
313
+ doc_p.add_argument("--priority", default="P1")
314
+ doc_p.add_argument(
315
+ "--source",
316
+ default="hook",
317
+ choices=[
318
+ "architectural_decision", "debug_solution", "technical_insight",
319
+ "session_note", "routine",
320
+ "hook", "session_extract", "explicit",
321
+ ],
322
+ )
323
+ doc_p.add_argument("--file-path", default=None, dest="file_path")
324
+ doc_p.add_argument("--force", action="store_true", help="Re-enhance already-llm docs")
325
+
326
+ # search
327
+ s_p = sub.add_parser("search")
328
+ s_p.add_argument("--query", required=True)
329
+ s_p.add_argument("--max-tokens", type=int, default=1500, dest="max_tokens")
330
+ s_p.add_argument("--format", default="inject", choices=["inject", "json"])
331
+
332
+ # state
333
+ st_p = sub.add_parser("state")
334
+ st_p.add_argument("action", choices=["set", "get"])
335
+ st_p.add_argument("--key", required=True)
336
+ st_p.add_argument("--value", default=None)
337
+
338
+ # session
339
+ se_p = sub.add_parser("session")
340
+ se_p.add_argument("action", choices=["start", "end", "delete", "message", "resume", "latest", "list", "checkpoint"])
341
+ se_p.add_argument("--project", default="")
342
+ se_p.add_argument("--topic", default="")
343
+ se_p.add_argument("--session-id", default=None, dest="session_id")
344
+ se_p.add_argument("--role", default="user")
345
+ se_p.add_argument("--content", default="")
346
+ se_p.add_argument("--summary", default=None)
347
+ se_p.add_argument("--max-tokens", type=int, default=2000, dest="max_tokens")
348
+ se_p.add_argument("--source", default="", dest="source")
349
+ se_p.add_argument("--limit", type=int, default=100)
350
+ se_p.add_argument("--include-cli", action="store_true", dest="include_cli")
351
+
352
+ # dream
353
+ dr_p = sub.add_parser("dream", help="Background memory consolidation")
354
+ dr_p.add_argument("--force", action="store_true", help="Skip gate checks")
355
+ dr_p.add_argument("--dry-run", action="store_true", dest="dry_run", help="Show planned actions without writing")
356
+ dr_p.add_argument("--status", action="store_true", help="Show dream status")
357
+ dr_p.add_argument("--min-hours", type=float, default=24, dest="min_hours")
358
+ dr_p.add_argument("--min-sessions", type=int, default=5, dest="min_sessions")
359
+
360
+ # status (called by hooks to show inline feedback)
361
+ status_p = sub.add_parser("status", help="Print one-line colored status (used by hooks)")
362
+ status_p.add_argument("--event", default="idle",
363
+ choices=["session", "checkpoint", "message", "save", "search", "prune", "error", "idle"])
364
+ status_p.add_argument("--detail", default="")
365
+
366
+ # config
367
+ cfg_p = sub.add_parser("config", help="Get/set am-memory configuration")
368
+ cfg_p.add_argument("action", choices=["set", "get"])
369
+ cfg_p.add_argument("--key", default=None, help="Config key (e.g. embedding_provider)")
370
+ cfg_p.add_argument("--value", default=None, help="Config value")
371
+
372
+ # namespace
373
+ ns_p = sub.add_parser("namespace", help="Manage project namespaces")
374
+ ns_p.add_argument("action", choices=["list", "stats"])
375
+ ns_p.add_argument("--name", default=None, help="Project name for stats")
376
+ ns_p.add_argument("--format", default="text", choices=["text", "json"])
377
+
378
+ # dashboard
379
+ dash_p = sub.add_parser("dashboard", help="Start web dashboard for knowledge base")
380
+ dash_p.add_argument("--port", type=int, default=8420)
381
+ dash_p.add_argument("--allow-edits", action="store_true", dest="allow_edits",
382
+ help="Enable delete and priority changes from UI")
383
+
384
+ # serve (SSE transport for cross-tool access)
385
+ serve_p = sub.add_parser("serve", help="Start MCP server with SSE transport")
386
+ serve_p.add_argument("--transport", default="sse", choices=["stdio", "sse"])
387
+ serve_p.add_argument("--port", type=int, default=3333, help="HTTP port for SSE (default 3333)")
388
+ serve_p.add_argument("--read-only", action="store_true", dest="read_only",
389
+ help="Disable write tools (am_save, am_state_set)")
390
+
391
+ sub.add_parser("init", help="One-time setup: MCP registration, hooks, CLAUDE.md")
392
+ sub.add_parser("mcp", help="Start MCP server (stdio transport)")
393
+ args = p.parse_args()
394
+
395
+ if args.cmd == "init":
396
+ cmd_init(args)
397
+ elif args.cmd == "mcp":
398
+ cmd_mcp(args)
399
+ elif args.cmd == "doc":
400
+ cmd_doc(args)
401
+ elif args.cmd == "search":
402
+ cmd_search(args)
403
+ elif args.cmd == "state":
404
+ cmd_state(args)
405
+ elif args.cmd == "session":
406
+ cmd_session(args)
407
+ elif args.cmd == "config":
408
+ cmd_config(args)
409
+ elif args.cmd == "namespace":
410
+ cmd_namespace(args)
411
+ elif args.cmd == "dream":
412
+ cmd_dream(args)
413
+ elif args.cmd == "dashboard":
414
+ cmd_dashboard(args)
415
+ elif args.cmd == "serve":
416
+ cmd_serve(args)
417
+ elif args.cmd == "status":
418
+ cmd_status(args)
419
+ else:
420
+ p.print_help()
421
+ sys.exit(1)
422
+
423
+
424
+ if __name__ == "__main__":
425
+ main()