@yivan-lab/pretty-please 1.0.0 → 1.1.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 (64) hide show
  1. package/README.md +98 -27
  2. package/bin/pls.tsx +135 -24
  3. package/dist/bin/pls.d.ts +1 -1
  4. package/dist/bin/pls.js +117 -24
  5. package/dist/package.json +80 -0
  6. package/dist/src/ai.d.ts +1 -41
  7. package/dist/src/ai.js +9 -190
  8. package/dist/src/builtin-detector.d.ts +14 -8
  9. package/dist/src/builtin-detector.js +36 -16
  10. package/dist/src/chat-history.d.ts +16 -11
  11. package/dist/src/chat-history.js +26 -4
  12. package/dist/src/components/Chat.js +3 -3
  13. package/dist/src/components/CommandBox.js +1 -16
  14. package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
  15. package/dist/src/components/ConfirmationPrompt.js +7 -3
  16. package/dist/src/components/MultiStepCommandGenerator.d.ts +2 -0
  17. package/dist/src/components/MultiStepCommandGenerator.js +110 -7
  18. package/dist/src/config.d.ts +27 -8
  19. package/dist/src/config.js +92 -33
  20. package/dist/src/history.d.ts +19 -5
  21. package/dist/src/history.js +26 -11
  22. package/dist/src/mastra-agent.d.ts +0 -1
  23. package/dist/src/mastra-agent.js +3 -4
  24. package/dist/src/mastra-chat.d.ts +28 -0
  25. package/dist/src/mastra-chat.js +93 -0
  26. package/dist/src/multi-step.d.ts +2 -2
  27. package/dist/src/multi-step.js +2 -2
  28. package/dist/src/prompts.d.ts +11 -0
  29. package/dist/src/prompts.js +140 -0
  30. package/dist/src/shell-hook.d.ts +35 -13
  31. package/dist/src/shell-hook.js +82 -7
  32. package/dist/src/sysinfo.d.ts +9 -5
  33. package/dist/src/sysinfo.js +2 -2
  34. package/dist/src/utils/console.d.ts +11 -11
  35. package/dist/src/utils/console.js +4 -6
  36. package/package.json +8 -6
  37. package/src/builtin-detector.ts +126 -0
  38. package/src/chat-history.ts +130 -0
  39. package/src/components/Chat.tsx +4 -4
  40. package/src/components/CommandBox.tsx +1 -16
  41. package/src/components/ConfirmationPrompt.tsx +9 -2
  42. package/src/components/MultiStepCommandGenerator.tsx +144 -7
  43. package/src/config.ts +309 -0
  44. package/src/history.ts +160 -0
  45. package/src/mastra-agent.ts +3 -4
  46. package/src/mastra-chat.ts +124 -0
  47. package/src/multi-step.ts +2 -2
  48. package/src/prompts.ts +154 -0
  49. package/src/shell-hook.ts +502 -0
  50. package/src/{sysinfo.js → sysinfo.ts} +28 -16
  51. package/src/utils/{console.js → console.ts} +16 -18
  52. package/bin/pls.js +0 -681
  53. package/src/ai.js +0 -324
  54. package/src/builtin-detector.js +0 -98
  55. package/src/chat-history.js +0 -94
  56. package/src/components/ChatStatus.tsx +0 -53
  57. package/src/components/CommandGenerator.tsx +0 -184
  58. package/src/components/ConfigDisplay.tsx +0 -64
  59. package/src/components/ConfigWizard.tsx +0 -101
  60. package/src/components/HistoryDisplay.tsx +0 -69
  61. package/src/components/HookManager.tsx +0 -150
  62. package/src/config.js +0 -221
  63. package/src/history.js +0 -131
  64. package/src/shell-hook.js +0 -393
@@ -6,16 +6,18 @@ interface ConfirmationPromptProps {
6
6
  prompt: string
7
7
  onConfirm: () => void
8
8
  onCancel: () => void
9
+ onEdit?: () => void // 新增:编辑回调
9
10
  }
10
11
 
11
12
  /**
12
13
  * ConfirmationPrompt 组件 - 单键确认提示
13
- * 回车 = 确认,Esc = 取消,Ctrl+C = 退出
14
+ * 回车 = 确认,E = 编辑,Esc = 取消,Ctrl+C = 退出
14
15
  */
15
16
  export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
16
17
  prompt,
17
18
  onConfirm,
18
19
  onCancel,
20
+ onEdit,
19
21
  }) => {
20
22
  useInput((input, key) => {
21
23
  if (key.return) {
@@ -24,6 +26,9 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
24
26
  } else if (key.escape) {
25
27
  // Esc 键
26
28
  onCancel()
29
+ } else if ((input === 'e' || input === 'E') && onEdit) {
30
+ // E 键进入编辑模式
31
+ onEdit()
27
32
  } else if (key.ctrl && input === 'c') {
28
33
  // Ctrl+C
29
34
  process.exit(0)
@@ -35,7 +40,9 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = ({
35
40
  <Text bold color={theme.warning}>
36
41
  {prompt}
37
42
  </Text>
38
- <Text color={theme.text.secondary}> [回车执行 / Esc 取消] </Text>
43
+ <Text color={theme.text.secondary}>
44
+ {onEdit ? ' [回车执行 / E 编辑 / Esc 取消] ' : ' [回车执行 / Esc 取消] '}
45
+ </Text>
39
46
  </Text>
40
47
  )
41
48
  }
@@ -1,5 +1,6 @@
1
1
  import React, { useState, useEffect } from 'react'
2
- import { Box, Text } from 'ink'
2
+ import { Box, Text, useInput } from 'ink'
3
+ import TextInput from 'ink-text-input'
3
4
  import Spinner from 'ink-spinner'
4
5
  import { generateMultiStepCommand, type CommandStep, type ExecutedStep } from '../multi-step.js'
5
6
  import { detectBuiltin, formatBuiltins } from '../builtin-detector.js'
@@ -7,12 +8,15 @@ import { CommandBox } from './CommandBox.js'
7
8
  import { ConfirmationPrompt } from './ConfirmationPrompt.js'
8
9
  import { Duration } from './Duration.js'
9
10
  import { theme } from '../ui/theme.js'
11
+ import { getConfig } from '../config.js'
10
12
 
11
13
  interface MultiStepCommandGeneratorProps {
12
14
  prompt: string
13
15
  debug?: boolean
14
16
  onStepComplete: (step: {
15
17
  command: string
18
+ aiGeneratedCommand?: string // 新增:AI 生成的原始命令
19
+ userModified?: boolean // 新增:用户是否修改
16
20
  confirmed: boolean
17
21
  cancelled?: boolean
18
22
  hasBuiltin?: boolean
@@ -29,6 +33,7 @@ interface MultiStepCommandGeneratorProps {
29
33
  type State =
30
34
  | { type: 'thinking' }
31
35
  | { type: 'showing_command'; stepData: CommandStep }
36
+ | { type: 'editing'; stepData: CommandStep } // 新增:编辑状态
32
37
  | { type: 'cancelled'; command: string }
33
38
  | { type: 'error'; error: string }
34
39
 
@@ -46,6 +51,17 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
46
51
  const [state, setState] = useState<State>({ type: 'thinking' })
47
52
  const [thinkDuration, setThinkDuration] = useState(0)
48
53
  const [debugInfo, setDebugInfo] = useState<any>(null)
54
+ const [editedCommand, setEditedCommand] = useState('') // 新增:编辑后的命令
55
+
56
+ // 监听编辑模式下的 Esc 键
57
+ useInput(
58
+ (input, key) => {
59
+ if (state.type === 'editing' && key.escape) {
60
+ handleEditCancel()
61
+ }
62
+ },
63
+ { isActive: state.type === 'editing' }
64
+ )
49
65
 
50
66
  // 初始化:调用 Mastra 生成命令
51
67
  useEffect(() => {
@@ -61,15 +77,29 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
61
77
  setDebugInfo(result.debugInfo)
62
78
  }
63
79
 
64
- setState({
65
- type: 'showing_command',
66
- stepData: result.stepData,
67
- })
80
+ // 如果 AI 返回空命令且决定不继续,说明 AI 放弃了
81
+ // 直接结束,不显示命令框
82
+ if (!result.stepData.command.trim() && result.stepData.continue === false) {
83
+ setTimeout(() => {
84
+ onStepComplete({
85
+ command: '',
86
+ confirmed: false,
87
+ reasoning: result.stepData.reasoning,
88
+ needsContinue: false,
89
+ })
90
+ }, 100)
91
+ return
92
+ }
68
93
 
69
- // 检测 builtin
94
+ // 检测 builtin(优先检测)
70
95
  const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command)
71
96
 
72
97
  if (hasBuiltin) {
98
+ // 有 builtin,不管什么模式都不编辑,直接提示
99
+ setState({
100
+ type: 'showing_command',
101
+ stepData: result.stepData,
102
+ })
73
103
  setTimeout(() => {
74
104
  onStepComplete({
75
105
  command: result.stepData.command,
@@ -80,6 +110,26 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
80
110
  needsContinue: result.stepData.continue,
81
111
  })
82
112
  }, 100)
113
+ return
114
+ }
115
+
116
+ // 根据 editMode 决定进入哪个状态
117
+ const config = getConfig()
118
+ const autoEdit = config.editMode === 'auto'
119
+
120
+ if (autoEdit) {
121
+ // auto 模式:直接进入编辑状态
122
+ setEditedCommand(result.stepData.command)
123
+ setState({
124
+ type: 'editing',
125
+ stepData: result.stepData,
126
+ })
127
+ } else {
128
+ // manual 模式:显示命令,等待用户操作
129
+ setState({
130
+ type: 'showing_command',
131
+ stepData: result.stepData,
132
+ })
83
133
  }
84
134
  })
85
135
  .catch((error: any) => {
@@ -99,6 +149,33 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
99
149
  if (state.type === 'showing_command') {
100
150
  onStepComplete({
101
151
  command: state.stepData.command,
152
+ aiGeneratedCommand: state.stepData.command, // 原始命令
153
+ userModified: false,
154
+ confirmed: true,
155
+ reasoning: state.stepData.reasoning,
156
+ needsContinue: state.stepData.continue,
157
+ nextStepHint: state.stepData.nextStepHint,
158
+ debugInfo: debugInfo,
159
+ })
160
+ }
161
+ }
162
+
163
+ // 处理编辑
164
+ const handleEdit = () => {
165
+ if (state.type === 'showing_command') {
166
+ setEditedCommand(state.stepData.command) // 初始化为 AI 生成的命令
167
+ setState({ type: 'editing', stepData: state.stepData })
168
+ }
169
+ }
170
+
171
+ // 编辑完成确认
172
+ const handleEditConfirm = () => {
173
+ if (state.type === 'editing') {
174
+ const modified = editedCommand !== state.stepData.command
175
+ onStepComplete({
176
+ command: editedCommand, // 使用编辑后的命令
177
+ aiGeneratedCommand: state.stepData.command, // 保存 AI 原始命令
178
+ userModified: modified,
102
179
  confirmed: true,
103
180
  reasoning: state.stepData.reasoning,
104
181
  needsContinue: state.stepData.continue,
@@ -108,6 +185,28 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
108
185
  }
109
186
  }
110
187
 
188
+ // 取消编辑
189
+ const handleEditCancel = () => {
190
+ if (state.type === 'editing') {
191
+ const config = getConfig()
192
+
193
+ if (config.editMode === 'auto') {
194
+ // auto 模式:Esc 直接取消整个操作
195
+ setState({ type: 'cancelled', command: state.stepData.command })
196
+ setTimeout(() => {
197
+ onStepComplete({
198
+ command: state.stepData.command,
199
+ confirmed: false,
200
+ cancelled: true,
201
+ })
202
+ }, 100)
203
+ } else {
204
+ // manual 模式:Esc 返回到 showing_command 状态
205
+ setState({ type: 'showing_command', stepData: state.stepData })
206
+ }
207
+ }
208
+ }
209
+
111
210
  // 处理取消
112
211
  const handleCancel = () => {
113
212
  if (state.type === 'showing_command') {
@@ -206,8 +305,46 @@ export const MultiStepCommandGenerator: React.FC<MultiStepCommandGeneratorProps>
206
305
 
207
306
  {/* 确认提示 */}
208
307
  {!detectBuiltin(state.stepData.command).hasBuiltin && (
209
- <ConfirmationPrompt prompt="执行?" onConfirm={handleConfirm} onCancel={handleCancel} />
308
+ <ConfirmationPrompt
309
+ prompt="执行?"
310
+ onConfirm={handleConfirm}
311
+ onCancel={handleCancel}
312
+ onEdit={handleEdit} // 新增:编辑回调
313
+ />
314
+ )}
315
+ </>
316
+ )}
317
+
318
+ {/* 编辑模式 */}
319
+ {state.type === 'editing' && (
320
+ <>
321
+ {/* 步骤信息(仅多步骤时显示) */}
322
+ {state.stepData.continue === true && (
323
+ <Box flexDirection="column" marginTop={1}>
324
+ <Text color={theme.text.secondary}>步骤 {currentStepNumber}/?</Text>
325
+ {state.stepData.reasoning && (
326
+ <Text color={theme.text.muted}>原因: {state.stepData.reasoning}</Text>
327
+ )}
328
+ </Box>
210
329
  )}
330
+
331
+ {/* 命令框(AI 建议) */}
332
+ <CommandBox command={state.stepData.command} />
333
+
334
+ {/* 编辑框 */}
335
+ <Box flexDirection="row">
336
+ <Text color={theme.primary}>{'> '}</Text>
337
+ <TextInput
338
+ value={editedCommand}
339
+ onChange={setEditedCommand}
340
+ onSubmit={handleEditConfirm}
341
+ />
342
+ </Box>
343
+ <Box marginTop={1}>
344
+ <Text color={theme.text.secondary}>
345
+ {getConfig().editMode === 'auto' ? '[回车执行 / Esc 取消]' : '[回车执行 / Esc 返回]'}
346
+ </Text>
347
+ </Box>
211
348
  </>
212
349
  )}
213
350
 
package/src/config.ts ADDED
@@ -0,0 +1,309 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import readline from 'readline'
5
+ import chalk from 'chalk'
6
+
7
+ // 配置文件路径
8
+ export const CONFIG_DIR = path.join(os.homedir(), '.please')
9
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
10
+
11
+ // 支持的 Provider 列表
12
+ const VALID_PROVIDERS = [
13
+ 'openai',
14
+ 'anthropic',
15
+ 'deepseek',
16
+ 'google',
17
+ 'groq',
18
+ 'mistral',
19
+ 'cohere',
20
+ 'fireworks',
21
+ 'together',
22
+ ] as const
23
+
24
+ type Provider = (typeof VALID_PROVIDERS)[number]
25
+
26
+ // 编辑模式
27
+ const VALID_EDIT_MODES = ['manual', 'auto'] as const
28
+ type EditMode = (typeof VALID_EDIT_MODES)[number]
29
+
30
+ /**
31
+ * 配置接口
32
+ */
33
+ export interface Config {
34
+ apiKey: string
35
+ baseUrl: string
36
+ model: string
37
+ provider: Provider
38
+ shellHook: boolean
39
+ chatHistoryLimit: number
40
+ commandHistoryLimit: number
41
+ shellHistoryLimit: number
42
+ editMode: EditMode
43
+ }
44
+
45
+ /**
46
+ * 默认配置
47
+ */
48
+ const DEFAULT_CONFIG: Config = {
49
+ apiKey: '',
50
+ baseUrl: 'https://api.openai.com/v1',
51
+ model: 'gpt-4-turbo',
52
+ provider: 'openai',
53
+ shellHook: false,
54
+ chatHistoryLimit: 10,
55
+ commandHistoryLimit: 10,
56
+ shellHistoryLimit: 15,
57
+ editMode: 'manual',
58
+ }
59
+
60
+ /**
61
+ * 确保配置目录存在
62
+ */
63
+ function ensureConfigDir(): void {
64
+ if (!fs.existsSync(CONFIG_DIR)) {
65
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 读取配置
71
+ */
72
+ export function getConfig(): Config {
73
+ ensureConfigDir()
74
+
75
+ if (!fs.existsSync(CONFIG_FILE)) {
76
+ return { ...DEFAULT_CONFIG }
77
+ }
78
+
79
+ try {
80
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8')
81
+ return { ...DEFAULT_CONFIG, ...JSON.parse(content) }
82
+ } catch {
83
+ return { ...DEFAULT_CONFIG }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * 保存配置
89
+ */
90
+ export function saveConfig(config: Config): void {
91
+ ensureConfigDir()
92
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
93
+ }
94
+
95
+ /**
96
+ * 设置单个配置项
97
+ */
98
+ export function setConfigValue(key: string, value: string | boolean | number): Config {
99
+ const config = getConfig()
100
+
101
+ if (!(key in DEFAULT_CONFIG)) {
102
+ throw new Error(`未知的配置项: ${key}`)
103
+ }
104
+
105
+ // 处理特殊类型
106
+ if (key === 'shellHook') {
107
+ config.shellHook = value === 'true' || value === true
108
+ } else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
109
+ const num = typeof value === 'number' ? value : parseInt(String(value), 10)
110
+ if (isNaN(num) || num < 1) {
111
+ throw new Error(`${key} 必须是大于 0 的整数`)
112
+ }
113
+ config[key] = num
114
+ } else if (key === 'provider') {
115
+ const strValue = String(value)
116
+ if (!VALID_PROVIDERS.includes(strValue as Provider)) {
117
+ throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`)
118
+ }
119
+ config.provider = strValue as Provider
120
+ } else if (key === 'editMode') {
121
+ const strValue = String(value)
122
+ if (!VALID_EDIT_MODES.includes(strValue as EditMode)) {
123
+ throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`)
124
+ }
125
+ config.editMode = strValue as EditMode
126
+ } else if (key === 'apiKey' || key === 'baseUrl' || key === 'model') {
127
+ config[key] = String(value)
128
+ }
129
+
130
+ saveConfig(config)
131
+ return config
132
+ }
133
+
134
+ /**
135
+ * 检查配置是否有效
136
+ */
137
+ export function isConfigValid(): boolean {
138
+ const config = getConfig()
139
+ return config.apiKey.length > 0
140
+ }
141
+
142
+ /**
143
+ * 隐藏 API Key 中间部分
144
+ */
145
+ export function maskApiKey(apiKey: string): string {
146
+ if (!apiKey || apiKey.length < 10) return apiKey || '(未设置)'
147
+ return apiKey.slice(0, 6) + '****' + apiKey.slice(-4)
148
+ }
149
+
150
+ /**
151
+ * 显示当前配置
152
+ */
153
+ export function displayConfig(): void {
154
+ const config = getConfig()
155
+ console.log(chalk.bold('\n当前配置:'))
156
+ console.log(chalk.gray('━'.repeat(50)))
157
+ console.log(` ${chalk.cyan('apiKey')}: ${maskApiKey(config.apiKey)}`)
158
+ console.log(` ${chalk.cyan('baseUrl')}: ${config.baseUrl}`)
159
+ console.log(` ${chalk.cyan('provider')}: ${config.provider}`)
160
+ console.log(` ${chalk.cyan('model')}: ${config.model}`)
161
+ console.log(
162
+ ` ${chalk.cyan('shellHook')}: ${config.shellHook ? chalk.green('已启用') : chalk.gray('未启用')}`
163
+ )
164
+ console.log(
165
+ ` ${chalk.cyan('editMode')}: ${
166
+ config.editMode === 'auto' ? chalk.hex('#00D9FF')('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
167
+ }`
168
+ )
169
+ console.log(` ${chalk.cyan('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
170
+ console.log(` ${chalk.cyan('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
171
+ console.log(` ${chalk.cyan('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
172
+ console.log(chalk.gray('━'.repeat(50)))
173
+ console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`))
174
+ }
175
+
176
+ /**
177
+ * 创建 readline 接口
178
+ */
179
+ function createReadlineInterface(): readline.Interface {
180
+ return readline.createInterface({
181
+ input: process.stdin,
182
+ output: process.stdout,
183
+ })
184
+ }
185
+
186
+ /**
187
+ * 异步提问
188
+ */
189
+ function question(rl: readline.Interface, prompt: string): Promise<string> {
190
+ return new Promise((resolve) => {
191
+ rl.question(prompt, (answer) => {
192
+ resolve(answer)
193
+ })
194
+ })
195
+ }
196
+
197
+ /**
198
+ * 交互式配置向导
199
+ */
200
+ export async function runConfigWizard(): Promise<void> {
201
+ const rl = createReadlineInterface()
202
+ const config = getConfig()
203
+
204
+ console.log(chalk.bold.hex('#00D9FF')('\n🔧 Pretty Please 配置向导'))
205
+ console.log(chalk.gray('━'.repeat(50)))
206
+ console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'))
207
+
208
+ try {
209
+ // 1. Provider
210
+ const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`)
211
+ const providerPrompt = `${chalk.cyan('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.provider)} ${chalk.gray('→')} `
212
+ const provider = await question(rl, providerPrompt)
213
+ if (provider.trim()) {
214
+ if (!VALID_PROVIDERS.includes(provider.trim() as Provider)) {
215
+ console.log(chalk.hex('#EF4444')(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`))
216
+ console.log()
217
+ rl.close()
218
+ return
219
+ }
220
+ config.provider = provider.trim() as Provider
221
+ }
222
+
223
+ // 2. Base URL
224
+ const baseUrlPrompt = `${chalk.cyan('API Base URL')}\n${chalk.gray('默认:')} ${chalk.yellow(config.baseUrl)} ${chalk.gray('→')} `
225
+ const baseUrl = await question(rl, baseUrlPrompt)
226
+ if (baseUrl.trim()) {
227
+ config.baseUrl = baseUrl.trim()
228
+ }
229
+
230
+ // 3. API Key
231
+ const currentKeyDisplay = config.apiKey ? maskApiKey(config.apiKey) : '(未设置)'
232
+ const apiKeyPrompt = `${chalk.cyan('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `
233
+ const apiKey = await question(rl, apiKeyPrompt)
234
+ if (apiKey.trim()) {
235
+ config.apiKey = apiKey.trim()
236
+ }
237
+
238
+ // 4. Model
239
+ const modelPrompt = `${chalk.cyan('Model')}\n${chalk.gray('默认:')} ${chalk.yellow(config.model)} ${chalk.gray('→')} `
240
+ const model = await question(rl, modelPrompt)
241
+ if (model.trim()) {
242
+ config.model = model.trim()
243
+ }
244
+
245
+ // 5. Shell Hook
246
+ const shellHookPrompt = `${chalk.cyan('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `
247
+ const shellHook = await question(rl, shellHookPrompt)
248
+ if (shellHook.trim()) {
249
+ config.shellHook = shellHook.trim() === 'true'
250
+ }
251
+
252
+ // 6. Edit Mode
253
+ const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)')
254
+ const editModePrompt = `${chalk.cyan('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.editMode)} ${chalk.gray('→')} `
255
+ const editMode = await question(rl, editModePrompt)
256
+ if (editMode.trim()) {
257
+ if (!VALID_EDIT_MODES.includes(editMode.trim() as EditMode)) {
258
+ console.log(chalk.hex('#EF4444')(`\n✗ 无效的 editMode,必须是: manual 或 auto`))
259
+ console.log()
260
+ rl.close()
261
+ return
262
+ }
263
+ config.editMode = editMode.trim() as EditMode
264
+ }
265
+
266
+ // 7. Chat History Limit
267
+ const chatHistoryPrompt = `${chalk.cyan('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.chatHistoryLimit)} ${chalk.gray('→')} `
268
+ const chatHistoryLimit = await question(rl, chatHistoryPrompt)
269
+ if (chatHistoryLimit.trim()) {
270
+ const num = parseInt(chatHistoryLimit.trim(), 10)
271
+ if (!isNaN(num) && num > 0) {
272
+ config.chatHistoryLimit = num
273
+ }
274
+ }
275
+
276
+ // 8. Command History Limit
277
+ const commandHistoryPrompt = `${chalk.cyan('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.commandHistoryLimit)} ${chalk.gray('→')} `
278
+ const commandHistoryLimit = await question(rl, commandHistoryPrompt)
279
+ if (commandHistoryLimit.trim()) {
280
+ const num = parseInt(commandHistoryLimit.trim(), 10)
281
+ if (!isNaN(num) && num > 0) {
282
+ config.commandHistoryLimit = num
283
+ }
284
+ }
285
+
286
+ // 9. Shell History Limit
287
+ const shellHistoryPrompt = `${chalk.cyan('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHistoryLimit)} ${chalk.gray('→')} `
288
+ const shellHistoryLimit = await question(rl, shellHistoryPrompt)
289
+ if (shellHistoryLimit.trim()) {
290
+ const num = parseInt(shellHistoryLimit.trim(), 10)
291
+ if (!isNaN(num) && num > 0) {
292
+ config.shellHistoryLimit = num
293
+ }
294
+ }
295
+
296
+ saveConfig(config)
297
+
298
+ console.log('\n' + chalk.gray('━'.repeat(50)))
299
+ console.log(chalk.hex('#10B981')('✅ 配置已保存'))
300
+ console.log(chalk.gray(` ${CONFIG_FILE}`))
301
+ console.log()
302
+ } catch (error) {
303
+ const message = error instanceof Error ? error.message : String(error)
304
+ console.log(chalk.hex('#EF4444')(`\n✗ 配置失败: ${message}`))
305
+ console.log()
306
+ } finally {
307
+ rl.close()
308
+ }
309
+ }