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: // // Working directory: /Users/x/proj // Is directory a git repo: Yes // ... // regexp.MustCompile(`(?si).*?working directory:\s*(\S+)`), // Generic ... wrapper. regexp.MustCompile(`(?i)\s*([^<\s][^<]*?)\s*`), // "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 block there), then user/assistant text blocks (some clients // embed it as 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()) }