memory-fabric 0.4.1__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,41 @@
1
+ """Memory Fabric public package API."""
2
+
3
+ from memory_fabric.version import __version__
4
+ from memory_fabric.storage import (
5
+ doctor,
6
+ dream,
7
+ initialize_memory_fabric,
8
+ keyword_search,
9
+ propose_memory_patch,
10
+ read_combined_context,
11
+ read_section,
12
+ rollback,
13
+ status,
14
+ write_local_memory,
15
+ prepare_dream_payload,
16
+ apply_dream_results,
17
+ )
18
+ from memory_fabric.eval import (
19
+ evaluate_dream_quality,
20
+ evaluate_memory_fabric,
21
+ evaluate_memory_quality,
22
+ )
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "doctor",
27
+ "dream",
28
+ "evaluate_dream_quality",
29
+ "evaluate_memory_fabric",
30
+ "evaluate_memory_quality",
31
+ "initialize_memory_fabric",
32
+ "keyword_search",
33
+ "propose_memory_patch",
34
+ "read_combined_context",
35
+ "read_section",
36
+ "rollback",
37
+ "status",
38
+ "write_local_memory",
39
+ "prepare_dream_payload",
40
+ "apply_dream_results",
41
+ ]
memory_fabric/cli.py ADDED
@@ -0,0 +1,397 @@
1
+ """Command line interface for Memory Fabric."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from memory_fabric.version import __version__
13
+ from memory_fabric.contracts import DreamEvalResult, EvalResult
14
+ from memory_fabric.eval import evaluate_dream_quality, evaluate_memory_fabric
15
+ from memory_fabric.paths import local_memory_dir
16
+ from memory_fabric.storage import (
17
+ delete_memory_store,
18
+ doctor,
19
+ dream,
20
+ initialize_memory_fabric,
21
+ keyword_search,
22
+ list_memory_store,
23
+ propose_memory_patch,
24
+ read_memory_store,
25
+ rollback,
26
+ status,
27
+ write_memory_store,
28
+ sync_agent_rules,
29
+ )
30
+
31
+
32
+ def main(argv: list[str] | None = None) -> int:
33
+ parser = build_parser()
34
+ args = parser.parse_args(argv)
35
+
36
+ if getattr(args, "debug_llm", False):
37
+ import os
38
+
39
+ os.environ["MEMORY_FABRIC_LLM_DEBUG"] = "1"
40
+
41
+ cwd = str(Path(args.cwd).expanduser().resolve())
42
+
43
+ from memory_fabric.llm import load_env_from_cwd
44
+
45
+ load_env_from_cwd(cwd)
46
+
47
+ try:
48
+ if args.command == "init":
49
+ init_result = initialize_memory_fabric(
50
+ cwd,
51
+ install_hooks=args.install_hooks,
52
+ memory_prompt=args.memory_prompt,
53
+ )
54
+ _print_result(init_result, args.json)
55
+ return 0
56
+ if args.command == "status":
57
+ _print_result(status(cwd), args.json)
58
+ return 0
59
+ if args.command == "doctor":
60
+ doctor_result = doctor(cwd)
61
+ _print_result(doctor_result, args.json)
62
+ return 0 if doctor_result["ok"] else 1
63
+ if args.command == "dream":
64
+ dream_result = asyncio.run(
65
+ dream(
66
+ cwd,
67
+ mode=args.mode,
68
+ apply=args.apply,
69
+ llm_rewrite=args.llm_rewrite,
70
+ max_rewrite_tasks=args.max_rewrite_tasks,
71
+ )
72
+ )
73
+ if args.eval and args.apply:
74
+ dream_result["evaluation"] = asyncio.run(
75
+ evaluate_dream_quality(
76
+ cwd,
77
+ snapshot=dream_result["snapshot"] or "latest",
78
+ save_report=True,
79
+ llm_review=args.llm_review,
80
+ )
81
+ )
82
+ elif args.eval and not args.apply:
83
+ dream_result["warnings"].append(
84
+ "Dream evaluation requires --apply because candidate mode does not mutate live memory."
85
+ )
86
+ _print_result(dream_result, args.json)
87
+ return 0
88
+ if args.command == "eval":
89
+ eval_result: EvalResult | DreamEvalResult
90
+ if args.dream_snapshot:
91
+ eval_result = asyncio.run(
92
+ evaluate_dream_quality(
93
+ cwd,
94
+ snapshot=args.dream_snapshot,
95
+ save_report=not args.no_save,
96
+ llm_review=args.llm_review,
97
+ )
98
+ )
99
+ else:
100
+ eval_result = asyncio.run(
101
+ evaluate_memory_fabric(
102
+ cwd,
103
+ save_report=not args.no_save,
104
+ llm_review=args.llm_review,
105
+ )
106
+ )
107
+ _print_result(eval_result, args.json)
108
+ return 0 if eval_result["status"] != "fail" else 1
109
+ if args.command == "query":
110
+ query_result = keyword_search(cwd, args.query, max_results=args.max_results)
111
+ _print_result(query_result, args.json)
112
+ return 0
113
+ if args.command == "sync-agents":
114
+ sync_result = sync_agent_rules(cwd)
115
+ _print_result(sync_result, args.json)
116
+ return 0 if sync_result.get("success") else 1
117
+ if args.command == "sync-global":
118
+ memory_dir = local_memory_dir(cwd)
119
+ if not args.json and sys.stdin.isatty() and memory_dir.exists():
120
+ from memory_fabric.paths import global_memory_dir
121
+ from memory_fabric.storage import (
122
+ _iter_markdown_files,
123
+ _is_ignored_local_memory_path,
124
+ )
125
+ from memory_fabric.locking import locked_file
126
+ import shutil
127
+
128
+ promoted_count = 0
129
+ local_files = [
130
+ path
131
+ for path in _iter_markdown_files(memory_dir)
132
+ if path.name != "index.md"
133
+ and not _is_ignored_local_memory_path(memory_dir, path)
134
+ ]
135
+
136
+ if not local_files:
137
+ print("No local memory files found to promote.")
138
+ return 0
139
+
140
+ print("Interactive Global Memory Sync:")
141
+ print("===============================")
142
+ for path in local_files:
143
+ rel_name = path.name
144
+ choice = (
145
+ input(f"Promote local/{rel_name} to global/{rel_name}? [y/N]: ")
146
+ .strip()
147
+ .lower()
148
+ )
149
+ if choice in {"y", "yes"}:
150
+ target_dir = global_memory_dir()
151
+ target_dir.mkdir(parents=True, exist_ok=True)
152
+ target_path = target_dir / rel_name
153
+
154
+ action = "copy"
155
+ if target_path.exists():
156
+ overwrite = (
157
+ input(f" global/{rel_name} already exists. Overwrite? [y/N]: ")
158
+ .strip()
159
+ .lower()
160
+ )
161
+ if overwrite in {"y", "yes"}:
162
+ action = "copy"
163
+ else:
164
+ append = (
165
+ input(f" Append content to global/{rel_name} instead? [y/N]: ")
166
+ .strip()
167
+ .lower()
168
+ )
169
+ if append in {"y", "yes"}:
170
+ action = "append"
171
+ else:
172
+ action = "skip"
173
+
174
+ if action == "copy":
175
+ with locked_file(target_path):
176
+ shutil.copy2(path, target_path)
177
+ print(f" -> Promoted to {target_path}")
178
+ promoted_count += 1
179
+ elif action == "append":
180
+ from memory_fabric.frontmatter import (
181
+ parse_frontmatter,
182
+ dump_frontmatter,
183
+ )
184
+
185
+ with locked_file(target_path):
186
+ local_meta, local_body = parse_frontmatter(
187
+ path.read_text(encoding="utf-8")
188
+ )
189
+ global_meta, global_body = parse_frontmatter(
190
+ target_path.read_text(encoding="utf-8")
191
+ )
192
+ new_body = global_body.rstrip() + "\n\n" + local_body.lstrip()
193
+ global_meta["last_updated"] = local_meta.get("last_updated", "")
194
+ target_path.write_text(
195
+ dump_frontmatter(global_meta, new_body), encoding="utf-8"
196
+ )
197
+ print(f" -> Appended to {target_path}")
198
+ promoted_count += 1
199
+ else:
200
+ print(f" -> Skipped global/{rel_name}")
201
+
202
+ print(f"\nSync complete. Promoted {promoted_count} section(s) to global memory.")
203
+ return 0
204
+ else:
205
+ preview = propose_memory_patch(
206
+ cwd,
207
+ "Review local memory for durable rules before manually promoting them to global memory.",
208
+ )
209
+ _print_result(
210
+ {
211
+ "message": "Global sync is preview-only in v1 when non-interactive or JSON requested. Review local memory before promoting.",
212
+ "preview": preview,
213
+ },
214
+ args.json,
215
+ )
216
+ return 0
217
+ if args.command == "rollback":
218
+ rollback_result = rollback(cwd, args.to)
219
+ _print_result(rollback_result, args.json)
220
+ return 0
221
+ if args.command == "store":
222
+ store_action = getattr(args, "store_action", None)
223
+ if store_action == "write":
224
+ tag_list = (
225
+ [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else None
226
+ )
227
+ store_write_result = write_memory_store(
228
+ cwd,
229
+ store_path=args.store_path,
230
+ content=args.content,
231
+ title=args.title,
232
+ tags=tag_list,
233
+ priority=args.priority,
234
+ mode=args.mode,
235
+ )
236
+ _print_result(store_write_result, args.json)
237
+ return 0
238
+ elif store_action == "read":
239
+ store_read_result = read_memory_store(cwd, store_path=args.store_path)
240
+ _print_result(store_read_result, args.json)
241
+ return 0
242
+ elif store_action == "list":
243
+ tag_list = (
244
+ [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else None
245
+ )
246
+ store_list_result = list_memory_store(
247
+ cwd,
248
+ prefix=args.prefix,
249
+ tags=tag_list,
250
+ max_results=args.max_results,
251
+ )
252
+ _print_result(store_list_result, args.json)
253
+ return 0
254
+ elif store_action == "delete":
255
+ store_delete_result = delete_memory_store(cwd, store_path=args.store_path)
256
+ _print_result(store_delete_result, args.json)
257
+ return 0
258
+ else:
259
+ parser.parse_args(["store", "--help"])
260
+ return 1
261
+ except Exception as exc: # noqa: BLE001 - CLI should surface all operational failures.
262
+ print(f"ai-memory: {exc}", file=sys.stderr)
263
+ return 1
264
+
265
+ parser.print_help()
266
+ return 1
267
+
268
+
269
+ def build_parser() -> argparse.ArgumentParser:
270
+ parser = argparse.ArgumentParser(prog="ai-memory", description="Memory Fabric CLI")
271
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
272
+ parser.add_argument("--cwd", default=".", help="Project working directory")
273
+ parser.add_argument("--json", action="store_true", help="Print JSON output")
274
+ parser.add_argument(
275
+ "--debug-llm", action="store_true", help="Enable LLM prompt and response logging"
276
+ )
277
+
278
+ subparsers = parser.add_subparsers(dest="command", required=True)
279
+ init_parser = subparsers.add_parser("init", help="Create .ai-memory scaffolding")
280
+ init_parser.add_argument(
281
+ "--install-hooks", action="store_true", help="Install opt-in git hooks"
282
+ )
283
+ init_parser.add_argument(
284
+ "--memory-prompt", default=None, help="Steering instructions for agent memory capture"
285
+ )
286
+ subparsers.add_parser("status", help="Show memory status")
287
+ subparsers.add_parser("doctor", help="Validate memory files and environment")
288
+
289
+ dream_parser = subparsers.add_parser("dream", help="Run local memory maintenance")
290
+ dream_parser.add_argument("--mode", choices=["light", "deep"], default="light")
291
+ dream_parser.add_argument(
292
+ "--apply", action="store_true", help="Apply candidate changes to live .ai-memory files"
293
+ )
294
+ dream_parser.add_argument(
295
+ "--llm-rewrite",
296
+ action="store_true",
297
+ help="Generate agent-assisted rewrite tasks from Dreaming output",
298
+ )
299
+ dream_parser.add_argument("--max-rewrite-tasks", type=int, default=5)
300
+ dream_parser.add_argument(
301
+ "--eval", action="store_true", help="Evaluate quality before and after Dreaming"
302
+ )
303
+ dream_parser.add_argument(
304
+ "--llm-review", action="store_true", help="Add optional qualitative LLM review notes"
305
+ )
306
+
307
+ eval_parser = subparsers.add_parser("eval", help="Evaluate memory and Dreaming quality")
308
+ eval_parser.add_argument(
309
+ "--llm-review", action="store_true", help="Add optional qualitative LLM review notes"
310
+ )
311
+ eval_parser.add_argument(
312
+ "--dream",
313
+ dest="dream_snapshot",
314
+ help="Evaluate a Dreaming run against a snapshot name or latest",
315
+ )
316
+ eval_parser.add_argument(
317
+ "--no-save", action="store_true", help="Do not save eval reports under .ai-memory/evals"
318
+ )
319
+
320
+ query_parser = subparsers.add_parser("query", help="Search memory")
321
+ query_parser.add_argument("query")
322
+ query_parser.add_argument("--max-results", type=int, default=10)
323
+
324
+ subparsers.add_parser(
325
+ "sync-agents",
326
+ help="Synchronize agent instruction files using AGENTS.md as the source of truth",
327
+ )
328
+ subparsers.add_parser("sync-global", help="Preview local-to-global promotion")
329
+
330
+ rollback_parser = subparsers.add_parser("rollback", help="Restore local memory from a snapshot")
331
+ rollback_parser.add_argument("--to", required=True, help="Snapshot name")
332
+
333
+ store_parser = subparsers.add_parser("store", help="Memory store operations")
334
+ store_subs = store_parser.add_subparsers(dest="store_action")
335
+
336
+ store_write = store_subs.add_parser("write", help="Write a store file")
337
+ store_write.add_argument(
338
+ "store_path", help="Semantic path (e.g. architecture/decisions/auth-service)"
339
+ )
340
+ store_write.add_argument("--content", required=True, help="Content to write")
341
+ store_write.add_argument("--title", default="", help="Title for the memory")
342
+ store_write.add_argument("--tags", default="", help="Comma-separated tags")
343
+ store_write.add_argument("--priority", choices=["high", "medium", "low"], default="medium")
344
+ store_write.add_argument("--mode", choices=["replace", "append"], default="replace")
345
+
346
+ store_read = store_subs.add_parser("read", help="Read a store file")
347
+ store_read.add_argument("store_path", help="Semantic path")
348
+
349
+ store_list = store_subs.add_parser("list", help="List store files")
350
+ store_list.add_argument("--prefix", default="", help="Filter by path prefix")
351
+ store_list.add_argument("--tags", default="", help="Comma-separated tags to filter by")
352
+ store_list.add_argument("--max-results", type=int, default=50)
353
+
354
+ store_delete = store_subs.add_parser("delete", help="Delete a store file")
355
+ store_delete.add_argument("store_path", help="Semantic path")
356
+
357
+ return parser
358
+
359
+
360
+ def _print_result(result: Any, as_json: bool) -> None:
361
+ if as_json:
362
+ print(json.dumps(result, indent=2, ensure_ascii=False))
363
+ return
364
+
365
+ if isinstance(result, list):
366
+ for item in result:
367
+ print(_format_item(item))
368
+ return
369
+
370
+ if isinstance(result, dict):
371
+ for key, value in result.items():
372
+ print(f"{key}: {_format_value(value)}")
373
+ return
374
+
375
+ print(result)
376
+
377
+
378
+ def _format_item(item: Any) -> str:
379
+ if isinstance(item, dict):
380
+ if {"path", "line", "snippet"}.issubset(item):
381
+ return f"{item['path']}:{item['line']}: {item['snippet']}"
382
+ return json.dumps(item, ensure_ascii=False)
383
+ return str(item)
384
+
385
+
386
+ def _format_value(value: Any) -> str:
387
+ if isinstance(value, list):
388
+ if not value:
389
+ return "[]"
390
+ return "\n - " + "\n - ".join(str(item) for item in value)
391
+ if isinstance(value, dict):
392
+ return "\n" + json.dumps(value, indent=2, ensure_ascii=False)
393
+ return str(value)
394
+
395
+
396
+ if __name__ == "__main__":
397
+ raise SystemExit(main())
@@ -0,0 +1,212 @@
1
+ """Typed result contracts for Memory Fabric public APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import Any, Literal
7
+
8
+ # pydantic (pulled in by the optional `mcp` extra, to build tool schemas from these
9
+ # TypedDicts) rejects typing.TypedDict on Python < 3.12 in favor of typing_extensions'
10
+ # version. typing_extensions is only guaranteed present when `mcp` is installed (it's a
11
+ # transitive pydantic dependency) — core-only installs on 3.11 fall back to stdlib, which
12
+ # is fine there since pydantic never touches these types without the `mcp` extra.
13
+ if sys.version_info >= (3, 12):
14
+ from typing import NotRequired, TypedDict
15
+ else:
16
+ try:
17
+ from typing_extensions import NotRequired, TypedDict
18
+ except ImportError:
19
+ from typing import NotRequired, TypedDict
20
+
21
+
22
+ Priority = Literal["high", "medium", "low"]
23
+ WriteMode = Literal["append", "replace"]
24
+ DreamMode = Literal["light", "deep"]
25
+
26
+
27
+ class InitResult(TypedDict):
28
+ created: bool
29
+ memory_dir: str
30
+ files_created: list[str]
31
+ warnings: list[str]
32
+ resource_uris: NotRequired[list[str]]
33
+
34
+
35
+ class ContextBundle(TypedDict):
36
+ text: str
37
+ included_sections: list[str]
38
+ omitted_sections: list[str]
39
+ token_budget: int
40
+ estimated_tokens: int
41
+ warnings: list[str]
42
+
43
+
44
+ class MemorySection(TypedDict):
45
+ section: str
46
+ path: str
47
+ text: str
48
+ metadata: dict[str, Any]
49
+ truncated: bool
50
+ warnings: list[str]
51
+
52
+
53
+ class SearchResult(TypedDict):
54
+ section: str
55
+ path: str
56
+ line: int
57
+ snippet: str
58
+ backend: NotRequired[str] # 'ripgrep' | 'python' — which search backend was used
59
+
60
+
61
+ class WriteResult(TypedDict):
62
+ changed: bool
63
+ path: str
64
+ redactions: int
65
+ warnings: list[str]
66
+
67
+
68
+ class PatchPreview(TypedDict):
69
+ patch: str
70
+ affected_files: list[str]
71
+ redactions: int
72
+ warnings: list[str]
73
+
74
+
75
+ class EpisodicJournalResult(TypedDict):
76
+ """Result returned by write_session_journal_tool."""
77
+
78
+ changed: bool
79
+ store_path: str
80
+ path: str
81
+ date: str
82
+ redactions: int
83
+ warnings: list[str]
84
+
85
+
86
+ class StoreWriteResult(TypedDict):
87
+ changed: bool
88
+ path: str
89
+ store_path: str
90
+ redactions: int
91
+ warnings: list[str]
92
+
93
+
94
+ class StoreReadResult(TypedDict):
95
+ store_path: str
96
+ path: str
97
+ text: str
98
+ metadata: dict[str, Any]
99
+ truncated: bool
100
+ warnings: list[str]
101
+
102
+
103
+ class StoreEntry(TypedDict):
104
+ store_path: str
105
+ path: str
106
+ summary: str
107
+ priority: str
108
+ tags: list[str]
109
+ last_updated: str
110
+
111
+
112
+ class StoreListResult(TypedDict):
113
+ entries: list[StoreEntry]
114
+ total: int
115
+ warnings: list[str]
116
+
117
+
118
+ class DoctorResult(TypedDict):
119
+ ok: bool
120
+ errors: list[str]
121
+ warnings: list[str]
122
+ checked_files: list[str]
123
+
124
+
125
+ class StatusResult(TypedDict):
126
+ cwd: str
127
+ memory_dir: str
128
+ memory_exists: bool
129
+ global_dir: str
130
+ provider_configured: bool
131
+ local_files: list[str]
132
+ memory_sizes: dict[str, dict[str, int]]
133
+ version: str
134
+
135
+
136
+ class DreamResult(TypedDict):
137
+ changed: bool
138
+ snapshot: str | None
139
+ warnings: list[str]
140
+ checked_files: list[str]
141
+ candidate_store: str
142
+ patch_preview: str
143
+ affected_files: list[str]
144
+ consolidation: "DreamConsolidation"
145
+ rewrite_tasks: list["DreamRewriteTask"]
146
+ apply_required: bool
147
+ redactions: int
148
+ evaluation: NotRequired["DreamEvalResult"]
149
+
150
+
151
+ class DreamConsolidation(TypedDict):
152
+ duplicates_found: int
153
+ lines_removed: int
154
+ files_touched: list[str]
155
+
156
+
157
+ class DreamRewriteTask(TypedDict):
158
+ section: str
159
+ reason: str
160
+ instruction: str
161
+
162
+
163
+ class EvalCheck(TypedDict):
164
+ id: str
165
+ status: Literal["pass", "warn", "fail"]
166
+ severity: Literal["info", "low", "medium", "high"]
167
+ message: str
168
+ recommendation: str
169
+ command: NotRequired[str]
170
+
171
+
172
+ class EvalCategory(TypedDict):
173
+ name: str
174
+ score: int
175
+ status: Literal["pass", "warn", "fail"]
176
+ weight: int
177
+ checks: list[EvalCheck]
178
+
179
+
180
+ class EvalResult(TypedDict):
181
+ kind: Literal["memory"]
182
+ generated_at: str
183
+ cwd: str
184
+ memory_dir: str
185
+ score: int
186
+ status: Literal["pass", "warn", "fail"]
187
+ categories: list[EvalCategory]
188
+ recommendations: list[str]
189
+ report_paths: list[str]
190
+ warnings: list[str]
191
+ llm_notes: list[str]
192
+
193
+
194
+ class DreamEvalResult(TypedDict):
195
+ kind: Literal["dream"]
196
+ generated_at: str
197
+ cwd: str
198
+ memory_dir: str
199
+ baseline_snapshot: str
200
+ before_score: int
201
+ after_score: int
202
+ delta: int
203
+ score: int
204
+ status: Literal["pass", "warn", "fail"]
205
+ changed_files: list[str]
206
+ improvements: list[str]
207
+ regressions: list[str]
208
+ categories: list[EvalCategory]
209
+ recommendations: list[str]
210
+ report_paths: list[str]
211
+ warnings: list[str]
212
+ llm_notes: list[str]