@twelvehart/supermemory-runtime 1.0.0-next.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 (156) hide show
  1. package/.env.example +57 -0
  2. package/README.md +374 -0
  3. package/dist/index.js +189 -0
  4. package/dist/mcp/index.js +1132 -0
  5. package/docker-compose.prod.yml +91 -0
  6. package/docker-compose.yml +358 -0
  7. package/drizzle/0000_dapper_the_professor.sql +159 -0
  8. package/drizzle/0001_api_keys.sql +51 -0
  9. package/drizzle/meta/0000_snapshot.json +1532 -0
  10. package/drizzle/meta/_journal.json +13 -0
  11. package/drizzle.config.ts +20 -0
  12. package/package.json +114 -0
  13. package/scripts/add-extraction-job.ts +122 -0
  14. package/scripts/benchmark-pgvector.ts +122 -0
  15. package/scripts/bootstrap.sh +209 -0
  16. package/scripts/check-runtime-pack.ts +111 -0
  17. package/scripts/claude-mcp-config.ts +336 -0
  18. package/scripts/docker-entrypoint.sh +183 -0
  19. package/scripts/doctor.ts +377 -0
  20. package/scripts/init-db.sql +33 -0
  21. package/scripts/install.sh +1110 -0
  22. package/scripts/mcp-setup.ts +271 -0
  23. package/scripts/migrations/001_create_pgvector_extension.sql +31 -0
  24. package/scripts/migrations/002_create_memory_embeddings_table.sql +75 -0
  25. package/scripts/migrations/003_create_hnsw_index.sql +94 -0
  26. package/scripts/migrations/004_create_memory_embeddings_standalone.sql +70 -0
  27. package/scripts/migrations/005_create_chunks_table.sql +95 -0
  28. package/scripts/migrations/006_create_processing_queue.sql +45 -0
  29. package/scripts/migrations/generate_test_data.sql +42 -0
  30. package/scripts/migrations/phase1_comprehensive_test.sql +204 -0
  31. package/scripts/migrations/run_migrations.sh +286 -0
  32. package/scripts/migrations/test_hnsw_index.sql +255 -0
  33. package/scripts/pre-commit-secrets +282 -0
  34. package/scripts/run-extraction-worker.ts +46 -0
  35. package/scripts/run-phase1-tests.sh +291 -0
  36. package/scripts/setup.ts +222 -0
  37. package/scripts/smoke-install.sh +12 -0
  38. package/scripts/test-health-endpoint.sh +328 -0
  39. package/src/api/index.ts +2 -0
  40. package/src/api/middleware/auth.ts +80 -0
  41. package/src/api/middleware/csrf.ts +308 -0
  42. package/src/api/middleware/errorHandler.ts +166 -0
  43. package/src/api/middleware/rateLimit.ts +360 -0
  44. package/src/api/middleware/validation.ts +514 -0
  45. package/src/api/routes/documents.ts +286 -0
  46. package/src/api/routes/profiles.ts +237 -0
  47. package/src/api/routes/search.ts +71 -0
  48. package/src/api/stores/index.ts +58 -0
  49. package/src/config/bootstrap-env.ts +3 -0
  50. package/src/config/env.ts +71 -0
  51. package/src/config/feature-flags.ts +25 -0
  52. package/src/config/index.ts +140 -0
  53. package/src/config/secrets.config.ts +291 -0
  54. package/src/db/client.ts +92 -0
  55. package/src/db/index.ts +73 -0
  56. package/src/db/postgres.ts +72 -0
  57. package/src/db/schema/chunks.schema.ts +31 -0
  58. package/src/db/schema/containers.schema.ts +46 -0
  59. package/src/db/schema/documents.schema.ts +49 -0
  60. package/src/db/schema/embeddings.schema.ts +32 -0
  61. package/src/db/schema/index.ts +11 -0
  62. package/src/db/schema/memories.schema.ts +72 -0
  63. package/src/db/schema/profiles.schema.ts +34 -0
  64. package/src/db/schema/queue.schema.ts +59 -0
  65. package/src/db/schema/relationships.schema.ts +42 -0
  66. package/src/db/schema.ts +223 -0
  67. package/src/db/worker-connection.ts +47 -0
  68. package/src/index.ts +235 -0
  69. package/src/mcp/CLAUDE.md +1 -0
  70. package/src/mcp/index.ts +1380 -0
  71. package/src/mcp/legacyState.ts +22 -0
  72. package/src/mcp/rateLimit.ts +358 -0
  73. package/src/mcp/resources.ts +309 -0
  74. package/src/mcp/results.ts +104 -0
  75. package/src/mcp/tools.ts +401 -0
  76. package/src/queues/config.ts +119 -0
  77. package/src/queues/index.ts +289 -0
  78. package/src/sdk/client.ts +225 -0
  79. package/src/sdk/errors.ts +266 -0
  80. package/src/sdk/http.ts +560 -0
  81. package/src/sdk/index.ts +244 -0
  82. package/src/sdk/resources/base.ts +65 -0
  83. package/src/sdk/resources/connections.ts +204 -0
  84. package/src/sdk/resources/documents.ts +163 -0
  85. package/src/sdk/resources/index.ts +10 -0
  86. package/src/sdk/resources/memories.ts +150 -0
  87. package/src/sdk/resources/search.ts +60 -0
  88. package/src/sdk/resources/settings.ts +36 -0
  89. package/src/sdk/types.ts +674 -0
  90. package/src/services/chunking/index.ts +451 -0
  91. package/src/services/chunking.service.ts +650 -0
  92. package/src/services/csrf.service.ts +252 -0
  93. package/src/services/documents.repository.ts +219 -0
  94. package/src/services/documents.service.ts +191 -0
  95. package/src/services/embedding.service.ts +404 -0
  96. package/src/services/extraction.service.ts +300 -0
  97. package/src/services/extractors/code.extractor.ts +451 -0
  98. package/src/services/extractors/index.ts +9 -0
  99. package/src/services/extractors/markdown.extractor.ts +461 -0
  100. package/src/services/extractors/pdf.extractor.ts +315 -0
  101. package/src/services/extractors/text.extractor.ts +118 -0
  102. package/src/services/extractors/url.extractor.ts +243 -0
  103. package/src/services/index.ts +235 -0
  104. package/src/services/ingestion.service.ts +177 -0
  105. package/src/services/llm/anthropic.ts +400 -0
  106. package/src/services/llm/base.ts +460 -0
  107. package/src/services/llm/contradiction-detector.service.ts +526 -0
  108. package/src/services/llm/heuristics.ts +148 -0
  109. package/src/services/llm/index.ts +309 -0
  110. package/src/services/llm/memory-classifier.service.ts +383 -0
  111. package/src/services/llm/memory-extension-detector.service.ts +523 -0
  112. package/src/services/llm/mock.ts +470 -0
  113. package/src/services/llm/openai.ts +398 -0
  114. package/src/services/llm/prompts.ts +438 -0
  115. package/src/services/llm/types.ts +373 -0
  116. package/src/services/memory.repository.ts +1769 -0
  117. package/src/services/memory.service.ts +1338 -0
  118. package/src/services/memory.types.ts +234 -0
  119. package/src/services/persistence/index.ts +295 -0
  120. package/src/services/pipeline.service.ts +509 -0
  121. package/src/services/profile.repository.ts +436 -0
  122. package/src/services/profile.service.ts +560 -0
  123. package/src/services/profile.types.ts +270 -0
  124. package/src/services/relationships/detector.ts +1128 -0
  125. package/src/services/relationships/index.ts +268 -0
  126. package/src/services/relationships/memory-integration.ts +459 -0
  127. package/src/services/relationships/strategies.ts +132 -0
  128. package/src/services/relationships/types.ts +370 -0
  129. package/src/services/search.service.ts +761 -0
  130. package/src/services/search.types.ts +220 -0
  131. package/src/services/secrets.service.ts +384 -0
  132. package/src/services/vectorstore/base.ts +327 -0
  133. package/src/services/vectorstore/index.ts +444 -0
  134. package/src/services/vectorstore/memory.ts +286 -0
  135. package/src/services/vectorstore/migration.ts +295 -0
  136. package/src/services/vectorstore/mock.ts +403 -0
  137. package/src/services/vectorstore/pgvector.ts +695 -0
  138. package/src/services/vectorstore/types.ts +247 -0
  139. package/src/startup.ts +389 -0
  140. package/src/types/api.types.ts +193 -0
  141. package/src/types/document.types.ts +103 -0
  142. package/src/types/index.ts +241 -0
  143. package/src/types/profile.base.ts +133 -0
  144. package/src/utils/errors.ts +447 -0
  145. package/src/utils/id.ts +15 -0
  146. package/src/utils/index.ts +101 -0
  147. package/src/utils/logger.ts +313 -0
  148. package/src/utils/sanitization.ts +501 -0
  149. package/src/utils/secret-validation.ts +273 -0
  150. package/src/utils/synonyms.ts +188 -0
  151. package/src/utils/validation.ts +581 -0
  152. package/src/workers/chunking.worker.ts +242 -0
  153. package/src/workers/embedding.worker.ts +358 -0
  154. package/src/workers/extraction.worker.ts +346 -0
  155. package/src/workers/indexing.worker.ts +505 -0
  156. package/tsconfig.json +38 -0
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Secret Validation Utilities
3
+ *
4
+ * Provides validation functions for various secret formats and types.
5
+ * Used during startup and secret rotation to ensure secret quality.
6
+ */
7
+
8
+ import { randomBytes } from 'crypto'
9
+ import { ValidationError } from './errors.js'
10
+
11
+ /** API key format patterns for common providers */
12
+ const API_KEY_PATTERNS = {
13
+ generic: /^[a-zA-Z0-9_-]{20,64}$/,
14
+ anthropic: /^sk-ant-[a-zA-Z0-9-_]{95,}$/,
15
+ openai: /^sk-[a-zA-Z0-9]{48,}$/,
16
+ stripe: /^sk_(live|test)_[a-zA-Z0-9]{24,}$/,
17
+ aws: /^AKIA[0-9A-Z]{16}$/,
18
+ google: /^AIza[0-9A-Za-z_-]{35}$/,
19
+ } as const
20
+
21
+ /** Database URL patterns */
22
+ const DATABASE_URL_PATTERNS = {
23
+ postgresql: /^postgresql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)$/,
24
+ mysql: /^mysql:\/\/([^:]+):([^@]+)@([^:]+):(\d+)\/(.+)$/,
25
+ mongodb: /^mongodb(?:\+srv)?:\/\/([^:]+):([^@]+)@([^/]+)\/(.+)$/,
26
+ } as const
27
+
28
+ const JWT_PATTERN = /^eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+$/
29
+
30
+ export interface ApiKeyValidation {
31
+ valid: boolean
32
+ format?: keyof typeof API_KEY_PATTERNS
33
+ error?: string
34
+ }
35
+
36
+ export interface DatabaseUrlComponents {
37
+ type: 'postgresql' | 'mysql' | 'mongodb'
38
+ username: string
39
+ password: string
40
+ host: string
41
+ port: number
42
+ database: string
43
+ }
44
+
45
+ export interface SecretStrength {
46
+ entropy: number
47
+ strength: 'weak' | 'fair' | 'good' | 'strong'
48
+ diversity: {
49
+ hasLowercase: boolean
50
+ hasUppercase: boolean
51
+ hasNumbers: boolean
52
+ hasSymbols: boolean
53
+ uniqueChars: number
54
+ }
55
+ recommendations: string[]
56
+ }
57
+
58
+ /**
59
+ * Validate API key format
60
+ * @param apiKey - API key to validate
61
+ * @param expectedFormat - Optional expected format
62
+ * @returns Validation result
63
+ */
64
+ export function validateApiKey(apiKey: string, expectedFormat?: keyof typeof API_KEY_PATTERNS): ApiKeyValidation {
65
+ if (!apiKey || apiKey.trim().length === 0) {
66
+ return { valid: false, error: 'API key cannot be empty' }
67
+ }
68
+
69
+ // If specific format expected, check only that format
70
+ if (expectedFormat) {
71
+ const pattern = API_KEY_PATTERNS[expectedFormat]
72
+ if (pattern.test(apiKey)) {
73
+ return { valid: true, format: expectedFormat }
74
+ }
75
+ return {
76
+ valid: false,
77
+ error: `API key does not match ${expectedFormat} format`,
78
+ }
79
+ }
80
+
81
+ // Check all known formats
82
+ for (const [format, pattern] of Object.entries(API_KEY_PATTERNS)) {
83
+ if (pattern.test(apiKey)) {
84
+ return {
85
+ valid: true,
86
+ format: format as keyof typeof API_KEY_PATTERNS,
87
+ }
88
+ }
89
+ }
90
+
91
+ // Not matching any known format
92
+ return {
93
+ valid: false,
94
+ error: 'API key format not recognized',
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Validate and parse database URL
100
+ * @param url - Database connection URL
101
+ * @returns Parsed components
102
+ * @throws ValidationError if URL is invalid
103
+ */
104
+ export function validateDatabaseUrl(url: string): DatabaseUrlComponents {
105
+ if (!url || url.trim().length === 0) {
106
+ throw new ValidationError('Database URL cannot be empty')
107
+ }
108
+
109
+ const parsers: Array<{
110
+ type: DatabaseUrlComponents['type']
111
+ pattern: RegExp
112
+ defaultPort?: number
113
+ }> = [
114
+ { type: 'postgresql', pattern: DATABASE_URL_PATTERNS.postgresql },
115
+ { type: 'mysql', pattern: DATABASE_URL_PATTERNS.mysql },
116
+ { type: 'mongodb', pattern: DATABASE_URL_PATTERNS.mongodb, defaultPort: 27017 },
117
+ ]
118
+
119
+ for (const { type, pattern, defaultPort } of parsers) {
120
+ const match = url.match(pattern)
121
+ if (match) {
122
+ return {
123
+ type,
124
+ username: decodeURIComponent(match[1]!),
125
+ password: decodeURIComponent(match[2]!),
126
+ host: match[3]!,
127
+ port: defaultPort ?? parseInt(match[4]!, 10),
128
+ database: type === 'mongodb' ? match[4]! : match[5]!,
129
+ }
130
+ }
131
+ }
132
+
133
+ throw new ValidationError('Invalid database URL format', {
134
+ url: ['URL must be in format: protocol://username:password@host:port/database'],
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Check secret strength based on entropy and character diversity
140
+ * @param secret - Secret to check
141
+ * @returns Strength analysis
142
+ */
143
+ export function checkSecretStrength(secret: string): SecretStrength {
144
+ const entropy = calculateEntropy(secret)
145
+ const diversity = analyzeCharacterDiversity(secret)
146
+ const recommendations: string[] = []
147
+
148
+ let strength: SecretStrength['strength']
149
+ if (entropy < 64) {
150
+ strength = 'weak'
151
+ recommendations.push('Increase length to at least 16 characters')
152
+ } else if (entropy < 96) {
153
+ strength = 'fair'
154
+ recommendations.push('Consider using at least 24 characters for better security')
155
+ } else if (entropy < 128) {
156
+ strength = 'good'
157
+ } else {
158
+ strength = 'strong'
159
+ }
160
+
161
+ if (!diversity.hasLowercase) recommendations.push('Add lowercase letters')
162
+ if (!diversity.hasUppercase) recommendations.push('Add uppercase letters')
163
+ if (!diversity.hasNumbers) recommendations.push('Add numbers')
164
+ if (!diversity.hasSymbols) recommendations.push('Add symbols for maximum security')
165
+ if (diversity.uniqueChars < secret.length * 0.5) {
166
+ recommendations.push('Increase character diversity (too many repeated characters)')
167
+ }
168
+
169
+ return { entropy, strength, diversity, recommendations }
170
+ }
171
+
172
+ /**
173
+ * Generate a cryptographically secure secret
174
+ * @param length - Length in bytes (default: 32)
175
+ * @param encoding - Output encoding (default: base64url)
176
+ * @returns Generated secret
177
+ */
178
+ export function generateSecret(length: number = 32, encoding: 'hex' | 'base64' | 'base64url' = 'base64url'): string {
179
+ if (length < 16) {
180
+ throw new ValidationError('Secret length must be at least 16 bytes')
181
+ }
182
+
183
+ return randomBytes(length).toString(encoding)
184
+ }
185
+
186
+ /**
187
+ * Validate JWT token format
188
+ * @param token - Token to validate
189
+ * @returns True if valid JWT format
190
+ */
191
+ export function validateJwtFormat(token: string): boolean {
192
+ return JWT_PATTERN.test(token)
193
+ }
194
+
195
+ /**
196
+ * Sanitize database URL for logging (hide credentials)
197
+ * @param url - Database URL
198
+ * @returns Sanitized URL with hidden credentials
199
+ */
200
+ export function sanitizeDatabaseUrl(url: string): string {
201
+ try {
202
+ const parsed = validateDatabaseUrl(url)
203
+ return `${parsed.type}://[REDACTED]:[REDACTED]@${parsed.host}:${parsed.port}/${parsed.database}`
204
+ } catch {
205
+ return '[INVALID_DATABASE_URL]'
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Check if a string appears to be a secret (high entropy, base64-like)
211
+ * @param value - String to check
212
+ * @returns True if likely a secret
213
+ */
214
+ export function looksLikeSecret(value: string): boolean {
215
+ // Must be at least 16 chars
216
+ if (value.length < 16) {
217
+ return false
218
+ }
219
+
220
+ // Check entropy threshold
221
+ const entropy = calculateEntropy(value)
222
+ if (entropy < 64) {
223
+ return false
224
+ }
225
+
226
+ // Check if it's base64-like (alphanumeric + special chars)
227
+ const base64Like = /^[a-zA-Z0-9+/=_-]+$/
228
+ if (!base64Like.test(value)) {
229
+ return false
230
+ }
231
+
232
+ return true
233
+ }
234
+
235
+ /**
236
+ * Calculate Shannon entropy of a string
237
+ * @param str - Input string
238
+ * @returns Entropy in bits
239
+ */
240
+ export function calculateEntropy(str: string): number {
241
+ const len = str.length
242
+ if (len === 0) return 0
243
+
244
+ const frequencies = new Map<string, number>()
245
+ for (const char of str) {
246
+ frequencies.set(char, (frequencies.get(char) || 0) + 1)
247
+ }
248
+
249
+ let entropy = 0
250
+ for (const count of frequencies.values()) {
251
+ const p = count / len
252
+ entropy -= p * Math.log2(p)
253
+ }
254
+
255
+ return entropy * len
256
+ }
257
+
258
+ function analyzeCharacterDiversity(str: string) {
259
+ return {
260
+ hasLowercase: /[a-z]/.test(str),
261
+ hasUppercase: /[A-Z]/.test(str),
262
+ hasNumbers: /[0-9]/.test(str),
263
+ hasSymbols: /[^a-zA-Z0-9]/.test(str),
264
+ uniqueChars: new Set(str).size,
265
+ }
266
+ }
267
+
268
+ /** Export secret patterns for use in secret detection */
269
+ export const SECRET_FORMAT_PATTERNS = {
270
+ apiKey: API_KEY_PATTERNS,
271
+ databaseUrl: DATABASE_URL_PATTERNS,
272
+ jwt: JWT_PATTERN,
273
+ } as const
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Shared Synonym and Query Expansion Utilities
3
+ *
4
+ * Provides centralized synonym mappings and abbreviation expansions
5
+ * for use across search and profile services.
6
+ */
7
+
8
+ /**
9
+ * Common action/verb synonyms for query expansion.
10
+ * Keys are the base term, values are arrays of synonyms.
11
+ */
12
+ export const ACTION_SYNONYMS: Readonly<Record<string, readonly string[]>> = {
13
+ create: ['make', 'build', 'generate', 'construct', 'establish'],
14
+ delete: ['remove', 'destroy', 'erase', 'eliminate', 'clear'],
15
+ update: ['modify', 'change', 'edit', 'revise', 'alter'],
16
+ search: ['find', 'look', 'query', 'seek', 'locate'],
17
+ get: ['retrieve', 'fetch', 'obtain', 'acquire', 'access'],
18
+ list: ['show', 'display', 'enumerate', 'view', 'browse'],
19
+ error: ['bug', 'issue', 'problem', 'fault', 'defect'],
20
+ fix: ['solve', 'resolve', 'repair', 'correct', 'patch'],
21
+ add: ['insert', 'append', 'include', 'attach', 'incorporate'],
22
+ start: ['begin', 'launch', 'initiate', 'commence', 'activate'],
23
+ stop: ['end', 'halt', 'terminate', 'cease', 'pause'],
24
+ send: ['transmit', 'dispatch', 'deliver', 'forward', 'submit'],
25
+ } as const
26
+
27
+ /**
28
+ * Common technical abbreviations with their full forms.
29
+ * Used for expanding abbreviated terms in queries.
30
+ */
31
+ export const ABBREVIATION_EXPANSIONS: Readonly<Record<string, string>> = {
32
+ api: 'application programming interface',
33
+ db: 'database',
34
+ auth: 'authentication',
35
+ config: 'configuration',
36
+ env: 'environment',
37
+ var: 'variable',
38
+ func: 'function',
39
+ impl: 'implementation',
40
+ repo: 'repository',
41
+ deps: 'dependencies',
42
+ pkg: 'package',
43
+ src: 'source',
44
+ lib: 'library',
45
+ util: 'utility',
46
+ req: 'request',
47
+ res: 'response',
48
+ msg: 'message',
49
+ err: 'error',
50
+ doc: 'document',
51
+ docs: 'documentation',
52
+ dev: 'development',
53
+ prod: 'production',
54
+ ui: 'user interface',
55
+ ux: 'user experience',
56
+ } as const
57
+
58
+ /**
59
+ * Options for query expansion
60
+ */
61
+ export interface QueryExpansionOptions {
62
+ /** Include synonym expansions (default: true) */
63
+ includeSynonyms?: boolean
64
+ /** Expand abbreviations (default: true) */
65
+ expandAbbreviations?: boolean
66
+ /** Maximum synonyms to include per term (default: 2) */
67
+ maxSynonymsPerTerm?: number
68
+ /** Custom synonym mappings to merge with defaults */
69
+ customSynonyms?: Record<string, string[]>
70
+ /** Custom abbreviation expansions to merge with defaults */
71
+ customAbbreviations?: Record<string, string>
72
+ }
73
+
74
+ /**
75
+ * Get synonyms for a given term.
76
+ *
77
+ * @param term - The term to find synonyms for (case-insensitive)
78
+ * @param limit - Maximum number of synonyms to return (default: all)
79
+ * @param customSynonyms - Additional synonyms to check
80
+ * @returns Array of synonyms, empty if none found
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * getSynonyms('create'); // ['make', 'build', 'generate', ...]
85
+ * getSynonyms('create', 2); // ['make', 'build']
86
+ * ```
87
+ */
88
+ export function getSynonyms(term: string, limit?: number, customSynonyms?: Record<string, string[]>): string[] {
89
+ const lowerTerm = term.toLowerCase()
90
+ const allSynonyms = { ...ACTION_SYNONYMS, ...customSynonyms }
91
+ const synonyms = allSynonyms[lowerTerm]
92
+
93
+ if (!synonyms) {
94
+ return []
95
+ }
96
+
97
+ return limit !== undefined ? [...synonyms].slice(0, limit) : [...synonyms]
98
+ }
99
+
100
+ /**
101
+ * Expand an abbreviation to its full form.
102
+ *
103
+ * @param abbreviation - The abbreviation to expand (case-insensitive)
104
+ * @param customAbbreviations - Additional abbreviations to check
105
+ * @returns The expanded form, or undefined if not found
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * expandAbbreviation('api'); // 'application programming interface'
110
+ * expandAbbreviation('unknown'); // undefined
111
+ * ```
112
+ */
113
+ export function expandAbbreviation(
114
+ abbreviation: string,
115
+ customAbbreviations?: Record<string, string>
116
+ ): string | undefined {
117
+ const lowerAbbr = abbreviation.toLowerCase()
118
+ const allAbbreviations = { ...ABBREVIATION_EXPANSIONS, ...customAbbreviations }
119
+ return allAbbreviations[lowerAbbr]
120
+ }
121
+
122
+ /**
123
+ * Expand a query by adding synonyms and expanding abbreviations.
124
+ *
125
+ * @param query - The original query string
126
+ * @param options - Expansion options
127
+ * @returns Expanded query string with additional terms
128
+ *
129
+ * @example
130
+ * ```typescript
131
+ * expandQuery('create api'); // 'create api make build application programming interface'
132
+ * expandQuery('fix db error', { maxSynonymsPerTerm: 1 }); // 'fix db error solve database bug'
133
+ * ```
134
+ */
135
+ export function expandQuery(query: string, options: QueryExpansionOptions = {}): string {
136
+ const {
137
+ includeSynonyms = true,
138
+ expandAbbreviations = true,
139
+ maxSynonymsPerTerm = 2,
140
+ customSynonyms,
141
+ customAbbreviations,
142
+ } = options
143
+
144
+ const tokens = query
145
+ .toLowerCase()
146
+ .split(/\s+/)
147
+ .filter((t) => t.length > 0)
148
+ const expanded: string[] = [...tokens]
149
+
150
+ if (includeSynonyms) {
151
+ for (const token of tokens) {
152
+ const synonyms = getSynonyms(token, maxSynonymsPerTerm, customSynonyms)
153
+ expanded.push(...synonyms)
154
+ }
155
+ }
156
+
157
+ if (expandAbbreviations) {
158
+ for (const token of tokens) {
159
+ const expansion = expandAbbreviation(token, customAbbreviations)
160
+ if (expansion) {
161
+ expanded.push(expansion)
162
+ }
163
+ }
164
+ }
165
+
166
+ // Remove duplicates and return
167
+ return [...new Set(expanded)].join(' ')
168
+ }
169
+
170
+ /**
171
+ * Check if a term has known synonyms.
172
+ *
173
+ * @param term - The term to check
174
+ * @returns True if synonyms exist for this term
175
+ */
176
+ export function hasSynonyms(term: string): boolean {
177
+ return term.toLowerCase() in ACTION_SYNONYMS
178
+ }
179
+
180
+ /**
181
+ * Check if a term is a known abbreviation.
182
+ *
183
+ * @param term - The term to check
184
+ * @returns True if this is a known abbreviation
185
+ */
186
+ export function isAbbreviation(term: string): boolean {
187
+ return term.toLowerCase() in ABBREVIATION_EXPANSIONS
188
+ }