@terronex/aifbin-recall 0.1.0

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 (51) hide show
  1. package/CONTRIBUTING.md +65 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +250 -0
  5. package/dist/cli.d.ts +6 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +182 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/db.d.ts +29 -0
  10. package/dist/db.d.ts.map +1 -0
  11. package/dist/db.js +252 -0
  12. package/dist/db.js.map +1 -0
  13. package/dist/embedder.d.ts +47 -0
  14. package/dist/embedder.d.ts.map +1 -0
  15. package/dist/embedder.js +152 -0
  16. package/dist/embedder.js.map +1 -0
  17. package/dist/index.d.ts +27 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +45 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/indexer.d.ts +34 -0
  22. package/dist/indexer.d.ts.map +1 -0
  23. package/dist/indexer.js +246 -0
  24. package/dist/indexer.js.map +1 -0
  25. package/dist/mcp.d.ts +7 -0
  26. package/dist/mcp.d.ts.map +1 -0
  27. package/dist/mcp.js +207 -0
  28. package/dist/mcp.js.map +1 -0
  29. package/dist/search.d.ts +27 -0
  30. package/dist/search.d.ts.map +1 -0
  31. package/dist/search.js +159 -0
  32. package/dist/search.js.map +1 -0
  33. package/dist/server.d.ts +13 -0
  34. package/dist/server.d.ts.map +1 -0
  35. package/dist/server.js +250 -0
  36. package/dist/server.js.map +1 -0
  37. package/dist/types.d.ts +79 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +20 -0
  40. package/dist/types.js.map +1 -0
  41. package/package.json +64 -0
  42. package/src/cli.ts +195 -0
  43. package/src/db.ts +295 -0
  44. package/src/embedder.ts +175 -0
  45. package/src/index.ts +46 -0
  46. package/src/indexer.ts +272 -0
  47. package/src/mcp.ts +244 -0
  48. package/src/search.ts +201 -0
  49. package/src/server.ts +270 -0
  50. package/src/types.ts +103 -0
  51. package/tsconfig.json +20 -0
package/src/server.ts ADDED
@@ -0,0 +1,270 @@
1
+ /**
2
+ * HTTP server for AIF-BIN Recall
3
+ */
4
+
5
+ import express, { Request, Response, NextFunction } from 'express';
6
+ import cors from 'cors';
7
+ import type { ServerConfig, SearchOptions } from './types.js';
8
+ import { DEFAULT_CONFIG } from './types.js';
9
+ import { EngramDB } from './db.js';
10
+ import { SearchEngine } from './search.js';
11
+ import { Indexer } from './indexer.js';
12
+ import { Embedder, type EmbeddingModelName } from './embedder.js';
13
+
14
+ export interface ServerOptions {
15
+ db: EngramDB;
16
+ config?: Partial<ServerConfig>;
17
+ }
18
+
19
+ export function createServer(options: ServerOptions): express.Application {
20
+ const { db, config } = options;
21
+ const serverConfig = { ...DEFAULT_CONFIG.server, ...config };
22
+
23
+ const app = express();
24
+ const search = new SearchEngine(db);
25
+ const indexer = new Indexer(db);
26
+
27
+ // Middleware
28
+ app.use(cors());
29
+ app.use(express.json({ limit: '10mb' }));
30
+
31
+ // Health check
32
+ app.get('/health', (_req: Request, res: Response) => {
33
+ res.json({ status: 'ok', version: '0.1.0' });
34
+ });
35
+
36
+ // List collections
37
+ app.get('/collections', (_req: Request, res: Response) => {
38
+ try {
39
+ const collections = db.listCollections();
40
+ res.json({ collections });
41
+ } catch (err) {
42
+ res.status(500).json({ error: String(err) });
43
+ }
44
+ });
45
+
46
+ // Get collection
47
+ app.get('/collections/:name', (req: Request, res: Response) => {
48
+ try {
49
+ const collection = db.getCollection(req.params.name);
50
+ if (!collection) {
51
+ res.status(404).json({ error: 'Collection not found' });
52
+ return;
53
+ }
54
+ res.json(collection);
55
+ } catch (err) {
56
+ res.status(500).json({ error: String(err) });
57
+ }
58
+ });
59
+
60
+ // Create collection
61
+ app.post('/collections/:name', (req: Request, res: Response) => {
62
+ try {
63
+ const { description } = req.body || {};
64
+ const existing = db.getCollection(req.params.name);
65
+ if (existing) {
66
+ res.json(existing);
67
+ return;
68
+ }
69
+ const collection = db.createCollection(req.params.name, description);
70
+ res.status(201).json(collection);
71
+ } catch (err) {
72
+ res.status(500).json({ error: String(err) });
73
+ }
74
+ });
75
+
76
+ // Delete collection
77
+ app.delete('/collections/:name', (req: Request, res: Response) => {
78
+ try {
79
+ const deleted = db.deleteCollection(req.params.name);
80
+ if (!deleted) {
81
+ res.status(404).json({ error: 'Collection not found' });
82
+ return;
83
+ }
84
+ res.json({ deleted: true });
85
+ } catch (err) {
86
+ res.status(500).json({ error: String(err) });
87
+ }
88
+ });
89
+
90
+ // Lazy-loaded embedder for text queries
91
+ let embedder: Embedder | null = null;
92
+
93
+ async function getEmbedder(model?: EmbeddingModelName): Promise<Embedder> {
94
+ if (!embedder) {
95
+ embedder = new Embedder(model || 'minilm');
96
+ await embedder.init();
97
+ }
98
+ return embedder;
99
+ }
100
+
101
+ // Search - accepts either embedding array OR text query (will embed automatically)
102
+ app.post('/search', async (req: Request, res: Response) => {
103
+ try {
104
+ const { embedding, query, text, collection, limit, threshold, hybridWeight, model } = req.body;
105
+
106
+ // Use 'query' or 'text' for the search text
107
+ const queryText = query || text;
108
+
109
+ // Get or generate embedding
110
+ let queryEmbedding: number[];
111
+ if (embedding && Array.isArray(embedding)) {
112
+ queryEmbedding = embedding;
113
+ } else if (queryText) {
114
+ // Auto-embed the query text
115
+ const emb = await getEmbedder(model);
116
+ queryEmbedding = await emb.embed(queryText);
117
+ } else {
118
+ res.status(400).json({
119
+ error: 'Either "query" (text) or "embedding" (array) required',
120
+ hint: 'Send { "query": "your search text" } for automatic embedding',
121
+ });
122
+ return;
123
+ }
124
+
125
+ const options: SearchOptions = {
126
+ collection,
127
+ limit: limit || 10,
128
+ threshold: threshold || 0,
129
+ hybridWeight: hybridWeight ?? 0.7,
130
+ };
131
+
132
+ let results;
133
+ if (queryText && (hybridWeight ?? 0.7) < 1.0) {
134
+ results = await search.hybridSearch(queryEmbedding, queryText, options);
135
+ } else {
136
+ results = await search.search(queryEmbedding, options);
137
+ }
138
+
139
+ res.json({
140
+ results: results.map(r => ({
141
+ id: r.chunk.id,
142
+ text: r.chunk.text,
143
+ score: r.score,
144
+ vectorScore: r.vectorScore,
145
+ keywordScore: r.keywordScore,
146
+ sourceFile: r.chunk.sourceFile,
147
+ chunkIndex: r.chunk.chunkIndex,
148
+ metadata: r.chunk.metadata,
149
+ })),
150
+ });
151
+ } catch (err) {
152
+ res.status(500).json({ error: String(err) });
153
+ }
154
+ });
155
+
156
+ // GET search - simple text query
157
+ app.get('/search', async (req: Request, res: Response) => {
158
+ try {
159
+ const { q, query, collection, limit } = req.query;
160
+ const queryText = (q || query) as string;
161
+
162
+ if (!queryText) {
163
+ res.status(400).json({
164
+ error: 'Query parameter "q" or "query" required',
165
+ example: '/search?q=your+search+text&collection=myproject',
166
+ });
167
+ return;
168
+ }
169
+
170
+ // Embed the query
171
+ const emb = await getEmbedder();
172
+ const queryEmbedding = await emb.embed(queryText);
173
+
174
+ const options: SearchOptions = {
175
+ collection: collection as string,
176
+ limit: limit ? parseInt(limit as string, 10) : 10,
177
+ };
178
+
179
+ const results = await search.hybridSearch(queryEmbedding, queryText, options);
180
+
181
+ res.json({
182
+ query: queryText,
183
+ results: results.map(r => ({
184
+ id: r.chunk.id,
185
+ text: r.chunk.text,
186
+ score: r.score,
187
+ vectorScore: r.vectorScore,
188
+ keywordScore: r.keywordScore,
189
+ sourceFile: r.chunk.sourceFile,
190
+ chunkIndex: r.chunk.chunkIndex,
191
+ metadata: r.chunk.metadata,
192
+ })),
193
+ });
194
+ } catch (err) {
195
+ res.status(500).json({ error: String(err) });
196
+ }
197
+ });
198
+
199
+ // Recall specific chunk
200
+ app.get('/recall/:id', (req: Request, res: Response) => {
201
+ try {
202
+ const chunk = search.recall(req.params.id);
203
+ if (!chunk) {
204
+ res.status(404).json({ error: 'Chunk not found' });
205
+ return;
206
+ }
207
+ res.json({
208
+ id: chunk.id,
209
+ text: chunk.text,
210
+ sourceFile: chunk.sourceFile,
211
+ chunkIndex: chunk.chunkIndex,
212
+ metadata: chunk.metadata,
213
+ createdAt: chunk.createdAt,
214
+ updatedAt: chunk.updatedAt,
215
+ });
216
+ } catch (err) {
217
+ res.status(500).json({ error: String(err) });
218
+ }
219
+ });
220
+
221
+ // Index a directory
222
+ app.post('/index', (req: Request, res: Response) => {
223
+ try {
224
+ const { path: dirPath, collection, recursive } = req.body;
225
+
226
+ if (!dirPath || !collection) {
227
+ res.status(400).json({ error: 'path and collection required' });
228
+ return;
229
+ }
230
+
231
+ const result = indexer.indexDirectory(dirPath, {
232
+ collection,
233
+ recursive: recursive !== false,
234
+ });
235
+
236
+ res.json({
237
+ indexed: true,
238
+ files: result.files,
239
+ chunks: result.chunks,
240
+ });
241
+ } catch (err) {
242
+ res.status(500).json({ error: String(err) });
243
+ }
244
+ });
245
+
246
+ // Error handler
247
+ app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
248
+ console.error('Server error:', err);
249
+ res.status(500).json({ error: 'Internal server error' });
250
+ });
251
+
252
+ return app;
253
+ }
254
+
255
+ export function startServer(options: ServerOptions): void {
256
+ const config = { ...DEFAULT_CONFIG.server, ...options.config };
257
+ const app = createServer(options);
258
+
259
+ app.listen(config.port, config.host, () => {
260
+ console.log(`AIF-BIN Recall server running at http://${config.host}:${config.port}`);
261
+ console.log('');
262
+ console.log('Endpoints:');
263
+ console.log(' GET /health - Health check');
264
+ console.log(' GET /collections - List collections');
265
+ console.log(' POST /collections/:n - Create collection');
266
+ console.log(' POST /search - Semantic search');
267
+ console.log(' GET /recall/:id - Retrieve chunk');
268
+ console.log(' POST /index - Index directory');
269
+ });
270
+ }
package/src/types.ts ADDED
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Core types for AIF-BIN Recall memory server
3
+ */
4
+
5
+ export interface MemoryChunk {
6
+ id: string;
7
+ collectionId: string;
8
+ sourceFile: string;
9
+ chunkIndex: number;
10
+ text: string;
11
+ embedding: number[];
12
+ metadata: Record<string, unknown>;
13
+ createdAt: Date;
14
+ updatedAt: Date;
15
+ }
16
+
17
+ export interface Collection {
18
+ id: string;
19
+ name: string;
20
+ description?: string;
21
+ fileCount: number;
22
+ chunkCount: number;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ }
26
+
27
+ export interface SearchResult {
28
+ chunk: MemoryChunk;
29
+ score: number;
30
+ vectorScore: number;
31
+ keywordScore?: number;
32
+ }
33
+
34
+ export interface SearchOptions {
35
+ collection?: string;
36
+ limit?: number;
37
+ threshold?: number;
38
+ hybridWeight?: number; // 0 = keywords only, 1 = vectors only
39
+ filters?: Record<string, unknown>;
40
+ }
41
+
42
+ export interface IndexOptions {
43
+ collection: string;
44
+ recursive?: boolean;
45
+ watch?: boolean;
46
+ }
47
+
48
+ export interface ServerConfig {
49
+ port: number;
50
+ host: string;
51
+ }
52
+
53
+ export interface IndexConfig {
54
+ path: string;
55
+ }
56
+
57
+ export interface SearchConfig {
58
+ defaultLimit: number;
59
+ hybridWeight: number;
60
+ }
61
+
62
+ export interface EngramConfig {
63
+ server: ServerConfig;
64
+ index: IndexConfig;
65
+ search: SearchConfig;
66
+ }
67
+
68
+ export const DEFAULT_CONFIG: EngramConfig = {
69
+ server: {
70
+ port: 3847,
71
+ host: 'localhost',
72
+ },
73
+ index: {
74
+ path: '~/.aifbin-recall/index.db',
75
+ },
76
+ search: {
77
+ defaultLimit: 10,
78
+ hybridWeight: 0.7,
79
+ },
80
+ };
81
+
82
+ export interface AifBinHeader {
83
+ magic: Uint8Array;
84
+ version: number;
85
+ flags: number;
86
+ chunkCount: number;
87
+ embeddingDim: number;
88
+ createdAt: number;
89
+ modifiedAt: number;
90
+ }
91
+
92
+ export interface AifBinChunk {
93
+ id: string;
94
+ text: string;
95
+ embedding: number[];
96
+ metadata: Record<string, unknown>;
97
+ }
98
+
99
+ export interface AifBinFile {
100
+ header: AifBinHeader;
101
+ chunks: AifBinChunk[];
102
+ sourcePath: string;
103
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
20
+ }