synapse-gateway 2.0.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 (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. package/tsconfig.json +34 -0
@@ -0,0 +1,137 @@
1
+ import { db } from '../db'
2
+ import { modelBenchmarks } from '../db/schema'
3
+ import { eq, desc, sql } from 'drizzle-orm'
4
+ import { logger } from '../utils/logger'
5
+ import { v4 as uuid } from 'uuid'
6
+ import type { NormalizedRequest, NormalizedResponse } from '../providers/types'
7
+
8
+ export interface BenchmarkResult {
9
+ id: string
10
+ model: string
11
+ taskType: string
12
+ latencyMs: number | null
13
+ tokensUsed: number | null
14
+ qualityScore: number | null
15
+ cost: number | null
16
+ providerId: string | null
17
+ isShadow: boolean
18
+ }
19
+
20
+ export interface ModelComparison {
21
+ model: string
22
+ avgLatencyMs: number
23
+ avgQualityScore: number
24
+ avgCost: number
25
+ totalBenchmarks: number
26
+ valueScore: number
27
+ }
28
+
29
+ class BenchmarkEngine {
30
+ async runBenchmark(params: {
31
+ model: string
32
+ taskType: string
33
+ providerId?: string
34
+ isShadow?: boolean
35
+ }, request: NormalizedRequest, response: NormalizedResponse): Promise<BenchmarkResult> {
36
+ const latencyMs = response.latencyMs
37
+ const tokensUsed = response.usage.totalTokens
38
+ const cost = 0
39
+ const qualityScore = this.calculateQualityScore(request, response)
40
+
41
+ const result: BenchmarkResult = {
42
+ id: uuid(),
43
+ model: params.model,
44
+ taskType: params.taskType,
45
+ latencyMs,
46
+ tokensUsed,
47
+ qualityScore,
48
+ cost,
49
+ providerId: params.providerId || null,
50
+ isShadow: params.isShadow ?? false,
51
+ }
52
+
53
+ try {
54
+ await db.insert(modelBenchmarks).values({
55
+ id: result.id,
56
+ model: result.model,
57
+ taskType: result.taskType,
58
+ latencyMs: result.latencyMs ?? null,
59
+ tokensUsed: result.tokensUsed ?? null,
60
+ qualityScore: result.qualityScore ?? null,
61
+ cost: result.cost ?? null,
62
+ providerId: result.providerId,
63
+ isShadow: result.isShadow,
64
+ })
65
+ } catch (err) {
66
+ logger.warn({ error: (err as Error).message }, 'Failed to save benchmark')
67
+ }
68
+
69
+ return result
70
+ }
71
+
72
+ private calculateQualityScore(request: NormalizedRequest, response: NormalizedResponse): number {
73
+ let score = 0.5
74
+ const content = response.choices[0]?.message?.content || ''
75
+
76
+ if (content.length > 50) score += 0.1
77
+ if (content.length > 200) score += 0.1
78
+ if (!content.includes('I cannot') && !content.includes('I\'m unable')) score += 0.1
79
+ if (content.includes('```')) score += 0.05
80
+ if (response.usage.totalTokens < request.messages.reduce((s, m) => s + m.content.length, 0) / 2) score += 0.05
81
+
82
+ if (response.latencyMs < 1000) score += 0.05
83
+ if (response.latencyMs < 500) score += 0.05
84
+
85
+ return Math.min(score, 1.0)
86
+ }
87
+
88
+ async compareModels(taskType?: string): Promise<ModelComparison[]> {
89
+ try {
90
+ const filter = taskType
91
+ ? sql`${modelBenchmarks.taskType} = ${taskType}`
92
+ : sql`1=1`
93
+
94
+ const rows = await db.select({
95
+ model: modelBenchmarks.model,
96
+ avgLatency: sql<number>`coalesce(avg(${modelBenchmarks.latencyMs}), 0)`,
97
+ avgQuality: sql<number>`coalesce(avg(${modelBenchmarks.qualityScore}), 0)`,
98
+ avgCost: sql<number>`coalesce(avg(${modelBenchmarks.cost}), 0)`,
99
+ count: sql<number>`count(*)`,
100
+ }).from(modelBenchmarks).where(filter).groupBy(modelBenchmarks.model)
101
+
102
+ return rows.map((r) => ({
103
+ model: r.model,
104
+ avgLatencyMs: Math.round(Number(r.avgLatency)),
105
+ avgQualityScore: Number(Number(r.avgQuality).toFixed(2)),
106
+ avgCost: Number(Number(r.avgCost).toFixed(4)),
107
+ totalBenchmarks: r.count,
108
+ valueScore: r.count > 0 ? Number(r.avgQuality) / (Number(r.avgCost) + 0.001) : 0,
109
+ })).sort((a, b) => b.avgQualityScore - a.avgQualityScore)
110
+ } catch {
111
+ return []
112
+ }
113
+ }
114
+
115
+ async getRecentBenchmarks(limit = 50): Promise<BenchmarkResult[]> {
116
+ try {
117
+ const rows = await db.select().from(modelBenchmarks)
118
+ .orderBy(desc(modelBenchmarks.createdAt))
119
+ .limit(limit)
120
+ return rows.map((r) => ({
121
+ id: r.id,
122
+ model: r.model,
123
+ taskType: r.taskType,
124
+ latencyMs: r.latencyMs,
125
+ tokensUsed: r.tokensUsed,
126
+ qualityScore: r.qualityScore ? Number(r.qualityScore) : null,
127
+ cost: r.cost ? Number(r.cost) : null,
128
+ providerId: r.providerId,
129
+ isShadow: !!r.isShadow,
130
+ }))
131
+ } catch {
132
+ return []
133
+ }
134
+ }
135
+ }
136
+
137
+ export const benchmarkEngine = new BenchmarkEngine()
@@ -0,0 +1,122 @@
1
+ import { registry } from './providers/registry'
2
+ import { OpenAIAdapter } from './providers/api-key/openai'
3
+ import { AnthropicAdapter } from './providers/api-key/anthropic'
4
+ import { DeepSeekAdapter } from './providers/api-key/deepseek'
5
+ import { OpenRouterAdapter } from './providers/api-key/openrouter'
6
+ import { GeminiAdapter } from './providers/api-key/gemini'
7
+ import { migrate } from './db/migrate'
8
+ import { experienceDistiller } from './distiller'
9
+ import { healthChecker } from './health'
10
+ import { logger } from './utils/logger'
11
+ import { registerTool, registerResource } from './mcp/registry'
12
+ import { pluginManager } from './plugins/index'
13
+ import { analytics } from './analytics'
14
+ import { memoryStore } from './memory'
15
+ import { semanticCacheStore } from './cache'
16
+ import type { Plugin, PluginContext } from './plugins/index'
17
+
18
+ function registerMCPTools() {
19
+ registerTool({
20
+ name: 'get_usage_stats',
21
+ description: 'Get usage statistics',
22
+ inputSchema: { type: 'object', properties: { days: { type: 'number' } } },
23
+ handler: async (args) => analytics.getUsageStats((args.days as number) || 7),
24
+ })
25
+ registerTool({
26
+ name: 'get_health_status',
27
+ description: 'Get provider health',
28
+ inputSchema: { type: 'object', properties: {} },
29
+ handler: async () => healthChecker.getProviderHealth(),
30
+ })
31
+ registerTool({
32
+ name: 'get_cache_stats',
33
+ description: 'Get cache statistics',
34
+ inputSchema: { type: 'object', properties: {} },
35
+ handler: async () => semanticCacheStore.getStats(),
36
+ })
37
+ registerTool({
38
+ name: 'search_memory',
39
+ description: 'Search memory',
40
+ inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
41
+ handler: async (args) => memoryStore.search(args.query as string),
42
+ })
43
+ registerTool({
44
+ name: 'clear_cache',
45
+ description: 'Clear cache',
46
+ inputSchema: { type: 'object', properties: {} },
47
+ handler: async () => { await semanticCacheStore.clear(); return { success: true } },
48
+ })
49
+ registerTool({
50
+ name: 'trigger_distillation',
51
+ description: 'Run distillation',
52
+ inputSchema: { type: 'object', properties: {} },
53
+ handler: async () => experienceDistiller.run(),
54
+ })
55
+ registerResource({
56
+ uri: 'synapse://config',
57
+ name: 'Configuration',
58
+ description: 'Current config',
59
+ mimeType: 'application/json',
60
+ read: async () => JSON.stringify({ version: '2.0.0' }),
61
+ })
62
+ registerResource({
63
+ uri: 'synapse://providers',
64
+ name: 'Providers',
65
+ description: 'Provider list',
66
+ mimeType: 'application/json',
67
+ read: async () => JSON.stringify(registry.list().map((a) => ({ id: a.info.id, name: a.info.name }))),
68
+ })
69
+ }
70
+
71
+ function registerPlugins() {
72
+ pluginManager.register({
73
+ name: 'pii-redactor',
74
+ description: 'Redacts PII (SSN, email, phone)',
75
+ phase: 'pre_request',
76
+ execute: async (ctx: PluginContext) => {
77
+ let content = ctx.content
78
+ content = content.replace(/\b\d{3}[-.]?\d{2}[-.]?\d{4}\b/g, '[REDACTED]')
79
+ content = content.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '[REDACTED]')
80
+ content = content.replace(/\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g, '[REDACTED]')
81
+ return { modified: content !== ctx.content, content }
82
+ },
83
+ })
84
+ pluginManager.register({
85
+ name: 'response-validator',
86
+ description: 'Validates response quality',
87
+ phase: 'post_response',
88
+ execute: async (ctx: PluginContext) => {
89
+ const valid = ctx.content.trim().length > 0
90
+ return { modified: false, content: ctx.content, metadata: { validation: { valid } } }
91
+ },
92
+ })
93
+ }
94
+
95
+ export async function bootstrap() {
96
+ logger.info('Bootstrapping Synapse...')
97
+
98
+ await migrate()
99
+
100
+ const builtInProviders = [
101
+ new OpenAIAdapter(),
102
+ new AnthropicAdapter(),
103
+ new GeminiAdapter(),
104
+ new DeepSeekAdapter(),
105
+ new OpenRouterAdapter(),
106
+ ]
107
+
108
+ for (const adapter of builtInProviders) {
109
+ registry.register(adapter)
110
+ }
111
+
112
+ logger.info({ providers: builtInProviders.length }, 'Providers registered')
113
+
114
+ registerMCPTools()
115
+ registerPlugins()
116
+ logger.info('MCP tools and plugins registered')
117
+
118
+ healthChecker.start(5 * 60 * 1000)
119
+ experienceDistiller.start()
120
+
121
+ logger.info('Synapse bootstrap complete')
122
+ }
@@ -0,0 +1 @@
1
+ export { semanticCacheStore, computeQueryHash } from './semantic'
@@ -0,0 +1,211 @@
1
+ import { db } from '../db'
2
+ import { semanticCache as semanticCacheTable } from '../db/schema'
3
+ import { eq, desc, sql, and, gt } from 'drizzle-orm'
4
+ import { logger } from '../utils/logger'
5
+ import { v4 as uuid } from 'uuid'
6
+
7
+ interface CacheEntry {
8
+ id: string
9
+ promptHash: string
10
+ promptText: string
11
+ responseText: string
12
+ model: string
13
+ inputTokens: number | null
14
+ outputTokens: number | null
15
+ similarityThreshold: number
16
+ hits: number
17
+ qualityScore: number | null
18
+ expiresAt: string
19
+ createdAt: string
20
+ }
21
+
22
+ class SemanticCache {
23
+ private memoryCache = new Map<string, { entry: CacheEntry; expiresAt: number }>()
24
+ private maxMemoryEntries = 500
25
+
26
+ async get(promptHash: string): Promise<CacheEntry | null> {
27
+ const memEntry = this.memoryCache.get(promptHash)
28
+ if (memEntry) {
29
+ if (Date.now() > memEntry.expiresAt) {
30
+ this.memoryCache.delete(promptHash)
31
+ } else {
32
+ memEntry.entry.hits++
33
+ return memEntry.entry
34
+ }
35
+ }
36
+
37
+ try {
38
+ const rows = await db.select().from(semanticCacheTable)
39
+ .where(and(eq(semanticCacheTable.promptHash, promptHash), gt(semanticCacheTable.expiresAt, new Date().toISOString())))
40
+ .limit(1)
41
+
42
+ if (rows.length === 0) return null
43
+
44
+ const row = rows[0]
45
+ await db.update(semanticCacheTable).set({
46
+ hits: sql`${semanticCacheTable.hits} + 1`,
47
+ }).where(eq(semanticCacheTable.id, row.id))
48
+
49
+ const entry: CacheEntry = {
50
+ id: row.id,
51
+ promptHash: row.promptHash,
52
+ promptText: row.promptText,
53
+ responseText: row.responseText,
54
+ model: row.model,
55
+ inputTokens: row.inputTokens ?? null,
56
+ outputTokens: row.outputTokens ?? null,
57
+ similarityThreshold: row.similarityThreshold ?? 0.95,
58
+ hits: row.hits ?? 0,
59
+ qualityScore: row.qualityScore ?? null,
60
+ expiresAt: row.expiresAt,
61
+ createdAt: row.createdAt,
62
+ }
63
+
64
+ this.setMemoryCache(promptHash, entry)
65
+ return entry
66
+ } catch {
67
+ return null
68
+ }
69
+ }
70
+
71
+ async set(params: {
72
+ promptHash: string
73
+ promptText: string
74
+ responseText: string
75
+ model: string
76
+ inputTokens?: number
77
+ outputTokens?: number
78
+ similarityThreshold?: number
79
+ qualityScore?: number
80
+ ttlHours?: number
81
+ }): Promise<void> {
82
+ const id = uuid()
83
+ const ttlMs = (params.ttlHours || 24) * 60 * 60 * 1000
84
+ const expiresAt = new Date(Date.now() + ttlMs).toISOString()
85
+
86
+ const entry: CacheEntry = {
87
+ id,
88
+ promptHash: params.promptHash,
89
+ promptText: params.promptText,
90
+ responseText: params.responseText,
91
+ model: params.model,
92
+ inputTokens: params.inputTokens ?? null,
93
+ outputTokens: params.outputTokens ?? null,
94
+ similarityThreshold: params.similarityThreshold ?? 0.95,
95
+ hits: 0,
96
+ qualityScore: params.qualityScore ?? null,
97
+ expiresAt,
98
+ createdAt: new Date().toISOString(),
99
+ }
100
+
101
+ this.setMemoryCache(params.promptHash, entry)
102
+
103
+ try {
104
+ await db.insert(semanticCacheTable).values({
105
+ id,
106
+ embedding: Buffer.alloc(0),
107
+ promptHash: params.promptHash,
108
+ promptText: params.promptText,
109
+ responseText: params.responseText,
110
+ model: params.model,
111
+ inputTokens: params.inputTokens ?? null,
112
+ outputTokens: params.outputTokens ?? null,
113
+ similarityThreshold: params.similarityThreshold ?? 0.95,
114
+ hits: 0,
115
+ qualityScore: params.qualityScore ?? null,
116
+ expiresAt,
117
+ })
118
+ } catch (err) {
119
+ logger.warn({ error: (err as Error).message }, 'Failed to cache response')
120
+ }
121
+ }
122
+
123
+ async invalidate(promptHash: string): Promise<void> {
124
+ this.memoryCache.delete(promptHash)
125
+ try {
126
+ await db.delete(semanticCacheTable).where(eq(semanticCacheTable.promptHash, promptHash))
127
+ } catch {
128
+ // ignore
129
+ }
130
+ }
131
+
132
+ async clear(): Promise<void> {
133
+ this.memoryCache.clear()
134
+ try {
135
+ await db.delete(semanticCacheTable)
136
+ } catch {
137
+ // ignore
138
+ }
139
+ }
140
+
141
+ async cleanup(): Promise<number> {
142
+ const now = new Date().toISOString()
143
+ let deleted = 0
144
+
145
+ try {
146
+ const expired = await db.select({ id: semanticCacheTable.id }).from(semanticCacheTable)
147
+ .where(sql`${semanticCacheTable.expiresAt} < ${now}`)
148
+ deleted = expired.length
149
+
150
+ if (deleted > 0) {
151
+ await db.delete(semanticCacheTable).where(sql`${semanticCacheTable.expiresAt} < ${now}`)
152
+ logger.info({ deleted }, 'Cleaned up expired cache entries')
153
+ }
154
+ } catch {
155
+ // ignore
156
+ }
157
+
158
+ for (const [key, val] of this.memoryCache) {
159
+ if (Date.now() > val.expiresAt) {
160
+ this.memoryCache.delete(key)
161
+ }
162
+ }
163
+
164
+ return deleted
165
+ }
166
+
167
+ async getStats() {
168
+ try {
169
+ const [stats] = await db.select({
170
+ total: sql<number>`count(*)`,
171
+ hits: sql<number>`coalesce(sum(${semanticCacheTable.hits}), 0)`,
172
+ }).from(semanticCacheTable)
173
+
174
+ const total = stats?.total ?? 0
175
+ const hits = stats?.hits ?? 0
176
+
177
+ return {
178
+ totalEntries: total,
179
+ memoryEntries: this.memoryCache.size,
180
+ totalHits: hits,
181
+ hitRate: total > 0 ? hits / total : 0,
182
+ }
183
+ } catch {
184
+ return {
185
+ totalEntries: 0,
186
+ memoryEntries: this.memoryCache.size,
187
+ totalHits: 0,
188
+ hitRate: 0,
189
+ }
190
+ }
191
+ }
192
+
193
+ private setMemoryCache(key: string, entry: CacheEntry) {
194
+ if (this.memoryCache.size >= this.maxMemoryEntries) {
195
+ const oldest = this.memoryCache.keys().next().value
196
+ if (oldest) this.memoryCache.delete(oldest)
197
+ }
198
+ this.memoryCache.set(key, { entry, expiresAt: new Date(entry.expiresAt).getTime() })
199
+ }
200
+ }
201
+
202
+ export async function computeQueryHash(query: string, model: string): Promise<string> {
203
+ const normalized = query.toLowerCase().trim().replace(/\s+/g, ' ')
204
+ const data = `${normalized}:${model}`
205
+ const encoder = new TextEncoder()
206
+ const hashBuffer = await crypto.subtle.digest('SHA-256', encoder.encode(data))
207
+ const hashArray = Array.from(new Uint8Array(hashBuffer))
208
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
209
+ }
210
+
211
+ export const semanticCacheStore = new SemanticCache()
@@ -0,0 +1,61 @@
1
+ import type { Config } from './schema'
2
+
3
+ export const configDefaults: Config = {
4
+ port: 3333,
5
+ hostname: '127.0.0.1',
6
+ logLevel: 'info',
7
+ dataDir: '',
8
+ auth: {
9
+ initialPassword: 'changeme',
10
+ jwtSecret: '',
11
+ requireApiKey: false,
12
+ },
13
+ rtk: {
14
+ enabled: true,
15
+ compressionLevel: 'balanced',
16
+ filters: ['git-diff', 'grep', 'ls-tree', 'log-dump', 'smart-truncate'],
17
+ },
18
+ proxy: {
19
+ enabled: false,
20
+ type: 'socks5',
21
+ host: '127.0.0.1',
22
+ port: 9050,
23
+ },
24
+ tor: {
25
+ enabled: false,
26
+ socksPort: 9050,
27
+ controlPort: 9051,
28
+ password: '',
29
+ },
30
+ cache: {
31
+ enabled: true,
32
+ ttlMinutes: 10,
33
+ maxSize: '100MB',
34
+ },
35
+ fallback: {
36
+ maxRetries: 3,
37
+ timeoutMs: 30000,
38
+ cooldownMinutes: 5,
39
+ },
40
+ cloudSync: {
41
+ enabled: false,
42
+ url: '',
43
+ encryptionKey: '',
44
+ },
45
+ semanticCache: {
46
+ enabled: true,
47
+ similarityThreshold: 0.95,
48
+ ttlMinutes: 30,
49
+ maxEntries: 10000,
50
+ embeddingModel: 'local-minilm',
51
+ },
52
+ memory: {
53
+ enabled: true,
54
+ episodicTtlDays: 90,
55
+ distillationIntervalHours: 6,
56
+ },
57
+ skills: {
58
+ enabled: true,
59
+ defaultRotationStrategy: 'task_match',
60
+ },
61
+ }
@@ -0,0 +1,72 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { configSchema, type Config } from './schema'
5
+ import { configDefaults } from './defaults'
6
+
7
+ let cachedConfig: Config | null = null
8
+
9
+ function getConfigPath(): string {
10
+ const dataDir = process.env.DATA_DIR || path.join(os.homedir(), '.synapse')
11
+ return path.join(dataDir, 'config.json')
12
+ }
13
+
14
+ function ensureDataDir(): void {
15
+ const dataDir = process.env.DATA_DIR || path.join(os.homedir(), '.synapse')
16
+ if (!fs.existsSync(dataDir)) {
17
+ fs.mkdirSync(dataDir, { recursive: true })
18
+ }
19
+ }
20
+
21
+ export function loadConfig(): Config {
22
+ if (cachedConfig) return cachedConfig
23
+
24
+ ensureDataDir()
25
+ const configPath = getConfigPath()
26
+
27
+ let rawConfig: Record<string, unknown> = {}
28
+ if (fs.existsSync(configPath)) {
29
+ try {
30
+ const raw = fs.readFileSync(configPath, 'utf-8')
31
+ rawConfig = JSON.parse(raw)
32
+ } catch {
33
+ rawConfig = {}
34
+ }
35
+ }
36
+
37
+ const merged = { ...configDefaults, ...rawConfig }
38
+ const parsed = configSchema.safeParse(merged)
39
+
40
+ if (!parsed.success) {
41
+ console.error('Config validation errors:', parsed.error.flatten())
42
+ cachedConfig = configDefaults
43
+ return cachedConfig
44
+ }
45
+
46
+ cachedConfig = parsed.data
47
+
48
+ if (!fs.existsSync(configPath)) {
49
+ saveConfig(cachedConfig)
50
+ }
51
+
52
+ return cachedConfig
53
+ }
54
+
55
+ export function saveConfig(config: Config): void {
56
+ ensureDataDir()
57
+ const configPath = getConfigPath()
58
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
59
+ cachedConfig = config
60
+ }
61
+
62
+ export function updateConfig(partial: Partial<Config>): Config {
63
+ const current = loadConfig()
64
+ const updated = { ...current, ...partial }
65
+ const parsed = configSchema.parse(updated)
66
+ saveConfig(parsed)
67
+ return parsed
68
+ }
69
+
70
+ export function getConfig(): Config {
71
+ return loadConfig()
72
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod'
2
+
3
+ export const configSchema = z.object({
4
+ port: z.number().default(3333),
5
+ hostname: z.string().default('127.0.0.1'),
6
+ logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
7
+ dataDir: z.string().default(''),
8
+ auth: z.object({
9
+ initialPassword: z.string().default('changeme'),
10
+ jwtSecret: z.string().default(''),
11
+ requireApiKey: z.boolean().default(false),
12
+ }).default({ initialPassword: 'changeme', jwtSecret: '', requireApiKey: false }),
13
+ rtk: z.object({
14
+ enabled: z.boolean().default(true),
15
+ compressionLevel: z.enum(['none', 'light', 'balanced', 'aggressive']).default('balanced'),
16
+ filters: z.array(z.string()).default(['git-diff', 'grep', 'ls-tree', 'log-dump', 'smart-truncate']),
17
+ }).default({ enabled: true, compressionLevel: 'balanced', filters: ['git-diff', 'grep', 'ls-tree', 'log-dump', 'smart-truncate'] }),
18
+ proxy: z.object({
19
+ enabled: z.boolean().default(false),
20
+ type: z.enum(['http', 'socks5']).default('socks5'),
21
+ host: z.string().default('127.0.0.1'),
22
+ port: z.number().default(9050),
23
+ }).default({ enabled: false, type: 'socks5', host: '127.0.0.1', port: 9050 }),
24
+ tor: z.object({
25
+ enabled: z.boolean().default(false),
26
+ socksPort: z.number().default(9050),
27
+ controlPort: z.number().default(9051),
28
+ password: z.string().default(''),
29
+ }).default({ enabled: false, socksPort: 9050, controlPort: 9051, password: '' }),
30
+ cache: z.object({
31
+ enabled: z.boolean().default(true),
32
+ ttlMinutes: z.number().default(10),
33
+ maxSize: z.string().default('100MB'),
34
+ }).default({ enabled: true, ttlMinutes: 10, maxSize: '100MB' }),
35
+ fallback: z.object({
36
+ maxRetries: z.number().default(3),
37
+ timeoutMs: z.number().default(30000),
38
+ cooldownMinutes: z.number().default(5),
39
+ }).default({ maxRetries: 3, timeoutMs: 30000, cooldownMinutes: 5 }),
40
+ cloudSync: z.object({
41
+ enabled: z.boolean().default(false),
42
+ url: z.string().default(''),
43
+ encryptionKey: z.string().default(''),
44
+ }).default({ enabled: false, url: '', encryptionKey: '' }),
45
+ semanticCache: z.object({
46
+ enabled: z.boolean().default(true),
47
+ similarityThreshold: z.number().min(0).max(1).default(0.95),
48
+ ttlMinutes: z.number().default(30),
49
+ maxEntries: z.number().default(10000),
50
+ embeddingModel: z.string().default('local-minilm'),
51
+ }).default({ enabled: true, similarityThreshold: 0.95, ttlMinutes: 30, maxEntries: 10000, embeddingModel: 'local-minilm' }),
52
+ memory: z.object({
53
+ enabled: z.boolean().default(true),
54
+ episodicTtlDays: z.number().default(90),
55
+ distillationIntervalHours: z.number().default(6),
56
+ }).default({ enabled: true, episodicTtlDays: 90, distillationIntervalHours: 6 }),
57
+ skills: z.object({
58
+ enabled: z.boolean().default(true),
59
+ defaultRotationStrategy: z.enum(['task_match', 'round_robin', 'quality_based', 'schedule', 'weighted_random']).default('task_match'),
60
+ }).default({ enabled: true, defaultRotationStrategy: 'task_match' }),
61
+ })
62
+
63
+ export type Config = z.infer<typeof configSchema>
@@ -0,0 +1,22 @@
1
+ import Database from 'better-sqlite3'
2
+ import { drizzle } from 'drizzle-orm/better-sqlite3'
3
+ import * as schema from './schema'
4
+ import path from 'path'
5
+ import os from 'os'
6
+ import fs from 'fs'
7
+
8
+ const dataDir = process.env.DATA_DIR || path.join(os.homedir(), '.synapse')
9
+ const dbDir = path.join(dataDir, 'db')
10
+ const dbPath = path.join(dbDir, 'synapse.db')
11
+
12
+ if (!fs.existsSync(dbDir)) {
13
+ fs.mkdirSync(dbDir, { recursive: true })
14
+ }
15
+
16
+ const sqlite = new Database(dbPath)
17
+ sqlite.pragma('journal_mode = WAL')
18
+ sqlite.pragma('foreign_keys = ON')
19
+ sqlite.pragma('busy_timeout = 5000')
20
+
21
+ export const db = drizzle(sqlite, { schema })
22
+ export { sqlite }