@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
package/src/config.ts ADDED
@@ -0,0 +1,394 @@
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
+ import { getCurrentTheme } from './ui/theme.js'
7
+
8
+ // 获取主题颜色
9
+ function getColors() {
10
+ const theme = getCurrentTheme()
11
+ return {
12
+ primary: theme.primary,
13
+ secondary: theme.secondary,
14
+ success: theme.success,
15
+ error: theme.error
16
+ }
17
+ }
18
+
19
+ // 配置文件路径
20
+ export const CONFIG_DIR = path.join(os.homedir(), '.please')
21
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json')
22
+
23
+ // 支持的 Provider 列表
24
+ const VALID_PROVIDERS = [
25
+ 'openai',
26
+ 'anthropic',
27
+ 'deepseek',
28
+ 'google',
29
+ 'groq',
30
+ 'mistral',
31
+ 'cohere',
32
+ 'fireworks',
33
+ 'together',
34
+ ] as const
35
+
36
+ type Provider = (typeof VALID_PROVIDERS)[number]
37
+
38
+ // 编辑模式
39
+ const VALID_EDIT_MODES = ['manual', 'auto'] as const
40
+ type EditMode = (typeof VALID_EDIT_MODES)[number]
41
+
42
+ // 主题
43
+ const VALID_THEMES = ['dark', 'light'] as const
44
+ export type ThemeName = (typeof VALID_THEMES)[number]
45
+
46
+ /**
47
+ * 别名配置接口
48
+ */
49
+ export interface AliasConfig {
50
+ prompt: string
51
+ description?: string
52
+ }
53
+
54
+ /**
55
+ * 远程服务器配置接口
56
+ */
57
+ export interface RemoteConfig {
58
+ host: string
59
+ user: string
60
+ port: number
61
+ key?: string // SSH 私钥路径
62
+ password?: boolean // 是否使用密码认证(密码不存储,每次交互输入)
63
+ workDir?: string // 默认工作目录
64
+ }
65
+
66
+ /**
67
+ * 远程服务器系统信息缓存
68
+ */
69
+ export interface RemoteSysInfo {
70
+ os: string // 操作系统 (linux, darwin, etc.)
71
+ osVersion: string // 系统版本
72
+ shell: string // 默认 shell (bash, zsh, etc.)
73
+ hostname: string // 主机名
74
+ cachedAt: string // 缓存时间
75
+ }
76
+
77
+ /**
78
+ * 配置接口
79
+ */
80
+ export interface Config {
81
+ apiKey: string
82
+ baseUrl: string
83
+ model: string
84
+ provider: Provider
85
+ shellHook: boolean
86
+ chatHistoryLimit: number
87
+ commandHistoryLimit: number
88
+ shellHistoryLimit: number
89
+ editMode: EditMode
90
+ theme: ThemeName
91
+ aliases: Record<string, AliasConfig>
92
+ remotes: Record<string, RemoteConfig> // 远程服务器配置
93
+ defaultRemote?: string // 默认远程服务器名称
94
+ }
95
+
96
+ /**
97
+ * 默认配置
98
+ */
99
+ const DEFAULT_CONFIG: Config = {
100
+ apiKey: '',
101
+ baseUrl: 'https://api.openai.com/v1',
102
+ model: 'gpt-4-turbo',
103
+ provider: 'openai',
104
+ shellHook: false,
105
+ chatHistoryLimit: 10,
106
+ commandHistoryLimit: 10,
107
+ shellHistoryLimit: 15,
108
+ editMode: 'manual',
109
+ theme: 'dark',
110
+ aliases: {},
111
+ remotes: {},
112
+ defaultRemote: '',
113
+ }
114
+
115
+ /**
116
+ * 确保配置目录存在
117
+ */
118
+ function ensureConfigDir(): void {
119
+ if (!fs.existsSync(CONFIG_DIR)) {
120
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
121
+ }
122
+ }
123
+
124
+ /**
125
+ * 读取配置
126
+ * 优化:添加缓存,避免重复读取文件
127
+ */
128
+ let cachedConfig: Config | null = null
129
+
130
+ export function getConfig(): Config {
131
+ // 如果已有缓存,直接返回
132
+ if (cachedConfig !== null) {
133
+ return cachedConfig
134
+ }
135
+
136
+ ensureConfigDir()
137
+
138
+ let config: Config
139
+
140
+ if (!fs.existsSync(CONFIG_FILE)) {
141
+ config = { ...DEFAULT_CONFIG }
142
+ } else {
143
+ try {
144
+ const content = fs.readFileSync(CONFIG_FILE, 'utf-8')
145
+ config = { ...DEFAULT_CONFIG, ...JSON.parse(content) }
146
+ } catch {
147
+ config = { ...DEFAULT_CONFIG }
148
+ }
149
+ }
150
+
151
+ cachedConfig = config
152
+ return config
153
+ }
154
+
155
+ /**
156
+ * 保存配置
157
+ */
158
+ export function saveConfig(config: Config): void {
159
+ ensureConfigDir()
160
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2))
161
+ }
162
+
163
+ /**
164
+ * 设置单个配置项
165
+ */
166
+ export function setConfigValue(key: string, value: string | boolean | number): Config {
167
+ const config = getConfig()
168
+
169
+ if (!(key in DEFAULT_CONFIG)) {
170
+ throw new Error(`未知的配置项: ${key}`)
171
+ }
172
+
173
+ // 处理特殊类型
174
+ if (key === 'shellHook') {
175
+ config.shellHook = value === 'true' || value === true
176
+ } else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
177
+ const num = typeof value === 'number' ? value : parseInt(String(value), 10)
178
+ if (isNaN(num) || num < 1) {
179
+ throw new Error(`${key} 必须是大于 0 的整数`)
180
+ }
181
+ config[key] = num
182
+ } else if (key === 'provider') {
183
+ const strValue = String(value)
184
+ if (!VALID_PROVIDERS.includes(strValue as Provider)) {
185
+ throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`)
186
+ }
187
+ config.provider = strValue as Provider
188
+ } else if (key === 'editMode') {
189
+ const strValue = String(value)
190
+ if (!VALID_EDIT_MODES.includes(strValue as EditMode)) {
191
+ throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`)
192
+ }
193
+ config.editMode = strValue as EditMode
194
+ } else if (key === 'theme') {
195
+ const strValue = String(value)
196
+ if (!VALID_THEMES.includes(strValue as ThemeName)) {
197
+ throw new Error(`theme 必须是以下之一: ${VALID_THEMES.join(', ')}`)
198
+ }
199
+ config.theme = strValue as ThemeName
200
+ } else if (key === 'apiKey' || key === 'baseUrl' || key === 'model' || key === 'defaultRemote') {
201
+ config[key] = String(value)
202
+ }
203
+
204
+ saveConfig(config)
205
+
206
+ // 清除缓存,下次读取时会重新加载
207
+ cachedConfig = null
208
+
209
+ return config
210
+ }
211
+
212
+ /**
213
+ * 检查配置是否有效
214
+ */
215
+ export function isConfigValid(): boolean {
216
+ const config = getConfig()
217
+ return config.apiKey.length > 0
218
+ }
219
+
220
+ /**
221
+ * 隐藏 API Key 中间部分
222
+ */
223
+ export function maskApiKey(apiKey: string): string {
224
+ if (!apiKey || apiKey.length < 10) return apiKey || '(未设置)'
225
+ return apiKey.slice(0, 6) + '****' + apiKey.slice(-4)
226
+ }
227
+
228
+ /**
229
+ * 显示当前配置
230
+ */
231
+ export function displayConfig(): void {
232
+ const config = getConfig()
233
+ const colors = getColors()
234
+ console.log(chalk.bold('\n当前配置:'))
235
+ console.log(chalk.gray('━'.repeat(50)))
236
+ console.log(` ${chalk.hex(colors.primary)('apiKey')}: ${maskApiKey(config.apiKey)}`)
237
+ console.log(` ${chalk.hex(colors.primary)('baseUrl')}: ${config.baseUrl}`)
238
+ console.log(` ${chalk.hex(colors.primary)('provider')}: ${config.provider}`)
239
+ console.log(` ${chalk.hex(colors.primary)('model')}: ${config.model}`)
240
+ console.log(
241
+ ` ${chalk.hex(colors.primary)('shellHook')}: ${config.shellHook ? chalk.hex(colors.success)('已启用') : chalk.gray('未启用')}`
242
+ )
243
+ console.log(
244
+ ` ${chalk.hex(colors.primary)('editMode')}: ${
245
+ config.editMode === 'auto' ? chalk.hex(colors.primary)('auto (自动编辑)') : chalk.gray('manual (按E编辑)')
246
+ }`
247
+ )
248
+ console.log(` ${chalk.hex(colors.primary)('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`)
249
+ console.log(` ${chalk.hex(colors.primary)('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`)
250
+ console.log(` ${chalk.hex(colors.primary)('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`)
251
+ console.log(
252
+ ` ${chalk.hex(colors.primary)('theme')}: ${
253
+ config.theme === 'dark' ? chalk.hex(colors.primary)('dark (深色)') : chalk.hex(colors.primary)('light (浅色)')
254
+ }`
255
+ )
256
+ console.log(chalk.gray('━'.repeat(50)))
257
+ console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`))
258
+ }
259
+
260
+ /**
261
+ * 创建 readline 接口
262
+ */
263
+ function createReadlineInterface(): readline.Interface {
264
+ return readline.createInterface({
265
+ input: process.stdin,
266
+ output: process.stdout,
267
+ })
268
+ }
269
+
270
+ /**
271
+ * 异步提问
272
+ */
273
+ function question(rl: readline.Interface, prompt: string): Promise<string> {
274
+ return new Promise((resolve) => {
275
+ rl.question(prompt, (answer) => {
276
+ resolve(answer)
277
+ })
278
+ })
279
+ }
280
+
281
+ /**
282
+ * 交互式配置向导
283
+ */
284
+ export async function runConfigWizard(): Promise<void> {
285
+ const rl = createReadlineInterface()
286
+ const config = getConfig()
287
+ const colors = getColors()
288
+
289
+ console.log(chalk.bold.hex(colors.primary)('\n🔧 Pretty Please 配置向导'))
290
+ console.log(chalk.gray('━'.repeat(50)))
291
+ console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'))
292
+
293
+ try {
294
+ // 1. Provider
295
+ const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`)
296
+ const providerPrompt = `${chalk.hex(colors.primary)('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.provider)} ${chalk.gray('→')} `
297
+ const provider = await question(rl, providerPrompt)
298
+ if (provider.trim()) {
299
+ if (!VALID_PROVIDERS.includes(provider.trim() as Provider)) {
300
+ console.log(chalk.hex(colors.error)(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`))
301
+ console.log()
302
+ rl.close()
303
+ return
304
+ }
305
+ config.provider = provider.trim() as Provider
306
+ }
307
+
308
+ // 2. Base URL
309
+ const baseUrlPrompt = `${chalk.hex(colors.primary)('API Base URL')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.baseUrl)} ${chalk.gray('→')} `
310
+ const baseUrl = await question(rl, baseUrlPrompt)
311
+ if (baseUrl.trim()) {
312
+ config.baseUrl = baseUrl.trim()
313
+ }
314
+
315
+ // 3. API Key
316
+ const currentKeyDisplay = config.apiKey ? maskApiKey(config.apiKey) : '(未设置)'
317
+ const apiKeyPrompt = `${chalk.hex(colors.primary)('API Key')} ${chalk.gray(`(当前: ${currentKeyDisplay})`)}\n${chalk.gray('→')} `
318
+ const apiKey = await question(rl, apiKeyPrompt)
319
+ if (apiKey.trim()) {
320
+ config.apiKey = apiKey.trim()
321
+ }
322
+
323
+ // 4. Model
324
+ const modelPrompt = `${chalk.hex(colors.primary)('Model')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.model)} ${chalk.gray('→')} `
325
+ const model = await question(rl, modelPrompt)
326
+ if (model.trim()) {
327
+ config.model = model.trim()
328
+ }
329
+
330
+ // 5. Shell Hook
331
+ const shellHookPrompt = `${chalk.hex(colors.primary)('启用 Shell Hook')} ${chalk.gray('(记录终端命令历史)')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHook ? 'true' : 'false')} ${chalk.gray('→')} `
332
+ const shellHook = await question(rl, shellHookPrompt)
333
+ if (shellHook.trim()) {
334
+ config.shellHook = shellHook.trim() === 'true'
335
+ }
336
+
337
+ // 6. Edit Mode
338
+ const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)')
339
+ const editModePrompt = `${chalk.hex(colors.primary)('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.editMode)} ${chalk.gray('→')} `
340
+ const editMode = await question(rl, editModePrompt)
341
+ if (editMode.trim()) {
342
+ if (!VALID_EDIT_MODES.includes(editMode.trim() as EditMode)) {
343
+ console.log(chalk.hex(colors.error)(`\n✗ 无效的 editMode,必须是: manual 或 auto`))
344
+ console.log()
345
+ rl.close()
346
+ return
347
+ }
348
+ config.editMode = editMode.trim() as EditMode
349
+ }
350
+
351
+ // 7. Chat History Limit
352
+ const chatHistoryPrompt = `${chalk.hex(colors.primary)('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.chatHistoryLimit)} ${chalk.gray('→')} `
353
+ const chatHistoryLimit = await question(rl, chatHistoryPrompt)
354
+ if (chatHistoryLimit.trim()) {
355
+ const num = parseInt(chatHistoryLimit.trim(), 10)
356
+ if (!isNaN(num) && num > 0) {
357
+ config.chatHistoryLimit = num
358
+ }
359
+ }
360
+
361
+ // 8. Command History Limit
362
+ const commandHistoryPrompt = `${chalk.hex(colors.primary)('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.commandHistoryLimit)} ${chalk.gray('→')} `
363
+ const commandHistoryLimit = await question(rl, commandHistoryPrompt)
364
+ if (commandHistoryLimit.trim()) {
365
+ const num = parseInt(commandHistoryLimit.trim(), 10)
366
+ if (!isNaN(num) && num > 0) {
367
+ config.commandHistoryLimit = num
368
+ }
369
+ }
370
+
371
+ // 9. Shell History Limit
372
+ const shellHistoryPrompt = `${chalk.hex(colors.primary)('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.hex(colors.secondary)(config.shellHistoryLimit)} ${chalk.gray('→')} `
373
+ const shellHistoryLimit = await question(rl, shellHistoryPrompt)
374
+ if (shellHistoryLimit.trim()) {
375
+ const num = parseInt(shellHistoryLimit.trim(), 10)
376
+ if (!isNaN(num) && num > 0) {
377
+ config.shellHistoryLimit = num
378
+ }
379
+ }
380
+
381
+ saveConfig(config)
382
+
383
+ console.log('\n' + chalk.gray('━'.repeat(50)))
384
+ console.log(chalk.hex(getColors().success)('✅ 配置已保存'))
385
+ console.log(chalk.gray(` ${CONFIG_FILE}`))
386
+ console.log()
387
+ } catch (error) {
388
+ const message = error instanceof Error ? error.message : String(error)
389
+ console.log(chalk.hex(getColors().error)(`\n✗ 配置失败: ${message}`))
390
+ console.log()
391
+ } finally {
392
+ rl.close()
393
+ }
394
+ }
package/src/history.ts ADDED
@@ -0,0 +1,160 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import { getConfig } from './config.js'
5
+
6
+ const CONFIG_DIR = path.join(os.homedir(), '.please')
7
+ const HISTORY_FILE = path.join(CONFIG_DIR, 'history.json')
8
+ const MAX_HISTORY = 10
9
+ const MAX_OUTPUT_LENGTH = 500
10
+
11
+ /**
12
+ * 历史记录项
13
+ */
14
+ export interface HistoryRecord {
15
+ userPrompt: string
16
+ command: string
17
+ aiGeneratedCommand?: string
18
+ userModified?: boolean
19
+ executed: boolean
20
+ exitCode: number | null
21
+ output?: string
22
+ reason?: 'builtin' | string
23
+ timestamp?: string
24
+ }
25
+
26
+ /**
27
+ * 确保配置目录存在
28
+ */
29
+ function ensureConfigDir(): void {
30
+ if (!fs.existsSync(CONFIG_DIR)) {
31
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 读取历史记录
37
+ */
38
+ export function getHistory(): HistoryRecord[] {
39
+ ensureConfigDir()
40
+
41
+ if (!fs.existsSync(HISTORY_FILE)) {
42
+ return []
43
+ }
44
+
45
+ try {
46
+ const content = fs.readFileSync(HISTORY_FILE, 'utf-8')
47
+ return JSON.parse(content) as HistoryRecord[]
48
+ } catch {
49
+ return []
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 保存历史记录
55
+ */
56
+ function saveHistory(history: HistoryRecord[]): void {
57
+ ensureConfigDir()
58
+ fs.writeFileSync(HISTORY_FILE, JSON.stringify(history, null, 2))
59
+ }
60
+
61
+ /**
62
+ * 添加一条历史记录
63
+ */
64
+ export function addHistory(record: HistoryRecord): void {
65
+ const config = getConfig()
66
+ const history = getHistory()
67
+
68
+ // 截断输出
69
+ if (record.output && record.output.length > MAX_OUTPUT_LENGTH) {
70
+ record.output = record.output.slice(0, MAX_OUTPUT_LENGTH) + '...(截断)'
71
+ }
72
+
73
+ // 添加时间戳
74
+ record.timestamp = new Date().toISOString()
75
+
76
+ // 添加到开头
77
+ history.unshift(record)
78
+
79
+ // 保留最近 N 条(从配置读取)
80
+ const maxHistory = config.commandHistoryLimit || MAX_HISTORY
81
+ if (history.length > maxHistory) {
82
+ history.length = maxHistory
83
+ }
84
+
85
+ saveHistory(history)
86
+ }
87
+
88
+ /**
89
+ * 清空历史记录
90
+ */
91
+ export function clearHistory(): void {
92
+ saveHistory([])
93
+ }
94
+
95
+ /**
96
+ * 格式化历史记录供 AI 使用
97
+ */
98
+ export function formatHistoryForAI(): string {
99
+ const history = getHistory()
100
+
101
+ if (history.length === 0) {
102
+ return ''
103
+ }
104
+
105
+ const lines = history
106
+ .map((item, index) => {
107
+ const timeAgo = getTimeAgo(item.timestamp || '')
108
+
109
+ let status: string
110
+ if (item.executed) {
111
+ status = item.exitCode === 0 ? '✓' : `✗ 退出码:${item.exitCode}`
112
+ } else if (item.reason === 'builtin') {
113
+ status = '(包含 builtin,未执行)'
114
+ } else {
115
+ status = '(用户取消执行)'
116
+ }
117
+
118
+ // 检查是否用户修改了命令
119
+ if (item.userModified && item.aiGeneratedCommand) {
120
+ // 用户修改了命令
121
+ return `${index + 1}. [${timeAgo}] "${item.userPrompt}" → AI 生成: ${item.aiGeneratedCommand} / 用户修改为: ${item.command} ${status}`
122
+ } else {
123
+ // 未修改,使用原格式
124
+ let line = `${index + 1}. [${timeAgo}] "${item.userPrompt}" → ${item.command} ${status}`
125
+
126
+ // 如果有输出且命令失败,附加输出摘要
127
+ if (item.output && item.exitCode !== 0) {
128
+ line += `\n 输出: ${item.output.split('\n')[0]}` // 只取第一行
129
+ }
130
+
131
+ return line
132
+ }
133
+ })
134
+ .reverse() // 从旧到新排列
135
+
136
+ return `【最近通过 pls 执行的命令】\n${lines.join('\n')}`
137
+ }
138
+
139
+ /**
140
+ * 计算时间差的友好显示
141
+ */
142
+ function getTimeAgo(timestamp: string): string {
143
+ if (!timestamp) return '未知'
144
+
145
+ const now = Date.now()
146
+ const then = new Date(timestamp).getTime()
147
+ const diff = Math.floor((now - then) / 1000)
148
+
149
+ if (diff < 60) return '刚刚'
150
+ if (diff < 3600) return `${Math.floor(diff / 60)}分钟前`
151
+ if (diff < 86400) return `${Math.floor(diff / 3600)}小时前`
152
+ return `${Math.floor(diff / 86400)}天前`
153
+ }
154
+
155
+ /**
156
+ * 获取历史记录文件路径(供显示用)
157
+ */
158
+ export function getHistoryFilePath(): string {
159
+ return HISTORY_FILE
160
+ }
@@ -1,6 +1,6 @@
1
1
  import { Agent } from '@mastra/core'
2
2
  import { getConfig } from './config.js'
3
- import { buildSystemPrompt } from './ai.js'
3
+ import { buildCommandSystemPrompt } from './prompts.js'
4
4
  import { formatSystemInfo } from './sysinfo.js'
5
5
  import { formatHistoryForAI } from './history.js'
6
6
  import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
@@ -8,7 +8,6 @@ import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
8
8
  /**
9
9
  * 创建 Mastra Shell Agent
10
10
  * 根据用户配置的 API Key、Base URL、Provider 和 Model
11
- * 使用 ai.js 中的统一提示词
12
11
  */
13
12
  export function createShellAgent() {
14
13
  const config = getConfig()
@@ -16,12 +15,12 @@ export function createShellAgent() {
16
15
  // 组合 provider/model 格式(Mastra 要求)
17
16
  const modelId = `${config.provider}/${config.model}` as `${string}/${string}`
18
17
 
19
- // 构建系统提示词(使用 ai.js 中的统一函数)
18
+ // 构建系统提示词
20
19
  const sysinfo = formatSystemInfo()
21
20
  const plsHistory = formatHistoryForAI()
22
21
  const shellHistory = formatShellHistoryForAI()
23
22
  const shellHookEnabled = config.shellHook && getShellHistory().length > 0
24
- const systemPrompt = buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
23
+ const systemPrompt = buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
25
24
 
26
25
  return new Agent({
27
26
  name: 'shell-commander',
@@ -0,0 +1,124 @@
1
+ import { Agent } from '@mastra/core'
2
+ import { getConfig } from './config.js'
3
+ import { buildChatSystemPrompt } from './prompts.js'
4
+ import { formatSystemInfo } from './sysinfo.js'
5
+ import { formatHistoryForAI } from './history.js'
6
+ import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js'
7
+ import { getChatHistory, addChatMessage } from './chat-history.js'
8
+
9
+ /**
10
+ * 创建 Mastra Chat Agent
11
+ */
12
+ export function createChatAgent() {
13
+ const config = getConfig()
14
+
15
+ // 组合 provider/model 格式(Mastra 要求)
16
+ const modelId = `${config.provider}/${config.model}` as `${string}/${string}`
17
+
18
+ // 构建系统提示词
19
+ const sysinfo = formatSystemInfo()
20
+ const plsHistory = formatHistoryForAI()
21
+ const shellHistory = formatShellHistoryForAI()
22
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0
23
+ const systemPrompt = buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
24
+
25
+ return new Agent({
26
+ name: 'chat-assistant',
27
+ instructions: systemPrompt,
28
+ model: {
29
+ url: config.baseUrl,
30
+ id: modelId,
31
+ apiKey: config.apiKey,
32
+ },
33
+ })
34
+ }
35
+
36
+ /**
37
+ * 获取完整的系统提示词(用于调试)
38
+ */
39
+ export function getChatSystemPrompt(): string {
40
+ const config = getConfig()
41
+ const sysinfo = formatSystemInfo()
42
+ const plsHistory = formatHistoryForAI()
43
+ const shellHistory = formatShellHistoryForAI()
44
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0
45
+ return buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
46
+ }
47
+
48
+ /**
49
+ * 使用 Mastra 进行 AI 对话(支持流式输出)
50
+ */
51
+ export async function chatWithMastra(
52
+ prompt: string,
53
+ options: {
54
+ debug?: boolean
55
+ onChunk?: (chunk: string) => void
56
+ } = {}
57
+ ): Promise<{
58
+ reply: string
59
+ debug?: {
60
+ sysinfo: string
61
+ model: string
62
+ systemPrompt: string
63
+ chatHistory: Array<{ role: string; content: string }>
64
+ userPrompt: string
65
+ }
66
+ }> {
67
+ const config = getConfig()
68
+ const agent = createChatAgent()
69
+
70
+ // 获取对话历史
71
+ const chatHistory = getChatHistory()
72
+
73
+ // 构建消息数组(将历史和新消息合并)
74
+ const messages: string[] = []
75
+
76
+ // 添加历史对话
77
+ for (const msg of chatHistory) {
78
+ messages.push(msg.content)
79
+ }
80
+
81
+ // 添加当前用户消息
82
+ messages.push(prompt)
83
+
84
+ let fullContent = ''
85
+
86
+ // 流式输出模式
87
+ if (options.onChunk) {
88
+ const stream = await agent.stream(messages)
89
+
90
+ for await (const chunk of stream.textStream) {
91
+ if (chunk) {
92
+ fullContent += chunk
93
+ options.onChunk(chunk)
94
+ }
95
+ }
96
+ } else {
97
+ // 非流式模式
98
+ const response = await agent.generate(messages)
99
+ fullContent = response.text || ''
100
+ }
101
+
102
+ if (!fullContent) {
103
+ throw new Error('AI 返回了空的响应')
104
+ }
105
+
106
+ // 保存对话历史
107
+ addChatMessage(prompt, fullContent)
108
+
109
+ // 返回结果
110
+ if (options.debug) {
111
+ return {
112
+ reply: fullContent,
113
+ debug: {
114
+ sysinfo: formatSystemInfo(),
115
+ model: config.model,
116
+ systemPrompt: getChatSystemPrompt(),
117
+ chatHistory,
118
+ userPrompt: prompt,
119
+ },
120
+ }
121
+ }
122
+
123
+ return { reply: fullContent }
124
+ }