Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 111 additions & 19 deletions cmd/non_interactive_mode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -80,11 +81,84 @@ func filterTools(allTools []tools.BaseTool, allowedTools, excludedTools []string
return filteredTools
}

// toolWrapper wraps tools.BaseTool to implement permission.Tool interface
type toolWrapper struct {
tool tools.BaseTool
}

func (tw *toolWrapper) Info() permission.ToolInfo {
info := tw.tool.Info()
return permission.ToolInfo{
Name: info.Name,
Description: info.Description,
Parameters: info.Parameters,
Required: info.Required,
}
}

func (tw *toolWrapper) Run(ctx context.Context, params permission.ToolCall) (permission.ToolResponse, error) {
toolsParams := tools.ToolCall{
ID: params.ID,
Name: params.Name,
Input: params.Input,
}

result, err := tw.tool.Run(ctx, toolsParams)
if err != nil {
return permission.ToolResponse{}, err
}

return permission.ToolResponse{
Type: string(result.Type),
Content: result.Content,
Metadata: result.Metadata,
IsError: result.IsError,
}, nil
}

// findPermissionTool finds the specified permission prompt tool in the tools list
func findPermissionTool(allTools []tools.BaseTool, permissionToolName string) (tools.BaseTool, string, error) {
// Parse the claude-code format mcp__{server}__{tool} to OpenCode format {server}_{tool}
if !strings.HasPrefix(permissionToolName, "mcp__") {
return nil, "", fmt.Errorf("invalid permission prompt tool format: %s (expected: mcp__{server}__{tool})", permissionToolName)
}

// Remove "mcp__" prefix and convert "__" to "_"
parsed := strings.TrimPrefix(permissionToolName, "mcp__")
openCodeToolName := strings.Replace(parsed, "__", "_", 1)

// Find the permission tool
var permissionTool tools.BaseTool
var availableMCPTools []string

for _, tool := range allTools {
toolInfo := tool.Info()
// Check if this is an MCP tool (contains underscore indicating server_tool format)
if strings.Contains(toolInfo.Name, "_") {
availableMCPTools = append(availableMCPTools, "mcp__"+strings.Replace(toolInfo.Name, "_", "__", 1))
if toolInfo.Name == openCodeToolName {
permissionTool = tool
}
}
}

if permissionTool == nil {
if len(availableMCPTools) == 0 {
return nil, "", fmt.Errorf("MCP tool %s (passed via --permission-prompt-tool) not found. Available MCP tools: none", permissionToolName)
}
return nil, "", fmt.Errorf("MCP tool %s (passed via --permission-prompt-tool) not found. Available MCP tools: %s",
permissionToolName, strings.Join(availableMCPTools, ", "))
}

slog.Info("Found permission prompt tool", "tool", permissionTool.Info().Name)
return permissionTool, openCodeToolName, nil
}

// handleNonInteractiveMode processes a single prompt in non-interactive mode
func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat format.OutputFormat, quiet bool, verbose bool, allowedTools, excludedTools []string) error {
func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat format.OutputFormat, quiet bool, verbose bool, allowedTools, excludedTools []string, permissionPromptTool string) error {
// Initial log message using standard slog
slog.Info("Running in non-interactive mode", "prompt", prompt, "format", outputFormat, "quiet", quiet, "verbose", verbose,
"allowedTools", allowedTools, "excludedTools", excludedTools)
"allowedTools", allowedTools, "excludedTools", excludedTools, "permissionPromptTool", permissionPromptTool)

// Sanity check for mutually exclusive flags
if quiet && verbose {
Expand Down Expand Up @@ -161,8 +235,40 @@ func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat f
// Set the session as current
app.CurrentSession = &session

// Auto-approve all permissions for this session
permission.AutoApproveSession(ctx, session.ID)
// Initialize MCP tools synchronously in non-interactive mode (if any are configured)
mcpServers := config.Get().MCPServers
if len(mcpServers) > 0 {
mcpCtx, mcpCancel := context.WithTimeout(ctx, 10*time.Second)
agent.GetMcpTools(mcpCtx, app.Permissions)
mcpCancel()
}

// Get all tools including MCP tools
allTools := agent.PrimaryAgentTools(
app.Permissions,
app.Sessions,
app.Messages,
app.History,
app.LSPClients,
)

// Handle permission prompt tool setup
if permissionPromptTool != "" {
// Find the permission tool
permissionTool, openCodeToolName, err := findPermissionTool(allTools, permissionPromptTool)
if err != nil {
return err
}

// Store the permission tool for this session (wrapped to match interface)
permission.SetPermissionPromptTool(ctx, session.ID, &toolWrapper{tool: permissionTool})

// Add permission tool to excluded tools so it gets filtered out of LLM tools
excludedTools = append(excludedTools, openCodeToolName)
} else {
// Auto-approve all permissions for this session (current behavior)
permission.AutoApproveSession(ctx, session.ID)
}

// Create the user message
_, err = app.Messages.Create(ctx, session.ID, message.CreateMessageParams{
Expand All @@ -175,21 +281,7 @@ func handleNonInteractiveMode(ctx context.Context, prompt string, outputFormat f

// If tool restrictions are specified, create a new agent with filtered tools
if len(allowedTools) > 0 || len(excludedTools) > 0 {
// Initialize MCP tools synchronously to ensure they're included in filtering
mcpCtx, mcpCancel := context.WithTimeout(ctx, 10*time.Second)
agent.GetMcpTools(mcpCtx, app.Permissions)
mcpCancel()

// Get all available tools including MCP tools
allTools := agent.PrimaryAgentTools(
app.Permissions,
app.Sessions,
app.Messages,
app.History,
app.LSPClients,
)

// Filter tools based on allowed/excluded lists
// Filter tools based on allowed/excluded lists (permission tool automatically excluded if present)
filteredTools := filterTools(allTools, allowedTools, excludedTools)

// Log the filtered tools for debugging
Expand Down
4 changes: 3 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ to assist developers in writing, debugging, and understanding code directly from
// Get tool restriction flags
allowedTools, _ := cmd.Flags().GetStringSlice("allowedTools")
excludedTools, _ := cmd.Flags().GetStringSlice("excludedTools")
permissionPromptTool, _ := cmd.Flags().GetString("permission-prompt-tool")

return handleNonInteractiveMode(cmd.Context(), prompt, outputFormat, quiet, verbose, allowedTools, excludedTools)
return handleNonInteractiveMode(cmd.Context(), prompt, outputFormat, quiet, verbose, allowedTools, excludedTools, permissionPromptTool)
}

// Run LSP auto-discovery
Expand Down Expand Up @@ -352,6 +353,7 @@ func init() {
rootCmd.Flags().BoolP("verbose", "", false, "Display logs to stderr in non-interactive mode")
rootCmd.Flags().StringSlice("allowedTools", nil, "Restrict the agent to only use the specified tools in non-interactive mode (comma-separated list)")
rootCmd.Flags().StringSlice("excludedTools", nil, "Prevent the agent from using the specified tools in non-interactive mode (comma-separated list)")
rootCmd.Flags().String("permission-prompt-tool", "", "MCP tool for handling permission prompts in non-interactive mode")

// Make allowedTools and excludedTools mutually exclusive
rootCmd.MarkFlagsMutuallyExclusive("allowedTools", "excludedTools")
Expand Down
15 changes: 15 additions & 0 deletions internal/llm/models/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
Claude35Haiku ModelID = "claude-3.5-haiku"
Claude3Opus ModelID = "claude-3-opus"
Claude4Sonnet ModelID = "claude-4-sonnet"
Claude4Opus ModelID = "claude-4-opus"
)

// https://docs.anthropic.com/en/docs/about-claude/models/all-models
Expand Down Expand Up @@ -68,6 +69,20 @@ var AnthropicModels = map[ModelID]Model{
CanReason: true,
SupportsAttachments: true,
},
Claude4Opus: {
ID: Claude4Opus,
Name: "Claude 4 Opus",
Provider: ProviderAnthropic,
APIModel: "claude-opus-4-20250514",
CostPer1MIn: 15.0,
CostPer1MInCached: 18.75,
CostPer1MOutCached: 1.50,
CostPer1MOut: 75.0,
ContextWindow: 200000,
DefaultMaxTokens: 32000,
CanReason: true,
SupportsAttachments: true,
},
Claude35Haiku: {
ID: Claude35Haiku,
Name: "Claude 3.5 Haiku",
Expand Down
30 changes: 30 additions & 0 deletions internal/llm/models/bedrock.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const (

// Models
BedrockClaude37Sonnet ModelID = "bedrock.claude-3.7-sonnet"
BedrockClaude4Sonnet ModelID = "bedrock.claude-4.0-sonnet"
BedrockClaude4Opus ModelID = "bedrock.claude-4.0-opus"
)

var BedrockModels = map[ModelID]Model{
Expand All @@ -22,4 +24,32 @@ var BedrockModels = map[ModelID]Model{
CanReason: true,
SupportsAttachments: true,
},
BedrockClaude4Sonnet: {
ID: BedrockClaude4Sonnet,
Name: "Bedrock: Claude 4 Sonnet",
Provider: ProviderBedrock,
APIModel: "anthropic.claude-sonnet-4-20250514-v1:0",
CostPer1MIn: 3.0,
CostPer1MInCached: 3.75,
CostPer1MOutCached: 0.30,
CostPer1MOut: 15.0,
ContextWindow: 200_000,
DefaultMaxTokens: 50_000,
CanReason: true,
SupportsAttachments: true,
},
BedrockClaude4Opus: {
ID: BedrockClaude4Opus,
Name: "Bedrock: Claude 4 Opus",
Provider: ProviderBedrock,
APIModel: "anthropic.claude-opus-4-20250514-v1:0",
CostPer1MIn: 15.0,
CostPer1MInCached: 18.75,
CostPer1MOutCached: 1.50,
CostPer1MOut: 75.0,
ContextWindow: 200_000,
DefaultMaxTokens: 50_000,
CanReason: true,
SupportsAttachments: true,
},
}
Loading