@feelingmindful/thinking-graph 1.14.1 → 1.15.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/dist/engine/graph.js +1 -1
- package/dist/index.js +6 -6
- 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/dist/tools/plan-skills.d.ts +2 -2
- package/dist/tools/recommend-skills.d.ts +2 -2
- package/dist/tools/research.js +59 -42
- package/dist/tools/route-skills.d.ts +2 -2
- package/dist/tools/think.js +1 -1
- package/package.json +3 -2
package/dist/engine/graph.js
CHANGED
|
@@ -257,7 +257,7 @@ export class ThinkingGraph {
|
|
|
257
257
|
{ name: 'relate', used: relateUsed, purpose: 'connect nodes with typed edges' },
|
|
258
258
|
{ name: 'recall', used: null, purpose: 'query prior knowledge (not trackable)' },
|
|
259
259
|
{ name: 'learn', used: learnUsed, purpose: 'persist insights cross-project' },
|
|
260
|
-
{ name: 'research', used: researchUsed, purpose: 'web research via
|
|
260
|
+
{ name: 'research', used: researchUsed, purpose: 'web research via parallel-cli' },
|
|
261
261
|
];
|
|
262
262
|
lines.push('', '## Tool Utilization');
|
|
263
263
|
for (const t of tools) {
|
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');
|
|
@@ -56,7 +56,7 @@ server.tool('relate', 'Create a typed, directional relationship between two node
|
|
|
56
56
|
server.tool('recall', 'Query the thinking graph and Obsidian vault — search by text, filter by type, traverse relationships, or search across projects.', recallSchema.shape, async (input) => recallHandler(graph, input, vault, projectSlug));
|
|
57
57
|
server.tool('learn', 'Store durable knowledge — code facts, tech debt, insights, principles. Writes to both SQLite graph and Obsidian vault.', learnSchema.shape, async (input) => learnHandler(graph, input, vault, projectSlug));
|
|
58
58
|
server.tool('export', 'Export the thinking graph as JSON or a human-readable markdown summary.', exportSchema.shape, async (input) => exportHandler(graph, input));
|
|
59
|
-
server.tool('research', 'Research a topic using
|
|
59
|
+
server.tool('research', 'Research a topic using parallel-cli, then ingest findings into the graph and Obsidian vault. Two-phase: call once to get an action plan, then again with findings to store them.', researchSchema.shape, async (input) => researchHandler(graph, input, vault, projectSlug));
|
|
60
60
|
server.tool('recommend-skills', 'Recommend installed marketplace skills by area, verb, platform, or what they produce/detect. Use during reasoning to find skills that can help with the current task.', recommendSkillsSchema.shape, async (input) => recommendSkillsHandler(graph, input));
|
|
61
61
|
server.tool('route-skills', 'Shortlist and rank installed skills for a task using routing heuristics based on platform, verb, areas, detections, and graph context.', routeSkillsSchema.shape, async (input) => routeSkillsHandler(graph, input));
|
|
62
62
|
server.tool('plan-skills', 'Convert a request into an ordered skill execution plan with explicit approval gates before execution. This tool plans but does not execute skills.', planSkillsSchema.shape, async (input) => planSkillsHandler(graph, input));
|
|
@@ -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
|
+
}
|
|
@@ -13,7 +13,7 @@ export declare const planSkillsSchema: z.ZodObject<{
|
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
14
|
request: string;
|
|
15
15
|
areas?: string[] | undefined;
|
|
16
|
-
platform?: "all" | "
|
|
16
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
17
17
|
currentState?: "unknown" | "new" | "existing" | undefined;
|
|
18
18
|
goalVerb?: string | undefined;
|
|
19
19
|
detectedNeeds?: string[] | undefined;
|
|
@@ -23,7 +23,7 @@ export declare const planSkillsSchema: z.ZodObject<{
|
|
|
23
23
|
}, {
|
|
24
24
|
request: string;
|
|
25
25
|
areas?: string[] | undefined;
|
|
26
|
-
platform?: "all" | "
|
|
26
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
27
27
|
currentState?: "unknown" | "new" | "existing" | undefined;
|
|
28
28
|
goalVerb?: string | undefined;
|
|
29
29
|
detectedNeeds?: string[] | undefined;
|
|
@@ -11,14 +11,14 @@ export declare const recommendSkillsSchema: z.ZodObject<{
|
|
|
11
11
|
verb?: string | undefined;
|
|
12
12
|
detects?: string | undefined;
|
|
13
13
|
produces?: string | undefined;
|
|
14
|
-
platform?: "all" | "
|
|
14
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
15
15
|
area?: string | undefined;
|
|
16
16
|
query?: string | undefined;
|
|
17
17
|
}, {
|
|
18
18
|
verb?: string | undefined;
|
|
19
19
|
detects?: string | undefined;
|
|
20
20
|
produces?: string | undefined;
|
|
21
|
-
platform?: "all" | "
|
|
21
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
22
22
|
area?: string | undefined;
|
|
23
23
|
query?: string | undefined;
|
|
24
24
|
}>;
|
package/dist/tools/research.js
CHANGED
|
@@ -27,12 +27,44 @@ export const researchSchema = z.object({
|
|
|
27
27
|
findings: z.array(findingSchema).optional().describe('Results to store (phase 2)'),
|
|
28
28
|
// Options
|
|
29
29
|
projectId: z.string().optional(),
|
|
30
|
-
scrapeUrls: z.array(z.string()).optional().describe('Specific URLs to
|
|
30
|
+
scrapeUrls: z.array(z.string()).optional().describe('Specific URLs to extract with `parallel-cli extract`'),
|
|
31
31
|
recencyFilter: z.enum(['hour', 'day', 'week', 'month', 'year']).optional().describe('How recent results should be'),
|
|
32
32
|
domainFilter: z.array(z.string()).optional().describe('Restrict to these domains'),
|
|
33
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
34
|
skipGrounded: coerceBool.optional().describe('Skip NotebookLM step even when it would normally auto-prepend'),
|
|
35
35
|
});
|
|
36
|
+
// parallel-cli is a shell CLI (run via Bash), not an MCP. Every research/scrape
|
|
37
|
+
// step shells out to `parallel-cli`. Recency filters map to `--after-date`.
|
|
38
|
+
const RECENCY_TO_DAYS = {
|
|
39
|
+
hour: 1,
|
|
40
|
+
day: 1,
|
|
41
|
+
week: 7,
|
|
42
|
+
month: 30,
|
|
43
|
+
year: 365,
|
|
44
|
+
};
|
|
45
|
+
function afterDateFor(recency) {
|
|
46
|
+
if (!recency)
|
|
47
|
+
return undefined;
|
|
48
|
+
const days = RECENCY_TO_DAYS[recency];
|
|
49
|
+
if (!days)
|
|
50
|
+
return undefined;
|
|
51
|
+
const d = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
|
|
52
|
+
return d.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
53
|
+
}
|
|
54
|
+
// Shell-quote a single argument so queries with spaces/quotes stay intact.
|
|
55
|
+
function shq(value) {
|
|
56
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
57
|
+
}
|
|
58
|
+
function searchFlags(input, recencyDefault) {
|
|
59
|
+
const flags = [];
|
|
60
|
+
const afterDate = afterDateFor(input.recencyFilter ?? recencyDefault);
|
|
61
|
+
if (afterDate)
|
|
62
|
+
flags.push(`--after-date ${afterDate}`);
|
|
63
|
+
if (input.domainFilter?.length) {
|
|
64
|
+
flags.push(`--include-domains ${input.domainFilter.map(shq).join(',')}`);
|
|
65
|
+
}
|
|
66
|
+
return flags.length ? ` ${flags.join(' ')}` : '';
|
|
67
|
+
}
|
|
36
68
|
function buildGroundedSteps(input) {
|
|
37
69
|
const steps = [];
|
|
38
70
|
const query = input.query;
|
|
@@ -75,90 +107,74 @@ function buildActionPlan(input) {
|
|
|
75
107
|
if (input.intent === 'grounded_qa') {
|
|
76
108
|
return steps;
|
|
77
109
|
}
|
|
78
|
-
// Choose
|
|
110
|
+
// Choose the parallel-cli command based on intent.
|
|
111
|
+
// search → facts/quick; research run → deep multi-source/reasoning.
|
|
79
112
|
switch (input.intent) {
|
|
80
113
|
case 'fact_check':
|
|
81
114
|
steps.push({
|
|
82
|
-
tool: '
|
|
83
|
-
description: 'Quick fact check with citations',
|
|
115
|
+
tool: 'Bash',
|
|
116
|
+
description: 'Quick fact check with citations via parallel-cli search',
|
|
84
117
|
args: {
|
|
85
|
-
|
|
86
|
-
search_context_size: 'medium',
|
|
87
|
-
...(input.recencyFilter && { search_recency_filter: input.recencyFilter }),
|
|
88
|
-
...(input.domainFilter && { search_domain_filter: input.domainFilter }),
|
|
118
|
+
command: `parallel-cli search ${shq(query)}${searchFlags(input)}`,
|
|
89
119
|
},
|
|
90
120
|
});
|
|
91
121
|
break;
|
|
92
122
|
case 'compare':
|
|
93
123
|
steps.push({
|
|
94
|
-
tool: '
|
|
95
|
-
description: 'Step-by-step comparison with web grounding',
|
|
124
|
+
tool: 'Bash',
|
|
125
|
+
description: 'Step-by-step comparison with web grounding via parallel-cli research run',
|
|
96
126
|
args: {
|
|
97
|
-
|
|
98
|
-
search_context_size: 'high',
|
|
99
|
-
...(input.recencyFilter && { search_recency_filter: input.recencyFilter }),
|
|
100
|
-
...(input.domainFilter && { search_domain_filter: input.domainFilter }),
|
|
127
|
+
command: `parallel-cli research run --text ${shq(query)} --text-description ${shq('step-by-step reasoning')} --processor pro -o .premium/research/research-compare`,
|
|
101
128
|
},
|
|
102
129
|
});
|
|
103
130
|
break;
|
|
104
131
|
case 'explore':
|
|
105
132
|
steps.push({
|
|
106
|
-
tool: '
|
|
107
|
-
description: 'Deep multi-source research (30s+)',
|
|
133
|
+
tool: 'Bash',
|
|
134
|
+
description: 'Deep multi-source research (30s+) via parallel-cli research run',
|
|
108
135
|
args: {
|
|
109
|
-
|
|
110
|
-
reasoning_effort: 'medium',
|
|
136
|
+
command: `parallel-cli research run --text ${shq(query)} --processor pro -o .premium/research/research-explore`,
|
|
111
137
|
},
|
|
112
138
|
});
|
|
113
139
|
break;
|
|
114
140
|
case 'how_to':
|
|
115
141
|
steps.push({
|
|
116
|
-
tool: '
|
|
117
|
-
description: 'Find implementation guidance',
|
|
142
|
+
tool: 'Bash',
|
|
143
|
+
description: 'Find implementation guidance via parallel-cli search',
|
|
118
144
|
args: {
|
|
119
|
-
|
|
120
|
-
search_context_size: 'high',
|
|
121
|
-
...(input.recencyFilter && { search_recency_filter: input.recencyFilter }),
|
|
122
|
-
...(input.domainFilter && { search_domain_filter: input.domainFilter }),
|
|
145
|
+
command: `parallel-cli search ${shq(query)} --mode advanced${searchFlags(input)}`,
|
|
123
146
|
},
|
|
124
147
|
});
|
|
125
148
|
break;
|
|
126
149
|
case 'current_state':
|
|
127
150
|
steps.push({
|
|
128
|
-
tool: '
|
|
129
|
-
description: 'Get current state with recency filter',
|
|
151
|
+
tool: 'Bash',
|
|
152
|
+
description: 'Get current state with recency filter via parallel-cli search',
|
|
130
153
|
args: {
|
|
131
|
-
|
|
132
|
-
search_recency_filter: input.recencyFilter ?? 'week',
|
|
133
|
-
search_context_size: 'medium',
|
|
134
|
-
...(input.domainFilter && { search_domain_filter: input.domainFilter }),
|
|
154
|
+
command: `parallel-cli search ${shq(query)}${searchFlags(input, 'week')}`,
|
|
135
155
|
},
|
|
136
156
|
});
|
|
137
157
|
break;
|
|
138
158
|
}
|
|
139
|
-
// If specific URLs provided, add
|
|
159
|
+
// If specific URLs provided, add parallel-cli extract steps
|
|
140
160
|
if (input.scrapeUrls?.length) {
|
|
141
161
|
for (const url of input.scrapeUrls) {
|
|
142
162
|
steps.push({
|
|
143
|
-
tool: '
|
|
144
|
-
description: `
|
|
163
|
+
tool: 'Bash',
|
|
164
|
+
description: `Extract clean markdown from ${url} via parallel-cli extract`,
|
|
145
165
|
args: {
|
|
146
|
-
url
|
|
147
|
-
formats: ['markdown'],
|
|
148
|
-
onlyMainContent: true,
|
|
166
|
+
command: `parallel-cli extract ${shq(url)}`,
|
|
149
167
|
},
|
|
150
168
|
});
|
|
151
169
|
}
|
|
152
170
|
}
|
|
153
|
-
// For explore/compare, also suggest a
|
|
171
|
+
// For explore/compare, also suggest a parallel-cli search for additional sources
|
|
154
172
|
if (input.intent === 'explore' || input.intent === 'compare') {
|
|
155
173
|
steps.push({
|
|
156
|
-
tool: '
|
|
157
|
-
description: 'Search for additional sources',
|
|
174
|
+
tool: 'Bash',
|
|
175
|
+
description: 'Search for additional sources via parallel-cli search',
|
|
158
176
|
args: {
|
|
159
|
-
query
|
|
160
|
-
limit: 5,
|
|
161
|
-
sources: [{ type: 'web' }],
|
|
177
|
+
command: `parallel-cli search ${shq(query)} --max-results 5`,
|
|
162
178
|
},
|
|
163
179
|
});
|
|
164
180
|
}
|
|
@@ -285,6 +301,7 @@ export async function researchHandler(graph, input, vault, projectSlug) {
|
|
|
285
301
|
researchId: node.id,
|
|
286
302
|
query: input.query,
|
|
287
303
|
intent: input.intent,
|
|
304
|
+
prerequisite: 'parallel-cli must be installed and authenticated. Install: `npm i -g parallel-cli` (or `pipx install parallel-cli`). Auth: run `parallel-cli login` (device OAuth) or set PARALLEL_API_KEY. Verify with `parallel-cli auth`.',
|
|
288
305
|
actionPlan,
|
|
289
306
|
ingestStep: {
|
|
290
307
|
tool: 'research',
|
|
@@ -11,7 +11,7 @@ export declare const routeSkillsSchema: z.ZodObject<{
|
|
|
11
11
|
}, "strip", z.ZodTypeAny, {
|
|
12
12
|
request: string;
|
|
13
13
|
areas?: string[] | undefined;
|
|
14
|
-
platform?: "all" | "
|
|
14
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
15
15
|
currentState?: "unknown" | "new" | "existing" | undefined;
|
|
16
16
|
goalVerb?: string | undefined;
|
|
17
17
|
detectedNeeds?: string[] | undefined;
|
|
@@ -19,7 +19,7 @@ export declare const routeSkillsSchema: z.ZodObject<{
|
|
|
19
19
|
}, {
|
|
20
20
|
request: string;
|
|
21
21
|
areas?: string[] | undefined;
|
|
22
|
-
platform?: "all" | "
|
|
22
|
+
platform?: "all" | "ios" | "android" | "web" | undefined;
|
|
23
23
|
currentState?: "unknown" | "new" | "existing" | undefined;
|
|
24
24
|
goalVerb?: string | undefined;
|
|
25
25
|
detectedNeeds?: string[] | undefined;
|
package/dist/tools/think.js
CHANGED
|
@@ -65,7 +65,7 @@ function buildSuggestions(type, thoughtNumber, relatedCount, stats, matchedSkill
|
|
|
65
65
|
if (type === 'research') {
|
|
66
66
|
suggestions.push({
|
|
67
67
|
tool: 'research',
|
|
68
|
-
when: 'Use the research tool instead — it generates an action plan with
|
|
68
|
+
when: 'Use the research tool instead — it generates an action plan with `parallel-cli` (search/research run/extract) calls and handles ingestion',
|
|
69
69
|
example: { query: '<what to research>', intent: 'explore' },
|
|
70
70
|
});
|
|
71
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@feelingmindful/thinking-graph",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.1",
|
|
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
|
}
|