loop-memory 0.4.6__tar.gz → 0.4.8__tar.gz

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.
Files changed (144) hide show
  1. {loop_memory-0.4.6 → loop_memory-0.4.8}/PKG-INFO +32 -10
  2. {loop_memory-0.4.6 → loop_memory-0.4.8}/README.md +31 -9
  3. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/cognitive.py +100 -0
  4. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/hooks.py +74 -6
  5. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/read.py +82 -4
  6. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/main.py +14 -0
  7. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/export.py +36 -0
  8. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/memories.py +16 -0
  9. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/system.py +92 -0
  10. loop_memory-0.4.8/loop_memory/storage/snapshot.py +231 -0
  11. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/sqlite_store.py +367 -1
  12. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/PKG-INFO +32 -10
  13. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/SOURCES.txt +10 -0
  14. {loop_memory-0.4.6 → loop_memory-0.4.8}/pyproject.toml +1 -1
  15. loop_memory-0.4.8/tests/test_agents_init.py +154 -0
  16. loop_memory-0.4.8/tests/test_cli_init.py +106 -0
  17. loop_memory-0.4.8/tests/test_cli_memory_stats.py +82 -0
  18. loop_memory-0.4.8/tests/test_cli_recall_paths.py +155 -0
  19. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_recall_verbose.py +11 -8
  20. loop_memory-0.4.8/tests/test_cli_snapshot.py +112 -0
  21. loop_memory-0.4.8/tests/test_memory_stats.py +155 -0
  22. loop_memory-0.4.8/tests/test_recall_paths.py +213 -0
  23. loop_memory-0.4.8/tests/test_routes_audit_2026_09_13.py +141 -0
  24. loop_memory-0.4.8/tests/test_snapshot.py +215 -0
  25. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_supersession_chain.py +1 -1
  26. {loop_memory-0.4.6 → loop_memory-0.4.8}/LICENSE +0 -0
  27. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/__init__.py +0 -0
  28. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/__init__.py +0 -0
  29. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/embedding.py +0 -0
  30. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/sentence_embedder.py +0 -0
  31. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/backends/vector_store.py +0 -0
  32. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/__init__.py +0 -0
  33. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/_common.py +0 -0
  34. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/__init__.py +0 -0
  35. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/diag.py +0 -0
  36. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/graph.py +0 -0
  37. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/rules.py +0 -0
  38. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/serve.py +0 -0
  39. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/cli/commands/write.py +0 -0
  40. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/__init__.py +0 -0
  41. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/loop.py +0 -0
  42. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/engine/reflect.py +0 -0
  43. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/examples/__init__.py +0 -0
  44. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/examples/demo.py +0 -0
  45. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/export/__init__.py +0 -0
  46. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/export/memory_md.py +0 -0
  47. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/__init__.py +0 -0
  48. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/build.py +0 -0
  49. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/graph/extract.py +0 -0
  50. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/__init__.py +0 -0
  51. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/loader.py +0 -0
  52. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/ingest/pipeline.py +0 -0
  53. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/__init__.py +0 -0
  54. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/cognitive.py +0 -0
  55. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/compact.py +0 -0
  56. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/consolidate.py +0 -0
  57. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/contradiction.py +0 -0
  58. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/evolution.py +0 -0
  59. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/graph.py +0 -0
  60. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/llm_compact_pass.py +0 -0
  61. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/llm_consolidate.py +0 -0
  62. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/jobs/scheduler.py +0 -0
  63. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/__init__.py +0 -0
  64. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/base.py +0 -0
  65. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/openai_adapter.py +0 -0
  66. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/llm/providers.py +0 -0
  67. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/mcp/__init__.py +0 -0
  68. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/memory/__init__.py +0 -0
  69. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/memory/types.py +0 -0
  70. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/__init__.py +0 -0
  71. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/private.py +0 -0
  72. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/privacy/redact.py +0 -0
  73. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/py.typed +0 -0
  74. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/sdk.py +0 -0
  75. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/sdk_extensions.py +0 -0
  76. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/security/__init__.py +0 -0
  77. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/security/secrets.py +0 -0
  78. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/__init__.py +0 -0
  79. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/app.py +0 -0
  80. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/handlers.py +0 -0
  81. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/_shared.py +0 -0
  82. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/admin.py +0 -0
  83. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/cognitive.py +0 -0
  84. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/graph.py +0 -0
  85. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/insights.py +0 -0
  86. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/sessions.py +0 -0
  87. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/routes/wiki.py +0 -0
  88. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/static/__init__.py +0 -0
  89. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/static/index.html +0 -0
  90. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/serve/watcher.py +0 -0
  91. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/__init__.py +0 -0
  92. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/storage/retrieval.py +0 -0
  93. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/__init__.py +0 -0
  94. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/backfill.py +0 -0
  95. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/classifier.py +0 -0
  96. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/prompts.py +0 -0
  97. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory/wiki/scope.py +0 -0
  98. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/dependency_links.txt +0 -0
  99. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/entry_points.txt +0 -0
  100. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/requires.txt +0 -0
  101. {loop_memory-0.4.6 → loop_memory-0.4.8}/loop_memory.egg-info/top_level.txt +0 -0
  102. {loop_memory-0.4.6 → loop_memory-0.4.8}/setup.cfg +0 -0
  103. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_admin_ingest_route.py +0 -0
  104. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_agent_memory_api.py +0 -0
  105. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_agent_memory_sdk.py +0 -0
  106. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_auth_token_rotate.py +0 -0
  107. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_audit_supersede.py +0 -0
  108. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_rules.py +0 -0
  109. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_v7.py +0 -0
  110. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_cli_version.py +0 -0
  111. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_contradictions.py +0 -0
  112. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_evolution.py +0 -0
  113. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_evolution_quality.py +0 -0
  114. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_expanduser.py +0 -0
  115. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_export_ask.py +0 -0
  116. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_graph.py +0 -0
  117. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_ingest.py +0 -0
  118. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_consolidator.py +0 -0
  119. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  120. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_providers.py +0 -0
  121. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_llm_test_endpoint.py +0 -0
  122. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_loop.py +0 -0
  123. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_mcp.py +0 -0
  124. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_memories_pagination.py +0 -0
  125. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_openclaw_loader.py +0 -0
  126. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_package_version.py +0 -0
  127. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_recall_provenance.py +0 -0
  128. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_reflection.py +0 -0
  129. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_repo_framing.py +0 -0
  130. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_score_api.py +0 -0
  131. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_scoring_v2.py +0 -0
  132. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_secrets.py +0 -0
  133. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_serve_app.py +0 -0
  134. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_serve_handlers.py +0 -0
  135. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_session_order.py +0 -0
  136. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_short_query.py +0 -0
  137. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_store.py +0 -0
  138. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_summarization.py +0 -0
  139. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_universal_memory.py +0 -0
  140. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_vector_store.py +0 -0
  141. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_watcher.py +0 -0
  142. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_wiki_classifier.py +0 -0
  143. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_wiki_export_escape.py +0 -0
  144. {loop_memory-0.4.6 → loop_memory-0.4.8}/tests/test_wiki_prompts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: loop-memory
3
- Version: 0.4.6
3
+ Version: 0.4.8
4
4
  Summary: A general-purpose, local memory system for every AI agent you run. Loop Memory auto-captures conversations from Codex / Claude / Hermes / OpenClaw, scores them by importance × recency × usage × feedback, distils them into a curated wiki, and serves everything from a single web UI.
5
5
  Author: Loop Memory contributors <loop-memory@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -66,15 +66,37 @@ Dynamic: license-file
66
66
 
67
67
  ---
68
68
 
69
- > **What's new in 0.4.6** — **memory supersession chain** + **recall
70
- > provenance labels**. `merge_memories()` no longer silently
71
- > `DELETE`s the loser it writes an explicit `superseded_by`
72
- > pointer (audit 2026-08-30, Mem0 v2.0.19 Dream pattern), the new
73
- > `loop-memory audit-supersede` walks the chain, and every
74
- > `recall()` hit now carries a `why: [...]` provenance list
75
- > (adopted from agentmemory v1.2.0) that you can surface with
76
- > `loop-memory recall --verbose`. 26 new regression cases pin
77
- > both shapes.
69
+ > **What's new in 0.4.8** — **L0 outline recall** +
70
+ > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
71
+ > (and the new `recall --outline` flag) returns only `id` +
72
+ > `abstract` + `score` + `why` for each hit — never the full
73
+ > body so an agent can identify the top candidates cheaply
74
+ > before deciding which bodies to fetch (audit 2026-09-13,
75
+ > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
76
+ > `loop-memory init --agent <name>` registers an agent in the
77
+ > new `agents` table and (with `--install-hooks`) also wires
78
+ > the MCP + SessionStart hooks for codex/claude/hermes; the
79
+ > index is bumped on every `install-hooks` run so a user can
80
+ > always answer "which CLIs are wired into this store?" with
81
+ > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
82
+ > pattern). Both shapes ship with HTTP routes
83
+ > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
84
+ > 57 new regression cases pin both shapes. [Full
85
+ > changelog →](CHANGELOG.md)
86
+ >
87
+ > **What's new in 0.4.7** — **per-memory lifetime stats** +
88
+ > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
89
+ > returns a flat dict (recall_count, positive, negative, age,
90
+ > etc.) so you can ask "is anyone still using this memory?"
91
+ > without cracking open the SQLite file (audit 2026-09-06,
92
+ > agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
93
+ > `loop-memory restore <in>` move a single-file, lossless
94
+ > snapshot of the entire store — including recall-quality
95
+ > signals and the supersession chain — to another machine
96
+ > (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
97
+ > ship with HTTP routes (`/api/memories/{id}/stats`,
98
+ > `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
99
+ > cases pin both shapes.
78
100
  > [Full changelog →](CHANGELOG.md)
79
101
 
80
102
  ---
@@ -22,15 +22,37 @@
22
22
 
23
23
  ---
24
24
 
25
- > **What's new in 0.4.6** — **memory supersession chain** + **recall
26
- > provenance labels**. `merge_memories()` no longer silently
27
- > `DELETE`s the loser it writes an explicit `superseded_by`
28
- > pointer (audit 2026-08-30, Mem0 v2.0.19 Dream pattern), the new
29
- > `loop-memory audit-supersede` walks the chain, and every
30
- > `recall()` hit now carries a `why: [...]` provenance list
31
- > (adopted from agentmemory v1.2.0) that you can surface with
32
- > `loop-memory recall --verbose`. 26 new regression cases pin
33
- > both shapes.
25
+ > **What's new in 0.4.8** — **L0 outline recall** +
26
+ > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
27
+ > (and the new `recall --outline` flag) returns only `id` +
28
+ > `abstract` + `score` + `why` for each hit — never the full
29
+ > body so an agent can identify the top candidates cheaply
30
+ > before deciding which bodies to fetch (audit 2026-09-13,
31
+ > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
32
+ > `loop-memory init --agent <name>` registers an agent in the
33
+ > new `agents` table and (with `--install-hooks`) also wires
34
+ > the MCP + SessionStart hooks for codex/claude/hermes; the
35
+ > index is bumped on every `install-hooks` run so a user can
36
+ > always answer "which CLIs are wired into this store?" with
37
+ > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
38
+ > pattern). Both shapes ship with HTTP routes
39
+ > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
40
+ > 57 new regression cases pin both shapes. [Full
41
+ > changelog →](CHANGELOG.md)
42
+ >
43
+ > **What's new in 0.4.7** — **per-memory lifetime stats** +
44
+ > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
45
+ > returns a flat dict (recall_count, positive, negative, age,
46
+ > etc.) so you can ask "is anyone still using this memory?"
47
+ > without cracking open the SQLite file (audit 2026-09-06,
48
+ > agentmemory v1.3.0 pattern). `loop-memory snapshot <out>` /
49
+ > `loop-memory restore <in>` move a single-file, lossless
50
+ > snapshot of the entire store — including recall-quality
51
+ > signals and the supersession chain — to another machine
52
+ > (audit 2026-09-06, codexa-memory v0.2.0 pattern). Both shapes
53
+ > ship with HTTP routes (`/api/memories/{id}/stats`,
54
+ > `/api/snapshot`, `/api/snapshot/restore`). 33 new regression
55
+ > cases pin both shapes.
34
56
  > [Full changelog →](CHANGELOG.md)
35
57
 
36
58
  ---
@@ -85,6 +85,29 @@ def _build_parser() -> argparse.ArgumentParser:
85
85
  rc.add_argument("--dry-run", action="store_true",
86
86
  help="Report what would change without persisting")
87
87
 
88
+ # Audit 2026-09-06: per-memory lifetime stats
89
+ # (agentmemory v1.3.0 pattern, slimmed down).
90
+ ms = sub.add_parser(
91
+ "memory-stats",
92
+ help="Per-memory lifetime stats (audit 2026-09-06)")
93
+ ms.add_argument("memory_id",
94
+ help="Memory id (or short prefix) to look up")
95
+ ms.add_argument("--prefix", action="store_true",
96
+ help="Allow a short prefix match (first 8+ chars)")
97
+
98
+ # Audit 2026-09-06: portable SQLite snapshot
99
+ # (codexa-memory v0.2.0 pattern).
100
+ sn = sub.add_parser(
101
+ "snapshot",
102
+ help="Write a portable SQLite snapshot of the live store")
103
+ sn.add_argument("out_path",
104
+ help="Destination .memory.sqlite file")
105
+ rs = sub.add_parser(
106
+ "restore",
107
+ help="Re-hydrate a portable SQLite snapshot into the live store")
108
+ rs.add_argument("in_path",
109
+ help="Source .memory.sqlite file")
110
+
88
111
  return p
89
112
 
90
113
 
@@ -242,3 +265,80 @@ def run_wiki_reclassify_legacy(args: list) -> int:
242
265
  })
243
266
  summary = reclassify_legacy_pages(s, batch=ns.batch)
244
267
  return _emit(summary)
268
+
269
+
270
+ def run_memory_stats(args: list) -> int:
271
+ """``loop-memory memory-stats <id> [--prefix]``
272
+
273
+ Per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).
274
+ Returns a flat dict suitable for ``jq`` and the dashboard.
275
+
276
+ With ``--prefix`` the caller can pass the first 8+ chars of the id
277
+ (handy in a shell pipeline where the full UUID is awkward).
278
+ """
279
+ import argparse as _ap
280
+ p = _ap.ArgumentParser(prog="loop-memory memory-stats")
281
+ p.add_argument("memory_id")
282
+ p.add_argument("--prefix", action="store_true")
283
+ p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
284
+ ns = p.parse_args(args)
285
+ s = MemoryStore(ns.db)
286
+ mid = ns.memory_id.strip()
287
+ if not mid:
288
+ return _emit({"error": "memory_id is required"})
289
+ if ns.prefix:
290
+ # Resolve a prefix to a full id. The store indexes by full
291
+ # id so we walk a list_memories scan (cheap because prefix
292
+ # queries are rare / human-driven).
293
+ candidates = [m.id for m in s.list_memories(limit=10000)
294
+ if m.id.startswith(mid)]
295
+ if not candidates:
296
+ return _emit({"error": f"no memory matches prefix {mid!r}"})
297
+ if len(candidates) > 1:
298
+ return _emit({
299
+ "error": f"prefix {mid!r} matches {len(candidates)} memories",
300
+ "candidates": candidates[:8],
301
+ })
302
+ mid = candidates[0]
303
+ res = s.memory_stats(mid)
304
+ if res is None:
305
+ return _emit({"error": f"memory {mid!r} not found"})
306
+ return _emit(res)
307
+
308
+
309
+ def run_snapshot(args: list) -> int:
310
+ """``loop-memory snapshot <out_path>``
311
+
312
+ Write a portable SQLite snapshot (audit 2026-09-06,
313
+ codexa-memory v0.2.0). Returns a summary dict with size + table
314
+ counts so the caller can confirm the snapshot is sane.
315
+ """
316
+ import argparse as _ap
317
+ p = _ap.ArgumentParser(prog="loop-memory snapshot")
318
+ p.add_argument("out_path")
319
+ p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
320
+ ns = p.parse_args(args)
321
+ s = MemoryStore(ns.db)
322
+ from ...storage.snapshot import snapshot as _snapshot
323
+ return _emit(_snapshot(s, ns.out_path))
324
+
325
+
326
+ def run_restore(args: list) -> int:
327
+ """``loop-memory restore <in_path>``
328
+
329
+ Re-hydrate a portable SQLite snapshot (audit 2026-09-06,
330
+ codexa-memory v0.2.0). Returns a summary dict with table counts
331
+ so the caller can confirm the restore is sane. Refuses
332
+ wrong-magic files loudly instead of corrupting the live store.
333
+ """
334
+ import argparse as _ap
335
+ p = _ap.ArgumentParser(prog="loop-memory restore")
336
+ p.add_argument("in_path")
337
+ p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
338
+ ns = p.parse_args(args)
339
+ s = MemoryStore(ns.db)
340
+ from ...storage.snapshot import restore as _restore
341
+ try:
342
+ return _emit(_restore(s, ns.in_path))
343
+ except (FileNotFoundError, ValueError) as e:
344
+ return _emit({"error": str(e)})
@@ -46,7 +46,7 @@ def _upsert_block(text: str, block: str, marker: str) -> str:
46
46
  return "".join(lines[:start]) + block + "".join(lines[end:])
47
47
 
48
48
 
49
- def _install_codex(home: Path, actions: list) -> None:
49
+ def _install_codex(home: Path, actions: list, store=None) -> None:
50
50
  codex_dir = home / ".codex"
51
51
  codex_cfg = codex_dir / "config.toml"
52
52
  # Codex's TOML schema: `hooks` is a struct (HooksToml) keyed by event
@@ -104,11 +104,13 @@ def _install_codex(home: Path, actions: list) -> None:
104
104
  except Exception: pass
105
105
  return
106
106
  actions.append(f"codex → {codex_cfg}" + ("" if parse_ok else " (unverified)"))
107
+ if store is not None:
108
+ store.register_agent("codex", hooks_installed=True)
107
109
  except Exception as e:
108
110
  actions.append(f"codex → SKIP ({e})")
109
111
 
110
112
 
111
- def _install_claude(home: Path, actions: list) -> None:
113
+ def _install_claude(home: Path, actions: list, store=None) -> None:
112
114
  claude_dir = home / ".claude"
113
115
  claude_mcp = claude_dir / "mcp.json"
114
116
  claude_settings = claude_dir / "settings.json"
@@ -148,9 +150,11 @@ def _install_claude(home: Path, actions: list) -> None:
148
150
  })
149
151
  claude_settings.write_text(json.dumps(existing_s, ensure_ascii=False, indent=2), encoding="utf-8")
150
152
  actions.append(f"claude (SessionStart hook) → {claude_settings}")
153
+ if store is not None:
154
+ store.register_agent("claude", hooks_installed=True)
151
155
 
152
156
 
153
- def _install_hermes(home: Path, actions: list) -> None:
157
+ def _install_hermes(home: Path, actions: list, store=None) -> None:
154
158
  hermes_dir = home / ".hermes"
155
159
  hermes_cfg = hermes_dir / "mcp.json"
156
160
  if not hermes_dir.exists():
@@ -168,6 +172,8 @@ def _install_hermes(home: Path, actions: list) -> None:
168
172
  hermes_cfg.parent.mkdir(parents=True, exist_ok=True)
169
173
  hermes_cfg.write_text(json.dumps(existing, ensure_ascii=False, indent=2), encoding="utf-8")
170
174
  actions.append(f"hermes → {hermes_cfg}")
175
+ if store is not None:
176
+ store.register_agent("hermes", hooks_installed=True)
171
177
 
172
178
 
173
179
  def _openclaw_hint(home: Path, actions: list) -> None:
@@ -200,13 +206,75 @@ def run_install_hooks(_args) -> int:
200
206
  """
201
207
  home = Path.home()
202
208
  actions: list = []
203
- _install_codex(home, actions)
204
- _install_claude(home, actions)
205
- _install_hermes(home, actions)
209
+ # Audit 2026-09-13 (Mem0 CLI init --agent pattern) — record which
210
+ # agents we touched so the per-agent index stays accurate. The
211
+ # store is best-effort: if the SQLite path can't open, we fall
212
+ # back to "install only" so the user is never blocked by a DB
213
+ # hiccup during a one-line CLI install.
214
+ store = None
215
+ try:
216
+ from ...storage.sqlite_store import MemoryStore
217
+ from ...cli._common import default_db_path
218
+ store = MemoryStore(default_db_path())
219
+ except Exception:
220
+ store = None
221
+ _install_codex(home, actions, store=store)
222
+ _install_claude(home, actions, store=store)
223
+ _install_hermes(home, actions, store=store)
206
224
  _openclaw_hint(home, actions)
225
+ if store is not None:
226
+ # OpenClaw is hinted (no auto-install) so we only touch it.
227
+ store.touch_agent("openclaw")
207
228
  print("[loop-memory] install-hooks results:")
208
229
  for a in actions:
209
230
  print(f" · {a}")
210
231
  print()
211
232
  print("Restart your CLI to pick up the new MCP server + hooks.")
212
233
  return 0
234
+
235
+
236
+
237
+ def run_init(args) -> int:
238
+ """``loop-memory init --agent <name> [--install-hooks]``.
239
+
240
+ Audit 2026-09-13 (Mem0 CLI ``mem0 init --agent <name>`` pattern).
241
+ Registers an agent in the per-agent identity index and, when
242
+ ``--install-hooks`` is passed, also runs the standard
243
+ ``install-hooks`` flow so MCP + SessionStart hooks are written
244
+ for that agent's CLI client. Idempotent — re-running for the
245
+ same name just bumps ``last_seen_at``.
246
+ """
247
+ from ...storage.sqlite_store import MemoryStore
248
+ from ...cli._common import default_db_path, die
249
+ agent_name: str | None = None
250
+ install_hooks = False
251
+ i = 0
252
+ while i < len(args):
253
+ a = args[i]
254
+ if a == "--agent" and i + 1 < len(args):
255
+ agent_name = args[i + 1].strip()
256
+ i += 2
257
+ elif a == "--install-hooks":
258
+ install_hooks = True
259
+ i += 1
260
+ elif a == "--help" or a == "-h":
261
+ print("usage: loop-memory init --agent <name> [--install-hooks]")
262
+ return 0
263
+ elif a.startswith("--"):
264
+ return die(f"[loop-memory] unknown flag: {a}")
265
+ else:
266
+ return die(f"[loop-memory] unexpected positional: {a}")
267
+ if not agent_name:
268
+ return die("usage: loop-memory init --agent <name> [--install-hooks]")
269
+ store = MemoryStore(default_db_path())
270
+ rec = store.register_agent(agent_name, hooks_installed=False)
271
+ verb = "registered" if rec["created"] else "refreshed"
272
+ print(f"[loop-memory] {verb} agent {agent_name!r} (scope={rec['scope']}, "
273
+ f"hooks_installed={rec['hooks_installed']}, "
274
+ f"last_seen_at={rec['last_seen_at']:.0f}).")
275
+ if install_hooks:
276
+ # Delegate to the standard install-hooks flow so the MCP + inject
277
+ # hook files get written for codex/claude/hermes if any of those
278
+ # clients are installed.
279
+ run_install_hooks([])
280
+ return 0
@@ -66,6 +66,7 @@ def run_recall(args) -> int:
66
66
  # we don't want argparse to swallow a token that happens to start
67
67
  # with "--" inside the user's query (e.g. ``recall --foo bar``).
68
68
  verbose = False
69
+ outline = False
69
70
  limit = 10
70
71
  qargs: list[str] = []
71
72
  i = 0
@@ -74,6 +75,12 @@ def run_recall(args) -> int:
74
75
  if a == "--verbose":
75
76
  verbose = True
76
77
  i += 1
78
+ elif a == "--outline":
79
+ # Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0) —
80
+ # cheap L0 list instead of full text. Sets a flag and
81
+ # routes the call to ``recall_paths`` below.
82
+ outline = True
83
+ i += 1
77
84
  elif a == "--limit" and i + 1 < len(args):
78
85
  try:
79
86
  limit = int(args[i + 1])
@@ -86,10 +93,13 @@ def run_recall(args) -> int:
86
93
  qargs.append(a)
87
94
  i += 1
88
95
  if not qargs:
89
- return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
96
+ return die("usage: loop-memory recall <query> [--verbose] [--outline] [--limit N]")
90
97
  store = MemoryStore(default_db_path())
91
98
  query = " ".join(qargs)
92
- r = store.recall(query, limit=limit)
99
+ if outline:
100
+ r = store.recall_paths(query, limit=limit)
101
+ else:
102
+ r = store.recall(query, limit=limit)
93
103
  has = False
94
104
  if r["wiki"]:
95
105
  has = True
@@ -97,7 +107,9 @@ def run_recall(args) -> int:
97
107
  for w in r["wiki"]:
98
108
  tag_s = " [" + ", ".join(w.get("tags") or []) + "]" if w.get("tags") else ""
99
109
  print(f"- **{w['title']}** (`{w['slug']}`) — imp {w['importance']:.2f}{tag_s}")
100
- if w.get("summary"):
110
+ if outline:
111
+ print(f" > {w.get('abstract','')}")
112
+ elif w.get("summary"):
101
113
  print(f" > {w['summary'][:240]}")
102
114
  print()
103
115
  if r["memories"]:
@@ -111,7 +123,11 @@ def run_recall(args) -> int:
111
123
  # render the list as a deterministic badge strip without
112
124
  # re-sorting.
113
125
  print(f" why: {', '.join(m['why'])}")
114
- print(f" {m['text'][:240]}")
126
+ if outline:
127
+ # L0 outline mode: print only the short abstract.
128
+ print(f" {m.get('abstract','')}")
129
+ else:
130
+ print(f" {m['text'][:240]}")
115
131
  print()
116
132
  if r["entities"]:
117
133
  has = True
@@ -398,3 +414,65 @@ def run_inject(args) -> int:
398
414
  out.write(f" {text}\n")
399
415
  sys.stdout.write(out.getvalue())
400
416
  return 0
417
+
418
+
419
+
420
+ def run_recall_paths(args) -> int:
421
+ """``loop-memory recall-paths <query> [--limit N]``.
422
+
423
+ Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0). Returns
424
+ only ``id`` + ``abstract`` + ``score`` + ``why`` for each hit —
425
+ no full text, no body. Use this when an agent needs to *decide*
426
+ which candidate to open; once it has picked an id, fetch the
427
+ body via ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
428
+ """
429
+ from ...storage.sqlite_store import MemoryStore
430
+ if not args:
431
+ return die("usage: loop-memory recall-paths <query> [--limit N]")
432
+ limit = 12
433
+ qargs: list[str] = []
434
+ i = 0
435
+ while i < len(args):
436
+ a = args[i]
437
+ if a == "--limit" and i + 1 < len(args):
438
+ try:
439
+ limit = int(args[i + 1])
440
+ except ValueError:
441
+ return die(f"--limit must be an integer, got {args[i + 1]!r}")
442
+ i += 2
443
+ elif a.startswith("--"):
444
+ return die(f"unknown flag: {a}")
445
+ else:
446
+ qargs.append(a)
447
+ i += 1
448
+ if not qargs:
449
+ return die("usage: loop-memory recall-paths <query> [--limit N]")
450
+ store = MemoryStore(default_db_path())
451
+ query = " ".join(qargs)
452
+ r = store.recall_paths(query, limit=limit)
453
+ has = False
454
+ if r["memories"]:
455
+ has = True
456
+ print(f"## Outline (memories, {len(r['memories'])} match{'es' if len(r['memories'])!=1 else ''})")
457
+ for m in r["memories"]:
458
+ why = f" why: {', '.join(m['why'])}" if m.get("why") else ""
459
+ print(f"- [{m['kind']}] score={m['score']:.2f} id={m['id']}{why}")
460
+ print(f" > {m.get('abstract','')}")
461
+ print()
462
+ if r["wiki"]:
463
+ has = True
464
+ print(f"## Outline (wiki, {len(r['wiki'])} match{'es' if len(r['wiki'])!=1 else ''})")
465
+ for w in r["wiki"]:
466
+ why = f" why: {', '.join(w['why'])}" if w.get("why") else ""
467
+ print(f"- score={w['score']:.2f} slug={w['slug']}{why}")
468
+ print(f" > {w.get('abstract','')}")
469
+ print()
470
+ if r["entities"]:
471
+ has = True
472
+ print(f"## Outline (entities, {len(r['entities'])})")
473
+ for e in r["entities"]:
474
+ print(f"- {e['name']} _({e['entity_kind']}, w={e['weight']:.2f})_")
475
+ print()
476
+ if not has:
477
+ print(f"_( nothing matched {query!r})_")
478
+ return 0
@@ -118,6 +118,13 @@ COMMANDS = {
118
118
  "subgraph": cognitive_cmd.run_subgraph,
119
119
  "graph-rebuild": cognitive_cmd.run_graph_rebuild,
120
120
  "wiki-reclassify-legacy": cognitive_cmd.run_wiki_reclassify_legacy,
121
+ # Audit 2026-09-06 — per-memory stats + portable snapshot.
122
+ "memory-stats": cognitive_cmd.run_memory_stats,
123
+ "snapshot": cognitive_cmd.run_snapshot,
124
+ "restore": cognitive_cmd.run_restore,
125
+ # Audit 2026-09-13 — L0 outline recall + per-agent identity bootstrap.
126
+ "recall-paths": read_cmd.run_recall_paths,
127
+ "init": hooks_cmd.run_init,
121
128
  "rules": rules_cmd.run_rules,
122
129
  "version": _run_version,
123
130
  }
@@ -159,6 +166,13 @@ COMMAND_HELP: dict[str, str] = {
159
166
  "subgraph": "loop-memory subgraph <query> # print a small subgraph.",
160
167
  "version": "loop-memory version # print the installed distribution version.",
161
168
  "wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
169
+ "memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
170
+ "snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
171
+ "restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
172
+ # Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
173
+ "recall-paths": "loop-memory recall-paths <query> [--limit N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13).",
174
+ # Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
175
+ "init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
162
176
  "rules": "loop-memory rules [--agent codex|claude|hermes|openclaw] [--write] [--force]\n Print, or append into the agent's rule file, the three-phase memory discipline (task start / mid-task / wrap-up). Never overwrites user content.",
163
177
  }
164
178
 
@@ -62,4 +62,40 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
62
62
  raise HTTPException(500, f"import failed: {e}")
63
63
  return r.to_dict()
64
64
 
65
+ @app.post("/api/snapshot")
66
+ def snapshot_create(body: dict):
67
+ """Write a portable SQLite snapshot of the live store.
65
68
 
69
+ Body: ``{"out_path": "/path/to/file.memory.sqlite"}``. Returns
70
+ the summary dict from ``loop_memory.storage.snapshot.snapshot``.
71
+ """
72
+ from ...storage.snapshot import snapshot as _snapshot
73
+ out = (body.get("out_path") or "").strip()
74
+ if not out:
75
+ raise HTTPException(400, "out_path is required")
76
+ try:
77
+ return _snapshot(store, out)
78
+ except Exception as e:
79
+ raise HTTPException(500, f"snapshot failed: {e}")
80
+
81
+ @app.post("/api/snapshot/restore")
82
+ def snapshot_restore(body: dict):
83
+ """Re-hydrate a portable SQLite snapshot into the live store.
84
+
85
+ Body: ``{"in_path": "/path/to/file.memory.sqlite"}``. Returns
86
+ the summary dict from ``loop_memory.storage.snapshot.restore``.
87
+ Refuses wrong-magic files with a 400 so the dashboard surfaces
88
+ the error clearly instead of corrupting the live store.
89
+ """
90
+ from ...storage.snapshot import restore as _restore
91
+ in_path = (body.get("in_path") or "").strip()
92
+ if not in_path:
93
+ raise HTTPException(400, "in_path is required")
94
+ try:
95
+ return _restore(store, in_path)
96
+ except FileNotFoundError as e:
97
+ raise HTTPException(404, str(e))
98
+ except ValueError as e:
99
+ raise HTTPException(400, str(e))
100
+ except Exception as e:
101
+ raise HTTPException(500, f"restore failed: {e}")
@@ -414,6 +414,22 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None)
414
414
  return {"deleted": n}
415
415
 
416
416
 
417
+ @app.get("/api/memories/{mid}/stats")
418
+ def memory_stats_route(mid: str):
419
+ """Per-memory lifetime stats (audit 2026-09-06).
420
+
421
+ Returns the flat dict produced by ``MemoryStore.memory_stats``
422
+ — see that method for the field list. 404 if the memory id
423
+ is unknown. The dashboard uses this to surface
424
+ recall / positive / negative counters alongside each memory
425
+ row without a second round-trip.
426
+ """
427
+ res = store.memory_stats(mid)
428
+ if res is None:
429
+ from fastapi import HTTPException
430
+ raise HTTPException(404, "memory not found")
431
+ return res
432
+
417
433
  @app.get("/api/memories/{mid}", name="memory_drill_down")
418
434
  def memory_drill_down(mid: str):
419
435
  """L2 drill-down for a single memory row (full text)."""
@@ -392,6 +392,98 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
392
392
  }
393
393
 
394
394
 
395
+ @app.get("/api/recall/outline")
396
+ def recall_outline(query: str = "", limit: int = 12,
397
+ include: str = "memories,wiki,entities",
398
+ bump: int = 0, source: str | None = None):
399
+ """Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
400
+ v0.3.0 recall ladder pattern).
401
+
402
+ Returns the same ranked lists as ``GET /api/recall`` but each
403
+ hit carries only ``id`` + ``kind`` + ``abstract`` (≤80 chars)
404
+ + ``score`` + ``why`` -- never the full ``text`` / ``body``.
405
+ Use this when an agent needs to *decide* which candidate to
406
+ open; once it has picked an id, fetch the body via
407
+ ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
408
+
409
+ ``bump`` defaults to 0 so the L0 listing does not inflate
410
+ ``recall_count`` before the agent has actually decided to read
411
+ the body. Set ``bump=1`` only when the agent commits to the
412
+ candidate set.
413
+ """
414
+ if not query:
415
+ raise HTTPException(status_code=400, detail="query is required")
416
+ wanted = tuple(s.strip() for s in include.split(",") if s.strip())
417
+ if not wanted:
418
+ wanted = ("memories", "wiki", "entities")
419
+ r = store.recall_paths(
420
+ query,
421
+ limit=limit,
422
+ include=wanted,
423
+ bump_signals=bool(bump),
424
+ source=source,
425
+ )
426
+ return {
427
+ "query": query,
428
+ "tokens": r.get("tokens", []),
429
+ "memories": r.get("memories", []),
430
+ "wiki": r.get("wiki", []),
431
+ "entities": r.get("entities", []),
432
+ "mode": "outline",
433
+ "source": source,
434
+ }
435
+
436
+
437
+ @app.get("/api/agents")
438
+ def list_agents_route():
439
+ """Audit 2026-09-13 — list all registered agents (Mem0 CLI
440
+ ``init --agent`` pattern). Each entry includes ``name``,
441
+ ``scope``, ``created_at``, ``last_seen_at``,
442
+ ``hooks_installed`` so a user can answer "which CLIs are
443
+ wired into this store?" with a single HTTP GET.
444
+ """
445
+ return {"agents": store.list_agents()}
446
+
447
+
448
+ @app.post("/api/init/agent")
449
+ def init_agent_route(body: dict):
450
+ """Audit 2026-09-13 — register (or refresh) a named agent.
451
+
452
+ Body keys (all optional except ``name``):
453
+ - ``name`` (str, required) — e.g. ``"codex"``, ``"claude"``,
454
+ ``"hermes"``, ``"openclaw"``, or a custom name.
455
+ - ``scope`` (str, default ``"global"``) — reserved for
456
+ future per-source isolation; today always ``"global"``.
457
+ - ``install_hooks`` (bool, default ``false``) — when true,
458
+ also runs the equivalent of ``install-hooks`` so MCP +
459
+ SessionStart files are written for codex/claude/hermes if
460
+ those clients are installed locally. Best-effort: a hook
461
+ failure does not fail the registration.
462
+
463
+ Idempotent — re-registering an existing name just bumps
464
+ ``last_seen_at``. Returns the persisted row.
465
+ """
466
+ if not isinstance(body, dict):
467
+ raise HTTPException(status_code=400, detail="body must be a JSON object")
468
+ name = (body.get("name") or "").strip()
469
+ if not name:
470
+ raise HTTPException(status_code=400, detail="name is required")
471
+ scope = (body.get("scope") or "global").strip() or "global"
472
+ want_hooks = bool(body.get("install_hooks", False))
473
+ rec = store.register_agent(name, scope=scope, hooks_installed=want_hooks)
474
+ if want_hooks:
475
+ # Best-effort: write MCP + SessionStart hooks for whatever
476
+ # local CLI clients the user has installed. We don't
477
+ # surface the per-client summary here; ``install-hooks``
478
+ # already prints its own summary when run from the CLI.
479
+ try:
480
+ from ...cli.commands.hooks import run_install_hooks
481
+ run_install_hooks([])
482
+ except Exception:
483
+ pass
484
+ return rec
485
+
486
+
395
487
  @app.get("/api/pipeline")
396
488
  def pipeline_dashboard_route():
397
489
  """5-stage data flow dashboard. See handlers.pipeline_dashboard."""