lonny-agent 0.2.4 → 0.2.6

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 (102) hide show
  1. package/README.md +326 -326
  2. package/dist/agent/llm.d.ts +4 -0
  3. package/dist/agent/llm.d.ts.map +1 -1
  4. package/dist/agent/prompt-builder.d.ts.map +1 -1
  5. package/dist/agent/prompt-builder.js +13 -9
  6. package/dist/agent/prompt-builder.js.map +1 -1
  7. package/dist/agent/providers/google.d.ts.map +1 -1
  8. package/dist/agent/providers/google.js.map +1 -1
  9. package/dist/agent/providers/openai.d.ts.map +1 -1
  10. package/dist/agent/providers/openai.js +43 -3
  11. package/dist/agent/providers/openai.js.map +1 -1
  12. package/dist/agent/session.d.ts +4 -0
  13. package/dist/agent/session.d.ts.map +1 -1
  14. package/dist/agent/session.js +45 -7
  15. package/dist/agent/session.js.map +1 -1
  16. package/dist/config/index.d.ts.map +1 -1
  17. package/dist/config/index.js +7 -1
  18. package/dist/config/index.js.map +1 -1
  19. package/dist/tools/__tests__/bash.test.js +1 -1
  20. package/dist/tools/__tests__/bash.test.js.map +1 -1
  21. package/dist/tools/__tests__/edit.test.js +875 -371
  22. package/dist/tools/__tests__/edit.test.js.map +1 -1
  23. package/dist/tools/__tests__/grep.test.js +32 -0
  24. package/dist/tools/__tests__/grep.test.js.map +1 -1
  25. package/dist/tools/__tests__/sed.test.d.ts +2 -0
  26. package/dist/tools/__tests__/sed.test.d.ts.map +1 -0
  27. package/dist/tools/__tests__/sed.test.js +228 -0
  28. package/dist/tools/__tests__/sed.test.js.map +1 -0
  29. package/dist/tools/bash.d.ts.map +1 -1
  30. package/dist/tools/bash.js +83 -10
  31. package/dist/tools/bash.js.map +1 -1
  32. package/dist/tools/edit.d.ts +50 -9
  33. package/dist/tools/edit.d.ts.map +1 -1
  34. package/dist/tools/edit.js +328 -158
  35. package/dist/tools/edit.js.map +1 -1
  36. package/dist/tools/grep.d.ts.map +1 -1
  37. package/dist/tools/grep.js +54 -30
  38. package/dist/tools/grep.js.map +1 -1
  39. package/dist/tools/registry.d.ts +0 -6
  40. package/dist/tools/registry.d.ts.map +1 -1
  41. package/dist/tools/registry.js +2 -29
  42. package/dist/tools/registry.js.map +1 -1
  43. package/dist/tools/sed.d.ts +4 -0
  44. package/dist/tools/sed.d.ts.map +1 -0
  45. package/dist/tools/sed.js +121 -0
  46. package/dist/tools/sed.js.map +1 -0
  47. package/dist/web/index.d.ts.map +1 -1
  48. package/dist/web/index.js +26 -3
  49. package/dist/web/index.js.map +1 -1
  50. package/dist/web/public/app.js +5 -855
  51. package/dist/web/public/confirm.js +70 -0
  52. package/dist/web/public/index.html +121 -120
  53. package/dist/web/public/input.js +107 -0
  54. package/dist/web/public/messages.js +395 -0
  55. package/dist/web/public/sidebar.js +82 -0
  56. package/dist/web/public/state.js +72 -0
  57. package/dist/web/public/style.css +996 -949
  58. package/dist/web/public/utils.js +37 -0
  59. package/dist/web/public/websocket.js +267 -0
  60. package/dist/web/public/ws.js +19 -0
  61. package/dist/web/session-bridge.d.ts.map +1 -1
  62. package/dist/web/session-bridge.js +3 -7
  63. package/dist/web/session-bridge.js.map +1 -1
  64. package/package.json +55 -54
  65. package/scripts/copy-native.mjs +24 -0
  66. package/scripts/copy-web.mjs +15 -0
  67. package/src/agent/index.ts +25 -25
  68. package/src/agent/llm.ts +4 -0
  69. package/src/agent/prompt-builder.ts +13 -9
  70. package/src/agent/providers/anthropic.ts +179 -179
  71. package/src/agent/providers/google.ts +210 -211
  72. package/src/agent/providers/ollama.ts +186 -186
  73. package/src/agent/providers/openai.ts +61 -5
  74. package/src/agent/session.ts +59 -3
  75. package/src/config/index.ts +342 -335
  76. package/src/tools/__tests__/bash.test.ts +1 -1
  77. package/src/tools/__tests__/edit.test.ts +981 -385
  78. package/src/tools/__tests__/grep.test.ts +35 -0
  79. package/src/tools/bash.ts +97 -11
  80. package/src/tools/edit.ts +362 -169
  81. package/src/tools/git.ts +76 -76
  82. package/src/tools/grep.ts +57 -30
  83. package/src/tools/registry.ts +2 -31
  84. package/src/tui/index.ts +786 -786
  85. package/src/web/index.ts +25 -3
  86. package/src/web/public/app.js +5 -855
  87. package/src/web/public/confirm.js +70 -0
  88. package/src/web/public/index.html +121 -120
  89. package/src/web/public/input.js +107 -0
  90. package/src/web/public/messages.js +395 -0
  91. package/src/web/public/sidebar.js +82 -0
  92. package/src/web/public/state.js +72 -0
  93. package/src/web/public/style.css +996 -949
  94. package/src/web/public/utils.js +37 -0
  95. package/src/web/public/websocket.js +267 -0
  96. package/src/web/public/ws.js +19 -0
  97. package/src/web/session-bridge.ts +193 -194
  98. package/.claude/settings.local.json +0 -14
  99. package/.kilo/plans/1780064105789-mighty-pixel.md +0 -171
  100. package/.kilo/plans/1780293725888-quick-wizard.md +0 -62
  101. package/.lonny/plan-web-cwd-status.md +0 -38
  102. package/src/tools/exec.ts +0 -348
@@ -1,335 +1,342 @@
1
- import * as fs from 'node:fs'
2
- import * as os from 'node:os'
3
- import * as path from 'node:path'
4
-
5
- export interface Config {
6
- apiKey: string
7
- baseUrl?: string
8
- mode: 'code' | 'plan' | 'ask'
9
- provider: 'openai' | 'anthropic' | 'google' | 'ollama'
10
- model: string
11
- contextWindow: number
12
- cwd: string
13
- autoApprove: boolean
14
- thinking?: boolean
15
- reasoningEffort?: string
16
- enableCache?: boolean
17
- strictTools?: boolean
18
- temperature?: number
19
- maxTokens?: number
20
- tavilyApiKey?: string
21
- }
22
-
23
- /**
24
- * 模型上下文窗口大小映射表(基于 2026 年最新数据)
25
- * 单位:tokens
26
- */
27
- const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
28
- // Doubao 系列
29
- 'doubao-seed-2.0-code': 256_000,
30
- 'doubao-seed-2.0-pro': 256_000,
31
- 'doubao-seed-2.0-lite': 256_000,
32
- 'doubao-seed-2.0-mini': 256_000,
33
- 'doubao-seed-2.0-code-preview-260215': 256_000,
34
- 'doubao-seed-1.8': 256_000,
35
- 'doubao-seed-1.6': 256_000,
36
-
37
- // DeepSeek 系列
38
- 'deepseek-v4-pro': 1_000_000,
39
- 'deepseek-v4-flash': 256_000,
40
- 'deepseek-v3.2': 160_000,
41
- 'deepseek-r1': 128_000,
42
- 'deepseek-chat': 128_000,
43
- 'deepseek-reasoner': 128_000,
44
-
45
- // Kimi 系列
46
- 'kimi-k2.5': 262_144,
47
- 'kimi-k2.6': 262_144,
48
- 'kimi-k2-thinking': 256_000,
49
-
50
- // MiniMax 系列
51
- 'minimax-m2.5': 200_000,
52
- 'minimax-m2.7': 200_000,
53
- 'minimax-m2': 205_000,
54
-
55
- // GLM 系列
56
- 'glm-5': 200_000,
57
- 'glm-5.1': 200_000,
58
- 'glm-4.7-flashx': 200_000,
59
- 'glm-4-flash': 128_000,
60
- 'glm-4': 128_000,
61
-
62
- // Qwen 系列
63
- 'qwen3.6-plus': 1_000_000,
64
- 'qwen3.7-max': 1_000_000,
65
- 'qwen3-max': 256_000,
66
- 'qwen3.5-plus': 1_000_000,
67
- 'qwen-flash': 1_000_000,
68
-
69
- // Claude 系列
70
- 'claude-opus-4.7': 1_000_000,
71
- 'claude-opus-4.6': 1_000_000,
72
- 'claude-sonnet-4.6': 1_000_000,
73
- 'claude-haiku-4.5': 200_000,
74
-
75
- // GPT 系列
76
- 'gpt-5.5': 1_000_000,
77
- 'gpt-5.4': 1_000_000,
78
- 'gpt-5.1': 1_000_000,
79
- 'gpt-5.2-codex': 512_000,
80
- 'gpt-4.1': 1_000_000,
81
- 'gpt-4.1-mini': 1_000_000,
82
- 'gpt-4o': 128_000,
83
- 'gpt-4': 128_000,
84
-
85
- // Gemini 系列
86
- 'gemini-2.5-pro': 1_000_000,
87
- 'gemini-2.5-flash': 1_000_000,
88
- 'gemini-3.1-pro': 1_000_000,
89
- 'gemini-3.1-flash': 1_000_000,
90
-
91
- // Llama 系列
92
- 'llama-4-maverick': 1_000_000,
93
- 'llama-4-scout': 10_000_000,
94
- 'llama-3.1': 128_000,
95
- 'llama-3': 128_000,
96
-
97
- // Mistral 系列
98
- 'mistral-small-3.1': 128_000,
99
- 'mistral-small-3.2': 128_000,
100
- }
101
-
102
- /**
103
- * 模糊匹配规则
104
- */
105
- interface FuzzyMatchRule {
106
- keywords: string[]
107
- windowSize: number
108
- }
109
-
110
- /**
111
- * 模糊匹配规则列表(从高优先级到低优先级)
112
- */
113
- const fuzzyMatchRules: FuzzyMatchRule[] = [
114
- // 首先匹配 128K 模型(防止 gpt-4 匹配到 gpt-5 规则)
115
- {
116
- keywords: [
117
- 'gpt-4o',
118
- 'gpt 4o',
119
- 'gpt-4',
120
- 'gpt 4',
121
- 'llama-3',
122
- 'llama 3',
123
- 'mistral-small',
124
- 'mistral small',
125
- 'glm-4-flash',
126
- 'glm 4 flash',
127
- 'glm-4',
128
- ],
129
- windowSize: 128_000,
130
- },
131
- // 百万上下文模型 - 更高优先级(更精确的关键词)
132
- {
133
- keywords: [
134
- 'deepseek-v4',
135
- 'deepseek v4',
136
- 'qwen3.6',
137
- 'qwen 3.6',
138
- 'qwen3.7',
139
- 'qwen 3.7',
140
- 'qwen3.5',
141
- 'qwen 3.5',
142
- 'qwen-flash',
143
- 'qwen flash',
144
- ],
145
- windowSize: 1_000_000,
146
- },
147
- {
148
- keywords: [
149
- 'claude-opus',
150
- 'claude sonnet',
151
- 'claude-sonnet',
152
- 'gpt-5.',
153
- 'gpt 5',
154
- 'gpt-4.1',
155
- 'gpt 4.1',
156
- 'gemini-2.5',
157
- 'gemini 2.5',
158
- 'gemini-3.1',
159
- 'gemini 3.1',
160
- 'llama-4',
161
- 'llama 4',
162
- ],
163
- windowSize: 1_000_000,
164
- },
165
- // 256K 上下文模型
166
- {
167
- keywords: [
168
- 'doubao-seed-2.0',
169
- 'doubao seed 2.0',
170
- 'doubao-seed-1',
171
- 'doubao seed 1',
172
- 'kimi-k2',
173
- 'kimi k2',
174
- 'kimi-k2.5',
175
- 'kimi k2.5',
176
- 'kimi-k2.6',
177
- 'kimi k2.6',
178
- 'qwen3',
179
- 'qwen 3',
180
- 'qwen3-max',
181
- 'qwen3 max',
182
- ],
183
- windowSize: 256_000,
184
- },
185
- // 200K 上下文模型
186
- {
187
- keywords: [
188
- 'minimax',
189
- 'glm-5',
190
- 'glm 5',
191
- 'glm-5.1',
192
- 'glm 5.1',
193
- 'glm-4.7',
194
- 'glm 4.7',
195
- 'claude-haiku',
196
- 'claude haiku',
197
- ],
198
- windowSize: 200_000,
199
- },
200
- // 160K 上下文模型
201
- {
202
- keywords: ['deepseek-v3', 'deepseek v3', 'deepseek-v3.2', 'deepseek v3.2'],
203
- windowSize: 160_000,
204
- },
205
- // 最后匹配通用关键词(deepseek)
206
- {
207
- keywords: ['deepseek'],
208
- windowSize: 128_000,
209
- },
210
- ]
211
-
212
- /**
213
- * 根据模型名称获取上下文窗口大小(改进的模糊匹配)
214
- * @param modelName 模型名称(不区分大小写)
215
- * @returns 上下文窗口大小(tokens)
216
- */
217
- export function getContextWindowForModel(modelName: string): number {
218
- // 处理空字符串
219
- if (!modelName) {
220
- return 128_000
221
- }
222
-
223
- const normalizedModelName = modelName.toLowerCase().replace(/[-_]/g, ' ')
224
-
225
- // 1. 精确匹配(原始键名)
226
- if (MODEL_CONTEXT_WINDOWS[modelName]) {
227
- return MODEL_CONTEXT_WINDOWS[modelName]
228
- }
229
-
230
- // 2. 精确匹配(小写键名)
231
- if (MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]) {
232
- return MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]
233
- }
234
-
235
- // 3. 前缀匹配(支持部分匹配)
236
- for (const [pattern, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
237
- const normalizedPattern = pattern.toLowerCase().replace(/[-_]/g, ' ')
238
- if (
239
- normalizedModelName.includes(normalizedPattern) ||
240
- normalizedPattern.includes(normalizedModelName)
241
- ) {
242
- return size
243
- }
244
- }
245
-
246
- // 4. 模糊匹配(关键词匹配)- 按照规则顺序(优先级)处理
247
- for (const rule of fuzzyMatchRules) {
248
- for (const keyword of rule.keywords) {
249
- const normalizedKeyword = keyword.toLowerCase().replace(/[-_]/g, ' ')
250
- if (normalizedModelName.includes(normalizedKeyword)) {
251
- return rule.windowSize
252
- }
253
- }
254
- }
255
-
256
- // 5. 兜底默认值
257
- return 128_000
258
- }
259
-
260
- interface JsonConfig {
261
- apiKey?: string
262
- baseUrl?: string
263
- provider?: string
264
- model?: string
265
- thinking?: boolean
266
- reasoningEffort?: string
267
- autoApprove?: boolean
268
- enableCache?: boolean
269
- strictTools?: boolean
270
- contextWindow?: number
271
- temperature?: number
272
- maxTokens?: number
273
- tavilyApiKey?: string
274
- }
275
-
276
- let cachedJsonConfig: JsonConfig | null = null
277
-
278
- function loadJsonConfig(): JsonConfig {
279
- if (cachedJsonConfig) return cachedJsonConfig
280
- const configPath = path.join(os.homedir(), '.lonny', 'config.json')
281
- try {
282
- const raw = fs.readFileSync(configPath, 'utf-8')
283
- cachedJsonConfig = JSON.parse(raw) as JsonConfig
284
- return cachedJsonConfig
285
- } catch {
286
- cachedJsonConfig = {}
287
- return cachedJsonConfig
288
- }
289
- }
290
-
291
- /** Check if the model is a DeepSeek model (supports enable_cache). */
292
- function isDeepSeekModel(model: string, baseUrl?: string): boolean {
293
- if (/deepseek/i.test(model)) return true
294
- if (baseUrl && /deepseek/i.test(baseUrl)) return true
295
- return false
296
- }
297
-
298
- export function loadConfig(options?: {
299
- mode?: 'code' | 'plan' | 'ask'
300
- autoApprove?: boolean
301
- cwd?: string
302
- }): Config {
303
- const jsonConfig = loadJsonConfig()
304
-
305
- const model = jsonConfig.model || 'deepseek-v4-flash'
306
- const baseUrl = process.env.LONNY_BASE_URL || jsonConfig.baseUrl || undefined
307
-
308
- // Auto-enable cache for DeepSeek models unless explicitly disabled
309
- const enableCache = jsonConfig.enableCache ?? (isDeepSeekModel(model, baseUrl) || undefined)
310
-
311
- // 根据模型名称获取上下文窗口大小,如果配置中有则优先使用配置值
312
- const contextWindow = jsonConfig.contextWindow || getContextWindowForModel(model)
313
-
314
- return {
315
- apiKey: jsonConfig.apiKey || '',
316
- baseUrl,
317
- provider: (process.env.LONNY_PROVIDER || jsonConfig.provider || 'openai') as
318
- | 'openai'
319
- | 'anthropic'
320
- | 'google'
321
- | 'ollama',
322
- mode: options?.mode || 'code',
323
- model,
324
- contextWindow,
325
- cwd: options?.cwd || process.cwd(),
326
- autoApprove: options?.autoApprove ?? jsonConfig.autoApprove ?? false,
327
- thinking: jsonConfig.thinking,
328
- reasoningEffort: jsonConfig.reasoningEffort,
329
- enableCache,
330
- strictTools: jsonConfig.strictTools,
331
- temperature: jsonConfig.temperature,
332
- maxTokens: jsonConfig.maxTokens,
333
- tavilyApiKey: jsonConfig.tavilyApiKey,
334
- }
335
- }
1
+ import * as fs from 'node:fs'
2
+ import * as os from 'node:os'
3
+ import * as path from 'node:path'
4
+
5
+ export interface Config {
6
+ apiKey: string
7
+ baseUrl?: string
8
+ mode: 'code' | 'plan' | 'ask'
9
+ provider: 'openai' | 'anthropic' | 'google' | 'ollama'
10
+ model: string
11
+ contextWindow: number
12
+ cwd: string
13
+ autoApprove: boolean
14
+ thinking?: boolean
15
+ reasoningEffort?: string
16
+ enableCache?: boolean
17
+ strictTools?: boolean
18
+ temperature?: number
19
+ maxTokens?: number
20
+ tavilyApiKey?: string
21
+ }
22
+
23
+ /**
24
+ * 模型上下文窗口大小映射表(基于 2026 年最新数据)
25
+ * 单位:tokens
26
+ */
27
+ const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
28
+ // Doubao 系列
29
+ 'doubao-seed-2.0-code': 256_000,
30
+ 'doubao-seed-2.0-pro': 256_000,
31
+ 'doubao-seed-2.0-lite': 256_000,
32
+ 'doubao-seed-2.0-mini': 256_000,
33
+ 'doubao-seed-2.0-code-preview-260215': 256_000,
34
+ 'doubao-seed-1.8': 256_000,
35
+ 'doubao-seed-1.6': 256_000,
36
+
37
+ // DeepSeek 系列
38
+ 'deepseek-v4-pro': 1_000_000,
39
+ 'deepseek-v4-flash': 256_000,
40
+ 'deepseek-v3.2': 160_000,
41
+ 'deepseek-r1': 128_000,
42
+ 'deepseek-chat': 128_000,
43
+ 'deepseek-reasoner': 128_000,
44
+
45
+ // Kimi 系列
46
+ 'kimi-k2.5': 262_144,
47
+ 'kimi-k2.6': 262_144,
48
+ 'kimi-k2-thinking': 256_000,
49
+
50
+ // MiniMax 系列
51
+ 'minimax-m2.5': 200_000,
52
+ 'minimax-m2.7': 200_000,
53
+ 'minimax-m2': 205_000,
54
+
55
+ // GLM 系列
56
+ 'glm-5': 200_000,
57
+ 'glm-5.1': 200_000,
58
+ 'glm-4.7-flashx': 200_000,
59
+ 'glm-4-flash': 128_000,
60
+ 'glm-4': 128_000,
61
+
62
+ // Qwen 系列
63
+ 'qwen3.6-plus': 1_000_000,
64
+ 'qwen3.7-max': 1_000_000,
65
+ 'qwen3-max': 256_000,
66
+ 'qwen3.5-plus': 1_000_000,
67
+ 'qwen-flash': 1_000_000,
68
+
69
+ // Claude 系列
70
+ 'claude-opus-4.7': 1_000_000,
71
+ 'claude-opus-4.6': 1_000_000,
72
+ 'claude-sonnet-4.6': 1_000_000,
73
+ 'claude-haiku-4.5': 200_000,
74
+
75
+ // GPT 系列
76
+ 'gpt-5.5': 1_000_000,
77
+ 'gpt-5.4': 1_000_000,
78
+ 'gpt-5.1': 1_000_000,
79
+ 'gpt-5.2-codex': 512_000,
80
+ 'gpt-4.1': 1_000_000,
81
+ 'gpt-4.1-mini': 1_000_000,
82
+ 'gpt-4o': 128_000,
83
+ 'gpt-4': 128_000,
84
+
85
+ // Gemini 系列
86
+ 'gemini-2.5-pro': 1_000_000,
87
+ 'gemini-2.5-flash': 1_000_000,
88
+ 'gemini-3.1-pro': 1_000_000,
89
+ 'gemini-3.1-flash': 1_000_000,
90
+
91
+ // Llama 系列
92
+ 'llama-4-maverick': 1_000_000,
93
+ 'llama-4-scout': 10_000_000,
94
+ 'llama-3.1': 128_000,
95
+ 'llama-3': 128_000,
96
+
97
+ // Mistral 系列
98
+ 'mistral-small-3.1': 128_000,
99
+ 'mistral-small-3.2': 128_000,
100
+ }
101
+
102
+ /**
103
+ * 模糊匹配规则
104
+ */
105
+ interface FuzzyMatchRule {
106
+ keywords: string[]
107
+ windowSize: number
108
+ }
109
+
110
+ /**
111
+ * 模糊匹配规则列表(从高优先级到低优先级)
112
+ */
113
+ const fuzzyMatchRules: FuzzyMatchRule[] = [
114
+ // 首先匹配 128K 模型(防止 gpt-4 匹配到 gpt-5 规则)
115
+ {
116
+ keywords: [
117
+ 'gpt-4o',
118
+ 'gpt 4o',
119
+ 'gpt-4',
120
+ 'gpt 4',
121
+ 'llama-3',
122
+ 'llama 3',
123
+ 'mistral-small',
124
+ 'mistral small',
125
+ 'glm-4-flash',
126
+ 'glm 4 flash',
127
+ 'glm-4',
128
+ ],
129
+ windowSize: 128_000,
130
+ },
131
+ // 百万上下文模型 - 更高优先级(更精确的关键词)
132
+ {
133
+ keywords: [
134
+ 'deepseek-v4',
135
+ 'deepseek v4',
136
+ 'qwen3.6',
137
+ 'qwen 3.6',
138
+ 'qwen3.7',
139
+ 'qwen 3.7',
140
+ 'qwen3.5',
141
+ 'qwen 3.5',
142
+ 'qwen-flash',
143
+ 'qwen flash',
144
+ ],
145
+ windowSize: 1_000_000,
146
+ },
147
+ {
148
+ keywords: [
149
+ 'claude-opus',
150
+ 'claude sonnet',
151
+ 'claude-sonnet',
152
+ 'gpt-5.',
153
+ 'gpt 5',
154
+ 'gpt-4.1',
155
+ 'gpt 4.1',
156
+ 'gemini-2.5',
157
+ 'gemini 2.5',
158
+ 'gemini-3.1',
159
+ 'gemini 3.1',
160
+ 'llama-4',
161
+ 'llama 4',
162
+ ],
163
+ windowSize: 1_000_000,
164
+ },
165
+ // 256K 上下文模型
166
+ {
167
+ keywords: [
168
+ 'doubao-seed-2.0',
169
+ 'doubao seed 2.0',
170
+ 'doubao-seed-1',
171
+ 'doubao seed 1',
172
+ 'kimi-k2',
173
+ 'kimi k2',
174
+ 'kimi-k2.5',
175
+ 'kimi k2.5',
176
+ 'kimi-k2.6',
177
+ 'kimi k2.6',
178
+ 'qwen3',
179
+ 'qwen 3',
180
+ 'qwen3-max',
181
+ 'qwen3 max',
182
+ ],
183
+ windowSize: 256_000,
184
+ },
185
+ // 200K 上下文模型
186
+ {
187
+ keywords: [
188
+ 'minimax',
189
+ 'glm-5',
190
+ 'glm 5',
191
+ 'glm-5.1',
192
+ 'glm 5.1',
193
+ 'glm-4.7',
194
+ 'glm 4.7',
195
+ 'claude-haiku',
196
+ 'claude haiku',
197
+ ],
198
+ windowSize: 200_000,
199
+ },
200
+ // 160K 上下文模型
201
+ {
202
+ keywords: ['deepseek-v3', 'deepseek v3', 'deepseek-v3.2', 'deepseek v3.2'],
203
+ windowSize: 160_000,
204
+ },
205
+ // 最后匹配通用关键词(deepseek)
206
+ {
207
+ keywords: ['deepseek'],
208
+ windowSize: 128_000,
209
+ },
210
+ ]
211
+
212
+ /**
213
+ * 根据模型名称获取上下文窗口大小(改进的模糊匹配)
214
+ * @param modelName 模型名称(不区分大小写)
215
+ * @returns 上下文窗口大小(tokens)
216
+ */
217
+ export function getContextWindowForModel(modelName: string): number {
218
+ // 处理空字符串
219
+ if (!modelName) {
220
+ return 128_000
221
+ }
222
+
223
+ const normalizedModelName = modelName.toLowerCase().replace(/[-_]/g, ' ')
224
+
225
+ // 1. 精确匹配(原始键名)
226
+ if (MODEL_CONTEXT_WINDOWS[modelName]) {
227
+ return MODEL_CONTEXT_WINDOWS[modelName]
228
+ }
229
+
230
+ // 2. 精确匹配(小写键名)
231
+ if (MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]) {
232
+ return MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]
233
+ }
234
+
235
+ // 3. 前缀匹配(支持部分匹配)
236
+ for (const [pattern, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
237
+ const normalizedPattern = pattern.toLowerCase().replace(/[-_]/g, ' ')
238
+ if (
239
+ normalizedModelName.includes(normalizedPattern) ||
240
+ normalizedPattern.includes(normalizedModelName)
241
+ ) {
242
+ return size
243
+ }
244
+ }
245
+
246
+ // 4. 模糊匹配(关键词匹配)- 按照规则顺序(优先级)处理
247
+ for (const rule of fuzzyMatchRules) {
248
+ for (const keyword of rule.keywords) {
249
+ const normalizedKeyword = keyword.toLowerCase().replace(/[-_]/g, ' ')
250
+ if (normalizedModelName.includes(normalizedKeyword)) {
251
+ return rule.windowSize
252
+ }
253
+ }
254
+ }
255
+
256
+ // 5. 兜底默认值
257
+ return 128_000
258
+ }
259
+
260
+ interface JsonConfig {
261
+ apiKey?: string
262
+ baseUrl?: string
263
+ provider?: string
264
+ model?: string
265
+ thinking?: boolean
266
+ reasoningEffort?: string
267
+ autoApprove?: boolean
268
+ enableCache?: boolean
269
+ strictTools?: boolean
270
+ contextWindow?: number
271
+ temperature?: number
272
+ maxTokens?: number
273
+ tavilyApiKey?: string
274
+ }
275
+
276
+ let cachedJsonConfig: JsonConfig | null = null
277
+
278
+ function loadJsonConfig(): JsonConfig {
279
+ if (cachedJsonConfig) return cachedJsonConfig
280
+ const configPath = path.join(os.homedir(), '.lonny', 'config.json')
281
+ try {
282
+ const raw = fs.readFileSync(configPath, 'utf-8')
283
+ cachedJsonConfig = JSON.parse(raw) as JsonConfig
284
+ return cachedJsonConfig
285
+ } catch {
286
+ cachedJsonConfig = {}
287
+ return cachedJsonConfig
288
+ }
289
+ }
290
+
291
+ function saveJsonConfig(config: JsonConfig): void {
292
+ const configPath = path.join(os.homedir(), '.lonny', 'config.json')
293
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8')
294
+ cachedJsonConfig = config
295
+ }
296
+
297
+ /** Check if the model is a DeepSeek model (supports enable_cache). */
298
+ function isDeepSeekModel(model: string, baseUrl?: string): boolean {
299
+ if (/deepseek/i.test(model)) return true
300
+ if (baseUrl && /deepseek/i.test(baseUrl)) return true
301
+ return false
302
+ }
303
+
304
+ export function loadConfig(options?: {
305
+ mode?: 'code' | 'plan' | 'ask'
306
+ autoApprove?: boolean
307
+ cwd?: string
308
+ }): Config {
309
+ const jsonConfig = loadJsonConfig()
310
+
311
+ const model = jsonConfig.model || 'deepseek-v4-flash'
312
+ const baseUrl = process.env.LONNY_BASE_URL || jsonConfig.baseUrl || undefined
313
+ const provider = (process.env.LONNY_PROVIDER || jsonConfig.provider || 'openai') as
314
+ | 'openai'
315
+ | 'anthropic'
316
+ | 'google'
317
+ | 'ollama'
318
+
319
+ // Auto-enable cache for DeepSeek models unless explicitly disabled
320
+ const enableCache = jsonConfig.enableCache ?? (isDeepSeekModel(model, baseUrl) || undefined)
321
+
322
+ // 根据模型名称获取上下文窗口大小,如果配置中有则优先使用配置值
323
+ const contextWindow = jsonConfig.contextWindow || getContextWindowForModel(model)
324
+
325
+ return {
326
+ apiKey: jsonConfig.apiKey || '',
327
+ baseUrl,
328
+ provider,
329
+ mode: options?.mode || 'code',
330
+ model,
331
+ contextWindow,
332
+ cwd: options?.cwd || process.cwd(),
333
+ autoApprove: options?.autoApprove ?? jsonConfig.autoApprove ?? false,
334
+ thinking: jsonConfig.thinking,
335
+ reasoningEffort: jsonConfig.reasoningEffort,
336
+ enableCache,
337
+ strictTools: jsonConfig.strictTools,
338
+ temperature: jsonConfig.temperature,
339
+ maxTokens: jsonConfig.maxTokens,
340
+ tavilyApiKey: jsonConfig.tavilyApiKey,
341
+ }
342
+ }