Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/claude/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ function buildWorkspaceSystemPrompt(): string {
- mode="writable": 需要修改代码、提交变更时使用。会创建隔离工作区和 feature 分支。

**如何使用:**
- 远程仓库: 使用 repo_url 参数传入仓库 URL
- 本地仓库: 使用 local_path 参数传入仓库绝对路径
- **优先使用 repo_url**: 当用户提供了仓库 URL(或你能从上下文推断出 URL),始终用 repo_url 参数。这会自动走缓存 + 隔离工作区流程
- local_path 仅用于项目目录(\`${projectsDir}\`)下已有的仓库,且路径必须在该目录范围内
- 根据意图选择 mode (readonly 或 writable)
- 可选指定 source_branch (源分支) 和 feature_branch (自定义分支名, 仅 writable)

**无需使用的场景:**
- 当前工作目录已经是目标仓库
- 用户只是查看/分析代码,且仓库已在项目目录中(直接 cd 或用绝对路径读取即可)
- 注意:如果用户要**修改**项目目录中已有的仓库代码,应使用 repo_url 创建隔离工作区,不要直接在原始仓库上改

**重要:调用 setup_workspace 后,系统将自动重启以加载项目配置(CLAUDE.md 等)。
请在调用后仅输出简短确认(如"工作区已就绪,正在重新加载项目配置..."),不要继续执行后续任务。**
Expand Down Expand Up @@ -184,7 +185,8 @@ export class ClaudeExecutor {
permissionMode: 'acceptEdits',
canUseTool: async (toolName: string, inputObj: Record<string, unknown>) => {
logger.info({ toolName, inputKeys: Object.keys(inputObj) }, 'canUseTool called — auto allowing');
return { behavior: 'allow' as const };
// updatedInput 必须显式传回,否则 SDK 内部 Zod 校验会因 undefined 报错
return { behavior: 'allow' as const, updatedInput: inputObj };
},

// 预算和限制
Expand Down
18 changes: 17 additions & 1 deletion src/workspace/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ export interface SetupWorkspaceResult {
const SAFE_BRANCH_RE = /^[a-zA-Z0-9._\/-]+$/;
/** git 远程 URL 协议前缀 */
const GIT_URL_RE = /^(https?:\/\/|git@|ssh:\/\/|git:\/\/)/;
/** SSH 简写格式: github.com:user/repo.git (缺少 git@ 前缀) */
const SSH_SHORTHAND_RE = /^[\w.-]+\.\w{2,}:[\w./-]+$/;

/**
* 归一化仓库 URL
* - SSH 简写 (github.com:user/repo) → git@github.com:user/repo
* - 其他格式原样返回
*/
function normalizeRepoUrl(url: string): string {
if (!GIT_URL_RE.test(url) && SSH_SHORTHAND_RE.test(url)) {
return `git@${url}`;
}
return url;
}


/**
Expand All @@ -65,7 +79,9 @@ export function deriveRepoName(source: string): string {
* 直接从本地路径 clone (不经过缓存层)
*/
export function setupWorkspace(options: SetupWorkspaceOptions): SetupWorkspaceResult {
const { repoUrl, localPath, mode = 'writable', sourceBranch, featureBranch } = options;
const { localPath, mode = 'writable', sourceBranch, featureBranch } = options;
// 归一化 URL: github.com:user/repo → git@github.com:user/repo
const repoUrl = options.repoUrl ? normalizeRepoUrl(options.repoUrl) : undefined;

const source = repoUrl || localPath;
if (!source) {
Expand Down
1 change: 1 addition & 0 deletions src/workspace/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function createWorkspaceMcpServer(onWorkspaceChanged?: SessionUpdater) {
feature_branch: z.string().optional().describe('自定义 feature 分支名 (默认自动生成, 仅 writable 模式)'),
},
async (args) => {
logger.info({ args: { repo_url: args.repo_url, local_path: args.local_path, mode: args.mode } }, 'setup_workspace tool invoked');
try {
const result = setupWorkspace({
repoUrl: args.repo_url,
Expand Down