@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.
- package/README.md +98 -27
- package/bin/pls.tsx +135 -24
- package/dist/bin/pls.d.ts +1 -1
- package/dist/bin/pls.js +117 -24
- package/dist/package.json +80 -0
- package/dist/src/ai.d.ts +1 -41
- package/dist/src/ai.js +9 -190
- package/dist/src/builtin-detector.d.ts +14 -8
- package/dist/src/builtin-detector.js +36 -16
- package/dist/src/chat-history.d.ts +16 -11
- package/dist/src/chat-history.js +26 -4
- package/dist/src/components/Chat.js +3 -3
- package/dist/src/components/CommandBox.js +1 -16
- package/dist/src/components/ConfirmationPrompt.d.ts +2 -1
- package/dist/src/components/ConfirmationPrompt.js +7 -3
- package/dist/src/components/MultiStepCommandGenerator.d.ts +2 -0
- package/dist/src/components/MultiStepCommandGenerator.js +110 -7
- package/dist/src/config.d.ts +27 -8
- package/dist/src/config.js +92 -33
- package/dist/src/history.d.ts +19 -5
- package/dist/src/history.js +26 -11
- package/dist/src/mastra-agent.d.ts +0 -1
- package/dist/src/mastra-agent.js +3 -4
- package/dist/src/mastra-chat.d.ts +28 -0
- package/dist/src/mastra-chat.js +93 -0
- package/dist/src/multi-step.d.ts +2 -2
- package/dist/src/multi-step.js +2 -2
- package/dist/src/prompts.d.ts +11 -0
- package/dist/src/prompts.js +140 -0
- package/dist/src/shell-hook.d.ts +35 -13
- package/dist/src/shell-hook.js +82 -7
- package/dist/src/sysinfo.d.ts +9 -5
- package/dist/src/sysinfo.js +2 -2
- package/dist/src/utils/console.d.ts +11 -11
- package/dist/src/utils/console.js +4 -6
- package/package.json +8 -6
- package/src/builtin-detector.ts +126 -0
- package/src/chat-history.ts +130 -0
- package/src/components/Chat.tsx +4 -4
- package/src/components/CommandBox.tsx +1 -16
- package/src/components/ConfirmationPrompt.tsx +9 -2
- package/src/components/MultiStepCommandGenerator.tsx +144 -7
- package/src/config.ts +309 -0
- package/src/history.ts +160 -0
- package/src/mastra-agent.ts +3 -4
- package/src/mastra-chat.ts +124 -0
- package/src/multi-step.ts +2 -2
- package/src/prompts.ts +154 -0
- package/src/shell-hook.ts +502 -0
- package/src/{sysinfo.js → sysinfo.ts} +28 -16
- package/src/utils/{console.js → console.ts} +16 -18
- package/bin/pls.js +0 -681
- package/src/ai.js +0 -324
- package/src/builtin-detector.js +0 -98
- package/src/chat-history.js +0 -94
- package/src/components/ChatStatus.tsx +0 -53
- package/src/components/CommandGenerator.tsx +0 -184
- package/src/components/ConfigDisplay.tsx +0 -64
- package/src/components/ConfigWizard.tsx +0 -101
- package/src/components/HistoryDisplay.tsx +0 -69
- package/src/components/HookManager.tsx +0 -150
- package/src/config.js +0 -221
- package/src/history.js +0 -131
- package/src/shell-hook.js +0 -393
package/dist/src/shell-hook.d.ts
CHANGED
|
@@ -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():
|
|
23
|
+
export declare function detectShell(): ShellType;
|
|
5
24
|
/**
|
|
6
25
|
* 获取 shell 配置文件路径
|
|
7
26
|
*/
|
|
8
|
-
export function getShellConfigPath(shellType:
|
|
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():
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 {};
|
package/dist/src/shell-hook.js
CHANGED
|
@@ -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
|
|
246
|
-
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/src/sysinfo.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 系统信息
|
|
3
3
|
*/
|
|
4
|
-
export
|
|
4
|
+
export interface SystemInfo {
|
|
5
5
|
os: NodeJS.Platform;
|
|
6
|
-
arch:
|
|
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;
|
package/dist/src/sysinfo.js
CHANGED
|
@@ -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:
|
|
4
|
+
export declare function getDisplayWidth(str: string): number;
|
|
5
5
|
/**
|
|
6
6
|
* 绘制命令框(原生版本)
|
|
7
7
|
*/
|
|
8
|
-
export function drawCommandBox(command:
|
|
8
|
+
export declare function drawCommandBox(command: string, title?: string): void;
|
|
9
9
|
/**
|
|
10
10
|
* 格式化耗时
|
|
11
11
|
*/
|
|
12
|
-
export function formatDuration(ms:
|
|
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:
|
|
20
|
+
export declare function success(message: string): void;
|
|
21
21
|
/**
|
|
22
22
|
* 输出错误消息
|
|
23
23
|
*/
|
|
24
|
-
export function error(message:
|
|
24
|
+
export declare function error(message: string): void;
|
|
25
25
|
/**
|
|
26
26
|
* 输出警告消息
|
|
27
27
|
*/
|
|
28
|
-
export function warning(message:
|
|
28
|
+
export declare function warning(message: string): void;
|
|
29
29
|
/**
|
|
30
30
|
* 输出信息消息
|
|
31
31
|
*/
|
|
32
|
-
export function info(message:
|
|
32
|
+
export declare function info(message: string): void;
|
|
33
33
|
/**
|
|
34
34
|
* 输出灰色文本
|
|
35
35
|
*/
|
|
36
|
-
export function muted(message:
|
|
36
|
+
export declare function muted(message: string): void;
|
|
37
37
|
/**
|
|
38
38
|
* 输出标题
|
|
39
39
|
*/
|
|
40
|
-
export function title(message:
|
|
40
|
+
export declare function title(message: string): void;
|
|
41
41
|
/**
|
|
42
42
|
* 输出主色文本
|
|
43
43
|
*/
|
|
44
|
-
export function primary(message:
|
|
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
|
|
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.
|
|
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
|
+
}
|
package/src/components/Chat.tsx
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
48
|
-
.then((result
|
|
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
|
*/
|