feat(index): add sqlite index with fts5 search

This commit is contained in:
chahinebrini 2026-06-24 23:35:53 +02:00
parent c5af4c7b6a
commit 2b9e409b8b
2 changed files with 146 additions and 0 deletions

111
src/core/index.ts Normal file
View File

@ -0,0 +1,111 @@
import Database from 'better-sqlite3';
import { mkdirSync } from 'fs';
import { dirname } from 'path';
import { getIndexPath } from './paths.js';
export interface IndexEntry {
id: string;
type: string;
title: string;
content: string;
filePath: string;
createdAt: string;
updatedAt: string;
status?: string;
role?: string;
assignedTo?: string;
tags?: string;
}
export class Index {
private db: Database.Database;
constructor(cwd: string) {
const path = getIndexPath(cwd);
mkdirSync(dirname(path), { recursive: true });
this.db = new Database(path);
this.db.exec(`
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
filePath TEXT NOT NULL,
createdAt TEXT NOT NULL,
updatedAt TEXT NOT NULL,
status TEXT,
role TEXT,
assignedTo TEXT,
tags TEXT
);
CREATE VIRTUAL TABLE IF NOT EXISTS search USING fts5(id, title, content);
`);
}
upsert(entry: IndexEntry): void {
const params = {
status: null,
role: null,
assignedTo: null,
tags: null,
...entry,
};
const insert = this.db.prepare(`
INSERT INTO entities (id, type, title, content, filePath, createdAt, updatedAt, status, role, assignedTo, tags)
VALUES (@id, @type, @title, @content, @filePath, @createdAt, @updatedAt, @status, @role, @assignedTo, @tags)
ON CONFLICT(id) DO UPDATE SET
type=@type, title=@title, content=@content, filePath=@filePath,
createdAt=@createdAt, updatedAt=@updatedAt, status=@status, role=@role,
assignedTo=@assignedTo, tags=@tags
`);
insert.run(params);
const search = this.db.prepare(`
INSERT OR REPLACE INTO search (id, title, content) VALUES (@id, @title, @content)
`);
search.run(params);
}
search(query: string): Array<{ id: string; type: string; title: string }> {
const stmt = this.db.prepare(`
SELECT e.id, e.type, e.title
FROM search s
JOIN entities e ON s.id = e.id
WHERE search MATCH @query
ORDER BY rank
`);
return stmt.all({ query }) as Array<{ id: string; type: string; title: string }>;
}
list(type?: string, filters?: { status?: string; role?: string; assignedTo?: string }): IndexEntry[] {
let sql = 'SELECT * FROM entities WHERE 1=1';
const params: Record<string, string> = {};
if (type) {
sql += ' AND type = @type';
params.type = type;
}
if (filters?.status) {
sql += ' AND status = @status';
params.status = filters.status;
}
if (filters?.role) {
sql += ' AND role = @role';
params.role = filters.role;
}
if (filters?.assignedTo) {
sql += ' AND assignedTo = @assignedTo';
params.assignedTo = filters.assignedTo;
}
sql += ' ORDER BY createdAt DESC';
const stmt = this.db.prepare(sql);
return stmt.all(params) as IndexEntry[];
}
close(): void {
this.db.close();
}
}

35
tests/index.test.ts Normal file
View File

@ -0,0 +1,35 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdtempSync, rmSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import { Index } from '../src/core/index.js';
describe('index', () => {
let cwd: string;
beforeEach(() => {
cwd = mkdtempSync(join(tmpdir(), 'agenthub-'));
});
afterEach(() => {
rmSync(cwd, { recursive: true, force: true });
});
it('indexes and searches a task', () => {
const index = new Index(cwd);
index.upsert({
id: 'TSK-0001',
type: 'task',
title: 'Implement DNS cache',
content: 'Add local DNS cache for performance',
filePath: join(cwd, '.agenthub', 'tasks', 'TSK-0001.md'),
createdAt: '2026-06-24T10:00:00Z',
updatedAt: '2026-06-24T10:00:00Z',
});
const results = index.search('DNS');
expect(results).toHaveLength(1);
expect(results[0].id).toBe('TSK-0001');
index.close();
});
});