@sylphx/flow 1.1.0 → 1.2.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 (45) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/package.json +12 -2
  3. package/src/commands/hook-command.ts +10 -230
  4. package/src/composables/index.ts +0 -1
  5. package/src/config/servers.ts +35 -78
  6. package/src/core/interfaces.ts +0 -33
  7. package/src/domains/index.ts +0 -2
  8. package/src/index.ts +0 -4
  9. package/src/services/mcp-service.ts +0 -16
  10. package/src/targets/claude-code.ts +3 -9
  11. package/src/targets/functional/claude-code-logic.ts +4 -22
  12. package/src/targets/opencode.ts +0 -6
  13. package/src/types/mcp.types.ts +29 -38
  14. package/src/types/target.types.ts +0 -2
  15. package/src/types.ts +0 -1
  16. package/src/commands/codebase-command.ts +0 -168
  17. package/src/commands/knowledge-command.ts +0 -161
  18. package/src/composables/useTargetConfig.ts +0 -45
  19. package/src/core/formatting/bytes.test.ts +0 -115
  20. package/src/core/validation/limit.test.ts +0 -155
  21. package/src/core/validation/query.test.ts +0 -44
  22. package/src/domains/codebase/index.ts +0 -5
  23. package/src/domains/codebase/tools.ts +0 -139
  24. package/src/domains/knowledge/index.ts +0 -10
  25. package/src/domains/knowledge/resources.ts +0 -537
  26. package/src/domains/knowledge/tools.ts +0 -174
  27. package/src/services/search/base-indexer.ts +0 -156
  28. package/src/services/search/codebase-indexer-types.ts +0 -38
  29. package/src/services/search/codebase-indexer.ts +0 -647
  30. package/src/services/search/embeddings-provider.ts +0 -455
  31. package/src/services/search/embeddings.ts +0 -316
  32. package/src/services/search/functional-indexer.ts +0 -323
  33. package/src/services/search/index.ts +0 -27
  34. package/src/services/search/indexer.ts +0 -380
  35. package/src/services/search/knowledge-indexer.ts +0 -422
  36. package/src/services/search/semantic-search.ts +0 -244
  37. package/src/services/search/tfidf.ts +0 -559
  38. package/src/services/search/unified-search-service.ts +0 -888
  39. package/src/services/storage/cache-storage.ts +0 -487
  40. package/src/services/storage/drizzle-storage.ts +0 -581
  41. package/src/services/storage/index.ts +0 -15
  42. package/src/services/storage/lancedb-vector-storage.ts +0 -494
  43. package/src/services/storage/memory-storage.ts +0 -268
  44. package/src/services/storage/separated-storage.ts +0 -467
  45. 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';