@vpxa/kb 0.1.4 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpxa/kb",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "Local-first AI developer toolkit — knowledge base, code analysis, context management, and developer tools for LLM agents",
6
6
  "license": "MIT",
@@ -41,10 +41,10 @@
41
41
  "@huggingface/transformers": "^3.x",
42
42
  "@lancedb/lancedb": "^0.x",
43
43
  "@modelcontextprotocol/sdk": "^1.x",
44
- "better-sqlite3": "^12.x",
45
44
  "express": "^5.x",
46
45
  "linkedom": "^0.x",
47
46
  "minimatch": "^10.x",
47
+ "sql.js": "^1.x",
48
48
  "turndown": "^7.x",
49
49
  "zod": "^4.x"
50
50
  },
@@ -1,18 +1,25 @@
1
1
  /**
2
2
  * SQLite-backed knowledge graph store.
3
3
  *
4
- * Uses better-sqlite3 for a zero-config, embedded, single-file graph database.
4
+ * Uses sql.js (WASM) for a zero-native-dependency, embedded, single-file graph database.
5
5
  * Stores nodes and edges with JSON properties, supports multi-hop traversal.
6
+ * Persists to disk by writing the full database buffer on mutations.
6
7
  */
7
8
  import type { GraphEdge, GraphNode, GraphStats, GraphTraversalOptions, GraphTraversalResult, IGraphStore } from './graph-store.interface.js';
8
9
  export declare class SqliteGraphStore implements IGraphStore {
9
10
  private db;
10
11
  private readonly dbPath;
12
+ private dirty;
11
13
  constructor(options?: {
12
14
  path?: string;
13
15
  });
14
16
  initialize(): Promise<void>;
15
17
  private ensureDb;
18
+ private persist;
19
+ private markDirty;
20
+ private flushIfDirty;
21
+ private query;
22
+ private run;
16
23
  upsertNode(node: GraphNode): Promise<void>;
17
24
  upsertEdge(edge: GraphEdge): Promise<void>;
18
25
  upsertNodes(nodes: GraphNode[]): Promise<void>;
@@ -37,7 +44,5 @@ export declare class SqliteGraphStore implements IGraphStore {
37
44
  clear(): Promise<void>;
38
45
  getStats(): Promise<GraphStats>;
39
46
  close(): Promise<void>;
40
- private toGraphNode;
41
- private toGraphEdge;
42
47
  }
43
48
  //# sourceMappingURL=sqlite-graph-store.d.ts.map
@@ -1,4 +1,4 @@
1
- import{existsSync as l,mkdirSync as N}from"node:fs";import{dirname as y,join as S}from"node:path";class f{db=null;dbPath;constructor(e){const t=e?.path??".kb-data";this.dbPath=S(t,"graph.db")}async initialize(){const e=y(this.dbPath);l(e)||N(e,{recursive:!0});const t=(await import("better-sqlite3")).default;this.db=new t(this.dbPath),this.db.pragma("journal_mode = WAL"),this.db.pragma("foreign_keys = ON"),this.db.exec(`
1
+ import{existsSync as g,mkdirSync as l,readFileSync as N,writeFileSync as f}from"node:fs";import{dirname as S,join as I}from"node:path";class b{db=null;dbPath;dirty=!1;constructor(e){const t=e?.path??".kb-data";this.dbPath=I(t,"graph.db")}async initialize(){const e=S(this.dbPath);g(e)||l(e,{recursive:!0});const t=(await import("sql.js")).default,s=await t();if(g(this.dbPath)){const i=N(this.dbPath);this.db=new s.Database(i)}else this.db=new s.Database;this.db.run("PRAGMA journal_mode = WAL"),this.db.exec("PRAGMA foreign_keys = ON;"),this.db.run(`
2
2
  CREATE TABLE IF NOT EXISTS nodes (
3
3
  id TEXT PRIMARY KEY,
4
4
  type TEXT NOT NULL,
@@ -7,8 +7,8 @@ import{existsSync as l,mkdirSync as N}from"node:fs";import{dirname as y,join as
7
7
  source_record_id TEXT,
8
8
  source_path TEXT,
9
9
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
10
- );
11
-
10
+ )
11
+ `),this.db.run(`
12
12
  CREATE TABLE IF NOT EXISTS edges (
13
13
  id TEXT PRIMARY KEY,
14
14
  from_id TEXT NOT NULL,
@@ -18,56 +18,29 @@ import{existsSync as l,mkdirSync as N}from"node:fs";import{dirname as y,join as
18
18
  properties TEXT NOT NULL DEFAULT '{}',
19
19
  FOREIGN KEY (from_id) REFERENCES nodes(id) ON DELETE CASCADE,
20
20
  FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE
21
- );
22
-
23
- CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type);
24
- CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name);
25
- CREATE INDEX IF NOT EXISTS idx_nodes_source_path ON nodes(source_path);
26
- CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id);
27
- CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id);
28
- CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type);
29
- `)}ensureDb(){if(!this.db)throw new Error("Graph store not initialized \u2014 call initialize() first");return this.db}async upsertNode(e){this.ensureDb().prepare(`
30
- INSERT INTO nodes (id, type, name, properties, source_record_id, source_path, created_at)
31
- VALUES (?, ?, ?, ?, ?, ?, ?)
32
- ON CONFLICT(id) DO UPDATE SET
33
- type = excluded.type,
34
- name = excluded.name,
35
- properties = excluded.properties,
36
- source_record_id = excluded.source_record_id,
37
- source_path = excluded.source_path
38
- `).run(e.id,e.type,e.name,JSON.stringify(e.properties),e.sourceRecordId??null,e.sourcePath??null,e.createdAt??new Date().toISOString())}async upsertEdge(e){this.ensureDb().prepare(`
39
- INSERT INTO edges (id, from_id, to_id, type, weight, properties)
40
- VALUES (?, ?, ?, ?, ?, ?)
41
- ON CONFLICT(id) DO UPDATE SET
42
- from_id = excluded.from_id,
43
- to_id = excluded.to_id,
44
- type = excluded.type,
45
- weight = excluded.weight,
46
- properties = excluded.properties
47
- `).run(e.id,e.fromId,e.toId,e.type,e.weight??1,JSON.stringify(e.properties??{}))}async upsertNodes(e){if(e.length===0)return;const t=this.ensureDb(),s=t.prepare(`
48
- INSERT INTO nodes (id, type, name, properties, source_record_id, source_path, created_at)
49
- VALUES (?, ?, ?, ?, ?, ?, ?)
50
- ON CONFLICT(id) DO UPDATE SET
51
- type = excluded.type,
52
- name = excluded.name,
53
- properties = excluded.properties,
54
- source_record_id = excluded.source_record_id,
55
- source_path = excluded.source_path
56
- `);t.transaction(()=>{for(const r of e)s.run(r.id,r.type,r.name,JSON.stringify(r.properties),r.sourceRecordId??null,r.sourcePath??null,r.createdAt??new Date().toISOString())})()}async upsertEdges(e){if(e.length===0)return;const t=this.ensureDb(),s=t.prepare(`
57
- INSERT INTO edges (id, from_id, to_id, type, weight, properties)
58
- VALUES (?, ?, ?, ?, ?, ?)
59
- ON CONFLICT(id) DO UPDATE SET
60
- from_id = excluded.from_id,
61
- to_id = excluded.to_id,
62
- type = excluded.type,
63
- weight = excluded.weight,
64
- properties = excluded.properties
65
- `);t.transaction(()=>{for(const r of e)s.run(r.id,r.fromId,r.toId,r.type,r.weight??1,JSON.stringify(r.properties??{}))})()}async getNode(e){const s=this.ensureDb().prepare("SELECT * FROM nodes WHERE id = ?").get(e);return s?this.toGraphNode(s):null}async getNeighbors(e,t){const s=this.ensureDb(),o=t?.direction??"both",r=t?.edgeType,a=t?.limit??50,p=[],d=[],E=new Set,_=`
21
+ )
22
+ `),this.db.run("CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)"),this.db.run("CREATE INDEX IF NOT EXISTS idx_nodes_name ON nodes(name)"),this.db.run("CREATE INDEX IF NOT EXISTS idx_nodes_source_path ON nodes(source_path)"),this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_id)"),this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_id)"),this.db.run("CREATE INDEX IF NOT EXISTS idx_edges_type ON edges(type)"),this.persist()}ensureDb(){if(!this.db)throw new Error("Graph store not initialized \u2014 call initialize() first");return this.db}persist(){if(!this.db)return;const e=this.db.export();f(this.dbPath,Buffer.from(e)),this.db.exec("PRAGMA foreign_keys = ON;"),this.dirty=!1}markDirty(){this.dirty=!0}flushIfDirty(){this.dirty&&this.persist()}query(e,t=[]){const i=this.ensureDb().prepare(e);i.bind(t);const d=[];for(;i.step();)d.push(i.getAsObject());return i.free(),d}run(e,t=[]){this.ensureDb().run(e,t)}async upsertNode(e){this.run(`INSERT INTO nodes (id, type, name, properties, source_record_id, source_path, created_at)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?)
24
+ ON CONFLICT(id) DO UPDATE SET
25
+ type = excluded.type, name = excluded.name, properties = excluded.properties,
26
+ source_record_id = excluded.source_record_id, source_path = excluded.source_path`,[e.id,e.type,e.name,JSON.stringify(e.properties),e.sourceRecordId??null,e.sourcePath??null,e.createdAt??new Date().toISOString()]),this.markDirty(),this.flushIfDirty()}async upsertEdge(e){this.run(`INSERT INTO edges (id, from_id, to_id, type, weight, properties)
27
+ VALUES (?, ?, ?, ?, ?, ?)
28
+ ON CONFLICT(id) DO UPDATE SET
29
+ from_id = excluded.from_id, to_id = excluded.to_id,
30
+ type = excluded.type, weight = excluded.weight, properties = excluded.properties`,[e.id,e.fromId,e.toId,e.type,e.weight??1,JSON.stringify(e.properties??{})]),this.markDirty(),this.flushIfDirty()}async upsertNodes(e){if(e.length!==0){this.ensureDb().run("BEGIN TRANSACTION");for(const t of e)this.run(`INSERT INTO nodes (id, type, name, properties, source_record_id, source_path, created_at)
31
+ VALUES (?, ?, ?, ?, ?, ?, ?)
32
+ ON CONFLICT(id) DO UPDATE SET
33
+ type = excluded.type, name = excluded.name, properties = excluded.properties,
34
+ source_record_id = excluded.source_record_id, source_path = excluded.source_path`,[t.id,t.type,t.name,JSON.stringify(t.properties),t.sourceRecordId??null,t.sourcePath??null,t.createdAt??new Date().toISOString()]);this.ensureDb().run("COMMIT"),this.markDirty(),this.flushIfDirty()}}async upsertEdges(e){if(e.length!==0){this.ensureDb().run("BEGIN TRANSACTION");for(const t of e)this.run(`INSERT INTO edges (id, from_id, to_id, type, weight, properties)
35
+ VALUES (?, ?, ?, ?, ?, ?)
36
+ ON CONFLICT(id) DO UPDATE SET
37
+ from_id = excluded.from_id, to_id = excluded.to_id,
38
+ type = excluded.type, weight = excluded.weight, properties = excluded.properties`,[t.id,t.fromId,t.toId,t.type,t.weight??1,JSON.stringify(t.properties??{})]);this.ensureDb().run("COMMIT"),this.markDirty(),this.flushIfDirty()}}async getNode(e){const t=this.query("SELECT * FROM nodes WHERE id = ?",[e]);return t.length>0?T(t[0]):null}async getNeighbors(e,t){const s=t?.direction??"both",i=t?.edgeType,d=t?.limit??50,p=[],n=[],u=new Set,h=`
66
39
  SELECT e.id AS edge_id, e.from_id, e.to_id, e.type AS edge_type, e.weight, e.properties AS edge_props,
67
40
  n.id AS node_id, n.type AS node_type, n.name AS node_name, n.properties AS node_props,
68
41
  n.source_record_id AS node_src_rec, n.source_path AS node_src_path, n.created_at AS node_created
69
- FROM edges e JOIN nodes n ON e.to_id = n.id WHERE e.from_id = ?`,u=`
42
+ FROM edges e JOIN nodes n ON e.to_id = n.id WHERE e.from_id = ?`,_=`
70
43
  SELECT e.id AS edge_id, e.from_id, e.to_id, e.type AS edge_type, e.weight, e.properties AS edge_props,
71
44
  n.id AS node_id, n.type AS node_type, n.name AS node_name, n.properties AS node_props,
72
45
  n.source_record_id AS node_src_rec, n.source_path AS node_src_path, n.created_at AS node_created
73
- FROM edges e JOIN nodes n ON e.from_id = n.id WHERE e.to_id = ?`;if(o==="outgoing"||o==="both"){let c=_;const i=[e];r&&(c+=" AND e.type = ?",i.push(r)),c+=" LIMIT ?",i.push(a);const g=s.prepare(c).all(...i);for(const h of g)d.push(T(h)),E.has(h.node_id)||(E.add(h.node_id),p.push(m(h)))}if(o==="incoming"||o==="both"){let c=u;const i=[e];r&&(c+=" AND e.type = ?",i.push(r)),c+=" LIMIT ?",i.push(a);const g=s.prepare(c).all(...i);for(const h of g)d.push(T(h)),E.has(h.node_id)||(E.add(h.node_id),p.push(m(h)))}return{nodes:p,edges:d}}async traverse(e,t){const s=t?.maxDepth??2,o=t?.direction??"both",r=t?.edgeType,a=t?.limit??50,p=new Map,d=new Map,E=new Set,_=[{nodeId:e,depth:0}];for(;_.length>0&&p.size<a;){const u=_.shift();if(!u||E.has(u.nodeId)||u.depth>s)continue;E.add(u.nodeId);const c=await this.getNeighbors(u.nodeId,{direction:o,edgeType:r,limit:a-p.size});for(const i of c.nodes)p.has(i.id)||(p.set(i.id,i),u.depth+1<s&&_.push({nodeId:i.id,depth:u.depth+1}));for(const i of c.edges)d.set(i.id,i)}return{nodes:[...p.values()],edges:[...d.values()]}}async findNodes(e){const t=this.ensureDb(),s=[],o=[];e.type&&(s.push("type = ?"),o.push(e.type)),e.namePattern&&(s.push("name LIKE ?"),o.push(`%${e.namePattern}%`)),e.sourcePath&&(s.push("source_path = ?"),o.push(e.sourcePath));const r=s.length>0?`WHERE ${s.join(" AND ")}`:"",a=e.limit??100;return t.prepare(`SELECT * FROM nodes ${r} LIMIT ?`).all(...o,a).map(d=>this.toGraphNode(d))}async findEdges(e){const t=this.ensureDb(),s=[],o=[];e.type&&(s.push("type = ?"),o.push(e.type)),e.fromId&&(s.push("from_id = ?"),o.push(e.fromId)),e.toId&&(s.push("to_id = ?"),o.push(e.toId));const r=s.length>0?`WHERE ${s.join(" AND ")}`:"",a=e.limit??100;return t.prepare(`SELECT * FROM edges ${r} LIMIT ?`).all(...o,a).map(d=>this.toGraphEdge(d))}async deleteNode(e){const t=this.ensureDb();t.transaction(()=>{t.prepare("DELETE FROM edges WHERE from_id = ? OR to_id = ?").run(e,e),t.prepare("DELETE FROM nodes WHERE id = ?").run(e)})()}async deleteBySourcePath(e){const t=this.ensureDb(),s=t.prepare("SELECT id FROM nodes WHERE source_path = ?").all(e);return s.length===0?0:(t.transaction(()=>{for(const{id:r}of s)t.prepare("DELETE FROM edges WHERE from_id = ? OR to_id = ?").run(r,r);t.prepare("DELETE FROM nodes WHERE source_path = ?").run(e)})(),s.length)}async clear(){this.ensureDb().exec("DELETE FROM edges; DELETE FROM nodes;")}async getStats(){const e=this.ensureDb(),t=e.prepare("SELECT COUNT(*) as count FROM nodes").get().count,s=e.prepare("SELECT COUNT(*) as count FROM edges").get().count,o=e.prepare("SELECT type, COUNT(*) as count FROM nodes GROUP BY type").all(),r={};for(const d of o)r[d.type]=d.count;const a=e.prepare("SELECT type, COUNT(*) as count FROM edges GROUP BY type").all(),p={};for(const d of a)p[d.type]=d.count;return{nodeCount:t,edgeCount:s,nodeTypes:r,edgeTypes:p}}async close(){this.db&&(this.db.close(),this.db=null)}toGraphNode(e){return{id:e.id,type:e.type,name:e.name,properties:JSON.parse(e.properties),sourceRecordId:e.source_record_id??void 0,sourcePath:e.source_path??void 0,createdAt:e.created_at}}toGraphEdge(e){return{id:e.id,fromId:e.from_id,toId:e.to_id,type:e.type,weight:e.weight??1,properties:JSON.parse(e.properties)}}}function T(n){return{id:n.edge_id,fromId:n.from_id,toId:n.to_id,type:n.edge_type,weight:n.weight??1,properties:JSON.parse(n.edge_props??"{}")}}function m(n){return{id:n.node_id,type:n.node_type,name:n.node_name,properties:JSON.parse(n.node_props??"{}"),sourceRecordId:n.node_src_rec??void 0,sourcePath:n.node_src_path??void 0,createdAt:n.node_created}}export{f as SqliteGraphStore};
46
+ FROM edges e JOIN nodes n ON e.from_id = n.id WHERE e.to_id = ?`;if(s==="outgoing"||s==="both"){let o=h;const c=[e];i&&(o+=" AND e.type = ?",c.push(i)),o+=" LIMIT ?",c.push(d);const a=this.query(o,c);for(const E of a)n.push(y(E)),u.has(E.node_id)||(u.add(E.node_id),p.push(m(E)))}if(s==="incoming"||s==="both"){let o=_;const c=[e];i&&(o+=" AND e.type = ?",c.push(i)),o+=" LIMIT ?",c.push(d);const a=this.query(o,c);for(const E of a)n.push(y(E)),u.has(E.node_id)||(u.add(E.node_id),p.push(m(E)))}return{nodes:p,edges:n}}async traverse(e,t){const s=t?.maxDepth??2,i=t?.direction??"both",d=t?.edgeType,p=t?.limit??50,n=new Map,u=new Map,h=new Set,_=[{nodeId:e,depth:0}];for(;_.length>0&&n.size<p;){const o=_.shift();if(!o||h.has(o.nodeId)||o.depth>s)continue;h.add(o.nodeId);const c=await this.getNeighbors(o.nodeId,{direction:i,edgeType:d,limit:p-n.size});for(const a of c.nodes)n.has(a.id)||(n.set(a.id,a),o.depth+1<s&&_.push({nodeId:a.id,depth:o.depth+1}));for(const a of c.edges)u.set(a.id,a)}return{nodes:[...n.values()],edges:[...u.values()]}}async findNodes(e){const t=[],s=[];e.type&&(t.push("type = ?"),s.push(e.type)),e.namePattern&&(t.push("name LIKE ?"),s.push(`%${e.namePattern}%`)),e.sourcePath&&(t.push("source_path = ?"),s.push(e.sourcePath));const i=t.length>0?`WHERE ${t.join(" AND ")}`:"",d=e.limit??100;return this.query(`SELECT * FROM nodes ${i} LIMIT ?`,[...s,d]).map(n=>T(n))}async findEdges(e){const t=[],s=[];e.type&&(t.push("type = ?"),s.push(e.type)),e.fromId&&(t.push("from_id = ?"),s.push(e.fromId)),e.toId&&(t.push("to_id = ?"),s.push(e.toId));const i=t.length>0?`WHERE ${t.join(" AND ")}`:"",d=e.limit??100;return this.query(`SELECT * FROM edges ${i} LIMIT ?`,[...s,d]).map(n=>O(n))}async deleteNode(e){this.ensureDb().run("BEGIN TRANSACTION"),this.run("DELETE FROM edges WHERE from_id = ? OR to_id = ?",[e,e]),this.run("DELETE FROM nodes WHERE id = ?",[e]),this.ensureDb().run("COMMIT"),this.markDirty(),this.flushIfDirty()}async deleteBySourcePath(e){const t=this.query("SELECT id FROM nodes WHERE source_path = ?",[e]);if(t.length===0)return 0;this.ensureDb().run("BEGIN TRANSACTION");for(const s of t)this.run("DELETE FROM edges WHERE from_id = ? OR to_id = ?",[s.id,s.id]);return this.run("DELETE FROM nodes WHERE source_path = ?",[e]),this.ensureDb().run("COMMIT"),this.markDirty(),this.flushIfDirty(),t.length}async clear(){this.run("DELETE FROM edges"),this.run("DELETE FROM nodes"),this.markDirty(),this.flushIfDirty()}async getStats(){const t=this.query("SELECT COUNT(*) as count FROM nodes")[0]?.count??0,i=this.query("SELECT COUNT(*) as count FROM edges")[0]?.count??0,d=this.query("SELECT type, COUNT(*) as count FROM nodes GROUP BY type"),p={};for(const h of d)p[h.type]=h.count;const n=this.query("SELECT type, COUNT(*) as count FROM edges GROUP BY type"),u={};for(const h of n)u[h.type]=h.count;return{nodeCount:t,edgeCount:i,nodeTypes:p,edgeTypes:u}}async close(){this.db&&(this.flushIfDirty(),this.db.close(),this.db=null)}}function T(r){return{id:r.id,type:r.type,name:r.name,properties:JSON.parse(r.properties),sourceRecordId:r.source_record_id??void 0,sourcePath:r.source_path??void 0,createdAt:r.created_at}}function O(r){return{id:r.id,fromId:r.from_id,toId:r.to_id,type:r.type,weight:r.weight??1,properties:JSON.parse(r.properties)}}function y(r){return{id:r.edge_id,fromId:r.from_id,toId:r.to_id,type:r.edge_type,weight:r.weight??1,properties:JSON.parse(r.edge_props??"{}")}}function m(r){return{id:r.node_id,type:r.node_type,name:r.node_name,properties:JSON.parse(r.node_props??"{}"),sourceRecordId:r.node_src_rec??void 0,sourcePath:r.node_src_path??void 0,createdAt:r.node_created}}export{b as SqliteGraphStore};