@feelingmindful/thinking-graph 1.2.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/compat/sequential.d.ts +45 -0
- package/dist/compat/sequential.js +46 -0
- package/dist/engine/dedup.d.ts +5 -0
- package/dist/engine/dedup.js +17 -0
- package/dist/engine/graph.d.ts +49 -0
- package/dist/engine/graph.js +250 -0
- package/dist/engine/seed.d.ts +7 -0
- package/dist/engine/seed.js +43 -0
- package/dist/engine/types.d.ts +114 -0
- package/dist/engine/types.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +52 -0
- package/dist/storage/adapter.d.ts +20 -0
- package/dist/storage/adapter.js +1 -0
- package/dist/storage/memory.d.ts +25 -0
- package/dist/storage/memory.js +171 -0
- package/dist/storage/sqlite.d.ts +30 -0
- package/dist/storage/sqlite.js +310 -0
- package/dist/tools/export.d.ts +31 -0
- package/dist/tools/export.js +42 -0
- package/dist/tools/learn.d.ts +66 -0
- package/dist/tools/learn.js +91 -0
- package/dist/tools/recall.d.ts +52 -0
- package/dist/tools/recall.js +63 -0
- package/dist/tools/relate.d.ts +31 -0
- package/dist/tools/relate.js +58 -0
- package/dist/tools/think.d.ts +75 -0
- package/dist/tools/think.js +69 -0
- package/migrations/001-initial.sql +108 -0
- package/package.json +44 -0
- package/seeds/principles.json +92 -0
- package/seeds/skill-registry.json +187 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Node, Edge, Session, SkillRegistryEntry, NodeQuery, PaginatedResult, EdgeType, ExportOpts, GraphExport, GraphStats } from '../engine/types.js';
|
|
2
|
+
import type { StorageAdapter } from './adapter.js';
|
|
3
|
+
export declare class InMemoryAdapter implements StorageAdapter {
|
|
4
|
+
private nodes;
|
|
5
|
+
private edges;
|
|
6
|
+
private sessions;
|
|
7
|
+
private skills;
|
|
8
|
+
initialize(): Promise<void>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
insertNode(node: Node): Promise<void>;
|
|
11
|
+
getNode(id: string): Promise<Node | null>;
|
|
12
|
+
queryNodes(query: NodeQuery): Promise<PaginatedResult<Node>>;
|
|
13
|
+
searchContent(text: string, limit?: number): Promise<Node[]>;
|
|
14
|
+
insertEdge(edge: Edge): Promise<boolean>;
|
|
15
|
+
getEdgesFrom(nodeId: string, type?: EdgeType): Promise<Edge[]>;
|
|
16
|
+
getEdgesTo(nodeId: string, type?: EdgeType): Promise<Edge[]>;
|
|
17
|
+
traverseEdges(startId: string, type: EdgeType, maxDepth: number): Promise<Node[]>;
|
|
18
|
+
insertSession(session: Session): Promise<void>;
|
|
19
|
+
getSession(id: string): Promise<Session | null>;
|
|
20
|
+
updateSession(id: string, fields: Partial<Session>): Promise<void>;
|
|
21
|
+
insertSkill(entry: SkillRegistryEntry): Promise<void>;
|
|
22
|
+
querySkills(filter: Partial<SkillRegistryEntry>): Promise<SkillRegistryEntry[]>;
|
|
23
|
+
exportAll(opts?: ExportOpts): Promise<GraphExport>;
|
|
24
|
+
getStats(): Promise<GraphStats>;
|
|
25
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
export class InMemoryAdapter {
|
|
2
|
+
nodes = new Map();
|
|
3
|
+
edges = new Map();
|
|
4
|
+
sessions = new Map();
|
|
5
|
+
skills = new Map();
|
|
6
|
+
async initialize() { }
|
|
7
|
+
async close() { }
|
|
8
|
+
// ─── Nodes ─────────────────────────────────────────────
|
|
9
|
+
async insertNode(node) {
|
|
10
|
+
this.nodes.set(node.id, { ...node });
|
|
11
|
+
}
|
|
12
|
+
async getNode(id) {
|
|
13
|
+
return this.nodes.get(id) ?? null;
|
|
14
|
+
}
|
|
15
|
+
async queryNodes(query) {
|
|
16
|
+
let results = [...this.nodes.values()];
|
|
17
|
+
if (query.type) {
|
|
18
|
+
const types = Array.isArray(query.type) ? query.type : [query.type];
|
|
19
|
+
results = results.filter(n => types.includes(n.type));
|
|
20
|
+
}
|
|
21
|
+
if (query.sessionId) {
|
|
22
|
+
results = results.filter(n => n.sessionId === query.sessionId);
|
|
23
|
+
}
|
|
24
|
+
if (query.projectId && !query.crossProject) {
|
|
25
|
+
results = results.filter(n => n.projectId === query.projectId);
|
|
26
|
+
}
|
|
27
|
+
if (query.since) {
|
|
28
|
+
results = results.filter(n => n.createdAt >= query.since);
|
|
29
|
+
}
|
|
30
|
+
if (query.query) {
|
|
31
|
+
const q = query.query.toLowerCase();
|
|
32
|
+
results = results.filter(n => n.content.toLowerCase().includes(q));
|
|
33
|
+
}
|
|
34
|
+
// Sort by createdAt desc
|
|
35
|
+
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
36
|
+
const totalCount = results.length;
|
|
37
|
+
const offset = query.offset ?? 0;
|
|
38
|
+
const limit = query.limit ?? 20;
|
|
39
|
+
const items = results.slice(offset, offset + limit);
|
|
40
|
+
return { items, totalCount, hasMore: offset + limit < totalCount };
|
|
41
|
+
}
|
|
42
|
+
async searchContent(text, limit = 20) {
|
|
43
|
+
const q = text.toLowerCase();
|
|
44
|
+
return [...this.nodes.values()]
|
|
45
|
+
.filter(n => n.content.toLowerCase().includes(q))
|
|
46
|
+
.slice(0, limit);
|
|
47
|
+
}
|
|
48
|
+
// ─── Edges ─────────────────────────────────────────────
|
|
49
|
+
async insertEdge(edge) {
|
|
50
|
+
// Check for duplicate (same source, target, type)
|
|
51
|
+
for (const e of this.edges.values()) {
|
|
52
|
+
if (e.sourceId === edge.sourceId && e.targetId === edge.targetId && e.type === edge.type) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.edges.set(edge.id, { ...edge });
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
async getEdgesFrom(nodeId, type) {
|
|
60
|
+
return [...this.edges.values()].filter(e => e.sourceId === nodeId && (!type || e.type === type));
|
|
61
|
+
}
|
|
62
|
+
async getEdgesTo(nodeId, type) {
|
|
63
|
+
return [...this.edges.values()].filter(e => e.targetId === nodeId && (!type || e.type === type));
|
|
64
|
+
}
|
|
65
|
+
async traverseEdges(startId, type, maxDepth) {
|
|
66
|
+
const visited = new Set();
|
|
67
|
+
const result = [];
|
|
68
|
+
let frontier = [startId];
|
|
69
|
+
for (let depth = 0; depth < maxDepth && frontier.length > 0; depth++) {
|
|
70
|
+
const nextFrontier = [];
|
|
71
|
+
for (const id of frontier) {
|
|
72
|
+
if (visited.has(id))
|
|
73
|
+
continue;
|
|
74
|
+
visited.add(id);
|
|
75
|
+
const edges = await this.getEdgesFrom(id, type);
|
|
76
|
+
for (const edge of edges) {
|
|
77
|
+
if (!visited.has(edge.targetId)) {
|
|
78
|
+
const node = await this.getNode(edge.targetId);
|
|
79
|
+
if (node) {
|
|
80
|
+
result.push(node);
|
|
81
|
+
nextFrontier.push(edge.targetId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
frontier = nextFrontier;
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
// ─── Sessions ──────────────────────────────────────────
|
|
91
|
+
async insertSession(session) {
|
|
92
|
+
this.sessions.set(session.id, { ...session });
|
|
93
|
+
}
|
|
94
|
+
async getSession(id) {
|
|
95
|
+
return this.sessions.get(id) ?? null;
|
|
96
|
+
}
|
|
97
|
+
async updateSession(id, fields) {
|
|
98
|
+
const session = this.sessions.get(id);
|
|
99
|
+
if (session) {
|
|
100
|
+
this.sessions.set(id, { ...session, ...fields });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ─── Skill Registry ───────────────────────────────────
|
|
104
|
+
async insertSkill(entry) {
|
|
105
|
+
this.skills.set(entry.id, { ...entry });
|
|
106
|
+
}
|
|
107
|
+
async querySkills(filter) {
|
|
108
|
+
return [...this.skills.values()].filter(s => {
|
|
109
|
+
if (filter.id && s.id !== filter.id)
|
|
110
|
+
return false;
|
|
111
|
+
if (filter.pluginName && s.pluginName !== filter.pluginName)
|
|
112
|
+
return false;
|
|
113
|
+
if (filter.skillName && s.skillName !== filter.skillName)
|
|
114
|
+
return false;
|
|
115
|
+
if (filter.verb && s.verb !== filter.verb)
|
|
116
|
+
return false;
|
|
117
|
+
if (filter.platform && s.platform !== filter.platform && s.platform !== 'all')
|
|
118
|
+
return false;
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// ─── Export ────────────────────────────────────────────
|
|
123
|
+
async exportAll(opts) {
|
|
124
|
+
let nodes = [...this.nodes.values()];
|
|
125
|
+
let edges = [...this.edges.values()];
|
|
126
|
+
if (opts?.sessionId) {
|
|
127
|
+
nodes = nodes.filter(n => n.sessionId === opts.sessionId);
|
|
128
|
+
}
|
|
129
|
+
if (opts?.projectId) {
|
|
130
|
+
nodes = nodes.filter(n => n.projectId === opts.projectId);
|
|
131
|
+
}
|
|
132
|
+
if (opts?.type) {
|
|
133
|
+
const types = Array.isArray(opts.type) ? opts.type : [opts.type];
|
|
134
|
+
nodes = nodes.filter(n => types.includes(n.type));
|
|
135
|
+
}
|
|
136
|
+
const nodeIds = new Set(nodes.map(n => n.id));
|
|
137
|
+
if (opts?.includeEdges !== false) {
|
|
138
|
+
edges = edges.filter(e => nodeIds.has(e.sourceId) || nodeIds.has(e.targetId));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
edges = [];
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
exportedAt: new Date().toISOString(),
|
|
145
|
+
nodeCount: nodes.length,
|
|
146
|
+
edgeCount: edges.length,
|
|
147
|
+
sessions: [...this.sessions.values()],
|
|
148
|
+
nodes,
|
|
149
|
+
edges,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
// ─── Stats ─────────────────────────────────────────────
|
|
153
|
+
async getStats() {
|
|
154
|
+
const nodesByType = {};
|
|
155
|
+
for (const n of this.nodes.values()) {
|
|
156
|
+
nodesByType[n.type] = (nodesByType[n.type] ?? 0) + 1;
|
|
157
|
+
}
|
|
158
|
+
const edgesByType = {};
|
|
159
|
+
for (const e of this.edges.values()) {
|
|
160
|
+
edgesByType[e.type] = (edgesByType[e.type] ?? 0) + 1;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
totalNodes: this.nodes.size,
|
|
164
|
+
totalEdges: this.edges.size,
|
|
165
|
+
nodesByType,
|
|
166
|
+
edgesByType,
|
|
167
|
+
techDebtCount: nodesByType['tech_debt'] ?? 0,
|
|
168
|
+
principleViolations: edgesByType['violates'] ?? 0,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Node, Edge, Session, SkillRegistryEntry, NodeQuery, PaginatedResult, EdgeType, ExportOpts, GraphExport, GraphStats } from '../engine/types.js';
|
|
2
|
+
import type { StorageAdapter } from './adapter.js';
|
|
3
|
+
export interface SQLiteAdapterOpts {
|
|
4
|
+
dbPath: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class SQLiteAdapter implements StorageAdapter {
|
|
7
|
+
private db;
|
|
8
|
+
private dbPath;
|
|
9
|
+
constructor(opts: SQLiteAdapterOpts);
|
|
10
|
+
initialize(): Promise<void>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
insertNode(node: Node): Promise<void>;
|
|
13
|
+
getNode(id: string): Promise<Node | null>;
|
|
14
|
+
queryNodes(query: NodeQuery): Promise<PaginatedResult<Node>>;
|
|
15
|
+
searchContent(text: string, limit?: number): Promise<Node[]>;
|
|
16
|
+
insertEdge(edge: Edge): Promise<boolean>;
|
|
17
|
+
getEdgesFrom(nodeId: string, type?: EdgeType): Promise<Edge[]>;
|
|
18
|
+
getEdgesTo(nodeId: string, type?: EdgeType): Promise<Edge[]>;
|
|
19
|
+
traverseEdges(startId: string, type: EdgeType, maxDepth: number): Promise<Node[]>;
|
|
20
|
+
insertSession(session: Session): Promise<void>;
|
|
21
|
+
getSession(id: string): Promise<Session | null>;
|
|
22
|
+
updateSession(id: string, fields: Partial<Session>): Promise<void>;
|
|
23
|
+
insertSkill(entry: SkillRegistryEntry): Promise<void>;
|
|
24
|
+
querySkills(filter: Partial<SkillRegistryEntry>): Promise<SkillRegistryEntry[]>;
|
|
25
|
+
exportAll(opts?: ExportOpts): Promise<GraphExport>;
|
|
26
|
+
getStats(): Promise<GraphStats>;
|
|
27
|
+
private rowToNode;
|
|
28
|
+
private rowToEdge;
|
|
29
|
+
private rowToSession;
|
|
30
|
+
}
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { join, dirname } from 'path';
|
|
4
|
+
import { mkdirSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const MIGRATION_PATH = join(__dirname, '../../migrations/001-initial.sql');
|
|
8
|
+
export class SQLiteAdapter {
|
|
9
|
+
db;
|
|
10
|
+
dbPath;
|
|
11
|
+
constructor(opts) {
|
|
12
|
+
this.dbPath = opts.dbPath;
|
|
13
|
+
}
|
|
14
|
+
async initialize() {
|
|
15
|
+
// Ensure parent directory exists
|
|
16
|
+
const dir = dirname(this.dbPath);
|
|
17
|
+
mkdirSync(dir, { recursive: true });
|
|
18
|
+
this.db = new Database(this.dbPath);
|
|
19
|
+
this.db.pragma('journal_mode = WAL');
|
|
20
|
+
this.db.pragma('foreign_keys = ON');
|
|
21
|
+
// Run migration
|
|
22
|
+
const sql = readFileSync(MIGRATION_PATH, 'utf-8');
|
|
23
|
+
this.db.exec(sql);
|
|
24
|
+
}
|
|
25
|
+
async close() {
|
|
26
|
+
this.db.close();
|
|
27
|
+
}
|
|
28
|
+
// ─── Nodes ─────────────────────────────────────────────
|
|
29
|
+
async insertNode(node) {
|
|
30
|
+
this.db.prepare(`
|
|
31
|
+
INSERT INTO nodes (id, type, content, session_id, project_id, metadata,
|
|
32
|
+
created_at, updated_at, thought_number, total_thoughts, branch_id,
|
|
33
|
+
is_revision, revises_thought)
|
|
34
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
35
|
+
`).run(node.id, node.type, node.content, node.sessionId, node.projectId ?? null, JSON.stringify(node.metadata), node.createdAt, node.updatedAt, node.thoughtNumber ?? null, node.totalThoughts ?? null, node.branchId ?? null, node.isRevision ? 1 : 0, node.revisesThought ?? null);
|
|
36
|
+
}
|
|
37
|
+
async getNode(id) {
|
|
38
|
+
const row = this.db.prepare('SELECT * FROM nodes WHERE id = ?').get(id);
|
|
39
|
+
return row ? this.rowToNode(row) : null;
|
|
40
|
+
}
|
|
41
|
+
async queryNodes(query) {
|
|
42
|
+
const conditions = [];
|
|
43
|
+
const params = [];
|
|
44
|
+
if (query.type) {
|
|
45
|
+
const types = Array.isArray(query.type) ? query.type : [query.type];
|
|
46
|
+
conditions.push(`type IN (${types.map(() => '?').join(',')})`);
|
|
47
|
+
params.push(...types);
|
|
48
|
+
}
|
|
49
|
+
if (query.sessionId) {
|
|
50
|
+
conditions.push('session_id = ?');
|
|
51
|
+
params.push(query.sessionId);
|
|
52
|
+
}
|
|
53
|
+
if (query.projectId && !query.crossProject) {
|
|
54
|
+
conditions.push('project_id = ?');
|
|
55
|
+
params.push(query.projectId);
|
|
56
|
+
}
|
|
57
|
+
if (query.since) {
|
|
58
|
+
conditions.push('created_at >= ?');
|
|
59
|
+
params.push(query.since);
|
|
60
|
+
}
|
|
61
|
+
if (query.query) {
|
|
62
|
+
// Use FTS for text search
|
|
63
|
+
conditions.push('rowid IN (SELECT rowid FROM nodes_fts WHERE nodes_fts MATCH ?)');
|
|
64
|
+
params.push(query.query);
|
|
65
|
+
}
|
|
66
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
67
|
+
const countRow = this.db.prepare(`SELECT COUNT(*) as cnt FROM nodes ${where}`).get(...params);
|
|
68
|
+
const totalCount = countRow.cnt;
|
|
69
|
+
const limit = query.limit ?? 20;
|
|
70
|
+
const offset = query.offset ?? 0;
|
|
71
|
+
const rows = this.db.prepare(`SELECT * FROM nodes ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
|
|
72
|
+
return {
|
|
73
|
+
items: rows.map(r => this.rowToNode(r)),
|
|
74
|
+
totalCount,
|
|
75
|
+
hasMore: offset + limit < totalCount,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async searchContent(text, limit = 20) {
|
|
79
|
+
const rows = this.db.prepare(`
|
|
80
|
+
SELECT n.* FROM nodes_fts fts
|
|
81
|
+
JOIN nodes n ON n.rowid = fts.rowid
|
|
82
|
+
WHERE nodes_fts MATCH ?
|
|
83
|
+
ORDER BY rank
|
|
84
|
+
LIMIT ?
|
|
85
|
+
`).all(text, limit);
|
|
86
|
+
return rows.map(r => this.rowToNode(r));
|
|
87
|
+
}
|
|
88
|
+
// ─── Edges ─────────────────────────────────────────────
|
|
89
|
+
async insertEdge(edge) {
|
|
90
|
+
try {
|
|
91
|
+
this.db.prepare(`
|
|
92
|
+
INSERT INTO edges (id, source_id, target_id, type, weight, reasoning, created_at)
|
|
93
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
94
|
+
`).run(edge.id, edge.sourceId, edge.targetId, edge.type, edge.weight, edge.reasoning ?? null, edge.createdAt);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
if (err instanceof Error && err.message.includes('UNIQUE constraint')) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async getEdgesFrom(nodeId, type) {
|
|
105
|
+
const sql = type
|
|
106
|
+
? 'SELECT * FROM edges WHERE source_id = ? AND type = ?'
|
|
107
|
+
: 'SELECT * FROM edges WHERE source_id = ?';
|
|
108
|
+
const params = type ? [nodeId, type] : [nodeId];
|
|
109
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
110
|
+
return rows.map(r => this.rowToEdge(r));
|
|
111
|
+
}
|
|
112
|
+
async getEdgesTo(nodeId, type) {
|
|
113
|
+
const sql = type
|
|
114
|
+
? 'SELECT * FROM edges WHERE target_id = ? AND type = ?'
|
|
115
|
+
: 'SELECT * FROM edges WHERE target_id = ?';
|
|
116
|
+
const params = type ? [nodeId, type] : [nodeId];
|
|
117
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
118
|
+
return rows.map(r => this.rowToEdge(r));
|
|
119
|
+
}
|
|
120
|
+
async traverseEdges(startId, type, maxDepth) {
|
|
121
|
+
const rows = this.db.prepare(`
|
|
122
|
+
WITH RECURSIVE chain(id, depth) AS (
|
|
123
|
+
SELECT target_id, 1
|
|
124
|
+
FROM edges
|
|
125
|
+
WHERE source_id = ? AND type = ?
|
|
126
|
+
UNION ALL
|
|
127
|
+
SELECT e.target_id, c.depth + 1
|
|
128
|
+
FROM edges e
|
|
129
|
+
JOIN chain c ON e.source_id = c.id
|
|
130
|
+
WHERE e.type = ? AND c.depth < ?
|
|
131
|
+
)
|
|
132
|
+
SELECT DISTINCT n.* FROM nodes n
|
|
133
|
+
JOIN chain c ON n.id = c.id
|
|
134
|
+
ORDER BY c.depth
|
|
135
|
+
`).all(startId, type, type, maxDepth);
|
|
136
|
+
return rows.map(r => this.rowToNode(r));
|
|
137
|
+
}
|
|
138
|
+
// ─── Sessions ──────────────────────────────────────────
|
|
139
|
+
async insertSession(session) {
|
|
140
|
+
this.db.prepare(`
|
|
141
|
+
INSERT INTO sessions (id, project_id, project_path, description, started_at, last_active)
|
|
142
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
143
|
+
`).run(session.id, session.projectId ?? null, session.projectPath ?? null, session.description ?? null, session.startedAt, session.lastActiveAt);
|
|
144
|
+
}
|
|
145
|
+
async getSession(id) {
|
|
146
|
+
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
147
|
+
return row ? this.rowToSession(row) : null;
|
|
148
|
+
}
|
|
149
|
+
async updateSession(id, fields) {
|
|
150
|
+
const sets = [];
|
|
151
|
+
const params = [];
|
|
152
|
+
if (fields.lastActiveAt !== undefined) {
|
|
153
|
+
sets.push('last_active = ?');
|
|
154
|
+
params.push(fields.lastActiveAt);
|
|
155
|
+
}
|
|
156
|
+
if (fields.description !== undefined) {
|
|
157
|
+
sets.push('description = ?');
|
|
158
|
+
params.push(fields.description);
|
|
159
|
+
}
|
|
160
|
+
if (sets.length > 0) {
|
|
161
|
+
params.push(id);
|
|
162
|
+
this.db.prepare(`UPDATE sessions SET ${sets.join(', ')} WHERE id = ?`).run(...params);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// ─── Skill Registry ───────────────────────────────────
|
|
166
|
+
async insertSkill(entry) {
|
|
167
|
+
this.db.prepare(`
|
|
168
|
+
INSERT OR IGNORE INTO skill_registry
|
|
169
|
+
(id, plugin_name, skill_name, verb, invocation, areas, detects, produces, invokes, platform)
|
|
170
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
171
|
+
`).run(entry.id, entry.pluginName, entry.skillName, entry.verb ?? null, entry.invocation, JSON.stringify(entry.areas), JSON.stringify(entry.detects), JSON.stringify(entry.produces), JSON.stringify(entry.invokes), entry.platform ?? null);
|
|
172
|
+
}
|
|
173
|
+
async querySkills(filter) {
|
|
174
|
+
const conditions = [];
|
|
175
|
+
const params = [];
|
|
176
|
+
if (filter.id) {
|
|
177
|
+
conditions.push('id = ?');
|
|
178
|
+
params.push(filter.id);
|
|
179
|
+
}
|
|
180
|
+
if (filter.pluginName) {
|
|
181
|
+
conditions.push('plugin_name = ?');
|
|
182
|
+
params.push(filter.pluginName);
|
|
183
|
+
}
|
|
184
|
+
if (filter.skillName) {
|
|
185
|
+
conditions.push('skill_name = ?');
|
|
186
|
+
params.push(filter.skillName);
|
|
187
|
+
}
|
|
188
|
+
if (filter.verb) {
|
|
189
|
+
conditions.push('verb = ?');
|
|
190
|
+
params.push(filter.verb);
|
|
191
|
+
}
|
|
192
|
+
if (filter.platform) {
|
|
193
|
+
conditions.push("(platform = ? OR platform = 'all')");
|
|
194
|
+
params.push(filter.platform);
|
|
195
|
+
}
|
|
196
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
197
|
+
const rows = this.db.prepare(`SELECT * FROM skill_registry ${where}`).all(...params);
|
|
198
|
+
return rows.map(r => ({
|
|
199
|
+
id: r.id,
|
|
200
|
+
pluginName: r.plugin_name,
|
|
201
|
+
skillName: r.skill_name,
|
|
202
|
+
verb: r.verb,
|
|
203
|
+
invocation: r.invocation,
|
|
204
|
+
areas: JSON.parse(r.areas),
|
|
205
|
+
detects: JSON.parse(r.detects),
|
|
206
|
+
produces: JSON.parse(r.produces),
|
|
207
|
+
invokes: JSON.parse(r.invokes),
|
|
208
|
+
platform: r.platform,
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
// ─── Export ────────────────────────────────────────────
|
|
212
|
+
async exportAll(opts) {
|
|
213
|
+
const conditions = [];
|
|
214
|
+
const params = [];
|
|
215
|
+
if (opts?.sessionId) {
|
|
216
|
+
conditions.push('session_id = ?');
|
|
217
|
+
params.push(opts.sessionId);
|
|
218
|
+
}
|
|
219
|
+
if (opts?.projectId) {
|
|
220
|
+
conditions.push('project_id = ?');
|
|
221
|
+
params.push(opts.projectId);
|
|
222
|
+
}
|
|
223
|
+
if (opts?.type) {
|
|
224
|
+
const types = Array.isArray(opts.type) ? opts.type : [opts.type];
|
|
225
|
+
conditions.push(`type IN (${types.map(() => '?').join(',')})`);
|
|
226
|
+
params.push(...types);
|
|
227
|
+
}
|
|
228
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
229
|
+
const nodeRows = this.db.prepare(`SELECT * FROM nodes ${where}`).all(...params);
|
|
230
|
+
const nodes = nodeRows.map(r => this.rowToNode(r));
|
|
231
|
+
let edges = [];
|
|
232
|
+
if (opts?.includeEdges !== false) {
|
|
233
|
+
const nodeIds = nodes.map(n => n.id);
|
|
234
|
+
if (nodeIds.length > 0) {
|
|
235
|
+
const placeholders = nodeIds.map(() => '?').join(',');
|
|
236
|
+
const edgeRows = this.db.prepare(`SELECT * FROM edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders})`).all(...nodeIds, ...nodeIds);
|
|
237
|
+
edges = edgeRows.map(r => this.rowToEdge(r));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const sessionRows = this.db.prepare('SELECT * FROM sessions').all();
|
|
241
|
+
return {
|
|
242
|
+
exportedAt: new Date().toISOString(),
|
|
243
|
+
nodeCount: nodes.length,
|
|
244
|
+
edgeCount: edges.length,
|
|
245
|
+
sessions: sessionRows.map(r => this.rowToSession(r)),
|
|
246
|
+
nodes,
|
|
247
|
+
edges,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
// ─── Stats ─────────────────────────────────────────────
|
|
251
|
+
async getStats() {
|
|
252
|
+
const nodesByType = {};
|
|
253
|
+
const ntRows = this.db.prepare('SELECT type, COUNT(*) as cnt FROM nodes GROUP BY type').all();
|
|
254
|
+
for (const r of ntRows)
|
|
255
|
+
nodesByType[r.type] = r.cnt;
|
|
256
|
+
const edgesByType = {};
|
|
257
|
+
const etRows = this.db.prepare('SELECT type, COUNT(*) as cnt FROM edges GROUP BY type').all();
|
|
258
|
+
for (const r of etRows)
|
|
259
|
+
edgesByType[r.type] = r.cnt;
|
|
260
|
+
const totalNodes = this.db.prepare('SELECT COUNT(*) as cnt FROM nodes').get().cnt;
|
|
261
|
+
const totalEdges = this.db.prepare('SELECT COUNT(*) as cnt FROM edges').get().cnt;
|
|
262
|
+
return {
|
|
263
|
+
totalNodes,
|
|
264
|
+
totalEdges,
|
|
265
|
+
nodesByType,
|
|
266
|
+
edgesByType,
|
|
267
|
+
techDebtCount: nodesByType['tech_debt'] ?? 0,
|
|
268
|
+
principleViolations: edgesByType['violates'] ?? 0,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
// ─── Row mappers ───────────────────────────────────────
|
|
272
|
+
rowToNode(row) {
|
|
273
|
+
return {
|
|
274
|
+
id: row.id,
|
|
275
|
+
type: row.type,
|
|
276
|
+
content: row.content,
|
|
277
|
+
sessionId: row.session_id,
|
|
278
|
+
projectId: row.project_id,
|
|
279
|
+
metadata: JSON.parse(row.metadata || '{}'),
|
|
280
|
+
createdAt: row.created_at,
|
|
281
|
+
updatedAt: row.updated_at,
|
|
282
|
+
thoughtNumber: row.thought_number,
|
|
283
|
+
totalThoughts: row.total_thoughts,
|
|
284
|
+
branchId: row.branch_id,
|
|
285
|
+
isRevision: row.is_revision === 1,
|
|
286
|
+
revisesThought: row.revises_thought,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
rowToEdge(row) {
|
|
290
|
+
return {
|
|
291
|
+
id: row.id,
|
|
292
|
+
sourceId: row.source_id,
|
|
293
|
+
targetId: row.target_id,
|
|
294
|
+
type: row.type,
|
|
295
|
+
weight: row.weight,
|
|
296
|
+
reasoning: row.reasoning,
|
|
297
|
+
createdAt: row.created_at,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
rowToSession(row) {
|
|
301
|
+
return {
|
|
302
|
+
id: row.id,
|
|
303
|
+
projectId: row.project_id,
|
|
304
|
+
projectPath: row.project_path,
|
|
305
|
+
description: row.description,
|
|
306
|
+
startedAt: row.started_at,
|
|
307
|
+
lastActiveAt: row.last_active,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ThinkingGraph } from '../engine/graph.js';
|
|
3
|
+
export declare const exportSchema: z.ZodObject<{
|
|
4
|
+
format: z.ZodEnum<["json", "summary"]>;
|
|
5
|
+
sessionId: z.ZodOptional<z.ZodString>;
|
|
6
|
+
projectId: z.ZodOptional<z.ZodString>;
|
|
7
|
+
type: z.ZodOptional<z.ZodUnion<[z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, z.ZodArray<z.ZodEnum<["thought", "decision", "insight", "code_fact", "assumption", "detection", "tech_debt", "principle", "pattern", "skill_result", "research"]>, "many">]>>;
|
|
8
|
+
includeEdges: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
9
|
+
outputPath: z.ZodOptional<z.ZodString>;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
format: "json" | "summary";
|
|
12
|
+
includeEdges: boolean;
|
|
13
|
+
type?: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research" | ("thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research")[] | undefined;
|
|
14
|
+
sessionId?: string | undefined;
|
|
15
|
+
projectId?: string | undefined;
|
|
16
|
+
outputPath?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
format: "json" | "summary";
|
|
19
|
+
type?: "thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research" | ("thought" | "decision" | "insight" | "code_fact" | "assumption" | "detection" | "tech_debt" | "principle" | "pattern" | "skill_result" | "research")[] | undefined;
|
|
20
|
+
sessionId?: string | undefined;
|
|
21
|
+
projectId?: string | undefined;
|
|
22
|
+
includeEdges?: boolean | undefined;
|
|
23
|
+
outputPath?: string | undefined;
|
|
24
|
+
}>;
|
|
25
|
+
export type ExportInput = z.infer<typeof exportSchema>;
|
|
26
|
+
export declare function exportHandler(graph: ThinkingGraph, input: ExportInput): Promise<{
|
|
27
|
+
content: {
|
|
28
|
+
type: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
}[];
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { NODE_TYPES } from '../engine/types.js';
|
|
4
|
+
export const exportSchema = z.object({
|
|
5
|
+
format: z.enum(['json', 'summary']).describe('Output format'),
|
|
6
|
+
sessionId: z.string().optional(),
|
|
7
|
+
projectId: z.string().optional(),
|
|
8
|
+
type: z.union([z.enum(NODE_TYPES), z.array(z.enum(NODE_TYPES))]).optional(),
|
|
9
|
+
includeEdges: z.boolean().optional().default(true),
|
|
10
|
+
outputPath: z.string().optional().describe('Write to file'),
|
|
11
|
+
});
|
|
12
|
+
export async function exportHandler(graph, input) {
|
|
13
|
+
if (input.format === 'summary') {
|
|
14
|
+
const summary = await graph.exportSummary({
|
|
15
|
+
format: 'summary',
|
|
16
|
+
sessionId: input.sessionId,
|
|
17
|
+
projectId: input.projectId,
|
|
18
|
+
type: input.type,
|
|
19
|
+
});
|
|
20
|
+
if (input.outputPath) {
|
|
21
|
+
writeFileSync(input.outputPath, summary, 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: 'text', text: summary }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// JSON format
|
|
28
|
+
const data = await graph.exportGraph({
|
|
29
|
+
format: 'json',
|
|
30
|
+
sessionId: input.sessionId,
|
|
31
|
+
projectId: input.projectId,
|
|
32
|
+
type: input.type,
|
|
33
|
+
includeEdges: input.includeEdges,
|
|
34
|
+
});
|
|
35
|
+
const json = JSON.stringify(data, null, 2);
|
|
36
|
+
if (input.outputPath) {
|
|
37
|
+
writeFileSync(input.outputPath, json, 'utf-8');
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: 'text', text: json }],
|
|
41
|
+
};
|
|
42
|
+
}
|