Skip to content

Commit 012a60c

Browse files
committed
fix vendored tool extraction and bash output formatting
1 parent 948ded1 commit 012a60c

6 files changed

Lines changed: 98 additions & 25 deletions

File tree

docs/en/changelog.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# Changelog
22

3+
## v0.1.15
4+
5+
### 🐛 Bug Fixes
6+
7+
- **Vendored Search Tool Availability**
8+
- Fixed `grep` and `find` so they prepare embedded `rg` / `fd` binaries on demand instead of failing when vendored tools have not been extracted yet
9+
- Restored executable permissions for already-extracted vendored binaries to avoid `permission denied` failures on reuse
10+
11+
- **Bash Tool Result Handling**
12+
- Fixed bash tool responses to report stdout, stderr, working directory, and exit code in a stable structured format
13+
- Preserved non-zero command exits as normal tool results with explicit `exit_code` output instead of mixing shell failures into transport-level errors
14+
- Standardized empty stdout/stderr rendering as `(no output)` for more predictable downstream handling
15+
16+
---
17+
318
## v0.1.14
419

520
### 🐛 Bug Fixes

docs/zh/changelog.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
# 更新日志
22

3+
## v0.1.15
4+
5+
### 🐛 问题修复
6+
7+
- **内嵌搜索工具可用性**
8+
- 修复 `grep``find`:当内嵌的 `rg` / `fd` 尚未释放到本地时,会按需准备二进制文件,而不是直接失败
9+
- 为已释放的内嵌二进制补齐可执行权限,避免复用时出现 `permission denied` 错误
10+
11+
- **Bash 工具结果处理**
12+
- 修复 bash 工具返回内容,稳定输出 stdout、stderr、工作目录和退出码等结构化信息
13+
- 将命令非零退出保留为正常工具结果,并通过明确的 `exit_code` 字段表达,而不是混入传输级错误
14+
- 统一将空 stdout/stderr 渲染为 `(no output)`,便于下游稳定处理
15+
16+
---
17+
318
## v0.1.14
419

520
### 🐛 问题修复

internal/tools/bash.go

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -233,24 +233,33 @@ func (t *BashTool) Execute(ctx context.Context, params map[string]any) (ToolResu
233233

234234
err := cmd.Run()
235235

236-
output := stdout.String()
237-
if stderr.Len() > 0 {
238-
if output != "" {
239-
output += "\n"
236+
stdoutStr := strings.TrimRight(stdout.String(), "\n")
237+
stderrStr := strings.TrimRight(stderr.String(), "\n")
238+
if stdoutStr == "" {
239+
stdoutStr = "(no output)"
240+
}
241+
if stderrStr == "" {
242+
stderrStr = "(no output)"
243+
}
244+
245+
exitCode := 0
246+
if err != nil {
247+
if exitErr, ok := err.(*exec.ExitError); ok {
248+
exitCode = exitErr.ExitCode()
240249
}
241-
output += "STDERR:\n" + stderr.String()
242250
}
243251

244-
// Build result with command info
245252
var result strings.Builder
246-
result.WriteString(fmt.Sprintf("$ %s\n", command))
247-
result.WriteString(fmt.Sprintf("(in %s)\n\n", workDir))
248-
249-
if output == "" {
250-
result.WriteString("(no output)")
251-
} else {
252-
result.WriteString(output)
253-
}
253+
result.WriteString("[command]\n")
254+
result.WriteString(command)
255+
result.WriteString("\n[cwd]\n")
256+
result.WriteString(workDir)
257+
result.WriteString("\n[stdout]\n")
258+
result.WriteString(stdoutStr)
259+
result.WriteString("\n[stderr]\n")
260+
result.WriteString(stderrStr)
261+
result.WriteString("\n[exit_code]\n")
262+
result.WriteString(fmt.Sprintf("%d", exitCode))
254263

255264
// Truncate large outputs
256265
const maxOutput = 50000
@@ -266,8 +275,8 @@ func (t *BashTool) Execute(ctx context.Context, params map[string]any) (ToolResu
266275
if errors.Is(err, exec.ErrWaitDelay) {
267276
return NewTextToolResult(resultStr), nil
268277
}
269-
if exitErr, ok := err.(*exec.ExitError); ok {
270-
return NewTextToolResult(fmt.Sprintf("%s\nExit code: %d", resultStr, exitErr.ExitCode())), nil
278+
if _, ok := err.(*exec.ExitError); ok {
279+
return NewTextToolResult(resultStr), nil
271280
}
272281
return ToolResult{}, fmt.Errorf("command failed: %w\n%s", err, resultStr)
273282
}

internal/tools/find.go

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,10 @@ func (t *FindTool) Execute(ctx context.Context, params map[string]any) (ToolResu
122122
maxResults = int(v)
123123
}
124124

125-
// 获取 fd 路径
126-
fdPath := vendored.FdPath()
127-
if fdPath == "" {
128-
return ToolResult{}, fmt.Errorf("fd 未安装,请先运行 make prepare-vendored")
125+
// 选择可用的 fd 命令(优先 vendored,其次系统 fd/fdfind)
126+
fdPath, err := resolveFdPath()
127+
if err != nil {
128+
return ToolResult{}, err
129129
}
130130

131131
// 将 glob 模式转为正则
@@ -154,7 +154,7 @@ func (t *FindTool) Execute(ctx context.Context, params map[string]any) (ToolResu
154154
cmd.Stdout = &stdout
155155
cmd.Stderr = &stderr
156156

157-
err := cmd.Run()
157+
err = cmd.Run()
158158
if err != nil {
159159
// fd 返回 1 表示没有匹配
160160
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
@@ -176,3 +176,17 @@ func (t *FindTool) Execute(ctx context.Context, params map[string]any) (ToolResu
176176
// fd 输出就是每行一个路径,与原实现格式一致
177177
return NewTextToolResult(output), nil
178178
}
179+
180+
func resolveFdPath() (string, error) {
181+
fdPath := vendored.FdPath()
182+
if fdPath == "" {
183+
return "", fmt.Errorf("无法确定 fd 路径")
184+
}
185+
186+
// 缺失或不可执行时,尝试从 go:embed 释放到 ~/.vibecoding/bin/
187+
if err := vendored.Ensure(); err != nil {
188+
return "", fmt.Errorf("准备 fd 失败: %w", err)
189+
}
190+
191+
return fdPath, nil
192+
}

internal/tools/grep.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,9 @@ func (t *GrepTool) Execute(ctx context.Context, params map[string]any) (ToolResu
8282
}
8383

8484
// 获取 rg 路径
85-
rgPath := vendored.RgPath()
86-
if rgPath == "" {
87-
return ToolResult{}, fmt.Errorf("ripgrep (rg) 未安装,请先运行 make prepare-vendored")
85+
rgPath, err := resolveRgPath()
86+
if err != nil {
87+
return ToolResult{}, err
8888
}
8989

9090
// 构建 rg 命令参数
@@ -107,7 +107,7 @@ func (t *GrepTool) Execute(ctx context.Context, params map[string]any) (ToolResu
107107
cmd.Stdout = &stdout
108108
cmd.Stderr = &stderr
109109

110-
err := cmd.Run()
110+
err = cmd.Run()
111111
if err != nil {
112112
// rg 返回 1 表示没有匹配,这不是错误
113113
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
@@ -130,3 +130,17 @@ func (t *GrepTool) Execute(ctx context.Context, params map[string]any) (ToolResu
130130
// 与原实现格式一致: file:line: content
131131
return NewTextToolResult(output), nil
132132
}
133+
134+
func resolveRgPath() (string, error) {
135+
rgPath := vendored.RgPath()
136+
if rgPath == "" {
137+
return "", fmt.Errorf("无法确定 rg 路径")
138+
}
139+
140+
// 缺失或不可执行时,尝试从 go:embed 释放到 ~/.vibecoding/bin/
141+
if err := vendored.Ensure(); err != nil {
142+
return "", fmt.Errorf("准备 rg 失败: %w", err)
143+
}
144+
145+
return rgPath, nil
146+
}

internal/vendored/vendored.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ func extractBinary(dest string, data []byte) error {
8787
// 检查是否已存在
8888
if info, err := os.Stat(dest); err == nil {
8989
if info.Size() == int64(len(data)) {
90+
// 确保已有文件可执行,避免 fork/exec permission denied。
91+
if info.Mode()&0o111 == 0 {
92+
if chmodErr := os.Chmod(dest, 0o755); chmodErr != nil {
93+
return fmt.Errorf("设置 %s 可执行权限失败: %w", dest, chmodErr)
94+
}
95+
}
9096
return nil // 已存在且大小一致,跳过
9197
}
9298
}

0 commit comments

Comments
 (0)