@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
@@ -3,18 +3,37 @@ 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
+ // 配置文件路径
7
+ export const CONFIG_DIR = path.join(os.homedir(), '.please');
7
8
  const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+ // 支持的 Provider 列表
10
+ const VALID_PROVIDERS = [
11
+ 'openai',
12
+ 'anthropic',
13
+ 'deepseek',
14
+ 'google',
15
+ 'groq',
16
+ 'mistral',
17
+ 'cohere',
18
+ 'fireworks',
19
+ 'together',
20
+ ];
21
+ // 编辑模式
22
+ const VALID_EDIT_MODES = ['manual', 'auto'];
23
+ /**
24
+ * 默认配置
25
+ */
8
26
  const DEFAULT_CONFIG = {
9
27
  apiKey: '',
10
28
  baseUrl: 'https://api.openai.com/v1',
11
29
  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 对话历史保留轮数
30
+ provider: 'openai',
31
+ shellHook: false,
32
+ chatHistoryLimit: 10,
33
+ commandHistoryLimit: 10,
34
+ shellHistoryLimit: 15,
35
+ editMode: 'manual',
15
36
  };
16
- // 导出配置目录路径
17
- export { CONFIG_DIR };
18
37
  /**
19
38
  * 确保配置目录存在
20
39
  */
@@ -56,25 +75,31 @@ export function setConfigValue(key, value) {
56
75
  }
57
76
  // 处理特殊类型
58
77
  if (key === 'shellHook') {
59
- config[key] = value === 'true' || value === true;
78
+ config.shellHook = value === 'true' || value === true;
60
79
  }
61
- else if (key === 'chatHistoryLimit') {
62
- const num = parseInt(value, 10);
80
+ else if (key === 'chatHistoryLimit' || key === 'commandHistoryLimit' || key === 'shellHistoryLimit') {
81
+ const num = typeof value === 'number' ? value : parseInt(String(value), 10);
63
82
  if (isNaN(num) || num < 1) {
64
- throw new Error('chatHistoryLimit 必须是大于 0 的整数');
83
+ throw new Error(`${key} 必须是大于 0 的整数`);
65
84
  }
66
85
  config[key] = num;
67
86
  }
68
87
  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(', ')}`);
88
+ const strValue = String(value);
89
+ if (!VALID_PROVIDERS.includes(strValue)) {
90
+ throw new Error(`provider 必须是以下之一: ${VALID_PROVIDERS.join(', ')}`);
73
91
  }
74
- config[key] = value;
92
+ config.provider = strValue;
75
93
  }
76
- else {
77
- config[key] = value;
94
+ else if (key === 'editMode') {
95
+ const strValue = String(value);
96
+ if (!VALID_EDIT_MODES.includes(strValue)) {
97
+ throw new Error(`editMode 必须是以下之一: ${VALID_EDIT_MODES.join(', ')}`);
98
+ }
99
+ config.editMode = strValue;
100
+ }
101
+ else if (key === 'apiKey' || key === 'baseUrl' || key === 'model') {
102
+ config[key] = String(value);
78
103
  }
79
104
  saveConfig(config);
80
105
  return config;
@@ -84,7 +109,7 @@ export function setConfigValue(key, value) {
84
109
  */
85
110
  export function isConfigValid() {
86
111
  const config = getConfig();
87
- return config.apiKey && config.apiKey.length > 0;
112
+ return config.apiKey.length > 0;
88
113
  }
89
114
  /**
90
115
  * 隐藏 API Key 中间部分
@@ -100,14 +125,17 @@ export function maskApiKey(apiKey) {
100
125
  export function displayConfig() {
101
126
  const config = getConfig();
102
127
  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)));
128
+ console.log(chalk.gray('━'.repeat(50)));
129
+ console.log(` ${chalk.cyan('apiKey')}: ${maskApiKey(config.apiKey)}`);
130
+ console.log(` ${chalk.cyan('baseUrl')}: ${config.baseUrl}`);
131
+ console.log(` ${chalk.cyan('provider')}: ${config.provider}`);
132
+ console.log(` ${chalk.cyan('model')}: ${config.model}`);
133
+ console.log(` ${chalk.cyan('shellHook')}: ${config.shellHook ? chalk.green('已启用') : chalk.gray('未启用')}`);
134
+ console.log(` ${chalk.cyan('editMode')}: ${config.editMode === 'auto' ? chalk.hex('#00D9FF')('auto (自动编辑)') : chalk.gray('manual (按E编辑)')}`);
135
+ console.log(` ${chalk.cyan('chatHistoryLimit')}: ${config.chatHistoryLimit} 轮`);
136
+ console.log(` ${chalk.cyan('commandHistoryLimit')}: ${config.commandHistoryLimit} 条`);
137
+ console.log(` ${chalk.cyan('shellHistoryLimit')}: ${config.shellHistoryLimit} 条`);
138
+ console.log(chalk.gray('━'.repeat(50)));
111
139
  console.log(chalk.gray(`配置文件: ${CONFIG_FILE}\n`));
112
140
  }
113
141
  /**
@@ -116,7 +144,7 @@ export function displayConfig() {
116
144
  function createReadlineInterface() {
117
145
  return readline.createInterface({
118
146
  input: process.stdin,
119
- output: process.stdout
147
+ output: process.stdout,
120
148
  });
121
149
  }
122
150
  /**
@@ -140,13 +168,12 @@ export async function runConfigWizard() {
140
168
  console.log(chalk.gray('直接回车使用默认值,输入值后回车确认\n'));
141
169
  try {
142
170
  // 1. Provider
143
- const validProviders = ['openai', 'anthropic', 'deepseek', 'google', 'groq', 'mistral', 'cohere', 'fireworks', 'together'];
144
- const providerHint = chalk.gray(`(可选: ${validProviders.join(', ')})`);
171
+ const providerHint = chalk.gray(`(可选: ${VALID_PROVIDERS.join(', ')})`);
145
172
  const providerPrompt = `${chalk.cyan('Provider')} ${providerHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.provider)} ${chalk.gray('→')} `;
146
173
  const provider = await question(rl, providerPrompt);
147
174
  if (provider.trim()) {
148
- if (!validProviders.includes(provider.trim())) {
149
- console.log(chalk.hex('#EF4444')(`\n✗ 无效的 provider,必须是以下之一: ${validProviders.join(', ')}`));
175
+ if (!VALID_PROVIDERS.includes(provider.trim())) {
176
+ console.log(chalk.hex('#EF4444')(`\n✗ 无效的 provider,必须是以下之一: ${VALID_PROVIDERS.join(', ')}`));
150
177
  console.log();
151
178
  rl.close();
152
179
  return;
@@ -178,7 +205,20 @@ export async function runConfigWizard() {
178
205
  if (shellHook.trim()) {
179
206
  config.shellHook = shellHook.trim() === 'true';
180
207
  }
181
- // 6. Chat History Limit
208
+ // 6. Edit Mode
209
+ const editModeHint = chalk.gray('(manual=按E编辑, auto=自动编辑)');
210
+ const editModePrompt = `${chalk.cyan('编辑模式')} ${editModeHint}\n${chalk.gray('默认:')} ${chalk.yellow(config.editMode)} ${chalk.gray('→')} `;
211
+ const editMode = await question(rl, editModePrompt);
212
+ if (editMode.trim()) {
213
+ if (!VALID_EDIT_MODES.includes(editMode.trim())) {
214
+ console.log(chalk.hex('#EF4444')(`\n✗ 无效的 editMode,必须是: manual 或 auto`));
215
+ console.log();
216
+ rl.close();
217
+ return;
218
+ }
219
+ config.editMode = editMode.trim();
220
+ }
221
+ // 7. Chat History Limit
182
222
  const chatHistoryPrompt = `${chalk.cyan('Chat 历史保留轮数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.chatHistoryLimit)} ${chalk.gray('→')} `;
183
223
  const chatHistoryLimit = await question(rl, chatHistoryPrompt);
184
224
  if (chatHistoryLimit.trim()) {
@@ -187,6 +227,24 @@ export async function runConfigWizard() {
187
227
  config.chatHistoryLimit = num;
188
228
  }
189
229
  }
230
+ // 8. Command History Limit
231
+ const commandHistoryPrompt = `${chalk.cyan('命令历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.commandHistoryLimit)} ${chalk.gray('→')} `;
232
+ const commandHistoryLimit = await question(rl, commandHistoryPrompt);
233
+ if (commandHistoryLimit.trim()) {
234
+ const num = parseInt(commandHistoryLimit.trim(), 10);
235
+ if (!isNaN(num) && num > 0) {
236
+ config.commandHistoryLimit = num;
237
+ }
238
+ }
239
+ // 9. Shell History Limit
240
+ const shellHistoryPrompt = `${chalk.cyan('Shell 历史保留条数')}\n${chalk.gray('默认:')} ${chalk.yellow(config.shellHistoryLimit)} ${chalk.gray('→')} `;
241
+ const shellHistoryLimit = await question(rl, shellHistoryPrompt);
242
+ if (shellHistoryLimit.trim()) {
243
+ const num = parseInt(shellHistoryLimit.trim(), 10);
244
+ if (!isNaN(num) && num > 0) {
245
+ config.shellHistoryLimit = num;
246
+ }
247
+ }
190
248
  saveConfig(config);
191
249
  console.log('\n' + chalk.gray('━'.repeat(50)));
192
250
  console.log(chalk.hex('#10B981')('✅ 配置已保存'));
@@ -194,7 +252,8 @@ export async function runConfigWizard() {
194
252
  console.log();
195
253
  }
196
254
  catch (error) {
197
- console.log(chalk.hex('#EF4444')(`\n✗ 配置失败: ${error.message}`));
255
+ const message = error instanceof Error ? error.message : String(error);
256
+ console.log(chalk.hex('#EF4444')(`\n✗ 配置失败: ${message}`));
198
257
  console.log();
199
258
  }
200
259
  finally {
@@ -1,20 +1,34 @@
1
+ /**
2
+ * 历史记录项
3
+ */
4
+ export interface HistoryRecord {
5
+ userPrompt: string;
6
+ command: string;
7
+ aiGeneratedCommand?: string;
8
+ userModified?: boolean;
9
+ executed: boolean;
10
+ exitCode: number | null;
11
+ output?: string;
12
+ reason?: 'builtin' | string;
13
+ timestamp?: string;
14
+ }
1
15
  /**
2
16
  * 读取历史记录
3
17
  */
4
- export function getHistory(): any;
18
+ export declare function getHistory(): HistoryRecord[];
5
19
  /**
6
20
  * 添加一条历史记录
7
21
  */
8
- export function addHistory(record: any): void;
22
+ export declare function addHistory(record: HistoryRecord): void;
9
23
  /**
10
24
  * 清空历史记录
11
25
  */
12
- export function clearHistory(): void;
26
+ export declare function clearHistory(): void;
13
27
  /**
14
28
  * 格式化历史记录供 AI 使用
15
29
  */
16
- export function formatHistoryForAI(): string;
30
+ export declare function formatHistoryForAI(): string;
17
31
  /**
18
32
  * 获取历史记录文件路径(供显示用)
19
33
  */
20
- export function getHistoryFilePath(): string;
34
+ export declare function getHistoryFilePath(): string;
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import os from 'os';
4
+ import { getConfig } from './config.js';
4
5
  const CONFIG_DIR = path.join(os.homedir(), '.please');
5
6
  const HISTORY_FILE = path.join(CONFIG_DIR, 'history.json');
6
7
  const MAX_HISTORY = 10;
@@ -40,6 +41,7 @@ function saveHistory(history) {
40
41
  * 添加一条历史记录
41
42
  */
42
43
  export function addHistory(record) {
44
+ const config = getConfig();
43
45
  const history = getHistory();
44
46
  // 截断输出
45
47
  if (record.output && record.output.length > MAX_OUTPUT_LENGTH) {
@@ -49,9 +51,10 @@ export function addHistory(record) {
49
51
  record.timestamp = new Date().toISOString();
50
52
  // 添加到开头
51
53
  history.unshift(record);
52
- // 保留最近 N
53
- if (history.length > MAX_HISTORY) {
54
- history.length = MAX_HISTORY;
54
+ // 保留最近 N 条(从配置读取)
55
+ const maxHistory = config.commandHistoryLimit || MAX_HISTORY;
56
+ if (history.length > maxHistory) {
57
+ history.length = maxHistory;
55
58
  }
56
59
  saveHistory(history);
57
60
  }
@@ -69,8 +72,9 @@ export function formatHistoryForAI() {
69
72
  if (history.length === 0) {
70
73
  return '';
71
74
  }
72
- const lines = history.map((item, index) => {
73
- const timeAgo = getTimeAgo(item.timestamp);
75
+ const lines = history
76
+ .map((item, index) => {
77
+ const timeAgo = getTimeAgo(item.timestamp || '');
74
78
  let status;
75
79
  if (item.executed) {
76
80
  status = item.exitCode === 0 ? '✓' : `✗ 退出码:${item.exitCode}`;
@@ -81,19 +85,30 @@ export function formatHistoryForAI() {
81
85
  else {
82
86
  status = '(用户取消执行)';
83
87
  }
84
- let line = `${index + 1}. [${timeAgo}] "${item.userPrompt}" → ${item.command} ${status}`;
85
- // 如果有输出且命令失败,附加输出摘要
86
- if (item.output && item.exitCode !== 0) {
87
- line += `\n 输出: ${item.output.split('\n')[0]}`; // 只取第一行
88
+ // 检查是否用户修改了命令
89
+ if (item.userModified && item.aiGeneratedCommand) {
90
+ // 用户修改了命令
91
+ return `${index + 1}. [${timeAgo}] "${item.userPrompt}" → AI 生成: ${item.aiGeneratedCommand} / 用户修改为: ${item.command} ${status}`;
88
92
  }
89
- return line;
90
- }).reverse(); // 从旧到新排列
93
+ else {
94
+ // 未修改,使用原格式
95
+ let line = `${index + 1}. [${timeAgo}] "${item.userPrompt}" → ${item.command} ${status}`;
96
+ // 如果有输出且命令失败,附加输出摘要
97
+ if (item.output && item.exitCode !== 0) {
98
+ line += `\n 输出: ${item.output.split('\n')[0]}`; // 只取第一行
99
+ }
100
+ return line;
101
+ }
102
+ })
103
+ .reverse(); // 从旧到新排列
91
104
  return `【最近通过 pls 执行的命令】\n${lines.join('\n')}`;
92
105
  }
93
106
  /**
94
107
  * 计算时间差的友好显示
95
108
  */
96
109
  function getTimeAgo(timestamp) {
110
+ if (!timestamp)
111
+ return '未知';
97
112
  const now = Date.now();
98
113
  const then = new Date(timestamp).getTime();
99
114
  const diff = Math.floor((now - then) / 1000);
@@ -2,6 +2,5 @@ import { Agent } from '@mastra/core';
2
2
  /**
3
3
  * 创建 Mastra Shell Agent
4
4
  * 根据用户配置的 API Key、Base URL、Provider 和 Model
5
- * 使用 ai.js 中的统一提示词
6
5
  */
7
6
  export declare function createShellAgent(): Agent<"shell-commander", Record<string, import("@mastra/core").ToolAction<any, any, any, any, import("@mastra/core").ToolExecutionContext<any, any, any>>>, Record<string, import("@mastra/core").Metric>>;
@@ -1,24 +1,23 @@
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';
7
7
  /**
8
8
  * 创建 Mastra Shell Agent
9
9
  * 根据用户配置的 API Key、Base URL、Provider 和 Model
10
- * 使用 ai.js 中的统一提示词
11
10
  */
12
11
  export function createShellAgent() {
13
12
  const config = getConfig();
14
13
  // 组合 provider/model 格式(Mastra 要求)
15
14
  const modelId = `${config.provider}/${config.model}`;
16
- // 构建系统提示词(使用 ai.js 中的统一函数)
15
+ // 构建系统提示词
17
16
  const sysinfo = formatSystemInfo();
18
17
  const plsHistory = formatHistoryForAI();
19
18
  const shellHistory = formatShellHistoryForAI();
20
19
  const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
21
- const systemPrompt = buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
20
+ const systemPrompt = buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
22
21
  return new Agent({
23
22
  name: 'shell-commander',
24
23
  instructions: systemPrompt,
@@ -0,0 +1,28 @@
1
+ import { Agent } from '@mastra/core';
2
+ /**
3
+ * 创建 Mastra Chat Agent
4
+ */
5
+ export declare function createChatAgent(): Agent<"chat-assistant", Record<string, import("@mastra/core").ToolAction<any, any, any, any, import("@mastra/core").ToolExecutionContext<any, any, any>>>, Record<string, import("@mastra/core").Metric>>;
6
+ /**
7
+ * 获取完整的系统提示词(用于调试)
8
+ */
9
+ export declare function getChatSystemPrompt(): string;
10
+ /**
11
+ * 使用 Mastra 进行 AI 对话(支持流式输出)
12
+ */
13
+ export declare function chatWithMastra(prompt: string, options?: {
14
+ debug?: boolean;
15
+ onChunk?: (chunk: string) => void;
16
+ }): Promise<{
17
+ reply: string;
18
+ debug?: {
19
+ sysinfo: string;
20
+ model: string;
21
+ systemPrompt: string;
22
+ chatHistory: Array<{
23
+ role: string;
24
+ content: string;
25
+ }>;
26
+ userPrompt: string;
27
+ };
28
+ }>;
@@ -0,0 +1,93 @@
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
+ * 创建 Mastra Chat Agent
10
+ */
11
+ export function createChatAgent() {
12
+ const config = getConfig();
13
+ // 组合 provider/model 格式(Mastra 要求)
14
+ const modelId = `${config.provider}/${config.model}`;
15
+ // 构建系统提示词
16
+ const sysinfo = formatSystemInfo();
17
+ const plsHistory = formatHistoryForAI();
18
+ const shellHistory = formatShellHistoryForAI();
19
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
20
+ const systemPrompt = buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
21
+ return new Agent({
22
+ name: 'chat-assistant',
23
+ instructions: systemPrompt,
24
+ model: {
25
+ url: config.baseUrl,
26
+ id: modelId,
27
+ apiKey: config.apiKey,
28
+ },
29
+ });
30
+ }
31
+ /**
32
+ * 获取完整的系统提示词(用于调试)
33
+ */
34
+ export function getChatSystemPrompt() {
35
+ const config = getConfig();
36
+ const sysinfo = formatSystemInfo();
37
+ const plsHistory = formatHistoryForAI();
38
+ const shellHistory = formatShellHistoryForAI();
39
+ const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
40
+ return buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
41
+ }
42
+ /**
43
+ * 使用 Mastra 进行 AI 对话(支持流式输出)
44
+ */
45
+ export async function chatWithMastra(prompt, options = {}) {
46
+ const config = getConfig();
47
+ const agent = createChatAgent();
48
+ // 获取对话历史
49
+ const chatHistory = getChatHistory();
50
+ // 构建消息数组(将历史和新消息合并)
51
+ const messages = [];
52
+ // 添加历史对话
53
+ for (const msg of chatHistory) {
54
+ messages.push(msg.content);
55
+ }
56
+ // 添加当前用户消息
57
+ messages.push(prompt);
58
+ let fullContent = '';
59
+ // 流式输出模式
60
+ if (options.onChunk) {
61
+ const stream = await agent.stream(messages);
62
+ for await (const chunk of stream.textStream) {
63
+ if (chunk) {
64
+ fullContent += chunk;
65
+ options.onChunk(chunk);
66
+ }
67
+ }
68
+ }
69
+ else {
70
+ // 非流式模式
71
+ const response = await agent.generate(messages);
72
+ fullContent = response.text || '';
73
+ }
74
+ if (!fullContent) {
75
+ throw new Error('AI 返回了空的响应');
76
+ }
77
+ // 保存对话历史
78
+ addChatMessage(prompt, fullContent);
79
+ // 返回结果
80
+ if (options.debug) {
81
+ return {
82
+ reply: fullContent,
83
+ debug: {
84
+ sysinfo: formatSystemInfo(),
85
+ model: config.model,
86
+ systemPrompt: getChatSystemPrompt(),
87
+ chatHistory,
88
+ userPrompt: prompt,
89
+ },
90
+ };
91
+ }
92
+ return { reply: fullContent };
93
+ }
@@ -9,13 +9,13 @@ export declare const CommandStepSchema: z.ZodObject<{
9
9
  nextStepHint: z.ZodOptional<z.ZodString>;
10
10
  }, "strip", z.ZodTypeAny, {
11
11
  command: string;
12
- continue?: boolean | undefined;
13
12
  reasoning?: string | undefined;
13
+ continue?: boolean | undefined;
14
14
  nextStepHint?: string | undefined;
15
15
  }, {
16
16
  command: string;
17
- continue?: boolean | undefined;
18
17
  reasoning?: string | undefined;
18
+ continue?: boolean | undefined;
19
19
  nextStepHint?: string | undefined;
20
20
  }>;
21
21
  export type CommandStep = z.infer<typeof CommandStepSchema>;
@@ -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';
@@ -23,7 +23,7 @@ export function getFullSystemPrompt() {
23
23
  const plsHistory = formatHistoryForAI();
24
24
  const shellHistory = formatShellHistoryForAI();
25
25
  const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
26
- return buildSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
26
+ return buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled);
27
27
  }
28
28
  /**
29
29
  * 使用 Mastra 生成多步骤命令
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 统一管理所有 AI 系统提示词
3
+ */
4
+ /**
5
+ * 构建命令生成模式的系统提示词
6
+ */
7
+ export declare function buildCommandSystemPrompt(sysinfo: string, plsHistory: string, shellHistory: string, shellHookEnabled: boolean): string;
8
+ /**
9
+ * 构建 Chat 对话模式的系统提示词
10
+ */
11
+ export declare function buildChatSystemPrompt(sysinfo: string, plsHistory: string, shellHistory: string, shellHookEnabled: boolean): string;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * 统一管理所有 AI 系统提示词
3
+ */
4
+ /**
5
+ * 构建命令生成模式的系统提示词
6
+ */
7
+ export function buildCommandSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled) {
8
+ let prompt = `你是一个专业的 shell 脚本生成器。用户会提供他们的系统信息和一个命令需求。
9
+ 你的任务是返回一个可执行的、原始的 shell 命令或脚本来完成他们的目标。
10
+
11
+ 重要规则:
12
+ 1. 返回 JSON 格式,command 字段必须是可直接执行的命令(无解释、无注释、无 markdown)
13
+ 2. 不要添加 shebang(如 #!/bin/bash)
14
+ 3. command 可以包含多条命令(用 && 连接),但整体算一个命令
15
+ 4. 根据用户的系统信息选择合适的命令(如包管理器)
16
+ 5. 如果用户引用了之前的操作(如"刚才的"、"上一个"),请参考历史记录
17
+ 6. 绝对不要输出 pls 或 please 命令!
18
+
19
+ 【输出格式 - 非常重要】
20
+
21
+ 单步模式(一个命令完成):
22
+ 如果任务只需要一个命令就能完成,只返回:
23
+ {
24
+ "command": "ls -la"
25
+ }
26
+
27
+ 多步模式(需要多个命令,后续依赖前面的结果):
28
+ 如果任务需要多个命令,且后续命令必须根据前面的执行结果来决定,则返回:
29
+
30
+ 【多步骤完整示例】
31
+ 用户:"查找大于100MB的日志文件并压缩"
32
+
33
+ 第一步你返回:
34
+ {
35
+ "command": "find . -name '*.log' -size +100M",
36
+ "continue": true,
37
+ "reasoning": "查找大日志", (精简即可)
38
+ "nextStepHint": "压缩找到的文件" (精简即可)
39
+ }
40
+
41
+ 执行后你会收到:
42
+ 命令已执行
43
+ 退出码: 0
44
+ 输出:
45
+ ./app.log
46
+ ./system.log
47
+
48
+ 然后你返回第二步:
49
+ {
50
+ "command": "tar -czf logs.tar.gz ./app.log ./system.log",
51
+ "continue": false,
52
+ "reasoning": "压缩日志文件"
53
+ }
54
+
55
+ 关键判断标准:
56
+ - 多步 = 后续命令依赖前面的输出(如先 find 看有哪些,再根据结果操作具体文件)
57
+ - 单步 = 一个命令就能完成(即使命令里有 && 连接多条,也算一个命令)
58
+
59
+ 常见场景举例:
60
+ - "删除空文件夹" → 单步:find . -empty -delete (一个命令完成)
61
+ - "查找大文件并压缩" → 多步:先 find 看有哪些,再 tar 压缩具体文件
62
+ - "安装 git" → 单步:brew install git
63
+ - "备份并删除旧日志" → 多步:先 mkdir backup,再 mv 文件到 backup
64
+ - "查看目录" → 单步:ls -la
65
+
66
+ 严格要求:单步模式只返回 {"command": "xxx"},绝对不要输出 continue/reasoning/nextStepHint!
67
+
68
+ 【错误处理】
69
+ 如果你收到命令执行失败的信息(退出码非0),你应该:
70
+ 1. 分析错误原因
71
+ 2. 调整命令策略,返回修正后的命令
72
+ 3. 设置 continue: true 重试,或设置 continue: false 放弃
73
+
74
+ 错误处理示例:
75
+ 上一步失败,你收到:
76
+ 命令已执行
77
+ 退出码: 1
78
+ 输出:
79
+ mv: rename ./test.zip to ./c/test.zip: No such file or directory
80
+
81
+ 你分析后返回修正:
82
+ {
83
+ "command": "cp test.zip a/ && cp test.zip b/ && cp test.zip c/",
84
+ "continue": false,
85
+ "reasoning": "改用 cp 复制而非 mv"
86
+ }
87
+
88
+ 或者如果决定放弃(无法修正),返回:
89
+ {
90
+ "command": "",
91
+ "continue": false,
92
+ "reasoning": "文件不存在且无法恢复,任务无法继续"
93
+ }
94
+
95
+ 重要:当 continue: false 且决定放弃时,command 可以留空,重点是在 reasoning 中说明为什么放弃。
96
+
97
+ 关于 pls/please 工具:
98
+ 用户正在使用 pls(pretty-please)工具,这是一个将自然语言转换为 shell 命令的 AI 助手。
99
+ 当用户输入 "pls <描述>" 时,AI(也就是你)会生成对应的 shell 命令供用户确认执行。
100
+ 历史记录中标记为 [pls] 的条目表示用户通过 pls 工具执行的命令。
101
+
102
+ 用户的系统信息:${sysinfo}`;
103
+ // 根据是否启用 shell hook 决定展示哪个历史
104
+ if (shellHookEnabled && shellHistory) {
105
+ prompt += `\n\n${shellHistory}`;
106
+ }
107
+ else if (plsHistory) {
108
+ prompt += `\n\n${plsHistory}`;
109
+ }
110
+ return prompt;
111
+ }
112
+ /**
113
+ * 构建 Chat 对话模式的系统提示词
114
+ */
115
+ export function buildChatSystemPrompt(sysinfo, plsHistory, shellHistory, shellHookEnabled) {
116
+ let prompt = `你是一个命令行专家助手,帮助用户理解和使用命令行工具。
117
+
118
+ 【你的能力】
119
+ - 解释命令的含义、参数、用法
120
+ - 分析命令的执行效果和潜在风险
121
+ - 回答命令行、Shell、系统管理相关问题
122
+ - 根据用户需求推荐合适的命令并解释
123
+
124
+ 【回答要求】
125
+ - 简洁清晰,避免冗余
126
+ - 危险操作要明确警告
127
+ - 适当给出示例命令
128
+ - 结合用户的系统环境给出针对性建议
129
+
130
+ 【用户系统信息】
131
+ ${sysinfo}`;
132
+ // 根据是否启用 shell hook 决定展示哪个历史
133
+ if (shellHookEnabled && shellHistory) {
134
+ prompt += `\n\n${shellHistory}`;
135
+ }
136
+ else if (plsHistory) {
137
+ prompt += `\n\n${plsHistory}`;
138
+ }
139
+ return prompt;
140
+ }