.opencode.json 🔗
@@ -1,4 +1,5 @@
{
+ "$schema": "./opencode-schema.json",
"lsp": {
"gopls": {
"command": "gopls"
Kujtim Hoxha created
.opencode.json | 1
cmd/schema/README.md | 64 +++++++
cmd/schema/main.go | 262 +++++++++++++++++++++++++++++++
internal/config/config.go | 276 ++++++++++++++++++++++++++++++++
internal/llm/agent/agent.go | 2
internal/llm/tools/edit.go | 27 ++
internal/llm/tools/write.go | 11 +
internal/permission/permission.go | 10
internal/tui/tui.go | 4
internal/version/version.go | 2
opencode-schema.json | 269 ++++++++++++++++++++++++++++++++
11 files changed, 911 insertions(+), 17 deletions(-)
@@ -1,4 +1,5 @@
{
+ "$schema": "./opencode-schema.json",
"lsp": {
"gopls": {
"command": "gopls"
@@ -0,0 +1,64 @@
+# OpenCode Configuration Schema Generator
+
+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.
+
+## Usage
+
+```bash
+go run cmd/schema/main.go > opencode-schema.json
+```
+
+This will generate a JSON Schema file that can be used to validate configuration files.
+
+## Schema Features
+
+The generated schema includes:
+
+- All configuration options with descriptions
+- Default values where applicable
+- Validation for enum values (e.g., model IDs, provider types)
+- Required fields
+- Type checking
+
+## Using the Schema
+
+You can use the generated schema in several ways:
+
+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.
+
+2. **Validation Tools**: You can use tools like [jsonschema](https://github.com/Julian/jsonschema) to validate your configuration files against the schema.
+
+3. **Documentation**: The schema serves as documentation for the configuration options.
+
+## Example Configuration
+
+Here's an example configuration that conforms to the schema:
+
+```json
+{
+ "data": {
+ "directory": ".opencode"
+ },
+ "debug": false,
+ "providers": {
+ "anthropic": {
+ "apiKey": "your-api-key"
+ }
+ },
+ "agents": {
+ "coder": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 5000,
+ "reasoningEffort": "medium"
+ },
+ "task": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 5000
+ },
+ "title": {
+ "model": "claude-3.7-sonnet",
+ "maxTokens": 80
+ }
+ }
+}
+```
@@ -0,0 +1,262 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/kujtimiihoxha/opencode/internal/config"
+ "github.com/kujtimiihoxha/opencode/internal/llm/models"
+)
+
+// JSONSchemaType represents a JSON Schema type
+type JSONSchemaType struct {
+ Type string `json:"type,omitempty"`
+ Description string `json:"description,omitempty"`
+ Properties map[string]any `json:"properties,omitempty"`
+ Required []string `json:"required,omitempty"`
+ AdditionalProperties any `json:"additionalProperties,omitempty"`
+ Enum []any `json:"enum,omitempty"`
+ Items map[string]any `json:"items,omitempty"`
+ OneOf []map[string]any `json:"oneOf,omitempty"`
+ AnyOf []map[string]any `json:"anyOf,omitempty"`
+ Default any `json:"default,omitempty"`
+}
+
+func main() {
+ schema := generateSchema()
+
+ // Pretty print the schema
+ encoder := json.NewEncoder(os.Stdout)
+ encoder.SetIndent("", " ")
+ if err := encoder.Encode(schema); err != nil {
+ fmt.Fprintf(os.Stderr, "Error encoding schema: %v\n", err)
+ os.Exit(1)
+ }
+}
+
+func generateSchema() map[string]any {
+ schema := map[string]any{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "OpenCode Configuration",
+ "description": "Configuration schema for the OpenCode application",
+ "type": "object",
+ "properties": map[string]any{},
+ }
+
+ // Add Data configuration
+ schema["properties"].(map[string]any)["data"] = map[string]any{
+ "type": "object",
+ "description": "Storage configuration",
+ "properties": map[string]any{
+ "directory": map[string]any{
+ "type": "string",
+ "description": "Directory where application data is stored",
+ "default": ".opencode",
+ },
+ },
+ "required": []string{"directory"},
+ }
+
+ // Add working directory
+ schema["properties"].(map[string]any)["wd"] = map[string]any{
+ "type": "string",
+ "description": "Working directory for the application",
+ }
+
+ // Add debug flags
+ schema["properties"].(map[string]any)["debug"] = map[string]any{
+ "type": "boolean",
+ "description": "Enable debug mode",
+ "default": false,
+ }
+
+ schema["properties"].(map[string]any)["debugLSP"] = map[string]any{
+ "type": "boolean",
+ "description": "Enable LSP debug mode",
+ "default": false,
+ }
+
+ // Add MCP servers
+ schema["properties"].(map[string]any)["mcpServers"] = map[string]any{
+ "type": "object",
+ "description": "Model Control Protocol server configurations",
+ "additionalProperties": map[string]any{
+ "type": "object",
+ "description": "MCP server configuration",
+ "properties": map[string]any{
+ "command": map[string]any{
+ "type": "string",
+ "description": "Command to execute for the MCP server",
+ },
+ "env": map[string]any{
+ "type": "array",
+ "description": "Environment variables for the MCP server",
+ "items": map[string]any{
+ "type": "string",
+ },
+ },
+ "args": map[string]any{
+ "type": "array",
+ "description": "Command arguments for the MCP server",
+ "items": map[string]any{
+ "type": "string",
+ },
+ },
+ "type": map[string]any{
+ "type": "string",
+ "description": "Type of MCP server",
+ "enum": []string{"stdio", "sse"},
+ "default": "stdio",
+ },
+ "url": map[string]any{
+ "type": "string",
+ "description": "URL for SSE type MCP servers",
+ },
+ "headers": map[string]any{
+ "type": "object",
+ "description": "HTTP headers for SSE type MCP servers",
+ "additionalProperties": map[string]any{
+ "type": "string",
+ },
+ },
+ },
+ "required": []string{"command"},
+ },
+ }
+
+ // Add providers
+ providerSchema := map[string]any{
+ "type": "object",
+ "description": "LLM provider configurations",
+ "additionalProperties": map[string]any{
+ "type": "object",
+ "description": "Provider configuration",
+ "properties": map[string]any{
+ "apiKey": map[string]any{
+ "type": "string",
+ "description": "API key for the provider",
+ },
+ "disabled": map[string]any{
+ "type": "boolean",
+ "description": "Whether the provider is disabled",
+ "default": false,
+ },
+ },
+ },
+ }
+
+ // Add known providers
+ knownProviders := []string{
+ string(models.ProviderAnthropic),
+ string(models.ProviderOpenAI),
+ string(models.ProviderGemini),
+ string(models.ProviderGROQ),
+ string(models.ProviderBedrock),
+ }
+
+ providerSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["provider"] = map[string]any{
+ "type": "string",
+ "description": "Provider type",
+ "enum": knownProviders,
+ }
+
+ schema["properties"].(map[string]any)["providers"] = providerSchema
+
+ // Add agents
+ agentSchema := map[string]any{
+ "type": "object",
+ "description": "Agent configurations",
+ "additionalProperties": map[string]any{
+ "type": "object",
+ "description": "Agent configuration",
+ "properties": map[string]any{
+ "model": map[string]any{
+ "type": "string",
+ "description": "Model ID for the agent",
+ },
+ "maxTokens": map[string]any{
+ "type": "integer",
+ "description": "Maximum tokens for the agent",
+ "minimum": 1,
+ },
+ "reasoningEffort": map[string]any{
+ "type": "string",
+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
+ "enum": []string{"low", "medium", "high"},
+ },
+ },
+ "required": []string{"model"},
+ },
+ }
+
+ // Add model enum
+ modelEnum := []string{}
+ for modelID := range models.SupportedModels {
+ modelEnum = append(modelEnum, string(modelID))
+ }
+ agentSchema["additionalProperties"].(map[string]any)["properties"].(map[string]any)["model"].(map[string]any)["enum"] = modelEnum
+
+ // Add specific agent properties
+ agentProperties := map[string]any{}
+ knownAgents := []string{
+ string(config.AgentCoder),
+ string(config.AgentTask),
+ string(config.AgentTitle),
+ }
+
+ for _, agentName := range knownAgents {
+ agentProperties[agentName] = map[string]any{
+ "$ref": "#/definitions/agent",
+ }
+ }
+
+ // Create a combined schema that allows both specific agents and additional ones
+ combinedAgentSchema := map[string]any{
+ "type": "object",
+ "description": "Agent configurations",
+ "properties": agentProperties,
+ "additionalProperties": agentSchema["additionalProperties"],
+ }
+
+ schema["properties"].(map[string]any)["agents"] = combinedAgentSchema
+ schema["definitions"] = map[string]any{
+ "agent": agentSchema["additionalProperties"],
+ }
+
+ // Add LSP configuration
+ schema["properties"].(map[string]any)["lsp"] = map[string]any{
+ "type": "object",
+ "description": "Language Server Protocol configurations",
+ "additionalProperties": map[string]any{
+ "type": "object",
+ "description": "LSP configuration for a language",
+ "properties": map[string]any{
+ "disabled": map[string]any{
+ "type": "boolean",
+ "description": "Whether the LSP is disabled",
+ "default": false,
+ },
+ "command": map[string]any{
+ "type": "string",
+ "description": "Command to execute for the LSP server",
+ },
+ "args": map[string]any{
+ "type": "array",
+ "description": "Command arguments for the LSP server",
+ "items": map[string]any{
+ "type": "string",
+ },
+ },
+ "options": map[string]any{
+ "type": "object",
+ "description": "Additional options for the LSP server",
+ },
+ },
+ "required": []string{"command"},
+ },
+ }
+
+ return schema
+}
+
@@ -120,13 +120,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
}
applyDefaultValues()
-
defaultLevel := slog.LevelInfo
if cfg.Debug {
defaultLevel = slog.LevelDebug
}
- // if we are in debug mode make the writer a file
- if cfg.Debug {
+ if os.Getenv("OPENCODE_DEV_DEBUG") == "true" {
loggingFile := fmt.Sprintf("%s/%s", cfg.Data.Directory, "debug.log")
// if file does not exist create it
@@ -156,6 +154,11 @@ func Load(workingDir string, debug bool) (*Config, error) {
slog.SetDefault(logger)
}
+ // Validate configuration
+ if err := Validate(); err != nil {
+ return cfg, fmt.Errorf("config validation failed: %w", err)
+ }
+
if cfg.Agents == nil {
cfg.Agents = make(map[AgentName]Agent)
}
@@ -302,6 +305,273 @@ func applyDefaultValues() {
}
}
+// Validate checks if the configuration is valid and applies defaults where needed.
+// It validates model IDs and providers, ensuring they are supported.
+func Validate() error {
+ if cfg == nil {
+ return fmt.Errorf("config not loaded")
+ }
+
+ // Validate agent models
+ for name, agent := range cfg.Agents {
+ // Check if model exists
+ model, modelExists := models.SupportedModels[agent.Model]
+ if !modelExists {
+ logging.Warn("unsupported model configured, reverting to default",
+ "agent", name,
+ "configured_model", agent.Model)
+
+ // Set default model based on available providers
+ if setDefaultModelForAgent(name) {
+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+ } else {
+ return fmt.Errorf("no valid provider available for agent %s", name)
+ }
+ continue
+ }
+
+ // Check if provider for the model is configured
+ provider := model.Provider
+ providerCfg, providerExists := cfg.Providers[provider]
+
+ if !providerExists {
+ // Provider not configured, check if we have environment variables
+ apiKey := getProviderAPIKey(provider)
+ if apiKey == "" {
+ logging.Warn("provider not configured for model, reverting to default",
+ "agent", name,
+ "model", agent.Model,
+ "provider", provider)
+
+ // Set default model based on available providers
+ if setDefaultModelForAgent(name) {
+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+ } else {
+ return fmt.Errorf("no valid provider available for agent %s", name)
+ }
+ } else {
+ // Add provider with API key from environment
+ cfg.Providers[provider] = Provider{
+ APIKey: apiKey,
+ }
+ logging.Info("added provider from environment", "provider", provider)
+ }
+ } else if providerCfg.Disabled || providerCfg.APIKey == "" {
+ // Provider is disabled or has no API key
+ logging.Warn("provider is disabled or has no API key, reverting to default",
+ "agent", name,
+ "model", agent.Model,
+ "provider", provider)
+
+ // Set default model based on available providers
+ if setDefaultModelForAgent(name) {
+ logging.Info("set default model for agent", "agent", name, "model", cfg.Agents[name].Model)
+ } else {
+ return fmt.Errorf("no valid provider available for agent %s", name)
+ }
+ }
+
+ // Validate max tokens
+ if agent.MaxTokens <= 0 {
+ logging.Warn("invalid max tokens, setting to default",
+ "agent", name,
+ "model", agent.Model,
+ "max_tokens", agent.MaxTokens)
+
+ // Update the agent with default max tokens
+ updatedAgent := cfg.Agents[name]
+ if model.DefaultMaxTokens > 0 {
+ updatedAgent.MaxTokens = model.DefaultMaxTokens
+ } else {
+ updatedAgent.MaxTokens = 4096 // Fallback default
+ }
+ cfg.Agents[name] = updatedAgent
+ } else if model.ContextWindow > 0 && agent.MaxTokens > model.ContextWindow/2 {
+ // Ensure max tokens doesn't exceed half the context window (reasonable limit)
+ logging.Warn("max tokens exceeds half the context window, adjusting",
+ "agent", name,
+ "model", agent.Model,
+ "max_tokens", agent.MaxTokens,
+ "context_window", model.ContextWindow)
+
+ // Update the agent with adjusted max tokens
+ updatedAgent := cfg.Agents[name]
+ updatedAgent.MaxTokens = model.ContextWindow / 2
+ cfg.Agents[name] = updatedAgent
+ }
+
+ // Validate reasoning effort for models that support reasoning
+ if model.CanReason && provider == models.ProviderOpenAI {
+ if agent.ReasoningEffort == "" {
+ // Set default reasoning effort for models that support it
+ logging.Info("setting default reasoning effort for model that supports reasoning",
+ "agent", name,
+ "model", agent.Model)
+
+ // Update the agent with default reasoning effort
+ updatedAgent := cfg.Agents[name]
+ updatedAgent.ReasoningEffort = "medium"
+ cfg.Agents[name] = updatedAgent
+ } else {
+ // Check if reasoning effort is valid (low, medium, high)
+ effort := strings.ToLower(agent.ReasoningEffort)
+ if effort != "low" && effort != "medium" && effort != "high" {
+ logging.Warn("invalid reasoning effort, setting to medium",
+ "agent", name,
+ "model", agent.Model,
+ "reasoning_effort", agent.ReasoningEffort)
+
+ // Update the agent with valid reasoning effort
+ updatedAgent := cfg.Agents[name]
+ updatedAgent.ReasoningEffort = "medium"
+ cfg.Agents[name] = updatedAgent
+ }
+ }
+ } else if !model.CanReason && agent.ReasoningEffort != "" {
+ // Model doesn't support reasoning but reasoning effort is set
+ logging.Warn("model doesn't support reasoning but reasoning effort is set, ignoring",
+ "agent", name,
+ "model", agent.Model,
+ "reasoning_effort", agent.ReasoningEffort)
+
+ // Update the agent to remove reasoning effort
+ updatedAgent := cfg.Agents[name]
+ updatedAgent.ReasoningEffort = ""
+ cfg.Agents[name] = updatedAgent
+ }
+ }
+
+ // Validate providers
+ for provider, providerCfg := range cfg.Providers {
+ if providerCfg.APIKey == "" && !providerCfg.Disabled {
+ logging.Warn("provider has no API key, marking as disabled", "provider", provider)
+ providerCfg.Disabled = true
+ cfg.Providers[provider] = providerCfg
+ }
+ }
+
+ // Validate LSP configurations
+ for language, lspConfig := range cfg.LSP {
+ if lspConfig.Command == "" && !lspConfig.Disabled {
+ logging.Warn("LSP configuration has no command, marking as disabled", "language", language)
+ lspConfig.Disabled = true
+ cfg.LSP[language] = lspConfig
+ }
+ }
+
+ return nil
+}
+
+// getProviderAPIKey gets the API key for a provider from environment variables
+func getProviderAPIKey(provider models.ModelProvider) string {
+ switch provider {
+ case models.ProviderAnthropic:
+ return os.Getenv("ANTHROPIC_API_KEY")
+ case models.ProviderOpenAI:
+ return os.Getenv("OPENAI_API_KEY")
+ case models.ProviderGemini:
+ return os.Getenv("GEMINI_API_KEY")
+ case models.ProviderGROQ:
+ return os.Getenv("GROQ_API_KEY")
+ case models.ProviderBedrock:
+ if hasAWSCredentials() {
+ return "aws-credentials-available"
+ }
+ }
+ return ""
+}
+
+// setDefaultModelForAgent sets a default model for an agent based on available providers
+func setDefaultModelForAgent(agent AgentName) bool {
+ // Check providers in order of preference
+ if apiKey := os.Getenv("ANTHROPIC_API_KEY"); apiKey != "" {
+ maxTokens := int64(5000)
+ if agent == AgentTitle {
+ maxTokens = 80
+ }
+ cfg.Agents[agent] = Agent{
+ Model: models.Claude37Sonnet,
+ MaxTokens: maxTokens,
+ }
+ return true
+ }
+
+ if apiKey := os.Getenv("OPENAI_API_KEY"); apiKey != "" {
+ var model models.ModelID
+ maxTokens := int64(5000)
+ reasoningEffort := ""
+
+ switch agent {
+ case AgentTitle:
+ model = models.GPT41Mini
+ maxTokens = 80
+ case AgentTask:
+ model = models.GPT41Mini
+ default:
+ model = models.GPT41
+ }
+
+ // Check if model supports reasoning
+ if modelInfo, ok := models.SupportedModels[model]; ok && modelInfo.CanReason {
+ reasoningEffort = "medium"
+ }
+
+ cfg.Agents[agent] = Agent{
+ Model: model,
+ MaxTokens: maxTokens,
+ ReasoningEffort: reasoningEffort,
+ }
+ return true
+ }
+
+ if apiKey := os.Getenv("GEMINI_API_KEY"); apiKey != "" {
+ var model models.ModelID
+ maxTokens := int64(5000)
+
+ if agent == AgentTitle {
+ model = models.Gemini25Flash
+ maxTokens = 80
+ } else {
+ model = models.Gemini25
+ }
+
+ cfg.Agents[agent] = Agent{
+ Model: model,
+ MaxTokens: maxTokens,
+ }
+ return true
+ }
+
+ if apiKey := os.Getenv("GROQ_API_KEY"); apiKey != "" {
+ maxTokens := int64(5000)
+ if agent == AgentTitle {
+ maxTokens = 80
+ }
+
+ cfg.Agents[agent] = Agent{
+ Model: models.QWENQwq,
+ MaxTokens: maxTokens,
+ }
+ return true
+ }
+
+ if hasAWSCredentials() {
+ maxTokens := int64(5000)
+ if agent == AgentTitle {
+ maxTokens = 80
+ }
+
+ cfg.Agents[agent] = Agent{
+ Model: models.BedrockClaude37Sonnet,
+ MaxTokens: maxTokens,
+ ReasoningEffort: "medium", // Claude models support reasoning
+ }
+ return true
+ }
+
+ return false
+}
+
// Get returns the current configuration.
// It's safe to call this function multiple times.
func Get() *Config {
@@ -471,7 +471,7 @@ func createAgentProvider(agentName config.AgentName) (provider.Provider, error)
provider.WithReasoningEffort(agentConfig.ReasoningEffort),
),
)
- } else if model.Provider == models.ProviderAnthropic && model.CanReason {
+ } else if model.Provider == models.ProviderAnthropic && model.CanReason && agentName == config.AgentCoder {
opts = append(
opts,
provider.WithAnthropicOptions(
@@ -196,11 +196,16 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
content,
filePath,
)
+ rootDir := config.WorkingDirectory()
+ permissionPath := filepath.Dir(filePath)
+ if strings.HasPrefix(filePath, rootDir) {
+ permissionPath = rootDir
+ }
p := e.permissions.Request(
permission.CreatePermissionRequest{
- Path: filepath.Dir(filePath),
+ Path: permissionPath,
ToolName: EditToolName,
- Action: "create",
+ Action: "write",
Description: fmt.Sprintf("Create file %s", filePath),
Params: EditPermissionsParams{
FilePath: filePath,
@@ -301,11 +306,16 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
filePath,
)
+ rootDir := config.WorkingDirectory()
+ permissionPath := filepath.Dir(filePath)
+ if strings.HasPrefix(filePath, rootDir) {
+ permissionPath = rootDir
+ }
p := e.permissions.Request(
permission.CreatePermissionRequest{
- Path: filepath.Dir(filePath),
+ Path: permissionPath,
ToolName: EditToolName,
- Action: "delete",
+ Action: "write",
Description: fmt.Sprintf("Delete content from file %s", filePath),
Params: EditPermissionsParams{
FilePath: filePath,
@@ -415,11 +425,16 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
newContent,
filePath,
)
+ rootDir := config.WorkingDirectory()
+ permissionPath := filepath.Dir(filePath)
+ if strings.HasPrefix(filePath, rootDir) {
+ permissionPath = rootDir
+ }
p := e.permissions.Request(
permission.CreatePermissionRequest{
- Path: filepath.Dir(filePath),
+ Path: permissionPath,
ToolName: EditToolName,
- Action: "replace",
+ Action: "write",
Description: fmt.Sprintf("Replace content in file %s", filePath),
Params: EditPermissionsParams{
FilePath: filePath,
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"path/filepath"
+ "strings"
"time"
"github.com/kujtimiihoxha/opencode/internal/config"
@@ -159,11 +160,17 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
params.Content,
filePath,
)
+
+ rootDir := config.WorkingDirectory()
+ permissionPath := filepath.Dir(filePath)
+ if strings.HasPrefix(filePath, rootDir) {
+ permissionPath = rootDir
+ }
p := w.permissions.Request(
permission.CreatePermissionRequest{
- Path: filePath,
+ Path: permissionPath,
ToolName: WriteToolName,
- Action: "create",
+ Action: "write",
Description: fmt.Sprintf("Create file %s", filePath),
Params: WritePermissionsParams{
FilePath: filePath,
@@ -2,10 +2,12 @@ package permission
import (
"errors"
+ "path/filepath"
"sync"
"time"
"github.com/google/uuid"
+ "github.com/kujtimiihoxha/opencode/internal/config"
"github.com/kujtimiihoxha/opencode/internal/pubsub"
)
@@ -67,9 +69,13 @@ func (s *permissionService) Deny(permission PermissionRequest) {
}
func (s *permissionService) Request(opts CreatePermissionRequest) bool {
+ dir := filepath.Dir(opts.Path)
+ if dir == "." {
+ dir = config.WorkingDirectory()
+ }
permission := PermissionRequest{
ID: uuid.New().String(),
- Path: opts.Path,
+ Path: dir,
ToolName: opts.ToolName,
Description: opts.Description,
Action: opts.Action,
@@ -77,7 +83,7 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
}
for _, p := range s.sessionPermissions {
- if p.ToolName == permission.ToolName && p.Action == permission.Action {
+ if p.ToolName == permission.ToolName && p.Action == permission.Action && p.SessionID == permission.SessionID && p.Path == permission.Path {
return true
}
}
@@ -57,8 +57,8 @@ var returnKey = key.NewBinding(
)
var logsKeyReturnKey = key.NewBinding(
- key.WithKeys("backspace"),
- key.WithHelp("backspace", "go back"),
+ key.WithKeys("backspace", "q"),
+ key.WithHelp("backspace/q", "go back"),
)
type appModel struct {
@@ -5,7 +5,7 @@ import "runtime/debug"
// Build-time parameters set via -ldflags
var Version = "unknown"
-// A user may install pug using `go install github.com/leg100/pug@latest`
+// A user may install pug using `go install github.com/kujtimiihoxha/opencode@latest`.
// without -ldflags, in which case the version above is unset. As a workaround
// we use the embedded build version that *is* set when using `go install` (and
// is only set for `go install` and not for `go build`).
@@ -0,0 +1,269 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "definitions": {
+ "agent": {
+ "description": "Agent configuration",
+ "properties": {
+ "maxTokens": {
+ "description": "Maximum tokens for the agent",
+ "minimum": 1,
+ "type": "integer"
+ },
+ "model": {
+ "description": "Model ID for the agent",
+ "enum": [
+ "gemini-2.0-flash",
+ "bedrock.claude-3.7-sonnet",
+ "claude-3-opus",
+ "claude-3.5-sonnet",
+ "gpt-4o-mini",
+ "o1",
+ "o3-mini",
+ "o1-pro",
+ "o4-mini",
+ "claude-3-haiku",
+ "gpt-4o",
+ "o3",
+ "gpt-4.1-mini",
+ "gpt-4.5-preview",
+ "gemini-2.5-flash",
+ "claude-3.5-haiku",
+ "gpt-4.1",
+ "gemini-2.0-flash-lite",
+ "claude-3.7-sonnet",
+ "o1-mini",
+ "gpt-4.1-nano",
+ "gemini-2.5"
+ ],
+ "type": "string"
+ },
+ "reasoningEffort": {
+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
+ "enum": [
+ "low",
+ "medium",
+ "high"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "model"
+ ],
+ "type": "object"
+ }
+ },
+ "description": "Configuration schema for the OpenCode application",
+ "properties": {
+ "agents": {
+ "additionalProperties": {
+ "description": "Agent configuration",
+ "properties": {
+ "maxTokens": {
+ "description": "Maximum tokens for the agent",
+ "minimum": 1,
+ "type": "integer"
+ },
+ "model": {
+ "description": "Model ID for the agent",
+ "enum": [
+ "gemini-2.0-flash",
+ "bedrock.claude-3.7-sonnet",
+ "claude-3-opus",
+ "claude-3.5-sonnet",
+ "gpt-4o-mini",
+ "o1",
+ "o3-mini",
+ "o1-pro",
+ "o4-mini",
+ "claude-3-haiku",
+ "gpt-4o",
+ "o3",
+ "gpt-4.1-mini",
+ "gpt-4.5-preview",
+ "gemini-2.5-flash",
+ "claude-3.5-haiku",
+ "gpt-4.1",
+ "gemini-2.0-flash-lite",
+ "claude-3.7-sonnet",
+ "o1-mini",
+ "gpt-4.1-nano",
+ "gemini-2.5"
+ ],
+ "type": "string"
+ },
+ "reasoningEffort": {
+ "description": "Reasoning effort for models that support it (OpenAI, Anthropic)",
+ "enum": [
+ "low",
+ "medium",
+ "high"
+ ],
+ "type": "string"
+ }
+ },
+ "required": [
+ "model"
+ ],
+ "type": "object"
+ },
+ "description": "Agent configurations",
+ "properties": {
+ "coder": {
+ "$ref": "#/definitions/agent"
+ },
+ "task": {
+ "$ref": "#/definitions/agent"
+ },
+ "title": {
+ "$ref": "#/definitions/agent"
+ }
+ },
+ "type": "object"
+ },
+ "data": {
+ "description": "Storage configuration",
+ "properties": {
+ "directory": {
+ "default": ".opencode",
+ "description": "Directory where application data is stored",
+ "type": "string"
+ }
+ },
+ "required": [
+ "directory"
+ ],
+ "type": "object"
+ },
+ "debug": {
+ "default": false,
+ "description": "Enable debug mode",
+ "type": "boolean"
+ },
+ "debugLSP": {
+ "default": false,
+ "description": "Enable LSP debug mode",
+ "type": "boolean"
+ },
+ "lsp": {
+ "additionalProperties": {
+ "description": "LSP configuration for a language",
+ "properties": {
+ "args": {
+ "description": "Command arguments for the LSP server",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "command": {
+ "description": "Command to execute for the LSP server",
+ "type": "string"
+ },
+ "disabled": {
+ "default": false,
+ "description": "Whether the LSP is disabled",
+ "type": "boolean"
+ },
+ "options": {
+ "description": "Additional options for the LSP server",
+ "type": "object"
+ }
+ },
+ "required": [
+ "command"
+ ],
+ "type": "object"
+ },
+ "description": "Language Server Protocol configurations",
+ "type": "object"
+ },
+ "mcpServers": {
+ "additionalProperties": {
+ "description": "MCP server configuration",
+ "properties": {
+ "args": {
+ "description": "Command arguments for the MCP server",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "command": {
+ "description": "Command to execute for the MCP server",
+ "type": "string"
+ },
+ "env": {
+ "description": "Environment variables for the MCP server",
+ "items": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "headers": {
+ "additionalProperties": {
+ "type": "string"
+ },
+ "description": "HTTP headers for SSE type MCP servers",
+ "type": "object"
+ },
+ "type": {
+ "default": "stdio",
+ "description": "Type of MCP server",
+ "enum": [
+ "stdio",
+ "sse"
+ ],
+ "type": "string"
+ },
+ "url": {
+ "description": "URL for SSE type MCP servers",
+ "type": "string"
+ }
+ },
+ "required": [
+ "command"
+ ],
+ "type": "object"
+ },
+ "description": "Model Control Protocol server configurations",
+ "type": "object"
+ },
+ "providers": {
+ "additionalProperties": {
+ "description": "Provider configuration",
+ "properties": {
+ "apiKey": {
+ "description": "API key for the provider",
+ "type": "string"
+ },
+ "disabled": {
+ "default": false,
+ "description": "Whether the provider is disabled",
+ "type": "boolean"
+ },
+ "provider": {
+ "description": "Provider type",
+ "enum": [
+ "anthropic",
+ "openai",
+ "gemini",
+ "groq",
+ "bedrock"
+ ],
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "description": "LLM provider configurations",
+ "type": "object"
+ },
+ "wd": {
+ "description": "Working directory for the application",
+ "type": "string"
+ }
+ },
+ "title": "OpenCode Configuration",
+ "type": "object"
+}