@draht/knowledge 2026.7.30 → 2026.10.4-2

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.
@@ -7,6 +7,8 @@ import * as path from "node:path";
7
7
  import { getKnowledgeDbPath } from "./client-config.js";
8
8
  import { VectorStore } from "./vector-store.js";
9
9
  export class KnowledgeManager {
10
+ store;
11
+ clientSlug;
10
12
  constructor(clientSlug, openaiApiKey) {
11
13
  const apiKey = openaiApiKey ?? process.env.OPENAI_API_KEY;
12
14
  if (!apiKey) {
@@ -1 +1 @@
1
- {"version":3,"file":"knowledge-manager.js","sourceRoot":"","sources":["../src/knowledge-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Knowledge manager — high-level API for client knowledge operations.\n * Wraps VectorStore with chunking, file indexing, and search modes.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getKnowledgeDbPath } from \"./client-config.js\";\nimport { type Chunk, type ChunkMetadata, type ChunkType, type SearchResult, VectorStore } from \"./vector-store.js\";\n\nexport type SearchMode = \"decide\" | \"connect\" | \"fuzzy\" | \"general\";\n\nexport class KnowledgeManager {\n\tprivate store: VectorStore;\n\tprivate clientSlug: string;\n\n\tconstructor(clientSlug: string, openaiApiKey?: string) {\n\t\tconst apiKey = openaiApiKey ?? process.env.OPENAI_API_KEY;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"OPENAI_API_KEY required for knowledge base embeddings\");\n\t\t}\n\t\tthis.clientSlug = clientSlug;\n\t\tthis.store = new VectorStore(getKnowledgeDbPath(clientSlug), apiKey);\n\t}\n\n\t/**\n\t * Index a file into the knowledge base.\n\t * Chunks by headings/paragraphs, deduplicates via content hash.\n\t */\n\tasync indexFile(filePath: string): Promise<number> {\n\t\tconst absPath = path.resolve(filePath);\n\t\tif (!fs.existsSync(absPath)) {\n\t\t\tthrow new Error(`File not found: ${absPath}`);\n\t\t}\n\n\t\tconst content = fs.readFileSync(absPath, \"utf-8\");\n\t\tconst chunks = chunkContent(content, {\n\t\t\tsource: absPath,\n\t\t\tclient: this.clientSlug,\n\t\t\ttimestamp: Date.now(),\n\t\t\ttype: inferChunkType(absPath),\n\t\t});\n\n\t\treturn this.store.index(chunks);\n\t}\n\n\t/**\n\t * Index a directory recursively (markdown files only).\n\t */\n\tasync indexDirectory(dirPath: string): Promise<number> {\n\t\tconst absDir = path.resolve(dirPath);\n\t\tif (!fs.existsSync(absDir)) {\n\t\t\tthrow new Error(`Directory not found: ${absDir}`);\n\t\t}\n\n\t\tlet total = 0;\n\t\tconst entries = fs.readdirSync(absDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(absDir, entry.name);\n\t\t\tif (entry.isDirectory() && !entry.name.startsWith(\".\") && entry.name !== \"node_modules\") {\n\t\t\t\ttotal += await this.indexDirectory(full);\n\t\t\t} else if (entry.isFile() && /\\.(md|txt)$/i.test(entry.name)) {\n\t\t\t\ttotal += await this.indexFile(full);\n\t\t\t}\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * Search knowledge with optional mode for query refinement.\n\t */\n\tasync searchKnowledge(query: string, mode: SearchMode = \"general\", limit = 5): Promise<SearchResult[]> {\n\t\tconst refinedQuery = refineQueryByMode(query, mode);\n\t\treturn this.store.search(refinedQuery, limit);\n\t}\n\n\t/**\n\t * Remove all knowledge from a specific source file.\n\t */\n\tasync forget(sourceFile: string): Promise<number> {\n\t\treturn this.store.delete(path.resolve(sourceFile));\n\t}\n\n\t/**\n\t * Get knowledge base statistics.\n\t */\n\tgetStats(): { totalChunks: number; sources: string[]; clientSlug: string } {\n\t\tconst stats = this.store.getStats();\n\t\treturn { ...stats, clientSlug: this.clientSlug };\n\t}\n\n\tclose(): void {\n\t\tthis.store.close();\n\t}\n}\n\n// ============================================================================\n// Chunking\n// ============================================================================\n\nconst MAX_CHUNK_CHARS = 2000; // ~500 tokens\n\nfunction chunkContent(content: string, baseMetadata: ChunkMetadata): Chunk[] {\n\tconst chunks: Chunk[] = [];\n\n\t// Split by markdown headings\n\tconst sections = content.split(/^(?=#{1,3}\\s)/m);\n\n\tfor (const section of sections) {\n\t\tconst trimmed = section.trim();\n\t\tif (!trimmed) continue;\n\n\t\tif (trimmed.length <= MAX_CHUNK_CHARS) {\n\t\t\tchunks.push({ content: trimmed, metadata: { ...baseMetadata } });\n\t\t} else {\n\t\t\t// Split long sections by paragraphs\n\t\t\tconst paragraphs = trimmed.split(/\\n\\n+/);\n\t\t\tlet current = \"\";\n\t\t\tfor (const para of paragraphs) {\n\t\t\t\tif (current.length + para.length + 2 > MAX_CHUNK_CHARS) {\n\t\t\t\t\tif (current.trim()) {\n\t\t\t\t\t\tchunks.push({ content: current.trim(), metadata: { ...baseMetadata } });\n\t\t\t\t\t}\n\t\t\t\t\tcurrent = para;\n\t\t\t\t} else {\n\t\t\t\t\tcurrent = current ? `${current}\\n\\n${para}` : para;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (current.trim()) {\n\t\t\t\tchunks.push({ content: current.trim(), metadata: { ...baseMetadata } });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn chunks;\n}\n\nfunction inferChunkType(filePath: string): ChunkType {\n\tconst basename = path.basename(filePath).toLowerCase();\n\tif (basename.includes(\"agents\") || basename.includes(\"convention\")) return \"convention\";\n\tif (basename.includes(\"decision\") || basename.includes(\"adr\")) return \"decision\";\n\tif (basename.includes(\"pattern\")) return \"pattern\";\n\treturn \"general\";\n}\n\nfunction refineQueryByMode(query: string, mode: SearchMode): string {\n\tswitch (mode) {\n\t\tcase \"decide\":\n\t\t\treturn `decisions and choices about: ${query}`;\n\t\tcase \"connect\":\n\t\t\treturn `relationships and connections related to: ${query}`;\n\t\tcase \"fuzzy\":\n\t\t\treturn query; // Use as-is for fuzzy/vague queries\n\t\tdefault:\n\t\t\treturn query;\n\t}\n}\n"]}
1
+ {"version":3,"file":"knowledge-manager.js","sourceRoot":"","sources":["../src/knowledge-manager.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Knowledge manager — high-level API for client knowledge operations.\n * Wraps VectorStore with chunking, file indexing, and search modes.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { getKnowledgeDbPath } from \"./client-config.js\";\nimport { type Chunk, type ChunkMetadata, type ChunkType, type SearchResult, VectorStore } from \"./vector-store.js\";\n\nexport type SearchMode = \"decide\" | \"connect\" | \"fuzzy\" | \"general\";\n\nexport class KnowledgeManager {\n\tprivate store: VectorStore;\n\tprivate clientSlug: string;\n\n\tconstructor(clientSlug: string, openaiApiKey?: string) {\n\t\tconst apiKey = openaiApiKey ?? process.env.OPENAI_API_KEY;\n\t\tif (!apiKey) {\n\t\t\tthrow new Error(\"OPENAI_API_KEY required for knowledge base embeddings\");\n\t\t}\n\t\tthis.clientSlug = clientSlug;\n\t\tthis.store = new VectorStore(getKnowledgeDbPath(clientSlug), apiKey);\n\t}\n\n\t/**\n\t * Index a file into the knowledge base.\n\t * Chunks by headings/paragraphs, deduplicates via content hash.\n\t */\n\tasync indexFile(filePath: string): Promise<number> {\n\t\tconst absPath = path.resolve(filePath);\n\t\tif (!fs.existsSync(absPath)) {\n\t\t\tthrow new Error(`File not found: ${absPath}`);\n\t\t}\n\n\t\tconst content = fs.readFileSync(absPath, \"utf-8\");\n\t\tconst chunks = chunkContent(content, {\n\t\t\tsource: absPath,\n\t\t\tclient: this.clientSlug,\n\t\t\ttimestamp: Date.now(),\n\t\t\ttype: inferChunkType(absPath),\n\t\t});\n\n\t\treturn this.store.index(chunks);\n\t}\n\n\t/**\n\t * Index a directory recursively (markdown files only).\n\t */\n\tasync indexDirectory(dirPath: string): Promise<number> {\n\t\tconst absDir = path.resolve(dirPath);\n\t\tif (!fs.existsSync(absDir)) {\n\t\t\tthrow new Error(`Directory not found: ${absDir}`);\n\t\t}\n\n\t\tlet total = 0;\n\t\tconst entries = fs.readdirSync(absDir, { withFileTypes: true });\n\t\tfor (const entry of entries) {\n\t\t\tconst full = path.join(absDir, entry.name);\n\t\t\tif (entry.isDirectory() && !entry.name.startsWith(\".\") && entry.name !== \"node_modules\") {\n\t\t\t\ttotal += await this.indexDirectory(full);\n\t\t\t} else if (entry.isFile() && /\\.(md|txt)$/i.test(entry.name)) {\n\t\t\t\ttotal += await this.indexFile(full);\n\t\t\t}\n\t\t}\n\t\treturn total;\n\t}\n\n\t/**\n\t * Search knowledge with optional mode for query refinement.\n\t */\n\tasync searchKnowledge(query: string, mode: SearchMode = \"general\", limit = 5): Promise<SearchResult[]> {\n\t\tconst refinedQuery = refineQueryByMode(query, mode);\n\t\treturn this.store.search(refinedQuery, limit);\n\t}\n\n\t/**\n\t * Remove all knowledge from a specific source file.\n\t */\n\tasync forget(sourceFile: string): Promise<number> {\n\t\treturn this.store.delete(path.resolve(sourceFile));\n\t}\n\n\t/**\n\t * Get knowledge base statistics.\n\t */\n\tgetStats(): { totalChunks: number; sources: string[]; clientSlug: string } {\n\t\tconst stats = this.store.getStats();\n\t\treturn { ...stats, clientSlug: this.clientSlug };\n\t}\n\n\tclose(): void {\n\t\tthis.store.close();\n\t}\n}\n\n// ============================================================================\n// Chunking\n// ============================================================================\n\nconst MAX_CHUNK_CHARS = 2000; // ~500 tokens\n\nfunction chunkContent(content: string, baseMetadata: ChunkMetadata): Chunk[] {\n\tconst chunks: Chunk[] = [];\n\n\t// Split by markdown headings\n\tconst sections = content.split(/^(?=#{1,3}\\s)/m);\n\n\tfor (const section of sections) {\n\t\tconst trimmed = section.trim();\n\t\tif (!trimmed) continue;\n\n\t\tif (trimmed.length <= MAX_CHUNK_CHARS) {\n\t\t\tchunks.push({ content: trimmed, metadata: { ...baseMetadata } });\n\t\t} else {\n\t\t\t// Split long sections by paragraphs\n\t\t\tconst paragraphs = trimmed.split(/\\n\\n+/);\n\t\t\tlet current = \"\";\n\t\t\tfor (const para of paragraphs) {\n\t\t\t\tif (current.length + para.length + 2 > MAX_CHUNK_CHARS) {\n\t\t\t\t\tif (current.trim()) {\n\t\t\t\t\t\tchunks.push({ content: current.trim(), metadata: { ...baseMetadata } });\n\t\t\t\t\t}\n\t\t\t\t\tcurrent = para;\n\t\t\t\t} else {\n\t\t\t\t\tcurrent = current ? `${current}\\n\\n${para}` : para;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (current.trim()) {\n\t\t\t\tchunks.push({ content: current.trim(), metadata: { ...baseMetadata } });\n\t\t\t}\n\t\t}\n\t}\n\n\treturn chunks;\n}\n\nfunction inferChunkType(filePath: string): ChunkType {\n\tconst basename = path.basename(filePath).toLowerCase();\n\tif (basename.includes(\"agents\") || basename.includes(\"convention\")) return \"convention\";\n\tif (basename.includes(\"decision\") || basename.includes(\"adr\")) return \"decision\";\n\tif (basename.includes(\"pattern\")) return \"pattern\";\n\treturn \"general\";\n}\n\nfunction refineQueryByMode(query: string, mode: SearchMode): string {\n\tswitch (mode) {\n\t\tcase \"decide\":\n\t\t\treturn `decisions and choices about: ${query}`;\n\t\tcase \"connect\":\n\t\t\treturn `relationships and connections related to: ${query}`;\n\t\tcase \"fuzzy\":\n\t\t\treturn query; // Use as-is for fuzzy/vague queries\n\t\tdefault:\n\t\t\treturn query;\n\t}\n}\n"]}
@@ -82,6 +82,8 @@ function bufferToEmbedding(buf) {
82
82
  // VectorStore
83
83
  // ============================================================================
84
84
  export class VectorStore {
85
+ db;
86
+ apiKey;
85
87
  constructor(dbPath, openaiApiKey) {
86
88
  this.apiKey = openaiApiKey;
87
89
  // Ensure directory exists
@@ -1 +1 @@
1
- {"version":3,"file":"vector-store.js","sourceRoot":"","sources":["../src/vector-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Vector store backed by SQLite with OpenAI embeddings.\n * Provides local-first semantic search per client namespace.\n */\n\nimport { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport Database from \"better-sqlite3\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ChunkType = \"decision\" | \"convention\" | \"pattern\" | \"general\";\n\nexport interface ChunkMetadata {\n\tsource: string;\n\tclient: string;\n\ttimestamp: number;\n\ttype: ChunkType;\n}\n\nexport interface Chunk {\n\tcontent: string;\n\tmetadata: ChunkMetadata;\n}\n\nexport interface SearchResult {\n\tcontent: string;\n\tmetadata: ChunkMetadata;\n\tscore: number;\n}\n\n// ============================================================================\n// Embedding\n// ============================================================================\n\nasync function getEmbedding(text: string, apiKey: string): Promise<number[]> {\n\tconst response = await fetch(\"https://api.openai.com/v1/embeddings\", {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: \"text-embedding-3-small\",\n\t\t\tinput: text,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Embedding API error: ${response.status} ${response.statusText}`);\n\t}\n\n\tconst data = (await response.json()) as { data: Array<{ embedding: number[] }> };\n\treturn data.data[0].embedding;\n}\n\nasync function getEmbeddingsBatch(texts: string[], apiKey: string): Promise<number[][]> {\n\tif (texts.length === 0) return [];\n\tif (texts.length === 1) return [await getEmbedding(texts[0], apiKey)];\n\n\tconst response = await fetch(\"https://api.openai.com/v1/embeddings\", {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: \"text-embedding-3-small\",\n\t\t\tinput: texts,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Embedding API error: ${response.status} ${response.statusText}`);\n\t}\n\n\tconst data = (await response.json()) as { data: Array<{ embedding: number[]; index: number }> };\n\t// Sort by index to maintain order\n\treturn data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n\tlet dot = 0;\n\tlet normA = 0;\n\tlet normB = 0;\n\tfor (let i = 0; i < a.length; i++) {\n\t\tdot += a[i] * b[i];\n\t\tnormA += a[i] * a[i];\n\t\tnormB += b[i] * b[i];\n\t}\n\treturn dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n\nfunction contentHash(content: string): string {\n\treturn createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\nfunction embeddingToBuffer(embedding: number[]): Buffer {\n\tconst buf = Buffer.alloc(embedding.length * 4);\n\tfor (let i = 0; i < embedding.length; i++) {\n\t\tbuf.writeFloatLE(embedding[i], i * 4);\n\t}\n\treturn buf;\n}\n\nfunction bufferToEmbedding(buf: Buffer): number[] {\n\tconst result: number[] = [];\n\tfor (let i = 0; i < buf.length; i += 4) {\n\t\tresult.push(buf.readFloatLE(i));\n\t}\n\treturn result;\n}\n\n// ============================================================================\n// VectorStore\n// ============================================================================\n\nexport class VectorStore {\n\tprivate db: Database.Database;\n\tprivate apiKey: string;\n\n\tconstructor(dbPath: string, openaiApiKey: string) {\n\t\tthis.apiKey = openaiApiKey;\n\n\t\t// Ensure directory exists\n\t\tconst dir = path.dirname(dbPath);\n\t\tif (!fs.existsSync(dir)) {\n\t\t\tfs.mkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tthis.db = new Database(dbPath);\n\t\tthis.db.pragma(\"journal_mode = WAL\");\n\t\tthis.initSchema();\n\t}\n\n\tprivate initSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS chunks (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tcontent TEXT NOT NULL,\n\t\t\t\tembedding BLOB NOT NULL,\n\t\t\t\tsource TEXT NOT NULL,\n\t\t\t\tclient TEXT NOT NULL,\n\t\t\t\ttimestamp INTEGER NOT NULL,\n\t\t\t\ttype TEXT NOT NULL DEFAULT 'general',\n\t\t\t\tcontent_hash TEXT NOT NULL UNIQUE\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_client ON chunks(client);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(content_hash);\n\t\t`);\n\t}\n\n\tasync index(chunks: Chunk[]): Promise<number> {\n\t\tif (chunks.length === 0) return 0;\n\n\t\t// Filter out duplicates by content hash\n\t\tconst existingHashes = new Set<string>();\n\t\tconst checkStmt = this.db.prepare(\"SELECT 1 FROM chunks WHERE content_hash = ?\");\n\t\tconst newChunks: Array<Chunk & { hash: string }> = [];\n\n\t\tfor (const chunk of chunks) {\n\t\t\tconst hash = contentHash(chunk.content);\n\t\t\tif (existingHashes.has(hash)) continue;\n\t\t\tconst exists = checkStmt.get(hash);\n\t\t\tif (exists) {\n\t\t\t\texistingHashes.add(hash);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnewChunks.push({ ...chunk, hash });\n\t\t\texistingHashes.add(hash);\n\t\t}\n\n\t\tif (newChunks.length === 0) return 0;\n\n\t\t// Batch embed\n\t\tconst embeddings = await getEmbeddingsBatch(\n\t\t\tnewChunks.map((c) => c.content),\n\t\t\tthis.apiKey,\n\t\t);\n\n\t\t// Insert\n\t\tconst insertStmt = this.db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO chunks (content, embedding, source, client, timestamp, type, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n\t\t);\n\n\t\tconst insertMany = this.db.transaction(() => {\n\t\t\tlet inserted = 0;\n\t\t\tfor (let i = 0; i < newChunks.length; i++) {\n\t\t\t\tconst chunk = newChunks[i];\n\t\t\t\tconst result = insertStmt.run(\n\t\t\t\t\tchunk.content,\n\t\t\t\t\tembeddingToBuffer(embeddings[i]),\n\t\t\t\t\tchunk.metadata.source,\n\t\t\t\t\tchunk.metadata.client,\n\t\t\t\t\tchunk.metadata.timestamp,\n\t\t\t\t\tchunk.metadata.type,\n\t\t\t\t\tchunk.hash,\n\t\t\t\t);\n\t\t\t\tif (result.changes > 0) inserted++;\n\t\t\t}\n\t\t\treturn inserted;\n\t\t});\n\n\t\treturn insertMany();\n\t}\n\n\tasync search(query: string, limit = 5): Promise<SearchResult[]> {\n\t\tconst queryEmbedding = await getEmbedding(query, this.apiKey);\n\t\tconst rows = this.db\n\t\t\t.prepare(\"SELECT content, embedding, source, client, timestamp, type FROM chunks\")\n\t\t\t.all() as Array<{\n\t\t\tcontent: string;\n\t\t\tembedding: Buffer;\n\t\t\tsource: string;\n\t\t\tclient: string;\n\t\t\ttimestamp: number;\n\t\t\ttype: ChunkType;\n\t\t}>;\n\n\t\tconst scored = rows.map((row) => ({\n\t\t\tcontent: row.content,\n\t\t\tmetadata: {\n\t\t\t\tsource: row.source,\n\t\t\t\tclient: row.client,\n\t\t\t\ttimestamp: row.timestamp,\n\t\t\t\ttype: row.type,\n\t\t\t},\n\t\t\tscore: cosineSimilarity(queryEmbedding, bufferToEmbedding(row.embedding)),\n\t\t}));\n\n\t\tscored.sort((a, b) => b.score - a.score);\n\t\treturn scored.slice(0, limit);\n\t}\n\n\tasync delete(sourceFile: string): Promise<number> {\n\t\tconst result = this.db.prepare(\"DELETE FROM chunks WHERE source = ?\").run(sourceFile);\n\t\treturn result.changes;\n\t}\n\n\tgetStats(): { totalChunks: number; sources: string[] } {\n\t\tconst count = this.db.prepare(\"SELECT COUNT(*) as count FROM chunks\").get() as { count: number };\n\t\tconst sources = this.db.prepare(\"SELECT DISTINCT source FROM chunks ORDER BY source\").all() as Array<{\n\t\t\tsource: string;\n\t\t}>;\n\t\treturn {\n\t\t\ttotalChunks: count.count,\n\t\t\tsources: sources.map((s) => s.source),\n\t\t};\n\t}\n\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n"]}
1
+ {"version":3,"file":"vector-store.js","sourceRoot":"","sources":["../src/vector-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Vector store backed by SQLite with OpenAI embeddings.\n * Provides local-first semantic search per client namespace.\n */\n\nimport { createHash } from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport Database from \"better-sqlite3\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type ChunkType = \"decision\" | \"convention\" | \"pattern\" | \"general\";\n\nexport interface ChunkMetadata {\n\tsource: string;\n\tclient: string;\n\ttimestamp: number;\n\ttype: ChunkType;\n}\n\nexport interface Chunk {\n\tcontent: string;\n\tmetadata: ChunkMetadata;\n}\n\nexport interface SearchResult {\n\tcontent: string;\n\tmetadata: ChunkMetadata;\n\tscore: number;\n}\n\n// ============================================================================\n// Embedding\n// ============================================================================\n\nasync function getEmbedding(text: string, apiKey: string): Promise<number[]> {\n\tconst response = await fetch(\"https://api.openai.com/v1/embeddings\", {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: \"text-embedding-3-small\",\n\t\t\tinput: text,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Embedding API error: ${response.status} ${response.statusText}`);\n\t}\n\n\tconst data = (await response.json()) as { data: Array<{ embedding: number[] }> };\n\treturn data.data[0].embedding;\n}\n\nasync function getEmbeddingsBatch(texts: string[], apiKey: string): Promise<number[][]> {\n\tif (texts.length === 0) return [];\n\tif (texts.length === 1) return [await getEmbedding(texts[0], apiKey)];\n\n\tconst response = await fetch(\"https://api.openai.com/v1/embeddings\", {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\tAuthorization: `Bearer ${apiKey}`,\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tmodel: \"text-embedding-3-small\",\n\t\t\tinput: texts,\n\t\t}),\n\t});\n\n\tif (!response.ok) {\n\t\tthrow new Error(`Embedding API error: ${response.status} ${response.statusText}`);\n\t}\n\n\tconst data = (await response.json()) as { data: Array<{ embedding: number[]; index: number }> };\n\t// Sort by index to maintain order\n\treturn data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);\n}\n\nfunction cosineSimilarity(a: number[], b: number[]): number {\n\tlet dot = 0;\n\tlet normA = 0;\n\tlet normB = 0;\n\tfor (let i = 0; i < a.length; i++) {\n\t\tdot += a[i] * b[i];\n\t\tnormA += a[i] * a[i];\n\t\tnormB += b[i] * b[i];\n\t}\n\treturn dot / (Math.sqrt(normA) * Math.sqrt(normB));\n}\n\nfunction contentHash(content: string): string {\n\treturn createHash(\"sha256\").update(content).digest(\"hex\");\n}\n\nfunction embeddingToBuffer(embedding: number[]): Buffer {\n\tconst buf = Buffer.alloc(embedding.length * 4);\n\tfor (let i = 0; i < embedding.length; i++) {\n\t\tbuf.writeFloatLE(embedding[i], i * 4);\n\t}\n\treturn buf;\n}\n\nfunction bufferToEmbedding(buf: Buffer): number[] {\n\tconst result: number[] = [];\n\tfor (let i = 0; i < buf.length; i += 4) {\n\t\tresult.push(buf.readFloatLE(i));\n\t}\n\treturn result;\n}\n\n// ============================================================================\n// VectorStore\n// ============================================================================\n\nexport class VectorStore {\n\tprivate db: Database.Database;\n\tprivate apiKey: string;\n\n\tconstructor(dbPath: string, openaiApiKey: string) {\n\t\tthis.apiKey = openaiApiKey;\n\n\t\t// Ensure directory exists\n\t\tconst dir = path.dirname(dbPath);\n\t\tif (!fs.existsSync(dir)) {\n\t\t\tfs.mkdirSync(dir, { recursive: true });\n\t\t}\n\n\t\tthis.db = new Database(dbPath);\n\t\tthis.db.pragma(\"journal_mode = WAL\");\n\t\tthis.initSchema();\n\t}\n\n\tprivate initSchema(): void {\n\t\tthis.db.exec(`\n\t\t\tCREATE TABLE IF NOT EXISTS chunks (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tcontent TEXT NOT NULL,\n\t\t\t\tembedding BLOB NOT NULL,\n\t\t\t\tsource TEXT NOT NULL,\n\t\t\t\tclient TEXT NOT NULL,\n\t\t\t\ttimestamp INTEGER NOT NULL,\n\t\t\t\ttype TEXT NOT NULL DEFAULT 'general',\n\t\t\t\tcontent_hash TEXT NOT NULL UNIQUE\n\t\t\t);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_source ON chunks(source);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_client ON chunks(client);\n\t\t\tCREATE INDEX IF NOT EXISTS idx_chunks_hash ON chunks(content_hash);\n\t\t`);\n\t}\n\n\tasync index(chunks: Chunk[]): Promise<number> {\n\t\tif (chunks.length === 0) return 0;\n\n\t\t// Filter out duplicates by content hash\n\t\tconst existingHashes = new Set<string>();\n\t\tconst checkStmt = this.db.prepare(\"SELECT 1 FROM chunks WHERE content_hash = ?\");\n\t\tconst newChunks: Array<Chunk & { hash: string }> = [];\n\n\t\tfor (const chunk of chunks) {\n\t\t\tconst hash = contentHash(chunk.content);\n\t\t\tif (existingHashes.has(hash)) continue;\n\t\t\tconst exists = checkStmt.get(hash);\n\t\t\tif (exists) {\n\t\t\t\texistingHashes.add(hash);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tnewChunks.push({ ...chunk, hash });\n\t\t\texistingHashes.add(hash);\n\t\t}\n\n\t\tif (newChunks.length === 0) return 0;\n\n\t\t// Batch embed\n\t\tconst embeddings = await getEmbeddingsBatch(\n\t\t\tnewChunks.map((c) => c.content),\n\t\t\tthis.apiKey,\n\t\t);\n\n\t\t// Insert\n\t\tconst insertStmt = this.db.prepare(\n\t\t\t\"INSERT OR IGNORE INTO chunks (content, embedding, source, client, timestamp, type, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?)\",\n\t\t);\n\n\t\tconst insertMany = this.db.transaction(() => {\n\t\t\tlet inserted = 0;\n\t\t\tfor (let i = 0; i < newChunks.length; i++) {\n\t\t\t\tconst chunk = newChunks[i];\n\t\t\t\tconst result = insertStmt.run(\n\t\t\t\t\tchunk.content,\n\t\t\t\t\tembeddingToBuffer(embeddings[i]),\n\t\t\t\t\tchunk.metadata.source,\n\t\t\t\t\tchunk.metadata.client,\n\t\t\t\t\tchunk.metadata.timestamp,\n\t\t\t\t\tchunk.metadata.type,\n\t\t\t\t\tchunk.hash,\n\t\t\t\t);\n\t\t\t\tif (result.changes > 0) inserted++;\n\t\t\t}\n\t\t\treturn inserted;\n\t\t});\n\n\t\treturn insertMany();\n\t}\n\n\tasync search(query: string, limit = 5): Promise<SearchResult[]> {\n\t\tconst queryEmbedding = await getEmbedding(query, this.apiKey);\n\t\tconst rows = this.db\n\t\t\t.prepare(\"SELECT content, embedding, source, client, timestamp, type FROM chunks\")\n\t\t\t.all() as Array<{\n\t\t\tcontent: string;\n\t\t\tembedding: Buffer;\n\t\t\tsource: string;\n\t\t\tclient: string;\n\t\t\ttimestamp: number;\n\t\t\ttype: ChunkType;\n\t\t}>;\n\n\t\tconst scored = rows.map((row) => ({\n\t\t\tcontent: row.content,\n\t\t\tmetadata: {\n\t\t\t\tsource: row.source,\n\t\t\t\tclient: row.client,\n\t\t\t\ttimestamp: row.timestamp,\n\t\t\t\ttype: row.type,\n\t\t\t},\n\t\t\tscore: cosineSimilarity(queryEmbedding, bufferToEmbedding(row.embedding)),\n\t\t}));\n\n\t\tscored.sort((a, b) => b.score - a.score);\n\t\treturn scored.slice(0, limit);\n\t}\n\n\tasync delete(sourceFile: string): Promise<number> {\n\t\tconst result = this.db.prepare(\"DELETE FROM chunks WHERE source = ?\").run(sourceFile);\n\t\treturn result.changes;\n\t}\n\n\tgetStats(): { totalChunks: number; sources: string[] } {\n\t\tconst count = this.db.prepare(\"SELECT COUNT(*) as count FROM chunks\").get() as { count: number };\n\t\tconst sources = this.db.prepare(\"SELECT DISTINCT source FROM chunks ORDER BY source\").all() as Array<{\n\t\t\tsource: string;\n\t\t}>;\n\t\treturn {\n\t\t\ttotalChunks: count.count,\n\t\t\tsources: sources.map((s) => s.source),\n\t\t};\n\t}\n\n\tclose(): void {\n\t\tthis.db.close();\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draht/knowledge",
3
- "version": "2026.7.30",
3
+ "version": "2026.10.4-2",
4
4
  "description": "Client knowledge base — per-client vector DB for knowledge persistence across projects",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "devDependencies": {
31
31
  "vitest": "^3.2.4",
32
- "@draht/coding-agent": "2026.7.30",
32
+ "@draht/coding-agent": "2026.10.4-2",
33
33
  "@types/better-sqlite3": "^7.6.12",
34
34
  "@types/node": "^22.10.5",
35
35
  "typescript": "7.0.2"