agents-memory 0.42__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,101 @@
1
+ """Scan Coding folders vs PROJECTS.md. Optional register/ignore."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import sys
7
+
8
+ from .cli_help import emit_help_json
9
+ from .store import (
10
+ ignore_slug,
11
+ inventory_report,
12
+ register_project,
13
+ sync_injection,
14
+ )
15
+
16
+
17
+ def build_parser() -> argparse.ArgumentParser:
18
+ parser = argparse.ArgumentParser(
19
+ description="Bestandaufnahme: disk vs local agent memory",
20
+ epilog="Machine-readable: python -m agent_memory inventory --help-json. Full spec: python -m agent_memory --help-json.",
21
+ )
22
+ parser.add_argument("--json", action="store_true", help="machine-readable report")
23
+ parser.add_argument(
24
+ "--register",
25
+ nargs=4,
26
+ metavar=("SLUG", "PATH", "ROLE", "STACK"),
27
+ help="add/update a project then sync",
28
+ )
29
+ parser.add_argument("--ignore", metavar="SLUG", help="never prompt this folder again")
30
+ parser.add_argument(
31
+ "--sync",
32
+ action="store_true",
33
+ help="rewrite your Agent injection files",
34
+ )
35
+ parser.add_argument(
36
+ "--no-repos",
37
+ action="store_true",
38
+ help="with --sync, only write global injection (not per-repo rules)",
39
+ )
40
+ parser.add_argument(
41
+ "--help-json",
42
+ action="store_true",
43
+ help="print machine-readable CLI spec as JSON and exit",
44
+ )
45
+ return parser
46
+
47
+
48
+ def main(argv: list[str] | None = None) -> int:
49
+ if hasattr(sys.stdout, "reconfigure"):
50
+ try:
51
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
52
+ except Exception:
53
+ pass
54
+ argv = list(argv if argv is not None else sys.argv[1:])
55
+ if "--help-json" in argv:
56
+ emit_help_json(argv, build_parser(), name="inventory")
57
+ return 0
58
+ parser = build_parser()
59
+ args = parser.parse_args(argv)
60
+
61
+ if args.register:
62
+ slug, path, role, stack = args.register
63
+ p = register_project(slug, path, role, stack)
64
+ written, _ = sync_injection(include_repos=not args.no_repos)
65
+ print(f"registered {p.slug} -> {p.path}")
66
+ print("synced:")
67
+ for w in written:
68
+ print(f" {w}")
69
+ return 0
70
+
71
+ if args.ignore:
72
+ ignore_slug(args.ignore)
73
+ print(f"ignored {args.ignore}")
74
+ return 0
75
+
76
+ report = inventory_report()
77
+ if args.json:
78
+ print(json.dumps(report, indent=2, ensure_ascii=False))
79
+ else:
80
+ print(f"tracked: {len(report['tracked'])}")
81
+ print(f"unknown (on disk, not in memory): {len(report['unknown'])}")
82
+ for u in report["unknown"]:
83
+ print(f" + {u['slug']}\t{u['path']}")
84
+ print(f"missing (in memory, path gone): {len(report['missing'])}")
85
+ for m in report["missing"]:
86
+ print(f" - {m['slug']}\t{m['path']}")
87
+ if report["ignored"]:
88
+ print("ignored slugs:", ", ".join(report["ignored"]))
89
+ if not report["unknown"] and not report["missing"]:
90
+ print("memory map matches disk.")
91
+
92
+ if args.sync:
93
+ written, _ = sync_injection(include_repos=not args.no_repos)
94
+ print("synced:")
95
+ for w in written:
96
+ print(f" {w}")
97
+ return 0
98
+
99
+
100
+ if __name__ == "__main__":
101
+ sys.exit(main())
@@ -0,0 +1,313 @@
1
+ """Local markdown memory MCP — reference implementation of abi/MCP.md."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import sys
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from .store import (
9
+ add_memory as store_add,
10
+ delete_memory as store_delete,
11
+ ensure_memory_layout,
12
+ get_project_memories as store_get_project,
13
+ ignore_slug,
14
+ inject_into_repo,
15
+ inventory_report,
16
+ parse_projects,
17
+ promote_bullet as store_promote,
18
+ register_project as store_register,
19
+ search_memory as store_search,
20
+ sync_injection,
21
+ )
22
+
23
+ ensure_memory_layout()
24
+
25
+ mcp = FastMCP("agent-memory")
26
+
27
+
28
+ @mcp.tool()
29
+ def search_memory(query: str, project: str = "") -> str:
30
+ """Search typed markdown under ~/.agents/memory and registered repos. Does not search product chat/jsonl graves — use chats-index.md for body paths."""
31
+ try:
32
+ hits = store_search(query, project=project)
33
+ if not hits:
34
+ return f"No local memories for '{query}'" + (f" in {project}" if project else "")
35
+ lines = [f"Found {len(hits)} hits:"]
36
+ for h in hits:
37
+ lines.append(f"- [{h['id']}] {h['text']}")
38
+ return "\n".join(lines)
39
+ except Exception as e:
40
+ return f"Error searching local memory: {e}"
41
+
42
+
43
+ @mcp.tool()
44
+ def add_memory(
45
+ fact_or_message: str,
46
+ kind: str = "",
47
+ name: str = "",
48
+ project: str = "",
49
+ collection: str = "",
50
+ ) -> str:
51
+ """File a durable fact in the right folder.
52
+
53
+ kind=concept|entity|workflow|project|note|scratch|research|plans|tasks|roadmap|waves|decision|proposed|implemented|rejected|staging
54
+ plus name= (file stem). collection= for notes/ or a note class
55
+ (feature, bug-fix, simplification, architecture, process, testing).
56
+ Sequential 001-topic.md: plans, tasks, waves, roadmap, decisions, lifecycle notes.
57
+ kind=research is topical (input). project= alone writes staging/captured.md (inbox).
58
+ Do not dump transcripts, emails, phones, tokens, or one-shot how-tos.
59
+ """
60
+ try:
61
+ loc = store_add(
62
+ fact_or_message,
63
+ kind=kind,
64
+ name=name,
65
+ project=project,
66
+ collection=collection,
67
+ )
68
+ return f"Saved to {loc}"
69
+ except Exception as e:
70
+ return f"Error saving memory: {e}"
71
+
72
+
73
+ @mcp.tool()
74
+ def promote_bullet(
75
+ bullet: str,
76
+ kind: str,
77
+ name: str,
78
+ project: str = "",
79
+ collection: str = "",
80
+ source_path: str = "",
81
+ ) -> str:
82
+ """Promote a staging bullet into a typed memory file (kind+name) and delete it from staging.
83
+
84
+ Example: promote_bullet("prefer dark mode", kind="note", name="ui", collection="preferences")
85
+ """
86
+ try:
87
+ loc, removed = store_promote(
88
+ bullet=bullet,
89
+ kind=kind,
90
+ name=name,
91
+ project=project,
92
+ collection=collection,
93
+ source_path=source_path,
94
+ )
95
+ status = "and removed from staging" if removed else "(staging bullet not found to delete)"
96
+ return f"Promoted to {loc} {status}"
97
+ except Exception as e:
98
+ return f"Error promoting bullet: {e}"
99
+
100
+
101
+ @mcp.tool()
102
+ def get_staging_inbox(project: str = "", limit: int = 20) -> str:
103
+ """Fetch un-distilled bullets from staging, grouped by source file.
104
+
105
+ Each bullet includes file and source_path for distill_batch.
106
+ """
107
+ try:
108
+ from .store import get_staging_inbox as store_get_inbox
109
+
110
+ payload = store_get_inbox(project=project, limit=limit)
111
+ if payload["total"] == 0:
112
+ return "Staging inbox is empty (all caught up)."
113
+ return json.dumps(payload, indent=2, ensure_ascii=False)
114
+ except Exception as e:
115
+ return f"Error reading staging inbox: {e}"
116
+
117
+
118
+ @mcp.tool()
119
+ def distill_batch(items_json: str) -> str:
120
+ """Batch-process staging bullets into memory or discard them.
121
+
122
+ Pass a JSON array of objects:
123
+ [{"bullet": "fact text", "kind": "note", "name": "stem", "project": "slug"},
124
+ {"bullet": "throwaway chatter", "discard": true}]
125
+ """
126
+ try:
127
+ from .store import distill_batch as store_distill_batch
128
+
129
+ parsed = json.loads(items_json) if isinstance(items_json, str) else items_json
130
+ if not isinstance(parsed, list):
131
+ return "Error: expected a JSON list of items"
132
+ result = store_distill_batch(parsed)
133
+ return json.dumps(result, indent=2, ensure_ascii=False)
134
+ except Exception as e:
135
+ return f"Error in distill_batch: {e}"
136
+
137
+
138
+ @mcp.tool()
139
+ def get_project_memories(project: str) -> str:
140
+ """Return the project link plus in-tree `.agents/memory` markdown."""
141
+ try:
142
+ return store_get_project(project)
143
+ except Exception as e:
144
+ return f"Error fetching project memories: {e}"
145
+
146
+
147
+ @mcp.tool()
148
+ def delete_memory(memory_id: str) -> str:
149
+ """Delete a memory line by id from search_memory, e.g. user/notes/programming/chat-stores.md:3 or project/git-updater/staging/captured.md:12."""
150
+ try:
151
+ removed = store_delete(memory_id)
152
+ return f"Deleted {memory_id}: {removed}"
153
+ except Exception as e:
154
+ return f"Error deleting memory: {e}"
155
+
156
+
157
+ @mcp.tool()
158
+ def list_projects() -> str:
159
+ """List all tracked projects (slug, path, role, stack, status)."""
160
+ rows = parse_projects()
161
+ if not rows:
162
+ return "No projects in PROJECTS.md"
163
+ lines = [f"{len(rows)} projects:"]
164
+ for p in rows:
165
+ lines.append(f"- {p.slug} | {p.path} | {p.role} | {p.stack} | {p.status}")
166
+ return "\n".join(lines)
167
+
168
+
169
+ @mcp.tool()
170
+ def inventory_projects() -> str:
171
+ """Bestandaufnahme: compare scan.json roots to PROJECTS.md. Returns unknown and missing folders."""
172
+ try:
173
+ return json.dumps(inventory_report(), indent=2, ensure_ascii=False)
174
+ except Exception as e:
175
+ return f"Error running inventory: {e}"
176
+
177
+
178
+ @mcp.tool()
179
+ def register_project(
180
+ slug: str,
181
+ path: str,
182
+ role: str = "unclassified",
183
+ stack: str = "—",
184
+ status: str = "active",
185
+ ) -> str:
186
+ """Add or update a project in PROJECTS.md, write `<repo>/.agents/memory/` (link + folders), inject AGENTS.md+CLAUDE.md, sync."""
187
+ try:
188
+ p = store_register(slug, path, role=role, stack=stack, status=status)
189
+ written, warnings = sync_injection(include_repos=True)
190
+ extra = inject_into_repo(p)
191
+ warn_txt = f" Warnings: {'; '.join(warnings)}" if warnings else ""
192
+ return (
193
+ f"Registered {p.slug} at {p.path}. "
194
+ f"Synced {len(written)} files. Repo inject: {len(extra)} files.{warn_txt}"
195
+ )
196
+ except Exception as e:
197
+ return f"Error registering project: {e}"
198
+
199
+
200
+ @mcp.tool()
201
+ def ignore_project(slug: str) -> str:
202
+ """Stop listing this folder as unknown in inventory (scan.json ignore_slugs)."""
203
+ try:
204
+ ignore_slug(slug)
205
+ return f"Ignored slug '{slug}'"
206
+ except Exception as e:
207
+ return f"Error ignoring project: {e}"
208
+
209
+
210
+ @mcp.tool()
211
+ def sync_local_agents_md(project_folder_path: str = "", project_slug: str = "") -> str:
212
+ """Sync always-on memory into your Agent hosts. Optional: also inject one repo by path or slug."""
213
+ try:
214
+ written, warnings = sync_injection(include_repos=True)
215
+ extra = []
216
+ if project_slug:
217
+ from .store import projects_by_slug
218
+
219
+ p = projects_by_slug().get(project_slug)
220
+ if p:
221
+ extra = inject_into_repo(p)
222
+ elif project_folder_path:
223
+ from .store import Project, inject_into_repo as inj
224
+ from pathlib import Path as P
225
+
226
+ slug = project_slug or P(project_folder_path).name
227
+ extra = inj(
228
+ Project(
229
+ slug=slug,
230
+ path=project_folder_path,
231
+ role="see PROJECTS.md",
232
+ stack="—",
233
+ )
234
+ )
235
+ out = "Synced:\n" + "\n".join(written + extra)
236
+ if warnings:
237
+ out += "\nWarnings:\n" + "\n".join(f"- {w}" for w in warnings)
238
+ return out
239
+ except Exception as e:
240
+ return f"Error syncing: {e}"
241
+
242
+
243
+ @mcp.tool()
244
+ def ingest_catalog() -> str:
245
+ """Catalog phase: rebuild chats-index.md + entity cards (titles/paths only). Bodies stay in product folders. Same contract for every ingest source."""
246
+ try:
247
+ from .ingest_catalog import run_catalog
248
+
249
+ result = run_catalog()
250
+ return json.dumps(result, indent=2)
251
+ except Exception as e:
252
+ return f"Error running ingest catalog: {e}"
253
+
254
+
255
+ @mcp.tool()
256
+ def ingest_extract(source_id: str = "") -> str:
257
+ """Extract phase: filter durable user lines into staging/ingest/<id>/captured.md (inbox, not memory). Distill explicitly afterward."""
258
+ try:
259
+ from .ingest_extractors import run_extract
260
+
261
+ result = run_extract(source_id=source_id)
262
+ return json.dumps(result, indent=2)
263
+ except Exception as e:
264
+ return f"Error running ingest extract: {e}"
265
+
266
+
267
+ @mcp.tool()
268
+ def ingest_status() -> str:
269
+ """Show ingest/state.json summary plus staging inbox depth and nag."""
270
+ try:
271
+ from .ingest_common import ingest_state_path, load_state
272
+ from .ingest_config import list_sources, load_ingest
273
+ from .store import staging_status_summary
274
+
275
+ cfg = load_ingest()
276
+ state = load_state()
277
+ rows = []
278
+ for src in list_sources(cfg):
279
+ sid = str(src["id"])
280
+ entry = state.get("sources", {}).get(sid, {})
281
+ rows.append(
282
+ {
283
+ "id": sid,
284
+ "kind": src.get("kind"),
285
+ "last_catalog": entry.get("last_catalog"),
286
+ "last_extract": entry.get("last_extract"),
287
+ "catalog_count": entry.get("catalog_count"),
288
+ "extract_count": entry.get("extract_count"),
289
+ "extract_capped": entry.get("extract_capped"),
290
+ "extract_total_before_cap": entry.get("extract_total_before_cap"),
291
+ "staging": entry.get("staging"),
292
+ }
293
+ )
294
+ return json.dumps(
295
+ {
296
+ "state_file": str(ingest_state_path()),
297
+ "staging": staging_status_summary(),
298
+ "sources": rows,
299
+ },
300
+ indent=2,
301
+ )
302
+ except Exception as e:
303
+ return f"Error reading ingest status: {e}"
304
+
305
+
306
+ def main() -> int:
307
+ print("Starting local agent-memory MCP on stdio...", file=sys.stderr)
308
+ mcp.run()
309
+ return 0
310
+
311
+
312
+ if __name__ == "__main__":
313
+ raise SystemExit(main())