anamnestic 0.2.0__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 (54) hide show
  1. anamnestic-0.2.0/LICENSE +21 -0
  2. anamnestic-0.2.0/PKG-INFO +143 -0
  3. anamnestic-0.2.0/README.md +116 -0
  4. anamnestic-0.2.0/anamnesis/__init__.py +3 -0
  5. anamnestic-0.2.0/anamnesis/audit.py +67 -0
  6. anamnestic-0.2.0/anamnesis/backup.py +61 -0
  7. anamnestic-0.2.0/anamnesis/cli.py +308 -0
  8. anamnestic-0.2.0/anamnesis/config.py +99 -0
  9. anamnestic-0.2.0/anamnesis/daemon/__init__.py +0 -0
  10. anamnestic-0.2.0/anamnesis/daemon/mcp_server.py +790 -0
  11. anamnestic-0.2.0/anamnesis/db.py +119 -0
  12. anamnestic-0.2.0/anamnesis/decay.py +105 -0
  13. anamnestic-0.2.0/anamnesis/entities.py +116 -0
  14. anamnestic-0.2.0/anamnesis/eval/__init__.py +0 -0
  15. anamnestic-0.2.0/anamnesis/eval/run.py +124 -0
  16. anamnestic-0.2.0/anamnesis/graph.py +214 -0
  17. anamnestic-0.2.0/anamnesis/importance.py +101 -0
  18. anamnestic-0.2.0/anamnesis/indexers/__init__.py +0 -0
  19. anamnestic-0.2.0/anamnesis/indexers/incremental_chroma.py +130 -0
  20. anamnestic-0.2.0/anamnesis/ingest/__init__.py +0 -0
  21. anamnestic-0.2.0/anamnesis/ingest/incremental.py +234 -0
  22. anamnestic-0.2.0/anamnesis/ingest/parsers.py +219 -0
  23. anamnestic-0.2.0/anamnesis/ingest/recover_main.py +90 -0
  24. anamnestic-0.2.0/anamnesis/ingest/vscode_copilot.py +140 -0
  25. anamnestic-0.2.0/anamnesis/restore.py +81 -0
  26. anamnestic-0.2.0/anamnesis/search/__init__.py +0 -0
  27. anamnestic-0.2.0/anamnesis/search/hybrid.py +446 -0
  28. anamnestic-0.2.0/anamnesis/search/rerank.py +74 -0
  29. anamnestic-0.2.0/anamnesis/search/temporal.py +208 -0
  30. anamnestic-0.2.0/anamnesis/summarize.py +182 -0
  31. anamnestic-0.2.0/anamnesis/sync/__init__.py +0 -0
  32. anamnestic-0.2.0/anamnesis/sync/cross.py +175 -0
  33. anamnestic-0.2.0/anamnesis/threading.py +166 -0
  34. anamnestic-0.2.0/anamnesis/verify.py +111 -0
  35. anamnestic-0.2.0/anamnestic.egg-info/PKG-INFO +143 -0
  36. anamnestic-0.2.0/anamnestic.egg-info/SOURCES.txt +52 -0
  37. anamnestic-0.2.0/anamnestic.egg-info/dependency_links.txt +1 -0
  38. anamnestic-0.2.0/anamnestic.egg-info/entry_points.txt +2 -0
  39. anamnestic-0.2.0/anamnestic.egg-info/requires.txt +8 -0
  40. anamnestic-0.2.0/anamnestic.egg-info/top_level.txt +1 -0
  41. anamnestic-0.2.0/pyproject.toml +51 -0
  42. anamnestic-0.2.0/setup.cfg +4 -0
  43. anamnestic-0.2.0/tests/test_decay.py +136 -0
  44. anamnestic-0.2.0/tests/test_entities.py +50 -0
  45. anamnestic-0.2.0/tests/test_graph.py +151 -0
  46. anamnestic-0.2.0/tests/test_hybrid.py +85 -0
  47. anamnestic-0.2.0/tests/test_importance.py +119 -0
  48. anamnestic-0.2.0/tests/test_mcp_server.py +159 -0
  49. anamnestic-0.2.0/tests/test_parsers.py +62 -0
  50. anamnestic-0.2.0/tests/test_rerank.py +68 -0
  51. anamnestic-0.2.0/tests/test_search_integration.py +458 -0
  52. anamnestic-0.2.0/tests/test_summarize.py +151 -0
  53. anamnestic-0.2.0/tests/test_temporal.py +148 -0
  54. anamnestic-0.2.0/tests/test_threading.py +116 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xomyachok-shaolin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: anamnestic
3
+ Version: 0.2.0
4
+ Summary: Persistent hybrid-search memory for AI CLI sessions (Claude Code, Codex, any turn-based jsonl)
5
+ Author: xomyachok-shaolin
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/xomyachok-shaolin/anamnestic
8
+ Project-URL: Repository, https://github.com/xomyachok-shaolin/anamnestic
9
+ Project-URL: Issues, https://github.com/xomyachok-shaolin/anamnestic/issues
10
+ Keywords: memory,search,mcp,claude,codex,rag,hybrid-search
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: chromadb>=0.4
20
+ Requires-Dist: fastembed>=0.6
21
+ Requires-Dist: mcp>=1.0
22
+ Requires-Dist: pyyaml>=6.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=7.0; extra == "dev"
25
+ Requires-Dist: pytest-cov; extra == "dev"
26
+ Dynamic: license-file
27
+
28
+ # anamnesis
29
+
30
+ Персистентная память с гибридным поиском для сессий AI-CLI.
31
+
32
+ Собирает исторические транскрипты из **Claude Code** (main + sub-агенты), **Codex CLI** и **VS Code Copilot** в единый корпус. Даёт гибридный поиск (BM25 + семантика + темпоральный + граф сущностей → RRF-слияние) и отдаёт результаты обратно клиентам как MCP-инструменты.
33
+
34
+ Построено как слой-расширение поверх [`claude-mem`](https://github.com/thedotmack/claude-mem): переиспользует его SQLite-файл как базовую схему и добавляет собственные таблицы, индексы и сервисы. Оба сосуществуют, не конфликтуя.
35
+
36
+ ```
37
+ pip install anamnestic
38
+ ```
39
+
40
+ ## Зачем
41
+
42
+ - Транскрипты рабочих сессий с AI-агентами накапливаются между проектами и клиентами. Grep по jsonl — медленно и семантически слепо; сами клиенты всё забывают между запусками.
43
+ - MCP-сервер, который на `mem_search("запрос")` возвращает ранжированные реплики из любой прошлой сессии, превращает архив в адресуемую поверхность знаний.
44
+ - Только BM25 пропускает парафразы. Только семантика не видит точных токенов (IP, CVE, пути). Четырёхканальный RRF даёт каждому каналу шанс вытащить релевантное.
45
+
46
+ ## Архитектура
47
+
48
+ ```
49
+ jsonl-источники (Claude Code / sub-агенты / Codex / VS Code Copilot)
50
+
51
+ │ mtime-сканер, парсер под формат
52
+
53
+ SQLite ── historical_turns (+ FTS5) ◄── BM25
54
+ │ ── session_summaries (+ FTS5) ◄── BM25 (слой наблюдений)
55
+ │ ── anamnesis_entities ◄── поиск по сущностям
56
+ │ ── anamnesis_entity_edges ◄── обход графа
57
+
58
+ │ инкрементальный ONNX-эмбеддер (MiniLM-L12-v2)
59
+
60
+ Chroma (persistent, file-based) ◄── семантика
61
+
62
+ │ 4-канальный Reciprocal Rank Fusion (K=60)
63
+ │ + importance weighting + temporal decay
64
+ │ + cross-encoder reranking
65
+
66
+ stdio MCP-сервер ──► Claude Code / Codex / любой MCP-клиент
67
+ ```
68
+
69
+ ## Поисковый пайплайн
70
+
71
+ Четыре канала извлечения, объединённые через RRF:
72
+
73
+ | Канал | Источник | Что находит |
74
+ |-------|----------|-------------|
75
+ | **BM25** | FTS5 по turns + саммари | Точные токены, пути к файлам, ошибки |
76
+ | **Семантика** | Chroma cosine similarity | Парафразы, концептуально похожий контент |
77
+ | **Темпоральный** | SQL по диапазону дат (EN/RU) | «вчера», «на прошлой неделе», «in March» |
78
+ | **Граф** | BFS по co-occurrence сущностей | Связанные turns через общие пути/URL |
79
+
80
+ Пост-фьюжн этапы:
81
+ - **Importance weighting** — повышает turns с кодом, ошибками, решениями
82
+ - **Temporal decay** — экспоненциальный полураспад (по умолчанию 90 дней), свежие результаты выше
83
+ - **Cross-encoder reranking** — ONNX MiniLM перескорирует top-20 для финальной точности
84
+
85
+ Каждый ответ поиска включает диагностику по каналам.
86
+
87
+ ## Контроль качества графа сущностей
88
+
89
+ - **Минимальный вес ребра** — одноразовые co-occurrence (weight < 2) отсекаются как шум
90
+ - **IDF-нормализация** — `score = weight / log₂(degree + 1)` подавляет сущности-хабы, поднимает редкие дискриминативные
91
+
92
+ ## MCP-инструменты
93
+
94
+ | Инструмент | Назначение |
95
+ |------------|------------|
96
+ | `mem_search` | Гибридный поиск с выбором режима (hybrid/bm25/semantic) |
97
+ | `mem_probe` | Оракул покрытия — «встречается ли этот токен?» |
98
+ | `mem_entity` | Поиск по сущности — «что мы делали с этим файлом?» |
99
+ | `mem_get_turn` | Получить turn с окружающим контекстом |
100
+ | `mem_get_session` | Обзор сессии с метаданными |
101
+ | `mem_get_thread` | Цепочка продолжений — все связанные сессии |
102
+ | `mem_stats` | Статистика корпуса |
103
+ | `mem_audit_tail` | Последние записи телеметрии |
104
+
105
+ ## CLI
106
+
107
+ ```bash
108
+ anamnesis sync # ingest + embed + обогащение (сущности, потоки, importance, саммари, граф)
109
+ anamnesis search "запрос"
110
+ anamnesis status # снимок здоровья корпуса
111
+ anamnesis verify # проверки целостности (FTS, drift, сироты)
112
+ anamnesis backup # WAL-safe tar (хранит последние 10)
113
+ anamnesis restore # восстановление из бэкапа
114
+ anamnesis audit # лог последних операций
115
+ anamnesis eval # регрессионный тест по golden-запросам
116
+ anamnesis archive # архивация старых low-importance turns
117
+ ```
118
+
119
+ ## Установка
120
+
121
+ Полная инструкция — установка, бэкфилл, регистрация MCP, systemd-таймеры, переезд — в **[SETUP.md](SETUP.md)**.
122
+
123
+ ## Принципы дизайна
124
+
125
+ - **Файл — единица идемпотентности.** `anamnesis_ingest_state` хранит `(source, path, mtime_ns)`; повторный запуск пропускает неизменённые файлы.
126
+ - **Turn — единица хранения.** `historical_turns` с UNIQUE-ключом `(content_session_id, turn_number)`; UPSERT не плодит дубликаты.
127
+ - **Формат — ответственность парсера.** Добавить новый CLI-агент = написать парсер в `anamnesis/ingest/` и зарегистрировать glob.
128
+ - **Каждая операция аудируется.** `anamnesis_audit` логирует sync/verify/backup/restore с длительностью и JSON-payload.
129
+ - **Auto-sync при старте MCP.** Лёгкий ingest + embed при запуске сервера — данные всегда актуальны.
130
+
131
+ ## Тесты
132
+
133
+ 93 теста, покрывающие все модули:
134
+ - Интеграционные тесты полного RRF-пайплайна (формула скора, multi-channel merge, importance, decay, граф, диагностика)
135
+ - Unit-тесты importance scoring, temporal parsing, decay, entity extraction, graph traversal, reranking, threading, summarization, parsers, MCP server
136
+
137
+ ```bash
138
+ pytest tests/ -v # <1с
139
+ ```
140
+
141
+ ## Лицензия
142
+
143
+ [MIT](LICENSE)
@@ -0,0 +1,116 @@
1
+ # anamnesis
2
+
3
+ Персистентная память с гибридным поиском для сессий AI-CLI.
4
+
5
+ Собирает исторические транскрипты из **Claude Code** (main + sub-агенты), **Codex CLI** и **VS Code Copilot** в единый корпус. Даёт гибридный поиск (BM25 + семантика + темпоральный + граф сущностей → RRF-слияние) и отдаёт результаты обратно клиентам как MCP-инструменты.
6
+
7
+ Построено как слой-расширение поверх [`claude-mem`](https://github.com/thedotmack/claude-mem): переиспользует его SQLite-файл как базовую схему и добавляет собственные таблицы, индексы и сервисы. Оба сосуществуют, не конфликтуя.
8
+
9
+ ```
10
+ pip install anamnestic
11
+ ```
12
+
13
+ ## Зачем
14
+
15
+ - Транскрипты рабочих сессий с AI-агентами накапливаются между проектами и клиентами. Grep по jsonl — медленно и семантически слепо; сами клиенты всё забывают между запусками.
16
+ - MCP-сервер, который на `mem_search("запрос")` возвращает ранжированные реплики из любой прошлой сессии, превращает архив в адресуемую поверхность знаний.
17
+ - Только BM25 пропускает парафразы. Только семантика не видит точных токенов (IP, CVE, пути). Четырёхканальный RRF даёт каждому каналу шанс вытащить релевантное.
18
+
19
+ ## Архитектура
20
+
21
+ ```
22
+ jsonl-источники (Claude Code / sub-агенты / Codex / VS Code Copilot)
23
+
24
+ │ mtime-сканер, парсер под формат
25
+
26
+ SQLite ── historical_turns (+ FTS5) ◄── BM25
27
+ │ ── session_summaries (+ FTS5) ◄── BM25 (слой наблюдений)
28
+ │ ── anamnesis_entities ◄── поиск по сущностям
29
+ │ ── anamnesis_entity_edges ◄── обход графа
30
+
31
+ │ инкрементальный ONNX-эмбеддер (MiniLM-L12-v2)
32
+
33
+ Chroma (persistent, file-based) ◄── семантика
34
+
35
+ │ 4-канальный Reciprocal Rank Fusion (K=60)
36
+ │ + importance weighting + temporal decay
37
+ │ + cross-encoder reranking
38
+
39
+ stdio MCP-сервер ──► Claude Code / Codex / любой MCP-клиент
40
+ ```
41
+
42
+ ## Поисковый пайплайн
43
+
44
+ Четыре канала извлечения, объединённые через RRF:
45
+
46
+ | Канал | Источник | Что находит |
47
+ |-------|----------|-------------|
48
+ | **BM25** | FTS5 по turns + саммари | Точные токены, пути к файлам, ошибки |
49
+ | **Семантика** | Chroma cosine similarity | Парафразы, концептуально похожий контент |
50
+ | **Темпоральный** | SQL по диапазону дат (EN/RU) | «вчера», «на прошлой неделе», «in March» |
51
+ | **Граф** | BFS по co-occurrence сущностей | Связанные turns через общие пути/URL |
52
+
53
+ Пост-фьюжн этапы:
54
+ - **Importance weighting** — повышает turns с кодом, ошибками, решениями
55
+ - **Temporal decay** — экспоненциальный полураспад (по умолчанию 90 дней), свежие результаты выше
56
+ - **Cross-encoder reranking** — ONNX MiniLM перескорирует top-20 для финальной точности
57
+
58
+ Каждый ответ поиска включает диагностику по каналам.
59
+
60
+ ## Контроль качества графа сущностей
61
+
62
+ - **Минимальный вес ребра** — одноразовые co-occurrence (weight < 2) отсекаются как шум
63
+ - **IDF-нормализация** — `score = weight / log₂(degree + 1)` подавляет сущности-хабы, поднимает редкие дискриминативные
64
+
65
+ ## MCP-инструменты
66
+
67
+ | Инструмент | Назначение |
68
+ |------------|------------|
69
+ | `mem_search` | Гибридный поиск с выбором режима (hybrid/bm25/semantic) |
70
+ | `mem_probe` | Оракул покрытия — «встречается ли этот токен?» |
71
+ | `mem_entity` | Поиск по сущности — «что мы делали с этим файлом?» |
72
+ | `mem_get_turn` | Получить turn с окружающим контекстом |
73
+ | `mem_get_session` | Обзор сессии с метаданными |
74
+ | `mem_get_thread` | Цепочка продолжений — все связанные сессии |
75
+ | `mem_stats` | Статистика корпуса |
76
+ | `mem_audit_tail` | Последние записи телеметрии |
77
+
78
+ ## CLI
79
+
80
+ ```bash
81
+ anamnesis sync # ingest + embed + обогащение (сущности, потоки, importance, саммари, граф)
82
+ anamnesis search "запрос"
83
+ anamnesis status # снимок здоровья корпуса
84
+ anamnesis verify # проверки целостности (FTS, drift, сироты)
85
+ anamnesis backup # WAL-safe tar (хранит последние 10)
86
+ anamnesis restore # восстановление из бэкапа
87
+ anamnesis audit # лог последних операций
88
+ anamnesis eval # регрессионный тест по golden-запросам
89
+ anamnesis archive # архивация старых low-importance turns
90
+ ```
91
+
92
+ ## Установка
93
+
94
+ Полная инструкция — установка, бэкфилл, регистрация MCP, systemd-таймеры, переезд — в **[SETUP.md](SETUP.md)**.
95
+
96
+ ## Принципы дизайна
97
+
98
+ - **Файл — единица идемпотентности.** `anamnesis_ingest_state` хранит `(source, path, mtime_ns)`; повторный запуск пропускает неизменённые файлы.
99
+ - **Turn — единица хранения.** `historical_turns` с UNIQUE-ключом `(content_session_id, turn_number)`; UPSERT не плодит дубликаты.
100
+ - **Формат — ответственность парсера.** Добавить новый CLI-агент = написать парсер в `anamnesis/ingest/` и зарегистрировать glob.
101
+ - **Каждая операция аудируется.** `anamnesis_audit` логирует sync/verify/backup/restore с длительностью и JSON-payload.
102
+ - **Auto-sync при старте MCP.** Лёгкий ingest + embed при запуске сервера — данные всегда актуальны.
103
+
104
+ ## Тесты
105
+
106
+ 93 теста, покрывающие все модули:
107
+ - Интеграционные тесты полного RRF-пайплайна (формула скора, multi-channel merge, importance, decay, граф, диагностика)
108
+ - Unit-тесты importance scoring, temporal parsing, decay, entity extraction, graph traversal, reranking, threading, summarization, parsers, MCP server
109
+
110
+ ```bash
111
+ pytest tests/ -v # <1с
112
+ ```
113
+
114
+ ## Лицензия
115
+
116
+ [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ """Anamnesis — persistent memory for AI CLI sessions."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,67 @@
1
+ """Audit log + health snapshot helpers."""
2
+ import json
3
+ import time
4
+ from contextlib import contextmanager
5
+
6
+ from anamnesis.config import HEALTH_FILE
7
+ from anamnesis.db import connect
8
+
9
+
10
+ def write_audit(action: str, status: str, duration_sec: float | None, details: dict):
11
+ conn = connect()
12
+ try:
13
+ conn.execute(
14
+ "INSERT INTO anamnesis_audit(action, status, duration_sec, details) "
15
+ "VALUES (?, ?, ?, ?)",
16
+ (action, status, duration_sec, json.dumps(details, ensure_ascii=False)),
17
+ )
18
+ conn.commit()
19
+ finally:
20
+ conn.close()
21
+
22
+
23
+ def write_health(snapshot: dict):
24
+ snapshot = dict(snapshot)
25
+ snapshot.setdefault("written_at", time.strftime("%Y-%m-%dT%H:%M:%S"))
26
+ with open(HEALTH_FILE, "w") as f:
27
+ json.dump(snapshot, f, ensure_ascii=False, indent=2)
28
+
29
+
30
+ @contextmanager
31
+ def audited(action: str):
32
+ """Context manager: log start, duration, status=ok|error; re-raise."""
33
+ t0 = time.time()
34
+ details = {}
35
+ try:
36
+ yield details
37
+ except Exception as e:
38
+ dt = round(time.time() - t0, 2)
39
+ details["error"] = f"{type(e).__name__}: {e}"
40
+ write_audit(action, "error", dt, details)
41
+ raise
42
+ else:
43
+ dt = round(time.time() - t0, 2)
44
+ status = details.pop("_status", "ok")
45
+ write_audit(action, status, dt, details)
46
+
47
+
48
+ def recent(limit: int = 20) -> list[dict]:
49
+ conn = connect()
50
+ try:
51
+ rows = conn.execute(
52
+ "SELECT at, action, status, duration_sec, details "
53
+ "FROM anamnesis_audit ORDER BY id DESC LIMIT ?",
54
+ (limit,),
55
+ ).fetchall()
56
+ return [
57
+ {
58
+ "at": r["at"],
59
+ "action": r["action"],
60
+ "status": r["status"],
61
+ "duration_sec": r["duration_sec"],
62
+ "details": json.loads(r["details"]) if r["details"] else {},
63
+ }
64
+ for r in rows
65
+ ]
66
+ finally:
67
+ conn.close()
@@ -0,0 +1,61 @@
1
+ """Safe backup of SQLite + Chroma to a dated tarball.
2
+
3
+ Uses SQLite .backup API (WAL-safe, online copy). Tars Chroma directory.
4
+ Output: ~/claude-mem-backups/claude-mem-YYYYMMDD-HHMMSS.tar.gz
5
+ """
6
+ import os
7
+ import shutil
8
+ import sqlite3
9
+ import tarfile
10
+ import time
11
+ from pathlib import Path
12
+
13
+ from anamnesis.config import DB_PATH, CHROMA_DIR, BACKUP_ROOT, BACKUP_KEEP_LAST
14
+
15
+ KEEP_LAST = BACKUP_KEEP_LAST
16
+
17
+
18
+ def _safe_sqlite_copy(dst):
19
+ """Online, consistent copy of WAL-mode SQLite."""
20
+ src = sqlite3.connect(f"file:{DB_PATH}?mode=ro", uri=True)
21
+ dst_conn = sqlite3.connect(dst)
22
+ with dst_conn:
23
+ src.backup(dst_conn)
24
+ dst_conn.close()
25
+ src.close()
26
+
27
+
28
+ def run():
29
+ os.makedirs(BACKUP_ROOT, exist_ok=True)
30
+ stamp = time.strftime("%Y%m%d-%H%M%S")
31
+ work = Path(BACKUP_ROOT) / f"work-{stamp}"
32
+ work.mkdir(parents=True, exist_ok=True)
33
+
34
+ db_copy = work / "claude-mem.db"
35
+ chroma_copy = work / "semantic-chroma"
36
+
37
+ _safe_sqlite_copy(str(db_copy))
38
+ if os.path.isdir(CHROMA_DIR):
39
+ shutil.copytree(CHROMA_DIR, chroma_copy)
40
+ else:
41
+ chroma_copy.mkdir(parents=True, exist_ok=True)
42
+
43
+ archive = Path(BACKUP_ROOT) / f"claude-mem-{stamp}.tar.gz"
44
+ with tarfile.open(archive, "w:gz") as tf:
45
+ tf.add(db_copy, arcname="claude-mem.db")
46
+ tf.add(chroma_copy, arcname="semantic-chroma")
47
+ shutil.rmtree(work)
48
+
49
+ # retention
50
+ archives = sorted(Path(BACKUP_ROOT).glob("claude-mem-*.tar.gz"))
51
+ if len(archives) > KEEP_LAST:
52
+ for old in archives[:-KEEP_LAST]:
53
+ old.unlink()
54
+
55
+ size_mb = archive.stat().st_size / (1024 * 1024)
56
+ return {"path": str(archive), "size_mb": round(size_mb, 1)}
57
+
58
+
59
+ if __name__ == "__main__":
60
+ info = run()
61
+ print(f"Backup: {info['path']} ({info['size_mb']} MB)")