on main help sign in
config validation
a8d5787Kujtim Hoxha
.opencode.json+1
@@ -1,4 +1,5 @@
1{
1{
2+ "$schema": "./opencode-schema.json",
2 "lsp": {
3 "lsp": {
3 "gopls": {
4 "gopls": {
4 "command": "gopls"
5 "command": "gopls"
cmd/schema/README.mdnew file+64
@@ -0,0 +1,64 @@
1+# OpenCode Configuration Schema Generator
2+
3+This tool generates a JSON Schema for the OpenCode configuration file. The schema can be used to validate configuration files and provide autocompletion in editors that support JSON Schema.
4+
5+## Usage
6+
7+```bash
8+go run cmd/schema/main.go > opencode-schema.json
9+```
10+
11+This will generate a JSON Schema file that can be used to validate configuration files.
12+
13+## Schema Features
14+
15+The generated schema includes:
16+
17+- All configuration options with descriptions
18+- Default values where applicable
19+- Validation for enum values (e.g., model IDs, provider types)
20+- Required fields
21+- Type checking
22+
23+## Using the Schema
24+
25+You can use the generated schema in several ways:
26+
27+1. **Editor Integration**: Many editors (VS Code, JetBrains IDEs, etc.) support JSON Schema for validation and autocompletion. You can configure your editor to use the generated schema for `.opencode.json` files.
28+
29+2. **Validation Tools**: You can use tools like [jsonschema](https://github.com/Julian/jsonschema) to validate your configuration files against the schema.
30+
31+3. **Documentation**: The schema serves as documentation for the configuration options.
32+
33+## Example Configuration
34+
35+Here's an example configuration that conforms to the schema:
36+
37+```json
38+{
39+ "data": {
40+ "directory": ".opencode"
41+ },
42+ "debug": false,
43+ "providers": {
44+ "anthropic": {
45+ "apiKey": "your-api-key"
46+ }
47+ },
48+ "agents": {
49+ "coder": {
50+ "model": "claude-3.7-sonnet",
51+ "maxTokens": 5000,
52+ "reasoningEffort": "medium"
53+ },
54+ "task": {
55+ "model": "claude-3.7-sonnet",
56+ "maxTokens": 5000
57+ },
58+ "title": {
59+ "model": "claude-3.7-sonnet",
60+ "maxTokens": 80
61+ }
62+ }
63+}
64+```
cmd/schema/main.gonew file+262
@@ -0,0 +1,262 @@
1+package main
2+
3+import (
4+ "encoding/json"
5+ "fmt"
6+ "os"
7+
8+ "github.com/kujtimiihoxha/opencode/internal/config"
9+ "github.com/kujtimiihoxha/opencode/internal/llm/models"
10+)
11+
12+// JSONSchemaType represents a JSON Schema type
13+type JSONSchemaType struct {
14+ Type string `json:"type,omitempty"`
15+ Description string `json:"description,omitempty"`
16+ Properties map[string]any `json:"properties,omitempty"`
17+ Required []string `json:"required,omitempty"`
18+ AdditionalProperties any `json:"additionalProperties,omitempty"`
19+ Enum []any `json:"enum,omitempty"`
20+ Items map[string]any `json:"items,omitempty"`
21+ OneOf []map[string]any `json:"oneOf,omitempty"`
22+ AnyOf []map[string]any `json:"anyOf,omitempty"`
23+ Default any `json:"default,omitempty"`
24+}
25+
26+func main() {
27+ schema := generateSchema()
28+
29+ // Pretty print the schema
30+ encoder := json.NewEncoder(os.Stdout)
31+ encoder.SetIndent("", " ")
32+ if err := encoder.Encode(schema); err != nil {
33+ fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
34+ os.Exit(1)
35+ }
36+}
37+
38+func generateSchema() map[string]any {
39+ schema := map[string]any{
40+ "$schema": "http://json-schema.org/draft-07/schema#",
41+ "title": "OpenCode Configuration",
42+ "description": "Configuration schema for the OpenCode application",
43+ "type": "object",
44+ "properties": map[string]any{},
45+ }
46+
47+ // Add Data configuration
48+ schema["properties"].(map[string]any)["data"] = map[string]any{
49+ "type": "object",
50+ "description": "Storage configuration",
51+ "properties": map[string]any{
52+ "directory": map[string]any{
53+ "type": "string",
54+ "description": "Directory where application data is stored",
55+ "default": ".opencode",
56+ },
57+ },
58+ "required": []string{"directory"},
59+ }
60+
61+ // Add working directory
62+ schema["properties"].(map[string]any)["wd"] = map[string]any{
63+ "type": "string",
64+ "description": "Working directory for the application",
65+ }
66+
67+ // Add debug flags
68+ schema["properties"].(map[string]any)["debug"] = map[string]any{
69+ "type": "boolean",
70+ "description": "Enable debug mode",
71+ "default": false,
72+ }
73+
74+ schema["properties"].(map[string]any)["debugLSP"] = map[string]any{
75+ "type": "boolean",
76+ "description": "Enable LSP debug mode",
77+ "default": false,
78+ }
79+
80+ // Add MCP servers
81+ schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
82+ "type": "object",
83+ "description": "Model Control Protocol server configurations",
84+ "additionalProperties": map[string]any{
85+ "type": "object",
86+ "description": "MCP server configuration",
87+ "properties": map[string]any{
88+ "command": map[string]any{
89+ "type": "string",
90+ "description": "Command to execute for the MCP server",
91+ },
92+ "env": map[string]any{
93+ "type": "array",
94+ "description": "Environment variables for the MCP server",
95+ "items": map[string]any{
96+ "type": "string",
97+ },
98+ },
99+ "args": map[string]any{
100+ "type": "array",
101+ "description": "Command arguments for the MCP server",
102+ "items": map[string]any{
103+ "type": "string",
104+ },
105+ },
106+ "type": map[string]any{
107+ "type": "string",
108+ "description": "Type of MCP server",
109+ "enum": []string{"stdio", "sse"},
110+ "default": "stdio",
111+ },
112+ "url": map[string]any{
113+ "type": "string",
114+ "description": "URL for SSE type MCP servers",
115+ },
116+ "headers": map[string]any{
117+ "type": "object",
118+ "description": "HTTP headers for SSE type MCP servers",
119+ "additionalProperties": map[string]any{
120+ "type": "string",
121+ },
122+ },
123+ },
124+ "required": []string{"command"},
125+ },
126+ }
127+
128+ // Add providers
129+ providerSchema := map[string]any{
130+ "type": "object",
131+ "description": "LLM provider configurations",
132+ "additionalProperties": map[string]any{
133+ "type": "object",
134+ "description": "Provider configuration",
135+ "properties": map[string]any{
136+ "apiKey": map[string]any{
137+ "type": "string",
138+ "description": "API key for the provider",
139+ },
140+ "disabled": map[string]any{
141+ "type": "boolean",
142+ "description": "Whether the provider is disabled",
143+ "default": false,
144+ },
145+ },
146+ },
147+ }
148+
149+ // Add known providers
150+ knownProviders := []string{
151+ string(models.ProviderAnthropic),
152+ string(models.ProviderOpenAI),
153+ string(models.ProviderGemini),
154+ string(models.ProviderGROQ),
155+ string(models.ProviderBedrock),
156+ }
157+
158+ providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
159+ "type": "string",
160+ "description": "Provider type",
161+ "enum": knownProviders,
162+ }
163+
164+ schema["properties"].(map[string]any)["providers"] = providerSchema
165+
166+ // Add agents
167+ agentSchema := map[string]any{
168+ "type": "object",
169+ "description": "Agent configurations",
170+ "additionalProperties": map[string]any{
171+ "type": "object",
172+ "description": "Agent configuration",
173+ "properties": map[string]any{
174+ "model": map[string]any{
175+ "type": "string",
176+ "description": "Model ID for the agent",
177+ },
178+ "maxTokens": map[string]any{
179+ "type": "integer",
180+ "description": "Maximum tokens for the agent",
181+ "minimum": 1,
182+ },
183+ "reasoningEffort": map[string]any{
184+ "type": "string",
185+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
186+ "enum": []string{"low", "medium", "high"},
187+ },
188+ },
189+ "required": []string{"model"},
190+ },
191+ }
192+
193+ // Add model enum
194+ modelEnum := []string{}
195+ for modelID := range models.SupportedModels {
196+ modelEnum = append(modelEnum, string(modelID))
197+ }
198+ agentSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["model"].(map[string]any)["enum"] = modelEnum
199+
200+ // Add specific agent properties
201+ agentProperties := map[string]any{}
202+ knownAgents := []string{
203+ string(config.AgentCoder),
204+ string(config.AgentTask),
205+ string(config.AgentTitle),
206+ }
207+
208+ for _, agentName := range knownAgents {
209+ agentProperties[agentName] = map[string]any{
210+ "$ref": "#/definitions/agent",
211+ }
212+ }
213+
214+ // Create a combined schema that allows both specific agents and additional ones
215+ combinedAgentSchema := map[string]any{
216+ "type": "object",
217+ "description": "Agent configurations",
218+ "properties": agentProperties,
219+ "additionalProperties": agentSchema["additionalProperties"],
220+ }
221+
222+ schema["properties"].(map[string]any)["agents"] = combinedAgentSchema
223+ schema["definitions"] = map[string]any{
224+ "agent": agentSchema["additionalProperties"],
225+ }
226+
227+ // Add LSP configuration
228+ schema["properties"].(map[string]any)["lsp"] = map[string]any{
229+ "type": "object",
230+ "description": "Language Server Protocol configurations",
231+ "additionalProperties": map[string]any{
232+ "type": "object",
233+ "description": "LSP configuration for a language",
234+ "properties": map[string]any{
235+ "disabled": map[string]any{
236+ "type": "boolean",
237+ "description": "Whether the LSP is disabled",
238+ "default": false,
239+ },
240+ "command": map[string]any{
241+ "type": "string",
242+ "description": "Command to execute for the LSP server",
243+ },
244+ "args": map[string]any{
245+ "type": "array",
246+ "description": "Command arguments for the LSP server",
247+ "items": map[string]any{
248+ "type": "string",
249+ },
250+ },
251+ "options": map[string]any{
252+ "type": "object",
253+ "description": "Additional options for the LSP server",
254+ },
255+ },
256+ "required": []string{"command"},
257+ },
258+ }
259+
260+ return schema
261+}
262+
internal/config/config.go+273−3
@@ -120,13 +120,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
120 }
120 }
121
121
122 applyDefaultValues()
122 applyDefaultValues()
123−
124 defaultLevel := slog.LevelInfo
123 defaultLevel := slog.LevelInfo
125 if cfg.Debug {
124 if cfg.Debug {
126 defaultLevel = slog.LevelDebug
125 defaultLevel = slog.LevelDebug
127 }
126 }
128− // if we are in debug mode make the writer a file
127+ if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
129− if cfg.Debug {
130 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
128 loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
131
129
132 // if file does not exist create it
130 // if file does not exist create it
@@ -156,6 +154,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
156 slog.SetDefault(logger)
154 slog.SetDefault(logger)
157 }
155 }
158
156
157+ // Validate configuration
158+ if err := Validate(); err != nil {
159+ return cfg, fmt.Errorf("config validation failed: %w", err)
160+ }
161+
159 if cfg.Agents == nil {
162 if cfg.Agents == nil {
160 cfg.Agents = make(map[AgentName]Agent)
163 cfg.Agents = make(map[AgentName]Agent)
161 }
164 }
@@ -302,6 +305,273 @@ func applyDefaultValues() {
302 }
305 }
303}
306}
304
307
308+// Validate checks if the configuration is valid and applies defaults where needed.
309+// It validates model IDs and providers, ensuring they are supported.
310+func Validate() error {
311+ if cfg == nil {
312+ return fmt.Errorf("config not loaded")
313+ }
314+
315+ // Validate agent models
316+ for name, agent := range cfg.Agents {
317+ // Check if model exists
318+ model, modelExists := models.SupportedModels[agent.Model]
319+ if !modelExists {
320+ logging.Warn("unsupported model configured, reverting to default",
321+ "agent", name,
322+ "configured_model", agent.Model)
323+
324+ // Set default model based on available providers
325+ if setDefaultModelForAgent(name) {
326+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
327+ } else {
328+ return fmt.Errorf("no valid provider available for agent %s", name)
329+ }
330+ continue
331+ }
332+
333+ // Check if provider for the model is configured
334+ provider := model.Provider
335+ providerCfg, providerExists := cfg.Providers[provider]
336+
337+ if !providerExists {
338+ // Provider not configured, check if we have environment variables
339+ apiKey := getProviderAPIKey(provider)
340+ if apiKey == "" {
341+ logging.Warn("provider not configured for model, reverting to default",
342+ "agent", name,
343+ "model", agent.Model,
344+ "provider", provider)
345+
346+ // Set default model based on available providers
347+ if setDefaultModelForAgent(name) {
348+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
349+ } else {
350+ return fmt.Errorf("no valid provider available for agent %s", name)
351+ }
352+ } else {
353+ // Add provider with API key from environment
354+ cfg.Providers[provider] = Provider{
355+ APIKey: apiKey,
356+ }
357+ logging.Info("added provider from environment", "provider", provider)
358+ }
359+ } else if providerCfg.Disabled || providerCfg.APIKey == "" {
360+ // Provider is disabled or has no API key
361+ logging.Warn("provider is disabled or has no API key, reverting to default",
362+ "agent", name,
363+ "model", agent.Model,
364+ "provider", provider)
365+
366+ // Set default model based on available providers
367+ if setDefaultModelForAgent(name) {
368+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
369+ } else {
370+ return fmt.Errorf("no valid provider available for agent %s", name)
371+ }
372+ }
373+
374+ // Validate max tokens
375+ if agent.MaxTokens <= 0 {
376+ logging.Warn("invalid max tokens, setting to default",
377+ "agent", name,
378+ "model", agent.Model,
379+ "max_tokens", agent.MaxTokens)
380+
381+ // Update the agent with default max tokens
382+ updatedAgent := cfg.Agents[name]
383+ if model.DefaultMaxTokens > 0 {
384+ updatedAgent.MaxTokens = model.DefaultMaxTokens
385+ } else {
386+ updatedAgent.MaxTokens = 4096 // Fallback default
387+ }
388+ cfg.Agents[name] = updatedAgent
389+ } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
390+ // Ensure max tokens doesn't exceed half the context window (reasonable limit)
391+ logging.Warn("max tokens exceeds half the context window, adjusting",
392+ "agent", name,
393+ "model", agent.Model,
394+ "max_tokens", agent.MaxTokens,
395+ "context_window", model.ContextWindow)
396+
397+ // Update the agent with adjusted max tokens
398+ updatedAgent := cfg.Agents[name]
399+ updatedAgent.MaxTokens = model.ContextWindow / 2
400+ cfg.Agents[name] = updatedAgent
401+ }
402+
403+ // Validate reasoning effort for models that support reasoning
404+ if model.CanReason && provider == models.ProviderOpenAI {
405+ if agent.ReasoningEffort == "" {
406+ // Set default reasoning effort for models that support it
407+ logging.Info("setting default reasoning effort for model that supports reasoning",
408+ "agent", name,
409+ "model", agent.Model)
410+
411+ // Update the agent with default reasoning effort
412+ updatedAgent := cfg.Agents[name]
413+ updatedAgent.ReasoningEffort = "medium"
414+ cfg.Agents[name] = updatedAgent
415+ } else {
416+ // Check if reasoning effort is valid (low, medium, high)
417+ effort := strings.ToLower(agent.ReasoningEffort)
418+ if effort != "low" && effort != "medium" && effort != "high" {
419+ logging.Warn("invalid reasoning effort, setting to medium",
420+ "agent", name,
421+ "model", agent.Model,
422+ "reasoning_effort", agent.ReasoningEffort)
423+
424+ // Update the agent with valid reasoning effort
425+ updatedAgent := cfg.Agents[name]
426+ updatedAgent.ReasoningEffort = "medium"
427+ cfg.Agents[name] = updatedAgent
428+ }
429+ }
430+ } else if !model.CanReason && agent.ReasoningEffort != "" {
431+ // Model doesn't support reasoning but reasoning effort is set
432+ logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
433+ "agent", name,
434+ "model", agent.Model,
435+ "reasoning_effort", agent.ReasoningEffort)
436+
437+ // Update the agent to remove reasoning effort
438+ updatedAgent := cfg.Agents[name]
439+ updatedAgent.ReasoningEffort = ""
440+ cfg.Agents[name] = updatedAgent
441+ }
442+ }
443+
444+ // Validate providers
445+ for provider, providerCfg := range cfg.Providers {
446+ if providerCfg.APIKey == "" && !providerCfg.Disabled {
447+ logging.Warn("provider has no API key, marking as disabled", "provider", provider)
448+ providerCfg.Disabled = true
449+ cfg.Providers[provider] = providerCfg
450+ }
451+ }
452+
453+ // Validate LSP configurations
454+ for language, lspConfig := range cfg.LSP {
455+ if lspConfig.Command == "" && !lspConfig.Disabled {
456+ logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
457+ lspConfig.Disabled = true
458+ cfg.LSP[language] = lspConfig
459+ }
460+ }
461+
462+ return nil
463+}
464+
465+// getProviderAPIKey gets the API key for a provider from environment variables
466+func getProviderAPIKey(provider models.ModelProvider) string {
467+ switch provider {
468+ case models.ProviderAnthropic:
469+ return os.Getenv("ANTHROPIC_API_KEY")
470+ case models.ProviderOpenAI:
471+ return os.Getenv("OPENAI_API_KEY")
472+ case models.ProviderGemini:
473+ return os.Getenv("GEMINI_API_KEY")
474+ case models.ProviderGROQ:
475+ return os.Getenv("GROQ_API_KEY")
476+ case models.ProviderBedrock:
477+ if hasAWSCredentials() {
478+ return "aws-credentials-available"
479+ }
480+ }
481+ return ""
482+}
483+
484+// setDefaultModelForAgent sets a default model for an agent based on available providers
485+func setDefaultModelForAgent(agent AgentName) bool {
486+ // Check providers in order of preference
487+ if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
488+ maxTokens := int64(5000)
489+ if agent == AgentTitle {
490+ maxTokens = 80
491+ }
492+ cfg.Agents[agent] = Agent{
493+ Model: models.Claude37Sonnet,
494+ MaxTokens: maxTokens,
495+ }
496+ return true
497+ }
498+
499+ if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
500+ var model models.ModelID
501+ maxTokens := int64(5000)
502+ reasoningEffort := ""
503+
504+ switch agent {
505+ case AgentTitle:
506+ model = models.GPT41Mini
507+ maxTokens = 80
508+ case AgentTask:
509+ model = models.GPT41Mini
510+ default:
511+ model = models.GPT41
512+ }
513+
514+ // Check if model supports reasoning
515+ if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
516+ reasoningEffort = "medium"
517+ }
518+
519+ cfg.Agents[agent] = Agent{
520+ Model: model,
521+ MaxTokens: maxTokens,
522+ ReasoningEffort: reasoningEffort,
523+ }
524+ return true
525+ }
526+
527+ if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
528+ var model models.ModelID
529+ maxTokens := int64(5000)
530+
531+ if agent == AgentTitle {
532+ model = models.Gemini25Flash
533+ maxTokens = 80
534+ } else {
535+ model = models.Gemini25
536+ }
537+
538+ cfg.Agents[agent] = Agent{
539+ Model: model,
540+ MaxTokens: maxTokens,
541+ }
542+ return true
543+ }
544+
545+ if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
546+ maxTokens := int64(5000)
547+ if agent == AgentTitle {
548+ maxTokens = 80
549+ }
550+
551+ cfg.Agents[agent] = Agent{
552+ Model: models.QWENQwq,
553+ MaxTokens: maxTokens,
554+ }
555+ return true
556+ }
557+
558+ if hasAWSCredentials() {
559+ maxTokens := int64(5000)
560+ if agent == AgentTitle {
561+ maxTokens = 80
562+ }
563+
564+ cfg.Agents[agent] = Agent{
565+ Model: models.BedrockClaude37Sonnet,
566+ MaxTokens: maxTokens,
567+ ReasoningEffort: "medium", // Claude models support reasoning
568+ }
569+ return true
570+ }
571+
572+ return false
573+}
574+
305// Get returns the current configuration.
575// Get returns the current configuration.
306// It's safe to call this function multiple times.
576// It's safe to call this function multiple times.
307func Get() *Config {
577func Get() *Config {
internal/llm/agent/agent.go+1−1
@@ -471,7 +471,7 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error)
471 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
471 provider.WithReasoningEffort(agentConfig.ReasoningEffort),
472 ),
472 ),
473 )
473 )
474− } else if model.Provider == models.ProviderAnthropic && model.CanReason {
474+ } else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
475 opts = append(
475 opts = append(
476 opts,
476 opts,
477 provider.WithAnthropicOptions(
477 provider.WithAnthropicOptions(
internal/llm/tools/edit.go+21−6
@@ -196,11 +196,16 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
196 content,
196 content,
197 filePath,
197 filePath,
198 )
198 )
199+ rootDir := config.WorkingDirectory()
200+ permissionPath := filepath.Dir(filePath)
201+ if strings.HasPrefix(filePath, rootDir) {
202+ permissionPath = rootDir
203+ }
199 p := e.permissions.Request(
204 p := e.permissions.Request(
200 permission.CreatePermissionRequest{
205 permission.CreatePermissionRequest{
201− Path: filepath.Dir(filePath),
206+ Path: permissionPath,
202 ToolName: EditToolName,
207 ToolName: EditToolName,
203− Action: "create",
208+ Action: "write",
204 Description: fmt.Sprintf("Create file %s", filePath),
209 Description: fmt.Sprintf("Create file %s", filePath),
205 Params: EditPermissionsParams{
210 Params: EditPermissionsParams{
206 FilePath: filePath,
211 FilePath: filePath,
@@ -301,11 +306,16 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
301 filePath,
306 filePath,
302 )
307 )
303
308
309+ rootDir := config.WorkingDirectory()
310+ permissionPath := filepath.Dir(filePath)
311+ if strings.HasPrefix(filePath, rootDir) {
312+ permissionPath = rootDir
313+ }
304 p := e.permissions.Request(
314 p := e.permissions.Request(
305 permission.CreatePermissionRequest{
315 permission.CreatePermissionRequest{
306− Path: filepath.Dir(filePath),
316+ Path: permissionPath,
307 ToolName: EditToolName,
317 ToolName: EditToolName,
308− Action: "delete",
318+ Action: "write",
309 Description: fmt.Sprintf("Delete content from file %s", filePath),
319 Description: fmt.Sprintf("Delete content from file %s", filePath),
310 Params: EditPermissionsParams{
320 Params: EditPermissionsParams{
311 FilePath: filePath,
321 FilePath: filePath,
@@ -415,11 +425,16 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
415 newContent,
425 newContent,
416 filePath,
426 filePath,
417 )
427 )
428+ rootDir := config.WorkingDirectory()
429+ permissionPath := filepath.Dir(filePath)
430+ if strings.HasPrefix(filePath, rootDir) {
431+ permissionPath = rootDir
432+ }
418 p := e.permissions.Request(
433 p := e.permissions.Request(
419 permission.CreatePermissionRequest{
434 permission.CreatePermissionRequest{
420− Path: filepath.Dir(filePath),
435+ Path: permissionPath,
421 ToolName: EditToolName,
436 ToolName: EditToolName,
422− Action: "replace",
437+ Action: "write",
423 Description: fmt.Sprintf("Replace content in file %s", filePath),
438 Description: fmt.Sprintf("Replace content in file %s", filePath),
424 Params: EditPermissionsParams{
439 Params: EditPermissionsParams{
425 FilePath: filePath,
440 FilePath: filePath,
internal/llm/tools/write.go+9−2
@@ -6,6 +6,7 @@ import (
6 "fmt"
6 "fmt"
7 "os"
7 "os"
8 "path/filepath"
8 "path/filepath"
9+ "strings"
9 "time"
10 "time"
10
11
11 "github.com/kujtimiihoxha/opencode/internal/config"
12 "github.com/kujtimiihoxha/opencode/internal/config"
@@ -159,11 +160,17 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
159 params.Content,
160 params.Content,
160 filePath,
161 filePath,
161 )
162 )
163+
164+ rootDir := config.WorkingDirectory()
165+ permissionPath := filepath.Dir(filePath)
166+ if strings.HasPrefix(filePath, rootDir) {
167+ permissionPath = rootDir
168+ }
162 p := w.permissions.Request(
169 p := w.permissions.Request(
163 permission.CreatePermissionRequest{
170 permission.CreatePermissionRequest{
164− Path: filePath,
171+ Path: permissionPath,
165 ToolName: WriteToolName,
172 ToolName: WriteToolName,
166− Action: "create",
173+ Action: "write",
167 Description: fmt.Sprintf("Create file %s", filePath),
174 Description: fmt.Sprintf("Create file %s", filePath),
168 Params: WritePermissionsParams{
175 Params: WritePermissionsParams{
169 FilePath: filePath,
176 FilePath: filePath,
internal/permission/permission.go+8−2
@@ -2,10 +2,12 @@ package permission
2
2
3import (
3import (
4 "errors"
4 "errors"
5+ "path/filepath"
5 "sync"
6 "sync"
6 "time"
7 "time"
7
8
8 "github.com/google/uuid"
9 "github.com/google/uuid"
10+ "github.com/kujtimiihoxha/opencode/internal/config"
9 "github.com/kujtimiihoxha/opencode/internal/pubsub"
11 "github.com/kujtimiihoxha/opencode/internal/pubsub"
10)
12)
11
13
@@ -67,9 +69,13 @@ func (s *permissionService) Deny(permission PermissionRequest) {
67}
69}
68
70
69func (s *permissionService) Request(opts CreatePermissionRequest) bool {
71func (s *permissionService) Request(opts CreatePermissionRequest) bool {
72+ dir := filepath.Dir(opts.Path)
73+ if dir == "." {
74+ dir = config.WorkingDirectory()
75+ }
70 permission := PermissionRequest{
76 permission := PermissionRequest{
71 ID: uuid.New().String(),
77 ID: uuid.New().String(),
72− Path: opts.Path,
78+ Path: dir,
73 ToolName: opts.ToolName,
79 ToolName: opts.ToolName,
74 Description: opts.Description,
80 Description: opts.Description,
75 Action: opts.Action,
81 Action: opts.Action,
@@ -77,7 +83,7 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
77 }
83 }
78
84
79 for _, p := range s.sessionPermissions {
85 for _, p := range s.sessionPermissions {
80− if p.ToolName == permission.ToolName && p.Action == permission.Action {
86+ if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
81 return true
87 return true
82 }
88 }
83 }
89 }
internal/tui/tui.go+2−2
@@ -57,8 +57,8 @@ var returnKey = key.NewBinding(
57)
57)
58
58
59var logsKeyReturnKey = key.NewBinding(
59var logsKeyReturnKey = key.NewBinding(
60− key.WithKeys("backspace"),
60+ key.WithKeys("backspace", "q"),
61− key.WithHelp("backspace", "go back"),
61+ key.WithHelp("backspace/q", "go back"),
62)
62)
63
63
64type appModel struct {
64type appModel struct {
internal/version/version.go+1−1
@@ -5,7 +5,7 @@ import "runtime/debug"
5// Build-time parameters set via -ldflags
5// Build-time parameters set via -ldflags
6var Version = "unknown"
6var Version = "unknown"
7
7
8−// A user may install pug using `go install github.com/leg100/pug@latest`
8+// A user may install pug using `go install github.com/kujtimiihoxha/opencode@latest`.
9// without -ldflags, in which case the version above is unset. As a workaround
9// without -ldflags, in which case the version above is unset. As a workaround
10// we use the embedded build version that *is* set when using `go install` (and
10// we use the embedded build version that *is* set when using `go install` (and
11// is only set for `go install` and not for `go build`).
11// is only set for `go install` and not for `go build`).
opencode-schema.jsonnew file+269
@@ -0,0 +1,269 @@
1+{
2+ "$schema": "http://json-schema.org/draft-07/schema#",
3+ "definitions": {
4+ "agent": {
5+ "description": "Agent configuration",
6+ "properties": {
7+ "maxTokens": {
8+ "description": "Maximum tokens for the agent",
9+ "minimum": 1,
10+ "type": "integer"
11+ },
12+ "model": {
13+ "description": "Model ID for the agent",
14+ "enum": [
15+ "gemini-2.0-flash",
16+ "bedrock.claude-3.7-sonnet",
17+ "claude-3-opus",
18+ "claude-3.5-sonnet",
19+ "gpt-4o-mini",
20+ "o1",
21+ "o3-mini",
22+ "o1-pro",
23+ "o4-mini",
24+ "claude-3-haiku",
25+ "gpt-4o",
26+ "o3",
27+ "gpt-4.1-mini",
28+ "gpt-4.5-preview",
29+ "gemini-2.5-flash",
30+ "claude-3.5-haiku",
31+ "gpt-4.1",
32+ "gemini-2.0-flash-lite",
33+ "claude-3.7-sonnet",
34+ "o1-mini",
35+ "gpt-4.1-nano",
36+ "gemini-2.5"
37+ ],
38+ "type": "string"
39+ },
40+ "reasoningEffort": {
41+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
42+ "enum": [
43+ "low",
44+ "medium",
45+ "high"
46+ ],
47+ "type": "string"
48+ }
49+ },
50+ "required": [
51+ "model"
52+ ],
53+ "type": "object"
54+ }
55+ },
56+ "description": "Configuration schema for the OpenCode application",
57+ "properties": {
58+ "agents": {
59+ "additionalProperties": {
60+ "description": "Agent configuration",
61+ "properties": {
62+ "maxTokens": {
63+ "description": "Maximum tokens for the agent",
64+ "minimum": 1,
65+ "type": "integer"
66+ },
67+ "model": {
68+ "description": "Model ID for the agent",
69+ "enum": [
70+ "gemini-2.0-flash",
71+ "bedrock.claude-3.7-sonnet",
72+ "claude-3-opus",
73+ "claude-3.5-sonnet",
74+ "gpt-4o-mini",
75+ "o1",
76+ "o3-mini",
77+ "o1-pro",
78+ "o4-mini",
79+ "claude-3-haiku",
80+ "gpt-4o",
81+ "o3",
82+ "gpt-4.1-mini",
83+ "gpt-4.5-preview",
84+ "gemini-2.5-flash",
85+ "claude-3.5-haiku",
86+ "gpt-4.1",
87+ "gemini-2.0-flash-lite",
88+ "claude-3.7-sonnet",
89+ "o1-mini",
90+ "gpt-4.1-nano",
91+ "gemini-2.5"
92+ ],
93+ "type": "string"
94+ },
95+ "reasoningEffort": {
96+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
97+ "enum": [
98+ "low",
99+ "medium",
100+ "high"
101+ ],
102+ "type": "string"
103+ }
104+ },
105+ "required": [
106+ "model"
107+ ],
108+ "type": "object"
109+ },
110+ "description": "Agent configurations",
111+ "properties": {
112+ "coder": {
113+ "$ref": "#/definitions/agent"
114+ },
115+ "task": {
116+ "$ref": "#/definitions/agent"
117+ },
118+ "title": {
119+ "$ref": "#/definitions/agent"
120+ }
121+ },
122+ "type": "object"
123+ },
124+ "data": {
125+ "description": "Storage configuration",
126+ "properties": {
127+ "directory": {
128+ "default": ".opencode",
129+ "description": "Directory where application data is stored",
130+ "type": "string"
131+ }
132+ },
133+ "required": [
134+ "directory"
135+ ],
136+ "type": "object"
137+ },
138+ "debug": {
139+ "default": false,
140+ "description": "Enable debug mode",
141+ "type": "boolean"
142+ },
143+ "debugLSP": {
144+ "default": false,
145+ "description": "Enable LSP debug mode",
146+ "type": "boolean"
147+ },
148+ "lsp": {
149+ "additionalProperties": {
150+ "description": "LSP configuration for a language",
151+ "properties": {
152+ "args": {
153+ "description": "Command arguments for the LSP server",
154+ "items": {
155+ "type": "string"
156+ },
157+ "type": "array"
158+ },
159+ "command": {
160+ "description": "Command to execute for the LSP server",
161+ "type": "string"
162+ },
163+ "disabled": {
164+ "default": false,
165+ "description": "Whether the LSP is disabled",
166+ "type": "boolean"
167+ },
168+ "options": {
169+ "description": "Additional options for the LSP server",
170+ "type": "object"
171+ }
172+ },
173+ "required": [
174+ "command"
175+ ],
176+ "type": "object"
177+ },
178+ "description": "Language Server Protocol configurations",
179+ "type": "object"
180+ },
181+ "mcpServers": {
182+ "additionalProperties": {
183+ "description": "MCP server configuration",
184+ "properties": {
185+ "args": {
186+ "description": "Command arguments for the MCP server",
187+ "items": {
188+ "type": "string"
189+ },
190+ "type": "array"
191+ },
192+ "command": {
193+ "description": "Command to execute for the MCP server",
194+ "type": "string"
195+ },
196+ "env": {
197+ "description": "Environment variables for the MCP server",
198+ "items": {
199+ "type": "string"
200+ },
201+ "type": "array"
202+ },
203+ "headers": {
204+ "additionalProperties": {
205+ "type": "string"
206+ },
207+ "description": "HTTP headers for SSE type MCP servers",
208+ "type": "object"
209+ },
210+ "type": {
211+ "default": "stdio",
212+ "description": "Type of MCP server",
213+ "enum": [
214+ "stdio",
215+ "sse"
216+ ],
217+ "type": "string"
218+ },
219+ "url": {
220+ "description": "URL for SSE type MCP servers",
221+ "type": "string"
222+ }
223+ },
224+ "required": [
225+ "command"
226+ ],
227+ "type": "object"
228+ },
229+ "description": "Model Control Protocol server configurations",
230+ "type": "object"
231+ },
232+ "providers": {
233+ "additionalProperties": {
234+ "description": "Provider configuration",
235+ "properties": {
236+ "apiKey": {
237+ "description": "API key for the provider",
238+ "type": "string"
239+ },
240+ "disabled": {
241+ "default": false,
242+ "description": "Whether the provider is disabled",
243+ "type": "boolean"
244+ },
245+ "provider": {
246+ "description": "Provider type",
247+ "enum": [
248+ "anthropic",
249+ "openai",
250+ "gemini",
251+ "groq",
252+ "bedrock"
253+ ],
254+ "type": "string"
255+ }
256+ },
257+ "type": "object"
258+ },
259+ "description": "LLM provider configurations",
260+ "type": "object"
261+ },
262+ "wd": {
263+ "description": "Working directory for the application",
264+ "type": "string"
265+ }
266+ },
267+ "title": "OpenCode Configuration",
268+ "type": "object"
269+}