opencode-claude-max-proxy 1.0.2 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Extract SDK AgentDefinition objects from OpenCode's Task tool description.
3
+ *
4
+ * OpenCode (via oh-my-opencode or other frameworks) sends a Task tool with
5
+ * descriptions of each available agent. We parse these and convert them into
6
+ * Claude Agent SDK `AgentDefinition` objects so the SDK's native Task handler
7
+ * routes to properly-configured subagents.
8
+ *
9
+ * This means whatever agents the user configures in their framework
10
+ * automatically become available as SDK subagents — with descriptions,
11
+ * model tiers, and tool access.
12
+ */
13
+
14
+ /** SDK-compatible agent definition */
15
+ export interface AgentDefinition {
16
+ description: string
17
+ prompt: string
18
+ model?: "sonnet" | "opus" | "haiku" | "inherit"
19
+ tools?: string[]
20
+ disallowedTools?: string[]
21
+ }
22
+
23
+ /**
24
+ * Parse agent entries from the Task tool description text.
25
+ *
26
+ * Expected format (from OpenCode):
27
+ * - agent-name: Description of what the agent does
28
+ *
29
+ * @returns Map of agent name → description
30
+ */
31
+ export function parseAgentDescriptions(taskDescription: string): Map<string, string> {
32
+ const agents = new Map<string, string>()
33
+
34
+ const agentSection = taskDescription.match(
35
+ /Available agent types.*?:\n((?:- [\w][\w-]*:.*\n?)+)/s
36
+ )
37
+ if (!agentSection) return agents
38
+
39
+ const entries = agentSection[1]!.matchAll(/^- ([\w][\w-]*):\s*(.+)/gm)
40
+ for (const match of entries) {
41
+ agents.set(match[1]!, match[2]!.trim())
42
+ }
43
+
44
+ return agents
45
+ }
46
+
47
+ /**
48
+ * Map an OpenCode model string to an SDK model tier.
49
+ *
50
+ * The SDK only accepts 'sonnet' | 'opus' | 'haiku' | 'inherit'.
51
+ * We map based on the model name pattern, defaulting to 'inherit'
52
+ * for non-Anthropic models (they'll use the parent session's model).
53
+ */
54
+ export function mapModelTier(model?: string): "sonnet" | "opus" | "haiku" | "inherit" {
55
+ if (!model) return "inherit"
56
+ const lower = model.toLowerCase()
57
+ if (lower.includes("opus")) return "opus"
58
+ if (lower.includes("haiku")) return "haiku"
59
+ if (lower.includes("sonnet")) return "sonnet"
60
+ return "inherit"
61
+ }
62
+
63
+ /**
64
+ * Build SDK AgentDefinition objects from the Task tool description.
65
+ *
66
+ * Each agent gets:
67
+ * - description: from the Task tool text (user-configured)
68
+ * - prompt: instructional prompt incorporating the description
69
+ * - model: 'inherit' (uses parent session model — all requests go through our proxy)
70
+ * - tools: undefined (inherit all tools from parent)
71
+ *
72
+ * @param taskDescription - The full Task tool description text from OpenCode
73
+ * @param mcpToolNames - Optional list of MCP tool names to make available to agents
74
+ */
75
+ export function buildAgentDefinitions(
76
+ taskDescription: string,
77
+ mcpToolNames?: string[]
78
+ ): Record<string, AgentDefinition> {
79
+ const descriptions = parseAgentDescriptions(taskDescription)
80
+ const agents: Record<string, AgentDefinition> = {}
81
+
82
+ for (const [name, description] of descriptions) {
83
+ agents[name] = {
84
+ description,
85
+ prompt: buildAgentPrompt(name, description),
86
+ model: "inherit",
87
+ // Give agents access to MCP tools if provided
88
+ ...(mcpToolNames?.length ? { tools: [...mcpToolNames] } : {}),
89
+ }
90
+ }
91
+
92
+ return agents
93
+ }
94
+
95
+ /**
96
+ * Build a system prompt for an agent based on its name and description.
97
+ */
98
+ function buildAgentPrompt(name: string, description: string): string {
99
+ return `You are the "${name}" agent. ${description}
100
+
101
+ Focus on your specific role and complete the task thoroughly. Return a clear, concise result.`
102
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Fuzzy matching for agent names.
3
+ *
4
+ * When Claude sends an invalid subagent_type, this tries to map it
5
+ * to the closest valid agent name. This is deterministic string matching,
6
+ * not LLM guessing.
7
+ *
8
+ * Matching priority:
9
+ * 1. Exact match (case-insensitive)
10
+ * 2. Known aliases (e.g., "general-purpose" → "general")
11
+ * 3. Prefix match (e.g., "lib" → "librarian")
12
+ * 4. Substring match (e.g., "junior" → "sisyphus-junior")
13
+ * 5. Suffix-stripped match (e.g., "explore-agent" → "explore")
14
+ * 6. Semantic aliases (e.g., "search" → "explore")
15
+ * 7. Fallback: return lowercased original
16
+ */
17
+
18
+ // Known aliases for common SDK mistakes
19
+ const KNOWN_ALIASES: Record<string, string> = {
20
+ "general-purpose": "general",
21
+ "default": "general",
22
+ "code-reviewer": "oracle",
23
+ "reviewer": "oracle",
24
+ "code-review": "oracle",
25
+ "review": "oracle",
26
+ "consultation": "oracle",
27
+ "analyzer": "oracle",
28
+ "debugger": "oracle",
29
+ "search": "explore",
30
+ "grep": "explore",
31
+ "find": "explore",
32
+ "codebase-search": "explore",
33
+ "research": "librarian",
34
+ "docs": "librarian",
35
+ "documentation": "librarian",
36
+ "lookup": "librarian",
37
+ "reference": "librarian",
38
+ "consult": "oracle",
39
+ "architect": "oracle",
40
+ "image-analyzer": "multimodal-looker",
41
+ "image": "multimodal-looker",
42
+ "pdf": "multimodal-looker",
43
+ "visual": "multimodal-looker",
44
+ "planner": "plan",
45
+ "planning": "plan",
46
+ "builder": "build",
47
+ "coder": "build",
48
+ "developer": "build",
49
+ "writer": "build",
50
+ "executor": "build",
51
+ }
52
+
53
+ // Common suffixes to strip
54
+ const STRIP_SUFFIXES = ["-agent", "-tool", "-worker", "-task", " agent", " tool"]
55
+
56
+ export function fuzzyMatchAgentName(input: string, validAgents: string[]): string {
57
+ if (!input) return input
58
+ if (validAgents.length === 0) return input.toLowerCase()
59
+
60
+ const lowered = input.toLowerCase()
61
+
62
+ // 1. Exact match (case-insensitive)
63
+ const exact = validAgents.find(a => a.toLowerCase() === lowered)
64
+ if (exact) return exact
65
+
66
+ // 2. Known aliases
67
+ const alias = KNOWN_ALIASES[lowered]
68
+ if (alias && validAgents.includes(alias)) return alias
69
+
70
+ // 3. Prefix match
71
+ const prefixMatch = validAgents.find(a => a.toLowerCase().startsWith(lowered))
72
+ if (prefixMatch) return prefixMatch
73
+
74
+ // 4. Substring match
75
+ const substringMatch = validAgents.find(a => a.toLowerCase().includes(lowered))
76
+ if (substringMatch) return substringMatch
77
+
78
+ // 5. Suffix-stripped match
79
+ for (const suffix of STRIP_SUFFIXES) {
80
+ if (lowered.endsWith(suffix)) {
81
+ const stripped = lowered.slice(0, -suffix.length)
82
+ const strippedMatch = validAgents.find(a => a.toLowerCase() === stripped)
83
+ if (strippedMatch) return strippedMatch
84
+ }
85
+ }
86
+
87
+ // 6. Reverse substring (input contains a valid agent name)
88
+ const reverseMatch = validAgents.find(a => lowered.includes(a.toLowerCase()))
89
+ if (reverseMatch) return reverseMatch
90
+
91
+ // 7. Fallback
92
+ return lowered
93
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Dynamic MCP tool registration for passthrough mode.
3
+ *
4
+ * In passthrough mode, OpenCode's tools need to be real callable tools
5
+ * (not just text descriptions in the prompt). We create an MCP server
6
+ * that registers each tool from OpenCode's request with the exact
7
+ * name and schema, so Claude generates proper tool_use blocks.
8
+ *
9
+ * Tool handlers are no-ops — the PreToolUse hook blocks execution.
10
+ * We just need the definitions so Claude can call them.
11
+ */
12
+
13
+ import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk"
14
+ import { z } from "zod"
15
+
16
+ export const PASSTHROUGH_MCP_NAME = "oc"
17
+ export const PASSTHROUGH_MCP_PREFIX = `mcp__${PASSTHROUGH_MCP_NAME}__`
18
+
19
+ /**
20
+ * Convert a JSON Schema object to a Zod schema (simplified).
21
+ * Handles the common types OpenCode sends. Falls back to z.any() for complex types.
22
+ */
23
+ function jsonSchemaToZod(schema: any): z.ZodTypeAny {
24
+ if (!schema || typeof schema !== "object") return z.any()
25
+
26
+ if (schema.type === "string") {
27
+ let s = z.string()
28
+ if (schema.description) s = s.describe(schema.description)
29
+ if (schema.enum) return z.enum(schema.enum as [string, ...string[]])
30
+ return s
31
+ }
32
+ if (schema.type === "number" || schema.type === "integer") {
33
+ let n = z.number()
34
+ if (schema.description) n = n.describe(schema.description)
35
+ return n
36
+ }
37
+ if (schema.type === "boolean") return z.boolean()
38
+ if (schema.type === "array") {
39
+ const items = schema.items ? jsonSchemaToZod(schema.items) : z.any()
40
+ return z.array(items)
41
+ }
42
+ if (schema.type === "object" && schema.properties) {
43
+ const shape: Record<string, z.ZodTypeAny> = {}
44
+ const required = new Set(schema.required || [])
45
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
46
+ const zodProp = jsonSchemaToZod(propSchema as any)
47
+ shape[key] = required.has(key) ? zodProp : zodProp.optional()
48
+ }
49
+ return z.object(shape)
50
+ }
51
+
52
+ return z.any()
53
+ }
54
+
55
+ /**
56
+ * Create an MCP server with tool definitions matching OpenCode's request.
57
+ */
58
+ export function createPassthroughMcpServer(
59
+ tools: Array<{ name: string; description?: string; input_schema?: any }>
60
+ ) {
61
+ const server = createSdkMcpServer({ name: PASSTHROUGH_MCP_NAME })
62
+ const toolNames: string[] = []
63
+
64
+ for (const tool of tools) {
65
+ try {
66
+ // Convert OpenCode's JSON Schema to Zod for MCP registration
67
+ const zodSchema = tool.input_schema?.properties
68
+ ? jsonSchemaToZod(tool.input_schema)
69
+ : z.object({})
70
+
71
+ // The raw shape for the tool() call needs to be a record of Zod types
72
+ const shape: Record<string, z.ZodTypeAny> =
73
+ zodSchema instanceof z.ZodObject
74
+ ? (zodSchema as any).shape
75
+ : { input: z.any() }
76
+
77
+ server.instance.tool(
78
+ tool.name,
79
+ tool.description || tool.name,
80
+ shape,
81
+ async () => ({ content: [{ type: "text" as const, text: "passthrough" }] })
82
+ )
83
+ toolNames.push(`${PASSTHROUGH_MCP_PREFIX}${tool.name}`)
84
+ } catch {
85
+ // If schema conversion fails, register with permissive schema
86
+ server.instance.tool(
87
+ tool.name,
88
+ tool.description || tool.name,
89
+ { input: z.string().optional() },
90
+ async () => ({ content: [{ type: "text" as const, text: "passthrough" }] })
91
+ )
92
+ toolNames.push(`${PASSTHROUGH_MCP_PREFIX}${tool.name}`)
93
+ }
94
+ }
95
+
96
+ return { server, toolNames }
97
+ }
98
+
99
+ /**
100
+ * Strip the MCP prefix from a tool name to get the OpenCode tool name.
101
+ * e.g., "mcp__oc__todowrite" → "todowrite"
102
+ */
103
+ export function stripMcpPrefix(toolName: string): string {
104
+ if (toolName.startsWith(PASSTHROUGH_MCP_PREFIX)) {
105
+ return toolName.slice(PASSTHROUGH_MCP_PREFIX.length)
106
+ }
107
+ return toolName
108
+ }