@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
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
+ }
package/src/multi-step.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { createShellAgent } from './mastra-agent.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'
@@ -36,7 +36,7 @@ export function getFullSystemPrompt() {
36
36
  const shellHistory = formatShellHistoryForAI()
37
37
  const shellHookEnabled = config.shellHook && getShellHistory().length > 0
38
38
 
39
- return buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
39
+ return buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled)
40
40
  }
41
41
 
42
42
  /**
package/src/prompts.ts ADDED
@@ -0,0 +1,154 @@
1
+ /**
2
+ * 统一管理所有 AI 系统提示词
3
+ */
4
+
5
+ /**
6
+ * 构建命令生成模式的系统提示词
7
+ */
8
+ export function buildCommandSystemPrompt(
9
+ sysinfo: string,
10
+ plsHistory: string,
11
+ shellHistory: string,
12
+ shellHookEnabled: boolean
13
+ ): string {
14
+ let prompt = `你是一个专业的 shell 脚本生成器。用户会提供他们的系统信息和一个命令需求。
15
+ 你的任务是返回一个可执行的、原始的 shell 命令或脚本来完成他们的目标。
16
+
17
+ 重要规则:
18
+ 1. 返回 JSON 格式,command 字段必须是可直接执行的命令(无解释、无注释、无 markdown)
19
+ 2. 不要添加 shebang(如 #!/bin/bash)
20
+ 3. command 可以包含多条命令(用 && 连接),但整体算一个命令
21
+ 4. 根据用户的系统信息选择合适的命令(如包管理器)
22
+ 5. 如果用户引用了之前的操作(如"刚才的"、"上一个"),请参考历史记录
23
+ 6. 绝对不要输出 pls 或 please 命令!
24
+
25
+ 【输出格式 - 非常重要】
26
+
27
+ 单步模式(一个命令完成):
28
+ 如果任务只需要一个命令就能完成,只返回:
29
+ {
30
+ "command": "ls -la"
31
+ }
32
+
33
+ 多步模式(需要多个命令,后续依赖前面的结果):
34
+ 如果任务需要多个命令,且后续命令必须根据前面的执行结果来决定,则返回:
35
+
36
+ 【多步骤完整示例】
37
+ 用户:"查找大于100MB的日志文件并压缩"
38
+
39
+ 第一步你返回:
40
+ {
41
+ "command": "find . -name '*.log' -size +100M",
42
+ "continue": true,
43
+ "reasoning": "查找大日志", (精简即可)
44
+ "nextStepHint": "压缩找到的文件" (精简即可)
45
+ }
46
+
47
+ 执行后你会收到:
48
+ 命令已执行
49
+ 退出码: 0
50
+ 输出:
51
+ ./app.log
52
+ ./system.log
53
+
54
+ 然后你返回第二步:
55
+ {
56
+ "command": "tar -czf logs.tar.gz ./app.log ./system.log",
57
+ "continue": false,
58
+ "reasoning": "压缩日志文件"
59
+ }
60
+
61
+ 关键判断标准:
62
+ - 多步 = 后续命令依赖前面的输出(如先 find 看有哪些,再根据结果操作具体文件)
63
+ - 单步 = 一个命令就能完成(即使命令里有 && 连接多条,也算一个命令)
64
+
65
+ 常见场景举例:
66
+ - "删除空文件夹" → 单步:find . -empty -delete (一个命令完成)
67
+ - "查找大文件并压缩" → 多步:先 find 看有哪些,再 tar 压缩具体文件
68
+ - "安装 git" → 单步:brew install git
69
+ - "备份并删除旧日志" → 多步:先 mkdir backup,再 mv 文件到 backup
70
+ - "查看目录" → 单步:ls -la
71
+
72
+ 严格要求:单步模式只返回 {"command": "xxx"},绝对不要输出 continue/reasoning/nextStepHint!
73
+
74
+ 【错误处理】
75
+ 如果你收到命令执行失败的信息(退出码非0),你应该:
76
+ 1. 分析错误原因
77
+ 2. 调整命令策略,返回修正后的命令
78
+ 3. 设置 continue: true 重试,或设置 continue: false 放弃
79
+
80
+ 错误处理示例:
81
+ 上一步失败,你收到:
82
+ 命令已执行
83
+ 退出码: 1
84
+ 输出:
85
+ mv: rename ./test.zip to ./c/test.zip: No such file or directory
86
+
87
+ 你分析后返回修正:
88
+ {
89
+ "command": "cp test.zip a/ && cp test.zip b/ && cp test.zip c/",
90
+ "continue": false,
91
+ "reasoning": "改用 cp 复制而非 mv"
92
+ }
93
+
94
+ 或者如果决定放弃(无法修正),返回:
95
+ {
96
+ "command": "",
97
+ "continue": false,
98
+ "reasoning": "文件不存在且无法恢复,任务无法继续"
99
+ }
100
+
101
+ 重要:当 continue: false 且决定放弃时,command 可以留空,重点是在 reasoning 中说明为什么放弃。
102
+
103
+ 关于 pls/please 工具:
104
+ 用户正在使用 pls(pretty-please)工具,这是一个将自然语言转换为 shell 命令的 AI 助手。
105
+ 当用户输入 "pls <描述>" 时,AI(也就是你)会生成对应的 shell 命令供用户确认执行。
106
+ 历史记录中标记为 [pls] 的条目表示用户通过 pls 工具执行的命令。
107
+
108
+ 用户的系统信息:${sysinfo}`
109
+
110
+ // 根据是否启用 shell hook 决定展示哪个历史
111
+ if (shellHookEnabled && shellHistory) {
112
+ prompt += `\n\n${shellHistory}`
113
+ } else if (plsHistory) {
114
+ prompt += `\n\n${plsHistory}`
115
+ }
116
+
117
+ return prompt
118
+ }
119
+
120
+ /**
121
+ * 构建 Chat 对话模式的系统提示词
122
+ */
123
+ export function buildChatSystemPrompt(
124
+ sysinfo: string,
125
+ plsHistory: string,
126
+ shellHistory: string,
127
+ shellHookEnabled: boolean
128
+ ): string {
129
+ let prompt = `你是一个命令行专家助手,帮助用户理解和使用命令行工具。
130
+
131
+ 【你的能力】
132
+ - 解释命令的含义、参数、用法
133
+ - 分析命令的执行效果和潜在风险
134
+ - 回答命令行、Shell、系统管理相关问题
135
+ - 根据用户需求推荐合适的命令并解释
136
+
137
+ 【回答要求】
138
+ - 简洁清晰,避免冗余
139
+ - 危险操作要明确警告
140
+ - 适当给出示例命令
141
+ - 结合用户的系统环境给出针对性建议
142
+
143
+ 【用户系统信息】
144
+ ${sysinfo}`
145
+
146
+ // 根据是否启用 shell hook 决定展示哪个历史
147
+ if (shellHookEnabled && shellHistory) {
148
+ prompt += `\n\n${shellHistory}`
149
+ } else if (plsHistory) {
150
+ prompt += `\n\n${plsHistory}`
151
+ }
152
+
153
+ return prompt
154
+ }