loop-memory 0.4.0__tar.gz → 0.4.1__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.
- {loop_memory-0.4.0 → loop_memory-0.4.1}/PKG-INFO +17 -11
- {loop_memory-0.4.0 → loop_memory-0.4.1}/README.md +16 -10
- loop_memory-0.4.1/loop_memory/cli/main.py +184 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/static/index.html +1 -1
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/PKG-INFO +17 -11
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/SOURCES.txt +2 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/pyproject.toml +1 -1
- loop_memory-0.4.1/tests/test_cli_version.py +130 -0
- loop_memory-0.4.1/tests/test_repo_framing.py +198 -0
- loop_memory-0.4.0/loop_memory/cli/main.py +0 -115
- {loop_memory-0.4.0 → loop_memory-0.4.1}/LICENSE +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/backends/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/backends/embedding.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/backends/sentence_embedder.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/backends/vector_store.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/_common.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/cognitive.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/diag.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/graph.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/hooks.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/read.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/serve.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/cli/commands/write.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/engine/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/engine/loop.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/engine/reflect.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/examples/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/examples/demo.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/export/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/export/memory_md.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/graph/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/graph/build.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/graph/extract.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/ingest/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/ingest/loader.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/ingest/pipeline.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/cognitive.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/compact.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/consolidate.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/contradiction.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/evolution.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/graph.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/llm_compact_pass.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/llm_consolidate.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/jobs/scheduler.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/llm/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/llm/base.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/llm/openai_adapter.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/llm/providers.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/mcp/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/memory/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/memory/types.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/privacy/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/privacy/private.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/privacy/redact.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/py.typed +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/sdk.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/sdk_extensions.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/security/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/security/secrets.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/app.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/handlers.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/_shared.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/admin.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/cognitive.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/export.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/graph.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/insights.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/memories.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/sessions.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/system.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/routes/wiki.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/static/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/serve/watcher.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/storage/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/storage/retrieval.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/storage/sqlite_store.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/wiki/__init__.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/wiki/backfill.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/wiki/classifier.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/wiki/prompts.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory/wiki/scope.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/dependency_links.txt +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/entry_points.txt +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/requires.txt +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/loop_memory.egg-info/top_level.txt +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/setup.cfg +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_admin_ingest_route.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_agent_memory_api.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_agent_memory_sdk.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_auth_token_rotate.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_cli_v7.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_contradictions.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_evolution.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_evolution_quality.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_expanduser.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_export_ask.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_graph.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_ingest.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_llm_consolidator.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_llm_fingerprint_not_persisted.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_llm_providers.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_llm_test_endpoint.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_loop.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_mcp.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_memories_pagination.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_openclaw_loader.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_reflection.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_score_api.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_scoring_v2.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_secrets.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_serve_app.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_serve_handlers.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_session_order.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_store.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_summarization.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_universal_memory.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_vector_store.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_watcher.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_wiki_classifier.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/tests/test_wiki_export_escape.py +0 -0
- {loop_memory-0.4.0 → loop_memory-0.4.1}/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.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -48,11 +48,14 @@ Dynamic: license-file
|
|
|
48
48
|
|
|
49
49
|
# Loop Memory
|
|
50
50
|
|
|
51
|
-
> **A general-purpose memory system for every AI agent you run
|
|
51
|
+
> **A general-purpose, local-first memory system that closes the agent loop for every AI agent you run.**
|
|
52
52
|
>
|
|
53
|
-
>
|
|
54
|
-
>
|
|
55
|
-
>
|
|
53
|
+
> Point Loop Memory at any agent's transcript directory and it
|
|
54
|
+
> quietly catches every conversation, scores it, distils the long
|
|
55
|
+
> tail into a tight wiki of stable knowledge, and re-injects the
|
|
56
|
+
> relevant pieces into the next session. Out of the box: hooks for
|
|
57
|
+
> Codex, Claude, Hermes, and OpenClaw; an SDK + generic watcher
|
|
58
|
+
> CLI cover everything else.
|
|
56
59
|
|
|
57
60
|
[](https://github.com/smartfind/loop-memory/actions)
|
|
58
61
|
[](https://pypi.org/project/loop-memory/)
|
|
@@ -66,11 +69,14 @@ Dynamic: license-file
|
|
|
66
69
|
## What it does
|
|
67
70
|
|
|
68
71
|
**Loop Memory** gives every agent you use a single, persistent brain
|
|
69
|
-
that outlives any one conversation.
|
|
70
|
-
Code, Hermes, OpenClaw / clawx,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
that outlives any one conversation. Any agent that drops transcripts
|
|
73
|
+
on disk — Codex CLI, Claude Code, Hermes, OpenClaw / clawx, Aider,
|
|
74
|
+
Cursor, … — works; the shipped hooks cover the popular ones and a
|
|
75
|
+
generic watcher CLI (`loop-memory hook --source <name> --watch <dir>`)
|
|
76
|
+
accepts anything else. Loop Memory quietly catches every fragment,
|
|
77
|
+
scores it by *importance × recency × usage × feedback*, distils the
|
|
78
|
+
long tail into a curated wiki, and re-injects the relevant pieces
|
|
79
|
+
into the next session.
|
|
74
80
|
|
|
75
81
|
```mermaid
|
|
76
82
|
flowchart LR
|
|
@@ -142,7 +148,7 @@ following — every other project we looked at lacks at least one:
|
|
|
142
148
|
|
|
143
149
|
| Capability | **Loop Memory** | Mem0 v3 | Hindsight | OpenViking | A-MEM |
|
|
144
150
|
| --- | --- | --- | --- | --- | --- |
|
|
145
|
-
| Multi-source capture (Codex / Claude / Hermes / OpenClaw
|
|
151
|
+
| Multi-source capture (any agent with on-disk transcripts) | ✅ generic watcher CLI; hooks shipped for Codex / Claude / Hermes / OpenClaw | ⚠ requires plugin per client | ⚠ hosted only | ⚠ SDK + companion app | ❌ |
|
|
146
152
|
| Local-first SQLite (zero external services) | ✅ | ❌ Postgres + Qdrant | ❌ Postgres + Qdrant | ⚠ file-system + cloud | ⚠ ChromaDB |
|
|
147
153
|
| Hybrid recall: BM25 + semantic + entity (RRF) | ✅ | ✅ | ✅ | ✅ | ⚠ entity-only |
|
|
148
154
|
| Temporal reasoning in retrieval (boost / suppress by date intent) | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
# Loop Memory
|
|
6
6
|
|
|
7
|
-
> **A general-purpose memory system for every AI agent you run
|
|
7
|
+
> **A general-purpose, local-first memory system that closes the agent loop for every AI agent you run.**
|
|
8
8
|
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
9
|
+
> Point Loop Memory at any agent's transcript directory and it
|
|
10
|
+
> quietly catches every conversation, scores it, distils the long
|
|
11
|
+
> tail into a tight wiki of stable knowledge, and re-injects the
|
|
12
|
+
> relevant pieces into the next session. Out of the box: hooks for
|
|
13
|
+
> Codex, Claude, Hermes, and OpenClaw; an SDK + generic watcher
|
|
14
|
+
> CLI cover everything else.
|
|
12
15
|
|
|
13
16
|
[](https://github.com/smartfind/loop-memory/actions)
|
|
14
17
|
[](https://pypi.org/project/loop-memory/)
|
|
@@ -22,11 +25,14 @@
|
|
|
22
25
|
## What it does
|
|
23
26
|
|
|
24
27
|
**Loop Memory** gives every agent you use a single, persistent brain
|
|
25
|
-
that outlives any one conversation.
|
|
26
|
-
Code, Hermes, OpenClaw / clawx,
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
that outlives any one conversation. Any agent that drops transcripts
|
|
29
|
+
on disk — Codex CLI, Claude Code, Hermes, OpenClaw / clawx, Aider,
|
|
30
|
+
Cursor, … — works; the shipped hooks cover the popular ones and a
|
|
31
|
+
generic watcher CLI (`loop-memory hook --source <name> --watch <dir>`)
|
|
32
|
+
accepts anything else. Loop Memory quietly catches every fragment,
|
|
33
|
+
scores it by *importance × recency × usage × feedback*, distils the
|
|
34
|
+
long tail into a curated wiki, and re-injects the relevant pieces
|
|
35
|
+
into the next session.
|
|
30
36
|
|
|
31
37
|
```mermaid
|
|
32
38
|
flowchart LR
|
|
@@ -98,7 +104,7 @@ following — every other project we looked at lacks at least one:
|
|
|
98
104
|
|
|
99
105
|
| Capability | **Loop Memory** | Mem0 v3 | Hindsight | OpenViking | A-MEM |
|
|
100
106
|
| --- | --- | --- | --- | --- | --- |
|
|
101
|
-
| Multi-source capture (Codex / Claude / Hermes / OpenClaw
|
|
107
|
+
| Multi-source capture (any agent with on-disk transcripts) | ✅ generic watcher CLI; hooks shipped for Codex / Claude / Hermes / OpenClaw | ⚠ requires plugin per client | ⚠ hosted only | ⚠ SDK + companion app | ❌ |
|
|
102
108
|
| Local-first SQLite (zero external services) | ✅ | ❌ Postgres + Qdrant | ❌ Postgres + Qdrant | ⚠ file-system + cloud | ⚠ ChromaDB |
|
|
103
109
|
| Hybrid recall: BM25 + semantic + entity (RRF) | ✅ | ✅ | ✅ | ✅ | ⚠ entity-only |
|
|
104
110
|
| Temporal reasoning in retrieval (boost / suppress by date intent) | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""CLI for Loop Memory.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
|
|
5
|
+
loop-memory chat # REPL with echo LLM
|
|
6
|
+
loop-memory stats # counters
|
|
7
|
+
loop-memory recall <text> # show top memories
|
|
8
|
+
loop-memory ingest codex # ingest local Codex transcripts
|
|
9
|
+
loop-memory ingest claude # ingest local Claude Code transcripts
|
|
10
|
+
loop-memory ingest hermes # ingest Hermes transcripts
|
|
11
|
+
loop-memory consolidate # rescore + GC + dedupe
|
|
12
|
+
loop-memory rescore [--half-life 30]
|
|
13
|
+
loop-memory serve [--port 7767] # start the local web UI
|
|
14
|
+
loop-memory mcp # stdio MCP server (for codex/claude/hermes)
|
|
15
|
+
loop-memory inject [query] # dump long-term context block (for SessionStart hooks)
|
|
16
|
+
loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients
|
|
17
|
+
loop-memory consolidate-now # ask the running server to trigger a pass right now
|
|
18
|
+
loop-memory export # legacy markdown export (no positional path)
|
|
19
|
+
loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md (≤ max-chars bytes)
|
|
20
|
+
loop-memory ask "what about…" # print a paste-ready context block for any LLM client
|
|
21
|
+
loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep (v7)
|
|
22
|
+
loop-memory audit [--kind X] [--action Y] # read the cognitive audit trail
|
|
23
|
+
loop-memory export <out_dir> # write a MEMORY.md bundle (v7)
|
|
24
|
+
loop-memory export-bundle <out_dir> # explicit v7 bundle alias
|
|
25
|
+
loop-memory import <in_dir> # re-hydrate a bundle
|
|
26
|
+
loop-memory fork [--branch-tag T] # snapshot every wiki page
|
|
27
|
+
loop-memory graph-edge <src> <dst> [--kind K] [--weight W] # push a relation
|
|
28
|
+
loop-memory subgraph <query> # print a small subgraph
|
|
29
|
+
loop-memory graph-rebuild # rebuild entities + entity_mentions
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import sys
|
|
35
|
+
|
|
36
|
+
from .commands import cognitive as cognitive_cmd
|
|
37
|
+
from .commands import diag as diag_cmd
|
|
38
|
+
from .commands import graph as graph_cmd
|
|
39
|
+
from .commands import hooks as hooks_cmd
|
|
40
|
+
from .commands import read as read_cmd
|
|
41
|
+
from .commands import serve as serve_cmd
|
|
42
|
+
from .commands import write as write_cmd
|
|
43
|
+
|
|
44
|
+
# Backwards-compatible re-exports. Earlier tests imported these
|
|
45
|
+
# private symbols from this module; keep the names available so
|
|
46
|
+
# external code doesn't break after the 0.3.0 split.
|
|
47
|
+
_upsert_block = hooks_cmd._upsert_block
|
|
48
|
+
cmd_inject = read_cmd.run_inject
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _run_export(args: list[str]) -> int:
|
|
52
|
+
"""Keep the legacy markdown export and expose the v7 bundle export.
|
|
53
|
+
|
|
54
|
+
A positional output directory selects the v7 bundle. The historical
|
|
55
|
+
``--out`` / ``--q`` form remains available so existing scripts keep
|
|
56
|
+
producing a single markdown file.
|
|
57
|
+
"""
|
|
58
|
+
if not args or any(flag in args for flag in ("--out", "--q")):
|
|
59
|
+
return read_cmd.run_export(args)
|
|
60
|
+
return cognitive_cmd.run_export(args)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _run_version(_args: list[str]) -> int:
|
|
64
|
+
"""Print the installed distribution version.
|
|
65
|
+
|
|
66
|
+
Resolved through :mod:`importlib.metadata` so the answer comes from
|
|
67
|
+
whatever wheel or sdist the user actually has installed, including
|
|
68
|
+
editable installs (``pip install -e .``). Falls back to ``"unknown"``
|
|
69
|
+
if the package metadata is missing (which would itself indicate a
|
|
70
|
+
broken install).
|
|
71
|
+
"""
|
|
72
|
+
from importlib import metadata as _ilmd
|
|
73
|
+
try:
|
|
74
|
+
ver = _ilmd.version("loop-memory")
|
|
75
|
+
except _ilmd.PackageNotFoundError:
|
|
76
|
+
ver = "unknown"
|
|
77
|
+
print(f"loop-memory {ver}")
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# Dispatch table: command name -> (callable, optional module doc).
|
|
82
|
+
# Each module's ``run_<name>`` function takes ``args: list[str]`` and
|
|
83
|
+
# returns an integer exit code. Keep the keys matching the docstring
|
|
84
|
+
# at the top of this file.
|
|
85
|
+
COMMANDS = {
|
|
86
|
+
"chat": read_cmd.run_chat,
|
|
87
|
+
"stats": read_cmd.run_stats,
|
|
88
|
+
"recall": read_cmd.run_recall,
|
|
89
|
+
"ingest": write_cmd.run_ingest,
|
|
90
|
+
"consolidate": write_cmd.run_consolidate,
|
|
91
|
+
"consolidate-now": write_cmd.run_consolidate_now,
|
|
92
|
+
"export": _run_export,
|
|
93
|
+
"digest": read_cmd.run_digest,
|
|
94
|
+
"ask": read_cmd.run_ask,
|
|
95
|
+
"rescore": write_cmd.run_rescore,
|
|
96
|
+
"serve": serve_cmd.run_serve,
|
|
97
|
+
"hook": serve_cmd.run_hook,
|
|
98
|
+
"mcp": serve_cmd.run_mcp,
|
|
99
|
+
"inject": read_cmd.run_inject,
|
|
100
|
+
"install-hooks": hooks_cmd.run_install_hooks,
|
|
101
|
+
"flush": write_cmd.run_flush,
|
|
102
|
+
"graph": graph_cmd.run_graph,
|
|
103
|
+
"doctor": diag_cmd.run_doctor,
|
|
104
|
+
"status": diag_cmd.run_status,
|
|
105
|
+
"openclaw-setup": diag_cmd.run_openclaw_setup,
|
|
106
|
+
# Universal Agent Memory v7 — graph, cognitive, export, fork
|
|
107
|
+
"cognitive-sleep": cognitive_cmd.run_cognitive_sleep,
|
|
108
|
+
"audit": cognitive_cmd.run_audit,
|
|
109
|
+
"export-bundle": cognitive_cmd.run_export,
|
|
110
|
+
"import": cognitive_cmd.run_import,
|
|
111
|
+
"fork": cognitive_cmd.run_fork,
|
|
112
|
+
"graph-edge": cognitive_cmd.run_graph_edge,
|
|
113
|
+
"subgraph": cognitive_cmd.run_subgraph,
|
|
114
|
+
"graph-rebuild": cognitive_cmd.run_graph_rebuild,
|
|
115
|
+
"wiki-reclassify-legacy": cognitive_cmd.run_wiki_reclassify_legacy,
|
|
116
|
+
"version": _run_version,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Per-command usage lines. The dispatcher prints these on
|
|
121
|
+
# ``<subcommand> --help`` *without* invoking the handler, so missing
|
|
122
|
+
# optional deps (e.g. ``[serve]`` for fastapi) and missing required
|
|
123
|
+
# positional args never crash the help path.
|
|
124
|
+
COMMAND_HELP: dict[str, str] = {
|
|
125
|
+
"ask": "loop-memory ask '<question>' # print a paste-ready context block for any LLM client.",
|
|
126
|
+
"audit": "loop-memory audit [--kind X] [--action Y] # read the cognitive audit trail.",
|
|
127
|
+
"chat": "loop-memory chat # REPL with echo LLM.",
|
|
128
|
+
"cognitive-sleep": "loop-memory cognitive-sleep [--apply] # dry-run / apply cognitive sweep.",
|
|
129
|
+
"consolidate": "loop-memory consolidate # rescore + GC + dedupe.",
|
|
130
|
+
"consolidate-now": "loop-memory consolidate-now # ask the running server to trigger a pass right now.",
|
|
131
|
+
"digest": "loop-memory digest [--out PATH] # compact knowledge digest for AGENTS.md.",
|
|
132
|
+
"doctor": "loop-memory doctor # diagnose local install (db, hooks, optional deps).",
|
|
133
|
+
"export": "loop-memory export [--out FILE | <out_dir>] # markdown dump or v7 bundle export.",
|
|
134
|
+
"export-bundle": "loop-memory export-bundle <out_dir> # explicit v7 bundle alias.",
|
|
135
|
+
"flush": "loop-memory flush # drop volatile caches; keep persisted data.",
|
|
136
|
+
"fork": "loop-memory fork [--branch-tag T] # snapshot every wiki page.",
|
|
137
|
+
"graph": "loop-memory graph # print the entities + relations table.",
|
|
138
|
+
"graph-edge": "loop-memory graph-edge <src> <dst> [--kind K] [--weight W] # push a relation.",
|
|
139
|
+
"graph-rebuild": "loop-memory graph-rebuild # rebuild entities + entity_mentions.",
|
|
140
|
+
"hook": "loop-memory hook --source <codex|claude|hermes|openclaw> --watch <path> [--idle SECONDS]\n Watch a directory for new transcripts and ingest them.",
|
|
141
|
+
"import": "loop-memory import <in_dir> # re-hydrate a v7 bundle.",
|
|
142
|
+
"ingest": "loop-memory ingest {codex|claude|hermes|openclaw}\n Ingest transcripts from a local source into the SQLite store.",
|
|
143
|
+
"inject": "loop-memory inject [query] # dump long-term context block (for SessionStart hooks).",
|
|
144
|
+
"install-hooks": "loop-memory install-hooks # auto-write MCP + SessionStart hooks for known clients.",
|
|
145
|
+
"mcp": "loop-memory mcp # stdio MCP server (for codex/claude/hermes). Requires ``[serve]`` for HTTP transport.",
|
|
146
|
+
"openclaw-setup": "loop-memory openclaw-setup # install the openclaw/clawx auto-ingest watcher (launchd on macOS).",
|
|
147
|
+
"recall": "loop-memory recall <text> # show top memories.",
|
|
148
|
+
"rescore": "loop-memory rescore [--half-life 30] # recompute memory scores.",
|
|
149
|
+
"serve": "loop-memory serve [--port 7767] [--no-browser] # start the local web UI. Requires the ``[serve]`` extra (fastapi + uvicorn).",
|
|
150
|
+
"stats": "loop-memory stats # counters.",
|
|
151
|
+
"status": "loop-memory status # one-shot summary of ingest / recall / serve / hook state.",
|
|
152
|
+
"subgraph": "loop-memory subgraph <query> # print a small subgraph.",
|
|
153
|
+
"version": "loop-memory version # print the installed distribution version.",
|
|
154
|
+
"wiki-reclassify-legacy": "loop-memory wiki-reclassify-legacy # back-fill scope + scope_filter for H4+H5 pages.",
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def main(argv: list | None = None) -> int:
|
|
160
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
161
|
+
if not args or args[0] in {"-h", "--help"}:
|
|
162
|
+
print(__doc__)
|
|
163
|
+
return 0
|
|
164
|
+
if args[0] in {"-V", "--version"}:
|
|
165
|
+
return _run_version(args[1:])
|
|
166
|
+
cmd, rest = args[0], args[1:]
|
|
167
|
+
# Per-subcommand --help / -h: print the static help line and exit 0
|
|
168
|
+
# *without* invoking the handler. Avoids ValueError on ingest (when
|
|
169
|
+
# ``--help`` would be treated as a source name) and ModuleNotFoundError
|
|
170
|
+
# on serve (when [serve] is not installed). Each handler that wants
|
|
171
|
+
# richer help can still implement it locally — our interceptor is a
|
|
172
|
+
# fallback.
|
|
173
|
+
if rest and rest[0] in {"-h", "--help"} and cmd in COMMAND_HELP:
|
|
174
|
+
print(COMMAND_HELP[cmd])
|
|
175
|
+
return 0
|
|
176
|
+
fn = COMMANDS.get(cmd)
|
|
177
|
+
if fn is None:
|
|
178
|
+
print(f"unknown command: {cmd}", file=sys.stderr)
|
|
179
|
+
return 2
|
|
180
|
+
return fn(rest)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
sys.exit(main())
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>循环记忆 · Loop Memory</title>
|
|
7
|
-
<meta name="description" content="Loop Memory — local-first memory loop for Codex
|
|
7
|
+
<meta name="description" content="Loop Memory — local-first memory system that closes the agent loop for every AI agent. Hooks for Codex / Claude / Hermes / OpenClaw; SDK + generic watcher CLI for the rest." />
|
|
8
8
|
<link rel="stylesheet" href="static/css/app.css" />
|
|
9
9
|
<link rel="stylesheet" href="static/css/layout.css" />
|
|
10
10
|
</head>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: loop-memory
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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
|
|
@@ -48,11 +48,14 @@ Dynamic: license-file
|
|
|
48
48
|
|
|
49
49
|
# Loop Memory
|
|
50
50
|
|
|
51
|
-
> **A general-purpose memory system for every AI agent you run
|
|
51
|
+
> **A general-purpose, local-first memory system that closes the agent loop for every AI agent you run.**
|
|
52
52
|
>
|
|
53
|
-
>
|
|
54
|
-
>
|
|
55
|
-
>
|
|
53
|
+
> Point Loop Memory at any agent's transcript directory and it
|
|
54
|
+
> quietly catches every conversation, scores it, distils the long
|
|
55
|
+
> tail into a tight wiki of stable knowledge, and re-injects the
|
|
56
|
+
> relevant pieces into the next session. Out of the box: hooks for
|
|
57
|
+
> Codex, Claude, Hermes, and OpenClaw; an SDK + generic watcher
|
|
58
|
+
> CLI cover everything else.
|
|
56
59
|
|
|
57
60
|
[](https://github.com/smartfind/loop-memory/actions)
|
|
58
61
|
[](https://pypi.org/project/loop-memory/)
|
|
@@ -66,11 +69,14 @@ Dynamic: license-file
|
|
|
66
69
|
## What it does
|
|
67
70
|
|
|
68
71
|
**Loop Memory** gives every agent you use a single, persistent brain
|
|
69
|
-
that outlives any one conversation.
|
|
70
|
-
Code, Hermes, OpenClaw / clawx,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
that outlives any one conversation. Any agent that drops transcripts
|
|
73
|
+
on disk — Codex CLI, Claude Code, Hermes, OpenClaw / clawx, Aider,
|
|
74
|
+
Cursor, … — works; the shipped hooks cover the popular ones and a
|
|
75
|
+
generic watcher CLI (`loop-memory hook --source <name> --watch <dir>`)
|
|
76
|
+
accepts anything else. Loop Memory quietly catches every fragment,
|
|
77
|
+
scores it by *importance × recency × usage × feedback*, distils the
|
|
78
|
+
long tail into a curated wiki, and re-injects the relevant pieces
|
|
79
|
+
into the next session.
|
|
74
80
|
|
|
75
81
|
```mermaid
|
|
76
82
|
flowchart LR
|
|
@@ -142,7 +148,7 @@ following — every other project we looked at lacks at least one:
|
|
|
142
148
|
|
|
143
149
|
| Capability | **Loop Memory** | Mem0 v3 | Hindsight | OpenViking | A-MEM |
|
|
144
150
|
| --- | --- | --- | --- | --- | --- |
|
|
145
|
-
| Multi-source capture (Codex / Claude / Hermes / OpenClaw
|
|
151
|
+
| Multi-source capture (any agent with on-disk transcripts) | ✅ generic watcher CLI; hooks shipped for Codex / Claude / Hermes / OpenClaw | ⚠ requires plugin per client | ⚠ hosted only | ⚠ SDK + companion app | ❌ |
|
|
146
152
|
| Local-first SQLite (zero external services) | ✅ | ❌ Postgres + Qdrant | ❌ Postgres + Qdrant | ⚠ file-system + cloud | ⚠ ChromaDB |
|
|
147
153
|
| Hybrid recall: BM25 + semantic + entity (RRF) | ✅ | ✅ | ✅ | ✅ | ⚠ entity-only |
|
|
148
154
|
| Temporal reasoning in retrieval (boost / suppress by date intent) | ✅ | ✅ | ❌ | ❌ | ❌ |
|
|
@@ -90,6 +90,7 @@ tests/test_agent_memory_api.py
|
|
|
90
90
|
tests/test_agent_memory_sdk.py
|
|
91
91
|
tests/test_auth_token_rotate.py
|
|
92
92
|
tests/test_cli_v7.py
|
|
93
|
+
tests/test_cli_version.py
|
|
93
94
|
tests/test_contradictions.py
|
|
94
95
|
tests/test_evolution.py
|
|
95
96
|
tests/test_evolution_quality.py
|
|
@@ -106,6 +107,7 @@ tests/test_mcp.py
|
|
|
106
107
|
tests/test_memories_pagination.py
|
|
107
108
|
tests/test_openclaw_loader.py
|
|
108
109
|
tests/test_reflection.py
|
|
110
|
+
tests/test_repo_framing.py
|
|
109
111
|
tests/test_score_api.py
|
|
110
112
|
tests/test_scoring_v2.py
|
|
111
113
|
tests/test_secrets.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "loop-memory"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Regression tests for the ``loop-memory --version`` family of flags.
|
|
2
|
+
|
|
3
|
+
Before this fix, ``loop-memory --version`` returned ``unknown command:
|
|
4
|
+
--version`` which tripped users on the most basic operation a CLI
|
|
5
|
+
must support. These tests pin all three forms (``--version``, ``-V``,
|
|
6
|
+
``version``) and assert the printed value matches the installed
|
|
7
|
+
distribution's metadata.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import sys
|
|
13
|
+
import unittest
|
|
14
|
+
from contextlib import redirect_stdout
|
|
15
|
+
from importlib import metadata as importlib_metadata
|
|
16
|
+
|
|
17
|
+
from loop_memory.cli.main import main
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _run(args: list[str]) -> tuple[int, str]:
|
|
21
|
+
"""Run ``loop-memory <args>`` and return ``(rc, stdout)``."""
|
|
22
|
+
buf = io.StringIO()
|
|
23
|
+
with redirect_stdout(buf):
|
|
24
|
+
rc = main(args)
|
|
25
|
+
return rc, buf.getvalue()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CliVersionTests(unittest.TestCase):
|
|
29
|
+
def test_dash_dash_version_prints_distribution_version(self) -> None:
|
|
30
|
+
expected = importlib_metadata.version("loop-memory")
|
|
31
|
+
rc, out = _run(["--version"])
|
|
32
|
+
self.assertEqual(rc, 0, out)
|
|
33
|
+
self.assertIn(expected, out.strip(), out)
|
|
34
|
+
# The output should be human-friendly: at minimum include the package name.
|
|
35
|
+
self.assertIn("loop-memory", out)
|
|
36
|
+
|
|
37
|
+
def test_dash_V_short_flag_also_works(self) -> None:
|
|
38
|
+
rc, out = _run(["-V"])
|
|
39
|
+
self.assertEqual(rc, 0, out)
|
|
40
|
+
self.assertIn(importlib_metadata.version("loop-memory"), out.strip(), out)
|
|
41
|
+
|
|
42
|
+
def test_version_subcommand_also_works(self) -> None:
|
|
43
|
+
rc, out = _run(["version"])
|
|
44
|
+
self.assertEqual(rc, 0, out)
|
|
45
|
+
self.assertIn(importlib_metadata.version("loop-memory"), out.strip(), out)
|
|
46
|
+
|
|
47
|
+
def test_version_does_not_require_a_database(self) -> None:
|
|
48
|
+
"""``version`` must work even when ``$LOOP_MEMORY_DB`` points
|
|
49
|
+
somewhere nonexistent — it is a metadata-only probe.
|
|
50
|
+
"""
|
|
51
|
+
# Save and unset
|
|
52
|
+
prev = sys.modules.pop("loop_memory.storage.sqlite_store", None)
|
|
53
|
+
try:
|
|
54
|
+
rc, out = _run(["version"])
|
|
55
|
+
self.assertEqual(rc, 0, out)
|
|
56
|
+
finally:
|
|
57
|
+
if prev is not None:
|
|
58
|
+
sys.modules["loop_memory.storage.sqlite_store"] = prev
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CliSubcommandHelpTests(unittest.TestCase):
|
|
65
|
+
"""Pin dispatcher behaviour for ``<subcommand> --help`` / ``-h``.
|
|
66
|
+
|
|
67
|
+
The install smoke test caught three real bugs against the
|
|
68
|
+
published 0.4.0 wheel:
|
|
69
|
+
|
|
70
|
+
* ``ingest --help`` crashed with ``ValueError: unknown source:
|
|
71
|
+
'--help'`` because the handler treated ``--help`` as a source name.
|
|
72
|
+
* ``serve --help`` crashed with ``ModuleNotFoundError: No module
|
|
73
|
+
named 'fastapi'`` because the [serve] extra wasn't installed
|
|
74
|
+
(the handler lazy-imports fastapi). Users on a base install
|
|
75
|
+
couldn't read serve's help text.
|
|
76
|
+
* ``hook --help`` returned rc=2 from the ``die()`` helper even
|
|
77
|
+
though it printed the correct usage line.
|
|
78
|
+
|
|
79
|
+
The dispatcher now intercepts ``-h`` / ``--help`` AFTER the
|
|
80
|
+
subcommand name and prints a static help line, never invoking
|
|
81
|
+
the handler — so all three forms exit cleanly with rc=0 even on
|
|
82
|
+
a ``pip install loop-memory`` (no [serve] extra) install.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def _run(self, args: list[str]) -> tuple[int, str]:
|
|
86
|
+
import io
|
|
87
|
+
from contextlib import redirect_stdout
|
|
88
|
+
buf = io.StringIO()
|
|
89
|
+
with redirect_stdout(buf):
|
|
90
|
+
rc = main(args)
|
|
91
|
+
return rc, buf.getvalue()
|
|
92
|
+
|
|
93
|
+
def test_ingest_help_does_not_crash_with_value_error(self) -> None:
|
|
94
|
+
rc, out = self._run(["ingest", "--help"])
|
|
95
|
+
self.assertEqual(rc, 0, out)
|
|
96
|
+
self.assertIn("ingest", out.lower())
|
|
97
|
+
|
|
98
|
+
def test_serve_help_works_without_serve_extra(self) -> None:
|
|
99
|
+
"""Must not require the [serve] extra to be installed."""
|
|
100
|
+
rc, out = self._run(["serve", "--help"])
|
|
101
|
+
self.assertEqual(rc, 0, out)
|
|
102
|
+
self.assertIn("serve", out.lower())
|
|
103
|
+
|
|
104
|
+
def test_hook_help_returns_zero(self) -> None:
|
|
105
|
+
rc, out = self._run(["hook", "--help"])
|
|
106
|
+
self.assertEqual(rc, 0, out)
|
|
107
|
+
self.assertIn("hook", out.lower())
|
|
108
|
+
|
|
109
|
+
def test_short_form_dash_h_also_works(self) -> None:
|
|
110
|
+
rc, _ = self._run(["ingest", "-h"])
|
|
111
|
+
self.assertEqual(rc, 0)
|
|
112
|
+
|
|
113
|
+
def test_unknown_subcommand_still_returns_2(self) -> None:
|
|
114
|
+
"""Make sure the help interceptor doesn't swallow real
|
|
115
|
+
unknown-command errors.
|
|
116
|
+
"""
|
|
117
|
+
rc, _ = self._run(["totally-not-a-command", "--help"])
|
|
118
|
+
self.assertEqual(rc, 2)
|
|
119
|
+
|
|
120
|
+
def test_every_registered_subcommand_has_help_text(self) -> None:
|
|
121
|
+
"""Every COMMANDS entry MUST have a corresponding help string
|
|
122
|
+
so a fresh user can ``--help`` any subcommand without surprises.
|
|
123
|
+
"""
|
|
124
|
+
from loop_memory.cli import main as cli_main
|
|
125
|
+
missing = sorted(c for c in cli_main.COMMANDS if c not in cli_main.COMMAND_HELP)
|
|
126
|
+
self.assertEqual(missing, [], f"missing COMMAND_HELP for: {missing}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
unittest.main()
|