@pcircle/memesh 2.11.0 → 3.0.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 (74) hide show
  1. package/README.md +153 -52
  2. package/dist/cli/assets/d3.v7.min.js +2 -0
  3. package/dist/cli/view.d.ts +1 -0
  4. package/dist/cli/view.d.ts.map +1 -1
  5. package/dist/cli/view.js +2198 -10
  6. package/dist/cli/view.js.map +1 -1
  7. package/dist/core/config.d.ts +33 -0
  8. package/dist/core/config.d.ts.map +1 -0
  9. package/dist/core/config.js +70 -0
  10. package/dist/core/config.js.map +1 -0
  11. package/dist/core/extractor.d.ts +26 -0
  12. package/dist/core/extractor.d.ts.map +1 -0
  13. package/dist/core/extractor.js +106 -0
  14. package/dist/core/extractor.js.map +1 -0
  15. package/dist/core/lifecycle.d.ts +9 -0
  16. package/dist/core/lifecycle.d.ts.map +1 -0
  17. package/dist/core/lifecycle.js +51 -0
  18. package/dist/core/lifecycle.js.map +1 -0
  19. package/dist/core/operations.d.ts +9 -0
  20. package/dist/core/operations.d.ts.map +1 -0
  21. package/dist/core/operations.js +342 -0
  22. package/dist/core/operations.js.map +1 -0
  23. package/dist/core/query-expander.d.ts +4 -0
  24. package/dist/core/query-expander.d.ts.map +1 -0
  25. package/dist/core/query-expander.js +114 -0
  26. package/dist/core/query-expander.js.map +1 -0
  27. package/dist/core/schema-export.d.ts +2 -0
  28. package/dist/core/schema-export.d.ts.map +1 -0
  29. package/dist/core/schema-export.js +67 -0
  30. package/dist/core/schema-export.js.map +1 -0
  31. package/dist/core/scoring.d.ts +25 -0
  32. package/dist/core/scoring.d.ts.map +1 -0
  33. package/dist/core/scoring.js +40 -0
  34. package/dist/core/scoring.js.map +1 -0
  35. package/dist/core/types.d.ts +124 -0
  36. package/dist/core/types.d.ts.map +1 -0
  37. package/dist/core/types.js +2 -0
  38. package/dist/core/types.js.map +1 -0
  39. package/dist/core/version-check.d.ts +10 -0
  40. package/dist/core/version-check.d.ts.map +1 -0
  41. package/dist/core/version-check.js +44 -0
  42. package/dist/core/version-check.js.map +1 -0
  43. package/dist/db.d.ts.map +1 -1
  44. package/dist/db.js +33 -0
  45. package/dist/db.js.map +1 -1
  46. package/dist/knowledge-graph.d.ts +16 -30
  47. package/dist/knowledge-graph.d.ts.map +1 -1
  48. package/dist/knowledge-graph.js +205 -24
  49. package/dist/knowledge-graph.js.map +1 -1
  50. package/dist/mcp/server.js +8 -1
  51. package/dist/mcp/server.js.map +1 -1
  52. package/dist/mcp/tools.d.ts +1 -92
  53. package/dist/mcp/tools.d.ts.map +1 -1
  54. package/dist/mcp/tools.js +1 -182
  55. package/dist/mcp/tools.js.map +1 -1
  56. package/dist/transports/cli/cli.d.ts +3 -0
  57. package/dist/transports/cli/cli.d.ts.map +1 -0
  58. package/dist/transports/cli/cli.js +378 -0
  59. package/dist/transports/cli/cli.js.map +1 -0
  60. package/dist/transports/http/server.d.ts +5 -0
  61. package/dist/transports/http/server.d.ts.map +1 -0
  62. package/dist/transports/http/server.js +314 -0
  63. package/dist/transports/http/server.js.map +1 -0
  64. package/dist/transports/mcp/handlers.d.ts +184 -0
  65. package/dist/transports/mcp/handlers.d.ts.map +1 -0
  66. package/dist/transports/mcp/handlers.js +271 -0
  67. package/dist/transports/mcp/handlers.js.map +1 -0
  68. package/hooks/hooks.json +24 -0
  69. package/package.json +27 -9
  70. package/plugin.json +4 -4
  71. package/scripts/hooks/post-commit.js +35 -6
  72. package/scripts/hooks/pre-compact.js +198 -0
  73. package/scripts/hooks/session-start.js +73 -25
  74. package/scripts/hooks/session-summary.js +255 -0
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createRequire } from 'module';
4
+ import { homedir } from 'os';
5
+ import { join, basename } from 'path';
6
+ import { existsSync, mkdirSync, readFileSync } from 'fs';
7
+
8
+ const require = createRequire(import.meta.url);
9
+
10
+ const SCHEMA_SQL = `
11
+ CREATE TABLE IF NOT EXISTS entities (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ name TEXT NOT NULL UNIQUE,
14
+ type TEXT NOT NULL,
15
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
16
+ metadata JSON
17
+ );
18
+
19
+ CREATE TABLE IF NOT EXISTS observations (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ entity_id INTEGER NOT NULL,
22
+ content TEXT NOT NULL,
23
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
24
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
25
+ );
26
+
27
+ CREATE TABLE IF NOT EXISTS relations (
28
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
29
+ from_entity_id INTEGER NOT NULL,
30
+ to_entity_id INTEGER NOT NULL,
31
+ relation_type TEXT NOT NULL,
32
+ metadata JSON,
33
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
34
+ FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
35
+ FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
36
+ UNIQUE(from_entity_id, to_entity_id, relation_type)
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS tags (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ entity_id INTEGER NOT NULL,
42
+ tag TEXT NOT NULL,
43
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
47
+ CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
48
+ DELETE FROM tags
49
+ WHERE id NOT IN (
50
+ SELECT MIN(id)
51
+ FROM tags
52
+ GROUP BY entity_id, tag
53
+ );
54
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
55
+ CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
56
+ CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
57
+ CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
58
+
59
+ CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
60
+ name, observations, content='',
61
+ tokenize='unicode61 remove_diacritics 1'
62
+ );
63
+ `;
64
+
65
+ // Timeout guard: always exit within 10 seconds
66
+ const TIMEOUT_MS = 10000;
67
+ const timeoutHandle = setTimeout(() => {
68
+ try { process.stderr.write('[memesh pre-compact] Timed out after 10s\n'); } catch {}
69
+ process.exit(0);
70
+ }, TIMEOUT_MS);
71
+ timeoutHandle.unref();
72
+
73
+ let input = '';
74
+ process.stdin.setEncoding('utf8');
75
+ process.stdin.on('data', (chunk) => { input += chunk; });
76
+ process.stdin.on('end', () => {
77
+ try {
78
+ // Opt-out check
79
+ if (process.env.MEMESH_AUTO_CAPTURE === 'false') {
80
+ return exit0();
81
+ }
82
+
83
+ const data = JSON.parse(input);
84
+ const sessionId = data.session_id || 'unknown';
85
+ const transcriptPath = data.transcript_path || '';
86
+ const cwd = data.cwd || process.cwd();
87
+ const reason = data.reason || 'auto';
88
+ const projectName = basename(cwd);
89
+
90
+ // Parse transcript to gather insights
91
+ let toolCallCount = 0;
92
+ const editedFiles = new Set();
93
+
94
+ if (transcriptPath && existsSync(transcriptPath)) {
95
+ try {
96
+ const lines = readFileSync(transcriptPath, 'utf8').split('\n');
97
+ for (const line of lines) {
98
+ if (!line.trim()) continue;
99
+ try {
100
+ const entry = JSON.parse(line);
101
+ // Count tool uses from assistant message content blocks
102
+ if (entry.role === 'assistant' && Array.isArray(entry.content)) {
103
+ for (const block of entry.content) {
104
+ if (block.type === 'tool_use') {
105
+ toolCallCount++;
106
+ // Track file edits
107
+ const name = block.name || '';
108
+ if (name === 'Edit' || name === 'Write' || name === 'MultiEdit') {
109
+ const filePath = block.input?.file_path || block.input?.path || '';
110
+ if (filePath) editedFiles.add(basename(filePath));
111
+ }
112
+ }
113
+ }
114
+ }
115
+ } catch {
116
+ // Skip malformed lines
117
+ }
118
+ }
119
+ } catch {
120
+ // Transcript read failed — proceed with zero counts
121
+ }
122
+ }
123
+
124
+ const insightCount = editedFiles.size + (toolCallCount > 0 ? 1 : 0);
125
+ const entityName = `pre-compact-${sessionId}`;
126
+
127
+ // Build observation content
128
+ const obsLines = [`Compaction reason: ${reason}`, `Tool calls: ${toolCallCount}`];
129
+ if (editedFiles.size > 0) {
130
+ obsLines.push(`Files edited: ${Array.from(editedFiles).join(', ')}`);
131
+ }
132
+
133
+ // Open (or create) database
134
+ const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
135
+ const dbDir = process.env.MEMESH_DB_PATH
136
+ ? join(process.env.MEMESH_DB_PATH, '..')
137
+ : join(homedir(), '.memesh');
138
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
139
+
140
+ const Database = require('better-sqlite3');
141
+ const db = new Database(dbPath);
142
+ try {
143
+ db.pragma('journal_mode = WAL');
144
+ db.pragma('foreign_keys = ON');
145
+ db.exec(SCHEMA_SQL);
146
+
147
+ // Upsert entity
148
+ const insertResult = db.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)').run(entityName, 'session-summary');
149
+ const isNew = insertResult.changes > 0;
150
+ const entity = db.prepare('SELECT id FROM entities WHERE name = ?').get(entityName);
151
+
152
+ if (entity) {
153
+ // Capture existing observations for FTS delete
154
+ const prevObs = isNew
155
+ ? []
156
+ : db.prepare('SELECT content FROM observations WHERE entity_id = ?').all(entity.id);
157
+ const prevObsText = isNew ? undefined : prevObs.map(o => o.content).join(' ');
158
+
159
+ // Insert each observation line
160
+ for (const line of obsLines) {
161
+ db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)').run(entity.id, line);
162
+ }
163
+
164
+ // Add tags
165
+ const tags = ['source:auto-capture', 'urgency:pre-compact', `project:${projectName}`];
166
+ for (const tag of tags) {
167
+ db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)').run(entity.id, tag);
168
+ }
169
+
170
+ // Update FTS
171
+ if (prevObsText !== undefined) {
172
+ db.prepare("INSERT INTO entities_fts(entities_fts, rowid, name, observations) VALUES('delete', ?, ?, ?)").run(entity.id, entityName, prevObsText);
173
+ }
174
+ const allObs = db.prepare('SELECT content FROM observations WHERE entity_id = ?').all(entity.id);
175
+ const allObsText = allObs.map(o => o.content).join(' ');
176
+ db.prepare('INSERT INTO entities_fts(rowid, name, observations) VALUES(?, ?, ?)').run(entity.id, entityName, allObsText);
177
+ }
178
+ } finally {
179
+ db.close();
180
+ }
181
+
182
+ const hookOutput = {
183
+ hookSpecificOutput: {
184
+ hookEventName: 'PreCompact',
185
+ additionalContext: `Saved ${insightCount} insights to MeMesh before compaction`,
186
+ },
187
+ };
188
+ console.log(JSON.stringify(hookOutput));
189
+ } catch (err) {
190
+ // Hooks must never crash Claude Code — exit cleanly
191
+ try { process.stderr.write(`[memesh pre-compact] ${err?.message || err}\n`); } catch {}
192
+ }
193
+ exit0();
194
+ });
195
+
196
+ function exit0() {
197
+ process.exit(0);
198
+ }
@@ -36,67 +36,115 @@ process.stdin.on('end', () => {
36
36
  return;
37
37
  }
38
38
 
39
- // Query project-specific recent entities with their observations
39
+ // Inspect available columns for backward compat
40
+ const columns = db.prepare("PRAGMA table_info(entities)").all();
41
+ const colNames = new Set(columns.map(col => col.name));
42
+
43
+ const hasStatus = colNames.has('status');
44
+ const hasScoringCols = colNames.has('access_count') && colNames.has('last_accessed_at') && colNames.has('confidence');
45
+
46
+ const statusFilter = hasStatus ? "AND e.status = 'active'" : '';
47
+ const recentStatusFilter = hasStatus ? "WHERE status = 'active'" : '';
48
+
49
+ // Configurable limit: how many top-N entities to load per section
50
+ const sessionLimit = parseInt(process.env.MEMESH_SESSION_LIMIT || '10', 10);
51
+
52
+ // Build scoring ORDER BY clause (or fallback to insertion order)
53
+ const scoringOrderBy = hasScoringCols
54
+ ? `ORDER BY
55
+ CASE WHEN e.confidence IS NULL THEN 0.5 ELSE e.confidence END * 0.4
56
+ + CASE WHEN e.access_count IS NULL THEN 0
57
+ ELSE MIN(CAST(e.access_count AS REAL) / 50.0, 1.0) END * 0.3
58
+ + CASE WHEN e.last_accessed_at IS NULL THEN 0.3
59
+ ELSE MIN(1.0, 1.0 / (1.0 + (julianday('now') - julianday(e.last_accessed_at)) / 30.0)) END * 0.3
60
+ DESC`
61
+ : 'ORDER BY e.id DESC';
62
+
63
+ const recentScoringOrderBy = hasScoringCols
64
+ ? `ORDER BY
65
+ CASE WHEN confidence IS NULL THEN 0.5 ELSE confidence END * 0.4
66
+ + CASE WHEN access_count IS NULL THEN 0
67
+ ELSE MIN(CAST(access_count AS REAL) / 50.0, 1.0) END * 0.3
68
+ + CASE WHEN last_accessed_at IS NULL THEN 0.3
69
+ ELSE MIN(1.0, 1.0 / (1.0 + (julianday('now') - julianday(last_accessed_at)) / 30.0)) END * 0.3
70
+ DESC`
71
+ : 'ORDER BY id DESC';
72
+
73
+ // Query project-specific top-N entities by relevance score
40
74
  const projectTag = `project:${projectName}`;
41
75
  const projectEntities = db.prepare(`
42
76
  SELECT DISTINCT e.id, e.name, e.type, e.created_at
43
77
  FROM entities e
44
78
  JOIN tags t ON t.entity_id = e.id
45
79
  WHERE t.tag = ?
46
- ORDER BY e.id DESC
47
- LIMIT 10
48
- `).all(projectTag);
80
+ ${statusFilter}
81
+ ${scoringOrderBy}
82
+ LIMIT ?
83
+ `).all(projectTag, sessionLimit);
49
84
 
50
- // Fetch observations for each entity (up to 3 per entity)
51
- const getObservations = db.prepare(
52
- 'SELECT content FROM observations WHERE entity_id = ? ORDER BY id DESC LIMIT 3'
85
+ // Fetch the first observation for each entity (for concise summary)
86
+ const getFirstObservation = db.prepare(
87
+ 'SELECT content FROM observations WHERE entity_id = ? ORDER BY id ASC LIMIT 1'
53
88
  );
54
89
 
55
- // Query global recent entities
90
+ // Query global recent/top entities (exclude project-tagged ones for this project)
56
91
  const recentEntities = db.prepare(`
57
92
  SELECT id, name, type, created_at
58
93
  FROM entities
59
- ORDER BY id DESC
94
+ ${recentStatusFilter}
95
+ ${recentScoringOrderBy}
60
96
  LIMIT 5
61
97
  `).all();
62
98
 
99
+ // Format entity as concise bullet: "• name (type): first observation (truncated)"
100
+ function formatEntity(entity) {
101
+ const obs = getFirstObservation.get(entity.id);
102
+ const snippet = obs ? obs.content.slice(0, 100) : '';
103
+ return snippet
104
+ ? `• ${entity.name} (${entity.type}): ${snippet}`
105
+ : `• ${entity.name} (${entity.type})`;
106
+ }
107
+
63
108
  // Build recall message
64
109
  const lines = [];
65
110
  if (projectEntities.length > 0) {
66
- lines.push(`Project "${projectName}" memories (${projectEntities.length}):`);
111
+ const label = hasScoringCols ? `top ${projectEntities.length} by relevance` : `${projectEntities.length}`;
112
+ lines.push(`Project "${projectName}" memories (${label}):`);
67
113
  for (const e of projectEntities) {
68
- lines.push(` - [${e.type}] ${e.name}`);
69
- const obs = getObservations.all(e.id);
70
- for (const o of obs) {
71
- lines.push(` ${o.content}`);
72
- }
114
+ lines.push(formatEntity(e));
73
115
  }
74
116
  }
75
117
  if (recentEntities.length > 0) {
76
- lines.push('');
118
+ if (lines.length > 0) lines.push('');
77
119
  lines.push('Recent memories:');
78
120
  for (const e of recentEntities) {
79
- lines.push(` - [${e.type}] ${e.name}`);
80
- const obs = getObservations.all(e.id);
81
- for (const o of obs) {
82
- lines.push(` ${o.content}`);
83
- }
121
+ lines.push(formatEntity(e));
84
122
  }
85
123
  }
124
+
125
+ // No memories at all — output nothing (don't clutter session)
86
126
  if (lines.length === 0) {
87
- lines.push('MeMesh: No memories found yet. Use remember tool to store knowledge.');
127
+ return;
88
128
  }
89
129
 
90
- output(lines.join('\n'));
130
+ const memorySummary = lines.join('\n');
131
+ const hookOutput = {
132
+ suppressOutput: true,
133
+ hookSpecificOutput: {
134
+ hookEventName: 'SessionStart',
135
+ additionalContext: memorySummary,
136
+ },
137
+ };
138
+ console.log(JSON.stringify(hookOutput));
91
139
  } finally {
92
140
  db.close();
93
141
  }
94
142
  } catch (err) {
95
143
  // Hooks must never crash Claude Code — but report honestly
96
- output(`MeMesh: Session start failed (${err?.message || 'unknown error'}). Memories not loaded.`);
144
+ console.log(JSON.stringify({ systemMessage: `MeMesh: Session start failed (${err?.message || 'unknown error'}). Memories not loaded.` }));
97
145
  }
98
146
  });
99
147
 
100
148
  function output(text) {
101
- console.log(JSON.stringify({ result: text }));
149
+ console.log(JSON.stringify({ systemMessage: text }));
102
150
  }
@@ -0,0 +1,255 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Session Auto-Capture — Stop hook
4
+ // Extracts knowledge from completed Claude Code sessions
5
+ // and stores as session-insight entities in MeMesh.
6
+
7
+ import { createRequire } from 'module';
8
+ import { homedir } from 'os';
9
+ import { join, basename } from 'path';
10
+ import { existsSync, mkdirSync, readFileSync } from 'fs';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ const SCHEMA_SQL = `
15
+ CREATE TABLE IF NOT EXISTS entities (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ name TEXT NOT NULL UNIQUE,
18
+ type TEXT NOT NULL,
19
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
20
+ metadata JSON
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS observations (
24
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
25
+ entity_id INTEGER NOT NULL,
26
+ content TEXT NOT NULL,
27
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
28
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS relations (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ from_entity_id INTEGER NOT NULL,
34
+ to_entity_id INTEGER NOT NULL,
35
+ relation_type TEXT NOT NULL,
36
+ metadata JSON,
37
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
38
+ FOREIGN KEY (from_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
39
+ FOREIGN KEY (to_entity_id) REFERENCES entities(id) ON DELETE CASCADE,
40
+ UNIQUE(from_entity_id, to_entity_id, relation_type)
41
+ );
42
+
43
+ CREATE TABLE IF NOT EXISTS tags (
44
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
45
+ entity_id INTEGER NOT NULL,
46
+ tag TEXT NOT NULL,
47
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_tags_entity ON tags(entity_id);
51
+ CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag);
52
+ DELETE FROM tags
53
+ WHERE id NOT IN (
54
+ SELECT MIN(id)
55
+ FROM tags
56
+ GROUP BY entity_id, tag
57
+ );
58
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_entity_tag_unique ON tags(entity_id, tag);
59
+ CREATE INDEX IF NOT EXISTS idx_observations_entity ON observations(entity_id);
60
+ CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity_id);
61
+ CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity_id);
62
+ `;
63
+
64
+ // Parse a JSONL transcript file.
65
+ // Mirrors logic in src/core/extractor.ts parseTranscript().
66
+ // Defensive: never throws — malformed lines are silently skipped.
67
+ function parseTranscript(transcriptPath) {
68
+ const filesEdited = new Set();
69
+ const bashCommands = [];
70
+ const errorsEncountered = [];
71
+ let toolCallCount = 0;
72
+
73
+ try {
74
+ const lines = readFileSync(transcriptPath, 'utf8').split('\n').filter(l => l.trim());
75
+ for (const line of lines) {
76
+ try {
77
+ const entry = JSON.parse(line);
78
+
79
+ // Count tool calls
80
+ if (entry.type === 'tool_use' || entry.tool_name) toolCallCount++;
81
+
82
+ // Track file edits (Write, Edit tools)
83
+ if (entry.tool_name === 'Write' || entry.tool_name === 'Edit') {
84
+ const inp = entry.tool_input ?? {};
85
+ const fp = (inp.file_path ?? inp.path);
86
+ if (fp && typeof fp === 'string') filesEdited.add(basename(fp));
87
+ }
88
+
89
+ // Track meaningful bash commands
90
+ if (entry.tool_name === 'Bash') {
91
+ const cmd = (entry.tool_input?.command) ?? '';
92
+ if (typeof cmd === 'string' && cmd.length > 10 && !cmd.startsWith('ls') && !cmd.startsWith('cd')) {
93
+ bashCommands.push(cmd.slice(0, 100));
94
+ }
95
+ }
96
+
97
+ // Track errors from tool results
98
+ if (entry.type === 'tool_result' && entry.content != null) {
99
+ const text = typeof entry.content === 'string'
100
+ ? entry.content
101
+ : JSON.stringify(entry.content);
102
+ if (text.includes('Error') || text.includes('FAIL') || text.includes('error:')) {
103
+ errorsEncountered.push(text.slice(0, 200));
104
+ }
105
+ }
106
+ } catch {
107
+ // Skip malformed JSONL lines
108
+ }
109
+ }
110
+ } catch {
111
+ // Transcript unreadable — return empty results
112
+ }
113
+
114
+ return { filesEdited: [...filesEdited], bashCommands, errorsEncountered, toolCallCount };
115
+ }
116
+
117
+ // Main: read stdin, extract insights, store in DB
118
+ let input = '';
119
+ process.stdin.setEncoding('utf8');
120
+ process.stdin.on('data', (chunk) => { input += chunk; });
121
+ process.stdin.on('end', () => {
122
+ try {
123
+ if (!input.trim()) return exit0();
124
+
125
+ // Opt-out check
126
+ if (process.env.MEMESH_AUTO_CAPTURE === 'false') return exit0();
127
+
128
+ let inputData;
129
+ try {
130
+ inputData = JSON.parse(input);
131
+ } catch {
132
+ return exit0();
133
+ }
134
+
135
+ const sessionId = inputData.session_id || 'unknown';
136
+ const transcriptPath = inputData.transcript_path;
137
+ const cwd = inputData.cwd || process.cwd();
138
+ const stopReason = inputData.stop_reason || 'unknown';
139
+ const wasAgenticLoop = inputData.was_in_agentic_loop === true;
140
+
141
+ // Guards: skip low-signal sessions
142
+ if (stopReason === 'user_interrupt') return exit0();
143
+ if (!wasAgenticLoop) return exit0();
144
+ if (!transcriptPath || !existsSync(transcriptPath)) return exit0();
145
+
146
+ // Parse transcript
147
+ const { filesEdited, bashCommands, errorsEncountered, toolCallCount } = parseTranscript(transcriptPath);
148
+
149
+ // Skip sessions with too little activity
150
+ if (toolCallCount < 3) return exit0();
151
+
152
+ // Open DB
153
+ const dbPath = process.env.MEMESH_DB_PATH || join(homedir(), '.memesh', 'knowledge-graph.db');
154
+ const dbDir = process.env.MEMESH_DB_PATH
155
+ ? join(process.env.MEMESH_DB_PATH, '..')
156
+ : join(homedir(), '.memesh');
157
+ if (!existsSync(dbDir)) mkdirSync(dbDir, { recursive: true });
158
+
159
+ const Database = require('better-sqlite3');
160
+ const sqliteVec = require('sqlite-vec');
161
+
162
+ const db = new Database(dbPath);
163
+ try {
164
+ db.pragma('journal_mode = WAL');
165
+ db.pragma('foreign_keys = ON');
166
+
167
+ // Ensure schema exists (tables may already exist from MeMesh server)
168
+ db.exec(SCHEMA_SQL);
169
+
170
+ // Migrate: add status column if missing (v2.11 -> v2.12)
171
+ const cols = db.prepare("PRAGMA table_info(entities)").all();
172
+ if (!cols.some(c => c.name === 'status')) {
173
+ db.exec("ALTER TABLE entities ADD COLUMN status TEXT NOT NULL DEFAULT 'active'");
174
+ db.exec("CREATE INDEX IF NOT EXISTS idx_entities_status ON entities(status)");
175
+ }
176
+
177
+ // Load sqlite-vec extension
178
+ sqliteVec.load(db);
179
+
180
+ // Duplicate detection: if we already captured this session, bail
181
+ const shortId = sessionId.slice(0, 8);
182
+ const alreadyCaptured = db.prepare("SELECT id FROM entities WHERE name = ?").get(`session-${shortId}-files`);
183
+ if (alreadyCaptured) return exit0();
184
+
185
+ // Build and store session memories
186
+ const projectName = basename(cwd);
187
+ const baseTags = ['source:auto-capture', `session:${shortId}`, `project:${projectName}`];
188
+
189
+ const insertEntity = db.prepare('INSERT OR IGNORE INTO entities (name, type) VALUES (?, ?)');
190
+ const selectEntity = db.prepare('SELECT id FROM entities WHERE name = ?');
191
+ const insertObs = db.prepare('INSERT INTO observations (entity_id, content) VALUES (?, ?)');
192
+ const insertTag = db.prepare('INSERT OR IGNORE INTO tags (entity_id, tag) VALUES (?, ?)');
193
+
194
+ function storeMemory(name, type, observations, tags) {
195
+ insertEntity.run(name, type);
196
+ const row = selectEntity.get(name);
197
+ if (!row) return;
198
+ for (const obs of observations) insertObs.run(row.id, obs);
199
+ for (const tag of tags) insertTag.run(row.id, tag);
200
+ }
201
+
202
+ // Rule 1: File editing session summary
203
+ if (filesEdited.length > 0) {
204
+ storeMemory(
205
+ `session-${shortId}-files`,
206
+ 'session-insight',
207
+ [
208
+ `Session edited ${filesEdited.length} file(s): ${filesEdited.join(', ')}`,
209
+ `Total tool calls: ${toolCallCount}`,
210
+ ],
211
+ baseTags
212
+ );
213
+ }
214
+
215
+ // Rule 2: Error -> Fix pattern detection
216
+ if (errorsEncountered.length > 0 && filesEdited.length > 0) {
217
+ storeMemory(
218
+ `session-${shortId}-fixes`,
219
+ 'session-insight',
220
+ [
221
+ `Fixed ${errorsEncountered.length} error(s) by editing ${filesEdited.join(', ')}`,
222
+ ...errorsEncountered.slice(0, 3).map(e => `Error: ${e.slice(0, 100)}`),
223
+ ],
224
+ [...baseTags, 'type:bugfix']
225
+ );
226
+ }
227
+
228
+ // Rule 3: Heavy session summary (20+ tool calls = significant work)
229
+ if (toolCallCount >= 20) {
230
+ storeMemory(
231
+ `session-${shortId}-summary`,
232
+ 'session-insight',
233
+ [
234
+ `Significant session: ${toolCallCount} tool calls, ${filesEdited.length} files edited`,
235
+ ...bashCommands.slice(0, 3).map(c => `Command: ${c}`),
236
+ ],
237
+ [...baseTags, 'type:heavy-session']
238
+ );
239
+ }
240
+ } finally {
241
+ db.close();
242
+ }
243
+ } catch (err) {
244
+ // Never crash Claude Code — leave a trace for debugging
245
+ try { process.stderr.write(`[memesh session-summary] ${err?.message || err}\n`); } catch {}
246
+ }
247
+
248
+ // Silent output — don't clutter Claude's response
249
+ console.log(JSON.stringify({ suppressOutput: true }));
250
+ exit0();
251
+ });
252
+
253
+ function exit0() {
254
+ process.exit(0);
255
+ }