loop-memory 0.4.7__tar.gz → 0.4.9__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 (149) hide show
  1. {loop_memory-0.4.7 → loop_memory-0.4.9}/PKG-INFO +21 -1
  2. {loop_memory-0.4.7 → loop_memory-0.4.9}/README.md +20 -0
  3. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/cognitive.py +33 -0
  4. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/hooks.py +74 -6
  5. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/read.py +114 -8
  6. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/main.py +12 -1
  7. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/system.py +147 -3
  8. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/sqlite_store.py +766 -1
  9. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/PKG-INFO +21 -1
  10. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/SOURCES.txt +10 -0
  11. {loop_memory-0.4.7 → loop_memory-0.4.9}/pyproject.toml +1 -1
  12. loop_memory-0.4.9/tests/test_agents_init.py +154 -0
  13. loop_memory-0.4.9/tests/test_cli_export_okf.py +89 -0
  14. loop_memory-0.4.9/tests/test_cli_init.py +106 -0
  15. loop_memory-0.4.9/tests/test_cli_recall_as_of.py +90 -0
  16. loop_memory-0.4.9/tests/test_cli_recall_paths.py +155 -0
  17. loop_memory-0.4.9/tests/test_export_okf.py +162 -0
  18. loop_memory-0.4.9/tests/test_recall_as_of.py +154 -0
  19. loop_memory-0.4.9/tests/test_recall_paths.py +213 -0
  20. loop_memory-0.4.9/tests/test_routes_audit_2026_09_13.py +141 -0
  21. loop_memory-0.4.9/tests/test_routes_audit_2026_09_20.py +119 -0
  22. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_supersession_chain.py +1 -1
  23. {loop_memory-0.4.7 → loop_memory-0.4.9}/LICENSE +0 -0
  24. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/__init__.py +0 -0
  25. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/__init__.py +0 -0
  26. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/embedding.py +0 -0
  27. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/sentence_embedder.py +0 -0
  28. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/backends/vector_store.py +0 -0
  29. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/__init__.py +0 -0
  30. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/_common.py +0 -0
  31. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/__init__.py +0 -0
  32. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/diag.py +0 -0
  33. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/graph.py +0 -0
  34. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/rules.py +0 -0
  35. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/serve.py +0 -0
  36. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/cli/commands/write.py +0 -0
  37. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/__init__.py +0 -0
  38. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/loop.py +0 -0
  39. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/engine/reflect.py +0 -0
  40. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/examples/__init__.py +0 -0
  41. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/examples/demo.py +0 -0
  42. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/export/__init__.py +0 -0
  43. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/export/memory_md.py +0 -0
  44. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/__init__.py +0 -0
  45. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/build.py +0 -0
  46. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/graph/extract.py +0 -0
  47. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/__init__.py +0 -0
  48. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/loader.py +0 -0
  49. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/ingest/pipeline.py +0 -0
  50. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/__init__.py +0 -0
  51. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/cognitive.py +0 -0
  52. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/compact.py +0 -0
  53. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/consolidate.py +0 -0
  54. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/contradiction.py +0 -0
  55. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/evolution.py +0 -0
  56. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/graph.py +0 -0
  57. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/llm_compact_pass.py +0 -0
  58. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/llm_consolidate.py +0 -0
  59. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/jobs/scheduler.py +0 -0
  60. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/__init__.py +0 -0
  61. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/base.py +0 -0
  62. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/openai_adapter.py +0 -0
  63. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/llm/providers.py +0 -0
  64. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/mcp/__init__.py +0 -0
  65. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/memory/__init__.py +0 -0
  66. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/memory/types.py +0 -0
  67. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/__init__.py +0 -0
  68. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/private.py +0 -0
  69. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/privacy/redact.py +0 -0
  70. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/py.typed +0 -0
  71. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/sdk.py +0 -0
  72. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/sdk_extensions.py +0 -0
  73. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/security/__init__.py +0 -0
  74. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/security/secrets.py +0 -0
  75. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/__init__.py +0 -0
  76. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/app.py +0 -0
  77. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/handlers.py +0 -0
  78. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/_shared.py +0 -0
  79. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/admin.py +0 -0
  80. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/cognitive.py +0 -0
  81. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/export.py +0 -0
  82. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/graph.py +0 -0
  83. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/insights.py +0 -0
  84. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/memories.py +0 -0
  85. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/sessions.py +0 -0
  86. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/routes/wiki.py +0 -0
  87. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/static/__init__.py +0 -0
  88. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/static/index.html +0 -0
  89. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/serve/watcher.py +0 -0
  90. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/__init__.py +0 -0
  91. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/retrieval.py +0 -0
  92. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/storage/snapshot.py +0 -0
  93. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/__init__.py +0 -0
  94. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/backfill.py +0 -0
  95. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/classifier.py +0 -0
  96. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/prompts.py +0 -0
  97. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory/wiki/scope.py +0 -0
  98. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/dependency_links.txt +0 -0
  99. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/entry_points.txt +0 -0
  100. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/requires.txt +0 -0
  101. {loop_memory-0.4.7 → loop_memory-0.4.9}/loop_memory.egg-info/top_level.txt +0 -0
  102. {loop_memory-0.4.7 → loop_memory-0.4.9}/setup.cfg +0 -0
  103. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_admin_ingest_route.py +0 -0
  104. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_agent_memory_api.py +0 -0
  105. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_agent_memory_sdk.py +0 -0
  106. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_auth_token_rotate.py +0 -0
  107. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_audit_supersede.py +0 -0
  108. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_memory_stats.py +0 -0
  109. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_recall_verbose.py +0 -0
  110. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_rules.py +0 -0
  111. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_snapshot.py +0 -0
  112. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_v7.py +0 -0
  113. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_cli_version.py +0 -0
  114. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_contradictions.py +0 -0
  115. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_evolution.py +0 -0
  116. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_evolution_quality.py +0 -0
  117. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_expanduser.py +0 -0
  118. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_export_ask.py +0 -0
  119. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_graph.py +0 -0
  120. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_ingest.py +0 -0
  121. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_consolidator.py +0 -0
  122. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_fingerprint_not_persisted.py +0 -0
  123. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_providers.py +0 -0
  124. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_llm_test_endpoint.py +0 -0
  125. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_loop.py +0 -0
  126. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_mcp.py +0 -0
  127. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_memories_pagination.py +0 -0
  128. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_memory_stats.py +0 -0
  129. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_openclaw_loader.py +0 -0
  130. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_package_version.py +0 -0
  131. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_recall_provenance.py +0 -0
  132. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_reflection.py +0 -0
  133. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_repo_framing.py +0 -0
  134. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_score_api.py +0 -0
  135. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_scoring_v2.py +0 -0
  136. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_secrets.py +0 -0
  137. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_serve_app.py +0 -0
  138. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_serve_handlers.py +0 -0
  139. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_session_order.py +0 -0
  140. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_short_query.py +0 -0
  141. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_snapshot.py +0 -0
  142. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_store.py +0 -0
  143. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_summarization.py +0 -0
  144. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_universal_memory.py +0 -0
  145. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_vector_store.py +0 -0
  146. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_watcher.py +0 -0
  147. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_wiki_classifier.py +0 -0
  148. {loop_memory-0.4.7 → loop_memory-0.4.9}/tests/test_wiki_export_escape.py +0 -0
  149. {loop_memory-0.4.7 → loop_memory-0.4.9}/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.7
3
+ Version: 0.4.9
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,6 +66,26 @@ Dynamic: license-file
66
66
 
67
67
  ---
68
68
 
69
+ > **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
70
+
71
+ > **What's new in 0.4.8** — **L0 outline recall** +
72
+ > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
73
+ > (and the new `recall --outline` flag) returns only `id` +
74
+ > `abstract` + `score` + `why` for each hit — never the full
75
+ > body — so an agent can identify the top candidates cheaply
76
+ > before deciding which bodies to fetch (audit 2026-09-13,
77
+ > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
78
+ > `loop-memory init --agent <name>` registers an agent in the
79
+ > new `agents` table and (with `--install-hooks`) also wires
80
+ > the MCP + SessionStart hooks for codex/claude/hermes; the
81
+ > index is bumped on every `install-hooks` run so a user can
82
+ > always answer "which CLIs are wired into this store?" with
83
+ > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
84
+ > pattern). Both shapes ship with HTTP routes
85
+ > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
86
+ > 57 new regression cases pin both shapes. [Full
87
+ > changelog →](CHANGELOG.md)
88
+ >
69
89
  > **What's new in 0.4.7** — **per-memory lifetime stats** +
70
90
  > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
71
91
  > returns a flat dict (recall_count, positive, negative, age,
@@ -22,6 +22,26 @@
22
22
 
23
23
  ---
24
24
 
25
+ > **What's new in 0.4.9** — **OKF v0.2 export** + **bi-temporal `as_of` recall**. `loop-memory export-okf <dir>` writes an [Open Knowledge Format](https://github.com/okf/SPEC) v0.2 bundle — one `.md` file per wiki page with YAML frontmatter + an `index.md` index — so a Loop Memory store can be handed to any OKF-aware tool (Grep, Obsidian, akitaonrails/ai-memory, okf-agent-memory, ...) without re-parsing the body (audit 2026-09-20, Google OKF v0.2 spec + akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory, all Apache-2.0/MIT, 2026-09-01 → 2026-09-06). `loop-memory recall <q> --as-of <ISO|epoch>` answers the bi-temporal question "what did we know about this query at that moment?" — a memory written after the moment is invisible, and a superseded memory walks its chain to find whether the invalidation had happened by then (audit 2026-09-20, loomcycle v1.33-v1.49 RFCs BL/BS/BU/BV/BW, Apache-2.0). Both shapes ship with HTTP routes (`POST /api/export/okf`, `GET /api/recall?as_of=...`). 34 new regression cases pin both shapes. [Full CHANGELOG →](./CHANGELOG.md#049---2026-09-20)
26
+
27
+ > **What's new in 0.4.8** — **L0 outline recall** +
28
+ > **per-agent identity bootstrap**. `loop-memory recall-paths <q>`
29
+ > (and the new `recall --outline` flag) returns only `id` +
30
+ > `abstract` + `score` + `why` for each hit — never the full
31
+ > body — so an agent can identify the top candidates cheaply
32
+ > before deciding which bodies to fetch (audit 2026-09-13,
33
+ > tigerless-labs/agent-memory v0.3.0 recall-ladder pattern).
34
+ > `loop-memory init --agent <name>` registers an agent in the
35
+ > new `agents` table and (with `--install-hooks`) also wires
36
+ > the MCP + SessionStart hooks for codex/claude/hermes; the
37
+ > index is bumped on every `install-hooks` run so a user can
38
+ > always answer "which CLIs are wired into this store?" with
39
+ > one SQL query (audit 2026-09-13, Mem0 CLI `init --agent`
40
+ > pattern). Both shapes ship with HTTP routes
41
+ > (`/api/recall/outline`, `/api/agents`, `/api/init/agent`).
42
+ > 57 new regression cases pin both shapes. [Full
43
+ > changelog →](CHANGELOG.md)
44
+ >
25
45
  > **What's new in 0.4.7** — **per-memory lifetime stats** +
26
46
  > **portable SQLite snapshot**. `loop-memory memory-stats <id>`
27
47
  > returns a flat dict (recall_count, positive, negative, age,
@@ -108,6 +108,17 @@ def _build_parser() -> argparse.ArgumentParser:
108
108
  rs.add_argument("in_path",
109
109
  help="Source .memory.sqlite file")
110
110
 
111
+ # Audit 2026-09-20: OKF v0.2 bundle export
112
+ # (akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory +
113
+ # Google OKF v0.2 spec).
114
+ eo = sub.add_parser(
115
+ "export-okf",
116
+ help="Export wiki pages as an OKF v0.2 markdown bundle")
117
+ eo.add_argument("out_dir",
118
+ help="Destination directory for the bundle")
119
+ eo.add_argument("--scope", default=None,
120
+ help="Filter: 'global' or a single source token (e.g. codex)")
121
+
111
122
  return p
112
123
 
113
124
 
@@ -342,3 +353,25 @@ def run_restore(args: list) -> int:
342
353
  return _emit(_restore(s, ns.in_path))
343
354
  except (FileNotFoundError, ValueError) as e:
344
355
  return _emit({"error": str(e)})
356
+
357
+
358
+ def run_export_okf(args) -> int:
359
+ """``loop-memory export-okf <out_dir> [--scope SCOPE]``
360
+
361
+ Audit 2026-09-20: OKF v0.2 bundle export
362
+ (akitaonrails/ai-memory 2.0 + okf-memory/okf-agent-memory +
363
+ Google OKF v0.2 spec). Returns a summary dict with the bundle
364
+ root + per-page list so the caller can confirm the bundle is
365
+ sane. ``--scope`` accepts ``global`` (only globally-scoped
366
+ pages) or a single source token (only pages whose scope list
367
+ contains the token); omit the flag to emit every page.
368
+ """
369
+ import argparse as _ap
370
+ p = _ap.ArgumentParser(prog="loop-memory export-okf")
371
+ p.add_argument("out_dir")
372
+ p.add_argument("--scope", default=None)
373
+ p.add_argument("--db", default=os.environ.get("LOOP_MEMORY_DB", DEFAULT_DB))
374
+ ns = p.parse_args(args)
375
+ s = MemoryStore(ns.db)
376
+ r = s.export_okf(ns.out_dir, scope_filter=ns.scope)
377
+ return _emit(r)
@@ -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
@@ -51,13 +51,17 @@ def run_stats(_args) -> int:
51
51
 
52
52
 
53
53
  def run_recall(args) -> int:
54
- """Run ``loop-memory recall <query> [--verbose] [--limit N]``.
54
+ """Run ``loop-memory recall <query> [--verbose] [--outline] [--as-of T] [--limit N]``.
55
55
 
56
56
  With ``--verbose`` each memory hit is annotated with the
57
57
  ``why: [...]`` provenance labels produced by ``MemoryStore.recall``
58
- (audit 2026-08-30, agentmemory v1.2.0 pattern). The wiki and
59
- entity sections are unchanged — only the raw-memory block grows
60
- the new line.
58
+ (audit 2026-08-30, agentmemory v1.2.0 pattern). ``--outline``
59
+ switches to the L0 list (audit 2026-09-13, tigerless-labs/
60
+ agent-memory v0.3.0). ``--as-of <float-or-ISO>`` enables
61
+ bi-temporal recall (audit 2026-09-20, loomcycle v1.33+) so a
62
+ caller can ask "what did we know about this query at that
63
+ moment?". The wiki and entity sections are unchanged — only
64
+ the raw-memory block grows the new line.
61
65
  """
62
66
  from ...storage.sqlite_store import MemoryStore
63
67
  if not args:
@@ -66,7 +70,9 @@ def run_recall(args) -> int:
66
70
  # we don't want argparse to swallow a token that happens to start
67
71
  # with "--" inside the user's query (e.g. ``recall --foo bar``).
68
72
  verbose = False
73
+ outline = False
69
74
  limit = 10
75
+ as_of: float | None = None
70
76
  qargs: list[str] = []
71
77
  i = 0
72
78
  while i < len(args):
@@ -74,6 +80,30 @@ def run_recall(args) -> int:
74
80
  if a == "--verbose":
75
81
  verbose = True
76
82
  i += 1
83
+ elif a == "--outline":
84
+ # Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0) —
85
+ # cheap L0 list instead of full text. Sets a flag and
86
+ # routes the call to ``recall_paths`` below.
87
+ outline = True
88
+ i += 1
89
+ elif a == "--as-of" and i + 1 < len(args):
90
+ # Audit 2026-09-20 (loomcycle v1.33+) — bi-temporal
91
+ # recall: "what did we know about this query at that
92
+ # moment?". Accepts float epoch OR ISO-8601 string.
93
+ raw = args[i + 1].strip()
94
+ try:
95
+ as_of = float(raw)
96
+ except ValueError:
97
+ try:
98
+ from datetime import datetime
99
+ as_of = datetime.fromisoformat(
100
+ raw.replace("Z", "+00:00")
101
+ ).timestamp()
102
+ except Exception:
103
+ return die(
104
+ f"--as-of must be float epoch or ISO-8601; got {raw!r}"
105
+ )
106
+ i += 2
77
107
  elif a == "--limit" and i + 1 < len(args):
78
108
  try:
79
109
  limit = int(args[i + 1])
@@ -86,10 +116,18 @@ def run_recall(args) -> int:
86
116
  qargs.append(a)
87
117
  i += 1
88
118
  if not qargs:
89
- return die("usage: loop-memory recall <query> [--verbose] [--limit N]")
119
+ return die("usage: loop-memory recall <query> [--verbose] [--outline] [--as-of <ISO|epoch>] [--limit N]")
90
120
  store = MemoryStore(default_db_path())
91
121
  query = " ".join(qargs)
92
- r = store.recall(query, limit=limit)
122
+ if as_of is not None:
123
+ try:
124
+ r = store.recall_as_of(query, as_of, limit=limit)
125
+ except ValueError as e:
126
+ return die(str(e))
127
+ elif outline:
128
+ r = store.recall_paths(query, limit=limit)
129
+ else:
130
+ r = store.recall(query, limit=limit)
93
131
  has = False
94
132
  if r["wiki"]:
95
133
  has = True
@@ -97,7 +135,9 @@ def run_recall(args) -> int:
97
135
  for w in r["wiki"]:
98
136
  tag_s = " [" + ", ".join(w.get("tags") or []) + "]" if w.get("tags") else ""
99
137
  print(f"- **{w['title']}** (`{w['slug']}`) — imp {w['importance']:.2f}{tag_s}")
100
- if w.get("summary"):
138
+ if outline:
139
+ print(f" > {w.get('abstract','')}")
140
+ elif w.get("summary"):
101
141
  print(f" > {w['summary'][:240]}")
102
142
  print()
103
143
  if r["memories"]:
@@ -111,7 +151,11 @@ def run_recall(args) -> int:
111
151
  # render the list as a deterministic badge strip without
112
152
  # re-sorting.
113
153
  print(f" why: {', '.join(m['why'])}")
114
- print(f" {m['text'][:240]}")
154
+ if outline:
155
+ # L0 outline mode: print only the short abstract.
156
+ print(f" {m.get('abstract','')}")
157
+ else:
158
+ print(f" {m['text'][:240]}")
115
159
  print()
116
160
  if r["entities"]:
117
161
  has = True
@@ -398,3 +442,65 @@ def run_inject(args) -> int:
398
442
  out.write(f" {text}\n")
399
443
  sys.stdout.write(out.getvalue())
400
444
  return 0
445
+
446
+
447
+
448
+ def run_recall_paths(args) -> int:
449
+ """``loop-memory recall-paths <query> [--limit N]``.
450
+
451
+ Audit 2026-09-13 (tigerless-labs/agent-memory v0.3.0). Returns
452
+ only ``id`` + ``abstract`` + ``score`` + ``why`` for each hit —
453
+ no full text, no body. Use this when an agent needs to *decide*
454
+ which candidate to open; once it has picked an id, fetch the
455
+ body via ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
456
+ """
457
+ from ...storage.sqlite_store import MemoryStore
458
+ if not args:
459
+ return die("usage: loop-memory recall-paths <query> [--limit N]")
460
+ limit = 12
461
+ qargs: list[str] = []
462
+ i = 0
463
+ while i < len(args):
464
+ a = args[i]
465
+ if a == "--limit" and i + 1 < len(args):
466
+ try:
467
+ limit = int(args[i + 1])
468
+ except ValueError:
469
+ return die(f"--limit must be an integer, got {args[i + 1]!r}")
470
+ i += 2
471
+ elif a.startswith("--"):
472
+ return die(f"unknown flag: {a}")
473
+ else:
474
+ qargs.append(a)
475
+ i += 1
476
+ if not qargs:
477
+ return die("usage: loop-memory recall-paths <query> [--limit N]")
478
+ store = MemoryStore(default_db_path())
479
+ query = " ".join(qargs)
480
+ r = store.recall_paths(query, limit=limit)
481
+ has = False
482
+ if r["memories"]:
483
+ has = True
484
+ print(f"## Outline (memories, {len(r['memories'])} match{'es' if len(r['memories'])!=1 else ''})")
485
+ for m in r["memories"]:
486
+ why = f" why: {', '.join(m['why'])}" if m.get("why") else ""
487
+ print(f"- [{m['kind']}] score={m['score']:.2f} id={m['id']}{why}")
488
+ print(f" > {m.get('abstract','')}")
489
+ print()
490
+ if r["wiki"]:
491
+ has = True
492
+ print(f"## Outline (wiki, {len(r['wiki'])} match{'es' if len(r['wiki'])!=1 else ''})")
493
+ for w in r["wiki"]:
494
+ why = f" why: {', '.join(w['why'])}" if w.get("why") else ""
495
+ print(f"- score={w['score']:.2f} slug={w['slug']}{why}")
496
+ print(f" > {w.get('abstract','')}")
497
+ print()
498
+ if r["entities"]:
499
+ has = True
500
+ print(f"## Outline (entities, {len(r['entities'])})")
501
+ for e in r["entities"]:
502
+ print(f"- {e['name']} _({e['entity_kind']}, w={e['weight']:.2f})_")
503
+ print()
504
+ if not has:
505
+ print(f"_( nothing matched {query!r})_")
506
+ return 0
@@ -122,6 +122,12 @@ COMMANDS = {
122
122
  "memory-stats": cognitive_cmd.run_memory_stats,
123
123
  "snapshot": cognitive_cmd.run_snapshot,
124
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,
128
+ # Audit 2026-09-20 — OKF v0.2 export (akitaonrails/ai-memory 2.0
129
+ # + okf-agent-memory + Google OKF spec).
130
+ "export-okf": cognitive_cmd.run_export_okf,
125
131
  "rules": rules_cmd.run_rules,
126
132
  "version": _run_version,
127
133
  }
@@ -155,7 +161,7 @@ COMMAND_HELP: dict[str, str] = {
155
161
  "install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
156
162
  "mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
157
163
  "openclaw-setup": "loop-memory openclaw-setup # install the openclaw/clawx auto-ingest watcher (launchd on macOS).",
158
- "recall": "loop-memory recall <text> # show top memories.",
164
+ "recall": "loop-memory recall <text> [--as-of <ISO|epoch>] # show top memories (--as-of = bi-temporal recall).",
159
165
  "rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
160
166
  "serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
161
167
  "stats": "loop-memory stats # counters.",
@@ -165,7 +171,12 @@ COMMAND_HELP: dict[str, str] = {
165
171
  "wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
166
172
  "memory-stats": "loop-memory memory-stats <id> [--prefix] # per-memory lifetime stats (audit 2026-09-06, agentmemory v1.3.0).",
167
173
  "snapshot": "loop-memory snapshot <out.memory.sqlite> # write a portable SQLite snapshot (audit 2026-09-06, codexa-memory v0.2.0).",
174
+ "export-okf": "loop-memory export-okf <out_dir> [--scope S] # write an OKF v0.2 bundle (audit 2026-09-20).",
168
175
  "restore": "loop-memory restore <in.memory.sqlite> # re-hydrate a portable SQLite snapshot.",
176
+ # Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory v0.3.0).
177
+ "recall-paths": "loop-memory recall-paths <query> [--limit N] # cheap L0 outline (id+abstract+score) instead of full text (audit 2026-09-13).",
178
+ # Audit 2026-09-13 — per-agent identity bootstrap (Mem0 CLI init --agent pattern).
179
+ "init": "loop-memory init --agent <name> [--install-hooks] # register an agent + (optionally) install hooks.",
169
180
  "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.",
170
181
  }
171
182
 
@@ -350,7 +350,8 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
350
350
  @app.get("/api/recall")
351
351
  def recall(query: str, limit: int = 10, include: str = "memories,wiki,entities",
352
352
  bump: int = 1, source: str | None = None,
353
- mode: str = "hybrid", level: int = 1):
353
+ mode: str = "hybrid", level: int = 1,
354
+ as_of: str | None = None):
354
355
  """Unified recall — wiki + memories + entities in one ranked stream.
355
356
 
356
357
  ``mode`` is 'hybrid' (default) for BM25+semantic+entity RRF,
@@ -363,11 +364,31 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
363
364
  and memories whose source matches this token are boosted. The
364
365
  dashboard passes the currently-active client (codex/claude/
365
366
  hermes/openclaw); the MCP/CLI tools pass the calling client.
367
+
368
+ ``as_of`` (audit 2026-09-20, loomcycle v1.33+) enables
369
+ bi-temporal recall: a float epoch OR an ISO-8601 string that
370
+ answers "what did we know about this query at that moment?".
371
+ When unset (default) the call returns the latest answer
372
+ (preserves the pre-0.4.9 contract). When set, the call
373
+ routes through ``MemoryStore.recall_as_of``.
366
374
  """
367
375
  wanted = tuple(s.strip() for s in include.split(",") if s.strip())
368
376
  if not wanted:
369
377
  wanted = ("memories", "wiki", "entities")
370
- if mode == "legacy" or not hasattr(store, "recall_hybrid"):
378
+ if as_of is not None and str(as_of).strip():
379
+ try:
380
+ r = store.recall_as_of(
381
+ query,
382
+ as_of,
383
+ limit=limit,
384
+ include=wanted,
385
+ bump_signals=bool(bump),
386
+ source=source,
387
+ )
388
+ recall_mode = "as_of"
389
+ except ValueError as e:
390
+ raise HTTPException(400, str(e))
391
+ elif mode == "legacy" or not hasattr(store, "recall_hybrid"):
371
392
  r = store.recall(
372
393
  query,
373
394
  limit=limit,
@@ -375,23 +396,146 @@ def register(app: FastAPI, store: MemoryStore, scheduler: Optional[Any] = None,
375
396
  bump_signals=bool(bump),
376
397
  source=source,
377
398
  )
399
+ recall_mode = mode
378
400
  else:
379
401
  r = store.recall_hybrid(
380
402
  query, limit=limit, include=wanted,
381
403
  bump_signals=bool(bump), source=source, level=level,
382
404
  )
405
+ recall_mode = mode
383
406
  return {
384
407
  "query": query,
385
408
  "tokens": r.get("tokens", []),
386
409
  "memories": r.get("memories", []),
387
410
  "wiki": r.get("wiki", []),
388
411
  "entities": r.get("entities", []),
389
- "mode": mode,
412
+ "mode": recall_mode,
390
413
  "source": source,
391
414
  "level": level,
415
+ "as_of": as_of,
392
416
  }
393
417
 
394
418
 
419
+ @app.post("/api/export/okf")
420
+ def export_okf(body: dict):
421
+ """Audit 2026-09-20: OKF v0.2 bundle export.
422
+
423
+ Body shape::
424
+
425
+ {
426
+ "out_dir": "/abs/path/to/out",
427
+ "scope": "global" # optional: "global" or a source token
428
+ }
429
+
430
+ Writes one ``.md`` file per wiki page under
431
+ ``<out_dir>/pages/<slug>.md`` plus an ``index.md`` index.
432
+ See ``MemoryStore.export_okf`` for the field-mapping
433
+ contract.
434
+ """
435
+ out_dir = (body.get("out_dir") or "").strip()
436
+ if not out_dir:
437
+ raise HTTPException(400, "out_dir is required")
438
+ scope = body.get("scope")
439
+ if scope is not None:
440
+ scope = str(scope).strip() or None
441
+ try:
442
+ r = store.export_okf(out_dir, scope_filter=scope)
443
+ except Exception as e:
444
+ raise HTTPException(500, f"export-okf failed: {e}")
445
+ return r
446
+
447
+ @app.get("/api/recall/outline")
448
+ def recall_outline(query: str = "", limit: int = 12,
449
+ include: str = "memories,wiki,entities",
450
+ bump: int = 0, source: str | None = None):
451
+ """Audit 2026-09-13 — L0 outline recall (tigerless-labs/agent-memory
452
+ v0.3.0 recall ladder pattern).
453
+
454
+ Returns the same ranked lists as ``GET /api/recall`` but each
455
+ hit carries only ``id`` + ``kind`` + ``abstract`` (≤80 chars)
456
+ + ``score`` + ``why`` -- never the full ``text`` / ``body``.
457
+ Use this when an agent needs to *decide* which candidate to
458
+ open; once it has picked an id, fetch the body via
459
+ ``GET /api/memories/{id}`` or ``GET /api/wiki/{slug}``.
460
+
461
+ ``bump`` defaults to 0 so the L0 listing does not inflate
462
+ ``recall_count`` before the agent has actually decided to read
463
+ the body. Set ``bump=1`` only when the agent commits to the
464
+ candidate set.
465
+ """
466
+ if not query:
467
+ raise HTTPException(status_code=400, detail="query is required")
468
+ wanted = tuple(s.strip() for s in include.split(",") if s.strip())
469
+ if not wanted:
470
+ wanted = ("memories", "wiki", "entities")
471
+ r = store.recall_paths(
472
+ query,
473
+ limit=limit,
474
+ include=wanted,
475
+ bump_signals=bool(bump),
476
+ source=source,
477
+ )
478
+ return {
479
+ "query": query,
480
+ "tokens": r.get("tokens", []),
481
+ "memories": r.get("memories", []),
482
+ "wiki": r.get("wiki", []),
483
+ "entities": r.get("entities", []),
484
+ "mode": "outline",
485
+ "source": source,
486
+ }
487
+
488
+
489
+ @app.get("/api/agents")
490
+ def list_agents_route():
491
+ """Audit 2026-09-13 — list all registered agents (Mem0 CLI
492
+ ``init --agent`` pattern). Each entry includes ``name``,
493
+ ``scope``, ``created_at``, ``last_seen_at``,
494
+ ``hooks_installed`` so a user can answer "which CLIs are
495
+ wired into this store?" with a single HTTP GET.
496
+ """
497
+ return {"agents": store.list_agents()}
498
+
499
+
500
+ @app.post("/api/init/agent")
501
+ def init_agent_route(body: dict):
502
+ """Audit 2026-09-13 — register (or refresh) a named agent.
503
+
504
+ Body keys (all optional except ``name``):
505
+ - ``name`` (str, required) — e.g. ``"codex"``, ``"claude"``,
506
+ ``"hermes"``, ``"openclaw"``, or a custom name.
507
+ - ``scope`` (str, default ``"global"``) — reserved for
508
+ future per-source isolation; today always ``"global"``.
509
+ - ``install_hooks`` (bool, default ``false``) — when true,
510
+ also runs the equivalent of ``install-hooks`` so MCP +
511
+ SessionStart files are written for codex/claude/hermes if
512
+ those clients are installed locally. Best-effort: a hook
513
+ failure does not fail the registration.
514
+
515
+ Idempotent — re-registering an existing name just bumps
516
+ ``last_seen_at``. Returns the persisted row.
517
+ """
518
+ if not isinstance(body, dict):
519
+ raise HTTPException(status_code=400, detail="body must be a JSON object")
520
+ name = (body.get("name") or "").strip()
521
+ if not name:
522
+ raise HTTPException(status_code=400, detail="name is required")
523
+ scope = (body.get("scope") or "global").strip() or "global"
524
+ want_hooks = bool(body.get("install_hooks", False))
525
+ rec = store.register_agent(name, scope=scope, hooks_installed=want_hooks)
526
+ if want_hooks:
527
+ # Best-effort: write MCP + SessionStart hooks for whatever
528
+ # local CLI clients the user has installed. We don't
529
+ # surface the per-client summary here; ``install-hooks``
530
+ # already prints its own summary when run from the CLI.
531
+ try:
532
+ from ...cli.commands.hooks import run_install_hooks
533
+ run_install_hooks([])
534
+ except Exception:
535
+ pass
536
+ return rec
537
+
538
+
395
539
  @app.get("/api/pipeline")
396
540
  def pipeline_dashboard_route():
397
541
  """5-stage data flow dashboard. See handlers.pipeline_dashboard."""