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,82 @@
1
+ import { registry } from '../providers/registry'
2
+ import type { NormalizedRequest, NormalizedResponse, ProviderAccount } from '../providers/types'
3
+ import { logger } from '../utils/logger'
4
+
5
+ export interface RouteResult {
6
+ response: NormalizedResponse
7
+ provider: string
8
+ model: string
9
+ fallbackUsed: boolean
10
+ fallbackChain?: string[]
11
+ }
12
+
13
+ export async function routeRequest(
14
+ req: NormalizedRequest,
15
+ getAccount: (providerId: string) => ProviderAccount | null
16
+ ): Promise<RouteResult> {
17
+ const resolved = registry.resolveModel(req.model)
18
+ if (!resolved) {
19
+ if (req.model.includes('/')) {
20
+ throw new Error(`No provider found for model: ${req.model}`)
21
+ }
22
+ const adapters = registry.list()
23
+ if (adapters.length === 0) {
24
+ throw new Error('No providers registered')
25
+ }
26
+ const fallbackChain: string[] = []
27
+ for (const adapter of adapters) {
28
+ const account = getAccount(adapter.info.id)
29
+ if (!account || !account.enabled) continue
30
+ try {
31
+ const modifiedReq = { ...req, model: req.model }
32
+ const response = await adapter.chatCompletion(modifiedReq, account)
33
+ return { response, provider: adapter.info.name, model: req.model, fallbackUsed: true, fallbackChain }
34
+ } catch (err) {
35
+ fallbackChain.push(`${adapter.info.name}: ${(err as Error).message}`)
36
+ logger.warn({ provider: adapter.info.name, error: (err as Error).message }, 'Provider failed, trying next')
37
+ }
38
+ }
39
+ throw new Error(`All providers failed: ${fallbackChain.join(' | ')}`)
40
+ }
41
+
42
+ const { adapter, modelName } = resolved
43
+ const account = getAccount(adapter.info.id)
44
+ if (!account || !account.enabled) {
45
+ throw new Error(`No enabled account for provider: ${adapter.info.name}`)
46
+ }
47
+
48
+ const modifiedReq = { ...req, model: modelName }
49
+ const response = await adapter.chatCompletion(modifiedReq, account)
50
+
51
+ return {
52
+ response,
53
+ provider: adapter.info.name,
54
+ model: req.model,
55
+ fallbackUsed: false,
56
+ }
57
+ }
58
+
59
+ export async function routeRequestStream(
60
+ req: NormalizedRequest,
61
+ getAccount: (providerId: string) => ProviderAccount | null
62
+ ): Promise<AsyncIterable<import('../providers/types').NormalizedChunk> & { provider: string; model: string }> {
63
+ const resolved = registry.resolveModel(req.model)
64
+ if (!resolved) {
65
+ throw new Error(`No provider found for model: ${req.model}`)
66
+ }
67
+
68
+ const { adapter, modelName } = resolved
69
+ const account = getAccount(adapter.info.id)
70
+ if (!account || !account.enabled) {
71
+ throw new Error(`No enabled account for provider: ${adapter.info.name}`)
72
+ }
73
+
74
+ const modifiedReq = { ...req, model: modelName }
75
+ const stream = adapter.chatCompletionStream(modifiedReq, account)
76
+
77
+ return {
78
+ ...stream,
79
+ provider: adapter.info.name,
80
+ model: req.model,
81
+ }
82
+ }
@@ -0,0 +1,57 @@
1
+ import { db } from '../db'
2
+ import { skills } from '../db/schema'
3
+ import type { SkillForgeRecipe } from './types'
4
+ import { skillRegistry } from './registry'
5
+ import { v4 as uuid } from 'uuid'
6
+
7
+ export class SkillForge {
8
+ async createFromRecipe(recipe: SkillForgeRecipe): Promise<string> {
9
+ const systemPrompt = this.buildSystemPrompt(recipe)
10
+ const skillId = uuid()
11
+
12
+ await skillRegistry.createSkill({
13
+ id: skillId,
14
+ name: recipe.name,
15
+ description: recipe.description,
16
+ systemPrompt,
17
+ preferredModel: recipe.preferredModel,
18
+ enabled: true,
19
+ groupId: null,
20
+ })
21
+
22
+ return skillId
23
+ }
24
+
25
+ async importFromOpenClaw(data: {
26
+ name: string
27
+ description: string
28
+ prompt: string
29
+ examples?: Array<{ input: string; output: string }>
30
+ }): Promise<string> {
31
+ return this.createFromRecipe({
32
+ name: data.name,
33
+ description: data.description,
34
+ basePrompt: data.prompt,
35
+ examples: data.examples || [],
36
+ })
37
+ }
38
+
39
+ private buildSystemPrompt(recipe: SkillForgeRecipe): string {
40
+ let prompt = recipe.basePrompt
41
+
42
+ if (recipe.constraints && recipe.constraints.length > 0) {
43
+ prompt += '\n\nConstraints:\n' + recipe.constraints.map((c) => `- ${c}`).join('\n')
44
+ }
45
+
46
+ if (recipe.examples.length > 0) {
47
+ prompt += '\n\nExamples:\n'
48
+ for (const ex of recipe.examples) {
49
+ prompt += `\nInput: ${ex.input}\nOutput: ${ex.output}\n`
50
+ }
51
+ }
52
+
53
+ return prompt
54
+ }
55
+ }
56
+
57
+ export const skillForge = new SkillForge()
@@ -0,0 +1,3 @@
1
+ export type { Skill, SkillGroup, SkillRotationResult, SkillForgeRecipe, RotationStrategy } from './types'
2
+ export { skillRegistry } from './registry'
3
+ export { skillForge } from './forge'
@@ -0,0 +1,195 @@
1
+ import { db } from '../db'
2
+ import { skills, skillGroups } from '../db/schema'
3
+ import { eq } from 'drizzle-orm'
4
+ import type { Skill, SkillGroup, RotationStrategy, SkillRotationResult } from './types'
5
+ import { logger } from '../utils/logger'
6
+
7
+ class SkillRegistry {
8
+ private skillCache = new Map<string, Skill>()
9
+ private groupCache = new Map<string, SkillGroup>()
10
+ private loaded = false
11
+
12
+ async load() {
13
+ if (this.loaded) return
14
+
15
+ try {
16
+ const allSkills = await db.select().from(skills)
17
+ const allGroups = await db.select().from(skillGroups)
18
+
19
+ this.skillCache.clear()
20
+ this.groupCache.clear()
21
+
22
+ for (const s of allSkills) {
23
+ const rotationConfig = s.rotationConfig ? JSON.parse(s.rotationConfig) : {}
24
+ this.skillCache.set(s.id, {
25
+ id: s.id,
26
+ name: s.name,
27
+ description: s.description || '',
28
+ systemPrompt: s.systemPrompt,
29
+ groupId: s.groupId,
30
+ temperature: rotationConfig.temperature ?? undefined,
31
+ maxTokens: rotationConfig.maxTokens ?? undefined,
32
+ preferredModel: rotationConfig.preferredModel ?? undefined,
33
+ enabled: s.enabled,
34
+ metadata: { usageCount: s.usageCount, qualityScore: s.qualityScore, tags: JSON.parse(s.tags) },
35
+ })
36
+ }
37
+
38
+ for (const g of allGroups) {
39
+ const taskTypes: string[] = g.taskTypes ? JSON.parse(g.taskTypes) : []
40
+ this.groupCache.set(g.id, {
41
+ id: g.id,
42
+ name: g.name,
43
+ description: g.description || '',
44
+ skillIds: taskTypes,
45
+ rotationStrategy: (g.rotationStrategy as RotationStrategy) || 'round_robin',
46
+ enabled: g.enabled,
47
+ })
48
+ }
49
+
50
+ this.loaded = true
51
+ logger.info({ skills: this.skillCache.size, groups: this.groupCache.size }, 'Skills loaded')
52
+ } catch (err) {
53
+ logger.warn({ error: (err as Error).message }, 'Failed to load skills from DB, using empty registry')
54
+ this.loaded = true
55
+ }
56
+ }
57
+
58
+ async getSkill(id: string): Promise<Skill | null> {
59
+ await this.load()
60
+ return this.skillCache.get(id) || null
61
+ }
62
+
63
+ async listSkills(): Promise<Skill[]> {
64
+ await this.load()
65
+ return Array.from(this.skillCache.values())
66
+ }
67
+
68
+ async listEnabledSkills(): Promise<Skill[]> {
69
+ await this.load()
70
+ return Array.from(this.skillCache.values()).filter((s) => s.enabled)
71
+ }
72
+
73
+ async getGroup(id: string): Promise<SkillGroup | null> {
74
+ await this.load()
75
+ return this.groupCache.get(id) || null
76
+ }
77
+
78
+ async listGroups(): Promise<SkillGroup[]> {
79
+ await this.load()
80
+ return Array.from(this.groupCache.values())
81
+ }
82
+
83
+ async createSkill(skill: Skill): Promise<void> {
84
+ const rotationConfig: Record<string, unknown> = {}
85
+ if (skill.temperature !== undefined) rotationConfig.temperature = skill.temperature
86
+ if (skill.maxTokens !== undefined) rotationConfig.maxTokens = skill.maxTokens
87
+ if (skill.preferredModel !== undefined) rotationConfig.preferredModel = skill.preferredModel
88
+
89
+ await db.insert(skills).values({
90
+ id: skill.id,
91
+ name: skill.name,
92
+ description: skill.description,
93
+ systemPrompt: skill.systemPrompt,
94
+ groupId: skill.groupId,
95
+ rotationConfig: JSON.stringify(rotationConfig),
96
+ enabled: skill.enabled,
97
+ })
98
+ this.skillCache.set(skill.id, skill)
99
+ }
100
+
101
+ async createGroup(group: SkillGroup): Promise<void> {
102
+ await db.insert(skillGroups).values({
103
+ id: group.id,
104
+ name: group.name,
105
+ description: group.description,
106
+ taskTypes: JSON.stringify(group.skillIds),
107
+ rotationStrategy: group.rotationStrategy,
108
+ enabled: group.enabled,
109
+ })
110
+ this.groupCache.set(group.id, group)
111
+ }
112
+
113
+ async updateSkill(id: string, updates: Partial<Skill>): Promise<void> {
114
+ const existing = this.skillCache.get(id)
115
+ if (!existing) throw new Error(`Skill not found: ${id}`)
116
+
117
+ const updated = { ...existing, ...updates }
118
+ const rotationConfig: Record<string, unknown> = {}
119
+ if (updated.temperature !== undefined) rotationConfig.temperature = updated.temperature
120
+ if (updated.maxTokens !== undefined) rotationConfig.maxTokens = updated.maxTokens
121
+ if (updated.preferredModel !== undefined) rotationConfig.preferredModel = updated.preferredModel
122
+
123
+ await db.update(skills).set({
124
+ name: updated.name,
125
+ description: updated.description,
126
+ systemPrompt: updated.systemPrompt,
127
+ groupId: updated.groupId,
128
+ rotationConfig: JSON.stringify(rotationConfig),
129
+ enabled: updated.enabled,
130
+ }).where(eq(skills.id, id))
131
+ this.skillCache.set(id, updated)
132
+ }
133
+
134
+ async deleteSkill(id: string): Promise<void> {
135
+ await db.delete(skills).where(eq(skills.id, id))
136
+ this.skillCache.delete(id)
137
+ }
138
+
139
+ async rotate(groupId: string, taskType?: string): Promise<SkillRotationResult> {
140
+ await this.load()
141
+ const group = this.groupCache.get(groupId)
142
+ if (!group) throw new Error(`Group not found: ${groupId}`)
143
+ if (!group.enabled) throw new Error(`Group "${group.name}" is disabled`)
144
+
145
+ const enabledSkills = group.skillIds
146
+ .map((sid) => this.skillCache.get(sid))
147
+ .filter((s): s is Skill => !!s && s.enabled)
148
+
149
+ if (enabledSkills.length === 0) throw new Error(`No enabled skills in group "${group.name}"`)
150
+ if (enabledSkills.length === 1) {
151
+ return { skill: enabledSkills[0], group, strategy: group.rotationStrategy, confidence: 1 }
152
+ }
153
+
154
+ const selected = this.applyStrategy(enabledSkills, group.rotationStrategy, taskType)
155
+ return { skill: selected, group, strategy: group.rotationStrategy, confidence: 0.8 }
156
+ }
157
+
158
+ private applyStrategy(candidates: Skill[], strategy: RotationStrategy, taskType?: string): Skill {
159
+ switch (strategy) {
160
+ case 'round_robin': {
161
+ const idx = Date.now() % candidates.length
162
+ return candidates[idx]
163
+ }
164
+ case 'quality_based':
165
+ return candidates[0]
166
+ case 'weighted_random': {
167
+ return candidates[Math.floor(Math.random() * candidates.length)]
168
+ }
169
+ case 'task_match': {
170
+ if (taskType) {
171
+ const match = candidates.find((s) =>
172
+ s.name.toLowerCase().includes(taskType.toLowerCase()) ||
173
+ s.description.toLowerCase().includes(taskType.toLowerCase())
174
+ )
175
+ if (match) return match
176
+ }
177
+ return candidates[0]
178
+ }
179
+ case 'schedule': {
180
+ const idx = Math.floor(Date.now() / 3600000) % candidates.length
181
+ return candidates[idx]
182
+ }
183
+ default:
184
+ return candidates[0]
185
+ }
186
+ }
187
+
188
+ invalidate() {
189
+ this.loaded = false
190
+ this.skillCache.clear()
191
+ this.groupCache.clear()
192
+ }
193
+ }
194
+
195
+ export const skillRegistry = new SkillRegistry()
@@ -0,0 +1,44 @@
1
+ export type RotationStrategy = 'task_match' | 'round_robin' | 'quality_based' | 'schedule' | 'weighted_random'
2
+
3
+ export interface Skill {
4
+ id: string
5
+ name: string
6
+ description: string
7
+ systemPrompt: string
8
+ groupId: string | null
9
+ temperature?: number
10
+ maxTokens?: number
11
+ preferredModel?: string
12
+ enabled: boolean
13
+ metadata?: Record<string, unknown>
14
+ }
15
+
16
+ export interface SkillGroup {
17
+ id: string
18
+ name: string
19
+ description: string
20
+ skillIds: string[]
21
+ rotationStrategy: RotationStrategy
22
+ enabled: boolean
23
+ schedule?: {
24
+ cron: string
25
+ timezone: string
26
+ }
27
+ weights?: Record<string, number>
28
+ }
29
+
30
+ export interface SkillRotationResult {
31
+ skill: Skill
32
+ group: SkillGroup | null
33
+ strategy: RotationStrategy
34
+ confidence: number
35
+ }
36
+
37
+ export interface SkillForgeRecipe {
38
+ name: string
39
+ description: string
40
+ basePrompt: string
41
+ examples: Array<{ input: string; output: string }>
42
+ constraints?: string[]
43
+ preferredModel?: string
44
+ }
@@ -0,0 +1,158 @@
1
+ export type CompressionLevel = 'none' | 'light' | 'balanced' | 'aggressive'
2
+
3
+ export interface SqueezeResult {
4
+ originalTokens: number
5
+ squeezedTokens: number
6
+ savingsPercent: number
7
+ level: CompressionLevel
8
+ filters: string[]
9
+ }
10
+
11
+ export interface SqueezeContext {
12
+ content: string
13
+ contentType: 'code' | 'text' | 'mixed'
14
+ contextWindow: number
15
+ usedTokens: number
16
+ targetTokens?: number
17
+ }
18
+
19
+ const CODE_PATTERNS = /(?:function|class|const |let |var |import |export |def |async |return |=>|```|\{|\}|\[|\])/
20
+ const COMMENT_PATTERNS = /(?:\/\/.*$|\/\*[\s\S]*?\*\/|#.*$)/gm
21
+ const WHITESPACE_MULTI = /\n{3,}/g
22
+ const TRAILING_WS = /[ \t]+$/gm
23
+ const EMPTY_LINES = /^\s*\n/gm
24
+ const CONSOLE_LOG = /console\.(log|debug|info|warn|error)\([^)]*\);?\n?/g
25
+ const TYPE_IMPORTS = /^import\s+type\s+.*$;\s*$/gm
26
+ const EMPTY_BLOCKS = /\{\s*\}/g
27
+
28
+ function classifyContent(content: string): 'code' | 'text' | 'mixed' {
29
+ const lines = content.split('\n')
30
+ let codeLines = 0
31
+ let textLines = 0
32
+
33
+ for (const line of lines) {
34
+ if (CODE_PATTERNS.test(line)) codeLines++
35
+ else if (line.trim()) textLines++
36
+ }
37
+
38
+ if (codeLines > textLines * 2) return 'code'
39
+ if (textLines > codeLines * 2) return 'text'
40
+ return 'mixed'
41
+ }
42
+
43
+ function estimateTokens(text: string): number {
44
+ return Math.ceil(text.length / 4)
45
+ }
46
+
47
+ function removeComments(content: string): string {
48
+ return content.replace(COMMENT_PATTERNS, '')
49
+ }
50
+
51
+ function collapseWhitespace(content: string): string {
52
+ return content
53
+ .replace(TRAILING_WS, '')
54
+ .replace(WHITESPACE_MULTI, '\n\n')
55
+ }
56
+
57
+ function removeConsoleLogs(content: string): string {
58
+ return content.replace(CONSOLE_LOG, '')
59
+ }
60
+
61
+ function removeTypeImports(content: string): string {
62
+ return content.replace(TYPE_IMPORTS, '')
63
+ }
64
+
65
+ function collapseEmptyBlocks(content: string): string {
66
+ return content.replace(EMPTY_BLOCKS, '{}')
67
+ }
68
+
69
+ function removeEmptyLines(content: string): string {
70
+ return content.replace(EMPTY_LINES, '')
71
+ }
72
+
73
+ function smartTruncate(content: string, targetTokens: number): string {
74
+ const currentTokens = estimateTokens(content)
75
+ if (currentTokens <= targetTokens) return content
76
+
77
+ const ratio = targetTokens / currentTokens
78
+ const targetChars = Math.floor(content.length * ratio)
79
+
80
+ const lines = content.split('\n')
81
+ const result: string[] = []
82
+ let charCount = 0
83
+
84
+ for (const line of lines) {
85
+ if (charCount + line.length > targetChars) break
86
+ result.push(line)
87
+ charCount += line.length + 1
88
+ }
89
+
90
+ return result.join('\n')
91
+ }
92
+
93
+ export function squeeze(ctx: SqueezeContext, level: CompressionLevel = 'balanced'): SqueezeResult {
94
+ const originalTokens = estimateTokens(ctx.content)
95
+ if (level === 'none') {
96
+ return { originalTokens, squeezedTokens: originalTokens, savingsPercent: 0, level: 'none', filters: [] }
97
+ }
98
+
99
+ let content = ctx.content
100
+ const contentType = classifyContent(content)
101
+ const appliedFilters: string[] = []
102
+
103
+ if (level === 'light') {
104
+ content = collapseWhitespace(content)
105
+ appliedFilters.push('collapse_whitespace')
106
+ }
107
+
108
+ if (level === 'balanced' || level === 'aggressive') {
109
+ content = collapseWhitespace(content)
110
+ appliedFilters.push('collapse_whitespace')
111
+
112
+ if (contentType === 'code' || contentType === 'mixed') {
113
+ content = removeComments(content)
114
+ appliedFilters.push('remove_comments')
115
+
116
+ content = removeConsoleLogs(content)
117
+ appliedFilters.push('remove_console_logs')
118
+ }
119
+
120
+ if (level === 'balanced') {
121
+ content = collapseEmptyBlocks(content)
122
+ appliedFilters.push('collapse_empty_blocks')
123
+ }
124
+ }
125
+
126
+ if (level === 'aggressive') {
127
+ if (contentType === 'code' || contentType === 'mixed') {
128
+ content = removeTypeImports(content)
129
+ appliedFilters.push('remove_type_imports')
130
+
131
+ content = removeEmptyLines(content)
132
+ appliedFilters.push('remove_empty_lines')
133
+ }
134
+
135
+ const contextRemaining = ctx.contextWindow - ctx.usedTokens
136
+ const targetTokens = Math.min(
137
+ ctx.targetTokens || contextRemaining,
138
+ contextRemaining,
139
+ )
140
+
141
+ if (estimateTokens(content) > targetTokens) {
142
+ content = smartTruncate(content, targetTokens)
143
+ appliedFilters.push('smart_truncate')
144
+ }
145
+ }
146
+
147
+ const squeezedTokens = estimateTokens(content)
148
+ const savingsPercent = originalTokens > 0 ? ((originalTokens - squeezedTokens) / originalTokens) * 100 : 0
149
+
150
+ return { originalTokens, squeezedTokens, savingsPercent, level, filters: appliedFilters }
151
+ }
152
+
153
+ export function selectCompressionLevel(contextFillRatio: number): CompressionLevel {
154
+ if (contextFillRatio < 0.5) return 'none'
155
+ if (contextFillRatio < 0.7) return 'light'
156
+ if (contextFillRatio < 0.85) return 'balanced'
157
+ return 'aggressive'
158
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,16 @@
1
+ import pino from 'pino'
2
+ import { getConfig } from '../config'
3
+
4
+ const config = getConfig()
5
+
6
+ export const logger = pino({
7
+ level: config.logLevel || 'info',
8
+ transport: {
9
+ target: 'pino/file',
10
+ options: { destination: 1 },
11
+ },
12
+ formatters: {
13
+ level: (label) => ({ level: label }),
14
+ },
15
+ timestamp: pino.stdTimeFunctions.isoTime,
16
+ })
@@ -0,0 +1,60 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { NextRequest } from 'next/server'
3
+
4
+ const rateLimitMap = new Map<string, { count: number; resetAt: number }>()
5
+ const WINDOW_MS = 60_000
6
+ const MAX_REQUESTS = 100
7
+
8
+ export function middleware(request: NextRequest) {
9
+ if (request.nextUrl.pathname.startsWith('/api/')) {
10
+ return handleApiMiddleware(request)
11
+ }
12
+ return NextResponse.next()
13
+ }
14
+
15
+ function handleApiMiddleware(request: NextRequest) {
16
+ const headers = new Headers()
17
+
18
+ headers.set('Access-Control-Allow-Origin', '*')
19
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
20
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key')
21
+ headers.set('Access-Control-Max-Age', '86400')
22
+
23
+ if (request.method === 'OPTIONS') {
24
+ return new NextResponse(null, { status: 204, headers })
25
+ }
26
+
27
+ headers.set('X-Request-Id', crypto.randomUUID())
28
+
29
+ const clientIp = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
30
+ const key = `${clientIp}:${request.nextUrl.pathname}`
31
+ const now = Date.now()
32
+
33
+ const entry = rateLimitMap.get(key)
34
+ if (!entry || now > entry.resetAt) {
35
+ rateLimitMap.set(key, { count: 1, resetAt: now + WINDOW_MS })
36
+ } else {
37
+ entry.count++
38
+ if (entry.count > MAX_REQUESTS) {
39
+ headers.set('Retry-After', String(Math.ceil((entry.resetAt - now) / 1000)))
40
+ return NextResponse.json(
41
+ { error: { type: 'rate_limit_exceeded', message: 'Too many requests' } },
42
+ { status: 429, headers },
43
+ )
44
+ }
45
+ }
46
+
47
+ headers.set('X-RateLimit-Limit', String(MAX_REQUESTS))
48
+ headers.set('X-RateLimit-Remaining', String(Math.max(0, MAX_REQUESTS - (entry?.count || 1))))
49
+
50
+ const response = NextResponse.next()
51
+ for (const [k, v] of headers.entries()) {
52
+ response.headers.set(k, v)
53
+ }
54
+
55
+ return response
56
+ }
57
+
58
+ export const config = {
59
+ matcher: ['/api/:path*'],
60
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }