@yivan-lab/pretty-please 1.0.0 → 1.2.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 (89) hide show
  1. package/README.md +381 -28
  2. package/bin/pls.tsx +1138 -109
  3. package/dist/bin/pls.d.ts +1 -1
  4. package/dist/bin/pls.js +994 -91
  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/alias.d.ts +41 -0
  9. package/dist/src/alias.js +240 -0
  10. package/dist/src/builtin-detector.d.ts +14 -8
  11. package/dist/src/builtin-detector.js +36 -16
  12. package/dist/src/chat-history.d.ts +16 -11
  13. package/dist/src/chat-history.js +35 -4
  14. package/dist/src/components/Chat.js +5 -4
  15. package/dist/src/components/CodeColorizer.js +26 -20
  16. package/dist/src/components/CommandBox.js +3 -17
  17. package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
  18. package/dist/src/components/ConfirmationPrompt.js +9 -4
  19. package/dist/src/components/Duration.js +2 -1
  20. package/dist/src/components/InlineRenderer.js +2 -1
  21. package/dist/src/components/MarkdownDisplay.js +2 -1
  22. package/dist/src/components/MultiStepCommandGenerator.d.ts +5 -1
  23. package/dist/src/components/MultiStepCommandGenerator.js +127 -14
  24. package/dist/src/components/TableRenderer.js +2 -1
  25. package/dist/src/config.d.ts +59 -9
  26. package/dist/src/config.js +147 -48
  27. package/dist/src/history.d.ts +19 -5
  28. package/dist/src/history.js +26 -11
  29. package/dist/src/mastra-agent.d.ts +0 -1
  30. package/dist/src/mastra-agent.js +3 -4
  31. package/dist/src/mastra-chat.d.ts +28 -0
  32. package/dist/src/mastra-chat.js +93 -0
  33. package/dist/src/multi-step.d.ts +23 -7
  34. package/dist/src/multi-step.js +29 -6
  35. package/dist/src/prompts.d.ts +11 -0
  36. package/dist/src/prompts.js +140 -0
  37. package/dist/src/remote-history.d.ts +63 -0
  38. package/dist/src/remote-history.js +315 -0
  39. package/dist/src/remote.d.ts +113 -0
  40. package/dist/src/remote.js +634 -0
  41. package/dist/src/shell-hook.d.ts +87 -12
  42. package/dist/src/shell-hook.js +315 -17
  43. package/dist/src/sysinfo.d.ts +9 -5
  44. package/dist/src/sysinfo.js +2 -2
  45. package/dist/src/ui/theme.d.ts +27 -24
  46. package/dist/src/ui/theme.js +71 -21
  47. package/dist/src/upgrade.d.ts +41 -0
  48. package/dist/src/upgrade.js +348 -0
  49. package/dist/src/utils/console.d.ts +11 -11
  50. package/dist/src/utils/console.js +26 -17
  51. package/package.json +11 -9
  52. package/src/alias.ts +301 -0
  53. package/src/builtin-detector.ts +126 -0
  54. package/src/chat-history.ts +140 -0
  55. package/src/components/Chat.tsx +6 -5
  56. package/src/components/CodeColorizer.tsx +27 -19
  57. package/src/components/CommandBox.tsx +3 -17
  58. package/src/components/ConfirmationPrompt.tsx +11 -3
  59. package/src/components/Duration.tsx +2 -1
  60. package/src/components/InlineRenderer.tsx +2 -1
  61. package/src/components/MarkdownDisplay.tsx +2 -1
  62. package/src/components/MultiStepCommandGenerator.tsx +167 -16
  63. package/src/components/TableRenderer.tsx +2 -1
  64. package/src/config.ts +394 -0
  65. package/src/history.ts +160 -0
  66. package/src/mastra-agent.ts +3 -4
  67. package/src/mastra-chat.ts +124 -0
  68. package/src/multi-step.ts +45 -8
  69. package/src/prompts.ts +154 -0
  70. package/src/remote-history.ts +390 -0
  71. package/src/remote.ts +800 -0
  72. package/src/shell-hook.ts +754 -0
  73. package/src/{sysinfo.js → sysinfo.ts} +28 -16
  74. package/src/ui/theme.ts +101 -24
  75. package/src/upgrade.ts +397 -0
  76. package/src/utils/{console.js → console.ts} +36 -27
  77. package/bin/pls.js +0 -681
  78. package/src/ai.js +0 -324
  79. package/src/builtin-detector.js +0 -98
  80. package/src/chat-history.js +0 -94
  81. package/src/components/ChatStatus.tsx +0 -53
  82. package/src/components/CommandGenerator.tsx +0 -184
  83. package/src/components/ConfigDisplay.tsx +0 -64
  84. package/src/components/ConfigWizard.tsx +0 -101
  85. package/src/components/HistoryDisplay.tsx +0 -69
  86. package/src/components/HookManager.tsx +0 -150
  87. package/src/config.js +0 -221
  88. package/src/history.js +0 -131
  89. package/src/shell-hook.js +0 -393
@@ -1,24 +1,34 @@
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 } from '../multi-step.js';
5
6
  import { detectBuiltin, formatBuiltins } from '../builtin-detector.js';
6
7
  import { CommandBox } from './CommandBox.js';
7
8
  import { ConfirmationPrompt } from './ConfirmationPrompt.js';
8
9
  import { Duration } from './Duration.js';
9
- import { theme } from '../ui/theme.js';
10
+ import { getCurrentTheme } from '../ui/theme.js';
11
+ import { getConfig } from '../config.js';
10
12
  /**
11
13
  * MultiStepCommandGenerator 组件 - 多步骤命令生成
12
14
  * 每次只生成一个命令,支持 continue 机制
13
15
  */
14
- export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], currentStepNumber = 1, onStepComplete, }) => {
16
+ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], currentStepNumber = 1, remoteContext, isRemote = false, onStepComplete, }) => {
17
+ const theme = getCurrentTheme();
15
18
  const [state, setState] = useState({ type: 'thinking' });
16
19
  const [thinkDuration, setThinkDuration] = useState(0);
17
20
  const [debugInfo, setDebugInfo] = useState(null);
21
+ const [editedCommand, setEditedCommand] = useState(''); // 新增:编辑后的命令
22
+ // 监听编辑模式下的 Esc 键
23
+ useInput((input, key) => {
24
+ if (state.type === 'editing' && key.escape) {
25
+ handleEditCancel();
26
+ }
27
+ }, { isActive: state.type === 'editing' });
18
28
  // 初始化:调用 Mastra 生成命令
19
29
  useEffect(() => {
20
30
  const thinkStart = Date.now();
21
- generateMultiStepCommand(prompt, previousSteps, { debug })
31
+ generateMultiStepCommand(prompt, previousSteps, { debug, remoteContext })
22
32
  .then((result) => {
23
33
  const thinkEnd = Date.now();
24
34
  setThinkDuration(thinkEnd - thinkStart);
@@ -26,13 +36,27 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
26
36
  if (debug && result.debugInfo) {
27
37
  setDebugInfo(result.debugInfo);
28
38
  }
29
- setState({
30
- type: 'showing_command',
31
- stepData: result.stepData,
32
- });
33
- // 检测 builtin
39
+ // 如果 AI 返回空命令且决定不继续,说明 AI 放弃了
40
+ // 直接结束,不显示命令框
41
+ if (!result.stepData.command.trim() && result.stepData.continue === false) {
42
+ setTimeout(() => {
43
+ onStepComplete({
44
+ command: '',
45
+ confirmed: false,
46
+ reasoning: result.stepData.reasoning,
47
+ needsContinue: false,
48
+ });
49
+ }, 100);
50
+ return;
51
+ }
52
+ // 检测 builtin(优先检测,但远程执行时跳过)
34
53
  const { hasBuiltin, builtins } = detectBuiltin(result.stepData.command);
35
- if (hasBuiltin) {
54
+ if (hasBuiltin && !isRemote) {
55
+ // 有 builtin 且是本地执行,不管什么模式都不编辑,直接提示
56
+ setState({
57
+ type: 'showing_command',
58
+ stepData: result.stepData,
59
+ });
36
60
  setTimeout(() => {
37
61
  onStepComplete({
38
62
  command: result.stepData.command,
@@ -43,6 +67,25 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
43
67
  needsContinue: result.stepData.continue,
44
68
  });
45
69
  }, 100);
70
+ return;
71
+ }
72
+ // 根据 editMode 决定进入哪个状态
73
+ const config = getConfig();
74
+ const autoEdit = config.editMode === 'auto';
75
+ if (autoEdit) {
76
+ // auto 模式:直接进入编辑状态
77
+ setEditedCommand(result.stepData.command);
78
+ setState({
79
+ type: 'editing',
80
+ stepData: result.stepData,
81
+ });
82
+ }
83
+ else {
84
+ // manual 模式:显示命令,等待用户操作
85
+ setState({
86
+ type: 'showing_command',
87
+ stepData: result.stepData,
88
+ });
46
89
  }
47
90
  })
48
91
  .catch((error) => {
@@ -55,12 +98,37 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
55
98
  });
56
99
  }, 100);
57
100
  });
58
- }, [prompt, previousSteps, debug]);
101
+ }, [prompt, previousSteps, debug, remoteContext]);
59
102
  // 处理确认
60
103
  const handleConfirm = () => {
61
104
  if (state.type === 'showing_command') {
62
105
  onStepComplete({
63
106
  command: state.stepData.command,
107
+ aiGeneratedCommand: state.stepData.command, // 原始命令
108
+ userModified: false,
109
+ confirmed: true,
110
+ reasoning: state.stepData.reasoning,
111
+ needsContinue: state.stepData.continue,
112
+ nextStepHint: state.stepData.nextStepHint,
113
+ debugInfo: debugInfo,
114
+ });
115
+ }
116
+ };
117
+ // 处理编辑
118
+ const handleEdit = () => {
119
+ if (state.type === 'showing_command') {
120
+ setEditedCommand(state.stepData.command); // 初始化为 AI 生成的命令
121
+ setState({ type: 'editing', stepData: state.stepData });
122
+ }
123
+ };
124
+ // 编辑完成确认
125
+ const handleEditConfirm = () => {
126
+ if (state.type === 'editing') {
127
+ const modified = editedCommand !== state.stepData.command;
128
+ onStepComplete({
129
+ command: editedCommand, // 使用编辑后的命令
130
+ aiGeneratedCommand: state.stepData.command, // 保存 AI 原始命令
131
+ userModified: modified,
64
132
  confirmed: true,
65
133
  reasoning: state.stepData.reasoning,
66
134
  needsContinue: state.stepData.continue,
@@ -69,6 +137,27 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
69
137
  });
70
138
  }
71
139
  };
140
+ // 取消编辑
141
+ const handleEditCancel = () => {
142
+ if (state.type === 'editing') {
143
+ const config = getConfig();
144
+ if (config.editMode === 'auto') {
145
+ // auto 模式:Esc 直接取消整个操作
146
+ setState({ type: 'cancelled', command: state.stepData.command });
147
+ setTimeout(() => {
148
+ onStepComplete({
149
+ command: state.stepData.command,
150
+ confirmed: false,
151
+ cancelled: true,
152
+ });
153
+ }, 100);
154
+ }
155
+ else {
156
+ // manual 模式:Esc 返回到 showing_command 状态
157
+ setState({ type: 'showing_command', stepData: state.stepData });
158
+ }
159
+ }
160
+ };
72
161
  // 处理取消
73
162
  const handleCancel = () => {
74
163
  if (state.type === 'showing_command') {
@@ -87,7 +176,9 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
87
176
  React.createElement(Text, { color: theme.info },
88
177
  React.createElement(Spinner, { type: "dots" }),
89
178
  ' ',
90
- currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`))),
179
+ remoteContext
180
+ ? (currentStepNumber === 1 ? `正在为 ${remoteContext.name} 思考...` : `正在规划步骤 ${currentStepNumber} (${remoteContext.name})...`)
181
+ : (currentStepNumber === 1 ? '正在思考...' : `正在规划步骤 ${currentStepNumber}...`)))),
91
182
  state.type !== 'thinking' && thinkDuration > 0 && (React.createElement(Box, null,
92
183
  React.createElement(Text, { color: theme.success }, "\u2713 \u601D\u8003\u5B8C\u6210 "),
93
184
  React.createElement(Duration, { ms: thinkDuration }))),
@@ -104,6 +195,13 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
104
195
  React.createElement(Text, { color: theme.text.secondary },
105
196
  "\u5DF2\u6267\u884C\u6B65\u9AA4\u6570: ",
106
197
  debugInfo.previousStepsCount))),
198
+ debugInfo.remoteContext && (React.createElement(Box, { marginTop: 1 },
199
+ React.createElement(Text, { color: theme.text.secondary },
200
+ "\u8FDC\u7A0B\u670D\u52A1\u5668: ",
201
+ debugInfo.remoteContext.name,
202
+ " (",
203
+ debugInfo.remoteContext.sysInfo.os,
204
+ ")"))),
107
205
  React.createElement(Box, { marginTop: 1 },
108
206
  React.createElement(Text, { color: theme.text.secondary }, "AI \u8FD4\u56DE\u7684 JSON:")),
109
207
  React.createElement(Text, { color: theme.text.dim }, JSON.stringify(debugInfo.response, null, 2)),
@@ -120,7 +218,7 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
120
218
  "\u4E0B\u4E00\u6B65: ",
121
219
  state.stepData.nextStepHint)))),
122
220
  React.createElement(CommandBox, { command: state.stepData.command }),
123
- (() => {
221
+ !isRemote && (() => {
124
222
  const { hasBuiltin, builtins } = detectBuiltin(state.stepData.command);
125
223
  if (hasBuiltin) {
126
224
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
@@ -132,7 +230,22 @@ export const MultiStepCommandGenerator = ({ prompt, debug, previousSteps = [], c
132
230
  }
133
231
  return null;
134
232
  })(),
135
- !detectBuiltin(state.stepData.command).hasBuiltin && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel })))),
233
+ (isRemote || !detectBuiltin(state.stepData.command).hasBuiltin) && (React.createElement(ConfirmationPrompt, { prompt: "\u6267\u884C\uFF1F", onConfirm: handleConfirm, onCancel: handleCancel, onEdit: handleEdit })))),
234
+ state.type === 'editing' && (React.createElement(React.Fragment, null,
235
+ state.stepData.continue === true && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
236
+ React.createElement(Text, { color: theme.text.secondary },
237
+ "\u6B65\u9AA4 ",
238
+ currentStepNumber,
239
+ "/?"),
240
+ state.stepData.reasoning && (React.createElement(Text, { color: theme.text.muted },
241
+ "\u539F\u56E0: ",
242
+ state.stepData.reasoning)))),
243
+ React.createElement(CommandBox, { command: state.stepData.command }),
244
+ React.createElement(Box, { flexDirection: "row" },
245
+ React.createElement(Text, { color: theme.primary }, '> '),
246
+ React.createElement(TextInput, { value: editedCommand, onChange: setEditedCommand, onSubmit: handleEditConfirm })),
247
+ React.createElement(Box, { marginTop: 1 },
248
+ React.createElement(Text, { color: theme.text.secondary }, getConfig().editMode === 'auto' ? '[回车执行 / Esc 取消]' : '[回车执行 / Esc 返回]')))),
136
249
  state.type === 'cancelled' && (React.createElement(Box, { marginTop: 1 },
137
250
  React.createElement(Text, { color: theme.text.secondary }, "\u5DF2\u53D6\u6D88\u6267\u884C"))),
138
251
  state.type === 'error' && (React.createElement(Box, { marginTop: 1 },
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import stringWidth from 'string-width';
4
- import { theme } from '../ui/theme.js';
4
+ import { getCurrentTheme } from '../ui/theme.js';
5
5
  import { RenderInline } from './InlineRenderer.js';
6
6
  /**
7
7
  * 计算纯文本长度(去除 markdown 标记)
@@ -40,6 +40,7 @@ function calculateColumnWidths(headers, rows, terminalWidth) {
40
40
  * 表格渲染组件
41
41
  */
42
42
  function TableRendererInternal({ headers, rows, terminalWidth }) {
43
+ const theme = getCurrentTheme();
43
44
  const columnWidths = calculateColumnWidths(headers, rows, terminalWidth);
44
45
  const baseColor = theme.text.primary;
45
46
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
@@ -1,29 +1,79 @@
1
+ export declare const CONFIG_DIR: string;
2
+ declare const VALID_PROVIDERS: readonly ["openai", "anthropic", "deepseek", "google", "groq", "mistral", "cohere", "fireworks", "together"];
3
+ type Provider = (typeof VALID_PROVIDERS)[number];
4
+ declare const VALID_EDIT_MODES: readonly ["manual", "auto"];
5
+ type EditMode = (typeof VALID_EDIT_MODES)[number];
6
+ declare const VALID_THEMES: readonly ["dark", "light"];
7
+ export type ThemeName = (typeof VALID_THEMES)[number];
1
8
  /**
2
- * 读取配置
9
+ * 别名配置接口
3
10
  */
4
- export function getConfig(): any;
11
+ export interface AliasConfig {
12
+ prompt: string;
13
+ description?: string;
14
+ }
15
+ /**
16
+ * 远程服务器配置接口
17
+ */
18
+ export interface RemoteConfig {
19
+ host: string;
20
+ user: string;
21
+ port: number;
22
+ key?: string;
23
+ password?: boolean;
24
+ workDir?: string;
25
+ }
26
+ /**
27
+ * 远程服务器系统信息缓存
28
+ */
29
+ export interface RemoteSysInfo {
30
+ os: string;
31
+ osVersion: string;
32
+ shell: string;
33
+ hostname: string;
34
+ cachedAt: string;
35
+ }
36
+ /**
37
+ * 配置接口
38
+ */
39
+ export interface Config {
40
+ apiKey: string;
41
+ baseUrl: string;
42
+ model: string;
43
+ provider: Provider;
44
+ shellHook: boolean;
45
+ chatHistoryLimit: number;
46
+ commandHistoryLimit: number;
47
+ shellHistoryLimit: number;
48
+ editMode: EditMode;
49
+ theme: ThemeName;
50
+ aliases: Record<string, AliasConfig>;
51
+ remotes: Record<string, RemoteConfig>;
52
+ defaultRemote?: string;
53
+ }
54
+ export declare function getConfig(): Config;
5
55
  /**
6
56
  * 保存配置
7
57
  */
8
- export function saveConfig(config: any): void;
58
+ export declare function saveConfig(config: Config): void;
9
59
  /**
10
60
  * 设置单个配置项
11
61
  */
12
- export function setConfigValue(key: any, value: any): any;
62
+ export declare function setConfigValue(key: string, value: string | boolean | number): Config;
13
63
  /**
14
64
  * 检查配置是否有效
15
65
  */
16
- export function isConfigValid(): any;
66
+ export declare function isConfigValid(): boolean;
17
67
  /**
18
68
  * 隐藏 API Key 中间部分
19
69
  */
20
- export function maskApiKey(apiKey: any): any;
70
+ export declare function maskApiKey(apiKey: string): string;
21
71
  /**
22
72
  * 显示当前配置
23
73
  */
24
- export function displayConfig(): void;
74
+ export declare function displayConfig(): void;
25
75
  /**
26
76
  * 交互式配置向导
27
77
  */
28
- export function runConfigWizard(): Promise<void>;
29
- export const CONFIG_DIR: string;
78
+ export declare function runConfigWizard(): Promise<void>;
79
+ export {};
@@ -3,18 +3,54 @@ import path from 'path';
3
3
  import os from 'os';
4
4
  import readline from 'readline';
5
5
  import chalk from 'chalk';
6
- const CONFIG_DIR = path.join(os.homedir(), '.please');
6
+ import { getCurrentTheme } from './ui/theme.js';
7
+ // 获取主题颜色
8
+ function getColors() {
9
+ const theme = getCurrentTheme();
10
+ return {
11
+ primary: theme.primary,
12
+ secondary: theme.secondary,
13
+ success: theme.success,
14
+ error: theme.error
15
+ };
16
+ }
17
+ // 配置文件路径
18
+ export const CONFIG_DIR = path.join(os.homedir(), '.please');
7
19
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
20
+ // 支持的 Provider 列表
21
+ const VALID_PROVIDERS = [
22
+ 'openai',
23
+ 'anthropic',
24
+ 'deepseek',
25
+ 'google',
26
+ 'groq',
27
+ 'mistral',
28
+ 'cohere',
29
+ 'fireworks',
30
+ 'together',
31
+ ];
32
+ // 编辑模式
33
+ const VALID_EDIT_MODES = ['manual', 'auto'];
34
+ // 主题
35
+ const VALID_THEMES = ['dark', 'light'];
36
+ /**
37
+ * 默认配置
38
+ */
8
39
  const DEFAULT_CONFIG = {
9
40
  apiKey: '',
10
41
  baseUrl: 'https://api.openai.com/v1',
11
42
  model: 'gpt-4-turbo',
12
- provider: 'openai', // Mastra provider: openai, anthropic, deepseek, google, groq, mistral, cohere 等
13
- shellHook: false, // 是否启用 shell hook 记录终端命令
14
- chatHistoryLimit: 10 // chat 对话历史保留轮数
43
+ provider: 'openai',
44
+ shellHook: false,
45
+ chatHistoryLimit: 10,
46
+ commandHistoryLimit: 10,
47
+ shellHistoryLimit: 15,
48
+ editMode: 'manual',
49
+ theme: 'dark',
50
+ aliases: {},
51
+ remotes: {},
52
+ defaultRemote: '',
15
53
  };
16
- // 导出配置目录路径
17
- export { CONFIG_DIR };
18
54
  /**
19
55
  * 确保配置目录存在
20
56
  */
@@ -25,19 +61,30 @@ function ensureConfigDir() {
25
61
  }
26
62
  /**
27
63
  * 读取配置
64
+ * 优化:添加缓存,避免重复读取文件
28
65
  */
66
+ let cachedConfig = null;
29
67
  export function getConfig() {
68
+ // 如果已有缓存,直接返回
69
+ if (cachedConfig !== null) {
70
+ return cachedConfig;
71
+ }
30
72
  ensureConfigDir();
73
+ let config;
31
74
  if (!fs.existsSync(CONFIG_FILE)) {
32
- return { ...DEFAULT_CONFIG };
75
+ config = { ...DEFAULT_CONFIG };
33
76
  }
34
- try {
35
- const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
36
- return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
37
- }
38
- catch {
39
- return { ...DEFAULT_CONFIG };
77
+ else {
78
+ try {
79
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
80
+ config = { ...DEFAULT_CONFIG, ...JSON.parse(content) };
81
+ }
82
+ catch {
83
+ config = { ...DEFAULT_CONFIG };
84
+ }
40
85
  }
86
+ cachedConfig = config;
87
+ return config;
41
88
  }
42
89
  /**
43
90
  * 保存配置
@@ -56,27 +103,42 @@ export function setConfigValue(key, value) {
56
103
  }
57
104
  // 处理特殊类型
58
105
  if (key === 'shellHook') {
59
- config[key] = value === 'true' || value === true;
106
+ config.shellHook = value === 'true' || value === true;
60
107
  }
61
- else if (key === 'chatHistoryLimit') {
62
- const num = parseInt(value, 10);
108
+ else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
109
+ const num = typeof value === 'number' ? value : parseInt(String(value), 10);
63
110
  if (isNaN(num) || num < 1) {
64
- throw new Error('chatHistoryLimit 必须是大于 0 的整数');
111
+ throw new Error(`${key} 必须是大于 0 的整数`);
65
112
  }
66
113
  config[key] = num;
67
114
  }
68
115
  else if (key === 'provider') {
69
- // 验证 provider
70
- const validProviders = ['openai', 'anthropic', 'deepseek', 'google', 'groq', 'mistral', 'cohere', 'fireworks', 'together'];
71
- if (!validProviders.includes(value)) {
72
- throw new Error(`provider 必须是以下之一: ${validProviders.join(', ')}`);
116
+ const strValue = String(value);
117
+ if (!VALID_PROVIDERS.includes(strValue)) {
118
+ throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`);
73
119
  }
74
- config[key] = value;
120
+ config.provider = strValue;
75
121
  }
76
- else {
77
- config[key] = value;
122
+ else if (key === 'editMode') {
123
+ const strValue = String(value);
124
+ if (!VALID_EDIT_MODES.includes(strValue)) {
125
+ throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`);
126
+ }
127
+ config.editMode = strValue;
128
+ }
129
+ else if (key === 'theme') {
130
+ const strValue = String(value);
131
+ if (!VALID_THEMES.includes(strValue)) {
132
+ throw new Error(`theme 必须是以下之一: ${VALID_THEMES.join(', ')}`);
133
+ }
134
+ config.theme = strValue;
135
+ }
136
+ else if (key === 'apiKey' || key === 'baseUrl' || key === 'model' || key === 'defaultRemote') {
137
+ config[key] = String(value);
78
138
  }
79
139
  saveConfig(config);
140
+ // 清除缓存,下次读取时会重新加载
141
+ cachedConfig = null;
80
142
  return config;
81
143
  }
82
144
  /**
@@ -84,7 +146,7 @@ export function setConfigValue(key, value) {
84
146
  */
85
147
  export function isConfigValid() {
86
148
  const config = getConfig();
87
- return config.apiKey && config.apiKey.length > 0;
149
+ return config.apiKey.length > 0;
88
150
  }
89
151
  /**
90
152
  * 隐藏 API Key 中间部分
@@ -99,15 +161,20 @@ export function maskApiKey(apiKey) {
99
161
  */
100
162
  export function displayConfig() {
101
163
  const config = getConfig();
164
+ const colors = getColors();
102
165
  console.log(chalk.bold('\n当前配置:'));
103
- console.log(chalk.gray('━'.repeat(40)));
104
- console.log(` ${chalk.cyan('apiKey')}: ${maskApiKey(config.apiKey)}`);
105
- console.log(` ${chalk.cyan('baseUrl')}: ${config.baseUrl}`);
106
- console.log(` ${chalk.cyan('provider')}: ${config.provider}`);
107
- console.log(` ${chalk.cyan('model')}: ${config.model}`);
108
- console.log(` ${chalk.cyan('shellHook')}: ${config.shellHook ? chalk.green('已启用') : chalk.gray('未启用')}`);
109
- console.log(` ${chalk.cyan('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
110
- console.log(chalk.gray(''.repeat(40)));
166
+ console.log(chalk.gray('━'.repeat(50)));
167
+ console.log(` ${chalk.hex(colors.primary)('apiKey')}: ${maskApiKey(config.apiKey)}`);
168
+ console.log(` ${chalk.hex(colors.primary)('baseUrl')}: ${config.baseUrl}`);
169
+ console.log(` ${chalk.hex(colors.primary)('provider')}: ${config.provider}`);
170
+ console.log(` ${chalk.hex(colors.primary)('model')}: ${config.model}`);
171
+ console.log(` ${chalk.hex(colors.primary)('shellHook')}: ${config.shellHook ? chalk.hex(colors.success)('已启用') : chalk.gray('未启用')}`);
172
+ console.log(` ${chalk.hex(colors.primary)('editMode')}: ${config.editMode === 'auto' ? chalk.hex(colors.primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')}`);
173
+ console.log(` ${chalk.hex(colors.primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
174
+ console.log(` ${chalk.hex(colors.primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
175
+ console.log(` ${chalk.hex(colors.primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
176
+ console.log(` ${chalk.hex(colors.primary)('theme')}: ${config.theme === 'dark' ? chalk.hex(colors.primary)('dark (深色)') : chalk.hex(colors.primary)('light (浅色)')}`);
177
+ console.log(chalk.gray('━'.repeat(50)));
111
178
  console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`));
112
179
  }
113
180
  /**
@@ -116,7 +183,7 @@ export function displayConfig() {
116
183
  function createReadlineInterface() {
117
184
  return readline.createInterface({
118
185
  input: process.stdin,
119
- output: process.stdout
186
+ output: process.stdout,
120
187
  });
121
188
  }
122
189
  /**
@@ -135,18 +202,18 @@ function question(rl, prompt) {
135
202
  export async function runConfigWizard() {
136
203
  const rl = createReadlineInterface();
137
204
  const config = getConfig();
138
- console.log(chalk.bold.hex('#00D9FF')('\n🔧 Pretty Please 配置向导'));
205
+ const colors = getColors();
206
+ console.log(chalk.bold.hex(colors.primary)('\n🔧 Pretty Please 配置向导'));
139
207
  console.log(chalk.gray('━'.repeat(50)));
140
208
  console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'));
141
209
  try {
142
210
  // 1. Provider
143
- const validProviders = ['openai', 'anthropic', 'deepseek', 'google', 'groq', 'mistral', 'cohere', 'fireworks', 'together'];
144
- const providerHint = chalk.gray(`(可选: ${validProviders.join(', ')})`);
145
- const providerPrompt = `${chalk.cyan('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.provider)} ${chalk.gray('→')} `;
211
+ const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`);
212
+ const providerPrompt = `${chalk.hex(colors.primary)('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.provider)} ${chalk.gray('→')} `;
146
213
  const provider = await question(rl, providerPrompt);
147
214
  if (provider.trim()) {
148
- if (!validProviders.includes(provider.trim())) {
149
- console.log(chalk.hex('#EF4444')(`\n✗ 无效的 provider,必须是以下之一: ${validProviders.join(', ')}`));
215
+ if (!VALID_PROVIDERS.includes(provider.trim())) {
216
+ console.log(chalk.hex(colors.error)(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`));
150
217
  console.log();
151
218
  rl.close();
152
219
  return;
@@ -154,32 +221,45 @@ export async function runConfigWizard() {
154
221
  config.provider = provider.trim();
155
222
  }
156
223
  // 2. Base URL
157
- const baseUrlPrompt = `${chalk.cyan('API Base URL')}\n${chalk.gray('默认:')} ${chalk.yellow(config.baseUrl)} ${chalk.gray('→')} `;
224
+ const baseUrlPrompt = `${chalk.hex(colors.primary)('API Base URL')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.baseUrl)} ${chalk.gray('→')} `;
158
225
  const baseUrl = await question(rl, baseUrlPrompt);
159
226
  if (baseUrl.trim()) {
160
227
  config.baseUrl = baseUrl.trim();
161
228
  }
162
229
  // 3. API Key
163
230
  const currentKeyDisplay = config.apiKey ? maskApiKey(config.apiKey) : '(未设置)';
164
- const apiKeyPrompt = `${chalk.cyan('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `;
231
+ const apiKeyPrompt = `${chalk.hex(colors.primary)('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `;
165
232
  const apiKey = await question(rl, apiKeyPrompt);
166
233
  if (apiKey.trim()) {
167
234
  config.apiKey = apiKey.trim();
168
235
  }
169
236
  // 4. Model
170
- const modelPrompt = `${chalk.cyan('Model')}\n${chalk.gray('默认:')} ${chalk.yellow(config.model)} ${chalk.gray('→')} `;
237
+ const modelPrompt = `${chalk.hex(colors.primary)('Model')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.model)} ${chalk.gray('→')} `;
171
238
  const model = await question(rl, modelPrompt);
172
239
  if (model.trim()) {
173
240
  config.model = model.trim();
174
241
  }
175
242
  // 5. Shell Hook
176
- const shellHookPrompt = `${chalk.cyan('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `;
243
+ const shellHookPrompt = `${chalk.hex(colors.primary)('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `;
177
244
  const shellHook = await question(rl, shellHookPrompt);
178
245
  if (shellHook.trim()) {
179
246
  config.shellHook = shellHook.trim() === 'true';
180
247
  }
181
- // 6. Chat History Limit
182
- const chatHistoryPrompt = `${chalk.cyan('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.chatHistoryLimit)} ${chalk.gray('→')} `;
248
+ // 6. Edit Mode
249
+ const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)');
250
+ const editModePrompt = `${chalk.hex(colors.primary)('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.editMode)} ${chalk.gray('→')} `;
251
+ const editMode = await question(rl, editModePrompt);
252
+ if (editMode.trim()) {
253
+ if (!VALID_EDIT_MODES.includes(editMode.trim())) {
254
+ console.log(chalk.hex(colors.error)(`\n✗ 无效的 editMode,必须是: manual 或 auto`));
255
+ console.log();
256
+ rl.close();
257
+ return;
258
+ }
259
+ config.editMode = editMode.trim();
260
+ }
261
+ // 7. Chat History Limit
262
+ const chatHistoryPrompt = `${chalk.hex(colors.primary)('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.chatHistoryLimit)} ${chalk.gray('→')} `;
183
263
  const chatHistoryLimit = await question(rl, chatHistoryPrompt);
184
264
  if (chatHistoryLimit.trim()) {
185
265
  const num = parseInt(chatHistoryLimit.trim(), 10);
@@ -187,14 +267,33 @@ export async function runConfigWizard() {
187
267
  config.chatHistoryLimit = num;
188
268
  }
189
269
  }
270
+ // 8. Command History Limit
271
+ const commandHistoryPrompt = `${chalk.hex(colors.primary)('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.commandHistoryLimit)} ${chalk.gray('→')} `;
272
+ const commandHistoryLimit = await question(rl, commandHistoryPrompt);
273
+ if (commandHistoryLimit.trim()) {
274
+ const num = parseInt(commandHistoryLimit.trim(), 10);
275
+ if (!isNaN(num) && num > 0) {
276
+ config.commandHistoryLimit = num;
277
+ }
278
+ }
279
+ // 9. Shell History Limit
280
+ const shellHistoryPrompt = `${chalk.hex(colors.primary)('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHistoryLimit)} ${chalk.gray('→')} `;
281
+ const shellHistoryLimit = await question(rl, shellHistoryPrompt);
282
+ if (shellHistoryLimit.trim()) {
283
+ const num = parseInt(shellHistoryLimit.trim(), 10);
284
+ if (!isNaN(num) && num > 0) {
285
+ config.shellHistoryLimit = num;
286
+ }
287
+ }
190
288
  saveConfig(config);
191
289
  console.log('\n' + chalk.gray('━'.repeat(50)));
192
- console.log(chalk.hex('#10B981')('✅ 配置已保存'));
290
+ console.log(chalk.hex(getColors().success)('✅ 配置已保存'));
193
291
  console.log(chalk.gray(` ${CONFIG_FILE}`));
194
292
  console.log();
195
293
  }
196
294
  catch (error) {
197
- console.log(chalk.hex('#EF4444')(`\n✗ 配置失败: ${error.message}`));
295
+ const message = error instanceof Error ? error.message : String(error);
296
+ console.log(chalk.hex(getColors().error)(`\n✗ 配置失败: ${message}`));
198
297
  console.log();
199
298
  }
200
299
  finally {