Skip to content

Commit 5dee17d

Browse files
committed
feat: functional qwen-code integration
- bug: zod error, seems internal to qwen
1 parent bc9be58 commit 5dee17d

19 files changed

Lines changed: 1279 additions & 37 deletions

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Download the latest binary for your platform from the [releases page](https://gi
3636
# If a fresh installation (configures default provider, default MCP servers, etc)
3737
opun setup
3838

39-
# Initialize a chat session with the default provider -- or, specify the provider (chat {gemini,claude})
39+
# Initialize a chat session with the default provider -- or, specify the provider (chat {gemini,claude,qwen})
4040
opun chat
4141

4242
# Add a workflow, tool or prompt -- this is interactive, no need for flags (--{prompt,workflow} --path --name)
@@ -123,7 +123,7 @@ agents:
123123
# First agent: Initial code analysis
124124
- id: analyzer # Unique ID for referencing this agent
125125
name: "Code Analyzer" # Human-readable name displayed during execution
126-
provider: claude # AI provider: claude or gemini
126+
provider: claude # AI provider: claude, gemini, or qwen
127127
model: sonnet # Model variant (provider-specific)
128128

129129
# The prompt is the instruction sent to the AI agent
@@ -446,6 +446,7 @@ command: "rg --type-add 'code:*.{js,ts,go,py,java,rs,cpp,c,h}' -t code"
446446
providers:
447447
- claude
448448
- gemini
449+
- qwen
449450
450451
---
451452
@@ -601,6 +602,7 @@ export MAX_FILE_SIZE="1000000"
601602
# Provider-specific settings
602603
export CLAUDE_MODEL="sonnet"
603604
export GEMINI_TEMPERATURE="0.7"
605+
export QWEN_MODEL="code"
604606
```
605607

606608
**Using in Configurations**:

internal/cli/chat.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ func runChat(cmd *cobra.Command, provider string, providerArgs []string) error {
8787
command = "claude"
8888
case "gemini":
8989
command = "gemini"
90+
case "qwen":
91+
command = "qwen"
9092
default:
9193
return fmt.Errorf("unsupported provider: %s", provider)
9294
}

internal/cli/chat_cmd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ func ChatCmd() *cobra.Command {
1313
cmd := &cobra.Command{
1414
Use: "chat [provider] [-- additional-args...]",
1515
Short: "Start an interactive chat session with an AI provider",
16-
Long: `Start an interactive chat session with Claude or Gemini.
16+
Long: `Start an interactive chat session with Claude, Gemini, or Qwen.
1717
1818
If no provider is specified, uses the default provider from your configuration.
1919
Your promptgarden prompts and configured slash commands are available through the injection system.
@@ -24,8 +24,10 @@ Examples:
2424
opun chat # Use default provider
2525
opun chat claude # Chat with Claude
2626
opun chat gemini # Chat with Gemini
27+
opun chat qwen # Chat with Qwen Code
2728
opun chat claude -- --continue # Chat with Claude using --continue flag
2829
opun chat gemini -- --model=pro # Chat with Gemini using specific model
30+
opun chat qwen -- --model=code # Chat with Qwen using specific model
2931
opun chat -- --continue # Use default provider with --continue flag`,
3032
Args: cobra.MinimumNArgs(0),
3133
RunE: func(cmd *cobra.Command, args []string) error {
@@ -36,6 +38,7 @@ Examples:
3638
knownProviders := map[string]bool{
3739
"claude": true,
3840
"gemini": true,
41+
"qwen": true,
3942
}
4043

4144
// Parse provider and additional arguments

internal/cli/chat_windows.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,13 +88,21 @@ func runChat(cmd *cobra.Command, provider string, providerArgs []string) error {
8888
} else {
8989
return fmt.Errorf("gemini command not found, please install Gemini CLI")
9090
}
91+
case "qwen":
92+
if _, err := exec.LookPath("qwen"); err == nil {
93+
command = "qwen"
94+
} else if _, err := exec.LookPath("qwen.exe"); err == nil {
95+
command = "qwen.exe"
96+
} else {
97+
return fmt.Errorf("qwen command not found, please install Qwen Code CLI")
98+
}
9199
default:
92100
return fmt.Errorf("unsupported provider: %s", provider)
93101
}
94102

95103
// Append provider arguments to command arguments
96104
commandArgs = append(commandArgs, providerArgs...)
97-
105+
98106
// Create command with prepared environment and provider arguments
99107
// #nosec G204 -- command is hardcoded based on provider type
100108
c := exec.Command(command, commandArgs...)

internal/cli/mcp.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ To use with Gemini, add this to ~/.gemini/settings.json:
130130
}
131131
}`,
132132
RunE: func(cmd *cobra.Command, args []string) error {
133+
// Set environment variable to suppress warnings that could interfere with JSON-RPC
134+
os.Setenv("OPUN_MCP_STDIO", "1")
133135
// Initialize components
134136
home, err := os.UserHomeDir()
135137
if err != nil {
@@ -140,8 +142,8 @@ To use with Gemini, add this to ~/.gemini/settings.json:
140142
gardenPath := filepath.Join(home, ".opun", "promptgarden")
141143
garden, err := promptgarden.NewGarden(gardenPath)
142144
if err != nil {
143-
// Log to stderr since stdout is used for MCP protocol
144-
fmt.Fprintf(os.Stderr, "Warning: failed to initialize prompt garden: %v\n", err)
145+
// Suppress warnings in stdio mode - they interfere with JSON-RPC protocol
146+
// fmt.Fprintf(os.Stderr, "Warning: failed to initialize prompt garden: %v\n", err)
145147
}
146148

147149
// Initialize command registry
@@ -155,16 +157,16 @@ To use with Gemini, add this to ~/.gemini/settings.json:
155157
workflowPath := filepath.Join(home, ".opun", "workflows")
156158
workflowMgr, err := workflow.NewManager(workflowPath)
157159
if err != nil {
158-
// Log to stderr
159-
fmt.Fprintf(os.Stderr, "Warning: failed to initialize workflow manager: %v\n", err)
160+
// Suppress warnings in stdio mode - they interfere with JSON-RPC protocol
161+
// fmt.Fprintf(os.Stderr, "Warning: failed to initialize workflow manager: %v\n", err)
160162
}
161163

162164
// Initialize tool registry
163165
toolsPath := filepath.Join(home, ".opun", "tools")
164166
toolLoader := tools.NewLoader(toolsPath)
165167
if err := toolLoader.LoadAll(); err != nil {
166-
// Log to stderr
167-
fmt.Fprintf(os.Stderr, "Warning: failed to load tools: %v\n", err)
168+
// Suppress warnings in stdio mode - they interfere with JSON-RPC protocol
169+
// fmt.Fprintf(os.Stderr, "Warning: failed to load tools: %v\n", err)
168170
}
169171
toolRegistry := toolLoader.GetRegistry()
170172

internal/cli/root.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func RootCmd() *cobra.Command {
3636
rootCmd := &cobra.Command{
3737
Use: "opun",
3838
Short: "AI code agent automation framework",
39-
Long: `Opun automates interaction with AI code agents (Claude Code and Gemini CLI)
39+
Long: `Opun automates interaction with AI code agents (Claude Code, Gemini CLI, and Qwen Code)
4040
by managing their interactive sessions and providing workflow orchestration.`,
4141
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
4242
return initConfig(configFile)
@@ -157,7 +157,7 @@ Use "{{.CommandPath}} [command] --help" for more information about a command.
157157

158158
// GetCustomHelp returns the formatted help text for display
159159
func GetCustomHelp() string {
160-
return `Opun automates interaction with AI code agents (Claude Code and Gemini CLI)
160+
return `Opun automates interaction with AI code agents (Claude Code, Gemini CLI, and Qwen Code)
161161
by managing their interactive sessions and providing workflow orchestration.
162162
163163
Usage:

internal/cli/setup.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ func selectProvider() (string, error) {
238238
description: "Google's Gemini - powerful multimodal AI",
239239
value: "gemini",
240240
},
241+
providerItem{
242+
name: "Qwen",
243+
description: "Qwen Code - optimized for coding tasks",
244+
value: "qwen",
245+
},
241246
}
242247

243248
// Calculate height to show all items with sufficient space
@@ -336,11 +341,12 @@ func installMCPServersShared(serverNames []string, provider string) error {
336341

337342
// Sync configuration to all providers
338343
providers := []string{provider}
339-
// Also sync to the other provider if user might use both
340-
if provider == "claude" {
341-
providers = append(providers, "gemini")
342-
} else {
343-
providers = append(providers, "claude")
344+
// Also sync to the other providers if user might use them
345+
allProviders := []string{"claude", "gemini", "qwen"}
346+
for _, p := range allProviders {
347+
if p != provider {
348+
providers = append(providers, p)
349+
}
344350
}
345351

346352
if err := installer.SyncConfigurations(providers); err != nil {

internal/config/injection_manager.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ func (m *InjectionManager) PrepareProviderEnvironment(provider string) (*Provide
8989
if err := m.prepareGeminiEnvironment(env); err != nil {
9090
return nil, fmt.Errorf("failed to prepare Gemini environment: %w", err)
9191
}
92+
case "qwen":
93+
if err := m.prepareQwenEnvironment(env); err != nil {
94+
return nil, fmt.Errorf("failed to prepare Qwen environment: %w", err)
95+
}
9296
default:
9397
return nil, fmt.Errorf("unsupported provider: %s", provider)
9498
}
@@ -155,6 +159,23 @@ func (m *InjectionManager) prepareGeminiEnvironment(env *ProviderEnvironment) er
155159
return nil
156160
}
157161

162+
// prepareQwenEnvironment prepares Qwen-specific environment
163+
func (m *InjectionManager) prepareQwenEnvironment(env *ProviderEnvironment) error {
164+
// Since Qwen is a fork of Gemini, it has similar limitations
165+
// We rely entirely on MCP servers for extensions
166+
167+
// Create QWEN.md for system prompt customization in workspace
168+
qwenMdPath := filepath.Join(m.workspaceDir, "QWEN.md")
169+
if err := m.generateQwenSystemPrompt(qwenMdPath); err != nil {
170+
return err
171+
}
172+
173+
// Ensure MCP servers include our slash command server
174+
// This is already handled by SyncToProvider
175+
176+
return nil
177+
}
178+
158179
// generateClaudeSlashCommands generates markdown files for Claude slash commands
159180
func (m *InjectionManager) generateClaudeSlashCommands(commandsDir string) error {
160181
commands := m.sharedManager.GetSlashCommands()
@@ -388,6 +409,70 @@ This is a managed Opun session with the following MCP servers available:
388409
return t.Execute(file, data)
389410
}
390411

412+
// generateQwenSystemPrompt generates QWEN.md for system customization
413+
func (m *InjectionManager) generateQwenSystemPrompt(mdPath string) error {
414+
tmpl := `# QWEN.md
415+
416+
This file provides system-level guidance for Qwen Code CLI when working in this Opun session.
417+
418+
## Available Commands via MCP
419+
420+
Since Qwen doesn't support custom slash commands natively, use the MCP tools to access Opun functionality:
421+
422+
### Workflows
423+
{{range .Commands}}{{if eq .Type "workflow"}}
424+
- **{{.Name}}**: {{.Description}}
425+
- Handler: {{.Handler}}
426+
{{end}}{{end}}
427+
428+
### Prompts
429+
{{range .Commands}}{{if eq .Type "prompt"}}
430+
- **{{.Name}}**: {{.Description}}
431+
- Handler: {{.Handler}}
432+
{{end}}{{end}}
433+
434+
### Built-in Commands
435+
{{range .Commands}}{{if eq .Type "builtin"}}
436+
- **{{.Name}}**: {{.Description}}
437+
{{end}}{{end}}
438+
439+
## Using Commands
440+
441+
To execute any of these commands, use the MCP tools:
442+
1. List available tools with the MCP server
443+
2. Execute the desired command through the opun tool
444+
3. For prompts, use the opun tool
445+
446+
## Session Configuration
447+
448+
This is a managed Opun session with the following MCP servers available:
449+
{{range .Servers}}{{if .Installed}}
450+
- **{{.Name}}**: {{.Package}}
451+
{{end}}{{end}}
452+
`
453+
454+
t, err := template.New("qwen").Parse(tmpl)
455+
if err != nil {
456+
return err
457+
}
458+
459+
data := struct {
460+
Commands []core.SharedSlashCommand
461+
Servers []core.SharedMCPServer
462+
}{
463+
Commands: m.sharedManager.GetSlashCommands(),
464+
Servers: m.sharedManager.GetMCPServers(),
465+
}
466+
467+
file, err := os.Create(mdPath)
468+
if err != nil {
469+
return err
470+
}
471+
defer file.Close()
472+
473+
return t.Execute(file, data)
474+
}
475+
391476
// ProviderEnvironment contains the prepared environment for a provider
392477
type ProviderEnvironment struct {
393478
Provider string

0 commit comments

Comments
 (0)