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.
Files changed (61) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +282 -0
  3. package/bin/cli.js +8 -0
  4. package/bin/daemon.js +7 -0
  5. package/package.json +70 -0
  6. package/src/agent/AGENTS.md +249 -0
  7. package/src/agent/agent.prompts.ts +66 -0
  8. package/src/agent/agent.test-runner.schemas.ts +158 -0
  9. package/src/agent/agent.test-runner.ts +436 -0
  10. package/src/agent/agent.ts +371 -0
  11. package/src/agent/agent.types.ts +94 -0
  12. package/src/backend/AGENTS.md +112 -0
  13. package/src/backend/backend.protocol.ts +95 -0
  14. package/src/backend/backend.schemas.ts +123 -0
  15. package/src/backend/backend.services.ts +151 -0
  16. package/src/backend/backend.ts +111 -0
  17. package/src/backend/backend.types.ts +34 -0
  18. package/src/cli/AGENTS.md +213 -0
  19. package/src/cli/cli.agent.ts +197 -0
  20. package/src/cli/cli.chat.ts +369 -0
  21. package/src/cli/cli.client.ts +55 -0
  22. package/src/cli/cli.collections.ts +491 -0
  23. package/src/cli/cli.config.ts +252 -0
  24. package/src/cli/cli.daemon.ts +160 -0
  25. package/src/cli/cli.documents.ts +413 -0
  26. package/src/cli/cli.mcp.ts +177 -0
  27. package/src/cli/cli.ts +28 -0
  28. package/src/cli/cli.utils.ts +122 -0
  29. package/src/client/AGENTS.md +135 -0
  30. package/src/client/client.adapters.ts +279 -0
  31. package/src/client/client.ts +86 -0
  32. package/src/client/client.types.ts +17 -0
  33. package/src/collections/AGENTS.md +185 -0
  34. package/src/collections/collections.schemas.ts +195 -0
  35. package/src/collections/collections.ts +1160 -0
  36. package/src/config/config.ts +118 -0
  37. package/src/daemon/AGENTS.md +168 -0
  38. package/src/daemon/daemon.config.ts +23 -0
  39. package/src/daemon/daemon.manager.ts +215 -0
  40. package/src/daemon/daemon.schemas.ts +22 -0
  41. package/src/daemon/daemon.ts +205 -0
  42. package/src/database/AGENTS.md +211 -0
  43. package/src/database/database.ts +64 -0
  44. package/src/database/migrations/migrations.001-init.ts +56 -0
  45. package/src/database/migrations/migrations.002-fts5.ts +32 -0
  46. package/src/database/migrations/migrations.ts +20 -0
  47. package/src/database/migrations/migrations.types.ts +9 -0
  48. package/src/documents/AGENTS.md +301 -0
  49. package/src/documents/documents.schemas.ts +190 -0
  50. package/src/documents/documents.ts +734 -0
  51. package/src/embedder/embedder.ts +53 -0
  52. package/src/exports.ts +0 -0
  53. package/src/mcp/AGENTS.md +264 -0
  54. package/src/mcp/mcp.ts +105 -0
  55. package/src/tools/AGENTS.md +228 -0
  56. package/src/tools/agent/agent.ts +45 -0
  57. package/src/tools/documents/documents.ts +401 -0
  58. package/src/tools/tools.langchain.ts +37 -0
  59. package/src/tools/tools.mcp.ts +46 -0
  60. package/src/tools/tools.types.ts +35 -0
  61. 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 };
@@ -0,0 +1,9 @@
1
+ import type { Knex } from 'knex';
2
+
3
+ type Migration = {
4
+ name: string;
5
+ up: (knex: Knex) => Promise<void>;
6
+ down: (knex: Knex) => Promise<void>;
7
+ };
8
+
9
+ export type { Migration };