Initial commit
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
# Anthropic API Key (必需)
|
||||||
|
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
||||||
|
|
||||||
|
# 可选配置
|
||||||
|
AI_MODEL=claude-sonnet-4-20250514
|
||||||
|
AI_MAX_TOKENS=4096
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage/
|
||||||
Generated
+1810
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"name": "ai-terminal-assistant",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A terminal-based AI coding assistant powered by Claude",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"bin": {
|
||||||
|
"ai-assist": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts",
|
||||||
|
"lint": "eslint src/**/*.ts"
|
||||||
|
},
|
||||||
|
"keywords": ["ai", "cli", "assistant", "claude", "terminal"],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@anthropic-ai/sdk": "^0.32.0",
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"commander": "^12.1.0",
|
||||||
|
"ora": "^8.1.0",
|
||||||
|
"inquirer": "^12.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.0.0",
|
||||||
|
"typescript": "^5.6.0",
|
||||||
|
"tsx": "^4.19.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import Anthropic from '@anthropic-ai/sdk';
|
||||||
|
import type { Tool, ToolResult, Message, AgentConfig } from '../types/index.js';
|
||||||
|
|
||||||
|
export class Agent {
|
||||||
|
private client: Anthropic;
|
||||||
|
private config: AgentConfig;
|
||||||
|
private tools: Map<string, Tool> = new Map();
|
||||||
|
private conversationHistory: Message[] = [];
|
||||||
|
|
||||||
|
constructor(config: AgentConfig) {
|
||||||
|
this.config = config;
|
||||||
|
this.client = new Anthropic({
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册工具
|
||||||
|
registerTool(tool: Tool): void {
|
||||||
|
this.tools.set(tool.name, tool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有工具定义(用于 Claude API)
|
||||||
|
private getToolDefinitions(): Anthropic.Tool[] {
|
||||||
|
return Array.from(this.tools.values()).map((tool) => ({
|
||||||
|
name: tool.name,
|
||||||
|
description: tool.description,
|
||||||
|
input_schema: {
|
||||||
|
type: 'object' as const,
|
||||||
|
properties: Object.fromEntries(
|
||||||
|
Object.entries(tool.parameters).map(([key, param]) => [
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
type: param.type,
|
||||||
|
description: param.description,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
),
|
||||||
|
required: Object.entries(tool.parameters)
|
||||||
|
.filter(([, param]) => param.required)
|
||||||
|
.map(([key]) => key),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行工具
|
||||||
|
private async executeTool(
|
||||||
|
toolName: string,
|
||||||
|
input: Record<string, unknown>
|
||||||
|
): Promise<ToolResult> {
|
||||||
|
const tool = this.tools.get(toolName);
|
||||||
|
if (!tool) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: `Tool "${toolName}" not found`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await tool.execute(input);
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送消息并处理响应
|
||||||
|
async chat(
|
||||||
|
userMessage: string,
|
||||||
|
onStream?: (text: string) => void
|
||||||
|
): Promise<string> {
|
||||||
|
// 添加用户消息到历史
|
||||||
|
this.conversationHistory.push({
|
||||||
|
role: 'user',
|
||||||
|
content: userMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages: Anthropic.MessageParam[] = this.conversationHistory.map(
|
||||||
|
(msg) => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let fullResponse = '';
|
||||||
|
|
||||||
|
// 循环处理,直到没有工具调用
|
||||||
|
while (true) {
|
||||||
|
const response = await this.client.messages.create({
|
||||||
|
model: this.config.model,
|
||||||
|
max_tokens: this.config.maxTokens,
|
||||||
|
system: this.config.systemPrompt,
|
||||||
|
tools: this.getToolDefinitions(),
|
||||||
|
messages,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理响应内容
|
||||||
|
const textBlocks: string[] = [];
|
||||||
|
const toolUseBlocks: Anthropic.ToolUseBlock[] = [];
|
||||||
|
|
||||||
|
for (const block of response.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
textBlocks.push(block.text);
|
||||||
|
if (onStream) {
|
||||||
|
onStream(block.text);
|
||||||
|
}
|
||||||
|
} else if (block.type === 'tool_use') {
|
||||||
|
toolUseBlocks.push(block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fullResponse += textBlocks.join('');
|
||||||
|
|
||||||
|
// 如果没有工具调用,结束循环
|
||||||
|
if (toolUseBlocks.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加 assistant 消息
|
||||||
|
messages.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: response.content,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理工具调用
|
||||||
|
const toolResults: Anthropic.ToolResultBlockParam[] = [];
|
||||||
|
|
||||||
|
for (const toolUse of toolUseBlocks) {
|
||||||
|
if (onStream) {
|
||||||
|
onStream(`\n[调用工具: ${toolUse.name}]\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.executeTool(
|
||||||
|
toolUse.name,
|
||||||
|
toolUse.input as Record<string, unknown>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (onStream) {
|
||||||
|
onStream(
|
||||||
|
result.success ? `[结果: ${result.output}]\n` : `[错误: ${result.error}]\n`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toolResults.push({
|
||||||
|
type: 'tool_result',
|
||||||
|
tool_use_id: toolUse.id,
|
||||||
|
content: result.success ? result.output : `Error: ${result.error}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加工具结果
|
||||||
|
messages.push({
|
||||||
|
role: 'user',
|
||||||
|
content: toolResults,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存助手响应到历史
|
||||||
|
this.conversationHistory.push({
|
||||||
|
role: 'assistant',
|
||||||
|
content: fullResponse,
|
||||||
|
});
|
||||||
|
|
||||||
|
return fullResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空对话历史
|
||||||
|
clearHistory(): void {
|
||||||
|
this.conversationHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对话历史
|
||||||
|
getHistory(): Message[] {
|
||||||
|
return [...this.conversationHistory];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { Command } from 'commander';
|
||||||
|
import { Agent } from './core/agent.js';
|
||||||
|
import { TerminalUI } from './ui/terminal.js';
|
||||||
|
import { loadConfig, initConfig } from './utils/config.js';
|
||||||
|
import {
|
||||||
|
bashTool,
|
||||||
|
readFileTool,
|
||||||
|
writeFileTool,
|
||||||
|
listDirTool,
|
||||||
|
searchFilesTool,
|
||||||
|
} from './tools/index.js';
|
||||||
|
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name('ai-assist')
|
||||||
|
.description('AI Terminal Assistant - 终端中的 AI 编程助手')
|
||||||
|
.version('1.0.0');
|
||||||
|
|
||||||
|
// 初始化命令
|
||||||
|
program
|
||||||
|
.command('init')
|
||||||
|
.description('初始化配置(设置 API Key 等)')
|
||||||
|
.action(async () => {
|
||||||
|
await initConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// 单次查询命令
|
||||||
|
program
|
||||||
|
.command('ask <question>')
|
||||||
|
.description('单次提问(不进入交互模式)')
|
||||||
|
.action(async (question: string) => {
|
||||||
|
const config = loadConfig();
|
||||||
|
const agent = new Agent(config);
|
||||||
|
|
||||||
|
// 注册工具
|
||||||
|
agent.registerTool(bashTool);
|
||||||
|
agent.registerTool(readFileTool);
|
||||||
|
agent.registerTool(writeFileTool);
|
||||||
|
agent.registerTool(listDirTool);
|
||||||
|
agent.registerTool(searchFilesTool);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await agent.chat(question, (text) => {
|
||||||
|
process.stdout.write(text);
|
||||||
|
});
|
||||||
|
console.log('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'错误:',
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 默认:交互模式
|
||||||
|
program.action(async () => {
|
||||||
|
const config = loadConfig();
|
||||||
|
const agent = new Agent(config);
|
||||||
|
|
||||||
|
// 注册所有工具
|
||||||
|
agent.registerTool(bashTool);
|
||||||
|
agent.registerTool(readFileTool);
|
||||||
|
agent.registerTool(writeFileTool);
|
||||||
|
agent.registerTool(listDirTool);
|
||||||
|
agent.registerTool(searchFilesTool);
|
||||||
|
|
||||||
|
// 启动终端 UI
|
||||||
|
const ui = new TerminalUI(agent);
|
||||||
|
|
||||||
|
// 优雅退出
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
console.log('\n\n👋 再见!');
|
||||||
|
ui.close();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await ui.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
program.parse();
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
import type { Tool, ToolResult } from '../types/index.js';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
export const bashTool: Tool = {
|
||||||
|
name: 'bash',
|
||||||
|
description: '执行 bash 命令。可以用于运行系统命令、安装包、git 操作等。',
|
||||||
|
parameters: {
|
||||||
|
command: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要执行的 bash 命令',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
cwd: {
|
||||||
|
type: 'string',
|
||||||
|
description: '工作目录(可选)',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||||
|
const command = params.command as string;
|
||||||
|
const cwd = (params.cwd as string) || process.cwd();
|
||||||
|
|
||||||
|
// 安全检查:禁止危险命令
|
||||||
|
const dangerousPatterns = [
|
||||||
|
/rm\s+-rf\s+\//, // rm -rf /
|
||||||
|
/mkfs/, // 格式化磁盘
|
||||||
|
/dd\s+if=.*of=\/dev/, // 直接写入设备
|
||||||
|
/>\s*\/dev\/sd/, // 重定向到磁盘设备
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of dangerousPatterns) {
|
||||||
|
if (pattern.test(command)) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: '检测到危险命令,已阻止执行',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execAsync(command, {
|
||||||
|
cwd,
|
||||||
|
timeout: 60000, // 60 秒超时
|
||||||
|
maxBuffer: 1024 * 1024 * 10, // 10MB 输出限制
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: stdout + (stderr ? `\nSTDERR: ${stderr}` : ''),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: execError.stdout || '',
|
||||||
|
error: execError.stderr || execError.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import * as fs from 'fs/promises';
|
||||||
|
import * as path from 'path';
|
||||||
|
import type { Tool, ToolResult } from '../types/index.js';
|
||||||
|
|
||||||
|
// 读取文件工具
|
||||||
|
export const readFileTool: Tool = {
|
||||||
|
name: 'read_file',
|
||||||
|
description: '读取指定文件的内容',
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要读取的文件路径(相对或绝对路径)',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||||
|
const filePath = params.path as string;
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(process.cwd(), filePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(absolutePath, 'utf-8');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: content,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 写入文件工具
|
||||||
|
export const writeFileTool: Tool = {
|
||||||
|
name: 'write_file',
|
||||||
|
description: '创建或覆盖文件内容',
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要写入的文件路径',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要写入的内容',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||||
|
const filePath = params.path as string;
|
||||||
|
const content = params.content as string;
|
||||||
|
const absolutePath = path.isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: path.join(process.cwd(), filePath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保目录存在
|
||||||
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||||
|
await fs.writeFile(absolutePath, content, 'utf-8');
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: `文件已写入: ${absolutePath}`,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 列出目录工具
|
||||||
|
export const listDirTool: Tool = {
|
||||||
|
name: 'list_directory',
|
||||||
|
description: '列出指定目录下的文件和文件夹',
|
||||||
|
parameters: {
|
||||||
|
path: {
|
||||||
|
type: 'string',
|
||||||
|
description: '要列出的目录路径',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||||
|
const dirPath = params.path as string;
|
||||||
|
const absolutePath = path.isAbsolute(dirPath)
|
||||||
|
? dirPath
|
||||||
|
: path.join(process.cwd(), dirPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
|
||||||
|
const result = entries
|
||||||
|
.map((entry) => {
|
||||||
|
const prefix = entry.isDirectory() ? '📁' : '📄';
|
||||||
|
return `${prefix} ${entry.name}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: result || '(空目录)',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 搜索文件工具
|
||||||
|
export const searchFilesTool: Tool = {
|
||||||
|
name: 'search_files',
|
||||||
|
description: '在目录中搜索匹配模式的文件',
|
||||||
|
parameters: {
|
||||||
|
directory: {
|
||||||
|
type: 'string',
|
||||||
|
description: '搜索的起始目录',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
pattern: {
|
||||||
|
type: 'string',
|
||||||
|
description: '文件名匹配模式(支持 glob 模式,如 *.ts)',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||||
|
const directory = params.directory as string;
|
||||||
|
const pattern = params.pattern as string;
|
||||||
|
const absolutePath = path.isAbsolute(directory)
|
||||||
|
? directory
|
||||||
|
: path.join(process.cwd(), directory);
|
||||||
|
|
||||||
|
const matches: string[] = [];
|
||||||
|
const regex = new RegExp(
|
||||||
|
pattern.replace(/\*/g, '.*').replace(/\?/g, '.'),
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
|
||||||
|
async function searchRecursive(dir: string, depth = 0): Promise<void> {
|
||||||
|
if (depth > 10) return; // 限制递归深度
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
// 跳过 node_modules 和隐藏目录
|
||||||
|
if (entry.name.startsWith('.') || entry.name === 'node_modules') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
await searchRecursive(fullPath, depth + 1);
|
||||||
|
} else if (regex.test(entry.name)) {
|
||||||
|
matches.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略权限错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await searchRecursive(absolutePath);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output:
|
||||||
|
matches.length > 0
|
||||||
|
? matches.join('\n')
|
||||||
|
: '没有找到匹配的文件',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: '',
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export { bashTool } from './bash.js';
|
||||||
|
export {
|
||||||
|
readFileTool,
|
||||||
|
writeFileTool,
|
||||||
|
listDirTool,
|
||||||
|
searchFilesTool,
|
||||||
|
} from './file.js';
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// 消息类型
|
||||||
|
export interface Message {
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具定义
|
||||||
|
export interface Tool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
parameters: Record<string, ToolParameter>;
|
||||||
|
execute: (params: Record<string, unknown>) => Promise<ToolResult>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolParameter {
|
||||||
|
type: 'string' | 'number' | 'boolean' | 'array' | 'object';
|
||||||
|
description: string;
|
||||||
|
required?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToolResult {
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具调用请求
|
||||||
|
export interface ToolCall {
|
||||||
|
name: string;
|
||||||
|
input: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agent 配置
|
||||||
|
export interface AgentConfig {
|
||||||
|
apiKey: string;
|
||||||
|
model: string;
|
||||||
|
maxTokens: number;
|
||||||
|
systemPrompt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会话上下文
|
||||||
|
export interface ConversationContext {
|
||||||
|
messages: Message[];
|
||||||
|
workingDirectory: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import * as readline from 'readline';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import ora from 'ora';
|
||||||
|
import type { Agent } from '../core/agent.js';
|
||||||
|
|
||||||
|
export class TerminalUI {
|
||||||
|
private agent: Agent;
|
||||||
|
private rl: readline.Interface;
|
||||||
|
private spinner = ora();
|
||||||
|
|
||||||
|
constructor(agent: Agent) {
|
||||||
|
this.agent = agent;
|
||||||
|
this.rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示欢迎信息
|
||||||
|
private showWelcome(): void {
|
||||||
|
console.log(chalk.cyan('\n╔════════════════════════════════════════╗'));
|
||||||
|
console.log(chalk.cyan('║') + chalk.bold.white(' 🤖 AI Terminal Assistant ') + chalk.cyan('║'));
|
||||||
|
console.log(chalk.cyan('║') + chalk.gray(' Powered by Claude ') + chalk.cyan('║'));
|
||||||
|
console.log(chalk.cyan('╚════════════════════════════════════════╝\n'));
|
||||||
|
console.log(chalk.gray('输入你的问题,或使用以下命令:'));
|
||||||
|
console.log(chalk.yellow(' /help') + chalk.gray(' - 显示帮助'));
|
||||||
|
console.log(chalk.yellow(' /clear') + chalk.gray(' - 清空对话历史'));
|
||||||
|
console.log(chalk.yellow(' /exit') + chalk.gray(' - 退出程序'));
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理特殊命令
|
||||||
|
private handleCommand(input: string): boolean {
|
||||||
|
const command = input.toLowerCase().trim();
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case '/help':
|
||||||
|
console.log(chalk.cyan('\n📖 帮助信息:'));
|
||||||
|
console.log(chalk.white(' 这是一个 AI 编程助手,可以帮你:'));
|
||||||
|
console.log(chalk.gray(' • 读写文件'));
|
||||||
|
console.log(chalk.gray(' • 执行 bash 命令'));
|
||||||
|
console.log(chalk.gray(' • 搜索代码'));
|
||||||
|
console.log(chalk.gray(' • 回答编程问题'));
|
||||||
|
console.log('');
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case '/clear':
|
||||||
|
this.agent.clearHistory();
|
||||||
|
console.log(chalk.green('✓ 对话历史已清空\n'));
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case '/exit':
|
||||||
|
case '/quit':
|
||||||
|
console.log(chalk.cyan('\n👋 再见!\n'));
|
||||||
|
process.exit(0);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提问并获取用户输入
|
||||||
|
private prompt(): Promise<string> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.rl.question(chalk.green('You > '), (answer) => {
|
||||||
|
resolve(answer);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主循环
|
||||||
|
async start(): Promise<void> {
|
||||||
|
this.showWelcome();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const input = await this.prompt();
|
||||||
|
|
||||||
|
// 跳过空输入
|
||||||
|
if (!input.trim()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理命令
|
||||||
|
if (input.startsWith('/')) {
|
||||||
|
if (this.handleCommand(input)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送给 AI
|
||||||
|
this.spinner.start(chalk.gray('思考中...'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
let isFirstChunk = true;
|
||||||
|
|
||||||
|
await this.agent.chat(input, (text) => {
|
||||||
|
if (isFirstChunk) {
|
||||||
|
this.spinner.stop();
|
||||||
|
process.stdout.write(chalk.blue('AI > '));
|
||||||
|
isFirstChunk = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理工具调用的输出
|
||||||
|
if (text.startsWith('\n[调用工具:')) {
|
||||||
|
process.stdout.write(chalk.yellow(text));
|
||||||
|
} else if (text.startsWith('[结果:') || text.startsWith('[错误:')) {
|
||||||
|
process.stdout.write(chalk.gray(text));
|
||||||
|
} else {
|
||||||
|
process.stdout.write(text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('\n');
|
||||||
|
} catch (error) {
|
||||||
|
this.spinner.stop();
|
||||||
|
console.log(
|
||||||
|
chalk.red(
|
||||||
|
`\n❌ 错误: ${error instanceof Error ? error.message : String(error)}\n`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭
|
||||||
|
close(): void {
|
||||||
|
this.rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import type { AgentConfig } from '../types/index.js';
|
||||||
|
|
||||||
|
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||||
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||||
|
|
||||||
|
interface StoredConfig {
|
||||||
|
apiKey?: string;
|
||||||
|
model?: string;
|
||||||
|
maxTokens?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认系统提示词
|
||||||
|
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
|
||||||
|
- 读取和写入文件
|
||||||
|
- 执行 bash 命令
|
||||||
|
- 搜索代码和文件
|
||||||
|
- 回答编程问题
|
||||||
|
|
||||||
|
使用工具时请注意:
|
||||||
|
1. 在修改文件前,先读取文件内容
|
||||||
|
2. 执行可能有风险的命令前,先向用户确认
|
||||||
|
3. 给出清晰、简洁的回答
|
||||||
|
|
||||||
|
当前工作目录: ${process.cwd()}
|
||||||
|
操作系统: ${process.platform}`;
|
||||||
|
|
||||||
|
// 加载配置
|
||||||
|
export function loadConfig(): AgentConfig {
|
||||||
|
// 优先从环境变量获取
|
||||||
|
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||||
|
const model = process.env.AI_MODEL || 'claude-sonnet-4-20250514';
|
||||||
|
const maxTokens = parseInt(process.env.AI_MAX_TOKENS || '4096', 10);
|
||||||
|
|
||||||
|
// 如果环境变量没有,尝试从配置文件读取
|
||||||
|
let storedConfig: StoredConfig = {};
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||||
|
storedConfig = JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
// 忽略解析错误
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalApiKey = apiKey || storedConfig.apiKey;
|
||||||
|
|
||||||
|
if (!finalApiKey) {
|
||||||
|
console.error('❌ 错误: 未设置 ANTHROPIC_API_KEY');
|
||||||
|
console.error('请设置环境变量: export ANTHROPIC_API_KEY=your-api-key');
|
||||||
|
console.error('或运行: ai-assist --init 进行初始化配置');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
apiKey: finalApiKey,
|
||||||
|
model: storedConfig.model || model,
|
||||||
|
maxTokens: storedConfig.maxTokens || maxTokens,
|
||||||
|
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
export function saveConfig(config: Partial<StoredConfig>): void {
|
||||||
|
// 确保目录存在
|
||||||
|
if (!fs.existsSync(CONFIG_DIR)) {
|
||||||
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取现有配置
|
||||||
|
let existingConfig: StoredConfig = {};
|
||||||
|
if (fs.existsSync(CONFIG_FILE)) {
|
||||||
|
try {
|
||||||
|
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
||||||
|
existingConfig = JSON.parse(content);
|
||||||
|
} catch {
|
||||||
|
// 忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并并保存
|
||||||
|
const newConfig = { ...existingConfig, ...config };
|
||||||
|
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置向导
|
||||||
|
export async function initConfig(): Promise<void> {
|
||||||
|
const { default: inquirer } = await import('inquirer');
|
||||||
|
|
||||||
|
console.log('\n🔧 初始化 AI Terminal Assistant 配置\n');
|
||||||
|
|
||||||
|
const answers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'apiKey',
|
||||||
|
message: '请输入你的 Anthropic API Key:',
|
||||||
|
validate: (input: string) =>
|
||||||
|
input.length > 0 || 'API Key 不能为空',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'model',
|
||||||
|
message: '选择默认模型:',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Claude Sonnet 4 (推荐,平衡性能和成本)', value: 'claude-sonnet-4-20250514' },
|
||||||
|
{ name: 'Claude Opus 4 (最强,成本较高)', value: 'claude-opus-4-20250514' },
|
||||||
|
{ name: 'Claude 3.5 Haiku (快速,成本低)', value: 'claude-3-5-haiku-20241022' },
|
||||||
|
],
|
||||||
|
default: 'claude-sonnet-4-20250514',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
name: 'maxTokens',
|
||||||
|
message: '最大输出 token 数:',
|
||||||
|
default: 4096,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
saveConfig(answers);
|
||||||
|
console.log('\n✅ 配置已保存到', CONFIG_FILE);
|
||||||
|
console.log('现在可以运行 ai-assist 开始使用了!\n');
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user