ctxpkg 0.0.1
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/LICENSE +661 -0
- package/README.md +282 -0
- package/bin/cli.js +8 -0
- package/bin/daemon.js +7 -0
- package/package.json +70 -0
- package/src/agent/AGENTS.md +249 -0
- package/src/agent/agent.prompts.ts +66 -0
- package/src/agent/agent.test-runner.schemas.ts +158 -0
- package/src/agent/agent.test-runner.ts +436 -0
- package/src/agent/agent.ts +371 -0
- package/src/agent/agent.types.ts +94 -0
- package/src/backend/AGENTS.md +112 -0
- package/src/backend/backend.protocol.ts +95 -0
- package/src/backend/backend.schemas.ts +123 -0
- package/src/backend/backend.services.ts +151 -0
- package/src/backend/backend.ts +111 -0
- package/src/backend/backend.types.ts +34 -0
- package/src/cli/AGENTS.md +213 -0
- package/src/cli/cli.agent.ts +197 -0
- package/src/cli/cli.chat.ts +369 -0
- package/src/cli/cli.client.ts +55 -0
- package/src/cli/cli.collections.ts +491 -0
- package/src/cli/cli.config.ts +252 -0
- package/src/cli/cli.daemon.ts +160 -0
- package/src/cli/cli.documents.ts +413 -0
- package/src/cli/cli.mcp.ts +177 -0
- package/src/cli/cli.ts +28 -0
- package/src/cli/cli.utils.ts +122 -0
- package/src/client/AGENTS.md +135 -0
- package/src/client/client.adapters.ts +279 -0
- package/src/client/client.ts +86 -0
- package/src/client/client.types.ts +17 -0
- package/src/collections/AGENTS.md +185 -0
- package/src/collections/collections.schemas.ts +195 -0
- package/src/collections/collections.ts +1160 -0
- package/src/config/config.ts +118 -0
- package/src/daemon/AGENTS.md +168 -0
- package/src/daemon/daemon.config.ts +23 -0
- package/src/daemon/daemon.manager.ts +215 -0
- package/src/daemon/daemon.schemas.ts +22 -0
- package/src/daemon/daemon.ts +205 -0
- package/src/database/AGENTS.md +211 -0
- package/src/database/database.ts +64 -0
- package/src/database/migrations/migrations.001-init.ts +56 -0
- package/src/database/migrations/migrations.002-fts5.ts +32 -0
- package/src/database/migrations/migrations.ts +20 -0
- package/src/database/migrations/migrations.types.ts +9 -0
- package/src/documents/AGENTS.md +301 -0
- package/src/documents/documents.schemas.ts +190 -0
- package/src/documents/documents.ts +734 -0
- package/src/embedder/embedder.ts +53 -0
- package/src/exports.ts +0 -0
- package/src/mcp/AGENTS.md +264 -0
- package/src/mcp/mcp.ts +105 -0
- package/src/tools/AGENTS.md +228 -0
- package/src/tools/agent/agent.ts +45 -0
- package/src/tools/documents/documents.ts +401 -0
- package/src/tools/tools.langchain.ts +37 -0
- package/src/tools/tools.mcp.ts +46 -0
- package/src/tools/tools.types.ts +35 -0
- package/src/utils/utils.services.ts +46 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { createServer, type Server } from 'node:http';
|
|
2
|
+
import { mkdir, rm, writeFile, unlink } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { WebSocketServer, WebSocket } from 'ws';
|
|
6
|
+
|
|
7
|
+
import { getSocketPath, getPidFile, getIdleTimeout } from './daemon.config.ts';
|
|
8
|
+
import type { DaemonOptions, DaemonStatus } from './daemon.schemas.ts';
|
|
9
|
+
|
|
10
|
+
import { Backend } from '#root/backend/backend.ts';
|
|
11
|
+
import { destroy } from '#root/utils/utils.services.ts';
|
|
12
|
+
|
|
13
|
+
class Daemon {
|
|
14
|
+
#backend: Backend;
|
|
15
|
+
#httpServer: Server | null = null;
|
|
16
|
+
#wsServer: WebSocketServer | null = null;
|
|
17
|
+
#connections = new Set<WebSocket>();
|
|
18
|
+
#idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
19
|
+
#startTime = 0;
|
|
20
|
+
#socketPath: string;
|
|
21
|
+
#pidFile: string;
|
|
22
|
+
#idleTimeout: number;
|
|
23
|
+
#isShuttingDown = false;
|
|
24
|
+
|
|
25
|
+
constructor(options?: DaemonOptions) {
|
|
26
|
+
this.#socketPath = options?.socketPath ?? getSocketPath();
|
|
27
|
+
this.#pidFile = options?.pidFile ?? getPidFile();
|
|
28
|
+
this.#idleTimeout = options?.idleTimeout ?? getIdleTimeout();
|
|
29
|
+
this.#backend = new Backend();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async start(): Promise<void> {
|
|
33
|
+
// Ensure data directory exists
|
|
34
|
+
await mkdir(dirname(this.#socketPath), { recursive: true });
|
|
35
|
+
|
|
36
|
+
// Remove existing socket file if present
|
|
37
|
+
try {
|
|
38
|
+
await rm(this.#socketPath, { force: true });
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore errors
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Write PID file
|
|
44
|
+
await writeFile(this.#pidFile, String(process.pid));
|
|
45
|
+
|
|
46
|
+
// Create HTTP server listening on Unix socket
|
|
47
|
+
this.#httpServer = createServer();
|
|
48
|
+
|
|
49
|
+
// Create WebSocket server attached to HTTP server
|
|
50
|
+
this.#wsServer = new WebSocketServer({ server: this.#httpServer });
|
|
51
|
+
this.#startTime = Date.now();
|
|
52
|
+
|
|
53
|
+
this.#wsServer.on('connection', (socket) => {
|
|
54
|
+
this.#handleConnection(socket);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
this.#wsServer.on('error', (error) => {
|
|
58
|
+
console.error('[daemon] WebSocket server error:', error);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.#httpServer.on('error', (error) => {
|
|
62
|
+
console.error('[daemon] HTTP server error:', error);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Start listening on Unix socket
|
|
66
|
+
await new Promise<void>((resolve, reject) => {
|
|
67
|
+
this.#httpServer?.listen(this.#socketPath, () => {
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
this.#httpServer?.on('error', reject);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Start idle timer if no connections
|
|
74
|
+
this.#resetIdleTimer();
|
|
75
|
+
|
|
76
|
+
// Handle shutdown signals
|
|
77
|
+
process.on('SIGTERM', () => this.stop());
|
|
78
|
+
process.on('SIGINT', () => this.stop());
|
|
79
|
+
|
|
80
|
+
console.log(`[daemon] Started on ${this.#socketPath} (PID: ${process.pid})`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#handleConnection(socket: WebSocket): void {
|
|
84
|
+
this.#connections.add(socket);
|
|
85
|
+
this.#backend.setConnectionCount(this.#connections.size);
|
|
86
|
+
this.#clearIdleTimer();
|
|
87
|
+
|
|
88
|
+
console.log(`[daemon] Client connected (${this.#connections.size} total)`);
|
|
89
|
+
|
|
90
|
+
socket.on('message', async (data) => {
|
|
91
|
+
try {
|
|
92
|
+
const request = JSON.parse(data.toString());
|
|
93
|
+
const response = await this.#backend.handleRequest(request);
|
|
94
|
+
socket.send(JSON.stringify(response));
|
|
95
|
+
} catch (error) {
|
|
96
|
+
const errorResponse = {
|
|
97
|
+
id: 'unknown',
|
|
98
|
+
error: {
|
|
99
|
+
code: -32700,
|
|
100
|
+
message: error instanceof Error ? error.message : 'Parse error',
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
socket.send(JSON.stringify(errorResponse));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
socket.on('close', () => {
|
|
108
|
+
this.#connections.delete(socket);
|
|
109
|
+
this.#backend.setConnectionCount(this.#connections.size);
|
|
110
|
+
console.log(`[daemon] Client disconnected (${this.#connections.size} remaining)`);
|
|
111
|
+
|
|
112
|
+
if (this.#connections.size === 0) {
|
|
113
|
+
this.#resetIdleTimer();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
socket.on('error', (error) => {
|
|
118
|
+
console.error('[daemon] Socket error:', error);
|
|
119
|
+
this.#connections.delete(socket);
|
|
120
|
+
this.#backend.setConnectionCount(this.#connections.size);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#resetIdleTimer(): void {
|
|
125
|
+
this.#clearIdleTimer();
|
|
126
|
+
|
|
127
|
+
if (this.#idleTimeout > 0 && this.#connections.size === 0) {
|
|
128
|
+
console.log(`[daemon] Starting idle timer (${this.#idleTimeout / 1000}s)`);
|
|
129
|
+
this.#idleTimer = setTimeout(() => {
|
|
130
|
+
console.log('[daemon] Idle timeout reached, shutting down');
|
|
131
|
+
this.stop();
|
|
132
|
+
}, this.#idleTimeout);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
#clearIdleTimer(): void {
|
|
137
|
+
if (this.#idleTimer) {
|
|
138
|
+
clearTimeout(this.#idleTimer);
|
|
139
|
+
this.#idleTimer = null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async stop(): Promise<void> {
|
|
144
|
+
if (this.#isShuttingDown) return;
|
|
145
|
+
this.#isShuttingDown = true;
|
|
146
|
+
|
|
147
|
+
console.log('[daemon] Shutting down...');
|
|
148
|
+
|
|
149
|
+
this.#clearIdleTimer();
|
|
150
|
+
|
|
151
|
+
// Close all WebSocket connections
|
|
152
|
+
for (const socket of this.#connections) {
|
|
153
|
+
socket.close(1000, 'Server shutting down');
|
|
154
|
+
}
|
|
155
|
+
this.#connections.clear();
|
|
156
|
+
|
|
157
|
+
// Close WebSocket server
|
|
158
|
+
const wsServer = this.#wsServer;
|
|
159
|
+
if (wsServer) {
|
|
160
|
+
await new Promise<void>((resolve) => {
|
|
161
|
+
wsServer.close(() => resolve());
|
|
162
|
+
});
|
|
163
|
+
this.#wsServer = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Close HTTP server
|
|
167
|
+
const httpServer = this.#httpServer;
|
|
168
|
+
if (httpServer) {
|
|
169
|
+
await new Promise<void>((resolve) => {
|
|
170
|
+
httpServer.close(() => resolve());
|
|
171
|
+
});
|
|
172
|
+
this.#httpServer = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Cleanup backend
|
|
176
|
+
await this.#backend[destroy]();
|
|
177
|
+
|
|
178
|
+
// Remove socket and PID files
|
|
179
|
+
try {
|
|
180
|
+
await unlink(this.#socketPath);
|
|
181
|
+
} catch {
|
|
182
|
+
// Ignore
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await unlink(this.#pidFile);
|
|
186
|
+
} catch {
|
|
187
|
+
// Ignore
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log('[daemon] Stopped');
|
|
191
|
+
process.exit(0);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getStatus(): DaemonStatus {
|
|
195
|
+
return {
|
|
196
|
+
running: this.#httpServer !== null,
|
|
197
|
+
socketPath: this.#socketPath,
|
|
198
|
+
pid: process.pid,
|
|
199
|
+
uptime: Date.now() - this.#startTime,
|
|
200
|
+
connections: this.#connections.size,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export { Daemon };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# Database — Agent Guidelines
|
|
2
|
+
|
|
3
|
+
This document describes the database module architecture for AI agents working on this codebase.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The database module provides SQLite storage with vector search capabilities using [sqlite-vec](https://github.com/asg017/sqlite-vec). It uses [Knex](https://knexjs.org/) for query building and migrations. The database stores collection metadata, reference documents, and vector embeddings for semantic search.
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/database/
|
|
13
|
+
├── database.ts # DatabaseService class
|
|
14
|
+
└── migrations/
|
|
15
|
+
├── migrations.ts # Migration source (collects all migrations)
|
|
16
|
+
├── migrations.types.ts # Migration type definition
|
|
17
|
+
└── migrations.001-init.ts # Initial schema
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Architecture
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
24
|
+
│ DatabaseService │
|
|
25
|
+
│ │
|
|
26
|
+
│ getInstance() → lazy init, singleton pattern │
|
|
27
|
+
│ │ │
|
|
28
|
+
│ ▼ │
|
|
29
|
+
│ ┌─────────────────────────────────────────────────────┐ │
|
|
30
|
+
│ │ Knex │ │
|
|
31
|
+
│ │ (query builder) │ │
|
|
32
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
33
|
+
│ │ │
|
|
34
|
+
│ ┌──────────────────────▼──────────────────────────────┐ │
|
|
35
|
+
│ │ better-sqlite3 │ │
|
|
36
|
+
│ │ (SQLite driver for Node.js) │ │
|
|
37
|
+
│ └──────────────────────┬──────────────────────────────┘ │
|
|
38
|
+
│ │ │
|
|
39
|
+
│ ┌──────────────────────▼──────────────────────────────┐ │
|
|
40
|
+
│ │ sqlite-vec │ │
|
|
41
|
+
│ │ (vector extension, loaded via afterCreate) │ │
|
|
42
|
+
│ └─────────────────────────────────────────────────────┘ │
|
|
43
|
+
└─────────────────────────────────────────────────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Schema
|
|
47
|
+
|
|
48
|
+
### `collections`
|
|
49
|
+
|
|
50
|
+
Tracks synced collection packages:
|
|
51
|
+
|
|
52
|
+
| Column | Type | Description |
|
|
53
|
+
|--------|------|-------------|
|
|
54
|
+
| `id` | string (PK) | Collection ID (`pkg:{url}`) |
|
|
55
|
+
| `url` | text | Manifest URL |
|
|
56
|
+
| `name` | string? | Package name from manifest |
|
|
57
|
+
| `version` | string? | Package version from manifest |
|
|
58
|
+
| `description` | text? | Package description from manifest |
|
|
59
|
+
| `manifest_hash` | string? | SHA-256 of manifest |
|
|
60
|
+
| `last_sync_at` | string? | ISO timestamp of last sync |
|
|
61
|
+
| `created_at` | string | ISO timestamp |
|
|
62
|
+
| `updated_at` | string | ISO timestamp |
|
|
63
|
+
|
|
64
|
+
### `reference_documents`
|
|
65
|
+
|
|
66
|
+
Stores full document content:
|
|
67
|
+
|
|
68
|
+
| Column | Type | Description |
|
|
69
|
+
|--------|------|-------------|
|
|
70
|
+
| `collection` | string (PK) | Collection ID |
|
|
71
|
+
| `id` | string (PK) | Document ID (e.g., file path) |
|
|
72
|
+
| `hash` | string | SHA-256 of content (change detection) |
|
|
73
|
+
| `content` | text | Full document content |
|
|
74
|
+
|
|
75
|
+
### `reference_document_chunks`
|
|
76
|
+
|
|
77
|
+
Stores document chunks with embeddings:
|
|
78
|
+
|
|
79
|
+
| Column | Type | Description |
|
|
80
|
+
|--------|------|-------------|
|
|
81
|
+
| `id` | string (PK) | UUID |
|
|
82
|
+
| `collection` | string | Collection ID |
|
|
83
|
+
| `document` | string | Parent document ID |
|
|
84
|
+
| `content` | text | Chunk text (~500 chars) |
|
|
85
|
+
| `embedding` | vector(1024) | Vector embedding |
|
|
86
|
+
|
|
87
|
+
## Usage
|
|
88
|
+
|
|
89
|
+
### Getting Database Instance
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { DatabaseService, tableNames } from '#root/database/database.ts';
|
|
93
|
+
|
|
94
|
+
const databaseService = services.get(DatabaseService);
|
|
95
|
+
const db = await databaseService.getInstance();
|
|
96
|
+
|
|
97
|
+
// Query using Knex
|
|
98
|
+
const docs = await db(tableNames.referenceDocuments)
|
|
99
|
+
.where({ collection: 'my-collection' })
|
|
100
|
+
.select('*');
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Vector Search
|
|
104
|
+
|
|
105
|
+
sqlite-vec provides `vec_distance_L2()` for Euclidean distance:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
const results = await db(tableNames.referenceDocumentChunks)
|
|
109
|
+
.select('*', db.raw('vec_distance_L2(?, embedding) as distance', [JSON.stringify(queryVector)]))
|
|
110
|
+
.orderBy('distance', 'asc')
|
|
111
|
+
.limit(10);
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Table Names
|
|
115
|
+
|
|
116
|
+
Always use `tableNames` constant for consistency:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
import { tableNames } from '#root/database/database.ts';
|
|
120
|
+
|
|
121
|
+
tableNames.collections // 'collections'
|
|
122
|
+
tableNames.referenceDocuments // 'reference_documents'
|
|
123
|
+
tableNames.referenceDocumentChunks // 'reference_documentchunks'
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Adding Migrations
|
|
127
|
+
|
|
128
|
+
### 1. Create Migration File
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// migrations/migrations.002-add-feature.ts
|
|
132
|
+
import type { Migration } from './migrations.types.ts';
|
|
133
|
+
|
|
134
|
+
const addFeature: Migration = {
|
|
135
|
+
name: 'add-feature',
|
|
136
|
+
up: async (knex) => {
|
|
137
|
+
await knex.schema.alterTable('some_table', (table) => {
|
|
138
|
+
table.string('new_column').nullable();
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
down: async (knex) => {
|
|
142
|
+
await knex.schema.alterTable('some_table', (table) => {
|
|
143
|
+
table.dropColumn('new_column');
|
|
144
|
+
});
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export { addFeature };
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 2. Register Migration
|
|
152
|
+
|
|
153
|
+
In `migrations/migrations.ts`:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { init } from './migrations.001-init.ts';
|
|
157
|
+
import { addFeature } from './migrations.002-add-feature.ts';
|
|
158
|
+
|
|
159
|
+
const migrations: Migration[] = [init, addFeature];
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### 3. Update Table Names (if adding tables)
|
|
163
|
+
|
|
164
|
+
Export new table names from the migration file and re-export from `migrations.ts`.
|
|
165
|
+
|
|
166
|
+
## Key Patterns
|
|
167
|
+
|
|
168
|
+
### Singleton Initialization
|
|
169
|
+
|
|
170
|
+
Database is lazily initialized on first `getInstance()` call:
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
public getInstance = async () => {
|
|
174
|
+
if (!this.#instance) {
|
|
175
|
+
this.#instance = this.#setup();
|
|
176
|
+
}
|
|
177
|
+
return await this.#instance;
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### sqlite-vec Loading
|
|
182
|
+
|
|
183
|
+
The extension is loaded via Knex pool's `afterCreate` hook:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
pool: {
|
|
187
|
+
afterCreate: (conn: Db, done: (err: unknown, conn: Db) => void) => {
|
|
188
|
+
sqliteVec.load(conn);
|
|
189
|
+
done(null, conn);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Vector Storage
|
|
195
|
+
|
|
196
|
+
Embeddings are stored as JSON strings and parsed by sqlite-vec:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Insert
|
|
200
|
+
await db(tableNames.referenceDocumentChunks).insert({
|
|
201
|
+
embedding: JSON.stringify(vectorArray),
|
|
202
|
+
// ...
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Query
|
|
206
|
+
db.raw('vec_distance_L2(?, embedding)', [JSON.stringify(queryVector)])
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Configuration
|
|
210
|
+
|
|
211
|
+
Database path is configured via `database.path` (default: `~/.ai-assist/data.db`).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import knex, { type Knex } from 'knex';
|
|
5
|
+
import type { Db } from 'sqlite-vec';
|
|
6
|
+
|
|
7
|
+
import { migrationSource } from './migrations/migrations.ts';
|
|
8
|
+
|
|
9
|
+
import { config } from '#root/config/config.ts';
|
|
10
|
+
import { destroy } from '#root/utils/utils.services.ts';
|
|
11
|
+
|
|
12
|
+
class DatabaseService {
|
|
13
|
+
#instance?: Promise<Knex>;
|
|
14
|
+
|
|
15
|
+
#setup = async () => {
|
|
16
|
+
const sqliteVec = await import('sqlite-vec');
|
|
17
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
|
+
const dbPath = (config as any).get('database.path') as string;
|
|
19
|
+
|
|
20
|
+
// Ensure directory exists
|
|
21
|
+
const dbDir = dirname(dbPath);
|
|
22
|
+
if (!existsSync(dbDir)) {
|
|
23
|
+
mkdirSync(dbDir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const db = knex({
|
|
27
|
+
client: 'better-sqlite3',
|
|
28
|
+
connection: {
|
|
29
|
+
filename: dbPath,
|
|
30
|
+
},
|
|
31
|
+
useNullAsDefault: true,
|
|
32
|
+
pool: {
|
|
33
|
+
afterCreate: (conn: Db, done: (err: unknown, conn: Db) => void) => {
|
|
34
|
+
sqliteVec.load(conn);
|
|
35
|
+
done(null, conn);
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
await db.migrate.latest({
|
|
41
|
+
migrationSource,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return db;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
public getInstance = async () => {
|
|
48
|
+
if (!this.#instance) {
|
|
49
|
+
this.#instance = this.#setup();
|
|
50
|
+
}
|
|
51
|
+
return await this.#instance;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
[destroy] = async () => {
|
|
55
|
+
if (!this.#instance) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const database = await this.#instance;
|
|
59
|
+
await database.destroy();
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { tableNames } from './migrations/migrations.ts';
|
|
64
|
+
export { DatabaseService };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Migration } from './migrations.types.ts';
|
|
2
|
+
|
|
3
|
+
const tableNames = {
|
|
4
|
+
collections: 'collections',
|
|
5
|
+
referenceDocuments: 'reference_documents',
|
|
6
|
+
referenceDocumentChunks: 'reference_documentchunks',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const init: Migration = {
|
|
10
|
+
name: 'init',
|
|
11
|
+
up: async (knex) => {
|
|
12
|
+
await knex.schema.createTable(tableNames.collections, (table) => {
|
|
13
|
+
table.string('id').primary();
|
|
14
|
+
table.text('url').notNullable();
|
|
15
|
+
|
|
16
|
+
// manifest metadata
|
|
17
|
+
table.string('name').nullable();
|
|
18
|
+
table.string('version').nullable();
|
|
19
|
+
table.text('description').nullable();
|
|
20
|
+
table.string('manifest_hash').nullable();
|
|
21
|
+
|
|
22
|
+
// sync state
|
|
23
|
+
table.string('last_sync_at').nullable();
|
|
24
|
+
|
|
25
|
+
table.string('created_at').notNullable();
|
|
26
|
+
table.string('updated_at').notNullable();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await knex.schema.createTable(tableNames.referenceDocuments, (table) => {
|
|
30
|
+
table.string('collection').notNullable();
|
|
31
|
+
table.string('id').notNullable().index();
|
|
32
|
+
table.string('hash').notNullable().index();
|
|
33
|
+
table.text('content').notNullable();
|
|
34
|
+
|
|
35
|
+
table.primary(['collection', 'id']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
await knex.schema.createTable(tableNames.referenceDocumentChunks, (table) => {
|
|
39
|
+
table.string('id').primary();
|
|
40
|
+
table.string('document').notNullable();
|
|
41
|
+
table.string('collection').notNullable();
|
|
42
|
+
table.text('content').notNullable();
|
|
43
|
+
table.specificType('embedding', 'vector(1024)').notNullable();
|
|
44
|
+
|
|
45
|
+
table.index(['collection']);
|
|
46
|
+
table.index(['collection', 'document']);
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
down: async (knex) => {
|
|
50
|
+
await knex.schema.dropTable(tableNames.referenceDocumentChunks);
|
|
51
|
+
await knex.schema.dropTable(tableNames.referenceDocuments);
|
|
52
|
+
await knex.schema.dropTable(tableNames.collections);
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export { init, tableNames };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Migration } from './migrations.types.ts';
|
|
2
|
+
|
|
3
|
+
const ftsTableNames = {
|
|
4
|
+
referenceDocumentChunksFts: 'reference_documentchunks_fts',
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Migration to add FTS5 virtual table for hybrid search.
|
|
9
|
+
* This enables keyword-based search alongside vector similarity search.
|
|
10
|
+
*/
|
|
11
|
+
const fts5: Migration = {
|
|
12
|
+
name: 'fts5',
|
|
13
|
+
up: async (knex) => {
|
|
14
|
+
// Create FTS5 virtual table for full-text search on chunks
|
|
15
|
+
// We use content="" to create a contentless FTS table (external content)
|
|
16
|
+
// This means we manage the content ourselves and just use FTS for indexing
|
|
17
|
+
await knex.raw(`
|
|
18
|
+
CREATE VIRTUAL TABLE ${ftsTableNames.referenceDocumentChunksFts} USING fts5(
|
|
19
|
+
id,
|
|
20
|
+
collection,
|
|
21
|
+
document,
|
|
22
|
+
content,
|
|
23
|
+
tokenize='porter unicode61'
|
|
24
|
+
)
|
|
25
|
+
`);
|
|
26
|
+
},
|
|
27
|
+
down: async (knex) => {
|
|
28
|
+
await knex.raw(`DROP TABLE IF EXISTS ${ftsTableNames.referenceDocumentChunksFts}`);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export { fts5, ftsTableNames };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Knex } from 'knex';
|
|
2
|
+
|
|
3
|
+
import type { Migration } from './migrations.types.ts';
|
|
4
|
+
import { init, tableNames as initTableNames } from './migrations.001-init.ts';
|
|
5
|
+
import { fts5, ftsTableNames } from './migrations.002-fts5.ts';
|
|
6
|
+
|
|
7
|
+
const migrations: Migration[] = [init, fts5];
|
|
8
|
+
|
|
9
|
+
const migrationSource: Knex.MigrationSource<Migration> = {
|
|
10
|
+
getMigration: async (migration) => migration,
|
|
11
|
+
getMigrationName: (migration) => migration.name,
|
|
12
|
+
getMigrations: async () => migrations,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const tableNames = {
|
|
16
|
+
...initTableNames,
|
|
17
|
+
...ftsTableNames,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export { tableNames, migrationSource };
|