@hir4ta/mneme 0.20.2 → 0.22.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.
Files changed (51) hide show
  1. package/.claude-plugin/plugin.json +2 -5
  2. package/README.ja.md +45 -283
  3. package/README.md +48 -280
  4. package/dist/lib/db.js +7 -5
  5. package/dist/lib/incremental-save.js +122 -28
  6. package/dist/lib/prompt-search.js +570 -0
  7. package/dist/lib/search-core.js +516 -0
  8. package/dist/lib/session-finalize.js +983 -0
  9. package/dist/lib/session-init.js +397 -0
  10. package/dist/lib/suppress-sqlite-warning.js +8 -0
  11. package/dist/public/assets/index-Bvl_IrPy.css +1 -0
  12. package/dist/public/assets/index-k5JYSPV6.js +351 -0
  13. package/dist/public/assets/{react-force-graph-2d-CGnpkwRw.js → react-force-graph-2d-Dlcfvz01.js} +1 -1
  14. package/dist/public/index.html +2 -2
  15. package/dist/server.js +565 -37
  16. package/dist/servers/db-server.js +1301 -98
  17. package/dist/servers/search-server.js +613 -333
  18. package/hooks/hooks.json +1 -0
  19. package/hooks/lib/common.sh +55 -0
  20. package/hooks/post-tool-use.sh +52 -58
  21. package/hooks/pre-compact.sh +30 -42
  22. package/hooks/session-end.sh +30 -142
  23. package/hooks/session-start.sh +32 -337
  24. package/hooks/stop.sh +31 -42
  25. package/hooks/user-prompt-submit.sh +58 -212
  26. package/package.json +10 -3
  27. package/scripts/export-weekly-knowledge-html.ts +906 -0
  28. package/scripts/search-benchmark.queries.json +78 -0
  29. package/scripts/search-benchmark.ts +120 -0
  30. package/scripts/validate-source-artifacts.mjs +378 -0
  31. package/servers/db-server.ts +995 -65
  32. package/servers/search-server.ts +117 -528
  33. package/skills/harvest/SKILL.md +78 -0
  34. package/skills/init-mneme/{skill.md → SKILL.md} +7 -1
  35. package/skills/resume/{skill.md → SKILL.md} +24 -9
  36. package/skills/save/SKILL.md +131 -0
  37. package/skills/search/SKILL.md +76 -0
  38. package/skills/using-mneme/SKILL.md +38 -0
  39. package/tsconfig.tsbuildinfo +1 -0
  40. package/dist/public/assets/index-CeHiZXwl.js +0 -345
  41. package/dist/public/assets/index-t_srr1OD.css +0 -1
  42. package/learn_claude_code/figma_exports/claude_code_map.svg +0 -107
  43. package/learn_claude_code/figma_exports/claude_code_whiteboard.excalidraw +0 -2578
  44. package/skills/AGENTS.override.md +0 -5
  45. package/skills/harvest/skill.md +0 -295
  46. package/skills/plan/skill.md +0 -422
  47. package/skills/report/skill.md +0 -74
  48. package/skills/review/skill.md +0 -419
  49. package/skills/save/skill.md +0 -496
  50. package/skills/search/skill.md +0 -175
  51. package/skills/using-mneme/skill.md +0 -185
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # mneme
2
2
 
3
+ ![Version](https://img.shields.io/badge/version-0.22.0-blue)
4
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D22.5.0-brightgreen)
3
5
  [![NPM Version](https://img.shields.io/npm/v/%40hir4ta%2Fmneme)](https://www.npmjs.com/package/@hir4ta/mneme)
4
6
  [![MIT License](https://img.shields.io/npm/l/%40hir4ta%2Fmneme)](https://github.com/hir4ta/mneme/blob/main/LICENSE)
5
7
 
@@ -9,41 +11,26 @@ Provides automatic session saving, intelligent memory search, and web dashboard
9
11
 
10
12
  ## Features
11
13
 
12
- ### Core Features
13
14
  - **Incremental save**: Save only new interactions on each turn completion (Node.js, fast)
14
15
  - **Auto memory search**: Related past sessions/decisions automatically injected on each prompt
15
16
  - **PreCompact support**: Catch up unsaved interactions before Auto-Compact (context 95% full)
16
17
  - **Full data extraction**: Save summary, decisions, patterns, and rules with `/mneme:save`
17
- - **Memory-informed planning**: Design and plan with past knowledge via `/mneme:plan`
18
18
  - **Session Resume**: Resume past sessions with `/mneme:resume` (with chain tracking)
19
19
  - **Session Suggestion**: Recent 3 sessions shown at session start
20
- - **Rule-based Review**: Code review based on `dev-rules.json` / `review-guidelines.json`
21
- - **GitHub PR Review**: Review GitHub PRs with `/mneme:review <PR URL>`
22
- - **Knowledge Harvesting**: Extract rules and patterns from PR comments with `/mneme:harvest`
23
- - **Weekly Reports**: Auto-generate Markdown reports aggregating review results
24
- - **Web Dashboard**: View sessions, decisions, patterns, and rules
20
+ - **Knowledge Harvesting**: Extract decision/pattern/rule sources from PR comments with `/mneme:harvest`
21
+ - **Web Dashboard**: View sessions, source artifacts, and development rules
22
+ - **Development Rules + Approval**: Generate rules from decisions/patterns/rules and approve/reject inline
23
+ - **Knowledge Graph Layer**: Visualize sessions and approved development rules as one graph
25
24
 
26
25
  ## Problems Solved
27
26
 
28
- ### Common Issues in Claude Code Development
27
+ Claude Code sessions lose context on exit or Auto-Compact, making past decisions untraceable and knowledge hard to reuse.
29
28
 
30
- - **Context Loss**: Conversation context is lost on session end or Auto-Compact
31
- - **Opaque Decisions**: "Why did we choose this design?" becomes untraceable
32
- - **Repeated Mistakes**: Same errors solved multiple times without learning
33
- - **Hard to Reuse Knowledge**: Past interactions and decisions are hard to search
29
+ **Common Issues**: Context loss across sessions, repeated mistakes, opaque design decisions
34
30
 
35
- ### What mneme Enables
31
+ **What mneme Enables**: Auto-save with resume, automatic memory search on every prompt, searchable decision/pattern history
36
32
 
37
- - **Auto-save + Resume** enables context continuity across sessions
38
- - **Auto memory search** brings relevant past knowledge to every conversation
39
- - **Decision & Pattern Recording** tracks reasoning and error solutions
40
- - **Search and Dashboard** for quick access to past records
41
- - **Review Feature** for repository-specific code review
42
-
43
- ### Team Benefits
44
-
45
- - `.mneme/` JSON files are **Git-manageable**, enabling team sharing of decisions and session history
46
- - Quickly understand background and context during onboarding or reviews
33
+ **Team Benefits**: `.mneme/` JSON files are Git-managed, enabling team sharing of decisions and session history.
47
34
 
48
35
  ## Installation
49
36
 
@@ -119,62 +106,29 @@ This will auto-update on Claude Code startup.
119
106
 
120
107
  ## Usage
121
108
 
122
- ### Incremental Save
123
-
124
- **Conversation logs are auto-saved on each turn completion** (Node.js streaming). No configuration needed.
125
-
126
- - **Stop hook**: Saves only new interactions on each assistant response completion
127
- - **PreCompact hook**: Catches up unsaved interactions before Auto-Compact
128
- - **SessionEnd hook**: Lightweight cleanup only (no heavy processing)
129
-
130
- **If you don't run `/mneme:save`, conversation history is deleted at session end** (prevents garbage data).
131
-
132
- ### Auto Memory Search
133
-
134
- **On every prompt**, mneme automatically:
135
- 1. Extracts keywords from your message
136
- 2. Searches sessions/decisions/patterns
137
- 3. Injects relevant context to Claude
138
-
139
- This means past knowledge is always available without manual lookup.
140
-
141
- ### Session Suggestion
142
-
143
- At session start, recent 3 sessions are shown:
144
-
145
- ```
146
- **Recent sessions:**
147
- 1. [abc123] JWT authentication implementation (2026-01-27, main)
148
- 2. [def456] Dashboard UI improvements (2026-01-26, main)
149
- 3. [ghi789] Bug fixes (2026-01-25, main)
150
-
151
- Continue from a previous session? Use `/mneme:resume <id>`
152
- ```
153
-
154
109
  ### Commands
155
110
 
156
- | Command | Description |
157
- |---------|-------------|
158
- | `/init-mneme` | Initialize mneme in current project |
159
- | `/mneme:save` | Extract all data: summary, decisions, patterns, rules |
160
- | `/mneme:plan [topic]` | Memory-informed design + Socratic questions + task breakdown |
161
- | `/mneme:resume [id]` | Resume session (show list if ID omitted) |
162
- | `/mneme:search "query"` | Search sessions, decisions, and patterns |
163
- | `/mneme:review [--staged\|--all\|--diff=branch\|--full]` | Rule-based code review |
164
- | `/mneme:review <PR URL>` | Review GitHub PR |
165
- | `/mneme:harvest <PR URL>` | Extract knowledge from PR review comments |
166
- | `/mneme:report [--from YYYY-MM-DD --to YYYY-MM-DD]` | Weekly review report |
111
+ | Command | Description |
112
+ | ------------------------ | ----------------------------------------------------- |
113
+ | `/init-mneme` | Initialize mneme in current project |
114
+ | `/mneme:save` | Extract all data: summary, decisions, patterns, rules |
115
+ | `/mneme:resume [id]` | Resume session (show list if ID omitted) |
116
+ | `/mneme:search "query"` | Search sessions and approved development rules |
117
+ | `/mneme:harvest <PR URL>`| Extract knowledge from PR review comments |
167
118
 
168
119
  ### Recommended Workflow
169
120
 
170
121
  ```
171
- plan → implement → save → review
122
+ implement → save → approve rules
172
123
  ```
173
124
 
174
- 1. **plan**: Design with memory lookup + Socratic questions + task breakdown
175
- 2. **implement**: Follow the plan
176
- 3. **save**: Extract decisions, patterns, rules
177
- 4. **review**: Verify against plan and code quality
125
+ 1. **implement**: Write code
126
+ 2. **save**: Extract source knowledge and generate development rule candidates
127
+ 3. **validate**: Run `npm run validate:sources` to enforce required fields/priority/tags
128
+ 4. **approve rules**: Review and approve/reject generated development rules inline
129
+
130
+ Detailed runtime flow (hooks, uncommitted policy, auto-compact path):
131
+ - `docs/mneme-runtime-flow.md`
178
132
 
179
133
  ### Dashboard
180
134
 
@@ -195,9 +149,7 @@ npx @hir4ta/mneme --dashboard --port 8080
195
149
  #### Screens
196
150
 
197
151
  - **Sessions**: List and view sessions
198
- - **Decisions**: List and view technical decisions
199
- - **Rules**: View dev rules and review guidelines
200
- - **Patterns**: View learned patterns (good patterns, anti-patterns, error solutions)
152
+ - **Development Rules**: Review and approve rules generated from decisions/patterns/rules
201
153
  - **Statistics**: View activity charts and session statistics
202
154
  - **Graph**: Visualize session connections by shared tags
203
155
 
@@ -205,227 +157,43 @@ npx @hir4ta/mneme --dashboard --port 8080
205
157
 
206
158
  The dashboard supports English and Japanese. Click the language toggle (EN/JA) in the header to switch. The preference is saved to localStorage.
207
159
 
208
- ### MCP Tools
209
-
210
- mneme provides MCP servers with search and database tools callable directly from Claude Code:
211
-
212
- | Server | Tool | Description |
213
- |--------|------|-------------|
214
- | mneme-search | `mneme_search` | Unified search (FTS5, tag alias resolution) |
215
- | mneme-search | `mneme_get_session` | Get session details |
216
- | mneme-search | `mneme_get_decision` | Get decision details |
217
- | mneme-db | `mneme_list_projects` | List all projects |
218
- | mneme-db | `mneme_cross_project_search` | Cross-project search |
219
-
220
- ### Subagents
221
-
222
- | Agent | Description |
223
- |-------|-------------|
224
- | `mneme-reviewer` | Rule-based code review (isolated context) |
225
-
226
- ## How It Works
227
-
228
- ```mermaid
229
- flowchart TB
230
- subgraph incremental [Incremental Save]
231
- A[Each Turn] --> B[Stop Hook]
232
- B --> C[Node.js streaming]
233
- C --> D[Save only new interactions]
234
- end
235
-
236
- subgraph autosearch [Auto Memory Search]
237
- E[User Prompt] --> F[UserPromptSubmit Hook]
238
- F --> G[Search sessions/decisions/patterns]
239
- G --> H[Inject relevant context]
240
- end
241
-
242
- subgraph precompact [PreCompact Catch-up]
243
- I[Context 95% Full] --> J[PreCompact Hook]
244
- J --> K[Catch up missed interactions]
245
- end
246
-
247
- subgraph sessionend [Session End]
248
- L[Exit] --> M[SessionEnd Hook]
249
- M --> N{Committed?}
250
- N -->|Yes| O[Keep interactions]
251
- N -->|No| P[Delete interactions]
252
- end
253
-
254
- subgraph manual [Manual Actions]
255
- Q["mneme:save"] --> R[Extract decisions + patterns + rules]
256
- R --> S[Mark session committed]
257
- T["mneme:plan"] --> U[Memory-informed design + tasks]
258
- end
259
-
260
- subgraph resume [Session Resume]
261
- V["mneme:resume"] --> W[Select from list]
262
- W --> X[Restore past context + set resumedFrom]
263
- end
264
-
265
- subgraph review [Review]
266
- Y["mneme:review"] --> Z[Rule-based findings]
267
- Z --> AA[Save review results]
268
- end
269
-
270
- subgraph dashboard [Dashboard]
271
- AB["npx @hir4ta/mneme -d"] --> AC[Open in browser]
272
- AC --> AD[View all data]
273
- end
274
-
275
- D --> Q
276
- H --> Q
277
- S --> AB
278
- AA --> AB
279
- ```
160
+ ### Weekly Knowledge HTML Export
280
161
 
281
- ## Data Storage
162
+ Generate a shareable HTML snapshot for the last 7 days of knowledge activity:
282
163
 
283
- mneme uses a **hybrid storage** approach for privacy and collaboration:
284
-
285
- | Storage | Location | Purpose | Sharing |
286
- |---------|----------|---------|---------|
287
- | **JSON** | `.mneme/` | Summaries, decisions, patterns, rules | Git-managed (team shared) |
288
- | **SQLite** | `.mneme/local.db` | Interactions, backups | Local only (gitignored) |
289
-
290
- **Why hybrid?**
291
- - **Privacy**: Conversation history (interactions) stays local (gitignored)
292
- - **Lightweight**: JSON files reduced from 100KB+ to ~5KB (interactions excluded)
293
- - **Future-ready**: Embeddings table prepared for semantic search
294
-
295
- ### Directory Structure
296
-
297
- **Project-local (`.mneme/`)**:
298
- ```text
299
- .mneme/
300
- ├── local.db # SQLite with interactions (gitignored)
301
- ├── tags.json # Tag master file (93 tags, prevents notation variations)
302
- ├── sessions/ # Session metadata (YYYY/MM) - Git-managed
303
- │ └── YYYY/MM/
304
- │ └── {id}.json # Metadata only (interactions in local.db)
305
- ├── decisions/ # Technical decisions (from /save) - Git-managed
306
- │ └── YYYY/MM/
307
- │ └── {id}.json
308
- ├── patterns/ # Error patterns (from /save) - Git-managed
309
- │ └── {user}.json
310
- ├── rules/ # Dev rules / review guidelines - Git-managed
311
- ├── reviews/ # Review results (YYYY/MM) - Git-managed
312
- └── reports/ # Weekly reports (YYYY-MM) - Git-managed
164
+ ```bash
165
+ npm run export:weekly-html
313
166
  ```
314
167
 
315
- The `local.db` file is automatically added to `.mneme/.gitignore` to keep conversations private.
316
-
317
- ### Session JSON Schema
318
-
319
- Session metadata is stored in JSON (interactions are stored in SQLite for privacy):
320
-
321
- ```json
322
- {
323
- "id": "abc12345",
324
- "sessionId": "full-uuid-from-claude-code",
325
- "createdAt": "2026-01-27T10:00:00Z",
326
- "endedAt": "2026-01-27T12:00:00Z",
327
- "title": "JWT authentication implementation",
328
- "tags": ["auth", "jwt"],
329
- "context": {
330
- "branch": "feature/auth",
331
- "projectDir": "/path/to/project",
332
- "user": { "name": "tanaka", "email": "tanaka@example.com" }
333
- },
334
- "metrics": {
335
- "userMessages": 5,
336
- "assistantResponses": 5,
337
- "thinkingBlocks": 5,
338
- "toolUsage": [{"name": "Edit", "count": 3}, {"name": "Write", "count": 2}]
339
- },
340
- "files": [
341
- { "path": "src/auth/jwt.ts", "action": "create" }
342
- ],
343
- "resumedFrom": "def45678",
344
- "status": "complete",
345
-
346
- "summary": {
347
- "title": "JWT authentication implementation",
348
- "goal": "Implement JWT-based auth with refresh token support",
349
- "outcome": "success",
350
- "description": "Implemented JWT auth with RS256 signing",
351
- "sessionType": "implementation"
352
- },
353
-
354
- "plan": {
355
- "tasks": ["[x] JWT signing method selection", "[x] Middleware implementation", "[ ] Add tests"],
356
- "remaining": ["Add tests"]
357
- },
358
-
359
- "discussions": [
360
- {
361
- "topic": "Signing algorithm",
362
- "decision": "Adopt RS256",
363
- "reasoning": "Security considerations for production",
364
- "alternatives": ["HS256 (simpler but requires shared secret)"]
365
- }
366
- ],
367
-
368
- "errors": [
369
- {
370
- "error": "secretOrPrivateKey must be asymmetric",
371
- "cause": "Using HS256 secret with RS256",
372
- "solution": "Generate RS256 key pair"
373
- }
374
- ],
375
-
376
- "handoff": {
377
- "stoppedReason": "Test creation postponed to next session",
378
- "notes": ["vitest configured", "Mock key pair in test/fixtures/"],
379
- "nextSteps": ["Create jwt.test.ts", "Add E2E tests"]
380
- },
381
-
382
- "references": [
383
- { "url": "https://jwt.io/introduction", "title": "JWT Introduction" }
384
- ]
385
- }
386
- ```
168
+ Output:
169
+ - `.mneme/exports/weekly-knowledge-YYYY-MM-DD.html`
387
170
 
388
- ### Session Types
171
+ ## Data Storage
389
172
 
390
- The `sessionType` field classifies the session type.
173
+ mneme uses a **hybrid storage** approach: JSON files are Git-managed for team sharing, while SQLite keeps conversations private (gitignored).
391
174
 
392
- | Type | Description |
393
- |------|-------------|
394
- | `decision` | Decision cycle present (design choices, tech selection) |
395
- | `implementation` | Code changes made |
396
- | `research` | Research, learning, catchup |
397
- | `exploration` | Codebase exploration |
398
- | `discussion` | Discussion, consultation only |
399
- | `debug` | Debugging, investigation |
400
- | `review` | Code review |
175
+ | Storage | Location | Purpose | Sharing |
176
+ | ---------- | ----------------- | ------------------------------------- | ------------------------- |
177
+ | **JSON** | `.mneme/` | Summaries, decisions, patterns, rules | Git-managed (team shared) |
178
+ | **SQLite** | `.mneme/local.db` | Interactions, backups | Local only (gitignored) |
401
179
 
402
- ### Tags
180
+ Conversations are auto-saved on each turn. No configuration needed.
403
181
 
404
- Tags are selected from `.mneme/tags.json` to prevent notation variations (e.g., "フロント" "frontend"). The master file contains 93 tags across 11 categories:
182
+ Auto memory search runs on every prompt: keywords are extracted, past sessions/development rules are searched, and relevant context is injected automatically.
405
183
 
406
- - **domain**: frontend, backend, api, db, infra, mobile, cli
407
- - **phase**: feature, bugfix, refactor, test, docs
408
- - **ai**: llm, ai-agent, mcp, rag, vector-db, embedding
409
- - **cloud**: serverless, microservices, edge, wasm
410
- - And more...
184
+ At session start, the 3 most recent sessions are shown so you can quickly resume with `/mneme:resume <id>`.
411
185
 
412
186
  ## Security and Privacy
413
187
 
414
188
  mneme operates **entirely locally** with no data sent to external servers.
415
189
 
416
- | Item | Description |
417
- |------|-------------|
418
- | **External Communication** | None - no curl/fetch/HTTP requests are made |
419
- | **Data Storage** | All data stored in project's `.mneme/` directory |
420
- | **Conversation History** | Stored in `local.db`, automatically gitignored (not shared via Git) |
421
- | **Tools Used** | bash, Node.js, jq, sqlite3 (no external dependencies) |
422
- | **Code** | Open source - all code is auditable |
423
-
424
- ### Privacy by Design
425
-
426
- - **Conversations (interactions) are local-only**: Stored in SQLite (`local.db`), auto-added to `.gitignore`
427
- - **Only metadata is Git-shareable**: Session summaries, decisions, patterns can be shared with team via JSON
428
- - **No telemetry**: No usage tracking or external data transmission
190
+ | Item | Description |
191
+ | -------------------------- | ------------------------------------------------------------------- |
192
+ | **External Communication** | None - no curl/fetch/HTTP requests are made |
193
+ | **Data Storage** | All data stored in project's `.mneme/` directory |
194
+ | **Conversation History** | Stored in `local.db`, automatically gitignored (not shared via Git) |
195
+ | **Tools Used** | bash, Node.js, jq, sqlite3 (no external dependencies) |
196
+ | **Code** | Open source - all code is auditable |
429
197
 
430
198
  ## License
431
199
 
package/dist/lib/db.js CHANGED
@@ -1,8 +1,4 @@
1
- // lib/db.ts
2
- import { execSync } from "node:child_process";
3
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
5
- import { fileURLToPath } from "node:url";
1
+ // lib/suppress-sqlite-warning.ts
6
2
  var originalEmit = process.emit;
7
3
  process.emit = (event, ...args) => {
8
4
  if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
@@ -10,6 +6,12 @@ process.emit = (event, ...args) => {
10
6
  }
11
7
  return originalEmit.apply(process, [event, ...args]);
12
8
  };
9
+
10
+ // lib/db.ts
11
+ import { execSync } from "node:child_process";
12
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
13
15
  var { DatabaseSync } = await import("node:sqlite");
14
16
  var __filename = fileURLToPath(import.meta.url);
15
17
  var __dirname = dirname(__filename);
@@ -1,10 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // lib/incremental-save.ts
4
- import * as fs from "node:fs";
5
- import * as os from "node:os";
6
- import * as path from "node:path";
7
- import * as readline from "node:readline";
3
+ // lib/suppress-sqlite-warning.ts
8
4
  var originalEmit = process.emit;
9
5
  process.emit = (event, ...args) => {
10
6
  if (event === "warning" && typeof args[0] === "object" && args[0] !== null && "name" in args[0] && args[0].name === "ExperimentalWarning" && "message" in args[0] && typeof args[0].message === "string" && args[0].message.includes("SQLite")) {
@@ -12,6 +8,12 @@ process.emit = (event, ...args) => {
12
8
  }
13
9
  return originalEmit.apply(process, [event, ...args]);
14
10
  };
11
+
12
+ // lib/incremental-save.ts
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import * as readline from "node:readline";
15
17
  var { DatabaseSync } = await import("node:sqlite");
16
18
  function getSchemaPath() {
17
19
  const scriptDir = path.dirname(new URL(import.meta.url).pathname);
@@ -414,6 +416,32 @@ function resolveMnemeSessionId(projectPath, claudeSessionId) {
414
416
  }
415
417
  return shortId;
416
418
  }
419
+ function findSessionFileById(projectPath, mnemeSessionId) {
420
+ const sessionsDir = path.join(projectPath, ".mneme", "sessions");
421
+ const searchDir = (dir) => {
422
+ if (!fs.existsSync(dir)) return null;
423
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
424
+ const fullPath = path.join(dir, entry.name);
425
+ if (entry.isDirectory()) {
426
+ const result = searchDir(fullPath);
427
+ if (result) return result;
428
+ } else if (entry.name === `${mnemeSessionId}.json`) {
429
+ return fullPath;
430
+ }
431
+ }
432
+ return null;
433
+ };
434
+ return searchDir(sessionsDir);
435
+ }
436
+ function hasSessionSummary(sessionFile) {
437
+ if (!sessionFile) return false;
438
+ try {
439
+ const session = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
440
+ return !!session.summary;
441
+ } catch {
442
+ return false;
443
+ }
444
+ }
417
445
  async function incrementalSave(claudeSessionId, transcriptPath, projectPath) {
418
446
  if (!claudeSessionId || !transcriptPath || !projectPath) {
419
447
  return {
@@ -572,29 +600,8 @@ function cleanupUncommittedSession(claudeSessionId, projectPath) {
572
600
  return { deleted: false, count: 0 };
573
601
  }
574
602
  const mnemeSessionId = resolveMnemeSessionId(projectPath, claudeSessionId);
575
- const sessionsDir = path.join(projectPath, ".mneme", "sessions");
576
- let hasSummary = false;
577
- const searchDir = (dir) => {
578
- if (!fs.existsSync(dir)) return null;
579
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
580
- if (entry.isDirectory()) {
581
- const result = searchDir(path.join(dir, entry.name));
582
- if (result) return result;
583
- } else if (entry.name === `${mnemeSessionId}.json`) {
584
- return path.join(dir, entry.name);
585
- }
586
- }
587
- return null;
588
- };
589
- const sessionFile = searchDir(sessionsDir);
590
- if (sessionFile) {
591
- try {
592
- const session = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
593
- hasSummary = !!session.summary;
594
- } catch {
595
- }
596
- }
597
- if (hasSummary) {
603
+ const sessionFile = findSessionFileById(projectPath, mnemeSessionId);
604
+ if (hasSessionSummary(sessionFile)) {
598
605
  return { deleted: false, count: 0 };
599
606
  }
600
607
  const countStmt = db.prepare(
@@ -620,6 +627,80 @@ function cleanupUncommittedSession(claudeSessionId, projectPath) {
620
627
  db.close();
621
628
  }
622
629
  }
630
+ function cleanupStaleUncommittedSessions(projectPath, graceDays) {
631
+ const dbPath = path.join(projectPath, ".mneme", "local.db");
632
+ if (!fs.existsSync(dbPath)) {
633
+ return { deletedSessions: 0, deletedInteractions: 0 };
634
+ }
635
+ const db = new DatabaseSync(dbPath);
636
+ let deletedSessions = 0;
637
+ let deletedInteractions = 0;
638
+ const normalizedGraceDays = Math.max(1, Math.floor(graceDays));
639
+ try {
640
+ const staleStmt = db.prepare(
641
+ `
642
+ SELECT claude_session_id, mneme_session_id
643
+ FROM session_save_state
644
+ WHERE is_committed = 0
645
+ AND updated_at <= datetime('now', ?)
646
+ `
647
+ );
648
+ const staleRows = staleStmt.all(`-${normalizedGraceDays} days`);
649
+ if (staleRows.length === 0) {
650
+ return { deletedSessions: 0, deletedInteractions: 0 };
651
+ }
652
+ const deleteInteractionStmt = db.prepare(
653
+ "DELETE FROM interactions WHERE claude_session_id = ?"
654
+ );
655
+ const countInteractionStmt = db.prepare(
656
+ "SELECT COUNT(*) as count FROM interactions WHERE claude_session_id = ?"
657
+ );
658
+ const deleteStateStmt = db.prepare(
659
+ "DELETE FROM session_save_state WHERE claude_session_id = ?"
660
+ );
661
+ for (const row of staleRows) {
662
+ const sessionFile = findSessionFileById(
663
+ projectPath,
664
+ row.mneme_session_id
665
+ );
666
+ if (hasSessionSummary(sessionFile)) {
667
+ continue;
668
+ }
669
+ const countResult = countInteractionStmt.get(row.claude_session_id);
670
+ const count = countResult?.count || 0;
671
+ if (count > 0) {
672
+ deleteInteractionStmt.run(row.claude_session_id);
673
+ deletedInteractions += count;
674
+ }
675
+ deleteStateStmt.run(row.claude_session_id);
676
+ if (sessionFile && fs.existsSync(sessionFile)) {
677
+ try {
678
+ fs.unlinkSync(sessionFile);
679
+ deletedSessions += 1;
680
+ } catch {
681
+ }
682
+ }
683
+ const linkPath = path.join(
684
+ projectPath,
685
+ ".mneme",
686
+ "session-links",
687
+ `${row.claude_session_id.slice(0, 8)}.json`
688
+ );
689
+ if (fs.existsSync(linkPath)) {
690
+ try {
691
+ fs.unlinkSync(linkPath);
692
+ } catch {
693
+ }
694
+ }
695
+ }
696
+ return { deletedSessions, deletedInteractions };
697
+ } catch (error) {
698
+ console.error(`[mneme] Error cleaning stale sessions: ${error}`);
699
+ return { deletedSessions: 0, deletedInteractions: 0 };
700
+ } finally {
701
+ db.close();
702
+ }
703
+ }
623
704
  async function main() {
624
705
  const args = process.argv.slice(2);
625
706
  const getArg = (name) => {
@@ -668,6 +749,18 @@ async function main() {
668
749
  const result = cleanupUncommittedSession(sessionId, projectPath);
669
750
  console.log(JSON.stringify(result));
670
751
  process.exit(0);
752
+ } else if (command === "cleanup-stale") {
753
+ const projectPath = getArg("project");
754
+ const graceDays = Number.parseInt(getArg("grace-days") || "7", 10);
755
+ if (!projectPath) {
756
+ console.error(
757
+ "Usage: incremental-save.js cleanup-stale --project <path> [--grace-days <n>]"
758
+ );
759
+ process.exit(1);
760
+ }
761
+ const result = cleanupStaleUncommittedSessions(projectPath, graceDays);
762
+ console.log(JSON.stringify(result));
763
+ process.exit(0);
671
764
  } else {
672
765
  console.error("Commands: save, commit, cleanup");
673
766
  process.exit(1);
@@ -677,6 +770,7 @@ if (import.meta.url === `file://${process.argv[1]}`) {
677
770
  main();
678
771
  }
679
772
  export {
773
+ cleanupStaleUncommittedSessions,
680
774
  cleanupUncommittedSession,
681
775
  incrementalSave,
682
776
  markSessionCommitted