From dddec9b6d5164d7d5e0c1cc0a0676775c3ee9eea Mon Sep 17 00:00:00 2001 From: kurihada Date: Thu, 11 Dec 2025 10:30:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20Git=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E9=9B=86=E5=92=8C=E9=87=8D=E7=BB=84=E6=8F=8F=E8=BF=B0?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E7=9B=AE=E5=BD=95=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git 工具集: - 新增 10 个 Git 工具: status, diff, log, branch, add, commit, push, pull, checkout, stash - 新增 GitPermissionChecker 权限检查器 - 读操作默认允许,写操作需要用户确认 目录结构优化: - 重组 descriptions 目录,按工具类别分子目录 (shell, filesystem, web, git, todo) - 更新 load_description.ts 支持子目录加载 - 更新 build 脚本支持递归复制描述文件 --- package.json | 2 +- src/permission/checkers/git.ts | 236 ++++++++++++++++++ src/permission/checkers/index.ts | 1 + src/permission/manager.ts | 19 ++ src/permission/types.ts | 41 +++ .../{ => filesystem}/copy_file.txt | 0 .../{ => filesystem}/create_directory.txt | 0 .../{ => filesystem}/delete_file.txt | 0 .../{ => filesystem}/edit_file.txt | 0 .../{ => filesystem}/get_file_info.txt | 0 .../{ => filesystem}/grep_content.txt | 0 .../{ => filesystem}/list_directory.txt | 0 .../{ => filesystem}/move_file.txt | 0 .../{ => filesystem}/read_file.txt | 0 .../{ => filesystem}/search_files.txt | 0 .../{ => filesystem}/write_file.txt | 0 src/tools/descriptions/git/git_add.txt | 13 + src/tools/descriptions/git/git_branch.txt | 15 ++ src/tools/descriptions/git/git_checkout.txt | 14 ++ src/tools/descriptions/git/git_commit.txt | 14 ++ src/tools/descriptions/git/git_diff.txt | 12 + src/tools/descriptions/git/git_log.txt | 14 ++ src/tools/descriptions/git/git_pull.txt | 14 ++ src/tools/descriptions/git/git_push.txt | 15 ++ src/tools/descriptions/git/git_stash.txt | 19 ++ src/tools/descriptions/git/git_status.txt | 14 ++ src/tools/descriptions/{ => shell}/bash.txt | 0 src/tools/descriptions/todo/todo_read.txt | 8 + src/tools/descriptions/todo/todo_write.txt | 13 + .../descriptions/{ => web}/web_extract.txt | 0 .../descriptions/{ => web}/web_search.txt | 0 src/tools/git/git_add.ts | 119 +++++++++ src/tools/git/git_branch.ts | 173 +++++++++++++ src/tools/git/git_checkout.ts | 136 ++++++++++ src/tools/git/git_commit.ts | 119 +++++++++ src/tools/git/git_diff.ts | 110 ++++++++ src/tools/git/git_log.ts | 126 ++++++++++ src/tools/git/git_pull.ts | 118 +++++++++ src/tools/git/git_push.ts | 145 +++++++++++ src/tools/git/git_stash.ts | 191 ++++++++++++++ src/tools/git/git_status.ts | 86 +++++++ src/tools/git/index.ts | 10 + src/tools/index.ts | 26 ++ src/tools/load_description.ts | 41 ++- 44 files changed, 1862 insertions(+), 2 deletions(-) create mode 100644 src/permission/checkers/git.ts rename src/tools/descriptions/{ => filesystem}/copy_file.txt (100%) rename src/tools/descriptions/{ => filesystem}/create_directory.txt (100%) rename src/tools/descriptions/{ => filesystem}/delete_file.txt (100%) rename src/tools/descriptions/{ => filesystem}/edit_file.txt (100%) rename src/tools/descriptions/{ => filesystem}/get_file_info.txt (100%) rename src/tools/descriptions/{ => filesystem}/grep_content.txt (100%) rename src/tools/descriptions/{ => filesystem}/list_directory.txt (100%) rename src/tools/descriptions/{ => filesystem}/move_file.txt (100%) rename src/tools/descriptions/{ => filesystem}/read_file.txt (100%) rename src/tools/descriptions/{ => filesystem}/search_files.txt (100%) rename src/tools/descriptions/{ => filesystem}/write_file.txt (100%) create mode 100644 src/tools/descriptions/git/git_add.txt create mode 100644 src/tools/descriptions/git/git_branch.txt create mode 100644 src/tools/descriptions/git/git_checkout.txt create mode 100644 src/tools/descriptions/git/git_commit.txt create mode 100644 src/tools/descriptions/git/git_diff.txt create mode 100644 src/tools/descriptions/git/git_log.txt create mode 100644 src/tools/descriptions/git/git_pull.txt create mode 100644 src/tools/descriptions/git/git_push.txt create mode 100644 src/tools/descriptions/git/git_stash.txt create mode 100644 src/tools/descriptions/git/git_status.txt rename src/tools/descriptions/{ => shell}/bash.txt (100%) create mode 100644 src/tools/descriptions/todo/todo_read.txt create mode 100644 src/tools/descriptions/todo/todo_write.txt rename src/tools/descriptions/{ => web}/web_extract.txt (100%) rename src/tools/descriptions/{ => web}/web_search.txt (100%) create mode 100644 src/tools/git/git_add.ts create mode 100644 src/tools/git/git_branch.ts create mode 100644 src/tools/git/git_checkout.ts create mode 100644 src/tools/git/git_commit.ts create mode 100644 src/tools/git/git_diff.ts create mode 100644 src/tools/git/git_log.ts create mode 100644 src/tools/git/git_pull.ts create mode 100644 src/tools/git/git_push.ts create mode 100644 src/tools/git/git_stash.ts create mode 100644 src/tools/git/git_status.ts create mode 100644 src/tools/git/index.ts diff --git a/package.json b/package.json index eab2c17..9ed95ef 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/permission/checkers/git.ts b/src/permission/checkers/git.ts new file mode 100644 index 0000000..1212d17 --- /dev/null +++ b/src/permission/checkers/git.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; + private sessionPermissions = new Map(); + + constructor() { + this.config = { ...DEFAULT_CONFIG }; + } + + /** + * 设置权限询问回调 + */ + setAskCallback(callback: (ctx: PermissionContext) => Promise): 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 { + 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 { + // 从 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): void { + this.config = { ...this.config, ...config }; + } +} diff --git a/src/permission/checkers/index.ts b/src/permission/checkers/index.ts index 7c84cf5..773ebbe 100644 --- a/src/permission/checkers/index.ts +++ b/src/permission/checkers/index.ts @@ -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'; diff --git a/src/permission/manager.ts b/src/permission/manager.ts index 2acab17..508cee3 100644 --- a/src/permission/manager.ts +++ b/src/permission/manager.ts @@ -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 { + const gitChecker = this.getChecker('git'); + if (!gitChecker) { + return { + allowed: false, + action: 'ask', + needsConfirmation: true, + reason: 'Git 权限检查器未注册', + }; + } + return gitChecker.checkGitPermission(ctx); + } + /** * 清除所有检查器的会话权限 */ diff --git a/src/permission/types.ts b/src/permission/types.ts index 3773aa7..37d2cdf 100644 --- a/src/permission/types.ts +++ b/src/permission/types.ts @@ -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; +} diff --git a/src/tools/descriptions/copy_file.txt b/src/tools/descriptions/filesystem/copy_file.txt similarity index 100% rename from src/tools/descriptions/copy_file.txt rename to src/tools/descriptions/filesystem/copy_file.txt diff --git a/src/tools/descriptions/create_directory.txt b/src/tools/descriptions/filesystem/create_directory.txt similarity index 100% rename from src/tools/descriptions/create_directory.txt rename to src/tools/descriptions/filesystem/create_directory.txt diff --git a/src/tools/descriptions/delete_file.txt b/src/tools/descriptions/filesystem/delete_file.txt similarity index 100% rename from src/tools/descriptions/delete_file.txt rename to src/tools/descriptions/filesystem/delete_file.txt diff --git a/src/tools/descriptions/edit_file.txt b/src/tools/descriptions/filesystem/edit_file.txt similarity index 100% rename from src/tools/descriptions/edit_file.txt rename to src/tools/descriptions/filesystem/edit_file.txt diff --git a/src/tools/descriptions/get_file_info.txt b/src/tools/descriptions/filesystem/get_file_info.txt similarity index 100% rename from src/tools/descriptions/get_file_info.txt rename to src/tools/descriptions/filesystem/get_file_info.txt diff --git a/src/tools/descriptions/grep_content.txt b/src/tools/descriptions/filesystem/grep_content.txt similarity index 100% rename from src/tools/descriptions/grep_content.txt rename to src/tools/descriptions/filesystem/grep_content.txt diff --git a/src/tools/descriptions/list_directory.txt b/src/tools/descriptions/filesystem/list_directory.txt similarity index 100% rename from src/tools/descriptions/list_directory.txt rename to src/tools/descriptions/filesystem/list_directory.txt diff --git a/src/tools/descriptions/move_file.txt b/src/tools/descriptions/filesystem/move_file.txt similarity index 100% rename from src/tools/descriptions/move_file.txt rename to src/tools/descriptions/filesystem/move_file.txt diff --git a/src/tools/descriptions/read_file.txt b/src/tools/descriptions/filesystem/read_file.txt similarity index 100% rename from src/tools/descriptions/read_file.txt rename to src/tools/descriptions/filesystem/read_file.txt diff --git a/src/tools/descriptions/search_files.txt b/src/tools/descriptions/filesystem/search_files.txt similarity index 100% rename from src/tools/descriptions/search_files.txt rename to src/tools/descriptions/filesystem/search_files.txt diff --git a/src/tools/descriptions/write_file.txt b/src/tools/descriptions/filesystem/write_file.txt similarity index 100% rename from src/tools/descriptions/write_file.txt rename to src/tools/descriptions/filesystem/write_file.txt diff --git a/src/tools/descriptions/git/git_add.txt b/src/tools/descriptions/git/git_add.txt new file mode 100644 index 0000000..347ae71 --- /dev/null +++ b/src/tools/descriptions/git/git_add.txt @@ -0,0 +1,13 @@ +将文件变更添加到 Git 暂存区,准备提交。 + +参数说明: +- files: 要暂存的文件列表(数组或单个文件路径) +- all: 暂存所有变更(包括新文件和删除) +- update: 仅暂存已跟踪文件的变更(不包括新文件) + +使用场景: +- 选择性暂存特定文件 +- 暂存所有变更准备提交 +- 分批次提交不同功能的代码 + +执行后会显示当前暂存状态。 diff --git a/src/tools/descriptions/git/git_branch.txt b/src/tools/descriptions/git/git_branch.txt new file mode 100644 index 0000000..8980cf8 --- /dev/null +++ b/src/tools/descriptions/git/git_branch.txt @@ -0,0 +1,15 @@ +管理 Git 分支:列出、创建、删除、重命名分支。 + +参数说明: +- action: 操作类型 + - list: 列出分支(默认) + - create: 创建新分支 + - delete: 删除分支 + - rename: 重命名分支 +- name: 分支名称(create/delete/rename 时必填) +- new_name: 新分支名(rename 时必填) +- remote: 显示远程分支 +- all: 显示所有分支(本地 + 远程) +- force: 强制删除未合并的分支 + +注意:创建分支后需要使用 git_checkout 切换到新分支。 diff --git a/src/tools/descriptions/git/git_checkout.txt b/src/tools/descriptions/git/git_checkout.txt new file mode 100644 index 0000000..8fe8e48 --- /dev/null +++ b/src/tools/descriptions/git/git_checkout.txt @@ -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 暂存。 diff --git a/src/tools/descriptions/git/git_commit.txt b/src/tools/descriptions/git/git_commit.txt new file mode 100644 index 0000000..2173fe3 --- /dev/null +++ b/src/tools/descriptions/git/git_commit.txt @@ -0,0 +1,14 @@ +创建 Git 提交,保存暂存区的变更。 + +参数说明: +- message: 提交信息(必填,除非使用 amend + 无修改) +- amend: 修改上次提交(可以修改提交信息或追加变更) +- all: 自动暂存所有已跟踪文件的变更后提交 + +使用流程: +1. 使用 git_add 暂存要提交的文件 +2. 使用 git_commit 创建提交 + +提交信息建议: +- 简洁明了地描述本次变更 +- 使用祈使语气(如 "Add feature" 而非 "Added feature") diff --git a/src/tools/descriptions/git/git_diff.txt b/src/tools/descriptions/git/git_diff.txt new file mode 100644 index 0000000..e731ad2 --- /dev/null +++ b/src/tools/descriptions/git/git_diff.txt @@ -0,0 +1,12 @@ +查看 Git 变更的详细差异,包括文件内容的具体修改。 + +参数说明: +- path: 指定要查看差异的文件或目录路径 +- staged: 查看已暂存的变更(默认查看未暂存的变更) +- commit: 与指定提交对比(如 HEAD~1, commit-hash) +- stat: 仅显示统计信息(修改了多少行) + +使用场景: +- 查看修改了哪些内容 +- 提交前检查变更 +- 对比不同版本的代码 diff --git a/src/tools/descriptions/git/git_log.txt b/src/tools/descriptions/git/git_log.txt new file mode 100644 index 0000000..4152077 --- /dev/null +++ b/src/tools/descriptions/git/git_log.txt @@ -0,0 +1,14 @@ +查看 Git 提交历史记录。 + +参数说明: +- limit: 显示的提交数量(默认 10) +- oneline: 每个提交单行显示 +- file: 查看指定文件的提交历史 +- author: 筛选指定作者的提交 +- since: 筛选指定日期之后的提交(如 "2024-01-01" 或 "1 week ago") +- graph: 显示分支合并图 + +返回内容: +- 提交 hash(简短) +- 提交信息 +- 作者和时间 diff --git a/src/tools/descriptions/git/git_pull.txt b/src/tools/descriptions/git/git_pull.txt new file mode 100644 index 0000000..25f2063 --- /dev/null +++ b/src/tools/descriptions/git/git_pull.txt @@ -0,0 +1,14 @@ +从远程仓库拉取更新并合并到本地分支。 + +参数说明: +- remote: 远程仓库名(默认 origin) +- branch: 要拉取的分支名(默认当前分支对应的远程分支) +- rebase: 使用 rebase 而非 merge(保持提交历史线性) + +常见场景: +- 更新当前分支: git_pull +- 使用 rebase 更新: git_pull with rebase: true + +注意事项: +- 如果有本地未提交的变更,可能需要先 git_stash 暂存 +- 如果发生冲突,需要手动解决后提交 diff --git a/src/tools/descriptions/git/git_push.txt b/src/tools/descriptions/git/git_push.txt new file mode 100644 index 0000000..e18354b --- /dev/null +++ b/src/tools/descriptions/git/git_push.txt @@ -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 拉取远程更新。 diff --git a/src/tools/descriptions/git/git_stash.txt b/src/tools/descriptions/git/git_stash.txt new file mode 100644 index 0000000..6b02159 --- /dev/null +++ b/src/tools/descriptions/git/git_stash.txt @@ -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" 恢复工作 diff --git a/src/tools/descriptions/git/git_status.txt b/src/tools/descriptions/git/git_status.txt new file mode 100644 index 0000000..5b54344 --- /dev/null +++ b/src/tools/descriptions/git/git_status.txt @@ -0,0 +1,14 @@ +查看 Git 仓库的当前状态,包括已修改、已暂存和未跟踪的文件。 + +这是检查仓库状态的首选工具,可以快速了解当前工作区的变更情况。 + +参数说明: +- short: 简短输出格式(类似 git status -s) +- branch: 显示当前分支信息(默认 true) + +返回内容: +- 当前分支名 +- 与远程分支的差异(领先/落后提交数) +- 已暂存的变更 +- 未暂存的变更 +- 未跟踪的文件 diff --git a/src/tools/descriptions/bash.txt b/src/tools/descriptions/shell/bash.txt similarity index 100% rename from src/tools/descriptions/bash.txt rename to src/tools/descriptions/shell/bash.txt diff --git a/src/tools/descriptions/todo/todo_read.txt b/src/tools/descriptions/todo/todo_read.txt new file mode 100644 index 0000000..71f9154 --- /dev/null +++ b/src/tools/descriptions/todo/todo_read.txt @@ -0,0 +1,8 @@ +读取当前会话的待办事项列表。 + +返回所有待办事项及其状态: +- pending: 待处理 +- in_progress: 进行中 +- completed: 已完成 + +用于查看任务进度和剩余工作。 diff --git a/src/tools/descriptions/todo/todo_write.txt b/src/tools/descriptions/todo/todo_write.txt new file mode 100644 index 0000000..e8a5e66 --- /dev/null +++ b/src/tools/descriptions/todo/todo_write.txt @@ -0,0 +1,13 @@ +管理会话的待办事项列表。 + +参数说明: +- todos: 完整的待办事项列表,每项包含: + - content: 任务内容 + - status: 状态(pending/in_progress/completed) + +使用场景: +- 规划复杂任务的步骤 +- 跟踪工作进度 +- 标记已完成的任务 + +注意:每次调用会替换整个列表,请确保包含所有需要保留的项目。 diff --git a/src/tools/descriptions/web_extract.txt b/src/tools/descriptions/web/web_extract.txt similarity index 100% rename from src/tools/descriptions/web_extract.txt rename to src/tools/descriptions/web/web_extract.txt diff --git a/src/tools/descriptions/web_search.txt b/src/tools/descriptions/web/web_search.txt similarity index 100% rename from src/tools/descriptions/web_search.txt rename to src/tools/descriptions/web/web_search.txt diff --git a/src/tools/git/git_add.ts b/src/tools/git/git_add.ts new file mode 100644 index 0000000..dda29b3 --- /dev/null +++ b/src/tools/git/git_add.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_branch.ts b/src/tools/git/git_branch.ts new file mode 100644 index 0000000..dcb20a1 --- /dev/null +++ b/src/tools/git/git_branch.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_checkout.ts b/src/tools/git/git_checkout.ts new file mode 100644 index 0000000..d2c03e9 --- /dev/null +++ b/src/tools/git/git_checkout.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_commit.ts b/src/tools/git/git_commit.ts new file mode 100644 index 0000000..e58611e --- /dev/null +++ b/src/tools/git/git_commit.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_diff.ts b/src/tools/git/git_diff.ts new file mode 100644 index 0000000..f3cf39e --- /dev/null +++ b/src/tools/git/git_diff.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_log.ts b/src/tools/git/git_log.ts new file mode 100644 index 0000000..a7c07a5 --- /dev/null +++ b/src/tools/git/git_log.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_pull.ts b/src/tools/git/git_pull.ts new file mode 100644 index 0000000..d7b2445 --- /dev/null +++ b/src/tools/git/git_pull.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_push.ts b/src/tools/git/git_push.ts new file mode 100644 index 0000000..1549a88 --- /dev/null +++ b/src/tools/git/git_push.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_stash.ts b/src/tools/git/git_stash.ts new file mode 100644 index 0000000..02e5ce3 --- /dev/null +++ b/src/tools/git/git_stash.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/git_status.ts b/src/tools/git/git_status.ts new file mode 100644 index 0000000..c60337c --- /dev/null +++ b/src/tools/git/git_status.ts @@ -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): Promise => { + 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, + }; + } + }, +}; diff --git a/src/tools/git/index.ts b/src/tools/git/index.ts new file mode 100644 index 0000000..2e3b9c4 --- /dev/null +++ b/src/tools/git/index.ts @@ -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'; diff --git a/src/tools/index.ts b/src/tools/index.ts index 0a6f9f0..e27f166 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -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 diff --git a/src/tools/load_description.ts b/src/tools/load_description.ts index 3cd7eed..28bf6ff 100644 --- a/src/tools/load_description.ts +++ b/src/tools/load_description.ts @@ -5,8 +5,47 @@ import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); +// 工具名到子目录的映射 +const TOOL_CATEGORY_MAP: Record = { + // 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 {