@pcircle/memesh 2.11.0 → 3.0.1
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.md +153 -52
- package/dist/cli/assets/d3.v7.min.js +2 -0
- package/dist/cli/view.d.ts +1 -0
- package/dist/cli/view.d.ts.map +1 -1
- package/dist/cli/view.js +2198 -10
- package/dist/cli/view.js.map +1 -1
- package/dist/core/config.d.ts +33 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +70 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/extractor.d.ts +26 -0
- package/dist/core/extractor.d.ts.map +1 -0
- package/dist/core/extractor.js +106 -0
- package/dist/core/extractor.js.map +1 -0
- package/dist/core/lifecycle.d.ts +9 -0
- package/dist/core/lifecycle.d.ts.map +1 -0
- package/dist/core/lifecycle.js +51 -0
- package/dist/core/lifecycle.js.map +1 -0
- package/dist/core/operations.d.ts +9 -0
- package/dist/core/operations.d.ts.map +1 -0
- package/dist/core/operations.js +342 -0
- package/dist/core/operations.js.map +1 -0
- package/dist/core/query-expander.d.ts +4 -0
- package/dist/core/query-expander.d.ts.map +1 -0
- package/dist/core/query-expander.js +114 -0
- package/dist/core/query-expander.js.map +1 -0
- package/dist/core/schema-export.d.ts +2 -0
- package/dist/core/schema-export.d.ts.map +1 -0
- package/dist/core/schema-export.js +67 -0
- package/dist/core/schema-export.js.map +1 -0
- package/dist/core/scoring.d.ts +25 -0
- package/dist/core/scoring.d.ts.map +1 -0
- package/dist/core/scoring.js +40 -0
- package/dist/core/scoring.js.map +1 -0
- package/dist/core/types.d.ts +124 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/version-check.d.ts +10 -0
- package/dist/core/version-check.d.ts.map +1 -0
- package/dist/core/version-check.js +44 -0
- package/dist/core/version-check.js.map +1 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +33 -0
- package/dist/db.js.map +1 -1
- package/dist/knowledge-graph.d.ts +16 -30
- package/dist/knowledge-graph.d.ts.map +1 -1
- package/dist/knowledge-graph.js +205 -24
- package/dist/knowledge-graph.js.map +1 -1
- package/dist/mcp/server.js +8 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.d.ts +1 -92
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +1 -182
- package/dist/mcp/tools.js.map +1 -1
- package/dist/transports/cli/cli.d.ts +3 -0
- package/dist/transports/cli/cli.d.ts.map +1 -0
- package/dist/transports/cli/cli.js +378 -0
- package/dist/transports/cli/cli.js.map +1 -0
- package/dist/transports/http/server.d.ts +5 -0
- package/dist/transports/http/server.d.ts.map +1 -0
- package/dist/transports/http/server.js +314 -0
- package/dist/transports/http/server.js.map +1 -0
- package/dist/transports/mcp/handlers.d.ts +184 -0
- package/dist/transports/mcp/handlers.d.ts.map +1 -0
- package/dist/transports/mcp/handlers.js +271 -0
- package/dist/transports/mcp/handlers.js.map +1 -0
- package/hooks/hooks.json +24 -0
- package/package.json +29 -9
- package/plugin.json +15 -7
- package/scripts/hooks/post-commit.js +35 -6
- package/scripts/hooks/pre-compact.js +198 -0
- package/scripts/hooks/session-start.js +73 -25
- package/scripts/hooks/session-summary.js +255 -0
- package/skills/memesh/SKILL.md +73 -0
- package/skills/memesh-review/SKILL.md +54 -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
|
-
//
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
80
|
+
${statusFilter}
|
|
81
|
+
${scoringOrderBy}
|
|
82
|
+
LIMIT ?
|
|
83
|
+
`).all(projectTag, sessionLimit);
|
|
49
84
|
|
|
50
|
-
// Fetch
|
|
51
|
-
const
|
|
52
|
-
'SELECT content FROM observations WHERE entity_id = ? ORDER BY id
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
127
|
+
return;
|
|
88
128
|
}
|
|
89
129
|
|
|
90
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memesh
|
|
3
|
+
description: Use MeMesh to remember, recall, and manage AI knowledge across sessions. Triggers when the user asks to remember something, recall past decisions, forget outdated info, or manage their memory. Also triggers proactively when you make important decisions, fix bugs, or learn lessons worth preserving.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# MeMesh — AI Memory Management
|
|
8
|
+
|
|
9
|
+
You have access to MeMesh, a persistent memory layer with 6 tools. Use them to remember important knowledge across sessions.
|
|
10
|
+
|
|
11
|
+
## When to Use (Proactive)
|
|
12
|
+
|
|
13
|
+
**Remember automatically when:**
|
|
14
|
+
- A design decision is made ("let's use OAuth" → remember it)
|
|
15
|
+
- A bug is fixed (root cause + fix → remember as lesson)
|
|
16
|
+
- A pattern is established ("we always use Zod for validation" → remember as pattern)
|
|
17
|
+
- Architecture changes ("moved from monolith to microservices" → remember)
|
|
18
|
+
- The user explicitly says "remember this" or "don't forget"
|
|
19
|
+
|
|
20
|
+
**Recall automatically when:**
|
|
21
|
+
- Starting work on a feature that might have prior decisions
|
|
22
|
+
- The user asks "what did we decide about X?"
|
|
23
|
+
- You need context about a project's conventions
|
|
24
|
+
|
|
25
|
+
**Forget when:**
|
|
26
|
+
- A decision is superseded by a new one (use `supersedes` relation)
|
|
27
|
+
- The user says "forget about X" or "that's outdated"
|
|
28
|
+
|
|
29
|
+
## Tools Reference
|
|
30
|
+
|
|
31
|
+
### remember
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"name": "auth-decision",
|
|
35
|
+
"type": "decision",
|
|
36
|
+
"observations": ["Use OAuth 2.0 with PKCE for authentication"],
|
|
37
|
+
"tags": ["project:myapp", "topic:auth"],
|
|
38
|
+
"relations": [{"to": "old-auth", "type": "supersedes"}]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
Types: `decision`, `pattern`, `lesson`, `bug_fix`, `architecture`, `convention`
|
|
42
|
+
|
|
43
|
+
### recall
|
|
44
|
+
```json
|
|
45
|
+
{"query": "authentication", "tag": "project:myapp", "limit": 10}
|
|
46
|
+
```
|
|
47
|
+
Omit query to list recent memories. Results are ranked by relevance, recency, and access frequency.
|
|
48
|
+
|
|
49
|
+
### forget
|
|
50
|
+
```json
|
|
51
|
+
{"name": "outdated-design"}
|
|
52
|
+
```
|
|
53
|
+
Archives the entity (soft-delete). Add `"observation": "specific text"` to remove just one fact.
|
|
54
|
+
|
|
55
|
+
### consolidate
|
|
56
|
+
```json
|
|
57
|
+
{"name": "auth-history", "min_observations": 5}
|
|
58
|
+
```
|
|
59
|
+
Compresses verbose memories using LLM. Requires Smart Mode configured.
|
|
60
|
+
|
|
61
|
+
### export / import
|
|
62
|
+
```json
|
|
63
|
+
{"tag": "project:myapp"}
|
|
64
|
+
```
|
|
65
|
+
Export memories as JSON for sharing. Import with merge strategies: `skip`, `overwrite`, `append`.
|
|
66
|
+
|
|
67
|
+
## Best Practices
|
|
68
|
+
|
|
69
|
+
1. **Be specific** — "Use OAuth 2.0 with PKCE" not "auth stuff decided"
|
|
70
|
+
2. **Tag by project** — Always include `project:<name>` tag
|
|
71
|
+
3. **Use relations** — Link related decisions with `related-to`, replace old ones with `supersedes`
|
|
72
|
+
4. **Type correctly** — Use the right type (decision/pattern/lesson) for better search
|
|
73
|
+
5. **Don't over-remember** — Skip trivial things. Remember decisions that took > 5 minutes to make.
|