cluxion-Agentplugin-AutoClearMemory 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,145 @@
1
+ Metadata-Version: 2.4
2
+ Name: cluxion-Agentplugin-AutoClearMemory
3
+ Version: 0.2.0
4
+ Summary: Universal agent memory plugin (ForgetForge): recall-centric retention, tiered forgetting, Rust scoring engine for Hermes, Claude Code, and Codex.
5
+ Project-URL: Homepage, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
6
+ Project-URL: Repository, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
7
+ Project-URL: Issues, https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory/issues
8
+ Author-email: cluxion <algocean1204@users.noreply.github.com>
9
+ License-Expression: Apache-2.0
10
+ Keywords: claude-code,cluxion,codex,forgetforge,hermes-agent,memory,plugin,recall
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Plugins
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: Apache Software License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Requires-Python: >=3.11
20
+ Requires-Dist: pyarrow>=15.0
21
+ Requires-Dist: pyyaml>=6.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: ruff>=0.8; extra == 'dev'
26
+ Requires-Dist: twine>=6.0; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # cluxion-Agentplugin-AutoClearMemory (ForgetForge)
30
+
31
+ 범용 에이전트 **망각·기억 강화 플러그인** — **Hermes, Claude Code, Codex, Grok Build**에서 동일 core로 동작합니다.
32
+
33
+ **Repository:** https://github.com/cluxion/cluxion-Agentplugin-AutoClearMemory
34
+
35
+ ## 한 줄 요약
36
+
37
+ 세션 메모리가 쌓일수록 컨텍스트가 비대해집니다. ForgetForge는 **회상(Recall) 횟수**로 기억 강도를 계산하고, 오래되거나 쓰이지 않은 기억을 자동으로 낮은 tier로 내립니다. **연결된 AI**가 skill·도구 지시에 따라 recall/status/keep/forget을 호출합니다.
38
+
39
+ ## 범용 에이전트 + Rust-First
40
+
41
+ | 계층 | 구현 |
42
+ |------|------|
43
+ | **Rust** (`forgetforge-engine`) | Retention scoring, tier decision |
44
+ | **Python** (`forgetforge`) | SQLite, recall tracker, pruner, CLI |
45
+ | **Agent adapter** | Hermes plugin, Claude skill, Codex CLI 가이드 |
46
+
47
+ 내부 로직은 **Rust 중심**. Python·Skill은 등록·DB·JSON bridge **thin wrapper**입니다.
48
+ Rust 바이너리가 없어도 **Python fallback**으로 동일 공식이 동작합니다.
49
+
50
+ ## 이 플러그인의 역할
51
+
52
+ - **Recall이 reinforcement**: 저장 횟수가 아니라 실제 회상 \(N_r\)가 강도를 결정
53
+ - **Tiered Memory**: Hot / Warm-Episodic / Warm-Semantic / Warm-Procedural / Cold
54
+ - **Decay + Boost**: Ebbinghaus forgetting curve + spaced repetition
55
+ - **FTS5 recall**: SQLite full-text search + LIKE fallback
56
+ - **Brief handoff**: preprocessing/supercoder brief → episodic memory (`import-brief`)
57
+ - **Hot inject**: Hermes `pre_llm_call` hook으로 hot tier 자동 주입
58
+ - **Contradiction hints**: store 시 유사·부정 기억 경고
59
+ - **Parquet archive**: cold tier → `~/.forgetforge/archive/` (parquet + jsonl + txt)
60
+ - **Background pruner**: 6시간 주기 tier migration (`forgetforge prune`)
61
+ - **Safety**: `#keep_forever`, `#forget_this` 사용자 태그
62
+
63
+ ForgetForge는 **모델·OAuth를 소유하지 않습니다.** 연결된 AI가 도구 결과를 읽고 맥락에 반영합니다.
64
+
65
+ ## 연결된 AI가 하는 일
66
+
67
+ | 상황 | 연결된 AI 동작 |
68
+ |------|----------------|
69
+ | 맥락이 커짐 | `forgetforge status` 또는 `forgetforge_status` |
70
+ | 과거 사실 필요 | `forgetforge recall <topic>` (explicit) |
71
+ | 응답에 기억 사용 | `forgetforge_recall` + `layer: implicit` |
72
+ | 세션 마무리 점검 | `forgetforge_recall` + `layer: reflection` |
73
+ | `#keep_forever` | `forgetforge keep <id>` |
74
+ | `#forget_this` | `forgetforge forget <id>` |
75
+
76
+ Hermes는 `forgetforge_*` 도구, Claude/Codex는 skill + CLI를 동일 규칙으로 따릅니다.
77
+
78
+ ## Retention formula
79
+
80
+ \[
81
+ R = e^{-t / S} \times \left(1 + 0.45 \cdot N_r + 0.30 \cdot I + 0.25 \cdot F \right), \quad S = \ln(1 + N_r)
82
+ \]
83
+
84
+ ## Tier 요약
85
+
86
+ | Tier | 조건 | 연결된 AI 동작 |
87
+ |------|------|----------------|
88
+ | **Hot** | 최근 7일 + \(N_r \geq 1\) | recall 결과를 맥락에 우선 반영 |
89
+ | **Warm-Episodic** | \(R \geq 0.65\) | 필요 시 recall |
90
+ | **Warm-Semantic** | \(R \geq 0.80\) | 장기 사실로 유지 |
91
+ | **Warm-Procedural** | skill + \(N_r \geq 3\) | 절차·스킬로 유지 |
92
+ | **Cold** | \(R < 0.40\) or 180일 무회상 | archive, 필요 시에만 recall |
93
+
94
+ ## 빠른 시작
95
+
96
+ ```bash
97
+ pip install cluxion-Agentplugin-AutoClearMemory
98
+ forgetforge init --agents=all
99
+ forgetforge check
100
+ hermes plugins enable forgetforge # Hermes
101
+ ```
102
+
103
+ ```bash
104
+ # Rust 가속 (선택)
105
+ cargo build --release --manifest-path rust/forgetforge_engine/Cargo.toml
106
+ ```
107
+
108
+ ## 공통 명령
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `forgetforge store <id> --content "..."` | Store or update memory |
113
+ | `forgetforge recall <topic>` | Explicit retrieval + tier boost |
114
+ | `forgetforge keep <id>` | `#keep_forever` |
115
+ | `forgetforge forget <id>` | `#forget_this` |
116
+ | `forgetforge status` | Memory health |
117
+ | `forgetforge prune` | Pruner 1회 실행 |
118
+ | `forgetforge pruner-daemon` | Background pruner (interval from config) |
119
+ | `forgetforge import-brief --source supercoder --brief "..."` | Brief → episodic memory |
120
+ | `forgetforge hot-context` | Hot tier context block (CLI) |
121
+
122
+ ## Hermes 도구 (`forgetforge` toolset)
123
+
124
+ | Tool | Description |
125
+ |------|-------------|
126
+ | `forgetforge_store` | Save or update memory (contradiction warnings when similar) |
127
+ | `forgetforge_recall` | FTS search + retrieval event (`layer`: explicit / implicit / reflection) |
128
+ | `forgetforge_status` | Tier counts, engine status |
129
+ | `forgetforge_keep` | Pin memory forever |
130
+ | `forgetforge_forget` | Mark for forgetting |
131
+ | `forgetforge_import_brief` | Import preprocessing/supercoder brief |
132
+ | `forgetforge_hot_context` | Hot tier block (also via `pre_llm_call` hook) |
133
+
134
+ ## 문서
135
+
136
+ - [Docs/README.md](Docs/README.md) — **처음 읽는 분** + 목차
137
+ - [Docs/architecture.md](Docs/architecture.md)
138
+ - [Docs/design.md](Docs/design.md)
139
+ - [Docs/installation.md](Docs/installation.md)
140
+ - [Docs/agent-surfaces.md](Docs/agent-surfaces.md)
141
+ - [Docs/rust-architecture.md](Docs/rust-architecture.md)
142
+
143
+ ## License
144
+
145
+ Apache-2.0
@@ -0,0 +1,19 @@
1
+ forgetforge/__init__.py,sha256=Wkwg9PUkaGjHLMEaYqFYYRLoVSlb80kQ1nRzGUSsuSg,134
2
+ forgetforge/archive.py,sha256=Qt4pM1mGPAzy0RVd90P_SmuSviCWhlWP5A5wKSnJOxk,1482
3
+ forgetforge/cli.py,sha256=R8kU8zUbbK5DqPPAycjTqgLVrMdiB81kkX4hnIsJ68k,7898
4
+ forgetforge/config.py,sha256=1FRtN9xdLaA989tNmBvfoATzb2nsrEKmraavxJaQHpk,1571
5
+ forgetforge/contradiction.py,sha256=f0S0C3zBQIPyPK2FJkleujKsXt_BXsPXUe4NK2t3WyA,2187
6
+ forgetforge/db.py,sha256=U4Cf1Tj3pAj9ofqg0yM-40279iP06T-8MdOEcpWQsiQ,9574
7
+ forgetforge/hot_inject.py,sha256=rH_tUL9LCaBWYiap2Vi8_fA-dFpAFTpqQR3XFN9vmQA,843
8
+ forgetforge/import_brief.py,sha256=QAp-usJ1Ih2HWfSrGDjaw2gfpa4_VRFeOrSk_SWAj_E,1318
9
+ forgetforge/pruner.py,sha256=aUW48-inawfOG8ljNRrfI_GrNlyRq5A-_CB1s3Kl-x4,2589
10
+ forgetforge/recall.py,sha256=5_DLGNMBmvwNFPqGdjZI1cl3T0H-2gpXY_O5E4PPfnY,3754
11
+ forgetforge/rust_bridge.py,sha256=rnsYMk95bXynzXjNr-_Ar8PYEgWd37WlwgnaR4lzRbA,4161
12
+ forgetforge/schemas.py,sha256=d23VRrG3iqPaog3igcdkaugulZHKqU0CmyyXIB6C4jc,2236
13
+ forgetforge/store.py,sha256=pTApLBXk-zN2gLccUqWb-Qfms6MURFgqOLvxQaop9uo,2687
14
+ forgetforge/adapters/__init__.py,sha256=OcCz6Ib0sIILnkPL4xqNEU0TldjdQIi1NOxiGx6UeuU,70
15
+ forgetforge/adapters/hermes.py,sha256=5W_sqEy3uGdoT5i2FZayGFqfwGXtlvBuljhAMISCa4A,5170
16
+ cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/METADATA,sha256=xMpKzjiwZRScCvzDbcHao-DBwFb3xAgLFK5NFAmhONM,6401
17
+ cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
18
+ cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/entry_points.txt,sha256=144bKiDTBBCqpsiev-IoSdW5xmdZPLgWbvgT8K9NW30,128
19
+ cluxion_agentplugin_autoclearmemory-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,5 @@
1
+ [console_scripts]
2
+ forgetforge = forgetforge.cli:main
3
+
4
+ [hermes_agent.plugins]
5
+ forgetforge = forgetforge.adapters.hermes:register
@@ -0,0 +1,5 @@
1
+ """ForgetForge — recall-centric memory plugin for universal agent environments."""
2
+
3
+ __version__ = "0.2.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ """Agent surface adapters for ForgetForge."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+
6
+ from forgetforge import db, hot_inject, import_brief
7
+ from forgetforge.config import load_config
8
+ from forgetforge.schemas import (
9
+ FORGET_SCHEMA,
10
+ HOT_CONTEXT_SCHEMA,
11
+ IMPORT_BRIEF_SCHEMA,
12
+ KEEP_SCHEMA,
13
+ RECALL_SCHEMA,
14
+ STATUS_SCHEMA,
15
+ STORE_SCHEMA,
16
+ )
17
+ from forgetforge import store as store_api
18
+
19
+
20
+ def register(ctx: object) -> None:
21
+ ctx.register_tool(
22
+ name="forgetforge_store",
23
+ toolset="forgetforge",
24
+ schema=STORE_SCHEMA,
25
+ handler=_wrap(_handle_store),
26
+ emoji="💾",
27
+ )
28
+ ctx.register_tool(
29
+ name="forgetforge_recall",
30
+ toolset="forgetforge",
31
+ schema=RECALL_SCHEMA,
32
+ handler=_wrap(_handle_recall),
33
+ emoji="🧠",
34
+ )
35
+ ctx.register_tool(
36
+ name="forgetforge_status",
37
+ toolset="forgetforge",
38
+ schema=STATUS_SCHEMA,
39
+ handler=_wrap(_handle_status),
40
+ emoji="📊",
41
+ )
42
+ ctx.register_tool(
43
+ name="forgetforge_keep",
44
+ toolset="forgetforge",
45
+ schema=KEEP_SCHEMA,
46
+ handler=_wrap(_handle_keep),
47
+ emoji="📌",
48
+ )
49
+ ctx.register_tool(
50
+ name="forgetforge_forget",
51
+ toolset="forgetforge",
52
+ schema=FORGET_SCHEMA,
53
+ handler=_wrap(_handle_forget),
54
+ emoji="🗑️",
55
+ )
56
+ ctx.register_tool(
57
+ name="forgetforge_import_brief",
58
+ toolset="forgetforge",
59
+ schema=IMPORT_BRIEF_SCHEMA,
60
+ handler=_wrap(_handle_import_brief),
61
+ emoji="📥",
62
+ )
63
+ ctx.register_tool(
64
+ name="forgetforge_hot_context",
65
+ toolset="forgetforge",
66
+ schema=HOT_CONTEXT_SCHEMA,
67
+ handler=_wrap(_handle_hot_context),
68
+ emoji="🔥",
69
+ )
70
+ register_hook = getattr(ctx, "register_hook", None)
71
+ if callable(register_hook):
72
+ register_hook("pre_llm_call", _pre_llm_hot_inject)
73
+
74
+
75
+ def _pre_llm_hot_inject(**_: object) -> dict[str, str]:
76
+ return hot_inject.hot_context_payload()
77
+
78
+
79
+ def _wrap(callback: Callable[[dict[str, object]], dict[str, object]]) -> Callable[[dict[str, object]], str]:
80
+ def handler(args: dict[str, object], **_: object) -> str:
81
+ try:
82
+ return json.dumps(callback(args), ensure_ascii=False, sort_keys=True)
83
+ except (ValueError, OSError) as exc:
84
+ return json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False, sort_keys=True)
85
+
86
+ return handler
87
+
88
+
89
+ def _conn():
90
+ cfg = load_config()
91
+ return db.connect(cfg.db_path)
92
+
93
+
94
+ def _handle_store(args: dict[str, object]) -> dict[str, object]:
95
+ conn = _conn()
96
+ try:
97
+ stored = store_api.store_memory(
98
+ conn,
99
+ memory_id=str(args.get("memory_id", "")),
100
+ content=str(args.get("content", "")),
101
+ importance=float(args.get("importance", 0.5)),
102
+ frequency=float(args.get("frequency", 0.0)),
103
+ is_procedural=bool(args.get("is_procedural", False)),
104
+ )
105
+ return {"ok": True, "stored": stored}
106
+ finally:
107
+ conn.close()
108
+
109
+
110
+ def _handle_recall(args: dict[str, object]) -> dict[str, object]:
111
+ query = str(args.get("query", "")).strip()
112
+ layer = str(args.get("layer", "explicit"))
113
+ if not query:
114
+ raise ValueError("query is required")
115
+ conn = _conn()
116
+ try:
117
+ return store_api.recall_with_feedback(conn, query, layer=layer)
118
+ finally:
119
+ conn.close()
120
+
121
+
122
+ def _handle_status(_: dict[str, object]) -> dict[str, object]:
123
+ from forgetforge import rust_bridge
124
+
125
+ conn = _conn()
126
+ try:
127
+ return {"ok": True, "stats": db.memory_stats(conn), "rust_engine": rust_bridge.engine_available()}
128
+ finally:
129
+ conn.close()
130
+
131
+
132
+ def _handle_keep(args: dict[str, object]) -> dict[str, object]:
133
+ memory_id = str(args.get("memory_id", "")).strip()
134
+ if not memory_id:
135
+ raise ValueError("memory_id is required")
136
+ conn = _conn()
137
+ try:
138
+ ok = db.mark_keep_forever(conn, memory_id)
139
+ return {"ok": ok, "memory_id": memory_id}
140
+ finally:
141
+ conn.close()
142
+
143
+
144
+ def _handle_forget(args: dict[str, object]) -> dict[str, object]:
145
+ memory_id = str(args.get("memory_id", "")).strip()
146
+ if not memory_id:
147
+ raise ValueError("memory_id is required")
148
+ conn = _conn()
149
+ try:
150
+ ok = db.mark_forget(conn, memory_id)
151
+ return {"ok": ok, "memory_id": memory_id}
152
+ finally:
153
+ conn.close()
154
+
155
+
156
+ def _handle_import_brief(args: dict[str, object]) -> dict[str, object]:
157
+ conn = _conn()
158
+ try:
159
+ return import_brief.import_brief(
160
+ conn,
161
+ source=str(args.get("source", "manual")),
162
+ brief=str(args.get("brief", "")),
163
+ memory_id=str(args.get("memory_id", "")).strip() or None,
164
+ importance=float(args.get("importance", 0.65)),
165
+ )
166
+ finally:
167
+ conn.close()
168
+
169
+
170
+ def _handle_hot_context(args: dict[str, object]) -> dict[str, object]:
171
+ limit = int(args.get("limit", 8))
172
+ conn = _conn()
173
+ try:
174
+ context = hot_inject.build_hot_context(conn, limit=limit)
175
+ return {"ok": True, "context": context, "has_hot": bool(context)}
176
+ finally:
177
+ conn.close()
178
+
179
+
180
+ __all__ = ["register"]
forgetforge/archive.py ADDED
@@ -0,0 +1,47 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from datetime import UTC, datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from forgetforge.config import ForgetForgeConfig
9
+
10
+
11
+ def write_cold_archive(
12
+ cfg: ForgetForgeConfig,
13
+ *,
14
+ memory_id: str,
15
+ content: str,
16
+ retention: float,
17
+ tier: str,
18
+ ) -> dict[str, Any]:
19
+ cfg.archive_dir.mkdir(parents=True, exist_ok=True)
20
+ summary = content.strip().splitlines()[0][:500] if content.strip() else ""
21
+ record = {
22
+ "memory_id": memory_id,
23
+ "retention": retention,
24
+ "tier": tier,
25
+ "summary": summary,
26
+ "archived_at": datetime.now(UTC).replace(microsecond=0).isoformat(),
27
+ }
28
+ parquet_path = cfg.archive_dir / f"{memory_id}.parquet"
29
+ jsonl_path = cfg.archive_dir / "cold_archive.jsonl"
30
+ written = "jsonl"
31
+ try:
32
+ import pyarrow as pa
33
+ import pyarrow.parquet as pq
34
+
35
+ table = pa.Table.from_pylist([record])
36
+ pq.write_table(table, parquet_path)
37
+ written = "parquet"
38
+ except ImportError:
39
+ parquet_path = None
40
+ with jsonl_path.open("a", encoding="utf-8") as handle:
41
+ handle.write(json.dumps(record, ensure_ascii=False) + "\n")
42
+ txt_path = cfg.archive_dir / f"{memory_id}.txt"
43
+ txt_path.write_text(f"# retention={retention:.3f}\n{summary}\n", encoding="utf-8")
44
+ return {"format": written, "parquet": str(parquet_path) if parquet_path else None, "jsonl": str(jsonl_path)}
45
+
46
+
47
+ __all__ = ["write_cold_archive"]
forgetforge/cli.py ADDED
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import shutil
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import TYPE_CHECKING
9
+
10
+ from forgetforge import __version__, db, hot_inject, import_brief, pruner, rust_bridge, store
11
+ from forgetforge.config import default_home, load_config
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Sequence
15
+
16
+
17
+ def main(argv: Sequence[str] | None = None) -> int:
18
+ parser = _parser()
19
+ args = parser.parse_args(argv)
20
+ if args.command == "check":
21
+ return _check()
22
+ if args.command == "init":
23
+ return _init(args)
24
+ if args.command == "status":
25
+ return _status()
26
+ if args.command == "recall":
27
+ return _recall(args)
28
+ if args.command == "keep":
29
+ return _keep(args)
30
+ if args.command == "forget":
31
+ return _forget(args)
32
+ if args.command == "prune":
33
+ return _prune()
34
+ if args.command == "store":
35
+ return _store(args)
36
+ if args.command == "pruner-daemon":
37
+ return _pruner_daemon(args)
38
+ if args.command == "import-brief":
39
+ return _import_brief(args)
40
+ if args.command == "hot-context":
41
+ return _hot_context(args)
42
+ parser.print_help(sys.stderr)
43
+ return 2
44
+
45
+
46
+ def _parser() -> argparse.ArgumentParser:
47
+ parser = argparse.ArgumentParser(prog="forgetforge")
48
+ parser.add_argument("--version", action="version", version=f"forgetforge {__version__}")
49
+ sub = parser.add_subparsers(dest="command")
50
+ sub.add_parser("check", help="Check Rust engine and database paths")
51
+ init = sub.add_parser("init", help="Initialize ~/.forgetforge and optional agent adapters")
52
+ init.add_argument("--agents", default="all", help="Comma-separated: hermes,claude,codex or all")
53
+ sub.add_parser("status", help="Memory health summary")
54
+ recall_cmd = sub.add_parser("recall", help="Recall memories matching a query")
55
+ recall_cmd.add_argument("query")
56
+ keep = sub.add_parser("keep", help="Mark memory as keep_forever")
57
+ keep.add_argument("memory_id")
58
+ forget = sub.add_parser("forget", help="Mark memory for forgetting")
59
+ forget.add_argument("memory_id")
60
+ sub.add_parser("prune", help="Run background pruner once")
61
+ store_cmd = sub.add_parser("store", help="Store or update a memory")
62
+ store_cmd.add_argument("memory_id")
63
+ store_cmd.add_argument("--content", required=True)
64
+ store_cmd.add_argument("--importance", type=float, default=0.5)
65
+ store_cmd.add_argument("--frequency", type=float, default=0.0)
66
+ store_cmd.add_argument("--procedural", action="store_true")
67
+ daemon = sub.add_parser("pruner-daemon", help="Run pruner on interval (background)")
68
+ daemon.add_argument("--interval-hours", type=int, default=None)
69
+ daemon.add_argument("--once", action="store_true", help="Run one cycle then exit")
70
+ brief = sub.add_parser("import-brief", help="Import preprocessing/supercoder brief into memory")
71
+ brief.add_argument("--source", choices=["preprocessing", "supercoder", "manual"], required=True)
72
+ brief.add_argument("--brief", required=True)
73
+ brief.add_argument("--memory-id", default=None)
74
+ brief.add_argument("--importance", type=float, default=0.65)
75
+ hot = sub.add_parser("hot-context", help="Print hot-tier context block")
76
+ hot.add_argument("--limit", type=int, default=8)
77
+ return parser
78
+
79
+
80
+ def _check() -> int:
81
+ home = default_home()
82
+ payload = {
83
+ "ok": True,
84
+ "home": str(home),
85
+ "rust_engine": rust_bridge.engine_available(),
86
+ "db_exists": (home / "db.sqlite").exists(),
87
+ }
88
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
89
+ return 0
90
+
91
+
92
+ def _init(args: argparse.Namespace) -> int:
93
+ cfg = load_config()
94
+ cfg.home.mkdir(parents=True, exist_ok=True)
95
+ cfg.archive_dir.mkdir(parents=True, exist_ok=True)
96
+ conn = db.connect(cfg.db_path)
97
+ conn.close()
98
+ agents = _parse_agents(str(args.agents))
99
+ adapter_root = Path(__file__).resolve().parents[2] / "adapters"
100
+ installed = []
101
+ for agent in agents:
102
+ src = adapter_root / agent
103
+ if src.exists():
104
+ installed.append(agent)
105
+ example = Path(__file__).resolve().parents[2] / "config.yaml.example"
106
+ target = cfg.home / "config.yaml"
107
+ if example.exists() and not target.exists():
108
+ shutil.copy(example, target)
109
+ print(
110
+ json.dumps(
111
+ {
112
+ "ok": True,
113
+ "home": str(cfg.home),
114
+ "db": str(cfg.db_path),
115
+ "agents": installed,
116
+ "config": str(target),
117
+ },
118
+ ensure_ascii=False,
119
+ indent=2,
120
+ )
121
+ )
122
+ return 0
123
+
124
+
125
+ def _status() -> int:
126
+ from forgetforge import recall
127
+
128
+ cfg = load_config()
129
+ conn = db.connect(cfg.db_path)
130
+ stats = db.memory_stats(conn)
131
+ hot = [recall.score_memory(row) for row in db.list_memories(conn, limit=50) if row.tier == "hot"]
132
+ conn.close()
133
+ print(
134
+ json.dumps(
135
+ {
136
+ "ok": True,
137
+ "rust_engine": rust_bridge.engine_available(),
138
+ "stats": stats,
139
+ "hot_samples": hot[:5],
140
+ },
141
+ ensure_ascii=False,
142
+ indent=2,
143
+ )
144
+ )
145
+ return 0
146
+
147
+
148
+ def _recall(args: argparse.Namespace) -> int:
149
+ cfg = load_config()
150
+ conn = db.connect(cfg.db_path)
151
+ payload = store.recall_with_feedback(conn, str(args.query))
152
+ conn.close()
153
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
154
+ return 0
155
+
156
+
157
+ def _store(args: argparse.Namespace) -> int:
158
+ cfg = load_config()
159
+ conn = db.connect(cfg.db_path)
160
+ stored = store.store_memory(
161
+ conn,
162
+ memory_id=str(args.memory_id),
163
+ content=str(args.content),
164
+ importance=float(args.importance),
165
+ frequency=float(args.frequency),
166
+ is_procedural=bool(args.procedural),
167
+ )
168
+ conn.close()
169
+ print(json.dumps({"ok": True, "stored": stored}, ensure_ascii=False, indent=2))
170
+ return 0
171
+
172
+
173
+ def _pruner_daemon(args: argparse.Namespace) -> int:
174
+ pruner.run_pruner_daemon(interval_hours=args.interval_hours, run_once=bool(args.once))
175
+ return 0
176
+
177
+
178
+ def _import_brief(args: argparse.Namespace) -> int:
179
+ cfg = load_config()
180
+ conn = db.connect(cfg.db_path)
181
+ result = import_brief.import_brief(
182
+ conn,
183
+ source=str(args.source),
184
+ brief=str(args.brief),
185
+ memory_id=str(args.memory_id) if args.memory_id else None,
186
+ importance=float(args.importance),
187
+ )
188
+ conn.close()
189
+ print(json.dumps(result, ensure_ascii=False, indent=2))
190
+ return 0
191
+
192
+
193
+ def _hot_context(args: argparse.Namespace) -> int:
194
+ cfg = load_config()
195
+ conn = db.connect(cfg.db_path)
196
+ context = hot_inject.build_hot_context(conn, limit=int(args.limit))
197
+ conn.close()
198
+ print(json.dumps({"ok": True, "context": context, "has_hot": bool(context)}, ensure_ascii=False, indent=2))
199
+ return 0
200
+
201
+
202
+ def _keep(args: argparse.Namespace) -> int:
203
+ cfg = load_config()
204
+ conn = db.connect(cfg.db_path)
205
+ ok = db.mark_keep_forever(conn, str(args.memory_id))
206
+ conn.close()
207
+ print(json.dumps({"ok": ok, "memory_id": args.memory_id}, ensure_ascii=False))
208
+ return 0 if ok else 1
209
+
210
+
211
+ def _forget(args: argparse.Namespace) -> int:
212
+ cfg = load_config()
213
+ conn = db.connect(cfg.db_path)
214
+ ok = db.mark_forget(conn, str(args.memory_id))
215
+ conn.close()
216
+ print(json.dumps({"ok": ok, "memory_id": args.memory_id}, ensure_ascii=False))
217
+ return 0 if ok else 1
218
+
219
+
220
+ def _prune() -> int:
221
+ cfg = load_config()
222
+ conn = db.connect(cfg.db_path)
223
+ result = pruner.run_pruner(conn, config=cfg)
224
+ conn.close()
225
+ print(json.dumps(result, ensure_ascii=False, indent=2))
226
+ return 0
227
+
228
+
229
+ def _parse_agents(raw: str) -> list[str]:
230
+ if raw.strip().lower() == "all":
231
+ return ["hermes", "claude", "codex"]
232
+ return [part.strip().lower() for part in raw.split(",") if part.strip()]
233
+
234
+
235
+ if __name__ == "__main__":
236
+ raise SystemExit(main())