103 lines
3.1 KiB
Go
103 lines
3.1 KiB
Go
|
|
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())
|
||
|
|
}
|