on main help sign in
feat: configure context paths (#86)
8f3a94dGarrett Ladley
cmd/schema/main.go+21−1
@@ -77,6 +77,27 @@ func generateSchema() map[string]any {
77 "default": false,
77 "default": false,
78 }
78 }
79
79
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+
80 // Add MCP servers
101 // Add MCP servers
81 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
102 schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
82 "type": "object",
103 "type": "object",
@@ -259,4 +280,3 @@ func generateSchema() map[string]any {
259
280
260 return schema
281 return schema
261}
282}
262−
internal/config/config.go+24−8
@@ -67,14 +67,15 @@ type LSPConfig struct {
67
67
68// Config is the main configuration structure for the application.
68// Config is the main configuration structure for the application.
69type Config struct {
69type Config struct {
70− Data Data `json:"data"`
70+ Data Data `json:"data"`
71− WorkingDir string `json:"wd,omitempty"`
71+ WorkingDir string `json:"wd,omitempty"`
72− MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
72+ MCPServers map[string]MCPServer `json:"mcpServers,omitempty"`
73− Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
73+ Providers map[models.ModelProvider]Provider `json:"providers,omitempty"`
74− LSP map[string]LSPConfig `json:"lsp,omitempty"`
74+ LSP map[string]LSPConfig `json:"lsp,omitempty"`
75− Agents map[AgentName]Agent `json:"agents"`
75+ Agents map[AgentName]Agent `json:"agents"`
76− Debug bool `json:"debug,omitempty"`
76+ Debug bool `json:"debug,omitempty"`
77− DebugLSP bool `json:"debugLSP,omitempty"`
77+ DebugLSP bool `json:"debugLSP,omitempty"`
78+ ContextPaths []string `json:"contextPaths,omitempty"`
78}
79}
79
80
80// Application constants
81// Application constants
@@ -84,6 +85,20 @@ const (
84 appName = "opencode"
85 appName = "opencode"
85)
86)
86
87
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+
87// Global configuration instance
102// Global configuration instance
88var cfg *Config
103var cfg *Config
89
104
@@ -185,6 +200,7 @@ func configureViper() {
185// setDefaults configures default values for configuration options.
200// setDefaults configures default values for configuration options.
186func setDefaults(debug bool) {
201func setDefaults(debug bool) {
187 viper.SetDefault("data.directory", defaultDataDirectory)
202 viper.SetDefault("data.directory", defaultDataDirectory)
203+ viper.SetDefault("contextPaths", defaultContextPaths)
188
204
189 if debug {
205 if debug {
190 viper.SetDefault("debug", true)
206 viper.SetDefault("debug", true)
internal/llm/prompt/prompt.go+70−43
@@ -5,26 +5,12 @@ import (
5 "os"
5 "os"
6 "path/filepath"
6 "path/filepath"
7 "strings"
7 "strings"
8+ "sync"
8
9
9 "github.com/opencode-ai/opencode/internal/config"
10 "github.com/opencode-ai/opencode/internal/config"
10 "github.com/opencode-ai/opencode/internal/llm/models"
11 "github.com/opencode-ai/opencode/internal/llm/models"
11)
12)
12
13
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−
28func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
14func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) string {
29 basePrompt := ""
15 basePrompt := ""
30 switch agentName {
16 switch agentName {
@@ -40,45 +26,86 @@ func GetAgentPrompt(agentName config.AgentName, provider models.ModelProvider) s
40
26
41 if agentName == config.AgentCoder || agentName == config.AgentTask {
27 if agentName == config.AgentCoder || agentName == config.AgentTask {
42 // Add context from project-specific instruction files if they exist
28 // Add context from project-specific instruction files if they exist
43− contextContent := getContextFromFiles()
29+ contextContent := getContextFromPaths()
44 if contextContent != "" {
30 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)
46 }
32 }
47 }
33 }
48 return basePrompt
34 return basePrompt
49}
35}
50
36
51−// getContextFromFiles checks for the existence of context files and returns their content
37+var (
52−func getContextFromFiles() string {
38+ onceContext sync.Once
53− workDir := config.WorkingDirectory()
39+ contextContent string
54− var 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()
55
66
56− for _, path := range contextFiles {
57− // Check if path ends with a slash (indicating a directory)
58− if strings.HasSuffix(path, "/") {
67+ if strings.HasSuffix(p, "/") {
59− // Handle directory - read all files within it
68+ filepath.WalkDir(filepath.Join(workDir, p), func(path string, d os.DirEntry, err error) error {
60− dirPath := filepath.Join(workDir, path)
69+ if err != nil {
61− files, err := os.ReadDir(dirPath)
70+ return err
62− if err == nil {
71+ }
63− for _, file := range files {
64− if !file.IsDir() {
72+ if !d.IsDir() {
65− filePath := filepath.Join(dirPath, file.Name())
73+ if result := processFile(path); result != "" {
66− content, err := os.ReadFile(filePath)
74+ resultCh <- result
67− if err == nil {
68− contextContent += fmt.Sprintf("\n# From %s\n%s\n", file.Name(), string(content))
69 }
75 }
70 }
76 }
77+ return nil
78+ })
79+ } else {
80+ result := processFile(filepath.Join(workDir, p))
81+ if result != "" {
82+ resultCh <- result
71 }
83 }
72 }
84 }
73− } else {
85+ }(path)
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− }
81 }
86 }
82
87
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")
84}
103}
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 @@
12 "model": {
12 "model": {
13 "description": "Model ID for the agent",
13 "description": "Model ID for the agent",
14 "enum": [
14 "enum": [
15+ "bedrock.claude-3.7-sonnet",
16+ "claude-3-haiku",
15 "claude-3.7-sonnet",
17 "claude-3.7-sonnet",
16− "claude-3-opus",
18+ "claude-3.5-haiku",
17− "gpt-4.1-mini",
19+ "o3",
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",
23 "gpt-4.5-preview",
20 "gpt-4.5-preview",
24− "o1",
21+ "o1-pro",
22+ "o4-mini",
25− "gpt-4.1-nano",
23+ "gpt-4.1",
26 "o3-mini",
24 "o3-mini",
25+ "gpt-4.1-nano",
26+ "gpt-4o-mini",
27+ "o1",
27 "gemini-2.5-flash",
28 "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",
34 "qwen-qwq",
29 "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",
36 "deepseek-r1-distill-llama-70b",
35 "deepseek-r1-distill-llama-70b",
36+ "llama-3.3-70b-versatile",
37 "claude-3.5-sonnet",
37 "claude-3.5-sonnet",
38− "claude-3-haiku",
38+ "o1-mini",
39− "claude-3.5-haiku",
39+ "gpt-4.1-mini",
40− "o4-mini",
40+ "gemini-2.5",
41− "o1-mini"
41+ "meta-llama/llama-4-scout-17b-16e-instruct"
42 ],
42 ],
43 "type": "string"
43 "type": "string"
44 },
44 },
@@ -72,33 +72,33 @@
72 "model": {
72 "model": {
73 "description": "Model ID for the agent",
73 "description": "Model ID for the agent",
74 "enum": [
74 "enum": [
75+ "bedrock.claude-3.7-sonnet",
76+ "claude-3-haiku",
75 "claude-3.7-sonnet",
77 "claude-3.7-sonnet",
76− "claude-3-opus",
78+ "claude-3.5-haiku",
77− "gpt-4.1-mini",
79+ "o3",
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",
83 "gpt-4.5-preview",
80 "gpt-4.5-preview",
84− "o1",
81+ "o1-pro",
82+ "o4-mini",
85− "gpt-4.1-nano",
83+ "gpt-4.1",
86 "o3-mini",
84 "o3-mini",
85+ "gpt-4.1-nano",
86+ "gpt-4o-mini",
87+ "o1",
87 "gemini-2.5-flash",
88 "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",
94 "qwen-qwq",
89 "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",
96 "deepseek-r1-distill-llama-70b",
95 "deepseek-r1-distill-llama-70b",
96+ "llama-3.3-70b-versatile",
97 "claude-3.5-sonnet",
97 "claude-3.5-sonnet",
98− "claude-3-haiku",
98+ "o1-mini",
99− "claude-3.5-haiku",
99+ "gpt-4.1-mini",
100− "o4-mini",
100+ "gemini-2.5",
101− "o1-mini"
101+ "meta-llama/llama-4-scout-17b-16e-instruct"
102 ],
102 ],
103 "type": "string"
103 "type": "string"
104 },
104 },
@@ -131,6 +131,26 @@
131 },
131 },
132 "type": "object"
132 "type": "object"
133 },
133 },
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+ },
134 "data": {
154 "data": {
135 "description": "Storage configuration",
155 "description": "Storage configuration",
136 "properties": {
156 "properties": {