@sylphx/flow 1.1.1 → 1.3.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.
- package/CHANGELOG.md +34 -0
- package/package.json +1 -1
- package/src/commands/flow-command.ts +28 -0
- package/src/commands/hook-command.ts +10 -230
- package/src/composables/index.ts +0 -1
- package/src/config/servers.ts +35 -78
- package/src/core/interfaces.ts +0 -33
- package/src/domains/index.ts +0 -2
- package/src/index.ts +0 -4
- package/src/services/mcp-service.ts +0 -16
- package/src/targets/claude-code.ts +3 -9
- package/src/targets/functional/claude-code-logic.ts +4 -22
- package/src/targets/opencode.ts +0 -6
- package/src/types/mcp.types.ts +29 -38
- package/src/types/target.types.ts +0 -2
- package/src/types.ts +0 -1
- package/src/utils/sync-utils.ts +106 -0
- package/src/commands/codebase-command.ts +0 -168
- package/src/commands/knowledge-command.ts +0 -161
- package/src/composables/useTargetConfig.ts +0 -45
- package/src/core/formatting/bytes.test.ts +0 -115
- package/src/core/validation/limit.test.ts +0 -155
- package/src/core/validation/query.test.ts +0 -44
- package/src/domains/codebase/index.ts +0 -5
- package/src/domains/codebase/tools.ts +0 -139
- package/src/domains/knowledge/index.ts +0 -10
- package/src/domains/knowledge/resources.ts +0 -537
- package/src/domains/knowledge/tools.ts +0 -174
- package/src/services/search/base-indexer.ts +0 -156
- package/src/services/search/codebase-indexer-types.ts +0 -38
- package/src/services/search/codebase-indexer.ts +0 -647
- package/src/services/search/embeddings-provider.ts +0 -455
- package/src/services/search/embeddings.ts +0 -316
- package/src/services/search/functional-indexer.ts +0 -323
- package/src/services/search/index.ts +0 -27
- package/src/services/search/indexer.ts +0 -380
- package/src/services/search/knowledge-indexer.ts +0 -422
- package/src/services/search/semantic-search.ts +0 -244
- package/src/services/search/tfidf.ts +0 -559
- package/src/services/search/unified-search-service.ts +0 -888
- package/src/services/storage/cache-storage.ts +0 -487
- package/src/services/storage/drizzle-storage.ts +0 -581
- package/src/services/storage/index.ts +0 -15
- package/src/services/storage/lancedb-vector-storage.ts +0 -494
- package/src/services/storage/memory-storage.ts +0 -268
- package/src/services/storage/separated-storage.ts +0 -467
- package/src/services/storage/vector-storage.ts +0 -13
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Embedding generation utilities
|
|
3
|
-
* Supports OpenAI embeddings (with fallback to mock embeddings)
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { secretUtils } from '../../utils/secret-utils.js';
|
|
7
|
-
import { envSecurity, securitySchemas } from '../../utils/security.js';
|
|
8
|
-
import { generateMockEmbedding } from '../storage/vector-storage.js';
|
|
9
|
-
import { createLogger } from '../../utils/debug-logger.js';
|
|
10
|
-
|
|
11
|
-
const log = createLogger('search:embeddings');
|
|
12
|
-
|
|
13
|
-
export interface ModelInfo {
|
|
14
|
-
id: string;
|
|
15
|
-
object: string;
|
|
16
|
-
created?: number;
|
|
17
|
-
owned_by?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface EmbeddingModelOption {
|
|
21
|
-
id: string;
|
|
22
|
-
description: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface EmbeddingProvider {
|
|
26
|
-
name: string;
|
|
27
|
-
dimensions: number;
|
|
28
|
-
generateEmbedding(text: string): Promise<number[]>;
|
|
29
|
-
generateEmbeddings(texts: string[]): Promise<number[][]>;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* OpenAI Embedding Provider
|
|
34
|
-
* Requires OPENAI_API_KEY environment variable
|
|
35
|
-
*/
|
|
36
|
-
export class OpenAIEmbeddingProvider implements EmbeddingProvider {
|
|
37
|
-
name = 'openai';
|
|
38
|
-
dimensions = 1536; // text-embedding-3-small
|
|
39
|
-
private apiKey: string;
|
|
40
|
-
private model: string;
|
|
41
|
-
private baseURL: string;
|
|
42
|
-
|
|
43
|
-
constructor(
|
|
44
|
-
options: {
|
|
45
|
-
apiKey?: string;
|
|
46
|
-
model?: 'text-embedding-3-small' | 'text-embedding-3-large' | 'text-embedding-ada-002';
|
|
47
|
-
baseURL?: string;
|
|
48
|
-
} = {}
|
|
49
|
-
) {
|
|
50
|
-
// Validate and get API key with security checks
|
|
51
|
-
this.apiKey = options.apiKey || envSecurity.getEnvVar('OPENAI_API_KEY') || '';
|
|
52
|
-
this.model = options.model || 'text-embedding-3-small';
|
|
53
|
-
|
|
54
|
-
// Validate base URL
|
|
55
|
-
const providedBaseURL =
|
|
56
|
-
options.baseURL || envSecurity.getEnvVar('OPENAI_BASE_URL', 'https://api.openai.com/v1');
|
|
57
|
-
if (providedBaseURL) {
|
|
58
|
-
try {
|
|
59
|
-
this.baseURL = securitySchemas.url.parse(providedBaseURL);
|
|
60
|
-
} catch (_error) {
|
|
61
|
-
log('Invalid OPENAI_BASE_URL format, using default');
|
|
62
|
-
this.baseURL = 'https://api.openai.com/v1';
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
this.baseURL = 'https://api.openai.com/v1';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Set dimensions based on model
|
|
69
|
-
if (this.model === 'text-embedding-3-large') {
|
|
70
|
-
this.dimensions = 3072;
|
|
71
|
-
} else if (this.model === 'text-embedding-ada-002') {
|
|
72
|
-
this.dimensions = 1536;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (this.apiKey) {
|
|
76
|
-
// Validate API key format
|
|
77
|
-
try {
|
|
78
|
-
securitySchemas.apiKey.parse(this.apiKey);
|
|
79
|
-
} catch (_error) {
|
|
80
|
-
log('Invalid OPENAI_API_KEY format, using mock implementation');
|
|
81
|
-
this.apiKey = '';
|
|
82
|
-
}
|
|
83
|
-
} else {
|
|
84
|
-
log('OPENAI_API_KEY not set, using mock implementation');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async generateEmbedding(text: string): Promise<number[]> {
|
|
89
|
-
if (!this.apiKey) {
|
|
90
|
-
log('Using mock embedding (no API key)');
|
|
91
|
-
return generateMockEmbedding(text, this.dimensions);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const response = await fetch(`${this.baseURL}/embeddings`, {
|
|
96
|
-
method: 'POST',
|
|
97
|
-
headers: {
|
|
98
|
-
'Content-Type': 'application/json',
|
|
99
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
100
|
-
},
|
|
101
|
-
body: JSON.stringify({
|
|
102
|
-
model: this.model,
|
|
103
|
-
input: text,
|
|
104
|
-
}),
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (!response.ok) {
|
|
108
|
-
const error = await response.text();
|
|
109
|
-
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const data = await response.json();
|
|
113
|
-
return data.data[0].embedding;
|
|
114
|
-
} catch (error) {
|
|
115
|
-
log('Failed to generate OpenAI embedding:', error instanceof Error ? error.message : String(error));
|
|
116
|
-
log('Falling back to mock embedding');
|
|
117
|
-
return generateMockEmbedding(text, this.dimensions);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
async generateEmbeddings(texts: string[]): Promise<number[][]> {
|
|
122
|
-
if (!this.apiKey) {
|
|
123
|
-
log('Using mock embeddings (no API key)');
|
|
124
|
-
return texts.map((text) => generateMockEmbedding(text, this.dimensions));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
|
-
const response = await fetch(`${this.baseURL}/embeddings`, {
|
|
129
|
-
method: 'POST',
|
|
130
|
-
headers: {
|
|
131
|
-
'Content-Type': 'application/json',
|
|
132
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
133
|
-
},
|
|
134
|
-
body: JSON.stringify({
|
|
135
|
-
model: this.model,
|
|
136
|
-
input: texts,
|
|
137
|
-
}),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
if (!response.ok) {
|
|
141
|
-
const error = await response.text();
|
|
142
|
-
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const data = await response.json();
|
|
146
|
-
return data.data.map((item: { embedding: number[] }) => item.embedding);
|
|
147
|
-
} catch (error) {
|
|
148
|
-
log('Failed to generate OpenAI embeddings:', error instanceof Error ? error.message : String(error));
|
|
149
|
-
log('Falling back to mock embeddings');
|
|
150
|
-
return texts.map((text) => generateMockEmbedding(text, this.dimensions));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* List all available models from the OpenAI-compatible API
|
|
156
|
-
*/
|
|
157
|
-
async listModels(): Promise<ModelInfo[]> {
|
|
158
|
-
if (!this.apiKey) {
|
|
159
|
-
throw new Error('API key required to list models');
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
try {
|
|
163
|
-
const response = await fetch(`${this.baseURL}/models`, {
|
|
164
|
-
method: 'GET',
|
|
165
|
-
headers: {
|
|
166
|
-
'Content-Type': 'application/json',
|
|
167
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
168
|
-
},
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
const error = await response.text();
|
|
173
|
-
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const data = await response.json();
|
|
177
|
-
return data.data.map((model: any) => ({
|
|
178
|
-
id: model.id,
|
|
179
|
-
object: model.object,
|
|
180
|
-
created: model.created,
|
|
181
|
-
owned_by: model.owned_by,
|
|
182
|
-
}));
|
|
183
|
-
} catch (error) {
|
|
184
|
-
if (error instanceof Error) {
|
|
185
|
-
throw new Error(`Failed to list models: ${error.message}`);
|
|
186
|
-
}
|
|
187
|
-
throw new Error('Failed to list models: Unknown error');
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Filter models to only include embedding models
|
|
193
|
-
*/
|
|
194
|
-
async listEmbeddingModels(): Promise<ModelInfo[]> {
|
|
195
|
-
const allModels = await this.listModels();
|
|
196
|
-
return allModels.filter(
|
|
197
|
-
(model) => model.id.includes('embedding') || model.id.includes('text-embedding')
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Test if the API connection is working
|
|
203
|
-
*/
|
|
204
|
-
async testConnection(): Promise<boolean> {
|
|
205
|
-
try {
|
|
206
|
-
await this.listModels();
|
|
207
|
-
return true;
|
|
208
|
-
} catch (_error) {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Get available embedding models with their details
|
|
215
|
-
*/
|
|
216
|
-
async getEmbeddingModelOptions(): Promise<EmbeddingModelOption[]> {
|
|
217
|
-
const embeddingModels = await this.listEmbeddingModels();
|
|
218
|
-
|
|
219
|
-
return embeddingModels.map((model) => ({
|
|
220
|
-
id: model.id,
|
|
221
|
-
description: this.getModelDescription(model.id),
|
|
222
|
-
}));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
private getModelDescription(modelId: string): string {
|
|
226
|
-
const descriptions: Record<string, string> = {
|
|
227
|
-
'text-embedding-3-small': 'Latest small embedding model (1536 dimensions)',
|
|
228
|
-
'text-embedding-3-large': 'Latest large embedding model (3072 dimensions)',
|
|
229
|
-
'text-embedding-ada-002': 'Legacy embedding model (1536 dimensions)',
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
return descriptions[modelId] || `Embedding model: ${modelId}`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Mock Embedding Provider (for testing without API key)
|
|
238
|
-
*/
|
|
239
|
-
export class MockEmbeddingProvider implements EmbeddingProvider {
|
|
240
|
-
name = 'mock';
|
|
241
|
-
dimensions: number;
|
|
242
|
-
|
|
243
|
-
constructor(dimensions = 1536) {
|
|
244
|
-
this.dimensions = dimensions;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async generateEmbedding(text: string): Promise<number[]> {
|
|
248
|
-
return generateMockEmbedding(text, this.dimensions);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async generateEmbeddings(texts: string[]): Promise<number[][]> {
|
|
252
|
-
return texts.map((text) => generateMockEmbedding(text, this.dimensions));
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Get default embedding provider
|
|
258
|
-
* Uses OpenAI if API key is available, otherwise mock
|
|
259
|
-
*/
|
|
260
|
-
export async function getDefaultEmbeddingProvider(): Promise<EmbeddingProvider> {
|
|
261
|
-
// Try to load from secrets first
|
|
262
|
-
let secrets: Record<string, string> = {};
|
|
263
|
-
try {
|
|
264
|
-
secrets = await secretUtils.loadSecrets(process.cwd()).catch(() => ({}));
|
|
265
|
-
} catch (_error) {
|
|
266
|
-
// Ignore if secretUtils is not available
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const apiKey = secrets.OPENAI_API_KEY || envSecurity.getEnvVar('OPENAI_API_KEY');
|
|
270
|
-
const baseURL =
|
|
271
|
-
secrets.OPENAI_BASE_URL ||
|
|
272
|
-
envSecurity.getEnvVar('OPENAI_BASE_URL', 'https://api.openai.com/v1');
|
|
273
|
-
const model = secrets.EMBEDDING_MODEL || envSecurity.getEnvVar('EMBEDDING_MODEL');
|
|
274
|
-
|
|
275
|
-
if (apiKey) {
|
|
276
|
-
return new OpenAIEmbeddingProvider({
|
|
277
|
-
apiKey,
|
|
278
|
-
baseURL,
|
|
279
|
-
model: model as any,
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Return mock embeddings silently
|
|
284
|
-
return new MockEmbeddingProvider();
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Chunk text into smaller pieces for embedding
|
|
289
|
-
* Useful for long documents
|
|
290
|
-
*/
|
|
291
|
-
export function chunkText(
|
|
292
|
-
text: string,
|
|
293
|
-
options: {
|
|
294
|
-
maxChunkSize?: number; // Max characters per chunk
|
|
295
|
-
overlap?: number; // Overlap between chunks
|
|
296
|
-
} = {}
|
|
297
|
-
): string[] {
|
|
298
|
-
const { maxChunkSize = 1000, overlap = 100 } = options;
|
|
299
|
-
|
|
300
|
-
const chunks: string[] = [];
|
|
301
|
-
let start = 0;
|
|
302
|
-
|
|
303
|
-
while (start < text.length) {
|
|
304
|
-
const end = Math.min(start + maxChunkSize, text.length);
|
|
305
|
-
const chunk = text.slice(start, end);
|
|
306
|
-
chunks.push(chunk);
|
|
307
|
-
|
|
308
|
-
// Move start position with overlap
|
|
309
|
-
start = end - overlap;
|
|
310
|
-
if (start >= text.length) {
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return chunks;
|
|
316
|
-
}
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Functional Indexer
|
|
3
|
-
* Pure functional implementation using composition instead of inheritance
|
|
4
|
-
*
|
|
5
|
-
* Replaces BaseIndexer class with factory function and closures
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { SearchIndex } from './tfidf.js';
|
|
9
|
-
import { createLogger } from '../../utils/debug-logger.js';
|
|
10
|
-
|
|
11
|
-
const log = createLogger('search:indexing');
|
|
12
|
-
|
|
13
|
-
// ============================================================================
|
|
14
|
-
// TYPES
|
|
15
|
-
// ============================================================================
|
|
16
|
-
|
|
17
|
-
export interface IndexingStatus {
|
|
18
|
-
isIndexing: boolean;
|
|
19
|
-
progress: number; // 0-100
|
|
20
|
-
totalItems: number;
|
|
21
|
-
indexedItems: number;
|
|
22
|
-
startTime: number;
|
|
23
|
-
error?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface IndexerConfig {
|
|
27
|
-
name: string; // 'knowledge' or 'codebase'
|
|
28
|
-
buildIndex: () => Promise<SearchIndex>; // Injected build function
|
|
29
|
-
autoStart?: boolean; // Auto-start indexing (default: true)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface IndexerState {
|
|
33
|
-
readonly cachedIndex: SearchIndex | null;
|
|
34
|
-
readonly indexingPromise: Promise<SearchIndex> | null;
|
|
35
|
-
readonly status: IndexingStatus;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface Indexer {
|
|
39
|
-
readonly getStatus: () => IndexingStatus;
|
|
40
|
-
readonly isReady: () => boolean;
|
|
41
|
-
readonly loadIndex: () => Promise<SearchIndex>;
|
|
42
|
-
readonly clearCache: () => void;
|
|
43
|
-
readonly getStats: () => Promise<{
|
|
44
|
-
totalDocuments: number;
|
|
45
|
-
uniqueTerms: number;
|
|
46
|
-
generatedAt: string;
|
|
47
|
-
version: string;
|
|
48
|
-
} | null>;
|
|
49
|
-
readonly startBackgroundIndexing: () => void;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ============================================================================
|
|
53
|
-
// STATE MANAGEMENT (Pure Functions)
|
|
54
|
-
// ============================================================================
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Create initial indexer state
|
|
58
|
-
*/
|
|
59
|
-
const createInitialState = (): IndexerState => ({
|
|
60
|
-
cachedIndex: null,
|
|
61
|
-
indexingPromise: null,
|
|
62
|
-
status: {
|
|
63
|
-
isIndexing: false,
|
|
64
|
-
progress: 0,
|
|
65
|
-
totalItems: 0,
|
|
66
|
-
indexedItems: 0,
|
|
67
|
-
startTime: 0,
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Update status to indexing
|
|
73
|
-
*/
|
|
74
|
-
const setIndexing = (state: IndexerState): IndexerState => ({
|
|
75
|
-
...state,
|
|
76
|
-
status: {
|
|
77
|
-
...state.status,
|
|
78
|
-
isIndexing: true,
|
|
79
|
-
progress: 0,
|
|
80
|
-
startTime: Date.now(),
|
|
81
|
-
error: undefined,
|
|
82
|
-
},
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Update with completed index
|
|
87
|
-
*/
|
|
88
|
-
const setCompleted = (state: IndexerState, index: SearchIndex): IndexerState => ({
|
|
89
|
-
...state,
|
|
90
|
-
cachedIndex: index,
|
|
91
|
-
indexingPromise: null,
|
|
92
|
-
status: {
|
|
93
|
-
...state.status,
|
|
94
|
-
isIndexing: false,
|
|
95
|
-
progress: 100,
|
|
96
|
-
totalItems: index.totalDocuments,
|
|
97
|
-
indexedItems: index.totalDocuments,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Update with error
|
|
103
|
-
*/
|
|
104
|
-
const setError = (state: IndexerState, error: Error): IndexerState => ({
|
|
105
|
-
...state,
|
|
106
|
-
indexingPromise: null,
|
|
107
|
-
status: {
|
|
108
|
-
...state.status,
|
|
109
|
-
isIndexing: false,
|
|
110
|
-
error: error.message,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Set indexing promise
|
|
116
|
-
*/
|
|
117
|
-
const setIndexingPromise = (state: IndexerState, promise: Promise<SearchIndex>): IndexerState => ({
|
|
118
|
-
...state,
|
|
119
|
-
indexingPromise: promise,
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
// ============================================================================
|
|
123
|
-
// INDEXER FACTORY (Composition over Inheritance)
|
|
124
|
-
// ============================================================================
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Create indexer with closure-based state management
|
|
128
|
-
* Replaces BaseIndexer class with pure functional approach
|
|
129
|
-
*
|
|
130
|
-
* @example
|
|
131
|
-
* const knowledgeIndexer = createIndexer({
|
|
132
|
-
* name: 'knowledge',
|
|
133
|
-
* buildIndex: async () => {
|
|
134
|
-
* // Knowledge-specific indexing logic
|
|
135
|
-
* return searchIndex;
|
|
136
|
-
* }
|
|
137
|
-
* });
|
|
138
|
-
*
|
|
139
|
-
* const status = knowledgeIndexer.getStatus();
|
|
140
|
-
* const index = await knowledgeIndexer.loadIndex();
|
|
141
|
-
*/
|
|
142
|
-
export const createIndexer = (config: IndexerConfig): Indexer => {
|
|
143
|
-
// State managed in closure (not mutable class fields)
|
|
144
|
-
let state = createInitialState();
|
|
145
|
-
|
|
146
|
-
// Auto-start indexing (default behavior)
|
|
147
|
-
const shouldAutoStart = config.autoStart !== false;
|
|
148
|
-
if (shouldAutoStart) {
|
|
149
|
-
setTimeout(() => indexer.startBackgroundIndexing(), 0);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ========================================================================
|
|
153
|
-
// PUBLIC API
|
|
154
|
-
// ========================================================================
|
|
155
|
-
|
|
156
|
-
const indexer: Indexer = {
|
|
157
|
-
/**
|
|
158
|
-
* Get current indexing status (immutable copy)
|
|
159
|
-
*/
|
|
160
|
-
getStatus: () => ({ ...state.status }),
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Check if index is ready
|
|
164
|
-
*/
|
|
165
|
-
isReady: () => state.cachedIndex !== null && !state.status.isIndexing,
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Load or build index (with caching and deduplication)
|
|
169
|
-
*/
|
|
170
|
-
loadIndex: async () => {
|
|
171
|
-
// Return cached index if available
|
|
172
|
-
if (state.cachedIndex) {
|
|
173
|
-
return state.cachedIndex;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// If already indexing, wait for existing promise
|
|
177
|
-
if (state.indexingPromise) {
|
|
178
|
-
return state.indexingPromise;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Start new indexing
|
|
182
|
-
state = setIndexing(state);
|
|
183
|
-
|
|
184
|
-
const indexingPromise = config
|
|
185
|
-
.buildIndex()
|
|
186
|
-
.then((index) => {
|
|
187
|
-
state = setCompleted(state, index);
|
|
188
|
-
log(`${config.name} indexing complete:`, index.totalDocuments, 'documents');
|
|
189
|
-
return index;
|
|
190
|
-
})
|
|
191
|
-
.catch((error) => {
|
|
192
|
-
state = setError(state, error as Error);
|
|
193
|
-
log(`${config.name} indexing failed:`, error instanceof Error ? error.message : String(error));
|
|
194
|
-
throw error;
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
state = setIndexingPromise(state, indexingPromise);
|
|
198
|
-
return indexingPromise;
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Clear cache and reset state
|
|
203
|
-
*/
|
|
204
|
-
clearCache: () => {
|
|
205
|
-
state = createInitialState();
|
|
206
|
-
},
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Get index statistics
|
|
210
|
-
*/
|
|
211
|
-
getStats: async () => {
|
|
212
|
-
const index = await indexer.loadIndex();
|
|
213
|
-
if (!index) {
|
|
214
|
-
return null;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return {
|
|
218
|
-
totalDocuments: index.totalDocuments,
|
|
219
|
-
uniqueTerms: index.idf.size,
|
|
220
|
-
generatedAt: index.metadata.generatedAt,
|
|
221
|
-
version: index.metadata.version,
|
|
222
|
-
};
|
|
223
|
-
},
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Start background indexing (non-blocking)
|
|
227
|
-
*/
|
|
228
|
-
startBackgroundIndexing: () => {
|
|
229
|
-
if (state.status.isIndexing || state.cachedIndex) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
log(`Starting background ${config.name} indexing`);
|
|
234
|
-
indexer.loadIndex().catch((error) => {
|
|
235
|
-
log(`Background ${config.name} indexing failed:`, error instanceof Error ? error.message : String(error));
|
|
236
|
-
});
|
|
237
|
-
},
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
return Object.freeze(indexer); // Make API immutable
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
// ============================================================================
|
|
244
|
-
// UTILITIES
|
|
245
|
-
// ============================================================================
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* Compose indexer with additional behavior
|
|
249
|
-
* Example of function composition instead of inheritance
|
|
250
|
-
*/
|
|
251
|
-
export const withLogging =
|
|
252
|
-
(baseIndexer: Indexer) =>
|
|
253
|
-
(config: { verbose?: boolean } = {}): Indexer => {
|
|
254
|
-
const { verbose = false } = config;
|
|
255
|
-
|
|
256
|
-
return {
|
|
257
|
-
...baseIndexer,
|
|
258
|
-
loadIndex: async () => {
|
|
259
|
-
if (verbose) {
|
|
260
|
-
log('Loading index...');
|
|
261
|
-
}
|
|
262
|
-
const index = await baseIndexer.loadIndex();
|
|
263
|
-
if (verbose) {
|
|
264
|
-
log('Index loaded:', index.totalDocuments, 'documents');
|
|
265
|
-
}
|
|
266
|
-
return index;
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Compose indexer with custom cache strategy
|
|
273
|
-
*/
|
|
274
|
-
export const withCacheTTL =
|
|
275
|
-
(baseIndexer: Indexer) =>
|
|
276
|
-
(ttlMs: number): Indexer => {
|
|
277
|
-
let lastIndexTime = 0;
|
|
278
|
-
|
|
279
|
-
return {
|
|
280
|
-
...baseIndexer,
|
|
281
|
-
loadIndex: async () => {
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
const isExpired = now - lastIndexTime > ttlMs;
|
|
284
|
-
|
|
285
|
-
if (isExpired) {
|
|
286
|
-
baseIndexer.clearCache();
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const index = await baseIndexer.loadIndex();
|
|
290
|
-
lastIndexTime = now;
|
|
291
|
-
return index;
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Compose indexer with retry logic
|
|
298
|
-
*/
|
|
299
|
-
export const withRetry =
|
|
300
|
-
(baseIndexer: Indexer) =>
|
|
301
|
-
(maxRetries = 3, delayMs = 1000): Indexer => {
|
|
302
|
-
return {
|
|
303
|
-
...baseIndexer,
|
|
304
|
-
loadIndex: async () => {
|
|
305
|
-
let lastError: Error | null = null;
|
|
306
|
-
|
|
307
|
-
for (let i = 0; i < maxRetries; i++) {
|
|
308
|
-
try {
|
|
309
|
-
return await baseIndexer.loadIndex();
|
|
310
|
-
} catch (error) {
|
|
311
|
-
lastError = error as Error;
|
|
312
|
-
log(`Indexing attempt ${i + 1}/${maxRetries} failed:`, error instanceof Error ? error.message : String(error));
|
|
313
|
-
|
|
314
|
-
if (i < maxRetries - 1) {
|
|
315
|
-
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
throw lastError || new Error('Indexing failed after retries');
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Search Services Index
|
|
3
|
-
* Provides unified interface for all search-related services
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export { CodebaseIndexer } from './codebase-indexer.js';
|
|
7
|
-
// Types
|
|
8
|
-
export type {
|
|
9
|
-
CodebaseFile,
|
|
10
|
-
CodebaseIndexerOptions,
|
|
11
|
-
IndexCache,
|
|
12
|
-
IndexingStatus,
|
|
13
|
-
} from './codebase-indexer-types.js';
|
|
14
|
-
export type { EmbeddingProvider } from './embeddings.js';
|
|
15
|
-
|
|
16
|
-
// Embeddings
|
|
17
|
-
export { createEmbeddingProvider, getDefaultEmbeddingProvider } from './embeddings.js';
|
|
18
|
-
export { SemanticSearchService } from './semantic-search.js';
|
|
19
|
-
export type { SearchIndex, SearchResult as TFIDFSearchResult } from './tfidf.js';
|
|
20
|
-
// TF-IDF
|
|
21
|
-
export { buildSearchIndex, searchDocuments } from './tfidf.js';
|
|
22
|
-
export type {
|
|
23
|
-
SearchOptions,
|
|
24
|
-
SearchResult,
|
|
25
|
-
} from './unified-search-service.js';
|
|
26
|
-
// Main services
|
|
27
|
-
export { getSearchService } from './unified-search-service.js';
|