@yivan-lab/pretty-please 1.3.0 → 1.4.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.
package/bin/pls.tsx CHANGED
@@ -139,7 +139,7 @@ function executeCommand(command: string): Promise<{ exitCode: number; output: st
139
139
  ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
140
140
  console2.getDisplayWidth('生成命令')
141
141
  )
142
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2)
142
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
143
143
  console2.printSeparator('输出', boxWidth)
144
144
 
145
145
  // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
@@ -1240,7 +1240,7 @@ program
1240
1240
  .argument('[prompt...]', '自然语言描述你想执行的操作')
1241
1241
  .option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
1242
1242
  .option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
1243
- .action((promptArgs, options) => {
1243
+ .action(async (promptArgs, options) => {
1244
1244
  // 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
1245
1245
  if (typeof options.remote === 'string' && !getRemote(options.remote)) {
1246
1246
  // "查看当前目录" 不是服务器名,放回 prompt
@@ -1248,12 +1248,32 @@ program
1248
1248
  options.remote = true // 改为使用默认服务器
1249
1249
  }
1250
1250
 
1251
+ let prompt = ''
1252
+
1251
1253
  if (promptArgs.length === 0) {
1252
- program.help()
1253
- return
1254
- }
1254
+ // 无参数时:尝试自动检测上一条失败的命令
1255
+ const { getLastNonPlsCommand } = await import('../src/shell-hook.js')
1256
+ const lastCmd = getLastNonPlsCommand()
1257
+
1258
+ if (lastCmd && lastCmd.exit !== 0) {
1259
+ // 找到了失败的命令,自动生成 prompt
1260
+ prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`
1261
+
1262
+ if (options.debug) {
1263
+ console.log('')
1264
+ console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`)
1265
+ console2.muted(`生成 prompt: ${prompt}`)
1266
+ }
1255
1267
 
1256
- let prompt = promptArgs.join(' ')
1268
+ // 继续执行命令生成流程(不 return)
1269
+ } else {
1270
+ // 没有失败的命令,显示帮助
1271
+ program.help()
1272
+ return
1273
+ }
1274
+ } else {
1275
+ prompt = promptArgs.join(' ')
1276
+ }
1257
1277
 
1258
1278
  if (!prompt.trim()) {
1259
1279
  console.log('')
@@ -1712,7 +1732,7 @@ async function executeRemoteCommand(
1712
1732
  ...wrappedLines.map((l) => console2.getDisplayWidth(l)),
1713
1733
  console2.getDisplayWidth('生成命令')
1714
1734
  )
1715
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2)
1735
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
1716
1736
  console2.printSeparator(`远程输出 (${remoteName})`, boxWidth)
1717
1737
 
1718
1738
  try {
package/dist/bin/pls.js CHANGED
@@ -83,7 +83,7 @@ function executeCommand(command) {
83
83
  wrappedLines.push(...console2.wrapText(line, maxContentWidth));
84
84
  }
85
85
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
86
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2);
86
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
87
87
  console2.printSeparator('输出', boxWidth);
88
88
  // 使用 bash 并启用 pipefail,确保管道中任何命令失败都能正确返回非零退出码
89
89
  const child = exec(`set -o pipefail; ${command}`, { shell: '/bin/bash' });
@@ -1030,18 +1030,37 @@ program
1030
1030
  .argument('[prompt...]', '自然语言描述你想执行的操作')
1031
1031
  .option('-d, --debug', '显示调试信息(系统信息、完整 prompt 等)')
1032
1032
  .option('-r, --remote [name]', '在远程服务器上执行(不指定则使用默认服务器)')
1033
- .action((promptArgs, options) => {
1033
+ .action(async (promptArgs, options) => {
1034
1034
  // 智能处理 -r 参数:如果 -r 后面的值不是已注册的服务器名,把它当作 prompt 的一部分
1035
1035
  if (typeof options.remote === 'string' && !getRemote(options.remote)) {
1036
1036
  // "查看当前目录" 不是服务器名,放回 prompt
1037
1037
  promptArgs.unshift(options.remote);
1038
1038
  options.remote = true; // 改为使用默认服务器
1039
1039
  }
1040
+ let prompt = '';
1040
1041
  if (promptArgs.length === 0) {
1041
- program.help();
1042
- return;
1042
+ // 无参数时:尝试自动检测上一条失败的命令
1043
+ const { getLastNonPlsCommand } = await import('../src/shell-hook.js');
1044
+ const lastCmd = getLastNonPlsCommand();
1045
+ if (lastCmd && lastCmd.exit !== 0) {
1046
+ // 找到了失败的命令,自动生成 prompt
1047
+ prompt = `上一条命令「${lastCmd.cmd}」执行失败,退出码:${lastCmd.exit}。请生成正确的命令。`;
1048
+ if (options.debug) {
1049
+ console.log('');
1050
+ console2.muted(`自动检测到失败命令: ${lastCmd.cmd} (退出码: ${lastCmd.exit})`);
1051
+ console2.muted(`生成 prompt: ${prompt}`);
1052
+ }
1053
+ // 继续执行命令生成流程(不 return)
1054
+ }
1055
+ else {
1056
+ // 没有失败的命令,显示帮助
1057
+ program.help();
1058
+ return;
1059
+ }
1060
+ }
1061
+ else {
1062
+ prompt = promptArgs.join(' ');
1043
1063
  }
1044
- let prompt = promptArgs.join(' ');
1045
1064
  if (!prompt.trim()) {
1046
1065
  console.log('');
1047
1066
  console2.error('请提供你想执行的操作描述');
@@ -1452,7 +1471,7 @@ async function executeRemoteCommand(remoteName, command) {
1452
1471
  wrappedLines.push(...console2.wrapText(line, maxContentWidth));
1453
1472
  }
1454
1473
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => console2.getDisplayWidth(l)), console2.getDisplayWidth('生成命令'));
1455
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2);
1474
+ const boxWidth = Math.max(console2.MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
1456
1475
  console2.printSeparator(`远程输出 (${remoteName})`, boxWidth);
1457
1476
  try {
1458
1477
  const result = await sshExec(remoteName, actualCommand, {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { getCurrentTheme } from '../ui/theme.js';
4
- import { getDisplayWidth, wrapText } from '../utils/console.js';
4
+ import { getDisplayWidth, wrapText, MIN_COMMAND_BOX_WIDTH } from '../utils/console.js';
5
5
  /**
6
6
  * CommandBox 组件 - 显示带边框和标题的命令框
7
7
  */
@@ -20,7 +20,7 @@ export const CommandBox = ({ command, title = '生成命令' }) => {
20
20
  }
21
21
  // 计算实际使用的宽度
22
22
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => getDisplayWidth(l)), titleWidth);
23
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2);
23
+ const boxWidth = Math.max(MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
24
24
  // 顶部边框:┌─ 生成命令 ─────┐
25
25
  const topPadding = boxWidth - titleWidth - 5;
26
26
  const topBorder = '┌─ ' + title + ' ' + '─'.repeat(Math.max(0, topPadding)) + '┐';
@@ -3,7 +3,6 @@ import { getConfig } from './config.js';
3
3
  import { CHAT_SYSTEM_PROMPT, buildChatUserContext } from './prompts.js';
4
4
  import { formatSystemInfo } from './sysinfo.js';
5
5
  import { formatHistoryForAI } from './history.js';
6
- import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
7
6
  import { getChatHistory, addChatMessage } from './chat-history.js';
8
7
  /**
9
8
  * 创建 Mastra Chat Agent(使用静态系统提示词)
@@ -50,8 +49,10 @@ export async function chatWithMastra(prompt, options = {}) {
50
49
  // 3. 构建最新消息(动态上下文 + 用户问题)
51
50
  const sysinfo = formatSystemInfo();
52
51
  const plsHistory = formatHistoryForAI();
53
- const shellHistory = formatShellHistoryForAI();
54
- const shellHookEnabled = config.shellHook && getShellHistory().length > 0;
52
+ // 使用统一的历史获取接口(自动降级到系统历史)
53
+ const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
54
+ const shellHistory = formatShellHistoryForAIWithFallback();
55
+ const shellHookEnabled = !!shellHistory; // 如果有 shell 历史就视为启用
55
56
  const latestUserContext = buildChatUserContext(prompt, sysinfo, plsHistory, shellHistory, shellHookEnabled);
56
57
  messages.push(latestUserContext);
57
58
  // 4. 发送给 AI(流式或非流式)
@@ -3,8 +3,6 @@ import { createShellAgent } from './mastra-agent.js';
3
3
  import { SHELL_COMMAND_SYSTEM_PROMPT, buildUserContextPrompt } from './prompts.js';
4
4
  import { formatSystemInfo } from './sysinfo.js';
5
5
  import { formatHistoryForAI } from './history.js';
6
- import { formatShellHistoryForAI, getShellHistory } from './shell-hook.js';
7
- import { getConfig } from './config.js';
8
6
  import { formatRemoteHistoryForAI, formatRemoteShellHistoryForAI } from './remote-history.js';
9
7
  import { formatRemoteSysInfoForAI } from './remote.js';
10
8
  /**
@@ -47,10 +45,11 @@ export async function generateMultiStepCommand(userPrompt, previousSteps = [], o
47
45
  else {
48
46
  // 本地执行:格式化本地系统信息和历史
49
47
  sysinfoStr = formatSystemInfo();
50
- const config = getConfig();
51
48
  const plsHistory = formatHistoryForAI();
52
- const shellHistory = formatShellHistoryForAI();
53
- historyStr = (config.shellHook && getShellHistory().length > 0) ? shellHistory : plsHistory;
49
+ // 使用统一的历史获取接口(自动降级到系统历史)
50
+ const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js');
51
+ const shellHistory = formatShellHistoryForAIWithFallback();
52
+ historyStr = shellHistory || plsHistory; // 优先使用 shell 历史,降级到 pls 历史
54
53
  }
55
54
  // 构建包含所有动态数据的 User Prompt(XML 格式)
56
55
  const userContextPrompt = buildUserContextPrompt(userPrompt, sysinfoStr, historyStr, previousSteps);
@@ -59,6 +59,25 @@ export declare function reinstallHookForLimitChange(oldLimit: number, newLimit:
59
59
  * 清空 shell 历史
60
60
  */
61
61
  export declare function clearShellHistory(): void;
62
+ /**
63
+ * 获取 shell 历史(统一接口)
64
+ * 优先使用 shell hook,降级到系统历史文件
65
+ *
66
+ * 这是推荐的历史获取方式,会自动选择最佳来源:
67
+ * 1. 优先:shell hook(有退出码,最准确)
68
+ * 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
69
+ */
70
+ export declare function getShellHistoryWithFallback(): ShellHistoryItem[];
71
+ /**
72
+ * 获取最近一条非 pls 的命令(统一接口)
73
+ * 自动选择最佳历史来源
74
+ */
75
+ export declare function getLastNonPlsCommand(): ShellHistoryItem | null;
76
+ /**
77
+ * 格式化 shell 历史供 AI 使用(统一接口)
78
+ * 自动选择最佳历史来源
79
+ */
80
+ export declare function formatShellHistoryForAIWithFallback(): string;
62
81
  /**
63
82
  * 检测远程服务器的 shell 类型
64
83
  */
@@ -479,6 +479,69 @@ export function clearShellHistory() {
479
479
  console.log(chalk.hex(colors.success)('✓ Shell 历史已清空'));
480
480
  console.log('');
481
481
  }
482
+ // ================== 统一历史获取 ==================
483
+ /**
484
+ * 获取 shell 历史(统一接口)
485
+ * 优先使用 shell hook,降级到系统历史文件
486
+ *
487
+ * 这是推荐的历史获取方式,会自动选择最佳来源:
488
+ * 1. 优先:shell hook(有退出码,最准确)
489
+ * 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
490
+ */
491
+ export function getShellHistoryWithFallback() {
492
+ const config = getConfig();
493
+ // 优先使用 shell hook
494
+ if (config.shellHook) {
495
+ const history = getShellHistory();
496
+ if (history.length > 0) {
497
+ return history;
498
+ }
499
+ }
500
+ // 降级到系统历史文件
501
+ const { getSystemShellHistory } = require('./system-history.js');
502
+ return getSystemShellHistory();
503
+ }
504
+ /**
505
+ * 获取最近一条非 pls 的命令(统一接口)
506
+ * 自动选择最佳历史来源
507
+ */
508
+ export function getLastNonPlsCommand() {
509
+ const history = getShellHistoryWithFallback();
510
+ // 从后往前找第一条非 pls 命令
511
+ for (let i = history.length - 1; i >= 0; i--) {
512
+ const item = history[i];
513
+ if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
514
+ return item;
515
+ }
516
+ }
517
+ return null;
518
+ }
519
+ /**
520
+ * 格式化 shell 历史供 AI 使用(统一接口)
521
+ * 自动选择最佳历史来源
522
+ */
523
+ export function formatShellHistoryForAIWithFallback() {
524
+ const config = getConfig();
525
+ // 如果启用了 shell hook 且有记录,使用 hook 历史(包含详细信息)
526
+ if (config.shellHook) {
527
+ const hookHistory = getShellHistory();
528
+ if (hookHistory.length > 0) {
529
+ return formatShellHistoryForAI();
530
+ }
531
+ }
532
+ // 降级到系统历史
533
+ const { getSystemShellHistory } = require('./system-history.js');
534
+ const history = getSystemShellHistory();
535
+ if (history.length === 0) {
536
+ return '';
537
+ }
538
+ // 格式化系统历史(简单格式,无详细信息)
539
+ const lines = history.map((item, index) => {
540
+ const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`;
541
+ return `${index + 1}. ${item.cmd} ${status}`;
542
+ });
543
+ return `【用户终端最近执行的命令(来自系统历史)】\n${lines.join('\n')}`;
544
+ }
482
545
  // ================== 远程 Shell Hook ==================
483
546
  /**
484
547
  * 生成远程 zsh hook 脚本
@@ -0,0 +1,13 @@
1
+ import type { ShellHistoryItem } from './shell-hook.js';
2
+ /**
3
+ * 直接读取系统 shell 历史文件(类似 thefuck)
4
+ * 用于没有安装 shell hook 的情况
5
+ *
6
+ * 限制:系统历史文件不记录退出码,所以 exit 字段都是 0
7
+ */
8
+ export declare function getSystemShellHistory(): ShellHistoryItem[];
9
+ /**
10
+ * 从系统历史中获取最近一条命令
11
+ * 排除 pls 命令本身
12
+ */
13
+ export declare function getLastCommandFromSystem(): ShellHistoryItem | null;
@@ -0,0 +1,105 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ import { getConfig } from './config.js';
5
+ /**
6
+ * 直接读取系统 shell 历史文件(类似 thefuck)
7
+ * 用于没有安装 shell hook 的情况
8
+ *
9
+ * 限制:系统历史文件不记录退出码,所以 exit 字段都是 0
10
+ */
11
+ export function getSystemShellHistory() {
12
+ const shell = process.env.SHELL || '';
13
+ const home = os.homedir();
14
+ let historyFile;
15
+ let parser;
16
+ if (shell.includes('zsh')) {
17
+ // zsh 历史文件
18
+ historyFile = process.env.HISTFILE || path.join(home, '.zsh_history');
19
+ parser = parseZshHistoryLine;
20
+ }
21
+ else if (shell.includes('bash')) {
22
+ // bash 历史文件
23
+ historyFile = process.env.HISTFILE || path.join(home, '.bash_history');
24
+ parser = parseBashHistoryLine;
25
+ }
26
+ else {
27
+ // 不支持的 shell
28
+ return [];
29
+ }
30
+ if (!fs.existsSync(historyFile)) {
31
+ return [];
32
+ }
33
+ try {
34
+ const content = fs.readFileSync(historyFile, 'utf-8');
35
+ const lines = content.trim().split('\n');
36
+ const limit = getConfig().shellHistoryLimit || 10;
37
+ // 只取最后 N 条
38
+ const recentLines = lines.slice(-limit);
39
+ return recentLines
40
+ .map(line => parser(line))
41
+ .filter((item) => item !== null);
42
+ }
43
+ catch {
44
+ return [];
45
+ }
46
+ }
47
+ /**
48
+ * 解析 zsh 历史行
49
+ * 格式: ": 1234567890:0;ls -la"
50
+ * 或者: "ls -la" (简单格式)
51
+ */
52
+ function parseZshHistoryLine(line) {
53
+ // 扩展格式: ": timestamp:duration;command"
54
+ const extendedMatch = line.match(/^:\s*(\d+):\d+;(.+)$/);
55
+ if (extendedMatch) {
56
+ const timestamp = parseInt(extendedMatch[1]);
57
+ const cmd = extendedMatch[2].trim();
58
+ return {
59
+ cmd,
60
+ exit: 0, // 系统历史文件不记录退出码
61
+ time: new Date(timestamp * 1000).toISOString(),
62
+ };
63
+ }
64
+ // 简单格式
65
+ const cmd = line.trim();
66
+ if (cmd) {
67
+ return {
68
+ cmd,
69
+ exit: 0,
70
+ time: new Date().toISOString(),
71
+ };
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * 解析 bash 历史行
77
+ * 格式: "ls -la"
78
+ * bash 历史文件默认不记录时间戳
79
+ */
80
+ function parseBashHistoryLine(line) {
81
+ const cmd = line.trim();
82
+ if (cmd) {
83
+ return {
84
+ cmd,
85
+ exit: 0, // 系统历史文件不记录退出码
86
+ time: new Date().toISOString(),
87
+ };
88
+ }
89
+ return null;
90
+ }
91
+ /**
92
+ * 从系统历史中获取最近一条命令
93
+ * 排除 pls 命令本身
94
+ */
95
+ export function getLastCommandFromSystem() {
96
+ const history = getSystemShellHistory();
97
+ // 从后往前找第一条非 pls 命令
98
+ for (let i = history.length - 1; i >= 0; i--) {
99
+ const item = history[i];
100
+ if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
101
+ return item;
102
+ }
103
+ }
104
+ return null;
105
+ }
@@ -1,3 +1,8 @@
1
+ /**
2
+ * 原生控制台输出工具函数
3
+ * 用于不需要 Ink 的场景,避免清屏和性能问题
4
+ */
5
+ export declare const MIN_COMMAND_BOX_WIDTH = 20;
1
6
  /**
2
7
  * 计算字符串的显示宽度(中文占2个宽度)
3
8
  */
@@ -4,6 +4,8 @@ import { getCurrentTheme } from '../ui/theme.js';
4
4
  * 原生控制台输出工具函数
5
5
  * 用于不需要 Ink 的场景,避免清屏和性能问题
6
6
  */
7
+ // 命令框最小宽度(统一配置)
8
+ export const MIN_COMMAND_BOX_WIDTH = 20;
7
9
  // 获取当前主题颜色
8
10
  function getColors() {
9
11
  const theme = getCurrentTheme();
@@ -102,7 +104,7 @@ export function drawCommandBox(command, title = '生成命令') {
102
104
  }
103
105
  // 计算实际使用的宽度
104
106
  const actualMaxWidth = Math.max(...wrappedLines.map((l) => getDisplayWidth(l)), titleWidth);
105
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2);
107
+ const boxWidth = Math.max(MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2));
106
108
  const topPadding = boxWidth - titleWidth - 5;
107
109
  const topBorder = '┌─ ' + title + ' ' + '─'.repeat(Math.max(0, topPadding)) + '┐';
108
110
  const bottomBorder = '└' + '─'.repeat(boxWidth - 2) + '┘';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import { getCurrentTheme } from '../ui/theme.js'
4
- import { getDisplayWidth, wrapText } from '../utils/console.js'
4
+ import { getDisplayWidth, wrapText, MIN_COMMAND_BOX_WIDTH } from '../utils/console.js'
5
5
 
6
6
  interface CommandBoxProps {
7
7
  command: string
@@ -33,7 +33,7 @@ export const CommandBox: React.FC<CommandBoxProps> = ({ command, title = '生成
33
33
  ...wrappedLines.map((l) => getDisplayWidth(l)),
34
34
  titleWidth
35
35
  )
36
- const boxWidth = Math.min(actualMaxWidth + 4, termWidth - 2)
36
+ const boxWidth = Math.max(MIN_COMMAND_BOX_WIDTH, Math.min(actualMaxWidth + 4, termWidth - 2))
37
37
 
38
38
  // 顶部边框:┌─ 生成命令 ─────┐
39
39
  const topPadding = boxWidth - titleWidth - 5
@@ -73,8 +73,10 @@ export async function chatWithMastra(
73
73
  // 3. 构建最新消息(动态上下文 + 用户问题)
74
74
  const sysinfo = formatSystemInfo()
75
75
  const plsHistory = formatHistoryForAI()
76
- const shellHistory = formatShellHistoryForAI()
77
- const shellHookEnabled = config.shellHook && getShellHistory().length > 0
76
+ // 使用统一的历史获取接口(自动降级到系统历史)
77
+ const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js')
78
+ const shellHistory = formatShellHistoryForAIWithFallback()
79
+ const shellHookEnabled = !!shellHistory // 如果有 shell 历史就视为启用
78
80
 
79
81
  const latestUserContext = buildChatUserContext(
80
82
  prompt,
package/src/multi-step.ts CHANGED
@@ -75,10 +75,11 @@ export async function generateMultiStepCommand(
75
75
  } else {
76
76
  // 本地执行:格式化本地系统信息和历史
77
77
  sysinfoStr = formatSystemInfo()
78
- const config = getConfig()
79
78
  const plsHistory = formatHistoryForAI()
80
- const shellHistory = formatShellHistoryForAI()
81
- historyStr = (config.shellHook && getShellHistory().length > 0) ? shellHistory : plsHistory
79
+ // 使用统一的历史获取接口(自动降级到系统历史)
80
+ const { formatShellHistoryForAIWithFallback } = await import('./shell-hook.js')
81
+ const shellHistory = formatShellHistoryForAIWithFallback()
82
+ historyStr = shellHistory || plsHistory // 优先使用 shell 历史,降级到 pls 历史
82
83
  }
83
84
 
84
85
  // 构建包含所有动态数据的 User Prompt(XML 格式)
package/src/shell-hook.ts CHANGED
@@ -573,6 +573,82 @@ export function clearShellHistory(): void {
573
573
  console.log('')
574
574
  }
575
575
 
576
+ // ================== 统一历史获取 ==================
577
+
578
+ /**
579
+ * 获取 shell 历史(统一接口)
580
+ * 优先使用 shell hook,降级到系统历史文件
581
+ *
582
+ * 这是推荐的历史获取方式,会自动选择最佳来源:
583
+ * 1. 优先:shell hook(有退出码,最准确)
584
+ * 2. 降级:系统历史文件(无退出码,兼容未安装 hook 的情况)
585
+ */
586
+ export function getShellHistoryWithFallback(): ShellHistoryItem[] {
587
+ const config = getConfig()
588
+
589
+ // 优先使用 shell hook
590
+ if (config.shellHook) {
591
+ const history = getShellHistory()
592
+ if (history.length > 0) {
593
+ return history
594
+ }
595
+ }
596
+
597
+ // 降级到系统历史文件
598
+ const { getSystemShellHistory } = require('./system-history.js')
599
+ return getSystemShellHistory()
600
+ }
601
+
602
+ /**
603
+ * 获取最近一条非 pls 的命令(统一接口)
604
+ * 自动选择最佳历史来源
605
+ */
606
+ export function getLastNonPlsCommand(): ShellHistoryItem | null {
607
+ const history = getShellHistoryWithFallback()
608
+
609
+ // 从后往前找第一条非 pls 命令
610
+ for (let i = history.length - 1; i >= 0; i--) {
611
+ const item = history[i]
612
+ if (!item.cmd.startsWith('pls') && !item.cmd.startsWith('please')) {
613
+ return item
614
+ }
615
+ }
616
+
617
+ return null
618
+ }
619
+
620
+ /**
621
+ * 格式化 shell 历史供 AI 使用(统一接口)
622
+ * 自动选择最佳历史来源
623
+ */
624
+ export function formatShellHistoryForAIWithFallback(): string {
625
+ const config = getConfig()
626
+
627
+ // 如果启用了 shell hook 且有记录,使用 hook 历史(包含详细信息)
628
+ if (config.shellHook) {
629
+ const hookHistory = getShellHistory()
630
+ if (hookHistory.length > 0) {
631
+ return formatShellHistoryForAI()
632
+ }
633
+ }
634
+
635
+ // 降级到系统历史
636
+ const { getSystemShellHistory } = require('./system-history.js')
637
+ const history = getSystemShellHistory()
638
+
639
+ if (history.length === 0) {
640
+ return ''
641
+ }
642
+
643
+ // 格式化系统历史(简单格式,无详细信息)
644
+ const lines = history.map((item, index) => {
645
+ const status = item.exit === 0 ? '✓' : `✗ 退出码:${item.exit}`
646
+ return `${index + 1}. ${item.cmd} ${status}`
647
+ })
648
+
649
+ return `【用户终端最近执行的命令(来自系统历史)】\n${lines.join('\n')}`
650
+ }
651
+
576
652
  // ================== 远程 Shell Hook ==================
577
653
 
578
654
  /**