feat: 添加 Git 工具集和重组描述文件目录结构
Git 工具集: - 新增 10 个 Git 工具: status, diff, log, branch, add, commit, push, pull, checkout, stash - 新增 GitPermissionChecker 权限检查器 - 读操作默认允许,写操作需要用户确认 目录结构优化: - 重组 descriptions 目录,按工具类别分子目录 (shell, filesystem, web, git, todo) - 更新 load_description.ts 支持子目录加载 - 更新 build 脚本支持递归复制描述文件
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc && cp -r src/tools/descriptions/*.txt dist/tools/descriptions/",
|
||||
"build": "tsc && mkdir -p dist/tools/descriptions && cp -r src/tools/descriptions/* dist/tools/descriptions/",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts",
|
||||
"lint": "eslint src/**/*.ts"
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
import type {
|
||||
GitPermissionConfig,
|
||||
GitPermissionContext,
|
||||
GitOperation,
|
||||
PermissionCheckResult,
|
||||
PermissionDecision,
|
||||
PermissionContext,
|
||||
} from '../types.js';
|
||||
import type { PermissionChecker } from './base.js';
|
||||
|
||||
// 读操作列表
|
||||
const READ_OPERATIONS: GitOperation[] = [
|
||||
'status',
|
||||
'diff',
|
||||
'log',
|
||||
'branch_list',
|
||||
'show',
|
||||
];
|
||||
|
||||
// 写操作列表
|
||||
const WRITE_OPERATIONS: GitOperation[] = [
|
||||
'add',
|
||||
'commit',
|
||||
'push',
|
||||
'pull',
|
||||
'checkout',
|
||||
'branch_create',
|
||||
'branch_delete',
|
||||
'stash',
|
||||
'stash_pop',
|
||||
'merge',
|
||||
'rebase',
|
||||
];
|
||||
|
||||
// 危险操作(需要 force 参数的操作)
|
||||
const DANGEROUS_WHEN_FORCED: GitOperation[] = [
|
||||
'push',
|
||||
'reset',
|
||||
'checkout',
|
||||
'rebase',
|
||||
];
|
||||
|
||||
// 默认 Git 权限配置
|
||||
const DEFAULT_CONFIG: GitPermissionConfig = {
|
||||
readOperations: 'allow',
|
||||
writeOperations: 'ask',
|
||||
dangerousOperations: 'ask',
|
||||
};
|
||||
|
||||
/**
|
||||
* Git 操作权限检查器
|
||||
* 控制 Git 仓库操作的权限
|
||||
*/
|
||||
export class GitPermissionChecker implements PermissionChecker {
|
||||
readonly name = 'git';
|
||||
|
||||
private config: GitPermissionConfig;
|
||||
private askCallback?: (ctx: PermissionContext) => Promise<PermissionDecision>;
|
||||
private sessionPermissions = new Map<string, 'allow' | 'deny'>();
|
||||
|
||||
constructor() {
|
||||
this.config = { ...DEFAULT_CONFIG };
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置权限询问回调
|
||||
*/
|
||||
setAskCallback(callback: (ctx: PermissionContext) => Promise<PermissionDecision>): void {
|
||||
this.askCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断操作类型
|
||||
*/
|
||||
private getOperationType(operation: GitOperation, force?: boolean): 'read' | 'write' | 'dangerous' {
|
||||
// 强制操作属于危险操作
|
||||
if (force && DANGEROUS_WHEN_FORCED.includes(operation)) {
|
||||
return 'dangerous';
|
||||
}
|
||||
|
||||
// reset 总是危险操作
|
||||
if (operation === 'reset') {
|
||||
return 'dangerous';
|
||||
}
|
||||
|
||||
if (READ_OPERATIONS.includes(operation)) {
|
||||
return 'read';
|
||||
}
|
||||
|
||||
return 'write';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取操作的描述
|
||||
*/
|
||||
private getOperationDescription(ctx: GitPermissionContext): string {
|
||||
const { operation, target, remote, force, message } = ctx;
|
||||
|
||||
const parts: string[] = [`git ${operation.replace('_', ' ')}`];
|
||||
|
||||
if (force) {
|
||||
parts.push('--force');
|
||||
}
|
||||
|
||||
if (target) {
|
||||
parts.push(target);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
parts.push(`(remote: ${remote})`);
|
||||
}
|
||||
|
||||
if (message) {
|
||||
parts.push(`"${message.substring(0, 50)}${message.length > 50 ? '...' : ''}"`);
|
||||
}
|
||||
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Git 操作权限
|
||||
*/
|
||||
async checkGitPermission(ctx: GitPermissionContext): Promise<PermissionCheckResult> {
|
||||
const { operation, force } = ctx;
|
||||
const operationType = this.getOperationType(operation, force);
|
||||
const description = this.getOperationDescription(ctx);
|
||||
|
||||
// 1. 检查会话级别的临时权限
|
||||
const sessionKey = `git_${operationType}`;
|
||||
const sessionPerm = this.sessionPermissions.get(sessionKey);
|
||||
if (sessionPerm === 'allow') {
|
||||
return {
|
||||
allowed: true,
|
||||
action: 'allow',
|
||||
reason: `本次会话已允许 Git ${operationType === 'read' ? '读' : operationType === 'write' ? '写' : '危险'}操作`,
|
||||
};
|
||||
}
|
||||
if (sessionPerm === 'deny') {
|
||||
return {
|
||||
allowed: false,
|
||||
action: 'deny',
|
||||
reason: `本次会话已拒绝 Git ${operationType === 'read' ? '读' : operationType === 'write' ? '写' : '危险'}操作`,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 根据操作类型确定权限策略
|
||||
let action = this.config.writeOperations;
|
||||
if (operationType === 'read') {
|
||||
action = this.config.readOperations;
|
||||
} else if (operationType === 'dangerous') {
|
||||
action = this.config.dangerousOperations;
|
||||
}
|
||||
|
||||
// 3. 处理权限决策
|
||||
if (action === 'allow') {
|
||||
return {
|
||||
allowed: true,
|
||||
action: 'allow',
|
||||
reason: operationType === 'read' ? '读操作默认允许' : '配置允许',
|
||||
};
|
||||
}
|
||||
|
||||
if (action === 'deny') {
|
||||
return {
|
||||
allowed: false,
|
||||
action: 'deny',
|
||||
reason: operationType === 'dangerous' ? '危险操作默认拒绝' : '配置拒绝',
|
||||
};
|
||||
}
|
||||
|
||||
// action === 'ask'
|
||||
if (!this.askCallback) {
|
||||
return {
|
||||
allowed: false,
|
||||
action: 'ask',
|
||||
needsConfirmation: true,
|
||||
reason: description,
|
||||
};
|
||||
}
|
||||
|
||||
// 调用回调询问用户
|
||||
const decision = await this.askCallback({
|
||||
command: description,
|
||||
workdir: process.cwd(),
|
||||
});
|
||||
|
||||
if (decision.remember) {
|
||||
this.sessionPermissions.set(sessionKey, decision.allow ? 'allow' : 'deny');
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: decision.allow,
|
||||
action: decision.allow ? 'allow' : 'deny',
|
||||
reason: decision.allow ? '用户允许' : '用户拒绝',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现 PermissionChecker 接口的 check 方法
|
||||
*/
|
||||
async check(ctx: PermissionContext): Promise<PermissionCheckResult> {
|
||||
// 从 command 中解析 Git 操作
|
||||
const match = ctx.command.match(/^git[_\s]+(\w+)/);
|
||||
if (!match) {
|
||||
return {
|
||||
allowed: false,
|
||||
action: 'deny',
|
||||
reason: '无法解析 Git 操作',
|
||||
};
|
||||
}
|
||||
|
||||
const operation = match[1] as GitOperation;
|
||||
return this.checkGitPermission({ operation });
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除会话权限
|
||||
*/
|
||||
clearSessionPermissions(): void {
|
||||
this.sessionPermissions.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前配置
|
||||
*/
|
||||
getConfig(): GitPermissionConfig {
|
||||
return { ...this.config };
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
setConfig(config: Partial<GitPermissionConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,4 @@ export type { PermissionChecker, BasePermissionConfig } from './base.js';
|
||||
export { BashPermissionChecker } from './bash.js';
|
||||
export { FilePermissionChecker } from './file.js';
|
||||
export { WebPermissionChecker } from './web.js';
|
||||
export { GitPermissionChecker } from './git.js';
|
||||
|
||||
@@ -4,11 +4,13 @@ import type {
|
||||
PermissionDecision,
|
||||
FilePermissionContext,
|
||||
WebPermissionContext,
|
||||
GitPermissionContext,
|
||||
} from './types.js';
|
||||
import type { PermissionChecker } from './checkers/base.js';
|
||||
import { BashPermissionChecker } from './checkers/bash.js';
|
||||
import { FilePermissionChecker } from './checkers/file.js';
|
||||
import { WebPermissionChecker } from './checkers/web.js';
|
||||
import { GitPermissionChecker } from './checkers/git.js';
|
||||
|
||||
/**
|
||||
* 权限管理器
|
||||
@@ -23,6 +25,7 @@ export class PermissionManager {
|
||||
this.registerChecker(new BashPermissionChecker(projectRoot));
|
||||
this.registerChecker(new FilePermissionChecker(projectRoot));
|
||||
this.registerChecker(new WebPermissionChecker());
|
||||
this.registerChecker(new GitPermissionChecker());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,6 +122,22 @@ export class PermissionManager {
|
||||
return webChecker.checkWebPermission(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Git 操作权限(便捷方法)
|
||||
*/
|
||||
async checkGitPermission(ctx: GitPermissionContext): Promise<PermissionCheckResult> {
|
||||
const gitChecker = this.getChecker<GitPermissionChecker>('git');
|
||||
if (!gitChecker) {
|
||||
return {
|
||||
allowed: false,
|
||||
action: 'ask',
|
||||
needsConfirmation: true,
|
||||
reason: 'Git 权限检查器未注册',
|
||||
};
|
||||
}
|
||||
return gitChecker.checkGitPermission(ctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有检查器的会话权限
|
||||
*/
|
||||
|
||||
@@ -106,3 +106,44 @@ export interface WebPermissionConfig {
|
||||
// 搜索主题限制(空数组表示允许所有)
|
||||
allowedTopics: ('general' | 'news' | 'finance')[];
|
||||
}
|
||||
|
||||
// Git 操作类型
|
||||
export type GitOperation =
|
||||
// 读操作
|
||||
| 'status'
|
||||
| 'diff'
|
||||
| 'log'
|
||||
| 'branch_list'
|
||||
| 'show'
|
||||
// 写操作
|
||||
| 'add'
|
||||
| 'commit'
|
||||
| 'push'
|
||||
| 'pull'
|
||||
| 'checkout'
|
||||
| 'branch_create'
|
||||
| 'branch_delete'
|
||||
| 'stash'
|
||||
| 'stash_pop'
|
||||
| 'reset'
|
||||
| 'merge'
|
||||
| 'rebase';
|
||||
|
||||
// Git 权限请求上下文
|
||||
export interface GitPermissionContext {
|
||||
operation: GitOperation;
|
||||
target?: string; // 分支名、文件路径等
|
||||
remote?: string; // 远程仓库名
|
||||
force?: boolean; // 是否强制操作
|
||||
message?: string; // 提交信息等
|
||||
}
|
||||
|
||||
// Git 权限配置
|
||||
export interface GitPermissionConfig {
|
||||
// 读操作策略(默认 allow)
|
||||
readOperations: PermissionAction;
|
||||
// 写操作策略(默认 ask)
|
||||
writeOperations: PermissionAction;
|
||||
// 危险操作策略(force push, reset --hard 等,默认 ask)
|
||||
dangerousOperations: PermissionAction;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
将文件变更添加到 Git 暂存区,准备提交。
|
||||
|
||||
参数说明:
|
||||
- files: 要暂存的文件列表(数组或单个文件路径)
|
||||
- all: 暂存所有变更(包括新文件和删除)
|
||||
- update: 仅暂存已跟踪文件的变更(不包括新文件)
|
||||
|
||||
使用场景:
|
||||
- 选择性暂存特定文件
|
||||
- 暂存所有变更准备提交
|
||||
- 分批次提交不同功能的代码
|
||||
|
||||
执行后会显示当前暂存状态。
|
||||
@@ -0,0 +1,15 @@
|
||||
管理 Git 分支:列出、创建、删除、重命名分支。
|
||||
|
||||
参数说明:
|
||||
- action: 操作类型
|
||||
- list: 列出分支(默认)
|
||||
- create: 创建新分支
|
||||
- delete: 删除分支
|
||||
- rename: 重命名分支
|
||||
- name: 分支名称(create/delete/rename 时必填)
|
||||
- new_name: 新分支名(rename 时必填)
|
||||
- remote: 显示远程分支
|
||||
- all: 显示所有分支(本地 + 远程)
|
||||
- force: 强制删除未合并的分支
|
||||
|
||||
注意:创建分支后需要使用 git_checkout 切换到新分支。
|
||||
@@ -0,0 +1,14 @@
|
||||
切换 Git 分支或恢复文件到指定状态。
|
||||
|
||||
参数说明:
|
||||
- target: 目标分支名或文件路径(必填)
|
||||
- create: 创建新分支并切换(相当于 git checkout -b)
|
||||
- force: 强制切换(丢弃本地未提交的修改)
|
||||
- file: 指定 target 是文件路径(恢复文件到 HEAD 状态)
|
||||
|
||||
常见场景:
|
||||
- 切换到已有分支: git_checkout with target: "main"
|
||||
- 创建并切换到新分支: git_checkout with target: "feature-xxx", create: true
|
||||
- 恢复单个文件: git_checkout with target: "src/file.ts", file: true
|
||||
|
||||
注意:切换分支前确保没有未提交的变更,或使用 git_stash 暂存。
|
||||
@@ -0,0 +1,14 @@
|
||||
创建 Git 提交,保存暂存区的变更。
|
||||
|
||||
参数说明:
|
||||
- message: 提交信息(必填,除非使用 amend + 无修改)
|
||||
- amend: 修改上次提交(可以修改提交信息或追加变更)
|
||||
- all: 自动暂存所有已跟踪文件的变更后提交
|
||||
|
||||
使用流程:
|
||||
1. 使用 git_add 暂存要提交的文件
|
||||
2. 使用 git_commit 创建提交
|
||||
|
||||
提交信息建议:
|
||||
- 简洁明了地描述本次变更
|
||||
- 使用祈使语气(如 "Add feature" 而非 "Added feature")
|
||||
@@ -0,0 +1,12 @@
|
||||
查看 Git 变更的详细差异,包括文件内容的具体修改。
|
||||
|
||||
参数说明:
|
||||
- path: 指定要查看差异的文件或目录路径
|
||||
- staged: 查看已暂存的变更(默认查看未暂存的变更)
|
||||
- commit: 与指定提交对比(如 HEAD~1, commit-hash)
|
||||
- stat: 仅显示统计信息(修改了多少行)
|
||||
|
||||
使用场景:
|
||||
- 查看修改了哪些内容
|
||||
- 提交前检查变更
|
||||
- 对比不同版本的代码
|
||||
@@ -0,0 +1,14 @@
|
||||
查看 Git 提交历史记录。
|
||||
|
||||
参数说明:
|
||||
- limit: 显示的提交数量(默认 10)
|
||||
- oneline: 每个提交单行显示
|
||||
- file: 查看指定文件的提交历史
|
||||
- author: 筛选指定作者的提交
|
||||
- since: 筛选指定日期之后的提交(如 "2024-01-01" 或 "1 week ago")
|
||||
- graph: 显示分支合并图
|
||||
|
||||
返回内容:
|
||||
- 提交 hash(简短)
|
||||
- 提交信息
|
||||
- 作者和时间
|
||||
@@ -0,0 +1,14 @@
|
||||
从远程仓库拉取更新并合并到本地分支。
|
||||
|
||||
参数说明:
|
||||
- remote: 远程仓库名(默认 origin)
|
||||
- branch: 要拉取的分支名(默认当前分支对应的远程分支)
|
||||
- rebase: 使用 rebase 而非 merge(保持提交历史线性)
|
||||
|
||||
常见场景:
|
||||
- 更新当前分支: git_pull
|
||||
- 使用 rebase 更新: git_pull with rebase: true
|
||||
|
||||
注意事项:
|
||||
- 如果有本地未提交的变更,可能需要先 git_stash 暂存
|
||||
- 如果发生冲突,需要手动解决后提交
|
||||
@@ -0,0 +1,15 @@
|
||||
将本地提交推送到远程仓库。
|
||||
|
||||
参数说明:
|
||||
- remote: 远程仓库名(默认 origin)
|
||||
- branch: 要推送的分支名(默认当前分支)
|
||||
- force: 强制推送(危险!会覆盖远程历史)
|
||||
- set_upstream: 设置上游分支(首次推送新分支时使用 -u)
|
||||
- tags: 推送所有标签
|
||||
|
||||
常见场景:
|
||||
- 推送当前分支: git_push
|
||||
- 首次推送新分支: git_push with set_upstream: true
|
||||
- 推送到特定分支: git_push with branch: "feature-xxx"
|
||||
|
||||
注意:如果推送被拒绝,通常需要先 git_pull 拉取远程更新。
|
||||
@@ -0,0 +1,19 @@
|
||||
临时保存工作区的变更,方便切换分支或拉取更新。
|
||||
|
||||
参数说明:
|
||||
- action: 操作类型
|
||||
- push: 保存当前变更(默认)
|
||||
- pop: 恢复最近的暂存并删除记录
|
||||
- apply: 恢复暂存但保留记录
|
||||
- list: 列出所有暂存
|
||||
- drop: 删除指定暂存
|
||||
- clear: 清除所有暂存
|
||||
- show: 显示暂存的详细内容
|
||||
- message: 暂存说明(push 时可用)
|
||||
- index: 暂存索引(pop/apply/drop 时可用,默认 0 即最近的)
|
||||
- include_untracked: 包含未跟踪的文件
|
||||
|
||||
使用流程:
|
||||
1. git_stash 保存当前工作
|
||||
2. 切换分支或拉取更新
|
||||
3. git_stash with action: "pop" 恢复工作
|
||||
@@ -0,0 +1,14 @@
|
||||
查看 Git 仓库的当前状态,包括已修改、已暂存和未跟踪的文件。
|
||||
|
||||
这是检查仓库状态的首选工具,可以快速了解当前工作区的变更情况。
|
||||
|
||||
参数说明:
|
||||
- short: 简短输出格式(类似 git status -s)
|
||||
- branch: 显示当前分支信息(默认 true)
|
||||
|
||||
返回内容:
|
||||
- 当前分支名
|
||||
- 与远程分支的差异(领先/落后提交数)
|
||||
- 已暂存的变更
|
||||
- 未暂存的变更
|
||||
- 未跟踪的文件
|
||||
@@ -0,0 +1,8 @@
|
||||
读取当前会话的待办事项列表。
|
||||
|
||||
返回所有待办事项及其状态:
|
||||
- pending: 待处理
|
||||
- in_progress: 进行中
|
||||
- completed: 已完成
|
||||
|
||||
用于查看任务进度和剩余工作。
|
||||
@@ -0,0 +1,13 @@
|
||||
管理会话的待办事项列表。
|
||||
|
||||
参数说明:
|
||||
- todos: 完整的待办事项列表,每项包含:
|
||||
- content: 任务内容
|
||||
- status: 状态(pending/in_progress/completed)
|
||||
|
||||
使用场景:
|
||||
- 规划复杂任务的步骤
|
||||
- 跟踪工作进度
|
||||
- 标记已完成的任务
|
||||
|
||||
注意:每次调用会替换整个列表,请确保包含所有需要保留的项目。
|
||||
@@ -0,0 +1,119 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitAddTool: ToolWithMetadata = {
|
||||
name: 'git_add',
|
||||
description: loadDescription('git_add'),
|
||||
metadata: {
|
||||
name: 'git_add',
|
||||
category: 'git',
|
||||
description: '暂存文件到 Git',
|
||||
keywords: ['git', 'add', 'stage', '暂存', '添加', 'staging'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
files: {
|
||||
type: 'array',
|
||||
description: '要暂存的文件列表',
|
||||
required: false,
|
||||
},
|
||||
all: {
|
||||
type: 'boolean',
|
||||
description: '暂存所有变更(-A)',
|
||||
required: false,
|
||||
},
|
||||
update: {
|
||||
type: 'boolean',
|
||||
description: '仅暂存已跟踪文件的变更(-u)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
let files = params.files as string[] | string | undefined;
|
||||
const all = params.all as boolean;
|
||||
const update = params.update as boolean;
|
||||
|
||||
// 转换单个文件为数组
|
||||
if (typeof files === 'string') {
|
||||
files = [files];
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'add',
|
||||
target: all ? '所有文件' : files?.join(', '),
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git add\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let command: string;
|
||||
|
||||
if (all) {
|
||||
command = 'git add -A';
|
||||
} else if (update) {
|
||||
command = 'git add -u';
|
||||
} else if (files && files.length > 0) {
|
||||
// 转义文件名中的特殊字符
|
||||
const escapedFiles = files.map(f => `"${f}"`).join(' ');
|
||||
command = `git add ${escapedFiles}`;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '请指定要暂存的文件(files 参数)或使用 all: true 暂存所有变更',
|
||||
};
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// git add 成功时通常没有输出,显示暂存的文件
|
||||
let output = stdout || '文件已暂存';
|
||||
|
||||
// 获取暂存状态
|
||||
const { stdout: statusOut } = await execAsync('git status -s', {
|
||||
cwd: process.cwd(),
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
if (statusOut) {
|
||||
output += '\n\n当前暂存状态:\n' + statusOut;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output + (stderr ? `\n${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,173 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
import type { GitOperation } from '../../permission/types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitBranchTool: ToolWithMetadata = {
|
||||
name: 'git_branch',
|
||||
description: loadDescription('git_branch'),
|
||||
metadata: {
|
||||
name: 'git_branch',
|
||||
category: 'git',
|
||||
description: '管理 Git 分支',
|
||||
keywords: ['git', 'branch', '分支', 'branches', 'create', 'delete', '创建', '删除'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: '操作类型: list(默认), create, delete, rename',
|
||||
required: false,
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: '分支名称(create/delete/rename 时必填)',
|
||||
required: false,
|
||||
},
|
||||
new_name: {
|
||||
type: 'string',
|
||||
description: '新分支名(rename 时必填)',
|
||||
required: false,
|
||||
},
|
||||
remote: {
|
||||
type: 'boolean',
|
||||
description: '显示远程分支(-r)',
|
||||
required: false,
|
||||
},
|
||||
all: {
|
||||
type: 'boolean',
|
||||
description: '显示所有分支(-a)',
|
||||
required: false,
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: '强制删除未合并的分支(-D)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const action = (params.action as string) || 'list';
|
||||
const name = params.name as string | undefined;
|
||||
const newName = params.new_name as string | undefined;
|
||||
const remote = params.remote as boolean;
|
||||
const all = params.all as boolean;
|
||||
const force = params.force as boolean;
|
||||
|
||||
// 确定权限操作类型
|
||||
let operation: GitOperation = 'branch_list';
|
||||
if (action === 'create') {
|
||||
operation = 'branch_create';
|
||||
} else if (action === 'delete') {
|
||||
operation = 'branch_delete';
|
||||
} else if (action === 'rename') {
|
||||
operation = 'branch_create'; // rename 视为创建操作
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation,
|
||||
target: name,
|
||||
force,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git branch ${action}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let command: string;
|
||||
|
||||
switch (action) {
|
||||
case 'list':
|
||||
const listArgs = ['branch'];
|
||||
if (all) {
|
||||
listArgs.push('-a');
|
||||
} else if (remote) {
|
||||
listArgs.push('-r');
|
||||
}
|
||||
listArgs.push('-v'); // 显示最后提交信息
|
||||
command = `git ${listArgs.join(' ')}`;
|
||||
break;
|
||||
|
||||
case 'create':
|
||||
if (!name) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '创建分支需要提供分支名称(name 参数)',
|
||||
};
|
||||
}
|
||||
command = `git branch ${name}`;
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
if (!name) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '删除分支需要提供分支名称(name 参数)',
|
||||
};
|
||||
}
|
||||
command = `git branch ${force ? '-D' : '-d'} ${name}`;
|
||||
break;
|
||||
|
||||
case 'rename':
|
||||
if (!name || !newName) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '重命名分支需要提供原名称(name)和新名称(new_name)',
|
||||
};
|
||||
}
|
||||
command = `git branch -m ${name} ${newName}`;
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `未知操作: ${action}。支持的操作: list, create, delete, rename`,
|
||||
};
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let output = stdout;
|
||||
if (action !== 'list' && !stdout) {
|
||||
output = `分支操作成功: ${action} ${name}${newName ? ` -> ${newName}` : ''}`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output + (stderr ? `\n${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,136 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitCheckoutTool: ToolWithMetadata = {
|
||||
name: 'git_checkout',
|
||||
description: loadDescription('git_checkout'),
|
||||
metadata: {
|
||||
name: 'git_checkout',
|
||||
category: 'git',
|
||||
description: '切换分支或恢复文件',
|
||||
keywords: ['git', 'checkout', 'switch', '切换', '分支', 'restore', '恢复'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
target: {
|
||||
type: 'string',
|
||||
description: '目标分支名或文件路径',
|
||||
required: true,
|
||||
},
|
||||
create: {
|
||||
type: 'boolean',
|
||||
description: '创建新分支并切换(-b)',
|
||||
required: false,
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: '强制切换(丢弃本地修改)',
|
||||
required: false,
|
||||
},
|
||||
file: {
|
||||
type: 'boolean',
|
||||
description: '目标是文件路径(恢复文件到 HEAD 状态)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const target = params.target as string;
|
||||
const create = params.create as boolean;
|
||||
const force = params.force as boolean;
|
||||
const isFile = params.file as boolean;
|
||||
|
||||
if (!target) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '请指定目标分支名或文件路径(target 参数)',
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'checkout',
|
||||
target,
|
||||
force,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git checkout ${target}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['checkout'];
|
||||
|
||||
if (create) {
|
||||
args.push('-b');
|
||||
}
|
||||
|
||||
if (force) {
|
||||
args.push('-f');
|
||||
}
|
||||
|
||||
if (isFile) {
|
||||
args.push('--', target);
|
||||
} else {
|
||||
args.push(target);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// 切换分支成功的提示通常在 stderr
|
||||
const output = stdout || stderr || `已切换到 ${target}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
const errorMsg = execError.stderr || execError.message;
|
||||
|
||||
// 检查常见错误
|
||||
if (errorMsg.includes('local changes') || errorMsg.includes('would be overwritten')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `本地有未提交的变更会被覆盖。请先提交或暂存(git_stash)变更,或使用 force: true 强制切换(会丢失本地修改)。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (errorMsg.includes('did not match') || errorMsg.includes('pathspec')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `找不到分支或文件: ${target}\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitCommitTool: ToolWithMetadata = {
|
||||
name: 'git_commit',
|
||||
description: loadDescription('git_commit'),
|
||||
metadata: {
|
||||
name: 'git_commit',
|
||||
category: 'git',
|
||||
description: '提交 Git 变更',
|
||||
keywords: ['git', 'commit', '提交', 'save', '保存', 'snapshot'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '提交信息(必填)',
|
||||
required: true,
|
||||
},
|
||||
amend: {
|
||||
type: 'boolean',
|
||||
description: '修改上次提交(--amend)',
|
||||
required: false,
|
||||
},
|
||||
all: {
|
||||
type: 'boolean',
|
||||
description: '自动暂存所有已跟踪文件的变更(-a)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const message = params.message as string;
|
||||
const amend = params.amend as boolean;
|
||||
const all = params.all as boolean;
|
||||
|
||||
if (!message && !amend) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '提交信息是必填的(message 参数)',
|
||||
};
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'commit',
|
||||
message,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git commit\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['commit'];
|
||||
|
||||
if (all) {
|
||||
args.push('-a');
|
||||
}
|
||||
|
||||
if (amend) {
|
||||
args.push('--amend');
|
||||
if (message) {
|
||||
args.push('-m', `"${message.replace(/"/g, '\\"')}"`);
|
||||
} else {
|
||||
args.push('--no-edit');
|
||||
}
|
||||
} else {
|
||||
args.push('-m', `"${message.replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: stdout + (stderr ? `\n${stderr}` : ''),
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
|
||||
// 检查是否是没有变更可提交
|
||||
if (execError.message?.includes('nothing to commit') ||
|
||||
execError.stderr?.includes('nothing to commit')) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '没有变更需要提交。请先使用 git_add 暂存文件。',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: execError.stderr || execError.message,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitDiffTool: ToolWithMetadata = {
|
||||
name: 'git_diff',
|
||||
description: loadDescription('git_diff'),
|
||||
metadata: {
|
||||
name: 'git_diff',
|
||||
category: 'git',
|
||||
description: '查看 Git 变更差异',
|
||||
keywords: ['git', 'diff', '差异', '对比', 'changes', '变更', 'compare'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
path: {
|
||||
type: 'string',
|
||||
description: '指定文件或目录路径',
|
||||
required: false,
|
||||
},
|
||||
staged: {
|
||||
type: 'boolean',
|
||||
description: '显示已暂存的变更(--staged)',
|
||||
required: false,
|
||||
},
|
||||
commit: {
|
||||
type: 'string',
|
||||
description: '与指定提交对比(如 HEAD~1, commit-hash)',
|
||||
required: false,
|
||||
},
|
||||
stat: {
|
||||
type: 'boolean',
|
||||
description: '仅显示统计信息(--stat)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const path = params.path as string | undefined;
|
||||
const staged = params.staged as boolean;
|
||||
const commit = params.commit as string | undefined;
|
||||
const stat = params.stat as boolean;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'diff',
|
||||
target: path,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git diff\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['diff'];
|
||||
|
||||
if (staged) {
|
||||
args.push('--staged');
|
||||
}
|
||||
|
||||
if (stat) {
|
||||
args.push('--stat');
|
||||
}
|
||||
|
||||
if (commit) {
|
||||
args.push(commit);
|
||||
}
|
||||
|
||||
if (path) {
|
||||
args.push('--', path);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 60000,
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
});
|
||||
|
||||
const output = stdout || '(无差异)';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output + (stderr ? `\n${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,126 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitLogTool: ToolWithMetadata = {
|
||||
name: 'git_log',
|
||||
description: loadDescription('git_log'),
|
||||
metadata: {
|
||||
name: 'git_log',
|
||||
category: 'git',
|
||||
description: '查看 Git 提交历史',
|
||||
keywords: ['git', 'log', 'history', '历史', '提交', 'commit', 'commits'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: '显示的提交数量(默认 10)',
|
||||
required: false,
|
||||
},
|
||||
oneline: {
|
||||
type: 'boolean',
|
||||
description: '单行显示(--oneline)',
|
||||
required: false,
|
||||
},
|
||||
file: {
|
||||
type: 'string',
|
||||
description: '查看指定文件的提交历史',
|
||||
required: false,
|
||||
},
|
||||
author: {
|
||||
type: 'string',
|
||||
description: '筛选指定作者的提交',
|
||||
required: false,
|
||||
},
|
||||
since: {
|
||||
type: 'string',
|
||||
description: '起始日期(如 "2024-01-01" 或 "1 week ago")',
|
||||
required: false,
|
||||
},
|
||||
graph: {
|
||||
type: 'boolean',
|
||||
description: '显示分支图(--graph)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const limit = (params.limit as number) || 10;
|
||||
const oneline = params.oneline as boolean;
|
||||
const file = params.file as string | undefined;
|
||||
const author = params.author as string | undefined;
|
||||
const since = params.since as string | undefined;
|
||||
const graph = params.graph as boolean;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'log',
|
||||
target: file,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git log\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['log', `-${limit}`];
|
||||
|
||||
if (oneline) {
|
||||
args.push('--oneline');
|
||||
} else {
|
||||
// 默认格式:更易读
|
||||
args.push('--format=%h %s (%an, %ar)');
|
||||
}
|
||||
|
||||
if (graph) {
|
||||
args.push('--graph');
|
||||
}
|
||||
|
||||
if (author) {
|
||||
args.push(`--author=${author}`);
|
||||
}
|
||||
|
||||
if (since) {
|
||||
args.push(`--since="${since}"`);
|
||||
}
|
||||
|
||||
if (file) {
|
||||
args.push('--', file);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: stdout || '(无提交记录)',
|
||||
};
|
||||
} 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,118 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitPullTool: ToolWithMetadata = {
|
||||
name: 'git_pull',
|
||||
description: loadDescription('git_pull'),
|
||||
metadata: {
|
||||
name: 'git_pull',
|
||||
category: 'git',
|
||||
description: '从远程仓库拉取更新',
|
||||
keywords: ['git', 'pull', '拉取', 'fetch', 'download', '下载', 'update', '更新'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
remote: {
|
||||
type: 'string',
|
||||
description: '远程仓库名(默认 origin)',
|
||||
required: false,
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: '分支名(默认当前分支)',
|
||||
required: false,
|
||||
},
|
||||
rebase: {
|
||||
type: 'boolean',
|
||||
description: '使用 rebase 而非 merge(--rebase)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const remote = (params.remote as string) || 'origin';
|
||||
const branch = params.branch as string | undefined;
|
||||
const rebase = params.rebase as boolean;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'pull',
|
||||
remote,
|
||||
target: branch,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git pull\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['pull'];
|
||||
|
||||
if (rebase) {
|
||||
args.push('--rebase');
|
||||
}
|
||||
|
||||
args.push(remote);
|
||||
|
||||
if (branch) {
|
||||
args.push(branch);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
const output = stdout || stderr || '已是最新';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
const errorMsg = execError.stderr || execError.message;
|
||||
|
||||
// 检查冲突
|
||||
if (errorMsg.includes('CONFLICT') || errorMsg.includes('conflict')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `拉取时发生合并冲突,请手动解决冲突后使用 git_add 和 git_commit 提交。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 检查本地变更
|
||||
if (errorMsg.includes('local changes') || errorMsg.includes('uncommitted changes')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `本地有未提交的变更。请先提交或暂存(git_stash)本地变更。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,145 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitPushTool: ToolWithMetadata = {
|
||||
name: 'git_push',
|
||||
description: loadDescription('git_push'),
|
||||
metadata: {
|
||||
name: 'git_push',
|
||||
category: 'git',
|
||||
description: '推送 Git 变更到远程仓库',
|
||||
keywords: ['git', 'push', '推送', 'upload', '上传', 'remote'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
remote: {
|
||||
type: 'string',
|
||||
description: '远程仓库名(默认 origin)',
|
||||
required: false,
|
||||
},
|
||||
branch: {
|
||||
type: 'string',
|
||||
description: '分支名(默认当前分支)',
|
||||
required: false,
|
||||
},
|
||||
force: {
|
||||
type: 'boolean',
|
||||
description: '强制推送(--force,危险操作)',
|
||||
required: false,
|
||||
},
|
||||
set_upstream: {
|
||||
type: 'boolean',
|
||||
description: '设置上游分支(-u)',
|
||||
required: false,
|
||||
},
|
||||
tags: {
|
||||
type: 'boolean',
|
||||
description: '推送所有标签(--tags)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const remote = (params.remote as string) || 'origin';
|
||||
const branch = params.branch as string | undefined;
|
||||
const force = params.force as boolean;
|
||||
const setUpstream = params.set_upstream as boolean;
|
||||
const tags = params.tags as boolean;
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'push',
|
||||
remote,
|
||||
target: branch,
|
||||
force,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git push${force ? ' --force' : ''}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
// 强制推送警告
|
||||
if (force) {
|
||||
// 这里权限系统已经处理了确认,但可以添加额外的输出
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['push'];
|
||||
|
||||
if (setUpstream) {
|
||||
args.push('-u');
|
||||
}
|
||||
|
||||
if (force) {
|
||||
args.push('--force');
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
args.push('--tags');
|
||||
}
|
||||
|
||||
args.push(remote);
|
||||
|
||||
if (branch) {
|
||||
args.push(branch);
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 120000, // 推送可能需要更长时间
|
||||
});
|
||||
|
||||
// git push 的输出通常在 stderr
|
||||
const output = stdout || stderr || '推送成功';
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output,
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
|
||||
// 检查常见错误
|
||||
const errorMsg = execError.stderr || execError.message;
|
||||
|
||||
if (errorMsg.includes('rejected')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `推送被拒绝。远程分支有新的提交,请先执行 git_pull 拉取更新。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (errorMsg.includes('no upstream')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `当前分支没有设置上游分支。请使用 set_upstream: true 参数。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
import type { GitOperation } from '../../permission/types.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitStashTool: ToolWithMetadata = {
|
||||
name: 'git_stash',
|
||||
description: loadDescription('git_stash'),
|
||||
metadata: {
|
||||
name: 'git_stash',
|
||||
category: 'git',
|
||||
description: '暂存工作区变更',
|
||||
keywords: ['git', 'stash', '暂存', '保存', 'save', 'temporary', '临时'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
action: {
|
||||
type: 'string',
|
||||
description: '操作类型: push(默认), pop, list, apply, drop, clear',
|
||||
required: false,
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '暂存说明(push 时可用)',
|
||||
required: false,
|
||||
},
|
||||
index: {
|
||||
type: 'number',
|
||||
description: '暂存索引(pop/apply/drop 时可用,默认 0)',
|
||||
required: false,
|
||||
},
|
||||
include_untracked: {
|
||||
type: 'boolean',
|
||||
description: '包含未跟踪的文件(-u)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const action = (params.action as string) || 'push';
|
||||
const message = params.message as string | undefined;
|
||||
const index = params.index as number | undefined;
|
||||
const includeUntracked = params.include_untracked as boolean;
|
||||
|
||||
// 确定权限操作类型
|
||||
let operation: GitOperation = 'stash';
|
||||
if (action === 'pop' || action === 'apply') {
|
||||
operation = 'stash_pop';
|
||||
}
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation,
|
||||
message,
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git stash ${action}\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
let command: string;
|
||||
const stashRef = index !== undefined ? `stash@{${index}}` : '';
|
||||
|
||||
switch (action) {
|
||||
case 'push':
|
||||
case 'save':
|
||||
const pushArgs = ['stash', 'push'];
|
||||
if (includeUntracked) {
|
||||
pushArgs.push('-u');
|
||||
}
|
||||
if (message) {
|
||||
pushArgs.push('-m', `"${message.replace(/"/g, '\\"')}"`);
|
||||
}
|
||||
command = `git ${pushArgs.join(' ')}`;
|
||||
break;
|
||||
|
||||
case 'pop':
|
||||
command = `git stash pop ${stashRef}`.trim();
|
||||
break;
|
||||
|
||||
case 'apply':
|
||||
command = `git stash apply ${stashRef}`.trim();
|
||||
break;
|
||||
|
||||
case 'drop':
|
||||
command = `git stash drop ${stashRef}`.trim();
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
command = 'git stash list';
|
||||
break;
|
||||
|
||||
case 'clear':
|
||||
command = 'git stash clear';
|
||||
break;
|
||||
|
||||
case 'show':
|
||||
command = `git stash show -p ${stashRef}`.trim();
|
||||
break;
|
||||
|
||||
default:
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `未知操作: ${action}。支持的操作: push, pop, apply, drop, list, clear, show`,
|
||||
};
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(command, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024 * 10,
|
||||
});
|
||||
|
||||
let output = stdout;
|
||||
|
||||
// 针对不同操作给出友好输出
|
||||
if (!output) {
|
||||
switch (action) {
|
||||
case 'push':
|
||||
case 'save':
|
||||
output = '工作区变更已暂存';
|
||||
break;
|
||||
case 'pop':
|
||||
output = '暂存已恢复并删除';
|
||||
break;
|
||||
case 'apply':
|
||||
output = '暂存已恢复(暂存记录保留)';
|
||||
break;
|
||||
case 'drop':
|
||||
output = '暂存已删除';
|
||||
break;
|
||||
case 'clear':
|
||||
output = '所有暂存已清除';
|
||||
break;
|
||||
case 'list':
|
||||
output = '(无暂存记录)';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: output + (stderr ? `\n${stderr}` : ''),
|
||||
};
|
||||
} catch (error) {
|
||||
const execError = error as { stdout?: string; stderr?: string; message: string };
|
||||
const errorMsg = execError.stderr || execError.message;
|
||||
|
||||
// 检查常见错误
|
||||
if (errorMsg.includes('No local changes') || errorMsg.includes('no changes')) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '没有需要暂存的变更',
|
||||
};
|
||||
}
|
||||
|
||||
if (errorMsg.includes('CONFLICT') || errorMsg.includes('conflict')) {
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: `恢复暂存时发生冲突,请手动解决。\n${errorMsg}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
output: execError.stdout || '',
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export const gitStatusTool: ToolWithMetadata = {
|
||||
name: 'git_status',
|
||||
description: loadDescription('git_status'),
|
||||
metadata: {
|
||||
name: 'git_status',
|
||||
category: 'git',
|
||||
description: '查看 Git 仓库状态',
|
||||
keywords: ['git', 'status', '状态', '仓库', 'repository', 'changes', '变更'],
|
||||
deferLoading: false,
|
||||
},
|
||||
parameters: {
|
||||
short: {
|
||||
type: 'boolean',
|
||||
description: '简短输出格式(-s)',
|
||||
required: false,
|
||||
},
|
||||
branch: {
|
||||
type: 'boolean',
|
||||
description: '显示分支信息(-b)',
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
execute: async (params: Record<string, unknown>): Promise<ToolResult> => {
|
||||
const short = params.short as boolean;
|
||||
const branch = params.branch !== false; // 默认显示分支
|
||||
|
||||
// 权限检查
|
||||
const permissionManager = getPermissionManager();
|
||||
const permResult = await permissionManager.checkGitPermission({
|
||||
operation: 'status',
|
||||
});
|
||||
|
||||
if (!permResult.allowed) {
|
||||
if (permResult.needsConfirmation) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `需要用户确认: git status\n原因: ${permResult.reason || '需要权限确认'}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: `权限被拒绝: ${permResult.reason || '操作不被允许'}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const args: string[] = ['status'];
|
||||
|
||||
if (short) {
|
||||
args.push('-s');
|
||||
}
|
||||
|
||||
if (branch) {
|
||||
args.push('-b');
|
||||
}
|
||||
|
||||
const { stdout, stderr } = await execAsync(`git ${args.join(' ')}`, {
|
||||
cwd: process.cwd(),
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: stdout + (stderr ? `\n${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,10 @@
|
||||
export { gitStatusTool } from './git_status.js';
|
||||
export { gitDiffTool } from './git_diff.js';
|
||||
export { gitLogTool } from './git_log.js';
|
||||
export { gitBranchTool } from './git_branch.js';
|
||||
export { gitAddTool } from './git_add.js';
|
||||
export { gitCommitTool } from './git_commit.js';
|
||||
export { gitPushTool } from './git_push.js';
|
||||
export { gitPullTool } from './git_pull.js';
|
||||
export { gitCheckoutTool } from './git_checkout.js';
|
||||
export { gitStashTool } from './git_stash.js';
|
||||
@@ -26,6 +26,20 @@ import {
|
||||
// Web 工具
|
||||
import { webSearchTool, webExtractTool } from './web/index.js';
|
||||
|
||||
// Git 工具
|
||||
import {
|
||||
gitStatusTool,
|
||||
gitDiffTool,
|
||||
gitLogTool,
|
||||
gitBranchTool,
|
||||
gitAddTool,
|
||||
gitCommitTool,
|
||||
gitPushTool,
|
||||
gitPullTool,
|
||||
gitCheckoutTool,
|
||||
gitStashTool,
|
||||
} from './git/index.js';
|
||||
|
||||
// 所有工具列表(用于注册)
|
||||
const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
// 核心工具 (deferLoading: false)
|
||||
@@ -50,6 +64,18 @@ const allToolsWithMetadata: ToolWithMetadata[] = [
|
||||
// Web 工具 (deferLoading: false)
|
||||
webSearchTool,
|
||||
webExtractTool,
|
||||
|
||||
// Git 工具 (deferLoading: false)
|
||||
gitStatusTool,
|
||||
gitDiffTool,
|
||||
gitLogTool,
|
||||
gitBranchTool,
|
||||
gitAddTool,
|
||||
gitCommitTool,
|
||||
gitPushTool,
|
||||
gitPullTool,
|
||||
gitCheckoutTool,
|
||||
gitStashTool,
|
||||
];
|
||||
|
||||
// 注册所有工具到 registry
|
||||
|
||||
@@ -5,8 +5,47 @@ import { fileURLToPath } from 'url';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// 工具名到子目录的映射
|
||||
const TOOL_CATEGORY_MAP: Record<string, string> = {
|
||||
// shell
|
||||
bash: 'shell',
|
||||
// filesystem
|
||||
read_file: 'filesystem',
|
||||
write_file: 'filesystem',
|
||||
edit_file: 'filesystem',
|
||||
list_directory: 'filesystem',
|
||||
create_directory: 'filesystem',
|
||||
search_files: 'filesystem',
|
||||
grep_content: 'filesystem',
|
||||
get_file_info: 'filesystem',
|
||||
move_file: 'filesystem',
|
||||
copy_file: 'filesystem',
|
||||
delete_file: 'filesystem',
|
||||
// web
|
||||
web_search: 'web',
|
||||
web_extract: 'web',
|
||||
// git
|
||||
git_status: 'git',
|
||||
git_diff: 'git',
|
||||
git_log: 'git',
|
||||
git_branch: 'git',
|
||||
git_add: 'git',
|
||||
git_commit: 'git',
|
||||
git_push: 'git',
|
||||
git_pull: 'git',
|
||||
git_checkout: 'git',
|
||||
git_stash: 'git',
|
||||
// todo
|
||||
todo_read: 'todo',
|
||||
todo_write: 'todo',
|
||||
};
|
||||
|
||||
export function loadDescription(toolName: string): string {
|
||||
const filePath = path.join(__dirname, 'descriptions', `${toolName}.txt`);
|
||||
const category = TOOL_CATEGORY_MAP[toolName];
|
||||
const filePath = category
|
||||
? path.join(__dirname, 'descriptions', category, `${toolName}.txt`)
|
||||
: path.join(__dirname, 'descriptions', `${toolName}.txt`);
|
||||
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf-8').trim();
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user