on main help sign in
feat: configure context paths (#86)
commit8f3a94df92ce783f8957aeb66b08801be736adfb authorGarrett Ladley <92384606+garrettladley@users.noreply.github.com> dateSun 27 Apr 2025, 14:11 (2 years ago) parent4415220
4 files changed+175−92
cmd/schema/main.go+21−1 internal/config/config.go+24−8 internal/llm/prompt/prompt.go+70−43 opencode-schema.json+60−40
cmd/schema/main.go+21−1
@@ -77,6 +77,27 @@ func generateSchema() map[string]any {
7777 "default": false,
7878 }
7979
80+ schema["properties"].(map[string]any)["contextPaths"] = map[string]any{
81+ "type": "array",
82+ "description": "Context paths for the application",
83+ "items": map[string]any{
84+ "type": "string",
85+ },
86+ "default": []string{
87+ ".github/copilot-instructions.md",
88+ ".cursorrules",
89+ ".cursor/rules/",
90+ "CLAUDE.md",
91+ "CLAUDE.local.md",
92+ "opencode.md",
93+ "opencode.local.md",
94+ "OpenCode.md",
95+ "OpenCode.local.md",
96+ "OPENCODE.md",
97+ "OPENCODE.local.md",
98+ },
99+ }
100+
80101 // Add MCP servers
81102 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
82103 "type": "object",
@@ -259,4 +280,3 @@ func generateSchema() map[string]any {
259280
260281 return schema
261282}
262−
internal/config/config.go+24−8
@@ -67,14 +67,15 @@ type LSPConfig struct {
6767
6868// Config is the main configuration structure for the application.
6969type Config struct {
70− Data Data `json:"data"`
71− WorkingDir string `json:"wd,omitempty"`
72− MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
73− Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
74− LSP map[string]LSPConfig `json:"lsp,omitempty"`
75− Agents map[AgentName]Agent `json:"agents"`
76− Debug bool `json:"debug,omitempty"`
77− DebugLSP bool `json:"debugLSP,omitempty"`
70+ Data Data `json:"data"`
71+ WorkingDir string `json:"wd,omitempty"`
72+ MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
73+ Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
74+ LSP map[string]LSPConfig `json:"lsp,omitempty"`
75+ Agents map[AgentName]Agent `json:"agents"`
76+ Debug bool `json:"debug,omitempty"`
77+ DebugLSP bool `json:"debugLSP,omitempty"`
78+ ContextPaths []string `json:"contextPaths,omitempty"`
7879}
7980
8081// Application constants
@@ -84,6 +85,20 @@ const (
8485 appName = "opencode"
8586)
8687
88+var defaultContextPaths = []string{
89+ ".github/copilot-instructions.md",
90+ ".cursorrules",
91+ ".cursor/rules/",
92+ "CLAUDE.md",
93+ "CLAUDE.local.md",
94+ "opencode.md",
95+ "opencode.local.md",
96+ "OpenCode.md",
97+ "OpenCode.local.md",
98+ "OPENCODE.md",
99+ "OPENCODE.local.md",
100+}
101+
87102// Global configuration instance
88103var cfg *Config
89104
@@ -185,6 +200,7 @@ func configureViper() {
185200// setDefaults configures default values for configuration options.
186201func setDefaults(debug bool) {
187202 viper.SetDefault("data.directory", defaultDataDirectory)
203+ viper.SetDefault("contextPaths", defaultContextPaths)
188204
189205 if debug {
190206 viper.SetDefault("debug", true)
internal/llm/prompt/prompt.go+70−43
@@ -5,26 +5,12 @@ import (
55 "os"
66 "path/filepath"
77 "strings"
8+ "sync"
89
910 "github.com/opencode-ai/opencode/internal/config"
1011 "github.com/opencode-ai/opencode/internal/llm/models"
1112)
1213
13−// contextFiles is a list of potential context files to check for
14−var contextFiles = []string{
15− ".github/copilot-instructions.md",
16− ".cursorrules",
17− ".cursor/rules/", // Directory containing multiple rule files
18− "CLAUDE.md",
19− "CLAUDE.local.md",
20− "opencode.md",
21− "opencode.local.md",
22− "OpenCode.md",
23− "OpenCode.local.md",
24− "OPENCODE.md",
25− "OPENCODE.local.md",
26−}
27−
2814func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
2915 basePrompt := ""
3016 switch agentName {
@@ -40,45 +26,86 @@ func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) s
4026
4127 if agentName == config.AgentCoder || agentName == config.AgentTask {
4228 // Add context from project-specific instruction files if they exist
43− contextContent := getContextFromFiles()
29+ contextContent := getContextFromPaths()
4430 if contextContent != "" {
45− return fmt.Sprintf("%s\n\n# Project-Specific Context\n%s", basePrompt, contextContent)
31+ return fmt.Sprintf("%s\n\n# Project-Specific Context\n Make sure to follow the instructions in the context below\n%s", basePrompt, contextContent)
4632 }
4733 }
4834 return basePrompt
4935}
5036
51−// getContextFromFiles checks for the existence of context files and returns their content
52−func getContextFromFiles() string {
53− workDir := config.WorkingDirectory()
54− var contextContent string
37+var (
38+ onceContext sync.Once
39+ contextContent string
40+)
41+
42+func getContextFromPaths() string {
43+ onceContext.Do(func() {
44+ var (
45+ cfg = config.Get()
46+ workDir = cfg.WorkingDir
47+ contextPaths = cfg.ContextPaths
48+ )
49+
50+ contextContent = processContextPaths(workDir, contextPaths)
51+ })
52+
53+ return contextContent
54+}
55+
56+func processContextPaths(workDir string, paths []string) string {
57+ var (
58+ wg sync.WaitGroup
59+ resultCh = make(chan string)
60+ )
61+
62+ for _, path := range paths {
63+ wg.Add(1)
64+ go func(p string) {
65+ defer wg.Done()
5566
56− for _, path := range contextFiles {
57− // Check if path ends with a slash (indicating a directory)
58− if strings.HasSuffix(path, "/") {
59− // Handle directory - read all files within it
60− dirPath := filepath.Join(workDir, path)
61− files, err := os.ReadDir(dirPath)
62− if err == nil {
63− for _, file := range files {
64− if !file.IsDir() {
65− filePath := filepath.Join(dirPath, file.Name())
66− content, err := os.ReadFile(filePath)
67− if err == nil {
68− contextContent += fmt.Sprintf("\n# From %s\n%s\n", file.Name(), string(content))
67+ if strings.HasSuffix(p, "/") {
68+ filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
69+ if err != nil {
70+ return err
71+ }
72+ if !d.IsDir() {
73+ if result := processFile(path); result != "" {
74+ resultCh <- result
6975 }
7076 }
77+ return nil
78+ })
79+ } else {
80+ result := processFile(filepath.Join(workDir, p))
81+ if result != "" {
82+ resultCh <- result
7183 }
7284 }
73− } else {
74− // Handle individual file as before
75− filePath := filepath.Join(workDir, path)
76− content, err := os.ReadFile(filePath)
77− if err == nil {
78− contextContent += fmt.Sprintf("\n%s\n", string(content))
79− }
80− }
85+ }(path)
8186 }
8287
83− return contextContent
88+ go func() {
89+ wg.Wait()
90+ close(resultCh)
91+ }()
92+
93+ var (
94+ results = make([]string, len(resultCh))
95+ i int
96+ )
97+ for result := range resultCh {
98+ results[i] = result
99+ i++
100+ }
101+
102+ return strings.Join(results, "\n")
84103}
104+
105+func processFile(filePath string) string {
106+ content, err := os.ReadFile(filePath)
107+ if err != nil {
108+ return ""
109+ }
110+ return "# From:" + filePath + "\n" + string(content)
111+}
opencode-schema.json+60−40
@@ -12,33 +12,33 @@
1212 "model": {
1313 "description": "Model ID for the agent",
1414 "enum": [
15+ "bedrock.claude-3.7-sonnet",
16+ "claude-3-haiku",
1517 "claude-3.7-sonnet",
16− "claude-3-opus",
17− "gpt-4.1-mini",
18− "gpt-4o",
19− "gpt-4o-mini",
20− "gemini-2.0-flash-lite",
21− "meta-llama/llama-4-maverick-17b-128e-instruct",
22− "gpt-4.1",
18+ "claude-3.5-haiku",
19+ "o3",
2320 "gpt-4.5-preview",
24− "o1",
25− "gpt-4.1-nano",
21+ "o1-pro",
22+ "o4-mini",
23+ "gpt-4.1",
2624 "o3-mini",
25+ "gpt-4.1-nano",
26+ "gpt-4o-mini",
27+ "o1",
2728 "gemini-2.5-flash",
28− "gemini-2.0-flash",
29− "meta-llama/llama-4-scout-17b-16e-instruct",
30− "bedrock.claude-3.7-sonnet",
31− "o1-pro",
32− "o3",
33− "gemini-2.5",
3429 "qwen-qwq",
35− "llama-3.3-70b-versatile",
30+ "meta-llama/llama-4-maverick-17b-128e-instruct",
31+ "claude-3-opus",
32+ "gpt-4o",
33+ "gemini-2.0-flash-lite",
34+ "gemini-2.0-flash",
3635 "deepseek-r1-distill-llama-70b",
36+ "llama-3.3-70b-versatile",
3737 "claude-3.5-sonnet",
38− "claude-3-haiku",
39− "claude-3.5-haiku",
40− "o4-mini",
41− "o1-mini"
38+ "o1-mini",
39+ "gpt-4.1-mini",
40+ "gemini-2.5",
41+ "meta-llama/llama-4-scout-17b-16e-instruct"
4242 ],
4343 "type": "string"
4444 },
@@ -72,33 +72,33 @@
7272 "model": {
7373 "description": "Model ID for the agent",
7474 "enum": [
75+ "bedrock.claude-3.7-sonnet",
76+ "claude-3-haiku",
7577 "claude-3.7-sonnet",
76− "claude-3-opus",
77− "gpt-4.1-mini",
78− "gpt-4o",
79− "gpt-4o-mini",
80− "gemini-2.0-flash-lite",
81− "meta-llama/llama-4-maverick-17b-128e-instruct",
82− "gpt-4.1",
78+ "claude-3.5-haiku",
79+ "o3",
8380 "gpt-4.5-preview",
84− "o1",
85− "gpt-4.1-nano",
81+ "o1-pro",
82+ "o4-mini",
83+ "gpt-4.1",
8684 "o3-mini",
85+ "gpt-4.1-nano",
86+ "gpt-4o-mini",
87+ "o1",
8788 "gemini-2.5-flash",
88− "gemini-2.0-flash",
89− "meta-llama/llama-4-scout-17b-16e-instruct",
90− "bedrock.claude-3.7-sonnet",
91− "o1-pro",
92− "o3",
93− "gemini-2.5",
9489 "qwen-qwq",
95− "llama-3.3-70b-versatile",
90+ "meta-llama/llama-4-maverick-17b-128e-instruct",
91+ "claude-3-opus",
92+ "gpt-4o",
93+ "gemini-2.0-flash-lite",
94+ "gemini-2.0-flash",
9695 "deepseek-r1-distill-llama-70b",
96+ "llama-3.3-70b-versatile",
9797 "claude-3.5-sonnet",
98− "claude-3-haiku",
99− "claude-3.5-haiku",
100− "o4-mini",
101− "o1-mini"
98+ "o1-mini",
99+ "gpt-4.1-mini",
100+ "gemini-2.5",
101+ "meta-llama/llama-4-scout-17b-16e-instruct"
102102 ],
103103 "type": "string"
104104 },
@@ -131,6 +131,26 @@
131131 },
132132 "type": "object"
133133 },
134+ "contextPaths": {
135+ "default": [
136+ ".github/copilot-instructions.md",
137+ ".cursorrules",
138+ ".cursor/rules/",
139+ "CLAUDE.md",
140+ "CLAUDE.local.md",
141+ "opencode.md",
142+ "opencode.local.md",
143+ "OpenCode.md",
144+ "OpenCode.local.md",
145+ "OPENCODE.md",
146+ "OPENCODE.local.md"
147+ ],
148+ "description": "Context paths for the application",
149+ "items": {
150+ "type": "string"
151+ },
152+ "type": "array"
153+ },
134154 "data": {
135155 "description": "Storage configuration",
136156 "properties": {