src / tools / knowledgeBase.ts
src / tools / knowledgeBase.ts
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { getState } from '../stateRegistry';
import { validatePath } from '../pathSecurity';
const KB_FILE = '.timber_kb.json';
interface KBEntry {
id: string;
file_path: string;
content_hash: string;
summary: string;
keywords: string[];
last_updated: number;
}
let knowledgeBase: KBEntry[] = [];
async function loadKB(workspaceRoot: string): Promise<void> {
try {
const content = await fs.readFile(path.join(workspaceRoot, KB_FILE), 'utf-8');
knowledgeBase = JSON.parse(content);
} catch {}
}
export async function kb_scan_directory(dirPath: string, maxDepth?: number, workspaceRoot: string = process.cwd()): Promise<{ scanned_files: number; indexed_entries: number }> {
await loadKB(workspaceRoot);
const safeDir = await validatePath(workspaceRoot, workspaceRoot, dirPath);
let scanned = 0;
let indexed = 0;
const currentFiles = await fs.readdir(safeDir);
for (const file of currentFiles) {
if (file.startsWith('.')) continue;
const fullPath = validatePath(workspaceRoot, workspaceRoot, path.join(safeDir, file));
const stat = await fs.stat(fullPath);
if (stat.isDirectory() && (!maxDepth || maxDepth > 0)) {
const result = await kb_scan_directory(fullPath, maxDepth ? maxDepth - 1 : undefined, workspaceRoot);
scanned += result.scanned_files;
indexed += result.indexed_entries;
} else if (stat.isFile() && (file.endsWith('.ts') || file.endsWith('.js') || file.endsWith('.md'))) {
const content = await fs.readFile(fullPath, 'utf-8');
const hash = crypto.createHash('sha256').update(content).digest('hex').substr(0, 16);
const existing = knowledgeBase.find(e => e.file_path === fullPath && e.content_hash === hash);
if (!existing) {
knowledgeBase.push({
id: `kb_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
file_path: fullPath,
content_hash: hash,
summary: `Indexed ${path.basename(fullPath)} (${content.length} chars)`,
keywords: [file.split('.')[1] || 'unknown'],
last_updated: Date.now()
});
indexed++;
}
scanned++;
}
}
await fs.writeFile(path.join(workspaceRoot, KB_FILE), JSON.stringify(knowledgeBase, null, 2));
getState().lastConfigChange = new Date();
return { scanned_files: scanned, indexed_entries: indexed };
}
export async function kb_search(query: string, workspaceRoot: string = process.cwd()): Promise<{ results: Array<{ file_path: string; summary: string; score: number }> }> {
await loadKB(workspaceRoot);
const queryLower = query.toLowerCase();
const scoredResults: Array<{ entry: KBEntry; score: number }> = [];
for (const entry of knowledgeBase) {
let score = 0;
// Score metadata matches
if (entry.summary.toLowerCase().includes(queryLower)) score += 10;
for (const k of entry.keywords) {
if (k.toLowerCase().includes(queryLower)) score += 5;
}
// Also search actual file content
try {
const content = await fs.readFile(entry.file_path, 'utf-8');
const lowerContent = content.toLowerCase();
// Count occurrences of query terms in the file
const words = queryLower.split(/\s+/).filter(w => w.length > 2);
for (const word of words) {
let idx = 0;
while ((idx = lowerContent.indexOf(word, idx)) !== -1) {
score += 3;
idx += word.length;
}
}
} catch {} // File may have been deleted or unreadable
if (score > 0) {
scoredResults.push({ entry, score });
}
}
// Sort by relevance score descending
scoredResults.sort((a, b) => b.score - a.score);
return {
results: scoredResults.map(r => ({
file_path: r.entry.file_path,
summary: r.entry.summary,
score: r.score
}))
};
}
export async function kb_get_entries(workspaceRoot: string = process.cwd()): Promise<{ entries: KBEntry[] }> {
await loadKB(workspaceRoot);
return { entries: knowledgeBase };
}
export async function kb_clear(workspaceRoot: string = process.cwd()): Promise<{ success: boolean }> {
try {
await fs.unlink(path.join(workspaceRoot, KB_FILE));
knowledgeBase = [];
return { success: true };
} catch {
return { success: false };
}
}
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { getState } from '../stateRegistry';
import { validatePath } from '../pathSecurity';
const KB_FILE = '.timber_kb.json';
interface KBEntry {
id: string;
file_path: string;
content_hash: string;
summary: string;
keywords: string[];
last_updated: number;
}
let knowledgeBase: KBEntry[] = [];
async function loadKB(workspaceRoot: string): Promise<void> {
try {
const content = await fs.readFile(path.join(workspaceRoot, KB_FILE), 'utf-8');
knowledgeBase = JSON.parse(content);
} catch {}
}
export async function kb_scan_directory(dirPath: string, maxDepth?: number, workspaceRoot: string = process.cwd()): Promise<{ scanned_files: number; indexed_entries: number }> {
await loadKB(workspaceRoot);
const safeDir = await validatePath(workspaceRoot, workspaceRoot, dirPath);
let scanned = 0;
let indexed = 0;
const currentFiles = await fs.readdir(safeDir);
for (const file of currentFiles) {
if (file.startsWith('.')) continue;
const fullPath = validatePath(workspaceRoot, workspaceRoot, path.join(safeDir, file));
const stat = await fs.stat(fullPath);
if (stat.isDirectory() && (!maxDepth || maxDepth > 0)) {
const result = await kb_scan_directory(fullPath, maxDepth ? maxDepth - 1 : undefined, workspaceRoot);
scanned += result.scanned_files;
indexed += result.indexed_entries;
} else if (stat.isFile() && (file.endsWith('.ts') || file.endsWith('.js') || file.endsWith('.md'))) {
const content = await fs.readFile(fullPath, 'utf-8');
const hash = crypto.createHash('sha256').update(content).digest('hex').substr(0, 16);
const existing = knowledgeBase.find(e => e.file_path === fullPath && e.content_hash === hash);
if (!existing) {
knowledgeBase.push({
id: `kb_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
file_path: fullPath,
content_hash: hash,
summary: `Indexed ${path.basename(fullPath)} (${content.length} chars)`,
keywords: [file.split('.')[1] || 'unknown'],
last_updated: Date.now()
});
indexed++;
}
scanned++;
}
}
await fs.writeFile(path.join(workspaceRoot, KB_FILE), JSON.stringify(knowledgeBase, null, 2));
getState().lastConfigChange = new Date();
return { scanned_files: scanned, indexed_entries: indexed };
}
export async function kb_search(query: string, workspaceRoot: string = process.cwd()): Promise<{ results: Array<{ file_path: string; summary: string; score: number }> }> {
await loadKB(workspaceRoot);
const queryLower = query.toLowerCase();
const scoredResults: Array<{ entry: KBEntry; score: number }> = [];
for (const entry of knowledgeBase) {
let score = 0;
// Score metadata matches
if (entry.summary.toLowerCase().includes(queryLower)) score += 10;
for (const k of entry.keywords) {
if (k.toLowerCase().includes(queryLower)) score += 5;
}
// Also search actual file content
try {
const content = await fs.readFile(entry.file_path, 'utf-8');
const lowerContent = content.toLowerCase();
// Count occurrences of query terms in the file
const words = queryLower.split(/\s+/).filter(w => w.length > 2);
for (const word of words) {
let idx = 0;
while ((idx = lowerContent.indexOf(word, idx)) !== -1) {
score += 3;
idx += word.length;
}
}
} catch {} // File may have been deleted or unreadable
if (score > 0) {
scoredResults.push({ entry, score });
}
}
// Sort by relevance score descending
scoredResults.sort((a, b) => b.score - a.score);
return {
results: scoredResults.map(r => ({
file_path: r.entry.file_path,
summary: r.entry.summary,
score: r.score
}))
};
}
export async function kb_get_entries(workspaceRoot: string = process.cwd()): Promise<{ entries: KBEntry[] }> {
await loadKB(workspaceRoot);
return { entries: knowledgeBase };
}
export async function kb_clear(workspaceRoot: string = process.cwd()): Promise<{ success: boolean }> {
try {
await fs.unlink(path.join(workspaceRoot, KB_FILE));
knowledgeBase = [];
return { success: true };
} catch {
return { success: false };
}
}