@feelingmindful/thinking-graph 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/storage/sqlite.d.ts +4 -0
- package/dist/storage/sqlite.js +111 -41
- package/migrations/001-initial.sql +1 -23
- package/package.json +7 -7
package/dist/storage/sqlite.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare class SQLiteAdapter implements StorageAdapter {
|
|
|
9
9
|
constructor(opts: SQLiteAdapterOpts);
|
|
10
10
|
initialize(): Promise<void>;
|
|
11
11
|
close(): Promise<void>;
|
|
12
|
+
private persist;
|
|
13
|
+
private runSql;
|
|
14
|
+
private getSingle;
|
|
15
|
+
private getAll;
|
|
12
16
|
insertNode(node: Node): Promise<void>;
|
|
13
17
|
getNode(id: string): Promise<Node | null>;
|
|
14
18
|
queryNodes(query: NodeQuery): Promise<PaginatedResult<Node>>;
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readFileSync } from 'fs';
|
|
1
|
+
import initSqlJs from 'sql.js';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
3
|
import { join, dirname } from 'path';
|
|
4
4
|
import { mkdirSync } from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const MIGRATION_PATH = join(__dirname, '../../migrations/001-initial.sql');
|
|
8
|
+
// Cache the WASM module so multiple instances don't re-initialize Emscripten
|
|
9
|
+
let sqlJsPromise = null;
|
|
10
|
+
function getSqlJs() {
|
|
11
|
+
if (!sqlJsPromise)
|
|
12
|
+
sqlJsPromise = initSqlJs();
|
|
13
|
+
return sqlJsPromise;
|
|
14
|
+
}
|
|
8
15
|
export class SQLiteAdapter {
|
|
9
16
|
db;
|
|
10
17
|
dbPath;
|
|
@@ -12,30 +19,85 @@ export class SQLiteAdapter {
|
|
|
12
19
|
this.dbPath = opts.dbPath;
|
|
13
20
|
}
|
|
14
21
|
async initialize() {
|
|
15
|
-
// Ensure parent directory exists
|
|
16
22
|
const dir = dirname(this.dbPath);
|
|
17
23
|
mkdirSync(dir, { recursive: true });
|
|
18
|
-
|
|
19
|
-
this.
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
const SQL = await getSqlJs();
|
|
25
|
+
if (existsSync(this.dbPath)) {
|
|
26
|
+
const fileBuffer = readFileSync(this.dbPath);
|
|
27
|
+
this.db = new SQL.Database(new Uint8Array(fileBuffer));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
this.db = new SQL.Database();
|
|
31
|
+
}
|
|
32
|
+
this.db.run('PRAGMA foreign_keys = ON');
|
|
33
|
+
// Run migration (multi-statement)
|
|
22
34
|
const sql = readFileSync(MIGRATION_PATH, 'utf-8');
|
|
23
35
|
this.db.exec(sql);
|
|
36
|
+
this.persist();
|
|
24
37
|
}
|
|
25
38
|
async close() {
|
|
39
|
+
this.persist();
|
|
26
40
|
this.db.close();
|
|
27
41
|
}
|
|
42
|
+
persist() {
|
|
43
|
+
const data = this.db.export();
|
|
44
|
+
const buffer = Buffer.from(data);
|
|
45
|
+
writeFileSync(this.dbPath, buffer);
|
|
46
|
+
}
|
|
47
|
+
// ─── Helpers ──────────────────────────────────────────
|
|
48
|
+
runSql(sql, params = []) {
|
|
49
|
+
this.db.run(sql, params);
|
|
50
|
+
this.persist();
|
|
51
|
+
}
|
|
52
|
+
getSingle(sql, params = []) {
|
|
53
|
+
const stmt = this.db.prepare(sql);
|
|
54
|
+
stmt.bind(params);
|
|
55
|
+
if (stmt.step()) {
|
|
56
|
+
const columns = stmt.getColumnNames();
|
|
57
|
+
const values = stmt.get();
|
|
58
|
+
stmt.free();
|
|
59
|
+
const row = {};
|
|
60
|
+
for (let i = 0; i < columns.length; i++) {
|
|
61
|
+
row[columns[i]] = values[i];
|
|
62
|
+
}
|
|
63
|
+
return row;
|
|
64
|
+
}
|
|
65
|
+
stmt.free();
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
getAll(sql, params = []) {
|
|
69
|
+
const stmt = this.db.prepare(sql);
|
|
70
|
+
stmt.bind(params);
|
|
71
|
+
const rows = [];
|
|
72
|
+
while (stmt.step()) {
|
|
73
|
+
const columns = stmt.getColumnNames();
|
|
74
|
+
const values = stmt.get();
|
|
75
|
+
const row = {};
|
|
76
|
+
for (let i = 0; i < columns.length; i++) {
|
|
77
|
+
row[columns[i]] = values[i];
|
|
78
|
+
}
|
|
79
|
+
rows.push(row);
|
|
80
|
+
}
|
|
81
|
+
stmt.free();
|
|
82
|
+
return rows;
|
|
83
|
+
}
|
|
28
84
|
// ─── Nodes ─────────────────────────────────────────────
|
|
29
85
|
async insertNode(node) {
|
|
30
|
-
this.
|
|
86
|
+
this.runSql(`
|
|
31
87
|
INSERT INTO nodes (id, type, content, session_id, project_id, metadata,
|
|
32
88
|
created_at, updated_at, thought_number, total_thoughts, branch_id,
|
|
33
89
|
is_revision, revises_thought)
|
|
34
90
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
|
-
|
|
91
|
+
`, [
|
|
92
|
+
node.id, node.type, node.content, node.sessionId, node.projectId ?? null,
|
|
93
|
+
JSON.stringify(node.metadata), node.createdAt, node.updatedAt,
|
|
94
|
+
node.thoughtNumber ?? null, node.totalThoughts ?? null,
|
|
95
|
+
node.branchId ?? null, node.isRevision ? 1 : 0,
|
|
96
|
+
node.revisesThought ?? null,
|
|
97
|
+
]);
|
|
36
98
|
}
|
|
37
99
|
async getNode(id) {
|
|
38
|
-
const row = this.
|
|
100
|
+
const row = this.getSingle('SELECT * FROM nodes WHERE id = ?', [id]);
|
|
39
101
|
return row ? this.rowToNode(row) : null;
|
|
40
102
|
}
|
|
41
103
|
async queryNodes(query) {
|
|
@@ -59,16 +121,15 @@ export class SQLiteAdapter {
|
|
|
59
121
|
params.push(query.since);
|
|
60
122
|
}
|
|
61
123
|
if (query.query) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
params.push(query.query);
|
|
124
|
+
conditions.push('content LIKE ?');
|
|
125
|
+
params.push(`%${query.query}%`);
|
|
65
126
|
}
|
|
66
127
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
67
|
-
const countRow = this.
|
|
128
|
+
const countRow = this.getSingle(`SELECT COUNT(*) as cnt FROM nodes ${where}`, params);
|
|
68
129
|
const totalCount = countRow.cnt;
|
|
69
130
|
const limit = query.limit ?? 20;
|
|
70
131
|
const offset = query.offset ?? 0;
|
|
71
|
-
const rows = this.
|
|
132
|
+
const rows = this.getAll(`SELECT * FROM nodes ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, limit, offset]);
|
|
72
133
|
return {
|
|
73
134
|
items: rows.map(r => this.rowToNode(r)),
|
|
74
135
|
totalCount,
|
|
@@ -76,22 +137,24 @@ export class SQLiteAdapter {
|
|
|
76
137
|
};
|
|
77
138
|
}
|
|
78
139
|
async searchContent(text, limit = 20) {
|
|
79
|
-
const rows = this.
|
|
80
|
-
SELECT
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
ORDER BY rank
|
|
140
|
+
const rows = this.getAll(`
|
|
141
|
+
SELECT * FROM nodes
|
|
142
|
+
WHERE content LIKE ?
|
|
143
|
+
ORDER BY created_at DESC
|
|
84
144
|
LIMIT ?
|
|
85
|
-
|
|
145
|
+
`, [`%${text}%`, limit]);
|
|
86
146
|
return rows.map(r => this.rowToNode(r));
|
|
87
147
|
}
|
|
88
148
|
// ─── Edges ─────────────────────────────────────────────
|
|
89
149
|
async insertEdge(edge) {
|
|
90
150
|
try {
|
|
91
|
-
this.
|
|
151
|
+
this.runSql(`
|
|
92
152
|
INSERT INTO edges (id, source_id, target_id, type, weight, reasoning, created_at)
|
|
93
153
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
94
|
-
|
|
154
|
+
`, [
|
|
155
|
+
edge.id, edge.sourceId, edge.targetId, edge.type,
|
|
156
|
+
edge.weight, edge.reasoning ?? null, edge.createdAt,
|
|
157
|
+
]);
|
|
95
158
|
return true;
|
|
96
159
|
}
|
|
97
160
|
catch (err) {
|
|
@@ -106,7 +169,7 @@ export class SQLiteAdapter {
|
|
|
106
169
|
? 'SELECT * FROM edges WHERE source_id = ? AND type = ?'
|
|
107
170
|
: 'SELECT * FROM edges WHERE source_id = ?';
|
|
108
171
|
const params = type ? [nodeId, type] : [nodeId];
|
|
109
|
-
const rows = this.
|
|
172
|
+
const rows = this.getAll(sql, params);
|
|
110
173
|
return rows.map(r => this.rowToEdge(r));
|
|
111
174
|
}
|
|
112
175
|
async getEdgesTo(nodeId, type) {
|
|
@@ -114,11 +177,11 @@ export class SQLiteAdapter {
|
|
|
114
177
|
? 'SELECT * FROM edges WHERE target_id = ? AND type = ?'
|
|
115
178
|
: 'SELECT * FROM edges WHERE target_id = ?';
|
|
116
179
|
const params = type ? [nodeId, type] : [nodeId];
|
|
117
|
-
const rows = this.
|
|
180
|
+
const rows = this.getAll(sql, params);
|
|
118
181
|
return rows.map(r => this.rowToEdge(r));
|
|
119
182
|
}
|
|
120
183
|
async traverseEdges(startId, type, maxDepth) {
|
|
121
|
-
const rows = this.
|
|
184
|
+
const rows = this.getAll(`
|
|
122
185
|
WITH RECURSIVE chain(id, depth) AS (
|
|
123
186
|
SELECT target_id, 1
|
|
124
187
|
FROM edges
|
|
@@ -132,18 +195,21 @@ export class SQLiteAdapter {
|
|
|
132
195
|
SELECT DISTINCT n.* FROM nodes n
|
|
133
196
|
JOIN chain c ON n.id = c.id
|
|
134
197
|
ORDER BY c.depth
|
|
135
|
-
|
|
198
|
+
`, [startId, type, type, maxDepth]);
|
|
136
199
|
return rows.map(r => this.rowToNode(r));
|
|
137
200
|
}
|
|
138
201
|
// ─── Sessions ──────────────────────────────────────────
|
|
139
202
|
async insertSession(session) {
|
|
140
|
-
this.
|
|
203
|
+
this.runSql(`
|
|
141
204
|
INSERT INTO sessions (id, project_id, project_path, description, started_at, last_active)
|
|
142
205
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
143
|
-
|
|
206
|
+
`, [
|
|
207
|
+
session.id, session.projectId ?? null, session.projectPath ?? null,
|
|
208
|
+
session.description ?? null, session.startedAt, session.lastActiveAt,
|
|
209
|
+
]);
|
|
144
210
|
}
|
|
145
211
|
async getSession(id) {
|
|
146
|
-
const row = this.
|
|
212
|
+
const row = this.getSingle('SELECT * FROM sessions WHERE id = ?', [id]);
|
|
147
213
|
return row ? this.rowToSession(row) : null;
|
|
148
214
|
}
|
|
149
215
|
async updateSession(id, fields) {
|
|
@@ -159,16 +225,20 @@ export class SQLiteAdapter {
|
|
|
159
225
|
}
|
|
160
226
|
if (sets.length > 0) {
|
|
161
227
|
params.push(id);
|
|
162
|
-
this.
|
|
228
|
+
this.runSql(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`, params);
|
|
163
229
|
}
|
|
164
230
|
}
|
|
165
231
|
// ─── Skill Registry ───────────────────────────────────
|
|
166
232
|
async insertSkill(entry) {
|
|
167
|
-
this.
|
|
233
|
+
this.runSql(`
|
|
168
234
|
INSERT OR IGNORE INTO skill_registry
|
|
169
235
|
(id, plugin_name, skill_name, verb, invocation, areas, detects, produces, invokes, platform)
|
|
170
236
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
171
|
-
|
|
237
|
+
`, [
|
|
238
|
+
entry.id, entry.pluginName, entry.skillName, entry.verb ?? null,
|
|
239
|
+
entry.invocation, JSON.stringify(entry.areas), JSON.stringify(entry.detects),
|
|
240
|
+
JSON.stringify(entry.produces), JSON.stringify(entry.invokes), entry.platform ?? null,
|
|
241
|
+
]);
|
|
172
242
|
}
|
|
173
243
|
async querySkills(filter) {
|
|
174
244
|
const conditions = [];
|
|
@@ -194,7 +264,7 @@ export class SQLiteAdapter {
|
|
|
194
264
|
params.push(filter.platform);
|
|
195
265
|
}
|
|
196
266
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
197
|
-
const rows = this.
|
|
267
|
+
const rows = this.getAll(`SELECT * FROM skill_registry ${where}`, params);
|
|
198
268
|
return rows.map(r => ({
|
|
199
269
|
id: r.id,
|
|
200
270
|
pluginName: r.plugin_name,
|
|
@@ -226,18 +296,18 @@ export class SQLiteAdapter {
|
|
|
226
296
|
params.push(...types);
|
|
227
297
|
}
|
|
228
298
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
229
|
-
const nodeRows = this.
|
|
299
|
+
const nodeRows = this.getAll(`SELECT * FROM nodes ${where}`, params);
|
|
230
300
|
const nodes = nodeRows.map(r => this.rowToNode(r));
|
|
231
301
|
let edges = [];
|
|
232
302
|
if (opts?.includeEdges !== false) {
|
|
233
303
|
const nodeIds = nodes.map(n => n.id);
|
|
234
304
|
if (nodeIds.length > 0) {
|
|
235
305
|
const placeholders = nodeIds.map(() => '?').join(',');
|
|
236
|
-
const edgeRows = this.
|
|
306
|
+
const edgeRows = this.getAll(`SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`, [...nodeIds, ...nodeIds]);
|
|
237
307
|
edges = edgeRows.map(r => this.rowToEdge(r));
|
|
238
308
|
}
|
|
239
309
|
}
|
|
240
|
-
const sessionRows = this.
|
|
310
|
+
const sessionRows = this.getAll('SELECT * FROM sessions');
|
|
241
311
|
return {
|
|
242
312
|
exportedAt: new Date().toISOString(),
|
|
243
313
|
nodeCount: nodes.length,
|
|
@@ -250,15 +320,15 @@ export class SQLiteAdapter {
|
|
|
250
320
|
// ─── Stats ─────────────────────────────────────────────
|
|
251
321
|
async getStats() {
|
|
252
322
|
const nodesByType = {};
|
|
253
|
-
const ntRows = this.
|
|
323
|
+
const ntRows = this.getAll('SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type');
|
|
254
324
|
for (const r of ntRows)
|
|
255
325
|
nodesByType[r.type] = r.cnt;
|
|
256
326
|
const edgesByType = {};
|
|
257
|
-
const etRows = this.
|
|
327
|
+
const etRows = this.getAll('SELECT type, COUNT(*) as cnt FROM edges GROUP BY type');
|
|
258
328
|
for (const r of etRows)
|
|
259
329
|
edgesByType[r.type] = r.cnt;
|
|
260
|
-
const totalNodes = this.
|
|
261
|
-
const totalEdges = this.
|
|
330
|
+
const totalNodes = this.getSingle('SELECT COUNT(*) as cnt FROM nodes').cnt;
|
|
331
|
+
const totalEdges = this.getSingle('SELECT COUNT(*) as cnt FROM edges').cnt;
|
|
262
332
|
return {
|
|
263
333
|
totalNodes,
|
|
264
334
|
totalEdges,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
-- Thinking Graph Schema v1
|
|
2
|
-
-- Sessions, Nodes
|
|
2
|
+
-- Sessions, Nodes, Edges, Skill Registry
|
|
3
3
|
|
|
4
|
-
PRAGMA journal_mode = WAL;
|
|
5
4
|
PRAGMA foreign_keys = ON;
|
|
6
5
|
|
|
7
6
|
-- ─── Sessions ────────────────────────────────────────────
|
|
@@ -47,27 +46,6 @@ CREATE INDEX IF NOT EXISTS idx_nodes_project ON nodes(project_id);
|
|
|
47
46
|
CREATE INDEX IF NOT EXISTS idx_nodes_branch ON nodes(branch_id);
|
|
48
47
|
CREATE INDEX IF NOT EXISTS idx_nodes_created ON nodes(created_at);
|
|
49
48
|
|
|
50
|
-
-- Full-text search
|
|
51
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
52
|
-
content,
|
|
53
|
-
content='nodes',
|
|
54
|
-
content_rowid='rowid'
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
-- Keep FTS in sync
|
|
58
|
-
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
|
|
59
|
-
INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
60
|
-
END;
|
|
61
|
-
|
|
62
|
-
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
|
|
63
|
-
INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
64
|
-
INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
65
|
-
END;
|
|
66
|
-
|
|
67
|
-
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
|
|
68
|
-
INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
69
|
-
END;
|
|
70
|
-
|
|
71
49
|
-- ─── Edges ───────────────────────────────────────────────
|
|
72
50
|
|
|
73
51
|
CREATE TABLE IF NOT EXISTS edges (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelingmindful/thinking-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Persistent graph-based MCP thinking server for the feeling-mindful plugin marketplace",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
34
|
+
"sql.js": "^1.12.0",
|
|
35
|
+
"uuid": "^10.0.0",
|
|
36
|
+
"zod": "^3.23.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"
|
|
39
|
+
"@types/node": "^25.5.0",
|
|
40
|
+
"@types/uuid": "^10.0.0",
|
|
40
41
|
"typescript": "^5.5.0",
|
|
41
|
-
"
|
|
42
|
-
"@types/uuid": "^10.0.0"
|
|
42
|
+
"vitest": "^3.0.0"
|
|
43
43
|
}
|
|
44
44
|
}
|