@feelingmindful/thinking-graph 1.11.0 → 1.13.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/engine/seed.js +11 -0
- package/dist/index.js +7 -2
- package/dist/storage/sqlite.d.ts +5 -5
- package/dist/storage/sqlite.js +56 -89
- package/dist/tools/research.d.ts +9 -3
- package/dist/tools/research.js +45 -0
- package/migrations/001-initial.sql +21 -0
- package/package.json +3 -2
package/dist/engine/seed.js
CHANGED
|
@@ -37,6 +37,17 @@ export async function seedSkillRegistry(storage) {
|
|
|
37
37
|
return count;
|
|
38
38
|
}
|
|
39
39
|
export async function seedAll(storage) {
|
|
40
|
+
// Ensure a 'seed' session exists for FK constraint on seed nodes
|
|
41
|
+
const seedSession = await storage.getSession('seed');
|
|
42
|
+
if (!seedSession) {
|
|
43
|
+
const now = new Date().toISOString();
|
|
44
|
+
await storage.insertSession({
|
|
45
|
+
id: 'seed',
|
|
46
|
+
description: 'Seed data session',
|
|
47
|
+
startedAt: now,
|
|
48
|
+
lastActiveAt: now,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
40
51
|
const principles = await seedPrinciples(storage);
|
|
41
52
|
const skills = await seedSkillRegistry(storage);
|
|
42
53
|
return { principles, skills };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
2
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
6
|
import { ThinkingGraph } from './engine/graph.js';
|
|
@@ -17,15 +19,18 @@ import { planSkillsSchema, planSkillsHandler } from './tools/plan-skills.js';
|
|
|
17
19
|
import { executePlanSchema, executePlanHandler } from './tools/execute-plan.js';
|
|
18
20
|
import { executeSkillsSchema, executeSkillsHandler } from './tools/execute-skills.js';
|
|
19
21
|
// Legacy compat shim removed — use `think` tool directly
|
|
22
|
+
function expandHome(p) {
|
|
23
|
+
return p.startsWith('~') ? join(homedir(), p.slice(1)) : p;
|
|
24
|
+
}
|
|
20
25
|
// ─── Storage setup ───────────────────────────────────────
|
|
21
26
|
const memoryOnly = process.env.THINKING_GRAPH_MEMORY_ONLY === 'true';
|
|
22
27
|
const storage = memoryOnly
|
|
23
28
|
? new InMemoryAdapter()
|
|
24
29
|
: new SQLiteAdapter({
|
|
25
|
-
dbPath: process.env.THINKING_GRAPH_PROJECT_DB || '.premium/thinking.db',
|
|
30
|
+
dbPath: expandHome(process.env.THINKING_GRAPH_PROJECT_DB || '.premium/thinking.db'),
|
|
26
31
|
});
|
|
27
32
|
// ─── Vault bridge ────────────────────────────────────────
|
|
28
|
-
const vaultPath = process.env.THINKING_GRAPH_VAULT_PATH || '~/Documents/Obsidian/Dev';
|
|
33
|
+
const vaultPath = expandHome(process.env.THINKING_GRAPH_VAULT_PATH || '~/Documents/Obsidian/Dev');
|
|
29
34
|
const vault = new VaultBridge(vaultPath);
|
|
30
35
|
// Derive project slug from env or DB path (e.g. "feeling-mindful-plugins")
|
|
31
36
|
const projectSlug = process.env.THINKING_GRAPH_PROJECT_SLUG
|
package/dist/storage/sqlite.d.ts
CHANGED
|
@@ -5,14 +5,14 @@ export interface SQLiteAdapterOpts {
|
|
|
5
5
|
}
|
|
6
6
|
export declare class SQLiteAdapter implements StorageAdapter {
|
|
7
7
|
private db;
|
|
8
|
-
private dbPath;
|
|
8
|
+
private readonly dbPath;
|
|
9
9
|
constructor(opts: SQLiteAdapterOpts);
|
|
10
10
|
initialize(): Promise<void>;
|
|
11
11
|
close(): Promise<void>;
|
|
12
|
-
private
|
|
13
|
-
private
|
|
14
|
-
private
|
|
15
|
-
private
|
|
12
|
+
private backfillFts;
|
|
13
|
+
private run;
|
|
14
|
+
private get;
|
|
15
|
+
private all;
|
|
16
16
|
insertNode(node: Node): Promise<void>;
|
|
17
17
|
getNode(id: string): Promise<Node | null>;
|
|
18
18
|
queryNodes(query: NodeQuery): Promise<PaginatedResult<Node>>;
|
package/dist/storage/sqlite.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { readFileSync
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { readFileSync } 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
|
-
}
|
|
15
8
|
export class SQLiteAdapter {
|
|
16
9
|
db;
|
|
17
10
|
dbPath;
|
|
@@ -21,72 +14,39 @@ export class SQLiteAdapter {
|
|
|
21
14
|
async initialize() {
|
|
22
15
|
const dir = dirname(this.dbPath);
|
|
23
16
|
mkdirSync(dir, { recursive: true });
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
else {
|
|
30
|
-
this.db = new SQL.Database();
|
|
31
|
-
}
|
|
32
|
-
this.db.run('PRAGMA foreign_keys = ON');
|
|
33
|
-
// sql.js uses db.export() which serializes the full DB — WAL mode is not
|
|
34
|
-
// supported and causes data loss. Force DELETE journal mode.
|
|
35
|
-
this.db.run('PRAGMA journal_mode = DELETE');
|
|
17
|
+
this.db = new Database(this.dbPath);
|
|
18
|
+
this.db.pragma('journal_mode = WAL');
|
|
19
|
+
this.db.pragma('foreign_keys = ON');
|
|
20
|
+
this.db.pragma('busy_timeout = 5000');
|
|
36
21
|
// Run migration (multi-statement)
|
|
37
22
|
const sql = readFileSync(MIGRATION_PATH, 'utf-8');
|
|
38
23
|
this.db.exec(sql);
|
|
39
|
-
|
|
24
|
+
// Backfill FTS5 if nodes exist but index is empty
|
|
25
|
+
this.backfillFts();
|
|
40
26
|
}
|
|
41
27
|
async close() {
|
|
42
|
-
this.persist();
|
|
43
28
|
this.db.close();
|
|
44
29
|
}
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
30
|
+
backfillFts() {
|
|
31
|
+
const ftsCount = this.db.prepare('SELECT COUNT(*) as cnt FROM nodes_fts').get().cnt;
|
|
32
|
+
const nodeCount = this.db.prepare('SELECT COUNT(*) as cnt FROM nodes').get().cnt;
|
|
33
|
+
if (ftsCount === 0 && nodeCount > 0) {
|
|
34
|
+
this.db.exec('INSERT INTO nodes_fts(rowid, content) SELECT rowid, content FROM nodes');
|
|
35
|
+
}
|
|
49
36
|
}
|
|
50
37
|
// ─── Helpers ──────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
this.db.run(
|
|
53
|
-
this.persist();
|
|
38
|
+
run(sql, params = []) {
|
|
39
|
+
this.db.prepare(sql).run(...params);
|
|
54
40
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
stmt.bind(params);
|
|
58
|
-
if (stmt.step()) {
|
|
59
|
-
const columns = stmt.getColumnNames();
|
|
60
|
-
const values = stmt.get();
|
|
61
|
-
stmt.free();
|
|
62
|
-
const row = {};
|
|
63
|
-
for (let i = 0; i < columns.length; i++) {
|
|
64
|
-
row[columns[i]] = values[i];
|
|
65
|
-
}
|
|
66
|
-
return row;
|
|
67
|
-
}
|
|
68
|
-
stmt.free();
|
|
69
|
-
return undefined;
|
|
41
|
+
get(sql, params = []) {
|
|
42
|
+
return this.db.prepare(sql).get(...params);
|
|
70
43
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
stmt.bind(params);
|
|
74
|
-
const rows = [];
|
|
75
|
-
while (stmt.step()) {
|
|
76
|
-
const columns = stmt.getColumnNames();
|
|
77
|
-
const values = stmt.get();
|
|
78
|
-
const row = {};
|
|
79
|
-
for (let i = 0; i < columns.length; i++) {
|
|
80
|
-
row[columns[i]] = values[i];
|
|
81
|
-
}
|
|
82
|
-
rows.push(row);
|
|
83
|
-
}
|
|
84
|
-
stmt.free();
|
|
85
|
-
return rows;
|
|
44
|
+
all(sql, params = []) {
|
|
45
|
+
return this.db.prepare(sql).all(...params);
|
|
86
46
|
}
|
|
87
47
|
// ─── Nodes ─────────────────────────────────────────────
|
|
88
48
|
async insertNode(node) {
|
|
89
|
-
this.
|
|
49
|
+
this.run(`
|
|
90
50
|
INSERT INTO nodes (id, type, content, session_id, project_id, metadata,
|
|
91
51
|
created_at, updated_at, thought_number, total_thoughts, branch_id,
|
|
92
52
|
is_revision, revises_thought)
|
|
@@ -100,7 +60,7 @@ export class SQLiteAdapter {
|
|
|
100
60
|
]);
|
|
101
61
|
}
|
|
102
62
|
async getNode(id) {
|
|
103
|
-
const row = this.
|
|
63
|
+
const row = this.get('SELECT * FROM nodes WHERE id = ?', [id]);
|
|
104
64
|
return row ? this.rowToNode(row) : null;
|
|
105
65
|
}
|
|
106
66
|
async queryNodes(query) {
|
|
@@ -128,11 +88,11 @@ export class SQLiteAdapter {
|
|
|
128
88
|
params.push(`%${query.query}%`);
|
|
129
89
|
}
|
|
130
90
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
131
|
-
const countRow = this.
|
|
91
|
+
const countRow = this.get(`SELECT COUNT(*) as cnt FROM nodes ${where}`, params);
|
|
132
92
|
const totalCount = countRow.cnt;
|
|
133
93
|
const limit = query.limit ?? 20;
|
|
134
94
|
const offset = query.offset ?? 0;
|
|
135
|
-
const rows = this.
|
|
95
|
+
const rows = this.all(`SELECT * FROM nodes ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, limit, offset]);
|
|
136
96
|
return {
|
|
137
97
|
items: rows.map(r => this.rowToNode(r)),
|
|
138
98
|
totalCount,
|
|
@@ -140,18 +100,27 @@ export class SQLiteAdapter {
|
|
|
140
100
|
};
|
|
141
101
|
}
|
|
142
102
|
async searchContent(text, limit = 20) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
103
|
+
try {
|
|
104
|
+
const rows = this.all(`
|
|
105
|
+
SELECT n.* FROM nodes n
|
|
106
|
+
WHERE n.rowid IN (SELECT rowid FROM nodes_fts WHERE nodes_fts MATCH ?)
|
|
107
|
+
ORDER BY n.created_at DESC
|
|
108
|
+
LIMIT ?
|
|
109
|
+
`, [text, limit]);
|
|
110
|
+
return rows.map(r => this.rowToNode(r));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Fallback for queries that aren't valid FTS5 syntax
|
|
114
|
+
const rows = this.all(`
|
|
115
|
+
SELECT * FROM nodes WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?
|
|
116
|
+
`, [`%${text}%`, limit]);
|
|
117
|
+
return rows.map(r => this.rowToNode(r));
|
|
118
|
+
}
|
|
150
119
|
}
|
|
151
120
|
// ─── Edges ─────────────────────────────────────────────
|
|
152
121
|
async insertEdge(edge) {
|
|
153
122
|
try {
|
|
154
|
-
this.
|
|
123
|
+
this.run(`
|
|
155
124
|
INSERT INTO edges (id, source_id, target_id, type, weight, reasoning, created_at)
|
|
156
125
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
157
126
|
`, [
|
|
@@ -172,19 +141,17 @@ export class SQLiteAdapter {
|
|
|
172
141
|
? 'SELECT * FROM edges WHERE source_id = ? AND type = ?'
|
|
173
142
|
: 'SELECT * FROM edges WHERE source_id = ?';
|
|
174
143
|
const params = type ? [nodeId, type] : [nodeId];
|
|
175
|
-
|
|
176
|
-
return rows.map(r => this.rowToEdge(r));
|
|
144
|
+
return this.all(sql, params).map(r => this.rowToEdge(r));
|
|
177
145
|
}
|
|
178
146
|
async getEdgesTo(nodeId, type) {
|
|
179
147
|
const sql = type
|
|
180
148
|
? 'SELECT * FROM edges WHERE target_id = ? AND type = ?'
|
|
181
149
|
: 'SELECT * FROM edges WHERE target_id = ?';
|
|
182
150
|
const params = type ? [nodeId, type] : [nodeId];
|
|
183
|
-
|
|
184
|
-
return rows.map(r => this.rowToEdge(r));
|
|
151
|
+
return this.all(sql, params).map(r => this.rowToEdge(r));
|
|
185
152
|
}
|
|
186
153
|
async traverseEdges(startId, type, maxDepth) {
|
|
187
|
-
const rows = this.
|
|
154
|
+
const rows = this.all(`
|
|
188
155
|
WITH RECURSIVE chain(id, depth) AS (
|
|
189
156
|
SELECT target_id, 1
|
|
190
157
|
FROM edges
|
|
@@ -203,7 +170,7 @@ export class SQLiteAdapter {
|
|
|
203
170
|
}
|
|
204
171
|
// ─── Sessions ──────────────────────────────────────────
|
|
205
172
|
async insertSession(session) {
|
|
206
|
-
this.
|
|
173
|
+
this.run(`
|
|
207
174
|
INSERT INTO sessions (id, project_id, project_path, description, started_at, last_active)
|
|
208
175
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
209
176
|
`, [
|
|
@@ -212,7 +179,7 @@ export class SQLiteAdapter {
|
|
|
212
179
|
]);
|
|
213
180
|
}
|
|
214
181
|
async getSession(id) {
|
|
215
|
-
const row = this.
|
|
182
|
+
const row = this.get('SELECT * FROM sessions WHERE id = ?', [id]);
|
|
216
183
|
return row ? this.rowToSession(row) : null;
|
|
217
184
|
}
|
|
218
185
|
async updateSession(id, fields) {
|
|
@@ -228,12 +195,12 @@ export class SQLiteAdapter {
|
|
|
228
195
|
}
|
|
229
196
|
if (sets.length > 0) {
|
|
230
197
|
params.push(id);
|
|
231
|
-
this.
|
|
198
|
+
this.run(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`, params);
|
|
232
199
|
}
|
|
233
200
|
}
|
|
234
201
|
// ─── Skill Registry ───────────────────────────────────
|
|
235
202
|
async insertSkill(entry) {
|
|
236
|
-
this.
|
|
203
|
+
this.run(`
|
|
237
204
|
INSERT OR IGNORE INTO skill_registry
|
|
238
205
|
(id, plugin_name, skill_name, verb, invocation, areas, detects, produces, invokes, platform)
|
|
239
206
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -267,7 +234,7 @@ export class SQLiteAdapter {
|
|
|
267
234
|
params.push(filter.platform);
|
|
268
235
|
}
|
|
269
236
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
270
|
-
const rows = this.
|
|
237
|
+
const rows = this.all(`SELECT * FROM skill_registry ${where}`, params);
|
|
271
238
|
return rows.map(r => ({
|
|
272
239
|
id: r.id,
|
|
273
240
|
pluginName: r.plugin_name,
|
|
@@ -299,18 +266,18 @@ export class SQLiteAdapter {
|
|
|
299
266
|
params.push(...types);
|
|
300
267
|
}
|
|
301
268
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
302
|
-
const nodeRows = this.
|
|
269
|
+
const nodeRows = this.all(`SELECT * FROM nodes ${where}`, params);
|
|
303
270
|
const nodes = nodeRows.map(r => this.rowToNode(r));
|
|
304
271
|
let edges = [];
|
|
305
272
|
if (opts?.includeEdges !== false) {
|
|
306
273
|
const nodeIds = nodes.map(n => n.id);
|
|
307
274
|
if (nodeIds.length > 0) {
|
|
308
275
|
const placeholders = nodeIds.map(() => '?').join(',');
|
|
309
|
-
const edgeRows = this.
|
|
276
|
+
const edgeRows = this.all(`SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`, [...nodeIds, ...nodeIds]);
|
|
310
277
|
edges = edgeRows.map(r => this.rowToEdge(r));
|
|
311
278
|
}
|
|
312
279
|
}
|
|
313
|
-
const sessionRows = this.
|
|
280
|
+
const sessionRows = this.all('SELECT * FROM sessions');
|
|
314
281
|
return {
|
|
315
282
|
exportedAt: new Date().toISOString(),
|
|
316
283
|
nodeCount: nodes.length,
|
|
@@ -323,15 +290,15 @@ export class SQLiteAdapter {
|
|
|
323
290
|
// ─── Stats ─────────────────────────────────────────────
|
|
324
291
|
async getStats() {
|
|
325
292
|
const nodesByType = {};
|
|
326
|
-
const ntRows = this.
|
|
293
|
+
const ntRows = this.all('SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type');
|
|
327
294
|
for (const r of ntRows)
|
|
328
295
|
nodesByType[r.type] = r.cnt;
|
|
329
296
|
const edgesByType = {};
|
|
330
|
-
const etRows = this.
|
|
297
|
+
const etRows = this.all('SELECT type, COUNT(*) as cnt FROM edges GROUP BY type');
|
|
331
298
|
for (const r of etRows)
|
|
332
299
|
edgesByType[r.type] = r.cnt;
|
|
333
|
-
const totalNodes = this.
|
|
334
|
-
const totalEdges = this.
|
|
300
|
+
const totalNodes = this.get('SELECT COUNT(*) as cnt FROM nodes').cnt;
|
|
301
|
+
const totalEdges = this.get('SELECT COUNT(*) as cnt FROM edges').cnt;
|
|
335
302
|
return {
|
|
336
303
|
totalNodes,
|
|
337
304
|
totalEdges,
|
package/dist/tools/research.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ThinkingGraph } from '../engine/graph.js';
|
|
|
3
3
|
import { type VaultBridge } from '../vault/bridge.js';
|
|
4
4
|
export declare const researchSchema: z.ZodObject<{
|
|
5
5
|
query: z.ZodString;
|
|
6
|
-
intent: z.ZodDefault<z.ZodEnum<["fact_check", "explore", "compare", "how_to", "current_state"]>>;
|
|
6
|
+
intent: z.ZodDefault<z.ZodEnum<["fact_check", "explore", "compare", "how_to", "current_state", "grounded_qa"]>>;
|
|
7
7
|
context: z.ZodOptional<z.ZodString>;
|
|
8
8
|
researchId: z.ZodOptional<z.ZodString>;
|
|
9
9
|
findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -23,9 +23,11 @@ export declare const researchSchema: z.ZodObject<{
|
|
|
23
23
|
scrapeUrls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
24
24
|
recencyFilter: z.ZodOptional<z.ZodEnum<["hour", "day", "week", "month", "year"]>>;
|
|
25
25
|
domainFilter: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
26
|
+
notebookId: z.ZodOptional<z.ZodString>;
|
|
27
|
+
skipGrounded: z.ZodOptional<z.ZodEffects<z.ZodBoolean, boolean, unknown>>;
|
|
26
28
|
}, "strip", z.ZodTypeAny, {
|
|
27
29
|
query: string;
|
|
28
|
-
intent: "fact_check" | "explore" | "compare" | "how_to" | "current_state";
|
|
30
|
+
intent: "fact_check" | "explore" | "compare" | "how_to" | "current_state" | "grounded_qa";
|
|
29
31
|
projectId?: string | undefined;
|
|
30
32
|
context?: string | undefined;
|
|
31
33
|
researchId?: string | undefined;
|
|
@@ -37,10 +39,12 @@ export declare const researchSchema: z.ZodObject<{
|
|
|
37
39
|
scrapeUrls?: string[] | undefined;
|
|
38
40
|
recencyFilter?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
39
41
|
domainFilter?: string[] | undefined;
|
|
42
|
+
notebookId?: string | undefined;
|
|
43
|
+
skipGrounded?: boolean | undefined;
|
|
40
44
|
}, {
|
|
41
45
|
query: string;
|
|
42
46
|
projectId?: string | undefined;
|
|
43
|
-
intent?: "fact_check" | "explore" | "compare" | "how_to" | "current_state" | undefined;
|
|
47
|
+
intent?: "fact_check" | "explore" | "compare" | "how_to" | "current_state" | "grounded_qa" | undefined;
|
|
44
48
|
context?: string | undefined;
|
|
45
49
|
researchId?: string | undefined;
|
|
46
50
|
findings?: {
|
|
@@ -51,6 +55,8 @@ export declare const researchSchema: z.ZodObject<{
|
|
|
51
55
|
scrapeUrls?: string[] | undefined;
|
|
52
56
|
recencyFilter?: "hour" | "day" | "week" | "month" | "year" | undefined;
|
|
53
57
|
domainFilter?: string[] | undefined;
|
|
58
|
+
notebookId?: string | undefined;
|
|
59
|
+
skipGrounded?: unknown;
|
|
54
60
|
}>;
|
|
55
61
|
export type ResearchInput = z.infer<typeof researchSchema>;
|
|
56
62
|
export declare function researchHandler(graph: ThinkingGraph, input: ResearchInput, vault?: VaultBridge, projectSlug?: string): Promise<{
|
package/dist/tools/research.js
CHANGED
|
@@ -7,7 +7,11 @@ const RESEARCH_INTENTS = [
|
|
|
7
7
|
'compare', // Compare options, libraries, approaches
|
|
8
8
|
'how_to', // Find implementation guidance
|
|
9
9
|
'current_state', // Get current state of something (version, status, etc.)
|
|
10
|
+
'grounded_qa', // Query a curated knowledge base (NotebookLM notebook) — zero hallucinations
|
|
10
11
|
];
|
|
12
|
+
// Intents that should preferentially route through a NotebookLM notebook when available.
|
|
13
|
+
// These are the cases where a curated, citation-backed source is most valuable.
|
|
14
|
+
const GROUNDED_PREFERRED_INTENTS = new Set(['how_to', 'fact_check', 'current_state', 'grounded_qa']);
|
|
11
15
|
const findingSchema = z.object({
|
|
12
16
|
content: z.string().describe('What was found'),
|
|
13
17
|
source: z.string().optional().describe('URL or citation'),
|
|
@@ -26,10 +30,51 @@ export const researchSchema = z.object({
|
|
|
26
30
|
scrapeUrls: z.array(z.string()).optional().describe('Specific URLs to scrape with Firecrawl'),
|
|
27
31
|
recencyFilter: z.enum(['hour', 'day', 'week', 'month', 'year']).optional().describe('How recent results should be'),
|
|
28
32
|
domainFilter: z.array(z.string()).optional().describe('Restrict to these domains'),
|
|
33
|
+
notebookId: z.string().optional().describe('Specific NotebookLM notebook ID to query. If omitted, action plan will list notebooks first so the caller can pick the best match.'),
|
|
34
|
+
skipGrounded: coerceBool.optional().describe('Skip NotebookLM step even when it would normally auto-prepend'),
|
|
29
35
|
});
|
|
36
|
+
function buildGroundedSteps(input) {
|
|
37
|
+
const steps = [];
|
|
38
|
+
const query = input.query;
|
|
39
|
+
// If caller specified a notebookId, select it then ask directly.
|
|
40
|
+
if (input.notebookId) {
|
|
41
|
+
steps.push({
|
|
42
|
+
tool: 'mcp__notebooklm__select_notebook',
|
|
43
|
+
description: 'Select the specified NotebookLM notebook',
|
|
44
|
+
args: { id: input.notebookId },
|
|
45
|
+
});
|
|
46
|
+
steps.push({
|
|
47
|
+
tool: 'mcp__notebooklm__ask_question',
|
|
48
|
+
description: 'Ask NotebookLM (grounded, citation-backed)',
|
|
49
|
+
args: { question: query },
|
|
50
|
+
});
|
|
51
|
+
return steps;
|
|
52
|
+
}
|
|
53
|
+
// Otherwise: search the library for the best match, then the agent picks one and asks.
|
|
54
|
+
steps.push({
|
|
55
|
+
tool: 'mcp__notebooklm__search_notebooks',
|
|
56
|
+
description: 'Find a NotebookLM notebook matching this query. Skip remaining NotebookLM steps if no match.',
|
|
57
|
+
args: { query },
|
|
58
|
+
});
|
|
59
|
+
steps.push({
|
|
60
|
+
tool: 'mcp__notebooklm__ask_question',
|
|
61
|
+
description: 'Ask the selected NotebookLM notebook. Only run after search_notebooks returns a match; fall back to the web-grounded steps below if the library has no relevant notebook.',
|
|
62
|
+
args: { question: query },
|
|
63
|
+
});
|
|
64
|
+
return steps;
|
|
65
|
+
}
|
|
30
66
|
function buildActionPlan(input) {
|
|
31
67
|
const steps = [];
|
|
32
68
|
const query = input.query;
|
|
69
|
+
// Grounded NotebookLM path (preferred for how_to / fact_check / current_state / grounded_qa).
|
|
70
|
+
const shouldPrependGrounded = !input.skipGrounded && GROUNDED_PREFERRED_INTENTS.has(input.intent ?? 'explore');
|
|
71
|
+
if (shouldPrependGrounded) {
|
|
72
|
+
steps.push(...buildGroundedSteps(input));
|
|
73
|
+
}
|
|
74
|
+
// grounded_qa is NotebookLM-only — no web fallback steps.
|
|
75
|
+
if (input.intent === 'grounded_qa') {
|
|
76
|
+
return steps;
|
|
77
|
+
}
|
|
33
78
|
// Choose Perplexity tool based on intent
|
|
34
79
|
switch (input.intent) {
|
|
35
80
|
case 'fact_check':
|
|
@@ -84,3 +84,24 @@ CREATE TABLE IF NOT EXISTS skill_registry (
|
|
|
84
84
|
|
|
85
85
|
UNIQUE(plugin_name, skill_name)
|
|
86
86
|
);
|
|
87
|
+
|
|
88
|
+
-- ─── Full-Text Search ────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(
|
|
91
|
+
content,
|
|
92
|
+
content='nodes',
|
|
93
|
+
content_rowid='rowid'
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
CREATE TRIGGER IF NOT EXISTS nodes_ai AFTER INSERT ON nodes BEGIN
|
|
97
|
+
INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
98
|
+
END;
|
|
99
|
+
|
|
100
|
+
CREATE TRIGGER IF NOT EXISTS nodes_au AFTER UPDATE ON nodes BEGIN
|
|
101
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
102
|
+
INSERT INTO nodes_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
103
|
+
END;
|
|
104
|
+
|
|
105
|
+
CREATE TRIGGER IF NOT EXISTS nodes_ad AFTER DELETE ON nodes BEGIN
|
|
106
|
+
INSERT INTO nodes_fts(nodes_fts, rowid, content) VALUES ('delete', old.rowid, old.content);
|
|
107
|
+
END;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelingmindful/thinking-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.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,12 +31,13 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
34
|
+
"better-sqlite3": "^12.8.0",
|
|
34
35
|
"gray-matter": "^4.0.3",
|
|
35
|
-
"sql.js": "^1.12.0",
|
|
36
36
|
"uuid": "^10.0.0",
|
|
37
37
|
"zod": "^3.23.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
40
41
|
"@types/node": "^25.5.0",
|
|
41
42
|
"@types/uuid": "^10.0.0",
|
|
42
43
|
"typescript": "^5.5.0",
|