@feelingmindful/thinking-graph 1.14.1 → 1.15.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/index.js +5 -5
- package/dist/storage/embedding.d.ts +1 -0
- package/dist/storage/embedding.js +32 -0
- package/dist/storage/jsonl.d.ts +23 -0
- package/dist/storage/jsonl.js +138 -0
- package/dist/storage/migrate-sqlite.d.ts +12 -0
- package/dist/storage/migrate-sqlite.js +75 -0
- package/dist/storage/vector-index.d.ts +11 -0
- package/dist/storage/vector-index.js +35 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from 'os';
|
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { ThinkingGraph } from './engine/graph.js';
|
|
7
|
-
import {
|
|
7
|
+
import { JSONLAdapter } from './storage/jsonl.js';
|
|
8
8
|
import { InMemoryAdapter } from './storage/memory.js';
|
|
9
9
|
import { VaultBridge } from './vault/bridge.js';
|
|
10
10
|
import { thinkSchema, thinkHandler } from './tools/think.js';
|
|
@@ -26,8 +26,8 @@ function expandHome(p) {
|
|
|
26
26
|
const memoryOnly = process.env.THINKING_GRAPH_MEMORY_ONLY === 'true';
|
|
27
27
|
const storage = memoryOnly
|
|
28
28
|
? new InMemoryAdapter()
|
|
29
|
-
: new
|
|
30
|
-
|
|
29
|
+
: new JSONLAdapter({
|
|
30
|
+
dir: expandHome(process.env.THINKING_GRAPH_DATA_DIR || '.premium/thinking'),
|
|
31
31
|
});
|
|
32
32
|
// ─── Vault bridge ────────────────────────────────────────
|
|
33
33
|
const vaultPath = expandHome(process.env.THINKING_GRAPH_VAULT_PATH || '~/Documents/Obsidian/Dev');
|
|
@@ -70,9 +70,9 @@ async function main() {
|
|
|
70
70
|
await server.connect(transport);
|
|
71
71
|
if (!process.env.DISABLE_THOUGHT_LOGGING) {
|
|
72
72
|
console.error('thinking-graph MCP server started');
|
|
73
|
-
console.error(` Storage: ${memoryOnly ? 'in-memory' : '
|
|
73
|
+
console.error(` Storage: ${memoryOnly ? 'in-memory' : 'JSONL'}`);
|
|
74
74
|
if (!memoryOnly) {
|
|
75
|
-
console.error(`
|
|
75
|
+
console.error(` Dir: ${process.env.THINKING_GRAPH_DATA_DIR || '.premium/thinking'}`);
|
|
76
76
|
}
|
|
77
77
|
console.error(` Vault: ${vault.root}`);
|
|
78
78
|
console.error(` Project: ${projectSlug}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function embedText(text: string): Promise<Float32Array | null>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
let pipeline = null;
|
|
2
|
+
let initPromise = null;
|
|
3
|
+
async function loadPipeline() {
|
|
4
|
+
try {
|
|
5
|
+
const { pipeline: createPipeline } = await import('@xenova/transformers');
|
|
6
|
+
const pipe = await createPipeline('feature-extraction', 'Xenova/bge-small-en-v1.5', {
|
|
7
|
+
quantized: true,
|
|
8
|
+
});
|
|
9
|
+
pipeline = async (text) => {
|
|
10
|
+
const output = await pipe(text, { pooling: 'mean', normalize: true });
|
|
11
|
+
return new Float32Array(output.data);
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
pipeline = null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function embedText(text) {
|
|
19
|
+
if (process.env.THINKING_GRAPH_EMBEDDINGS === 'false')
|
|
20
|
+
return null;
|
|
21
|
+
if (!initPromise)
|
|
22
|
+
initPromise = loadPipeline();
|
|
23
|
+
await initPromise;
|
|
24
|
+
if (!pipeline)
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
return await pipeline(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Node, Edge, Session, SkillRegistryEntry } from '../engine/types.js';
|
|
2
|
+
import { InMemoryAdapter } from './memory.js';
|
|
3
|
+
export interface JSONLAdapterOpts {
|
|
4
|
+
dir: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class JSONLAdapter extends InMemoryAdapter {
|
|
7
|
+
private readonly nodesPath;
|
|
8
|
+
private readonly edgesPath;
|
|
9
|
+
private readonly sessionsPath;
|
|
10
|
+
private readonly skillsPath;
|
|
11
|
+
private readonly embeddingsPath;
|
|
12
|
+
private readonly vectorIndex;
|
|
13
|
+
constructor(opts: JSONLAdapterOpts);
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
private maybeRunSqliteMigration;
|
|
17
|
+
insertNode(node: Node): Promise<void>;
|
|
18
|
+
searchContent(query: string): Promise<Node[]>;
|
|
19
|
+
insertEdge(edge: Edge): Promise<boolean>;
|
|
20
|
+
insertSession(session: Session): Promise<void>;
|
|
21
|
+
updateSession(id: string, fields: Partial<Session>): Promise<void>;
|
|
22
|
+
insertSkill(entry: SkillRegistryEntry): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { InMemoryAdapter } from './memory.js';
|
|
4
|
+
import { embedText } from './embedding.js';
|
|
5
|
+
import { VectorIndex } from './vector-index.js';
|
|
6
|
+
import { migrateSqliteToJsonl } from './migrate-sqlite.js';
|
|
7
|
+
export class JSONLAdapter extends InMemoryAdapter {
|
|
8
|
+
nodesPath;
|
|
9
|
+
edgesPath;
|
|
10
|
+
sessionsPath;
|
|
11
|
+
skillsPath;
|
|
12
|
+
embeddingsPath;
|
|
13
|
+
vectorIndex = new VectorIndex();
|
|
14
|
+
constructor(opts) {
|
|
15
|
+
super();
|
|
16
|
+
this.nodesPath = join(opts.dir, 'nodes.jsonl');
|
|
17
|
+
this.edgesPath = join(opts.dir, 'edges.jsonl');
|
|
18
|
+
this.sessionsPath = join(opts.dir, 'sessions.jsonl');
|
|
19
|
+
this.skillsPath = join(opts.dir, 'skills.jsonl');
|
|
20
|
+
this.embeddingsPath = join(opts.dir, 'embeddings.ndjson');
|
|
21
|
+
}
|
|
22
|
+
async initialize() {
|
|
23
|
+
mkdirSync(join(this.nodesPath, '..'), { recursive: true });
|
|
24
|
+
await this.maybeRunSqliteMigration();
|
|
25
|
+
for (const line of readLines(this.nodesPath)) {
|
|
26
|
+
await super.insertNode(JSON.parse(line));
|
|
27
|
+
}
|
|
28
|
+
for (const line of readLines(this.edgesPath)) {
|
|
29
|
+
await super.insertEdge(JSON.parse(line));
|
|
30
|
+
}
|
|
31
|
+
// Sessions: last line wins for each id (handles updateSession appends)
|
|
32
|
+
const sessionMap = new Map();
|
|
33
|
+
for (const line of readLines(this.sessionsPath)) {
|
|
34
|
+
const s = JSON.parse(line);
|
|
35
|
+
sessionMap.set(s.id, s);
|
|
36
|
+
}
|
|
37
|
+
for (const s of sessionMap.values()) {
|
|
38
|
+
await super.insertSession(s);
|
|
39
|
+
}
|
|
40
|
+
// Skills: deduplicated by id
|
|
41
|
+
const skillMap = new Map();
|
|
42
|
+
for (const line of readLines(this.skillsPath)) {
|
|
43
|
+
const sk = JSON.parse(line);
|
|
44
|
+
skillMap.set(sk.id, sk);
|
|
45
|
+
}
|
|
46
|
+
for (const sk of skillMap.values()) {
|
|
47
|
+
await super.insertSkill(sk);
|
|
48
|
+
}
|
|
49
|
+
this.vectorIndex.load(this.embeddingsPath);
|
|
50
|
+
}
|
|
51
|
+
async close() { }
|
|
52
|
+
async maybeRunSqliteMigration() {
|
|
53
|
+
const dbPath = process.env.THINKING_GRAPH_PROJECT_DB;
|
|
54
|
+
if (!dbPath)
|
|
55
|
+
return;
|
|
56
|
+
if (!existsSync(dbPath))
|
|
57
|
+
return;
|
|
58
|
+
// Only migrate if JSONL store is empty (first run after upgrade)
|
|
59
|
+
if (existsSync(this.nodesPath))
|
|
60
|
+
return;
|
|
61
|
+
console.error(`[thinking-graph] Migrating SQLite data from ${dbPath} to JSONL...`);
|
|
62
|
+
try {
|
|
63
|
+
const counts = await migrateSqliteToJsonl({
|
|
64
|
+
dbPath,
|
|
65
|
+
nodesPath: this.nodesPath,
|
|
66
|
+
edgesPath: this.edgesPath,
|
|
67
|
+
sessionsPath: this.sessionsPath,
|
|
68
|
+
skillsPath: this.skillsPath,
|
|
69
|
+
});
|
|
70
|
+
console.error(`[thinking-graph] Migration complete: ${counts.nodes} nodes, ${counts.edges} edges, ${counts.sessions} sessions, ${counts.skills} skills`);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
74
|
+
console.error(`[thinking-graph] SQLite migration skipped: ${msg}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// ─── Nodes ─────────────────────────────────────────────
|
|
78
|
+
async insertNode(node) {
|
|
79
|
+
await super.insertNode(node);
|
|
80
|
+
appendLine(this.nodesPath, node);
|
|
81
|
+
const vec = await embedText(node.content);
|
|
82
|
+
if (vec)
|
|
83
|
+
this.vectorIndex.append(this.embeddingsPath, node.id, vec);
|
|
84
|
+
}
|
|
85
|
+
async searchContent(query) {
|
|
86
|
+
const vec = await embedText(query);
|
|
87
|
+
if (!vec)
|
|
88
|
+
return super.searchContent(query);
|
|
89
|
+
const matches = this.vectorIndex.search(vec, 20).filter(m => m.score >= 0.5);
|
|
90
|
+
if (matches.length === 0)
|
|
91
|
+
return super.searchContent(query);
|
|
92
|
+
const nodes = [];
|
|
93
|
+
for (const { id } of matches) {
|
|
94
|
+
const node = await super.getNode(id);
|
|
95
|
+
if (node)
|
|
96
|
+
nodes.push(node);
|
|
97
|
+
}
|
|
98
|
+
return nodes;
|
|
99
|
+
}
|
|
100
|
+
// ─── Edges ─────────────────────────────────────────────
|
|
101
|
+
async insertEdge(edge) {
|
|
102
|
+
const created = await super.insertEdge(edge);
|
|
103
|
+
if (created)
|
|
104
|
+
appendLine(this.edgesPath, edge);
|
|
105
|
+
return created;
|
|
106
|
+
}
|
|
107
|
+
// ─── Sessions ──────────────────────────────────────────
|
|
108
|
+
async insertSession(session) {
|
|
109
|
+
await super.insertSession(session);
|
|
110
|
+
appendLine(this.sessionsPath, session);
|
|
111
|
+
}
|
|
112
|
+
async updateSession(id, fields) {
|
|
113
|
+
await super.updateSession(id, fields);
|
|
114
|
+
const updated = await super.getSession(id);
|
|
115
|
+
if (updated)
|
|
116
|
+
appendLine(this.sessionsPath, updated);
|
|
117
|
+
}
|
|
118
|
+
// ─── Skill Registry ───────────────────────────────────
|
|
119
|
+
async insertSkill(entry) {
|
|
120
|
+
// Idempotent: skip if already in memory (loaded from file or previously seeded)
|
|
121
|
+
const existing = await this.querySkills({ id: entry.id });
|
|
122
|
+
if (existing.length > 0)
|
|
123
|
+
return;
|
|
124
|
+
await super.insertSkill(entry);
|
|
125
|
+
appendLine(this.skillsPath, entry);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ─── Helpers ───────────────────────────────────────────
|
|
129
|
+
function readLines(path) {
|
|
130
|
+
if (!existsSync(path))
|
|
131
|
+
return [];
|
|
132
|
+
return readFileSync(path, 'utf-8')
|
|
133
|
+
.split('\n')
|
|
134
|
+
.filter(line => line.trim().length > 0);
|
|
135
|
+
}
|
|
136
|
+
function appendLine(path, value) {
|
|
137
|
+
appendFileSync(path, JSON.stringify(value) + '\n', 'utf-8');
|
|
138
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { existsSync, appendFileSync } from 'fs';
|
|
2
|
+
function appendLine(path, value) {
|
|
3
|
+
appendFileSync(path, JSON.stringify(value) + '\n', 'utf-8');
|
|
4
|
+
}
|
|
5
|
+
export async function migrateSqliteToJsonl(opts) {
|
|
6
|
+
if (!existsSync(opts.dbPath)) {
|
|
7
|
+
throw new Error(`SQLite database not found: ${opts.dbPath}`);
|
|
8
|
+
}
|
|
9
|
+
let Database;
|
|
10
|
+
try {
|
|
11
|
+
const mod = await import('better-sqlite3');
|
|
12
|
+
Database = mod.default;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error('better-sqlite3 not available. Install it to run migration:\n' +
|
|
16
|
+
' npm install better-sqlite3\n' +
|
|
17
|
+
'Then retry, or migrate manually by exporting data from the old database.');
|
|
18
|
+
}
|
|
19
|
+
const db = new Database(opts.dbPath);
|
|
20
|
+
try {
|
|
21
|
+
const nodes = db.prepare('SELECT * FROM nodes').all();
|
|
22
|
+
for (const r of nodes) {
|
|
23
|
+
const node = {
|
|
24
|
+
id: r.id, type: r.type, content: r.content,
|
|
25
|
+
sessionId: r.session_id, projectId: r.project_id ?? undefined,
|
|
26
|
+
metadata: JSON.parse(r.metadata || '{}'),
|
|
27
|
+
createdAt: r.created_at, updatedAt: r.updated_at,
|
|
28
|
+
thoughtNumber: r.thought_number ?? undefined,
|
|
29
|
+
totalThoughts: r.total_thoughts ?? undefined,
|
|
30
|
+
branchId: r.branch_id ?? undefined,
|
|
31
|
+
isRevision: r.is_revision === 1,
|
|
32
|
+
revisesThought: r.revises_thought ?? undefined,
|
|
33
|
+
};
|
|
34
|
+
appendLine(opts.nodesPath, node);
|
|
35
|
+
}
|
|
36
|
+
const edges = db.prepare('SELECT * FROM edges').all();
|
|
37
|
+
for (const r of edges) {
|
|
38
|
+
const edge = {
|
|
39
|
+
id: r.id, sourceId: r.source_id, targetId: r.target_id,
|
|
40
|
+
type: r.type, weight: r.weight,
|
|
41
|
+
reasoning: r.reasoning ?? undefined, createdAt: r.created_at,
|
|
42
|
+
};
|
|
43
|
+
appendLine(opts.edgesPath, edge);
|
|
44
|
+
}
|
|
45
|
+
const sessions = db.prepare('SELECT * FROM sessions').all();
|
|
46
|
+
for (const r of sessions) {
|
|
47
|
+
const session = {
|
|
48
|
+
id: r.id, projectId: r.project_id ?? undefined,
|
|
49
|
+
projectPath: r.project_path ?? undefined,
|
|
50
|
+
description: r.description ?? undefined,
|
|
51
|
+
startedAt: r.started_at, lastActiveAt: r.last_active,
|
|
52
|
+
};
|
|
53
|
+
appendLine(opts.sessionsPath, session);
|
|
54
|
+
}
|
|
55
|
+
let skills = [];
|
|
56
|
+
try {
|
|
57
|
+
skills = db.prepare('SELECT * FROM skill_registry').all();
|
|
58
|
+
}
|
|
59
|
+
catch { /* table may not exist in older DBs */ }
|
|
60
|
+
for (const r of skills) {
|
|
61
|
+
const skill = {
|
|
62
|
+
id: r.id, pluginName: r.plugin_name, skillName: r.skill_name,
|
|
63
|
+
verb: r.verb ?? undefined, invocation: r.invocation,
|
|
64
|
+
areas: JSON.parse(r.areas), detects: JSON.parse(r.detects),
|
|
65
|
+
produces: JSON.parse(r.produces), invokes: JSON.parse(r.invokes),
|
|
66
|
+
platform: r.platform ?? undefined,
|
|
67
|
+
};
|
|
68
|
+
appendLine(opts.skillsPath, skill);
|
|
69
|
+
}
|
|
70
|
+
return { nodes: nodes.length, edges: edges.length, sessions: sessions.length, skills: skills.length };
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
db.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface VectorMatch {
|
|
2
|
+
id: string;
|
|
3
|
+
score: number;
|
|
4
|
+
}
|
|
5
|
+
export declare class VectorIndex {
|
|
6
|
+
private readonly vectors;
|
|
7
|
+
load(path: string): void;
|
|
8
|
+
append(path: string, id: string, vec: Float32Array): void;
|
|
9
|
+
search(queryVec: Float32Array, topK: number): VectorMatch[];
|
|
10
|
+
has(id: string): boolean;
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { readFileSync, appendFileSync, existsSync } from 'fs';
|
|
2
|
+
export class VectorIndex {
|
|
3
|
+
vectors = new Map();
|
|
4
|
+
load(path) {
|
|
5
|
+
if (!existsSync(path))
|
|
6
|
+
return;
|
|
7
|
+
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
8
|
+
if (!line.trim())
|
|
9
|
+
continue;
|
|
10
|
+
const { id, vec } = JSON.parse(line);
|
|
11
|
+
this.vectors.set(id, new Float32Array(vec));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
append(path, id, vec) {
|
|
15
|
+
this.vectors.set(id, vec);
|
|
16
|
+
appendFileSync(path, JSON.stringify({ id, vec: Array.from(vec) }) + '\n', 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
search(queryVec, topK) {
|
|
19
|
+
const results = [];
|
|
20
|
+
for (const [id, vec] of this.vectors) {
|
|
21
|
+
results.push({ id, score: dot(queryVec, vec) });
|
|
22
|
+
}
|
|
23
|
+
results.sort((a, b) => b.score - a.score);
|
|
24
|
+
return results.slice(0, topK);
|
|
25
|
+
}
|
|
26
|
+
has(id) {
|
|
27
|
+
return this.vectors.has(id);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function dot(a, b) {
|
|
31
|
+
let sum = 0;
|
|
32
|
+
for (let i = 0; i < a.length; i++)
|
|
33
|
+
sum += a[i] * b[i];
|
|
34
|
+
return sum;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelingmindful/thinking-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.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,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
34
|
+
"@xenova/transformers": "^2.17.2",
|
|
34
35
|
"ajv": "^8.17.1",
|
|
35
|
-
"better-sqlite3": "^12.8.0",
|
|
36
36
|
"gray-matter": "^4.0.3",
|
|
37
37
|
"uuid": "^10.0.0",
|
|
38
38
|
"zod": "^3.23.0"
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"@types/better-sqlite3": "^7.6.13",
|
|
42
42
|
"@types/node": "^25.5.0",
|
|
43
43
|
"@types/uuid": "^10.0.0",
|
|
44
|
+
"better-sqlite3": "^12.8.0",
|
|
44
45
|
"typescript": "^5.5.0",
|
|
45
46
|
"vitest": "^3.0.0"
|
|
46
47
|
}
|