@nogataka/claw-memory 0.1.3 → 0.2.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.
- package/README.ja.md +47 -6
- package/README.md +52 -6
- package/dist/cli.js +182 -1
- package/dist/core/db.js +95 -0
- package/dist/core/distill.js +36 -2
- package/dist/core/lesson-extract.js +233 -0
- package/dist/core/lesson-quality.js +79 -0
- package/dist/core/lesson-search.js +145 -0
- package/dist/core/lesson-share.js +80 -0
- package/dist/core/lessons.js +335 -0
- package/dist/core/logsearch/parse.js +0 -0
- package/dist/core/logsearch/paths.js +22 -0
- package/dist/core/logsearch/recent.js +46 -2
- package/dist/core/logsearch/search.js +57 -3
- package/dist/core/recall.js +18 -0
- package/dist/mcp/server.js +176 -4
- package/dist/ui/page.js +125 -4
- package/dist/ui/server.js +64 -0
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **日本語**
|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
|
|
5
7
|
**AI コーディングエージェント(Claude Code / Codex)のためのローカル完結型・長期記憶。**
|
|
6
8
|
エージェントが過去のセッション・あなたの好み・以前の決定事項を記憶し、記録済みの全
|
|
7
9
|
トランスクリプトを横断検索できます。デーモン不要、Python 不要、外部ベクトル DB 不要。
|
|
@@ -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
|
|
|
@@ -195,11 +214,17 @@ npm link # 任意: `claw-memory` バイナリを公開
|
|
|
195
214
|
| `memory_remember(text, cwd?, sessionId?)` | フリーテキストのメモを永続保存。 |
|
|
196
215
|
| `memory_distill(cwd, sessionId? \| transcriptPath?)` | セッションを要約して記憶化(LLM バックエンドが必要)。 |
|
|
197
216
|
| `memory_get_preferences(cwd?)` | プロジェクトの保存済み好みを一覧。 |
|
|
198
|
-
| `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Claude Code + Codex
|
|
217
|
+
| `memory_search_logs(query, sources?, projectPath?, startDate?, endDate?, limit?, offset?)` | Claude Code + Codex + ChatGPT-web の生ログを全文検索(`sources`: `claude-code` / `codex` / `chatgpt-web`)。 |
|
|
199
218
|
| `memory_forget(ids)` | チャンクをソフト削除(検索 / recall / ビューアから除外)。 |
|
|
219
|
+
| `lesson_search(query, cwd?, limit?)` | 承認済みレッスンを関連度 + scope + confidence で検索。 |
|
|
220
|
+
| `lesson_inject(query, cwd?, limit?)` | 同上を `<relevant-lessons>` ブロックとして返す。 |
|
|
221
|
+
| `lesson_get(lesson_id)` | レッスン1件の詳細(全フィールド + 履歴 + リンク)。 |
|
|
222
|
+
| `lesson_extract(cwd, sessionId? \| transcriptPath?)` | セッションから専用のレッスン抽出(LLM 必要)。 |
|
|
223
|
+
| `lesson_approve / lesson_reject / lesson_archive(lesson_id, reason?)` | ステータス遷移。 |
|
|
224
|
+
| `lesson_supersede(old_lesson_id, new_lesson_id)` | 古いレッスンを新しいもので置き換え。 |
|
|
200
225
|
|
|
201
|
-
`memory_distill`(LLM)と `memory_search_logs
|
|
202
|
-
を直接読む)以外は完全ローカルで動作します。
|
|
226
|
+
`memory_distill` / `lesson_extract`(LLM)と `memory_search_logs`
|
|
227
|
+
(`~/.claude/projects`・`~/.codex/sessions` を直接読む)以外は完全ローカルで動作します。
|
|
203
228
|
|
|
204
229
|
---
|
|
205
230
|
|
|
@@ -218,6 +243,12 @@ npm link # 任意: `claw-memory` バイナリを公開
|
|
|
218
243
|
| `CLAW_MEMORY_EXCLUDED_PROJECTS` | — | 記録・想起しないパス部分文字列(カンマ/コロン区切り)。 |
|
|
219
244
|
| `MEMORY_SIMILARITY_MAX_DISTANCE` | `0.6` | セマンティックヒットの最大コサイン距離(小さいほど厳格)。 |
|
|
220
245
|
| `CLAW_MEMORY_UI_PORT` | `4319` | ビューアのポート。 |
|
|
246
|
+
| `LESSON_RECALL_LIMIT` | `3` | recall ブロックに注入する承認済みレッスン数(`0` で無効)。 |
|
|
247
|
+
| `CLAW_MEMORY_LESSON_DEDICATED` | — | `1` で専用の高品質レッスン抽出パスを実行(LLM 追加呼び出し)。 |
|
|
248
|
+
| `CLAW_MEMORY_LESSON_CONFLICT_LLM` | — | `1` で抽出時に LLM による矛盾検出を有効化。 |
|
|
249
|
+
| `LESSON_DECAY_FACTOR` / `LESSON_STALE_DAYS` | `0.9` / `30` | `lessons decay` の減衰係数と陳腐化しきい値。 |
|
|
250
|
+
| `CLAW_MEMORY_CHATGPT_EXPORT` | `~/.claw-memory/chatgpt` | ChatGPT `conversations.json` エクスポート(ファイル or `*.json` を含むフォルダ)。 |
|
|
251
|
+
| `CLAW_MEMORY_CHATGPT_MAX_BYTES` | `209715200`(200 MB) | これを超える ChatGPT エクスポートはスキップ(メモリ内パースのため)。 |
|
|
221
252
|
|
|
222
253
|
### LLM バックエンド
|
|
223
254
|
|
|
@@ -241,8 +272,18 @@ claw-memory mcp # stdio MCP サーバー(エ
|
|
|
241
272
|
claw-memory ui [--port N] [--open] # 読み取り専用 Web ビューア
|
|
242
273
|
claw-memory distill --cwd P --session ID [--path FILE] [--if-stale]
|
|
243
274
|
claw-memory distill-codex [--recent] [--limit N] [--all]
|
|
275
|
+
claw-memory distill-chatgpt [--limit N] [--all] # ChatGPT web エクスポートの会話を蒸留
|
|
244
276
|
claw-memory remember --cwd P "メモ"
|
|
245
|
-
claw-memory
|
|
277
|
+
claw-memory lessons list [--status candidate|approved|...] [--cwd P]
|
|
278
|
+
claw-memory lessons search "クエリ" [--cwd P] [--limit N]
|
|
279
|
+
claw-memory lessons inject "クエリ" [--cwd P] [--limit N]
|
|
280
|
+
claw-memory lessons extract --session ID [--cwd P] [--path FILE]
|
|
281
|
+
claw-memory lessons approve|reject|archive <lesson_id> [--reason R]
|
|
282
|
+
claw-memory lessons supersede <old_id> <new_id>
|
|
283
|
+
claw-memory lessons decay [--days N] [--factor F] [--dry]
|
|
284
|
+
claw-memory lessons export [--status S] [--cwd P] > bundle.json
|
|
285
|
+
claw-memory lessons import bundle.json [--status S] [--cwd P]
|
|
286
|
+
claw-memory search-logs "クエリ" [--source claude-code,codex,chatgpt-web] [--project P]
|
|
246
287
|
[--start ISO] [--end ISO] [--limit N] [--offset N]
|
|
247
288
|
claw-memory hook <recall|distill> # ライフサイクルフック(JSON を stdin で受領)
|
|
248
289
|
claw-memory install [--codex | --claude-code] # MCP + フックを登録(既定: codex)
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [日本語](README.ja.md)
|
|
4
4
|
|
|
5
|
+

|
|
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
|
|
@@ -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`)
|
|
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
|
|
|
@@ -200,11 +223,18 @@ npm link # optional: expose the `claw-memory` binary
|
|
|
200
223
|
| `memory_remember(text, cwd?, sessionId?)` | Store a durable free-text note. |
|
|
201
224
|
| `memory_distill(cwd, sessionId? \| transcriptPath?)` | Summarize a session into memory (needs an LLM backend). |
|
|
202
225
|
| `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. |
|
|
226
|
+
| `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
227
|
| `memory_forget(ids)` | Soft-delete chunks (hidden from search / recall / viewer). |
|
|
228
|
+
| `lesson_search(query, cwd?, limit?)` | Search approved reusable lessons, ranked by relevance + scope + confidence. |
|
|
229
|
+
| `lesson_inject(query, cwd?, limit?)` | Same, returned as a ready-to-read `<relevant-lessons>` block. |
|
|
230
|
+
| `lesson_get(lesson_id)` | Full detail of one lesson (fields + history + links). |
|
|
231
|
+
| `lesson_extract(cwd, sessionId? \| transcriptPath?)` | Dedicated lesson-extraction pass over a session (needs an LLM backend). |
|
|
232
|
+
| `lesson_approve / lesson_reject / lesson_archive(lesson_id, reason?)` | Lifecycle transitions. |
|
|
233
|
+
| `lesson_supersede(old_lesson_id, new_lesson_id)` | Replace an old lesson with a newer one. |
|
|
205
234
|
|
|
206
|
-
All tools are fully local except `memory_distill` (LLM) and
|
|
207
|
-
(reads `~/.claude/projects` and `~/.codex/sessions`
|
|
235
|
+
All tools are fully local except `memory_distill` / `lesson_extract` (LLM) and
|
|
236
|
+
`memory_search_logs` (reads `~/.claude/projects` and `~/.codex/sessions`
|
|
237
|
+
directly).
|
|
208
238
|
|
|
209
239
|
---
|
|
210
240
|
|
|
@@ -223,6 +253,12 @@ All tools are fully local except `memory_distill` (LLM) and `memory_search_logs`
|
|
|
223
253
|
| `CLAW_MEMORY_EXCLUDED_PROJECTS` | — | Comma/colon-separated path substrings to never record or recall. |
|
|
224
254
|
| `MEMORY_SIMILARITY_MAX_DISTANCE` | `0.6` | Max cosine distance for a semantic hit (lower = stricter). |
|
|
225
255
|
| `CLAW_MEMORY_UI_PORT` | `4319` | Viewer port. |
|
|
256
|
+
| `LESSON_RECALL_LIMIT` | `3` | Approved lessons injected into the recall block (`0` disables). |
|
|
257
|
+
| `CLAW_MEMORY_LESSON_DEDICATED` | — | `1` = run a separate, higher-quality lesson-extraction pass (extra LLM call). |
|
|
258
|
+
| `CLAW_MEMORY_LESSON_CONFLICT_LLM` | — | `1` = use the LLM to detect conflicting lessons during extraction. |
|
|
259
|
+
| `LESSON_DECAY_FACTOR` / `LESSON_STALE_DAYS` | `0.9` / `30` | Confidence-decay factor and staleness threshold for `lessons decay`. |
|
|
260
|
+
| `CLAW_MEMORY_CHATGPT_EXPORT` | `~/.claw-memory/chatgpt` | ChatGPT `conversations.json` export — a file or a folder of `*.json`. |
|
|
261
|
+
| `CLAW_MEMORY_CHATGPT_MAX_BYTES` | `209715200` (200 MB) | Skip ChatGPT export files larger than this (parsed in memory). |
|
|
226
262
|
|
|
227
263
|
### LLM backends
|
|
228
264
|
|
|
@@ -246,8 +282,18 @@ claw-memory mcp # stdio MCP server (what agents
|
|
|
246
282
|
claw-memory ui [--port N] [--open] # read-only web viewer
|
|
247
283
|
claw-memory distill --cwd P --session ID [--path FILE] [--if-stale]
|
|
248
284
|
claw-memory distill-codex [--recent] [--limit N] [--all]
|
|
285
|
+
claw-memory distill-chatgpt [--limit N] [--all] # distill ChatGPT web export conversations
|
|
249
286
|
claw-memory remember --cwd P "a note"
|
|
250
|
-
claw-memory
|
|
287
|
+
claw-memory lessons list [--status candidate|approved|...] [--cwd P]
|
|
288
|
+
claw-memory lessons search "query" [--cwd P] [--limit N]
|
|
289
|
+
claw-memory lessons inject "query" [--cwd P] [--limit N]
|
|
290
|
+
claw-memory lessons extract --session ID [--cwd P] [--path FILE]
|
|
291
|
+
claw-memory lessons approve|reject|archive <lesson_id> [--reason R]
|
|
292
|
+
claw-memory lessons supersede <old_id> <new_id>
|
|
293
|
+
claw-memory lessons decay [--days N] [--factor F] [--dry]
|
|
294
|
+
claw-memory lessons export [--status S] [--cwd P] > bundle.json
|
|
295
|
+
claw-memory lessons import bundle.json [--status S] [--cwd P]
|
|
296
|
+
claw-memory search-logs "query" [--source claude-code,codex,chatgpt-web] [--project P]
|
|
251
297
|
[--start ISO] [--end ISO] [--limit N] [--offset N]
|
|
252
298
|
claw-memory hook <recall|distill> # lifecycle hook (reads JSON on stdin)
|
|
253
299
|
claw-memory install [--codex | --claude-code] # register MCP + hooks (default: codex)
|
package/dist/cli.js
CHANGED
|
@@ -162,6 +162,47 @@ 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
|
+
}
|
|
165
206
|
case "remember": {
|
|
166
207
|
const { getOrCreateProjectByPath } = await import("./core/projects.js");
|
|
167
208
|
const { rememberText } = await import("./core/distill.js");
|
|
@@ -201,14 +242,154 @@ async function main() {
|
|
|
201
242
|
console.log(JSON.stringify(out, null, 2));
|
|
202
243
|
process.exit(0);
|
|
203
244
|
}
|
|
245
|
+
case "lessons": {
|
|
246
|
+
const sub = rest[0];
|
|
247
|
+
const subRest = rest.slice(1);
|
|
248
|
+
const positionals = (args) => {
|
|
249
|
+
const out = [];
|
|
250
|
+
for (let i = 0; i < args.length; i++) {
|
|
251
|
+
if (args[i].startsWith("--")) {
|
|
252
|
+
i++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
out.push(args[i]);
|
|
256
|
+
}
|
|
257
|
+
return out;
|
|
258
|
+
};
|
|
259
|
+
const projectId = async () => {
|
|
260
|
+
const { getOrCreateProjectByPath } = await import("./core/projects.js");
|
|
261
|
+
return getOrCreateProjectByPath(getFlag(subRest, "cwd") ?? process.cwd()).id;
|
|
262
|
+
};
|
|
263
|
+
switch (sub) {
|
|
264
|
+
case "list": {
|
|
265
|
+
const { listLessons } = await import("./core/lessons.js");
|
|
266
|
+
const status = getFlag(subRest, "status");
|
|
267
|
+
const rows = listLessons({ status, projectId: getFlag(subRest, "cwd") ? await projectId() : undefined });
|
|
268
|
+
for (const l of rows) {
|
|
269
|
+
console.log(`${l.id} [${l.status}/${l.scope}] conf=${l.confidence.toFixed(2)} ${l.title}`);
|
|
270
|
+
}
|
|
271
|
+
console.log(`(${rows.length} lessons)`);
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
case "search": {
|
|
275
|
+
const { searchLessons } = await import("./core/lesson-search.js");
|
|
276
|
+
const query = positionals(subRest).join(" ").trim();
|
|
277
|
+
if (!query)
|
|
278
|
+
throw new Error("lessons search requires a query");
|
|
279
|
+
const hits = await searchLessons(query, { projectId: await projectId() }, { limit: Number(getFlag(subRest, "limit") ?? 5) });
|
|
280
|
+
for (const l of hits) {
|
|
281
|
+
console.log(`${l.id} score=${l.score.toFixed(3)} [${l.scope}] ${l.title}`);
|
|
282
|
+
}
|
|
283
|
+
process.exit(0);
|
|
284
|
+
}
|
|
285
|
+
case "inject": {
|
|
286
|
+
const { injectLessons } = await import("./core/lesson-search.js");
|
|
287
|
+
const query = positionals(subRest).join(" ").trim();
|
|
288
|
+
if (!query)
|
|
289
|
+
throw new Error("lessons inject requires a query");
|
|
290
|
+
console.log(await injectLessons(query, { projectId: await projectId() }, { limit: Number(getFlag(subRest, "limit") ?? 5) }));
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
case "approve":
|
|
294
|
+
case "reject":
|
|
295
|
+
case "archive": {
|
|
296
|
+
const { setStatus } = await import("./core/lessons.js");
|
|
297
|
+
const id = positionals(subRest)[0];
|
|
298
|
+
if (!id)
|
|
299
|
+
throw new Error(`lessons ${sub} requires a lesson_id`);
|
|
300
|
+
const newStatus = sub === "approve" ? "approved" : sub === "reject" ? "rejected" : "archived";
|
|
301
|
+
const ok = setStatus(id, newStatus, getFlag(subRest, "reason"));
|
|
302
|
+
console.log(ok ? `${id} -> ${newStatus}` : "(該当なし)");
|
|
303
|
+
process.exit(ok ? 0 : 1);
|
|
304
|
+
}
|
|
305
|
+
case "supersede": {
|
|
306
|
+
const { supersede } = await import("./core/lessons.js");
|
|
307
|
+
const [oldId, newId] = positionals(subRest);
|
|
308
|
+
if (!oldId || !newId)
|
|
309
|
+
throw new Error("lessons supersede requires <old_id> <new_id>");
|
|
310
|
+
const ok = supersede(oldId, newId);
|
|
311
|
+
console.log(ok ? `${oldId} superseded by ${newId}` : "(該当なし)");
|
|
312
|
+
process.exit(ok ? 0 : 1);
|
|
313
|
+
}
|
|
314
|
+
case "decay": {
|
|
315
|
+
const { decayConfidence, listStale } = await import("./core/lesson-quality.js");
|
|
316
|
+
const dryRun = hasFlag(subRest, "dry");
|
|
317
|
+
const res = decayConfidence({
|
|
318
|
+
factor: getFlag(subRest, "factor") ? Number(getFlag(subRest, "factor")) : undefined,
|
|
319
|
+
staleDays: getFlag(subRest, "days") ? Number(getFlag(subRest, "days")) : undefined,
|
|
320
|
+
dryRun,
|
|
321
|
+
});
|
|
322
|
+
console.log(JSON.stringify({ ...res, dryRun, stale: listStale().length }, null, 2));
|
|
323
|
+
process.exit(0);
|
|
324
|
+
}
|
|
325
|
+
case "export": {
|
|
326
|
+
const { exportLessons } = await import("./core/lesson-share.js");
|
|
327
|
+
const bundle = exportLessons({
|
|
328
|
+
projectId: getFlag(subRest, "cwd") ? await projectId() : undefined,
|
|
329
|
+
status: getFlag(subRest, "status"),
|
|
330
|
+
});
|
|
331
|
+
console.log(JSON.stringify(bundle, null, 2));
|
|
332
|
+
process.exit(0);
|
|
333
|
+
}
|
|
334
|
+
case "import": {
|
|
335
|
+
const { importLessons } = await import("./core/lesson-share.js");
|
|
336
|
+
const { readFileSync } = await import("node:fs");
|
|
337
|
+
const file = positionals(subRest)[0];
|
|
338
|
+
if (!file)
|
|
339
|
+
throw new Error("lessons import requires a bundle file path");
|
|
340
|
+
const bundle = JSON.parse(readFileSync(file, "utf-8"));
|
|
341
|
+
const res = await importLessons(bundle, {
|
|
342
|
+
projectId: await projectId(),
|
|
343
|
+
status: getFlag(subRest, "status"),
|
|
344
|
+
});
|
|
345
|
+
console.log(JSON.stringify(res, null, 2));
|
|
346
|
+
process.exit(0);
|
|
347
|
+
}
|
|
348
|
+
case "extract": {
|
|
349
|
+
const { getOrCreateProjectByPath } = await import("./core/projects.js");
|
|
350
|
+
const { resolveSessionJsonl, loadTranscript } = await import("./core/transcript.js");
|
|
351
|
+
const { stripPrivate } = await import("./core/private.js");
|
|
352
|
+
const { extractDedicated, saveCandidates } = await import("./core/lesson-extract.js");
|
|
353
|
+
const cwd = getFlag(subRest, "cwd") ?? process.cwd();
|
|
354
|
+
const session = getFlag(subRest, "session") ?? "";
|
|
355
|
+
const path = getFlag(subRest, "path") ?? (session ? resolveSessionJsonl(cwd, session) : undefined);
|
|
356
|
+
if (!path)
|
|
357
|
+
throw new Error("lessons extract requires --session or --path");
|
|
358
|
+
const project = getOrCreateProjectByPath(cwd);
|
|
359
|
+
const msgs = loadTranscript(path).map((m) => ({ ...m, text: stripPrivate(m.text) })).filter((m) => m.text.trim());
|
|
360
|
+
const transcript = msgs.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.text.slice(0, 500)}`).join("\n");
|
|
361
|
+
const candidates = await extractDedicated(transcript);
|
|
362
|
+
const ids = await saveCandidates({ projectId: project.id, sessionId: session || path, candidates });
|
|
363
|
+
console.log(JSON.stringify({ extracted: candidates.length, saved: ids.length }, null, 2));
|
|
364
|
+
process.exit(0);
|
|
365
|
+
}
|
|
366
|
+
default:
|
|
367
|
+
console.error("Usage: claw-memory lessons <subcommand>\n" +
|
|
368
|
+
" list [--status candidate|approved|...] [--cwd P]\n" +
|
|
369
|
+
" search \"query\" [--cwd P] [--limit N]\n" +
|
|
370
|
+
" inject \"query\" [--cwd P] [--limit N]\n" +
|
|
371
|
+
" extract --session ID [--cwd P] [--path FILE]\n" +
|
|
372
|
+
" approve <lesson_id>\n" +
|
|
373
|
+
" reject <lesson_id> [--reason R]\n" +
|
|
374
|
+
" archive <lesson_id> [--reason R]\n" +
|
|
375
|
+
" supersede <old_id> <new_id>\n" +
|
|
376
|
+
" decay [--days N] [--factor F] [--dry]\n" +
|
|
377
|
+
" export [--status S] [--cwd P] print a portable JSON bundle\n" +
|
|
378
|
+
" import <file> [--status S] [--cwd P] import a bundle (default status: candidate)");
|
|
379
|
+
process.exit(sub ? 1 : 0);
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
204
383
|
default:
|
|
205
384
|
console.error("Usage: claw-memory <command>\n" +
|
|
206
385
|
" mcp start stdio MCP server\n" +
|
|
207
386
|
" ui [--port N] [--open] start memory viewer\n" +
|
|
208
387
|
" distill --cwd P --session ID [--path FILE] [--if-stale]\n" +
|
|
209
388
|
" distill-codex [--recent] [--limit N] [--all] distill recent Codex sessions\n" +
|
|
389
|
+
" distill-chatgpt [--limit N] [--all] distill ChatGPT web export conversations\n" +
|
|
210
390
|
" remember --cwd P \"text\"\n" +
|
|
211
|
-
" search
|
|
391
|
+
" lessons <list|search|inject|extract|approve|reject|archive|supersede>\n" +
|
|
392
|
+
" search-logs \"query\" [--source claude-code,codex,chatgpt-web] [--project P] [--start ISO] [--end ISO] [--limit N] [--offset N]\n" +
|
|
212
393
|
" hook <distill|recall> run a Claude Code lifecycle hook (reads JSON on stdin)\n" +
|
|
213
394
|
" inject-recall alias for `hook recall`\n" +
|
|
214
395
|
" install [--codex|--claude-code] register MCP + hooks (default: codex)\n" +
|
package/dist/core/db.js
CHANGED
|
@@ -90,3 +90,98 @@ sqlite.exec(`
|
|
|
90
90
|
tokenize = 'trigram'
|
|
91
91
|
);
|
|
92
92
|
`);
|
|
93
|
+
// --- Lesson layer ----------------------------------------------------------
|
|
94
|
+
// A "lesson" is reusable, abstracted knowledge distilled from sessions (bug-fix
|
|
95
|
+
// patterns, project constraints, design decisions, user preferences). Unlike
|
|
96
|
+
// conversation_chunks (raw history), lessons are explicit, structured, scoped,
|
|
97
|
+
// and lifecycle-managed (candidate → approved/rejected/archived/superseded).
|
|
98
|
+
// JSON-array columns (applies_when / avoid_when / concepts / files /
|
|
99
|
+
// source_chunk_ids) are stored as TEXT to keep the schema flat and ORM-free,
|
|
100
|
+
// mirroring conversation_chunks.concepts.
|
|
101
|
+
sqlite.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
103
|
+
id TEXT PRIMARY KEY,
|
|
104
|
+
project_id TEXT,
|
|
105
|
+
repo_id TEXT,
|
|
106
|
+
session_id TEXT,
|
|
107
|
+
vec_rowid INTEGER,
|
|
108
|
+
title TEXT NOT NULL,
|
|
109
|
+
lesson TEXT NOT NULL,
|
|
110
|
+
applies_when TEXT,
|
|
111
|
+
avoid_when TEXT,
|
|
112
|
+
evidence TEXT,
|
|
113
|
+
scope TEXT NOT NULL DEFAULT 'repo',
|
|
114
|
+
obs_type TEXT,
|
|
115
|
+
concepts TEXT,
|
|
116
|
+
files TEXT,
|
|
117
|
+
confidence REAL NOT NULL DEFAULT 0.5,
|
|
118
|
+
source_chunk_ids TEXT,
|
|
119
|
+
status TEXT NOT NULL DEFAULT 'candidate',
|
|
120
|
+
superseded_by TEXT,
|
|
121
|
+
valid_until TEXT,
|
|
122
|
+
last_used_at TEXT,
|
|
123
|
+
created_at TEXT NOT NULL,
|
|
124
|
+
updated_at TEXT NOT NULL
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
-- Lifecycle audit trail: every status transition / decay / scope change.
|
|
128
|
+
CREATE TABLE IF NOT EXISTS lesson_events (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
lesson_id TEXT NOT NULL,
|
|
131
|
+
event_type TEXT NOT NULL,
|
|
132
|
+
old_status TEXT,
|
|
133
|
+
new_status TEXT,
|
|
134
|
+
note TEXT,
|
|
135
|
+
created_at TEXT NOT NULL
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
-- Relations between lessons: duplicate / conflicts_with / supersedes /
|
|
139
|
+
-- related_to / derived_from (populated by dedup + conflict detection).
|
|
140
|
+
CREATE TABLE IF NOT EXISTS lesson_links (
|
|
141
|
+
id TEXT PRIMARY KEY,
|
|
142
|
+
lesson_id TEXT NOT NULL,
|
|
143
|
+
linked_lesson_id TEXT NOT NULL,
|
|
144
|
+
relation TEXT NOT NULL,
|
|
145
|
+
created_at TEXT NOT NULL
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_status ON lessons(status);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_project ON lessons(project_id);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_lesson_events_lesson ON lesson_events(lesson_id);
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_lesson_links_lesson ON lesson_links(lesson_id);
|
|
152
|
+
`);
|
|
153
|
+
// Additive, migration-less column evolution for lessons (same approach as
|
|
154
|
+
// conversation_chunks above): newly-introduced columns are ALTER-added in a
|
|
155
|
+
// try/catch so older ~/.claw-memory/memory.db files keep working.
|
|
156
|
+
for (const col of [
|
|
157
|
+
"repo_id TEXT",
|
|
158
|
+
"valid_until TEXT",
|
|
159
|
+
"last_used_at TEXT",
|
|
160
|
+
"superseded_by TEXT",
|
|
161
|
+
]) {
|
|
162
|
+
try {
|
|
163
|
+
sqlite.exec(`ALTER TABLE lessons ADD COLUMN ${col}`);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// column already exists
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
// vec0 virtual table for lesson embeddings (same 384-dim e5 space as
|
|
170
|
+
// vec_chunks). scope + project_id metadata columns let KNN filter inside the
|
|
171
|
+
// MATCH query without post-filter loss.
|
|
172
|
+
sqlite.exec(`
|
|
173
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lesson_vec USING vec0(
|
|
174
|
+
embedding float[384] distance_metric=cosine,
|
|
175
|
+
project_id text,
|
|
176
|
+
scope text
|
|
177
|
+
);
|
|
178
|
+
`);
|
|
179
|
+
// FTS5 keyword index over lesson title + body (trigram, same rationale as
|
|
180
|
+
// chunks_fts). lesson_id links back to lessons.id.
|
|
181
|
+
sqlite.exec(`
|
|
182
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
|
|
183
|
+
lesson_id UNINDEXED,
|
|
184
|
+
text,
|
|
185
|
+
tokenize = 'trigram'
|
|
186
|
+
);
|
|
187
|
+
`);
|
package/dist/core/distill.js
CHANGED
|
@@ -10,6 +10,7 @@ import { saveChunks, deleteChunksBySession, chunkExists, } from "./vector-memory
|
|
|
10
10
|
import { loadTranscript } from "./transcript.js";
|
|
11
11
|
import { stripPrivate } from "./private.js";
|
|
12
12
|
import { log } from "./logger.js";
|
|
13
|
+
import { saveCandidates, extractDedicated, dedicatedEnabled, } from "./lesson-extract.js";
|
|
13
14
|
const MIN_MESSAGES = 2;
|
|
14
15
|
const MIN_TEXT_LENGTH = 100;
|
|
15
16
|
const MAX_CHARS_PER_MESSAGE = 500;
|
|
@@ -26,9 +27,15 @@ const PROMPT = (transcript) => `以下の会話を分析して JSON で回答し
|
|
|
26
27
|
key は必ず次のいずれか(該当しなければ出さない):
|
|
27
28
|
language | response_style | detail_level | code_style | framework | tone | tools
|
|
28
29
|
変更がなければ空配列。
|
|
30
|
+
7. lessons: 次回以降の作業で再利用できる「教訓」だけを抽出(無ければ空配列)。
|
|
31
|
+
一時的な会話・未検証の推測・秘密情報・一回限りのコマンド・重複は抽出しない。
|
|
32
|
+
各 lesson は実行可能・具体的・再利用可能であること。
|
|
33
|
+
各要素: {"title","lesson","applies_when":[],"avoid_when":[],
|
|
34
|
+
"scope":"global|project|repo|file|task|user_preference","confidence":0.0-1.0,
|
|
35
|
+
"evidence","concepts":[],"files":[]}
|
|
29
36
|
|
|
30
37
|
JSON のみ回答:
|
|
31
|
-
{"summary": "...", "obs_type": "...", "concepts": ["..."], "files_read": ["..."], "files_modified": ["..."], "preferences": [{"key": "...", "value": "..."}]}
|
|
38
|
+
{"summary": "...", "obs_type": "...", "concepts": ["..."], "files_read": ["..."], "files_modified": ["..."], "preferences": [{"key": "...", "value": "..."}], "lessons": []}
|
|
32
39
|
|
|
33
40
|
<conversation>
|
|
34
41
|
${transcript}
|
|
@@ -77,6 +84,7 @@ export async function distill(input) {
|
|
|
77
84
|
const filesModified = (result.files_modified ?? []).map(String).filter(Boolean);
|
|
78
85
|
// --- Vector memory: re-chunk this session (idempotent) ---
|
|
79
86
|
let chunkCount = 0;
|
|
87
|
+
let savedChunkIds = [];
|
|
80
88
|
try {
|
|
81
89
|
deleteChunksBySession(input.sessionId);
|
|
82
90
|
const pairs = [];
|
|
@@ -110,24 +118,50 @@ export async function distill(input) {
|
|
|
110
118
|
});
|
|
111
119
|
}
|
|
112
120
|
if (toSave.length > 0)
|
|
113
|
-
saveChunks(toSave);
|
|
121
|
+
savedChunkIds = saveChunks(toSave);
|
|
114
122
|
chunkCount = toSave.length;
|
|
115
123
|
}
|
|
116
124
|
}
|
|
117
125
|
catch (err) {
|
|
118
126
|
console.error("[claw-memory] chunk embed failed:", err);
|
|
119
127
|
}
|
|
128
|
+
// --- Lesson layer: extract reusable lessons (best-effort) ---
|
|
129
|
+
// Candidates ride along in the summary JSON above (no extra LLM call). When
|
|
130
|
+
// CLAW_MEMORY_LESSON_DEDICATED=1, run a separate higher-quality extraction
|
|
131
|
+
// pass instead. Failures here must never break the summary path.
|
|
132
|
+
let lessonCount = 0;
|
|
133
|
+
try {
|
|
134
|
+
const candidates = dedicatedEnabled()
|
|
135
|
+
? await extractDedicated(transcript)
|
|
136
|
+
: Array.isArray(result.lessons)
|
|
137
|
+
? result.lessons
|
|
138
|
+
: [];
|
|
139
|
+
if (candidates.length > 0) {
|
|
140
|
+
const ids = await saveCandidates({
|
|
141
|
+
projectId: input.projectId,
|
|
142
|
+
sessionId: input.sessionId,
|
|
143
|
+
candidates,
|
|
144
|
+
sourceChunkIds: savedChunkIds,
|
|
145
|
+
});
|
|
146
|
+
lessonCount = ids.length;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
console.error("[claw-memory] lesson extract failed:", err);
|
|
151
|
+
}
|
|
120
152
|
log("distill", {
|
|
121
153
|
projectId: input.projectId,
|
|
122
154
|
sessionId: input.sessionId,
|
|
123
155
|
obsType,
|
|
124
156
|
chunks: chunkCount,
|
|
125
157
|
preferences: result.preferences?.length ?? 0,
|
|
158
|
+
lessons: lessonCount,
|
|
126
159
|
});
|
|
127
160
|
return {
|
|
128
161
|
summary: result.summary,
|
|
129
162
|
preferencesCount: result.preferences?.length ?? 0,
|
|
130
163
|
chunks: chunkCount,
|
|
164
|
+
lessons: lessonCount,
|
|
131
165
|
};
|
|
132
166
|
}
|
|
133
167
|
/** Store a single free-text note as an embedded chunk (memory_remember). */
|