opencode-cursor-agent/internal/server/cwd_extract.go

103 lines
3.1 KiB
Go
Raw Normal View History

2026-04-25 13:18:22 +00:00
package server
import (
"os"
"path/filepath"
"regexp"
"strings"
"github.com/daniel/cursor-adapter/internal/types"
)
// cwdPatterns matches the most common ways callers (Claude Code, opencode,
// Cursor CLI itself, custom clients) advertise their host working
// directory inside the prompt.
//
// Patterns must capture an absolute path in group 1.
var cwdPatterns = []*regexp.Regexp{
// Claude Code style:
// <env>
// Working directory: /Users/x/proj
// Is directory a git repo: Yes
// ...
// </env>
regexp.MustCompile(`(?si)<env>.*?working directory:\s*(\S+)`),
// Generic <cwd>...</cwd> wrapper.
regexp.MustCompile(`(?i)<cwd>\s*([^<\s][^<]*?)\s*</cwd>`),
// "Working directory: /abs/path" on its own line.
regexp.MustCompile(`(?im)^\s*working directory:\s*(/[^\s<>]+)\s*$`),
// "Current working directory is /abs/path" / "current working directory: /abs/path"
regexp.MustCompile(`(?i)current working directory(?: is)?[:\s]+(/[^\s<>]+)`),
// Loose "cwd: /abs/path" / "cwd=/abs/path".
regexp.MustCompile(`(?i)\bcwd\s*[:=]\s*(/[^\s<>]+)`),
}
// detectCallerWorkspace returns the first absolute, host-resident directory
// it can extract from corpus. It rejects:
// - non-absolute paths (e.g. "src/")
// - paths that don't exist on the host (e.g. "/sessions/..." sandbox
// paths sent by Claude Desktop's Cowork VM)
// - paths that point to a file rather than a directory
//
// Returning "" simply means "no usable workspace hint found", and callers
// should fall back to config defaults.
func detectCallerWorkspace(corpus string) string {
for _, p := range cwdPatterns {
m := p.FindStringSubmatch(corpus)
if len(m) < 2 {
continue
}
cand := strings.TrimSpace(m[1])
// Strip trailing punctuation that often follows a path in prose.
cand = strings.TrimRight(cand, `.,;:"'`+"`)>")
if cand == "" || !filepath.IsAbs(cand) {
continue
}
info, err := os.Stat(cand)
if err != nil || !info.IsDir() {
continue
}
return cand
}
return ""
}
// detectAnthropicCwd scans an Anthropic Messages request for a workspace
// hint. It walks system blocks first (Claude Code / opencode usually put
// the <env> block there), then user/assistant text blocks (some clients
// embed it as <system-reminder> inside the first user message).
func detectAnthropicCwd(req types.AnthropicMessagesRequest) string {
var sb strings.Builder
for _, b := range req.System {
if b.Type == "text" && b.Text != "" {
sb.WriteString(b.Text)
sb.WriteByte('\n')
}
}
for _, m := range req.Messages {
for _, b := range m.Content {
if b.Type == "text" && b.Text != "" {
sb.WriteString(b.Text)
sb.WriteByte('\n')
}
}
}
return detectCallerWorkspace(sb.String())
}
// detectOpenAICwd scans an OpenAI-style chat completion request for a
// workspace hint, including system messages (which the brain prompt
// builder otherwise drops).
func detectOpenAICwd(req types.ChatCompletionRequest) string {
var sb strings.Builder
for _, m := range req.Messages {
sb.WriteString(string(m.Content))
sb.WriteByte('\n')
}
return detectCallerWorkspace(sb.String())
}