refactor: remove hardcoded dir skip list from project dir extraction
Replaced assumption-based logic (skipping "projects", "repos", "src") with dynamic extraction: exec uses basename of cd target, file tools use the first path component after workspace. No conventions assumed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6297d3679e
commit
e6e8482613
2 changed files with 53 additions and 61 deletions
|
|
@ -956,49 +956,10 @@ var cdPrefixPattern = regexp.MustCompile(`^cd\s+(\S+)\s*&&\s*`)
|
||||||
// argument (e.g. "-A 20") are kept because removing them would lose context.
|
// argument (e.g. "-A 20") are kept because removing them would lose context.
|
||||||
var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
|
var optFlagPattern = regexp.MustCompile(`\s+--?\w[\w-]*(=\S*)?`)
|
||||||
|
|
||||||
// projectDirFromPath extracts the project directory name from an absolute path
|
// extractProjectDir extracts a working directory name from a tool call.
|
||||||
// by stripping the workspace prefix and returning the first meaningful component.
|
// For exec: uses the basename of the cd target (the directory AI actually works in).
|
||||||
// Generic directory names like "projects" or "repos" are skipped.
|
// For file tools: strips the workspace prefix and takes the first path component.
|
||||||
// Returns "" if the path doesn't extend beyond workspace.
|
// No assumptions are made about directory naming conventions.
|
||||||
func projectDirFromPath(absPath, workspace string) string {
|
|
||||||
absPath = strings.TrimRight(absPath, "/\\")
|
|
||||||
if absPath == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if workspace == "" {
|
|
||||||
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
|
|
||||||
return absPath[idx+1:]
|
|
||||||
}
|
|
||||||
return absPath
|
|
||||||
}
|
|
||||||
ws := strings.TrimRight(workspace, "/\\")
|
|
||||||
rest := strings.TrimPrefix(absPath, ws)
|
|
||||||
if rest == absPath {
|
|
||||||
// workspace not a prefix — fall back to last component
|
|
||||||
if idx := strings.LastIndex(absPath, "/"); idx >= 0 {
|
|
||||||
return absPath[idx+1:]
|
|
||||||
}
|
|
||||||
return absPath
|
|
||||||
}
|
|
||||||
rest = strings.TrimLeft(rest, "/\\")
|
|
||||||
if rest == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
// Take first path component, skipping generic directory names
|
|
||||||
parts := strings.SplitN(rest, "/", 3)
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
first := strings.ToLower(parts[0])
|
|
||||||
if first == "projects" || first == "repos" || first == "src" {
|
|
||||||
return parts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractProjectDir extracts the project directory name from a tool call.
|
|
||||||
// For exec: parses the "cd <path> && ..." prefix.
|
|
||||||
// For file tools: uses the file path directly.
|
|
||||||
// Returns "" if no project directory can be determined.
|
|
||||||
func extractProjectDir(toolName string, args map[string]interface{}, workspace string) string {
|
func extractProjectDir(toolName string, args map[string]interface{}, workspace string) string {
|
||||||
switch toolName {
|
switch toolName {
|
||||||
case "exec":
|
case "exec":
|
||||||
|
|
@ -1010,14 +971,38 @@ func extractProjectDir(toolName string, args map[string]interface{}, workspace s
|
||||||
if len(m) < 2 {
|
if len(m) < 2 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return projectDirFromPath(m[1], workspace)
|
// basename of cd target — the directory the AI actually cd's into
|
||||||
|
cdPath := strings.TrimRight(m[1], "/\\")
|
||||||
|
if idx := strings.LastIndex(cdPath, "/"); idx >= 0 {
|
||||||
|
return cdPath[idx+1:]
|
||||||
|
}
|
||||||
|
if idx := strings.LastIndex(cdPath, "\\"); idx >= 0 {
|
||||||
|
return cdPath[idx+1:]
|
||||||
|
}
|
||||||
|
return cdPath
|
||||||
|
|
||||||
case "read_file", "write_file", "edit_file", "append_file", "list_dir":
|
case "read_file", "write_file", "edit_file", "append_file", "list_dir":
|
||||||
path, _ := args["path"].(string)
|
path, _ := args["path"].(string)
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return projectDirFromPath(path, workspace)
|
ws := strings.TrimRight(workspace, "/\\")
|
||||||
|
if ws == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(path, ws)
|
||||||
|
if rest == path {
|
||||||
|
return "" // path not under workspace
|
||||||
|
}
|
||||||
|
rest = strings.TrimLeft(rest, "/\\")
|
||||||
|
if rest == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// first component after workspace
|
||||||
|
if idx := strings.IndexAny(rest, "/\\"); idx >= 0 {
|
||||||
|
return rest[:idx]
|
||||||
|
}
|
||||||
|
return rest
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1612,34 +1612,34 @@ func TestExtractProjectDir(t *testing.T) {
|
||||||
workspace string
|
workspace string
|
||||||
want string
|
want string
|
||||||
}{
|
}{
|
||||||
// exec: cd prefix
|
// exec: basename of cd target
|
||||||
{
|
{
|
||||||
name: "exec cd into projects subdir",
|
name: "exec cd deep path",
|
||||||
toolName: "exec",
|
toolName: "exec",
|
||||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest"},
|
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && pytest"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
want: "terra-py-form",
|
want: "terra-py-form",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "exec cd with trailing slash workspace",
|
name: "exec cd direct subdir",
|
||||||
toolName: "exec",
|
|
||||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/projects/terra-py-form && ls"},
|
|
||||||
workspace: ws + "/",
|
|
||||||
want: "terra-py-form",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "exec cd into direct subdir",
|
|
||||||
toolName: "exec",
|
toolName: "exec",
|
||||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app && make build"},
|
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app && make build"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
want: "my-app",
|
want: "my-app",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "exec cd trailing slash target",
|
||||||
|
toolName: "exec",
|
||||||
|
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace/my-app/ && ls"},
|
||||||
|
workspace: ws,
|
||||||
|
want: "my-app",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "exec cd to workspace itself",
|
name: "exec cd to workspace itself",
|
||||||
toolName: "exec",
|
toolName: "exec",
|
||||||
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace && ls"},
|
args: map[string]interface{}{"command": "cd /home/user/.picoclaw/workspace && ls"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
want: "",
|
want: "workspace",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "exec no cd prefix",
|
name: "exec no cd prefix",
|
||||||
|
|
@ -1648,16 +1648,16 @@ func TestExtractProjectDir(t *testing.T) {
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
// file tools
|
// file tools: first component after workspace
|
||||||
{
|
{
|
||||||
name: "read_file in projects subdir",
|
name: "read_file first component is projects",
|
||||||
toolName: "read_file",
|
toolName: "read_file",
|
||||||
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/projects/terra-py-form/src/main.py"},
|
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/projects/terra-py-form/src/main.py"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
want: "terra-py-form",
|
want: "projects",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "edit_file in direct subdir",
|
name: "edit_file direct subdir",
|
||||||
toolName: "edit_file",
|
toolName: "edit_file",
|
||||||
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/my-app/README.md"},
|
args: map[string]interface{}{"path": "/home/user/.picoclaw/workspace/my-app/README.md"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
|
|
@ -1671,7 +1671,14 @@ func TestExtractProjectDir(t *testing.T) {
|
||||||
want: "notes.txt",
|
want: "notes.txt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unknown tool returns empty",
|
name: "file outside workspace",
|
||||||
|
toolName: "read_file",
|
||||||
|
args: map[string]interface{}{"path": "/tmp/foo.txt"},
|
||||||
|
workspace: ws,
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "unknown tool",
|
||||||
toolName: "web_search",
|
toolName: "web_search",
|
||||||
args: map[string]interface{}{"query": "test"},
|
args: map[string]interface{}{"query": "test"},
|
||||||
workspace: ws,
|
workspace: ws,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue