@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
@@ -1,35 +1,57 @@
1
+ type ShellType = 'zsh' | 'bash' | 'powershell' | 'unknown';
2
+ /**
3
+ * Shell 历史记录项
4
+ */
5
+ export interface ShellHistoryItem {
6
+ cmd: string;
7
+ exit: number;
8
+ time: string;
9
+ }
10
+ /**
11
+ * Hook 状态
12
+ */
13
+ export interface HookStatus {
14
+ enabled: boolean;
15
+ installed: boolean;
16
+ shellType: ShellType;
17
+ configPath: string | null;
18
+ historyFile: string;
19
+ }
1
20
  /**
2
21
  * 检测当前 shell 类型
3
22
  */
4
- export function detectShell(): "unknown" | "zsh" | "bash" | "powershell";
23
+ export declare function detectShell(): ShellType;
5
24
  /**
6
25
  * 获取 shell 配置文件路径
7
26
  */
8
- export function getShellConfigPath(shellType: any): string | null;
27
+ export declare function getShellConfigPath(shellType: ShellType): string | null;
9
28
  /**
10
29
  * 安装 shell hook
11
30
  */
12
- export function installShellHook(): Promise<boolean>;
31
+ export declare function installShellHook(): Promise<boolean>;
13
32
  /**
14
33
  * 卸载 shell hook
15
34
  */
16
- export function uninstallShellHook(): boolean;
35
+ export declare function uninstallShellHook(): boolean;
17
36
  /**
18
37
  * 读取 shell 历史记录
19
38
  */
20
- export function getShellHistory(): any[];
39
+ export declare function getShellHistory(): ShellHistoryItem[];
21
40
  /**
22
41
  * 格式化 shell 历史供 AI 使用
23
42
  * 对于 pls 命令,会从 pls history 中查找对应的详细信息
24
43
  */
25
- export function formatShellHistoryForAI(): string;
44
+ export declare function formatShellHistoryForAI(): string;
26
45
  /**
27
46
  * 获取 hook 状态
28
47
  */
29
- export function getHookStatus(): {
30
- enabled: any;
31
- installed: boolean;
32
- shellType: string;
33
- configPath: string | null;
34
- historyFile: string;
35
- };
48
+ export declare function getHookStatus(): HookStatus;
49
+ /**
50
+ * 显示 shell 历史
51
+ */
52
+ export declare function displayShellHistory(): void;
53
+ /**
54
+ * 清空 shell 历史
55
+ */
56
+ export declare function clearShellHistory(): void;
57
+ export {};
@@ -242,15 +242,20 @@ export function getShellHistory() {
242
242
  }
243
243
  try {
244
244
  const content = fs.readFileSync(SHELL_HISTORY_FILE, 'utf-8');
245
- const lines = content.trim().split('\n').filter(line => line.trim());
246
- return lines.map(line => {
245
+ const lines = content
246
+ .trim()
247
+ .split('\n')
248
+ .filter((line) => line.trim());
249
+ return lines
250
+ .map((line) => {
247
251
  try {
248
252
  return JSON.parse(line);
249
253
  }
250
254
  catch {
251
255
  return null;
252
256
  }
253
- }).filter(Boolean);
257
+ })
258
+ .filter((item) => item !== null);
254
259
  }
255
260
  catch {
256
261
  return [];
@@ -258,8 +263,6 @@ export function getShellHistory() {
258
263
  }
259
264
  /**
260
265
  * 从 pls history 中查找匹配的记录
261
- * @param {string} prompt - pls 命令后面的 prompt 部分
262
- * @returns {object|null} 匹配的 pls history 记录
263
266
  */
264
267
  function findPlsHistoryMatch(prompt) {
265
268
  const plsHistory = getHistory();
@@ -312,7 +315,13 @@ export function formatShellHistoryForAI() {
312
315
  }
313
316
  else if (plsRecord.executed) {
314
317
  const execStatus = plsRecord.exitCode === 0 ? '✓' : `✗ 退出码:${plsRecord.exitCode}`;
315
- return `${index + 1}. [pls] "${prompt}" → 实际执行: ${plsRecord.command} ${execStatus}`;
318
+ // 检查用户是否修改了命令
319
+ if (plsRecord.userModified && plsRecord.aiGeneratedCommand) {
320
+ return `${index + 1}. [pls] "${prompt}" → AI 生成: ${plsRecord.aiGeneratedCommand} / 用户修改为: ${plsRecord.command} ${execStatus}`;
321
+ }
322
+ else {
323
+ return `${index + 1}. [pls] "${prompt}" → 实际执行: ${plsRecord.command} ${execStatus}`;
324
+ }
316
325
  }
317
326
  else {
318
327
  return `${index + 1}. [pls] "${prompt}" → 生成命令: ${plsRecord.command} (用户取消执行)`;
@@ -343,6 +352,72 @@ export function getHookStatus() {
343
352
  installed,
344
353
  shellType,
345
354
  configPath,
346
- historyFile: SHELL_HISTORY_FILE
355
+ historyFile: SHELL_HISTORY_FILE,
347
356
  };
348
357
  }
358
+ /**
359
+ * 显示 shell 历史
360
+ */
361
+ export function displayShellHistory() {
362
+ const config = getConfig();
363
+ const history = getShellHistory();
364
+ if (!config.shellHook) {
365
+ console.log('');
366
+ console.log(chalk.yellow('⚠️ Shell Hook 未启用'));
367
+ console.log(chalk.gray('运行 ') + chalk.cyan('pls hook install') + chalk.gray(' 启用 Shell Hook'));
368
+ console.log('');
369
+ return;
370
+ }
371
+ if (history.length === 0) {
372
+ console.log('');
373
+ console.log(chalk.gray('暂无 Shell 历史记录'));
374
+ console.log('');
375
+ return;
376
+ }
377
+ console.log('');
378
+ console.log(chalk.bold(`终端历史(最近 ${history.length} 条):`));
379
+ console.log(chalk.gray('━'.repeat(50)));
380
+ history.forEach((item, index) => {
381
+ const num = index + 1;
382
+ const status = item.exit === 0 ? chalk.green('✓') : chalk.red(`✗ (${item.exit})`);
383
+ // 检查是否是 pls 命令
384
+ const isPls = item.cmd.startsWith('pls ') || item.cmd.startsWith('please ');
385
+ if (isPls) {
386
+ // pls 命令:尝试从 history 查找详细信息
387
+ const args = item.cmd.replace(/^(pls|please)\s+/, '');
388
+ const plsRecord = findPlsHistoryMatch(args);
389
+ if (plsRecord && plsRecord.executed) {
390
+ // 检查用户是否修改了命令
391
+ if (plsRecord.userModified && plsRecord.aiGeneratedCommand) {
392
+ console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}"`);
393
+ console.log(` ${chalk.dim('AI 生成:')} ${chalk.gray(plsRecord.aiGeneratedCommand)}`);
394
+ console.log(` ${chalk.dim('用户修改为:')} ${plsRecord.command} ${status} ${chalk.yellow('(已修改)')}`);
395
+ }
396
+ else {
397
+ console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} "${args}" → ${plsRecord.command} ${status}`);
398
+ }
399
+ }
400
+ else {
401
+ console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${chalk.magenta('[pls]')} ${args} ${status}`);
402
+ }
403
+ }
404
+ else {
405
+ console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${item.cmd} ${status}`);
406
+ }
407
+ });
408
+ console.log(chalk.gray('━'.repeat(50)));
409
+ console.log(chalk.gray(`配置: 保留最近 ${config.shellHistoryLimit} 条`));
410
+ console.log(chalk.gray(`文件: ${SHELL_HISTORY_FILE}`));
411
+ console.log('');
412
+ }
413
+ /**
414
+ * 清空 shell 历史
415
+ */
416
+ export function clearShellHistory() {
417
+ if (fs.existsSync(SHELL_HISTORY_FILE)) {
418
+ fs.unlinkSync(SHELL_HISTORY_FILE);
419
+ }
420
+ console.log('');
421
+ console.log(chalk.green('✓ Shell 历史已清空'));
422
+ console.log('');
423
+ }
@@ -1,15 +1,19 @@
1
1
  /**
2
- * 收集系统信息
2
+ * 系统信息
3
3
  */
4
- export function collectSystemInfo(): {
4
+ export interface SystemInfo {
5
5
  os: NodeJS.Platform;
6
- arch: NodeJS.Architecture;
6
+ arch: string;
7
7
  shell: string;
8
8
  packageManager: string;
9
9
  cwd: string;
10
10
  user: string;
11
- };
11
+ }
12
+ /**
13
+ * 收集系统信息
14
+ */
15
+ export declare function collectSystemInfo(): SystemInfo;
12
16
  /**
13
17
  * 将系统信息格式化为字符串(供 AI 使用)
14
18
  */
15
- export function formatSystemInfo(): string;
19
+ export declare function formatSystemInfo(): string;
@@ -11,7 +11,7 @@ function detectPackageManager() {
11
11
  { name: 'yum', command: 'yum' },
12
12
  { name: 'pacman', command: 'pacman' },
13
13
  { name: 'zypper', command: 'zypper' },
14
- { name: 'apk', command: 'apk' }
14
+ { name: 'apk', command: 'apk' },
15
15
  ];
16
16
  for (const mgr of managers) {
17
17
  try {
@@ -40,7 +40,7 @@ export function collectSystemInfo() {
40
40
  shell: getCurrentShell(),
41
41
  packageManager: detectPackageManager(),
42
42
  cwd: process.cwd(),
43
- user: os.userInfo().username
43
+ user: os.userInfo().username,
44
44
  };
45
45
  }
46
46
  /**
@@ -1,44 +1,44 @@
1
1
  /**
2
2
  * 计算字符串的显示宽度(中文占2个宽度)
3
3
  */
4
- export function getDisplayWidth(str: any): number;
4
+ export declare function getDisplayWidth(str: string): number;
5
5
  /**
6
6
  * 绘制命令框(原生版本)
7
7
  */
8
- export function drawCommandBox(command: any, title?: string): void;
8
+ export declare function drawCommandBox(command: string, title?: string): void;
9
9
  /**
10
10
  * 格式化耗时
11
11
  */
12
- export function formatDuration(ms: any): string;
12
+ export declare function formatDuration(ms: number): string;
13
13
  /**
14
14
  * 输出分隔线
15
15
  */
16
- export function printSeparator(text?: string, length?: number): void;
16
+ export declare function printSeparator(text?: string, length?: number): void;
17
17
  /**
18
18
  * 输出成功消息
19
19
  */
20
- export function success(message: any): void;
20
+ export declare function success(message: string): void;
21
21
  /**
22
22
  * 输出错误消息
23
23
  */
24
- export function error(message: any): void;
24
+ export declare function error(message: string): void;
25
25
  /**
26
26
  * 输出警告消息
27
27
  */
28
- export function warning(message: any): void;
28
+ export declare function warning(message: string): void;
29
29
  /**
30
30
  * 输出信息消息
31
31
  */
32
- export function info(message: any): void;
32
+ export declare function info(message: string): void;
33
33
  /**
34
34
  * 输出灰色文本
35
35
  */
36
- export function muted(message: any): void;
36
+ export declare function muted(message: string): void;
37
37
  /**
38
38
  * 输出标题
39
39
  */
40
- export function title(message: any): void;
40
+ export declare function title(message: string): void;
41
41
  /**
42
42
  * 输出主色文本
43
43
  */
44
- export function primary(message: any): void;
44
+ export declare function primary(message: string): void;
@@ -35,7 +35,7 @@ export function getDisplayWidth(str) {
35
35
  export function drawCommandBox(command, title = '生成命令') {
36
36
  const lines = command.split('\n');
37
37
  const titleWidth = getDisplayWidth(title);
38
- const maxContentWidth = Math.max(...lines.map(l => getDisplayWidth(l)));
38
+ const maxContentWidth = Math.max(...lines.map((l) => getDisplayWidth(l)));
39
39
  const boxWidth = Math.max(maxContentWidth + 4, titleWidth + 6, 20);
40
40
  const topPadding = boxWidth - titleWidth - 5;
41
41
  const topBorder = '┌─ ' + title + ' ' + '─'.repeat(topPadding) + '┐';
@@ -44,10 +44,7 @@ export function drawCommandBox(command, title = '生成命令') {
44
44
  for (const line of lines) {
45
45
  const lineWidth = getDisplayWidth(line);
46
46
  const padding = ' '.repeat(boxWidth - lineWidth - 4);
47
- console.log(chalk.hex(colors.warning)('│ ') +
48
- chalk.hex(colors.primary)(line) +
49
- padding +
50
- chalk.hex(colors.warning)(' │'));
47
+ console.log(chalk.hex(colors.warning)('│ ') + chalk.hex(colors.primary)(line) + padding + chalk.hex(colors.warning)(' │'));
51
48
  }
52
49
  console.log(chalk.hex(colors.warning)(bottomBorder));
53
50
  }
@@ -65,7 +62,8 @@ export function formatDuration(ms) {
65
62
  */
66
63
  export function printSeparator(text = '输出', length = 38) {
67
64
  const textPart = text ? ` ${text} ` : '';
68
- const lineLength = Math.max(0, length - textPart.length);
65
+ const textWidth = getDisplayWidth(textPart); // 使用显示宽度而不是字符数
66
+ const lineLength = Math.max(0, length - textWidth);
69
67
  const leftDashes = '─'.repeat(Math.floor(lineLength / 2));
70
68
  const rightDashes = '─'.repeat(Math.ceil(lineLength / 2));
71
69
  console.log(chalk.gray(`${leftDashes}${textPart}${rightDashes}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yivan-lab/pretty-please",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "AI 驱动的命令行工具,将自然语言转换为可执行的 Shell 命令",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,8 +9,10 @@
9
9
  },
10
10
  "scripts": {
11
11
  "dev": "tsx bin/pls.tsx",
12
- "build": "tsc",
12
+ "build": "tsc && node scripts/postbuild.js",
13
13
  "start": "node dist/bin/pls.js",
14
+ "link:dev": "mkdir -p ~/.local/bin && ln -sf \"$(pwd)/bin/pls.tsx\" ~/.local/bin/pls-dev && echo '✅ pls-dev 已链接到 ~/.local/bin/pls-dev'",
15
+ "unlink:dev": "rm -f ~/.local/bin/pls-dev && echo '✅ pls-dev 已移除'",
14
16
  "prepublishOnly": "pnpm build"
15
17
  },
16
18
  "keywords": [
@@ -40,11 +42,12 @@
40
42
  },
41
43
  "files": [
42
44
  "bin",
43
- "dist",
45
+ "dist/bin",
46
+ "dist/src",
47
+ "dist/package.json",
44
48
  "src",
45
49
  "README.md",
46
50
  "LICENSE",
47
- "package.json",
48
51
  "tsconfig.json"
49
52
  ],
50
53
  "dependencies": {
@@ -60,8 +63,6 @@
60
63
  "ink-text-input": "^6.0.0",
61
64
  "lowlight": "^3.3.0",
62
65
  "marked": "^17.0.1",
63
- "openai": "^6.10.0",
64
- "ora": "^9.0.0",
65
66
  "react": "^19.2.3",
66
67
  "shiki": "^3.20.0",
67
68
  "string-width": "^8.1.0",
@@ -72,6 +73,7 @@
72
73
  "@types/hast": "^3.0.4",
73
74
  "@types/node": "^25.0.2",
74
75
  "@types/react": "^19.2.7",
76
+ "react-devtools-core": "^7.0.1",
75
77
  "tsx": "^4.21.0",
76
78
  "typescript": "^5.9.3"
77
79
  }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Shell builtin 命令检测器
3
+ *
4
+ * 用于检测命令中是否包含 shell 内置命令(builtin)
5
+ * 这些命令在子进程中执行可能无效或行为异常
6
+ */
7
+
8
+ // Shell 内置命令列表
9
+ const SHELL_BUILTINS: readonly string[] = [
10
+ // 目录相关
11
+ 'cd',
12
+ 'pushd',
13
+ 'popd',
14
+ 'dirs',
15
+
16
+ // 历史相关
17
+ 'history',
18
+
19
+ // 别名和函数
20
+ 'alias',
21
+ 'unalias',
22
+
23
+ // 环境变量
24
+ 'export',
25
+ 'set',
26
+ 'unset',
27
+ 'declare',
28
+ 'local',
29
+ 'readonly',
30
+
31
+ // 脚本执行
32
+ 'source',
33
+ '.',
34
+
35
+ // 任务控制
36
+ 'jobs',
37
+ 'fg',
38
+ 'bg',
39
+ 'disown',
40
+
41
+ // 其他
42
+ 'ulimit',
43
+ 'umask',
44
+ 'builtin',
45
+ 'command',
46
+ 'type',
47
+ 'enable',
48
+ 'hash',
49
+ 'help',
50
+ 'let',
51
+ 'read',
52
+ 'wait',
53
+ 'eval',
54
+ 'exec',
55
+ 'trap',
56
+ 'times',
57
+ 'shopt',
58
+ ]
59
+
60
+ /**
61
+ * Builtin 检测结果
62
+ */
63
+ export interface BuiltinResult {
64
+ hasBuiltin: boolean
65
+ builtins: string[]
66
+ }
67
+
68
+ /**
69
+ * 提取命令中的所有命令名
70
+ */
71
+ function extractCommandNames(command: string): string[] {
72
+ // 按分隔符拆分(&&, ||, ;, |, &, 换行)
73
+ // 注意:| 是管道,两边都在子进程中执行,所以也算
74
+ const parts = command.split(/[;&|]+|\n+/)
75
+
76
+ const commandNames: string[] = []
77
+
78
+ for (const part of parts) {
79
+ const trimmed = part.trim()
80
+ if (!trimmed) continue
81
+
82
+ // 提取第一个单词(命令名)
83
+ // 处理 sudo、env 等前缀
84
+ const words = trimmed.split(/\s+/)
85
+
86
+ // 跳过 sudo、env 等
87
+ let i = 0
88
+ while (i < words.length && ['sudo', 'env', 'nohup', 'nice'].includes(words[i])) {
89
+ i++
90
+ }
91
+
92
+ if (i < words.length) {
93
+ commandNames.push(words[i])
94
+ }
95
+ }
96
+
97
+ return commandNames
98
+ }
99
+
100
+ /**
101
+ * 检测命令中是否包含 builtin
102
+ */
103
+ export function detectBuiltin(command: string): BuiltinResult {
104
+ const commandNames = extractCommandNames(command)
105
+ const foundBuiltins: string[] = []
106
+
107
+ for (const name of commandNames) {
108
+ if (SHELL_BUILTINS.includes(name)) {
109
+ foundBuiltins.push(name)
110
+ }
111
+ }
112
+
113
+ return {
114
+ hasBuiltin: foundBuiltins.length > 0,
115
+ builtins: [...new Set(foundBuiltins)], // 去重
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 格式化 builtin 列表为易读的字符串
121
+ */
122
+ export function formatBuiltins(builtins: string[]): string {
123
+ if (builtins.length === 0) return ''
124
+ if (builtins.length === 1) return builtins[0]
125
+ return builtins.join(', ')
126
+ }
@@ -0,0 +1,130 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+ import chalk from 'chalk'
5
+ import { getConfig } from './config.js'
6
+
7
+ const CONFIG_DIR = path.join(os.homedir(), '.please')
8
+ const CHAT_HISTORY_FILE = path.join(CONFIG_DIR, 'chat_history.json')
9
+
10
+ /**
11
+ * 聊天消息
12
+ */
13
+ export interface ChatMessage {
14
+ role: 'user' | 'assistant'
15
+ content: string
16
+ }
17
+
18
+ /**
19
+ * 确保配置目录存在
20
+ */
21
+ function ensureConfigDir(): void {
22
+ if (!fs.existsSync(CONFIG_DIR)) {
23
+ fs.mkdirSync(CONFIG_DIR, { recursive: true })
24
+ }
25
+ }
26
+
27
+ /**
28
+ * 读取对话历史
29
+ */
30
+ export function getChatHistory(): ChatMessage[] {
31
+ ensureConfigDir()
32
+
33
+ if (!fs.existsSync(CHAT_HISTORY_FILE)) {
34
+ return []
35
+ }
36
+
37
+ try {
38
+ const content = fs.readFileSync(CHAT_HISTORY_FILE, 'utf-8')
39
+ return JSON.parse(content) as ChatMessage[]
40
+ } catch {
41
+ return []
42
+ }
43
+ }
44
+
45
+ /**
46
+ * 保存对话历史
47
+ */
48
+ function saveChatHistory(history: ChatMessage[]): void {
49
+ ensureConfigDir()
50
+ fs.writeFileSync(CHAT_HISTORY_FILE, JSON.stringify(history, null, 2))
51
+ }
52
+
53
+ /**
54
+ * 添加一轮对话(用户问题 + AI 回答)
55
+ */
56
+ export function addChatMessage(userMessage: string, assistantMessage: string): void {
57
+ const config = getConfig()
58
+ const history = getChatHistory()
59
+
60
+ // 添加新的对话
61
+ history.push({ role: 'user', content: userMessage })
62
+ history.push({ role: 'assistant', content: assistantMessage })
63
+
64
+ // 计算当前轮数(每 2 条消息 = 1 轮)
65
+ const currentRounds = Math.floor(history.length / 2)
66
+ const maxRounds = config.chatHistoryLimit || 10
67
+
68
+ // 如果超出限制,移除最早的对话
69
+ if (currentRounds > maxRounds) {
70
+ // 需要移除的轮数
71
+ const removeRounds = currentRounds - maxRounds
72
+ // 移除最早的 N 轮(N*2 条消息)
73
+ history.splice(0, removeRounds * 2)
74
+ }
75
+
76
+ saveChatHistory(history)
77
+ }
78
+
79
+ /**
80
+ * 清空对话历史
81
+ */
82
+ export function clearChatHistory(): void {
83
+ saveChatHistory([])
84
+ }
85
+
86
+ /**
87
+ * 获取对话历史文件路径
88
+ */
89
+ export function getChatHistoryFilePath(): string {
90
+ return CHAT_HISTORY_FILE
91
+ }
92
+
93
+ /**
94
+ * 获取当前对话轮数
95
+ */
96
+ export function getChatRoundCount(): number {
97
+ const history = getChatHistory()
98
+ return Math.floor(history.length / 2)
99
+ }
100
+
101
+ /**
102
+ * 显示对话历史(只显示用户的 prompt)
103
+ */
104
+ export function displayChatHistory(): void {
105
+ const history = getChatHistory()
106
+ const config = getConfig()
107
+
108
+ if (history.length === 0) {
109
+ console.log('\n' + chalk.gray('暂无对话历史'))
110
+ console.log('')
111
+ return
112
+ }
113
+
114
+ // 只提取用户消息
115
+ const userMessages = history.filter((msg) => msg.role === 'user')
116
+
117
+ console.log('')
118
+ console.log(chalk.bold(`对话历史(最近 ${userMessages.length} 轮):`))
119
+ console.log(chalk.gray('━'.repeat(50)))
120
+
121
+ userMessages.forEach((msg, index) => {
122
+ const num = index + 1
123
+ console.log(` ${chalk.cyan(num.toString().padStart(2, ' '))}. ${msg.content}`)
124
+ })
125
+
126
+ console.log(chalk.gray('━'.repeat(50)))
127
+ console.log(chalk.gray(`配置: 保留最近 ${config.chatHistoryLimit} 轮对话`))
128
+ console.log(chalk.gray(`文件: ${CHAT_HISTORY_FILE}`))
129
+ console.log('')
130
+ }
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import Spinner from 'ink-spinner'
4
4
  import { MarkdownDisplay } from './MarkdownDisplay.js'
5
- import { chatWithAI } from '../ai.js'
5
+ import { chatWithMastra } from '../mastra-chat.js'
6
6
  import { getChatRoundCount } from '../chat-history.js'
7
7
  import { theme } from '../ui/theme.js'
8
8
 
@@ -44,13 +44,13 @@ export function Chat({ prompt, debug, showRoundCount, onComplete }: ChatProps) {
44
44
  }
45
45
 
46
46
  // 调用 AI
47
- chatWithAI(prompt, { debug: debug || false, onChunk })
48
- .then((result: any) => {
47
+ chatWithMastra(prompt, { debug: debug || false, onChunk })
48
+ .then((result) => {
49
49
  const endTime = Date.now()
50
50
  setDuration(endTime - startTime)
51
51
  setStatus('done')
52
52
 
53
- if (debug && typeof result === 'object' && 'debug' in result) {
53
+ if (debug && typeof result === 'object' && 'debug' in result && result.debug) {
54
54
  setDebugInfo(result.debug)
55
55
  }
56
56
 
@@ -1,28 +1,13 @@
1
1
  import React from 'react'
2
2
  import { Box, Text } from 'ink'
3
3
  import { theme } from '../ui/theme.js'
4
+ import { getDisplayWidth } from '../utils/console.js'
4
5
 
5
6
  interface CommandBoxProps {
6
7
  command: string
7
8
  title?: string
8
9
  }
9
10
 
10
- /**
11
- * 计算字符串的显示宽度(中文占2个宽度)
12
- */
13
- function getDisplayWidth(str: string): number {
14
- let width = 0
15
- for (const char of str) {
16
- // 中文、日文、韩文等宽字符占 2 个宽度
17
- if (char.match(/[\u4e00-\u9fff\u3400-\u4dbf\uff00-\uffef\u3000-\u303f]/)) {
18
- width += 2
19
- } else {
20
- width += 1
21
- }
22
- }
23
- return width
24
- }
25
-
26
11
  /**
27
12
  * CommandBox 组件 - 显示带边框和标题的命令框
28
13
  */