@nogataka/claw-memory 0.1.3 → 0.3.0

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.
@@ -1,16 +1,18 @@
1
1
  {
2
2
  "name": "claw-memory-marketplace",
3
- "owner": { "name": "nogataka" },
3
+ "owner": {
4
+ "name": "nogataka"
5
+ },
4
6
  "metadata": {
5
7
  "description": "claw-memory: local semantic memory + raw-log search for Claude Code",
6
- "version": "0.1.3"
8
+ "version": "0.3.0"
7
9
  },
8
10
  "plugins": [
9
11
  {
10
12
  "name": "claw-memory",
11
13
  "source": "./",
12
14
  "description": "Local semantic memory: auto-distill, recall injection, and raw Claude Code + Codex transcript search.",
13
- "version": "0.1.3"
15
+ "version": "0.3.0"
14
16
  }
15
17
  ]
16
18
  }
@@ -1,9 +1,17 @@
1
1
  {
2
2
  "name": "claw-memory",
3
- "version": "0.1.3",
3
+ "version": "0.3.0",
4
4
  "description": "Local semantic memory for Claude Code: auto-distills sessions, injects relevant past context, and searches raw Claude Code + Codex transcripts. sqlite-vec + local embeddings, no daemon, no Python.",
5
- "author": { "name": "nogataka" },
5
+ "author": {
6
+ "name": "nogataka"
7
+ },
6
8
  "repository": "https://github.com/nogataka/claw-memory",
7
9
  "license": "MIT",
8
- "keywords": ["memory", "mcp", "context", "search", "codex"]
10
+ "keywords": [
11
+ "memory",
12
+ "mcp",
13
+ "context",
14
+ "search",
15
+ "codex"
16
+ ]
9
17
  }
package/README.ja.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  [English](README.md) | **日本語**
4
4
 
5
+ ![claw-memory メモリビューア](docs/screenshot.png)
6
+
5
7
  **AI コーディングエージェント(Claude Code / Codex)のためのローカル完結型・長期記憶。**
6
8
  エージェントが過去のセッション・あなたの好み・以前の決定事項を記憶し、記録済みの全
7
9
  トランスクリプトを横断検索できます。デーモン不要、Python 不要、外部ベクトル DB 不要。
@@ -12,7 +14,7 @@
12
14
  npm install -g @nogataka/claw-memory
13
15
  ```
14
16
 
15
- - **ストレージ**: `better-sqlite3` + `sqlite-vec` — ベクトルを 1 つの SQLite ファイル内に格納
17
+ - **ストレージ**: `node:sqlite`(Node >= 24 組み込み・ネイティブ ABI なし)+ `sqlite-vec` — ベクトルを 1 つの SQLite ファイル内に格納
16
18
  - **埋め込み**: ローカルの `Xenova/multilingual-e5-small`(384次元・多言語・オフライン)
17
19
  - **2 つの記憶ソース**: 蒸留済みのセマンティック DB と、Claude Code / Codex の生ログ全文検索
18
20
  - **自動取り込み**: フックで終了済みセッションを蒸留し、新しいセッションへ関連記憶を注入
@@ -42,11 +44,16 @@ npm install -g @nogataka/claw-memory
42
44
  | ソース | 内容 | ツール |
43
45
  |--------|------|--------|
44
46
  | **蒸留 DB** | LLM がセッションを要約 → 要約・好み・構造化メタデータ付きの埋め込みチャンク。セマンティック検索可能。 | `memory_recall`, `memory_search`, `memory_get` |
45
- | **生ログ検索** | 実際の Claude Code(`~/.claude/projects`)/ Codex(`~/.codex/sessions`)ログを全文検索。蒸留していないセッションも対象。 | `memory_search_logs` |
47
+ | **生ログ検索** | 実際の Claude Code(`~/.claude/projects`)/ Codex(`~/.codex/sessions`)/ **ChatGPT web** エクスポート(`conversations.json`)ログを全文検索。蒸留していないセッションも対象。 | `memory_search_logs` |
46
48
 
47
49
  蒸留 DB は整理済みで高速に想起でき、生ログ検索は claw-memory 導入前を含め「いつか話した
48
50
  こと」を必ず見つけられるセーフティネットです。
49
51
 
52
+ **ChatGPT web** の会話は OpenAI サーバー側にありローカルに残らないため、公式エクスポートから読み込みます。
53
+ ChatGPT → Settings → **Data controls** → **Export data** でダウンロードし、解凍して `conversations.json` を
54
+ `~/.claw-memory/chatgpt/` に置く(または `CLAW_MEMORY_CHATGPT_EXPORT` でファイル/フォルダを指定)だけで、
55
+ 他ログと同様に検索でき、`claw-memory distill-chatgpt` で専用 `chatgpt` プロジェクトとして意味 DB にも取り込めます。
56
+
50
57
  ### 2. 自動取り込み(distill)
51
58
 
52
59
  セッション終了時、トランスクリプトを次の形に蒸留します。
@@ -94,7 +101,19 @@ npm install -g @nogataka/claw-memory
94
101
 
95
102
  ビルド不要・読み取り専用のビューア(`claw-memory ui`)。プロジェクト・要約・チャンク
96
103
  (メタデータ付き)・好みを閲覧でき、生ログ検索も実行できます。SSE で開いている間は自動
97
- 更新。起動した時だけ動きます。
104
+ 更新。起動した時だけ動きます。抽出されたレッスンをレビュー・承認・編集できる **Lessons**
105
+ タブも備えています。
106
+
107
+ ### 8. 再利用可能なレッスン
108
+
109
+ 過去ログをそのまま検索するだけでなく、claw-memory は AI コーディングセッションを
110
+ **再利用可能なレッスン**へと蒸留します。プロジェクト固有の制約、デバッグパターン、設計
111
+ 判断、ユーザーの開発方針などの、実行可能で抽象化された知識です。レッスンは通常の要約と
112
+ 同時に抽出され(追加の LLM 呼び出しなし)、ローカルに保存され、同じローカルモデルで埋め込
113
+ まれ、(承認後のみ)類似タスク時に想起されます。各レッスンは `scope`・`confidence`・
114
+ `applies_when` / `avoid_when` と、ライフサイクル(candidate → approved → archived /
115
+ superseded)を持ち、重複・矛盾検出と時間経過による confidence 減衰を備えます。raw ログは
116
+ 根拠として保持しつつ、通常の想起では簡潔で再利用可能なレッスンを優先します。
98
117
 
99
118
  ---
100
119
 
@@ -125,7 +144,8 @@ npm install -g @nogataka/claw-memory
125
144
  Claude Code を再起動してください。次が自動登録されます。
126
145
 
127
146
  - **MCP サーバー**(8 つの記憶ツール)
128
- - **フック**: `SessionStart` / `UserPromptSubmit` recall 注入、`Stop` → 自動 distill
147
+ - **フック**: `SessionStart` コンパクトな recall 注入(Pull 型)、`Stop` → 自動 distill
148
+ フックのエラーは握り潰さず `~/.claw-memory/logs/hook-error.log` に記録されます
129
149
 
130
150
  手動設定は不要です。確認は `/mcp` で `claw-memory` が見えるかどうかで行えます。
131
151
 
@@ -146,7 +166,7 @@ codex
146
166
  プラグインは Codex が互換提供する `${CLAUDE_PLUGIN_ROOT}` を介して次を登録します。
147
167
 
148
168
  - `claw-memory` MCP サーバー(`.mcp.json`)
149
- - `SessionStart` / `UserPromptSubmit` → **recall 自動注入**(memory ブロックを developer context として)
169
+ - `SessionStart` **コンパクトな recall 自動注入**(設定+1行要約。詳細は `memory_recall` でオンデマンド取得)
150
170
  - `Stop` → 最近の Codex セッションの**自動 distill**(watermark 重複回避・非同期)
151
171
  - `memory-recall` skill
152
172
 
@@ -178,7 +198,7 @@ claw-memory distill-codex --all # 全件バックフィル
178
198
  ```bash
179
199
  git clone https://github.com/nogataka/claw-memory
180
200
  cd claw-memory
181
- npm install # ネイティブの better-sqlite3 / sqlite-vec をビルド
201
+ npm install # ネイティブビルドなし(ストレージは Node 組み込みの node:sqlite
182
202
  npm run build # tsc -> dist/
183
203
  npm link # 任意: `claw-memory` バイナリを公開
184
204
  ```
@@ -195,11 +215,17 @@ npm link # 任意: `claw-memory` バイナリを公開
195
215
  | `memory_remember(text, cwd?, sessionId?)` | フリーテキストのメモを永続保存。 |
196
216
  | `memory_distill(cwd, sessionId? \| transcriptPath?)` | セッションを要約して記憶化(LLM バックエンドが必要)。 |
197
217
  | `memory_get_preferences(cwd?)` | プロジェクトの保存済み好みを一覧。 |
198
- | `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Claude Code + Codex の生ログを全文検索。 |
218
+ | `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Claude Code + Codex + ChatGPT-web の生ログを全文検索(`sources`: `claude-code` / `codex` / `chatgpt-web`)。 |
199
219
  | `memory_forget(ids)` | チャンクをソフト削除(検索 / recall / ビューアから除外)。 |
220
+ | `lesson_search(query, cwd?, limit?)` | 承認済みレッスンを関連度 + scope + confidence で検索。 |
221
+ | `lesson_inject(query, cwd?, limit?)` | 同上を `<relevant-lessons>` ブロックとして返す。 |
222
+ | `lesson_get(lesson_id)` | レッスン1件の詳細(全フィールド + 履歴 + リンク)。 |
223
+ | `lesson_extract(cwd, sessionId? \| transcriptPath?)` | セッションから専用のレッスン抽出(LLM 必要)。 |
224
+ | `lesson_approve / lesson_reject / lesson_archive(lesson_id, reason?)` | ステータス遷移。 |
225
+ | `lesson_supersede(old_lesson_id, new_lesson_id)` | 古いレッスンを新しいもので置き換え。 |
200
226
 
201
- `memory_distill`(LLM)と `memory_search_logs`(`~/.claude/projects`・`~/.codex/sessions`
202
- を直接読む)以外は完全ローカルで動作します。
227
+ `memory_distill` / `lesson_extract`(LLM)と `memory_search_logs`
228
+ (`~/.claude/projects`・`~/.codex/sessions` を直接読む)以外は完全ローカルで動作します。
203
229
 
204
230
  ---
205
231
 
@@ -218,6 +244,12 @@ npm link # 任意: `claw-memory` バイナリを公開
218
244
  | `CLAW_MEMORY_EXCLUDED_PROJECTS` | — | 記録・想起しないパス部分文字列(カンマ/コロン区切り)。 |
219
245
  | `MEMORY_SIMILARITY_MAX_DISTANCE` | `0.6` | セマンティックヒットの最大コサイン距離(小さいほど厳格)。 |
220
246
  | `CLAW_MEMORY_UI_PORT` | `4319` | ビューアのポート。 |
247
+ | `LESSON_RECALL_LIMIT` | `3` | recall ブロックに注入する承認済みレッスン数(`0` で無効)。 |
248
+ | `CLAW_MEMORY_LESSON_DEDICATED` | — | `1` で専用の高品質レッスン抽出パスを実行(LLM 追加呼び出し)。 |
249
+ | `CLAW_MEMORY_LESSON_CONFLICT_LLM` | — | `1` で抽出時に LLM による矛盾検出を有効化。 |
250
+ | `LESSON_DECAY_FACTOR` / `LESSON_STALE_DAYS` | `0.9` / `30` | `lessons decay` の減衰係数と陳腐化しきい値。 |
251
+ | `CLAW_MEMORY_CHATGPT_EXPORT` | `~/.claw-memory/chatgpt` | ChatGPT `conversations.json` エクスポート(ファイル or `*.json` を含むフォルダ)。 |
252
+ | `CLAW_MEMORY_CHATGPT_MAX_BYTES` | `209715200`(200 MB) | これを超える ChatGPT エクスポートはスキップ(メモリ内パースのため)。 |
221
253
 
222
254
  ### LLM バックエンド
223
255
 
@@ -241,8 +273,18 @@ claw-memory mcp # stdio MCP サーバー(エ
241
273
  claw-memory ui [--port N] [--open] # 読み取り専用 Web ビューア
242
274
  claw-memory distill --cwd P --session ID [--path FILE] [--if-stale]
243
275
  claw-memory distill-codex [--recent] [--limit N] [--all]
276
+ claw-memory distill-chatgpt [--limit N] [--all] # ChatGPT web エクスポートの会話を蒸留
244
277
  claw-memory remember --cwd P "メモ"
245
- claw-memory search-logs "クエリ" [--source claude-code,codex] [--project P]
278
+ claw-memory lessons list [--status candidate|approved|...] [--cwd P]
279
+ claw-memory lessons search "クエリ" [--cwd P] [--limit N]
280
+ claw-memory lessons inject "クエリ" [--cwd P] [--limit N]
281
+ claw-memory lessons extract --session ID [--cwd P] [--path FILE]
282
+ claw-memory lessons approve|reject|archive <lesson_id> [--reason R]
283
+ claw-memory lessons supersede <old_id> <new_id>
284
+ claw-memory lessons decay [--days N] [--factor F] [--dry]
285
+ claw-memory lessons export [--status S] [--cwd P] > bundle.json
286
+ claw-memory lessons import bundle.json [--status S] [--cwd P]
287
+ claw-memory search-logs "クエリ" [--source claude-code,codex,chatgpt-web] [--project P]
246
288
  [--start ISO] [--end ISO] [--limit N] [--offset N]
247
289
  claw-memory hook <recall|distill> # ライフサイクルフック(JSON を stdin で受領)
248
290
  claw-memory install [--codex | --claude-code] # MCP + フックを登録(既定: codex)
@@ -311,7 +353,9 @@ npm uninstall -g @nogataka/claw-memory
311
353
 
312
354
  ## 補足
313
355
 
314
- - `better-sqlite3` / `sqlite-vec` はネイティブモジュールです。Node ABI 変更後は `npm rebuild` を実行してください。
356
+ - ストレージは Node 組み込みの `node:sqlite` を使用します(Node >= 24 必須)。Node を更新してもネイティブ ABI 不一致で壊れることはありません。`sqlite-vec` 拡張はプリビルト配布で ABI 非依存です。
357
+ - v0.3.0 で毎プロンプト注入を廃止しました(コンテキスト圧迫対策)。フックは `SessionStart` にコンパクトなブロックを1回注入するのみで、詳細は `memory_recall` / `memory_search`(MCP)または `memory-recall` スキルで明示的に取得します。
358
+ - `claw-memory cleanse` で、生 JSON が混入した過去チャンクを検出(`--apply` でソフトデリート)できます。
315
359
  - MCP サーバーはエージェントセッションごとに常駐するため、埋め込みモデルは 1 回だけロードされます。
316
360
  - ビューアと MCP は同時実行できます(SQLite WAL が並行読み書きを処理)。
317
361
  - インストール時、依存は `legacy-peer-deps=true` で解決されます(同梱 SDK 間の zod の
package/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  **English** | [日本語](README.ja.md)
4
4
 
5
+ ![claw-memory memory viewer](docs/screenshot.png)
6
+
5
7
  **Local, in-process long-term memory for AI coding agents (Claude Code & Codex).**
6
8
  Your agent remembers past sessions, your preferences, and prior decisions — and can
7
9
  search every raw transcript you've ever recorded. No daemon, no Python, no external
@@ -12,7 +14,7 @@ session, which you control).
12
14
  npm install -g @nogataka/claw-memory
13
15
  ```
14
16
 
15
- - **Storage**: `better-sqlite3` + `sqlite-vec` — vectors live *inside* one SQLite file
17
+ - **Storage**: `node:sqlite` (built into Node >= 24, no native ABI) + `sqlite-vec` — vectors live *inside* one SQLite file
16
18
  - **Embeddings**: local `Xenova/multilingual-e5-small` (384-dim, multilingual, offline)
17
19
  - **Two memory sources**: a distilled semantic DB **and** full-text search over raw
18
20
  Claude Code + Codex transcripts
@@ -44,11 +46,18 @@ npm install -g @nogataka/claw-memory
44
46
  | Source | What it is | Tooling |
45
47
  |--------|-----------|---------|
46
48
  | **Distilled DB** | LLM-summarized sessions → summaries, preferences, and embedded conversation chunks with structured metadata. Semantically searchable. | `memory_recall`, `memory_search`, `memory_get` |
47
- | **Raw transcript search** | Full-text grep over your *actual* Claude Code (`~/.claude/projects`) and Codex (`~/.codex/sessions`) logs — including sessions that were never distilled. | `memory_search_logs` |
49
+ | **Raw transcript search** | Full-text grep over your *actual* Claude Code (`~/.claude/projects`), Codex (`~/.codex/sessions`), and **ChatGPT web** export (`conversations.json`) logs — including sessions that were never distilled. | `memory_search_logs` |
48
50
 
49
51
  The distilled DB is curated and fast to recall; raw search is a safety net that finds
50
52
  anything you ever discussed, even before claw-memory was installed.
51
53
 
54
+ **ChatGPT web** conversations live on OpenAI's servers, not on disk, so claw-memory
55
+ reads them from the official export: ChatGPT → Settings → **Data controls** → **Export
56
+ data**; unzip and drop `conversations.json` into `~/.claw-memory/chatgpt/` (or point
57
+ `CLAW_MEMORY_CHATGPT_EXPORT` at the file/folder). Then it's searchable like any other
58
+ log, and `claw-memory distill-chatgpt` adds it to the semantic DB under a dedicated
59
+ `chatgpt` project.
60
+
52
61
  ### 2. Automatic capture (distill)
53
62
 
54
63
  When a session ends, claw-memory distills the transcript into:
@@ -99,7 +108,21 @@ handle the high-frequency distill work.
99
108
 
100
109
  A zero-build, read-only viewer (`claw-memory ui`) to browse projects, summaries,
101
110
  chunks (with their metadata), preferences, and to run raw-log search — with live
102
- updates via SSE. It runs only when you start it.
111
+ updates via SSE. It runs only when you start it. The viewer also has a **Lessons**
112
+ tab for reviewing, approving and editing extracted lessons.
113
+
114
+ ### 8. Reusable lessons
115
+
116
+ Beyond retrieving past logs, claw-memory distills AI coding sessions into
117
+ **reusable lessons** — actionable, abstracted knowledge such as project-specific
118
+ constraints, debugging patterns, design decisions and user preferences. Lessons
119
+ are extracted alongside the normal summary (no extra LLM call), stored locally,
120
+ embedded with the same local model, and surfaced (only after approval) when a
121
+ similar task appears later. Each lesson carries `scope`, `confidence`,
122
+ `applies_when` / `avoid_when` and a lifecycle (candidate → approved → archived /
123
+ superseded), with duplicate / conflict detection and confidence decay over time.
124
+ Raw logs remain available as evidence; day-to-day recall focuses on concise,
125
+ reusable lessons.
103
126
 
104
127
  ---
105
128
 
@@ -130,7 +153,8 @@ plugin falls back to `npx -y @nogataka/claw-memory@latest`, which is slower on f
130
153
  Restart Claude Code. This auto-registers:
131
154
 
132
155
  - the **MCP server** (8 memory tools), and
133
- - the **hooks**: `SessionStart` / `UserPromptSubmit` recall injection, `Stop` → auto-distill.
156
+ - the **hooks**: `SessionStart` compact recall injection (pull model), `Stop` → auto-distill.
157
+ Hook errors are logged to `~/.claw-memory/logs/hook-error.log` instead of being swallowed.
134
158
 
135
159
  No manual config. To verify, run `/mcp` and look for `claw-memory`.
136
160
 
@@ -151,7 +175,7 @@ codex
151
175
  The plugin registers, via Codex's `${CLAUDE_PLUGIN_ROOT}` (provided for compatibility):
152
176
 
153
177
  - the `claw-memory` MCP server (`.mcp.json`),
154
- - `SessionStart` / `UserPromptSubmit` → **auto recall injection** (memory block as developer context),
178
+ - `SessionStart` → **compact recall injection** (preferences + one-line summaries; details are pulled on demand via `memory_recall`),
155
179
  - `Stop` → **auto distill** of recent Codex sessions (watermark-deduped, async), and
156
180
  - the `memory-recall` skill.
157
181
 
@@ -183,7 +207,7 @@ claw-memory distill-codex --all # backfill everything
183
207
  ```bash
184
208
  git clone https://github.com/nogataka/claw-memory
185
209
  cd claw-memory
186
- npm install # builds native better-sqlite3 / sqlite-vec
210
+ npm install # no native builds — storage is Node's built-in node:sqlite
187
211
  npm run build # tsc -> dist/
188
212
  npm link # optional: expose the `claw-memory` binary
189
213
  ```
@@ -200,11 +224,18 @@ npm link # optional: expose the `claw-memory` binary
200
224
  | `memory_remember(text, cwd?, sessionId?)` | Store a durable free-text note. |
201
225
  | `memory_distill(cwd, sessionId? \| transcriptPath?)` | Summarize a session into memory (needs an LLM backend). |
202
226
  | `memory_get_preferences(cwd?)` | List stored preferences for the project. |
203
- | `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Full-text search over RAW Claude Code + Codex transcripts. |
227
+ | `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Full-text search over RAW Claude Code + Codex + ChatGPT-web transcripts (`sources`: `claude-code` / `codex` / `chatgpt-web`). |
204
228
  | `memory_forget(ids)` | Soft-delete chunks (hidden from search / recall / viewer). |
229
+ | `lesson_search(query, cwd?, limit?)` | Search approved reusable lessons, ranked by relevance + scope + confidence. |
230
+ | `lesson_inject(query, cwd?, limit?)` | Same, returned as a ready-to-read `<relevant-lessons>` block. |
231
+ | `lesson_get(lesson_id)` | Full detail of one lesson (fields + history + links). |
232
+ | `lesson_extract(cwd, sessionId? \| transcriptPath?)` | Dedicated lesson-extraction pass over a session (needs an LLM backend). |
233
+ | `lesson_approve / lesson_reject / lesson_archive(lesson_id, reason?)` | Lifecycle transitions. |
234
+ | `lesson_supersede(old_lesson_id, new_lesson_id)` | Replace an old lesson with a newer one. |
205
235
 
206
- All tools are fully local except `memory_distill` (LLM) and `memory_search_logs`
207
- (reads `~/.claude/projects` and `~/.codex/sessions` directly).
236
+ All tools are fully local except `memory_distill` / `lesson_extract` (LLM) and
237
+ `memory_search_logs` (reads `~/.claude/projects` and `~/.codex/sessions`
238
+ directly).
208
239
 
209
240
  ---
210
241
 
@@ -223,6 +254,12 @@ All tools are fully local except `memory_distill` (LLM) and `memory_search_logs`
223
254
  | `CLAW_MEMORY_EXCLUDED_PROJECTS` | — | Comma/colon-separated path substrings to never record or recall. |
224
255
  | `MEMORY_SIMILARITY_MAX_DISTANCE` | `0.6` | Max cosine distance for a semantic hit (lower = stricter). |
225
256
  | `CLAW_MEMORY_UI_PORT` | `4319` | Viewer port. |
257
+ | `LESSON_RECALL_LIMIT` | `3` | Approved lessons injected into the recall block (`0` disables). |
258
+ | `CLAW_MEMORY_LESSON_DEDICATED` | — | `1` = run a separate, higher-quality lesson-extraction pass (extra LLM call). |
259
+ | `CLAW_MEMORY_LESSON_CONFLICT_LLM` | — | `1` = use the LLM to detect conflicting lessons during extraction. |
260
+ | `LESSON_DECAY_FACTOR` / `LESSON_STALE_DAYS` | `0.9` / `30` | Confidence-decay factor and staleness threshold for `lessons decay`. |
261
+ | `CLAW_MEMORY_CHATGPT_EXPORT` | `~/.claw-memory/chatgpt` | ChatGPT `conversations.json` export — a file or a folder of `*.json`. |
262
+ | `CLAW_MEMORY_CHATGPT_MAX_BYTES` | `209715200` (200 MB) | Skip ChatGPT export files larger than this (parsed in memory). |
226
263
 
227
264
  ### LLM backends
228
265
 
@@ -246,8 +283,18 @@ claw-memory mcp # stdio MCP server (what agents
246
283
  claw-memory ui [--port N] [--open] # read-only web viewer
247
284
  claw-memory distill --cwd P --session ID [--path FILE] [--if-stale]
248
285
  claw-memory distill-codex [--recent] [--limit N] [--all]
286
+ claw-memory distill-chatgpt [--limit N] [--all] # distill ChatGPT web export conversations
249
287
  claw-memory remember --cwd P "a note"
250
- claw-memory search-logs "query" [--source claude-code,codex] [--project P]
288
+ claw-memory lessons list [--status candidate|approved|...] [--cwd P]
289
+ claw-memory lessons search "query" [--cwd P] [--limit N]
290
+ claw-memory lessons inject "query" [--cwd P] [--limit N]
291
+ claw-memory lessons extract --session ID [--cwd P] [--path FILE]
292
+ claw-memory lessons approve|reject|archive <lesson_id> [--reason R]
293
+ claw-memory lessons supersede <old_id> <new_id>
294
+ claw-memory lessons decay [--days N] [--factor F] [--dry]
295
+ claw-memory lessons export [--status S] [--cwd P] > bundle.json
296
+ claw-memory lessons import bundle.json [--status S] [--cwd P]
297
+ claw-memory search-logs "query" [--source claude-code,codex,chatgpt-web] [--project P]
251
298
  [--start ISO] [--end ISO] [--limit N] [--offset N]
252
299
  claw-memory hook <recall|distill> # lifecycle hook (reads JSON on stdin)
253
300
  claw-memory install [--codex | --claude-code] # register MCP + hooks (default: codex)
@@ -316,7 +363,9 @@ Your memory database is left untouched; delete `~/.claw-memory` to wipe it.
316
363
 
317
364
  ## Notes
318
365
 
319
- - `better-sqlite3` / `sqlite-vec` are native modules; run `npm rebuild` after a Node ABI change.
366
+ - Storage uses Node's built-in `node:sqlite` (Node >= 24 required), so Node upgrades can't break a native ABI. The `sqlite-vec` extension ships prebuilt and is version-independent.
367
+ - Per-prompt injection was removed in v0.3.0 (context-bloat fix): hooks inject a compact block at `SessionStart` only; pull details on demand with `memory_recall` / `memory_search` or the `memory-recall` skill.
368
+ - `claw-memory cleanse` finds (and with `--apply` tombstones) legacy chunks polluted by raw JSON payloads.
320
369
  - The MCP server is long-lived per agent session, so the embedding model loads once.
321
370
  - Viewer + MCP can run simultaneously — SQLite WAL handles concurrent read/write.
322
371
  - On install, dependencies resolve with `legacy-peer-deps=true` (a zod peer-range overlap
package/dist/cli.js CHANGED
@@ -162,6 +162,79 @@ async function main() {
162
162
  console.log(JSON.stringify({ scanned: files.length, distilled, skipped, failed }, null, 2));
163
163
  process.exit(0);
164
164
  }
165
+ case "distill-chatgpt": {
166
+ const { loadChatgptConversations } = await import("./core/logsearch/recent.js");
167
+ const { chatgptProjectPath } = await import("./core/logsearch/paths.js");
168
+ const { distill } = await import("./core/distill.js");
169
+ const { getOrCreateProjectByPath } = await import("./core/projects.js");
170
+ const { shouldDistill, markDistilled } = await import("./core/watermark.js");
171
+ const all = hasFlag(rest, "all");
172
+ const limit = Number(getFlag(rest, "limit") ?? (all ? 100000 : 50));
173
+ const convos = (await loadChatgptConversations()).slice(0, limit);
174
+ // ChatGPT has no cwd; all conversations live under one stable synthetic
175
+ // project so they never pollute a real repository's recall.
176
+ const project = getOrCreateProjectByPath(chatgptProjectPath);
177
+ let distilled = 0, skipped = 0, failed = 0;
178
+ for (const conv of convos) {
179
+ // Watermark per conversation, keyed by id + last write time.
180
+ const wmKey = `chatgpt:${conv.conversationId}`;
181
+ const mtimeMs = conv.updateTime ? new Date(conv.updateTime).getTime() : 0;
182
+ if (!shouldDistill(wmKey, mtimeMs)) {
183
+ skipped++;
184
+ continue;
185
+ }
186
+ try {
187
+ const res = await distill({
188
+ projectId: project.id,
189
+ sessionId: conv.conversationId,
190
+ messages: conv.messages.map((m) => ({ role: m.role, text: m.text })),
191
+ });
192
+ markDistilled(wmKey, mtimeMs);
193
+ if (res.skipped)
194
+ skipped++;
195
+ else
196
+ distilled++;
197
+ }
198
+ catch (e) {
199
+ failed++;
200
+ console.error(`distill failed ${conv.conversationId}: ${String(e)}`);
201
+ }
202
+ }
203
+ console.log(JSON.stringify({ scanned: convos.length, distilled, skipped, failed }, null, 2));
204
+ process.exit(0);
205
+ }
206
+ case "cleanse": {
207
+ // Hygiene pass over stored chunks: find rows whose text is a raw JSON
208
+ // payload (e.g. old distill responses stored verbatim) and tombstone
209
+ // them via the existing soft-delete. Dry-run by default; --apply acts.
210
+ const { sqlite } = await import("./core/db.js");
211
+ const { isContaminatedChunkText } = await import("./core/distill.js");
212
+ const { forgetChunks } = await import("./core/vector-memory.js");
213
+ const rows = sqlite
214
+ .prepare("SELECT id, user_text, assistant_text FROM conversation_chunks WHERE deleted_at IS NULL")
215
+ .all();
216
+ const bad = rows.filter((r) => isContaminatedChunkText(r.user_text) ||
217
+ isContaminatedChunkText(r.assistant_text));
218
+ if (hasFlag(rest, "apply")) {
219
+ const n = forgetChunks(bad.map((b) => b.id));
220
+ console.log(JSON.stringify({ scanned: rows.length, contaminated: bad.length, tombstoned: n }, null, 2));
221
+ }
222
+ else {
223
+ for (const b of bad.slice(0, Number(getFlag(rest, "show") ?? 5))) {
224
+ const t = isContaminatedChunkText(b.user_text)
225
+ ? b.user_text
226
+ : b.assistant_text;
227
+ console.log(`${b.id} ${t.slice(0, 120).replace(/\s+/g, " ")}`);
228
+ }
229
+ console.log(JSON.stringify({
230
+ scanned: rows.length,
231
+ contaminated: bad.length,
232
+ dryRun: true,
233
+ hint: "run with --apply to tombstone (soft-delete)",
234
+ }, null, 2));
235
+ }
236
+ process.exit(0);
237
+ }
165
238
  case "remember": {
166
239
  const { getOrCreateProjectByPath } = await import("./core/projects.js");
167
240
  const { rememberText } = await import("./core/distill.js");
@@ -201,14 +274,155 @@ async function main() {
201
274
  console.log(JSON.stringify(out, null, 2));
202
275
  process.exit(0);
203
276
  }
277
+ case "lessons": {
278
+ const sub = rest[0];
279
+ const subRest = rest.slice(1);
280
+ const positionals = (args) => {
281
+ const out = [];
282
+ for (let i = 0; i < args.length; i++) {
283
+ if (args[i].startsWith("--")) {
284
+ i++;
285
+ continue;
286
+ }
287
+ out.push(args[i]);
288
+ }
289
+ return out;
290
+ };
291
+ const projectId = async () => {
292
+ const { getOrCreateProjectByPath } = await import("./core/projects.js");
293
+ return getOrCreateProjectByPath(getFlag(subRest, "cwd") ?? process.cwd()).id;
294
+ };
295
+ switch (sub) {
296
+ case "list": {
297
+ const { listLessons } = await import("./core/lessons.js");
298
+ const status = getFlag(subRest, "status");
299
+ const rows = listLessons({ status, projectId: getFlag(subRest, "cwd") ? await projectId() : undefined });
300
+ for (const l of rows) {
301
+ console.log(`${l.id} [${l.status}/${l.scope}] conf=${l.confidence.toFixed(2)} ${l.title}`);
302
+ }
303
+ console.log(`(${rows.length} lessons)`);
304
+ process.exit(0);
305
+ }
306
+ case "search": {
307
+ const { searchLessons } = await import("./core/lesson-search.js");
308
+ const query = positionals(subRest).join(" ").trim();
309
+ if (!query)
310
+ throw new Error("lessons search requires a query");
311
+ const hits = await searchLessons(query, { projectId: await projectId() }, { limit: Number(getFlag(subRest, "limit") ?? 5) });
312
+ for (const l of hits) {
313
+ console.log(`${l.id} score=${l.score.toFixed(3)} [${l.scope}] ${l.title}`);
314
+ }
315
+ process.exit(0);
316
+ }
317
+ case "inject": {
318
+ const { injectLessons } = await import("./core/lesson-search.js");
319
+ const query = positionals(subRest).join(" ").trim();
320
+ if (!query)
321
+ throw new Error("lessons inject requires a query");
322
+ console.log(await injectLessons(query, { projectId: await projectId() }, { limit: Number(getFlag(subRest, "limit") ?? 5) }));
323
+ process.exit(0);
324
+ }
325
+ case "approve":
326
+ case "reject":
327
+ case "archive": {
328
+ const { setStatus } = await import("./core/lessons.js");
329
+ const id = positionals(subRest)[0];
330
+ if (!id)
331
+ throw new Error(`lessons ${sub} requires a lesson_id`);
332
+ const newStatus = sub === "approve" ? "approved" : sub === "reject" ? "rejected" : "archived";
333
+ const ok = setStatus(id, newStatus, getFlag(subRest, "reason"));
334
+ console.log(ok ? `${id} -> ${newStatus}` : "(該当なし)");
335
+ process.exit(ok ? 0 : 1);
336
+ }
337
+ case "supersede": {
338
+ const { supersede } = await import("./core/lessons.js");
339
+ const [oldId, newId] = positionals(subRest);
340
+ if (!oldId || !newId)
341
+ throw new Error("lessons supersede requires <old_id> <new_id>");
342
+ const ok = supersede(oldId, newId);
343
+ console.log(ok ? `${oldId} superseded by ${newId}` : "(該当なし)");
344
+ process.exit(ok ? 0 : 1);
345
+ }
346
+ case "decay": {
347
+ const { decayConfidence, listStale } = await import("./core/lesson-quality.js");
348
+ const dryRun = hasFlag(subRest, "dry");
349
+ const res = decayConfidence({
350
+ factor: getFlag(subRest, "factor") ? Number(getFlag(subRest, "factor")) : undefined,
351
+ staleDays: getFlag(subRest, "days") ? Number(getFlag(subRest, "days")) : undefined,
352
+ dryRun,
353
+ });
354
+ console.log(JSON.stringify({ ...res, dryRun, stale: listStale().length }, null, 2));
355
+ process.exit(0);
356
+ }
357
+ case "export": {
358
+ const { exportLessons } = await import("./core/lesson-share.js");
359
+ const bundle = exportLessons({
360
+ projectId: getFlag(subRest, "cwd") ? await projectId() : undefined,
361
+ status: getFlag(subRest, "status"),
362
+ });
363
+ console.log(JSON.stringify(bundle, null, 2));
364
+ process.exit(0);
365
+ }
366
+ case "import": {
367
+ const { importLessons } = await import("./core/lesson-share.js");
368
+ const { readFileSync } = await import("node:fs");
369
+ const file = positionals(subRest)[0];
370
+ if (!file)
371
+ throw new Error("lessons import requires a bundle file path");
372
+ const bundle = JSON.parse(readFileSync(file, "utf-8"));
373
+ const res = await importLessons(bundle, {
374
+ projectId: await projectId(),
375
+ status: getFlag(subRest, "status"),
376
+ });
377
+ console.log(JSON.stringify(res, null, 2));
378
+ process.exit(0);
379
+ }
380
+ case "extract": {
381
+ const { getOrCreateProjectByPath } = await import("./core/projects.js");
382
+ const { resolveSessionJsonl, loadTranscript } = await import("./core/transcript.js");
383
+ const { stripPrivate } = await import("./core/private.js");
384
+ const { extractDedicated, saveCandidates } = await import("./core/lesson-extract.js");
385
+ const cwd = getFlag(subRest, "cwd") ?? process.cwd();
386
+ const session = getFlag(subRest, "session") ?? "";
387
+ const path = getFlag(subRest, "path") ?? (session ? resolveSessionJsonl(cwd, session) : undefined);
388
+ if (!path)
389
+ throw new Error("lessons extract requires --session or --path");
390
+ const project = getOrCreateProjectByPath(cwd);
391
+ const msgs = loadTranscript(path).map((m) => ({ ...m, text: stripPrivate(m.text) })).filter((m) => m.text.trim());
392
+ const transcript = msgs.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.text.slice(0, 500)}`).join("\n");
393
+ const candidates = await extractDedicated(transcript);
394
+ const ids = await saveCandidates({ projectId: project.id, sessionId: session || path, candidates });
395
+ console.log(JSON.stringify({ extracted: candidates.length, saved: ids.length }, null, 2));
396
+ process.exit(0);
397
+ }
398
+ default:
399
+ console.error("Usage: claw-memory lessons <subcommand>\n" +
400
+ " list [--status candidate|approved|...] [--cwd P]\n" +
401
+ " search \"query\" [--cwd P] [--limit N]\n" +
402
+ " inject \"query\" [--cwd P] [--limit N]\n" +
403
+ " extract --session ID [--cwd P] [--path FILE]\n" +
404
+ " approve <lesson_id>\n" +
405
+ " reject <lesson_id> [--reason R]\n" +
406
+ " archive <lesson_id> [--reason R]\n" +
407
+ " supersede <old_id> <new_id>\n" +
408
+ " decay [--days N] [--factor F] [--dry]\n" +
409
+ " export [--status S] [--cwd P] print a portable JSON bundle\n" +
410
+ " import <file> [--status S] [--cwd P] import a bundle (default status: candidate)");
411
+ process.exit(sub ? 1 : 0);
412
+ }
413
+ return;
414
+ }
204
415
  default:
205
416
  console.error("Usage: claw-memory <command>\n" +
206
417
  " mcp start stdio MCP server\n" +
207
418
  " ui [--port N] [--open] start memory viewer\n" +
208
419
  " distill --cwd P --session ID [--path FILE] [--if-stale]\n" +
209
420
  " distill-codex [--recent] [--limit N] [--all] distill recent Codex sessions\n" +
421
+ " distill-chatgpt [--limit N] [--all] distill ChatGPT web export conversations\n" +
210
422
  " remember --cwd P \"text\"\n" +
211
- " search-logs \"query\" [--source claude-code,codex] [--project P] [--start ISO] [--end ISO] [--limit N] [--offset N]\n" +
423
+ " cleanse [--apply] [--show N] find (and with --apply tombstone) JSON-contaminated chunks\n" +
424
+ " lessons <list|search|inject|extract|approve|reject|archive|supersede>\n" +
425
+ " search-logs \"query\" [--source claude-code,codex,chatgpt-web] [--project P] [--start ISO] [--end ISO] [--limit N] [--offset N]\n" +
212
426
  " hook <distill|recall> run a Claude Code lifecycle hook (reads JSON on stdin)\n" +
213
427
  " inject-recall alias for `hook recall`\n" +
214
428
  " install [--codex|--claude-code] register MCP + hooks (default: codex)\n" +