opencode-mega-agent 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +106 -0
  2. package/index.mjs +504 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # opencode-mega-agent
2
+
3
+ OpenCode plugin that connects [MegaRouter](https://megarouter.ai) cloud agents to your editor. Agents are domain-trained specialist AIs (code reviewer, security auditor, debugger, etc.) that can read files, run commands, and search your codebase.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install opencode-mega-agent
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ Add to your `opencode.json`:
14
+
15
+ ```json
16
+ {
17
+ "plugin": ["opencode-mega-agent"],
18
+ "provider": {
19
+ "mega-router": {
20
+ "npm": "@ai-sdk/openai-compatible",
21
+ "options": {
22
+ "baseURL": "https://api.megarouter.ai/v1",
23
+ "apiKey": "sk-mega-..."
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ That's it. The plugin auto-discovers available agents from the API on startup and registers them as native OpenCode subagents. No manual agent or model definitions needed.
31
+
32
+ Restart OpenCode — cloud agents will appear as subagents in the task tool.
33
+
34
+ ## How It Works
35
+
36
+ On startup the plugin:
37
+
38
+ 1. **Discovers agents** — calls `GET /api/v1/agents` to fetch available agents
39
+ 2. **Injects config** — registers each agent as a native subagent (`cfg.agent`) and model (`cfg.provider.models`)
40
+ 3. **Caches manifest** — writes to `.opencode/.mega-router-agents.json` for offline resilience
41
+ 4. **Injects system prompt** — teaches the AI about available agents and how to invoke them
42
+ 5. **Registers fallback tool** — `cloud_agent` tool with local file access via SSE tool loop
43
+
44
+ ## Invocation
45
+
46
+ The AI can invoke agents two ways:
47
+
48
+ ```
49
+ task(subagent_type="code-reviewer", prompt="Review auth.go for security issues")
50
+ ```
51
+
52
+ Or via the fallback tool (if native subagent isn't available):
53
+
54
+ ```
55
+ cloud_agent(agent="code-reviewer", message="Review auth.go for security issues")
56
+ ```
57
+
58
+ ## Available Agents
59
+
60
+ | Agent | Specialty |
61
+ |-------|-----------|
62
+ | code-reviewer | Code review — bugs, security, style |
63
+ | security-reviewer | OWASP security audit |
64
+ | frontend-advisor | Frontend/UX — components, a11y, performance |
65
+ | code-debugger | Root cause debugging |
66
+ | devops-cli | Infrastructure & deployment |
67
+ | doc-writer | Technical documentation |
68
+ | data-analyst | Data analysis & visualization |
69
+ | research-assistant | Deep research with citations |
70
+ | git-assistant | Git workflow — commits, rebases |
71
+ | api-tester | API endpoint testing |
72
+ | office-hours | Product/business brainstorming |
73
+ | creative-writer | Stories, poems, copywriting |
74
+
75
+ ## Offline / Backend Down
76
+
77
+ If the MegaRouter API is unreachable at startup, the plugin loads agents from:
78
+
79
+ 1. **Disk cache** (`.opencode/.mega-router-agents.json`) — from last successful discovery
80
+ 2. **Hardcoded fallback** — built-in list of 12 standard agents
81
+
82
+ ## Configuration
83
+
84
+ | Source | Priority | Notes |
85
+ |--------|----------|-------|
86
+ | `opencode.json` provider.mega-router.options | 1st | Recommended |
87
+ | `MEGAROUTER_BASE_URL` / `MEGAROUTER_API_KEY` env vars | 2nd | Fallback |
88
+
89
+ ## Troubleshooting
90
+
91
+ **"Authentication failed"** — Your API key is invalid or expired. Check `opencode.json` → `provider.mega-router.options.apiKey`.
92
+
93
+ **"Insufficient credits"** — Your MegaRouter balance is too low. Top up at the dashboard.
94
+
95
+ **"Agent not found"** — The agent ID doesn't exist. Check available agents in the system prompt or run `curl $BASE_URL/api/v1/agents`.
96
+
97
+ **Agents not showing up** — Make sure the MegaRouter backend is running and reachable at the configured `baseURL`. Check `.opencode/.mega-router-agents.json` for cached state.
98
+
99
+ ## Development
100
+
101
+ ```bash
102
+ cd plugins/opencode-cloud-agent
103
+ npm install
104
+ npx vitest run # run tests
105
+ npx vitest --watch # watch mode
106
+ ```
package/index.mjs ADDED
@@ -0,0 +1,504 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
3
+ // ── Fallback Agent Registry ─────────────────────────────────────────
4
+
5
+ const FALLBACK_AGENTS = [
6
+ { id: "code-reviewer", description: "Review code for bugs, security issues, race conditions, and style", category: "coding" },
7
+ { id: "security-reviewer", description: "OWASP-trained security auditor — injection, auth bypass, supply chain", category: "security" },
8
+ { id: "frontend-advisor", description: "Frontend/UX specialist — component architecture, a11y, performance", category: "coding" },
9
+ { id: "devops-cli", description: "Infrastructure & deployment — Docker, CI/CD, cloud CLI tools", category: "coding" },
10
+ { id: "code-debugger", description: "Systematic debugging from root cause to verified fix", category: "coding" },
11
+ { id: "doc-writer", description: "Technical writing — READMEs, API docs, architecture guides", category: "productivity" },
12
+ { id: "data-analyst", description: "Data analysis with visualization and statistical methods", category: "research" },
13
+ { id: "research-assistant", description: "Deep research with citations and structured summaries", category: "research" },
14
+ { id: "git-assistant", description: "Git workflow — clean commits, rebases, history analysis", category: "productivity" },
15
+ { id: "api-tester", description: "API endpoint testing with repeatable test cases", category: "coding" },
16
+ { id: "office-hours", description: "YC-style product/business brainstorming", category: "creative" },
17
+ { id: "creative-writer", description: "Creative content — stories, poems, copywriting", category: "creative" },
18
+ ]
19
+
20
+ // ── Config ──────────────────────────────────────────────────────────
21
+
22
+ const MODEL_PREFIX = "agent:"
23
+ const PROVIDER_KEY = "mega-router"
24
+ const DISCOVERY_TIMEOUT = 5_000 // ms — don't block startup too long
25
+ const MAX_TOOL_ROUNDS = 30 // safety limit per invocation
26
+ const TOTAL_TIMEOUT_MS = 5 * 60 * 1000 // 5 min hard cap for entire cloud_agent call
27
+ const CACHE_FILENAME = ".opencode/.mega-router-agents.json"
28
+
29
+ let providerConfigCache = null
30
+ let discoveredAgents = []
31
+ let projectDir = null
32
+
33
+ function resolveConfig() {
34
+ if (providerConfigCache?.[PROVIDER_KEY]?.options) {
35
+ const opts = providerConfigCache[PROVIDER_KEY].options
36
+ const raw = opts.baseURL || opts.baseUrl || ""
37
+ const baseUrl = raw.replace(/\/v1\/?$/, "").replace(/\/$/, "")
38
+ return {
39
+ baseUrl: baseUrl || "http://localhost:8080",
40
+ apiKey: opts.apiKey || "",
41
+ }
42
+ }
43
+ return {
44
+ baseUrl: (process.env.MEGAROUTER_BASE_URL || "http://localhost:8080").replace(/\/$/, ""),
45
+ apiKey: process.env.MEGAROUTER_API_KEY || "",
46
+ }
47
+ }
48
+
49
+ function agentList() {
50
+ return discoveredAgents.length > 0 ? discoveredAgents : FALLBACK_AGENTS
51
+ }
52
+
53
+ // ── Manifest Cache ──────────────────────────────────────────────────
54
+
55
+ async function readCachedManifest(dir) {
56
+ if (!dir) return null
57
+ try {
58
+ const { readFileSync } = await import("fs")
59
+ const { join } = await import("path")
60
+ const raw = readFileSync(join(dir, CACHE_FILENAME), "utf-8")
61
+ const data = JSON.parse(raw)
62
+ if (Array.isArray(data) && data.length > 0 && data[0].id) return data
63
+ } catch { /* no cache or corrupt */ }
64
+ return null
65
+ }
66
+
67
+ async function writeCachedManifest(dir, agents) {
68
+ if (!dir || !agents?.length) return
69
+ try {
70
+ const { writeFileSync, mkdirSync } = await import("fs")
71
+ const { join, dirname } = await import("path")
72
+ const path = join(dir, CACHE_FILENAME)
73
+ mkdirSync(dirname(path), { recursive: true })
74
+ writeFileSync(path, JSON.stringify(agents, null, 2), "utf-8")
75
+ } catch { /* best-effort, non-fatal */ }
76
+ }
77
+
78
+ // ── Agent Discovery ─────────────────────────────────────────────────
79
+
80
+ async function discoverAgents(baseUrl, apiKey) {
81
+ const ac = new AbortController()
82
+ const timer = setTimeout(() => ac.abort(), DISCOVERY_TIMEOUT)
83
+ try {
84
+ const headers = {}
85
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
86
+ const resp = await fetch(`${baseUrl}/api/v1/agents`, {
87
+ headers,
88
+ signal: ac.signal,
89
+ })
90
+ clearTimeout(timer)
91
+ if (!resp.ok) return null
92
+ const data = await resp.json()
93
+ const agents = (data.agents || data || [])
94
+ return agents
95
+ .filter(a => a.id && a.id !== "direct")
96
+ .map(a => ({
97
+ id: a.id,
98
+ name: a.name || a.id,
99
+ description: a.description || "",
100
+ category: a.category || "",
101
+ tags: a.tags || [],
102
+ }))
103
+ } catch {
104
+ clearTimeout(timer)
105
+ return null
106
+ }
107
+ }
108
+
109
+ // ── SSE Parser ──────────────────────────────────────────────────────
110
+
111
+ function parseSSEStream(raw) {
112
+ const chunks = []
113
+ for (const line of raw.split("\n")) {
114
+ if (!line.startsWith("data: ")) continue
115
+ const payload = line.slice(6).trim()
116
+ if (payload === "[DONE]") break
117
+ try { chunks.push(JSON.parse(payload)) } catch { /* skip malformed */ }
118
+ }
119
+ return chunks
120
+ }
121
+
122
+ function extractFromChunks(chunks) {
123
+ let conversationId = ""
124
+ let text = ""
125
+ let toolCall = null
126
+ let finishReason = null
127
+
128
+ for (const chunk of chunks) {
129
+ if (chunk.x_conversation_id) conversationId = chunk.x_conversation_id
130
+ const choice = chunk.choices?.[0]
131
+ if (!choice) continue
132
+ if (choice.delta?.content) text += choice.delta.content
133
+ if (choice.delta?.tool_calls?.length) {
134
+ const tc = choice.delta.tool_calls[0]
135
+ toolCall = { id: tc.id, name: tc.function?.name, arguments: tc.function?.arguments }
136
+ }
137
+ if (choice.finish_reason) finishReason = choice.finish_reason
138
+ }
139
+ return { conversationId, text, toolCall, finishReason }
140
+ }
141
+
142
+ // ── Error Helpers ───────────────────────────────────────────────────
143
+
144
+ function parseAPIError(responseText) {
145
+ try {
146
+ const data = JSON.parse(responseText)
147
+ return data?.error?.message || data?.error || responseText
148
+ } catch {
149
+ return responseText
150
+ }
151
+ }
152
+
153
+ function formatAPIError(status, responseText) {
154
+ const msg = parseAPIError(responseText)
155
+ switch (status) {
156
+ case 401:
157
+ return `Authentication failed: ${msg}. Check your MegaRouter API key in opencode.json provider.mega-router.options.apiKey`
158
+ case 402:
159
+ return `Insufficient credits. ${msg}. Top up your balance to continue using cloud agents.`
160
+ case 404:
161
+ return `Agent not found: ${msg}. Run with a valid agent name — check available agents in the system prompt.`
162
+ case 429:
163
+ return `Rate limited: ${msg}. Too many requests — wait a moment and retry.`
164
+ case 500:
165
+ case 502:
166
+ return `MegaRouter server error (${status}): ${msg}. The backend may be restarting — retry in a few seconds.`
167
+ case 503:
168
+ return `MegaRouter unavailable (503): ${msg}. The service is temporarily down — retry shortly.`
169
+ default:
170
+ return `MegaRouter API error (${status}): ${msg}`
171
+ }
172
+ }
173
+
174
+ // ── Local Tool Executor ─────────────────────────────────────────────
175
+
176
+ async function executeLocalTool(toolName, argsJson, directory) {
177
+ let args
178
+ try { args = JSON.parse(argsJson) } catch { return `Error: invalid JSON arguments for ${toolName}: ${argsJson.slice(0, 200)}` }
179
+
180
+ const { execSync } = await import("child_process")
181
+ const { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } = await import("fs")
182
+ const { join, dirname } = await import("path")
183
+
184
+ const resolve = (p) => {
185
+ if (!p) return directory
186
+ if (p.startsWith("/")) return p
187
+ return join(directory, p)
188
+ }
189
+
190
+ try {
191
+ switch (toolName) {
192
+ case "Read": {
193
+ const target = resolve(args.file_path)
194
+ const stat = statSync(target)
195
+ if (stat.isDirectory()) {
196
+ return readdirSync(target, { withFileTypes: true })
197
+ .map(e => e.isDirectory() ? `${e.name}/` : e.name)
198
+ .join("\n")
199
+ }
200
+ const content = readFileSync(target, "utf-8")
201
+ return content.length > 100_000
202
+ ? content.slice(0, 100_000) + "\n... [truncated at 100 KB]"
203
+ : content
204
+ }
205
+
206
+ case "Bash": {
207
+ const result = execSync(args.command, {
208
+ cwd: directory, timeout: 30_000,
209
+ maxBuffer: 1024 * 1024, encoding: "utf-8",
210
+ stdio: ["pipe", "pipe", "pipe"],
211
+ })
212
+ return (result.length > 100_000
213
+ ? result.slice(0, 100_000) + "\n... [truncated at 100 KB]"
214
+ : result) || "(no output)"
215
+ }
216
+
217
+ case "Glob": {
218
+ const cmd = `find . -path './${args.pattern}' 2>/dev/null | head -200`
219
+ try {
220
+ const r = execSync(cmd, { cwd: directory, timeout: 10_000, maxBuffer: 256 * 1024, encoding: "utf-8" })
221
+ return r.trim() || "(no matches)"
222
+ } catch { return "(no matches)" }
223
+ }
224
+
225
+ case "Grep": {
226
+ const inc = args.include ? `--include='${args.include}'` : ""
227
+ const paths = args.paths?.join(" ") || "."
228
+ const safe = args.pattern.replace(/'/g, "'\\''")
229
+ const cmd = `grep -rn ${inc} '${safe}' ${paths} 2>/dev/null | head -150`
230
+ try {
231
+ return execSync(cmd, { cwd: directory, timeout: 15_000, maxBuffer: 512 * 1024, encoding: "utf-8" }) || "(no matches)"
232
+ } catch (e) { return e.stdout || "(no matches)" }
233
+ }
234
+
235
+ case "Edit": {
236
+ const target = resolve(args.file_path)
237
+ const content = readFileSync(target, "utf-8")
238
+ if (!content.includes(args.old_string)) return `Error: old_string not found in ${args.file_path}. File exists but the search string doesn't match.`
239
+ writeFileSync(target, content.replace(args.old_string, args.new_string), "utf-8")
240
+ return `Edited ${args.file_path}`
241
+ }
242
+
243
+ case "Write": {
244
+ const target = resolve(args.file_path)
245
+ mkdirSync(dirname(target), { recursive: true })
246
+ writeFileSync(target, args.content, "utf-8")
247
+ return `Wrote ${args.file_path}`
248
+ }
249
+
250
+ default:
251
+ return `Tool "${toolName}" is not available in this context. Use Read, Bash, Glob, or Grep.`
252
+ }
253
+ } catch (err) {
254
+ return `Error executing ${toolName}: ${err.message}`
255
+ }
256
+ }
257
+
258
+ // ── Client Tool Definitions ─────────────────────────────────────────
259
+
260
+ const CLIENT_TOOLS = [
261
+ { type: "function", function: { name: "Read", description: "Read file contents or list directory contents", parameters: { type: "object", properties: { file_path: { type: "string", description: "Path to file or directory" } }, required: ["file_path"] } } },
262
+ { type: "function", function: { name: "Bash", description: "Execute bash command in local environment", parameters: { type: "object", properties: { command: { type: "string", description: "Bash command to execute" } }, required: ["command"] } } },
263
+ { type: "function", function: { name: "Glob", description: "Find files matching pattern", parameters: { type: "object", properties: { pattern: { type: "string", description: "Glob pattern like **/*.go" } }, required: ["pattern"] } } },
264
+ { type: "function", function: { name: "Grep", description: "Search code with regex pattern", parameters: { type: "object", properties: { pattern: { type: "string", description: "Regex pattern" }, paths: { type: "array", items: { type: "string" }, description: "Paths to search" }, include: { type: "string", description: "File pattern to include" } }, required: ["pattern"] } } },
265
+ { type: "function", function: { name: "Edit", description: "Replace text in file", parameters: { type: "object", properties: { file_path: { type: "string" }, old_string: { type: "string" }, new_string: { type: "string" } }, required: ["file_path", "old_string", "new_string"] } } },
266
+ { type: "function", function: { name: "Write", description: "Create or overwrite file", parameters: { type: "object", properties: { file_path: { type: "string" }, content: { type: "string" } }, required: ["file_path", "content"] } } },
267
+ ]
268
+
269
+ // ── Cloud Agent Call with Tool Loop ─────────────────────────────────
270
+
271
+ async function callCloudAgent(baseUrl, apiKey, agentName, message, abort, directory) {
272
+ const model = `${MODEL_PREFIX}${agentName}`
273
+ const url = `${baseUrl}/v1/chat/completions`
274
+ const deadline = Date.now() + TOTAL_TIMEOUT_MS
275
+
276
+ let conversationId = ""
277
+ let messages = [{ role: "user", content: message }]
278
+ let fullText = ""
279
+
280
+ for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
281
+ if (abort?.aborted) throw new Error("Aborted by user")
282
+ if (Date.now() > deadline) {
283
+ return fullText || `Cloud agent timed out after ${TOTAL_TIMEOUT_MS / 1000}s. Partial output above (if any).`
284
+ }
285
+
286
+ const headers = { "Content-Type": "application/json" }
287
+ if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`
288
+ if (conversationId) headers["X-Conversation-ID"] = conversationId
289
+
290
+ const response = await fetch(url, {
291
+ method: "POST",
292
+ headers,
293
+ body: JSON.stringify({ model, messages, stream: true, tools: CLIENT_TOOLS }),
294
+ signal: abort,
295
+ })
296
+
297
+ if (!response.ok) {
298
+ const errText = await response.text().catch(() => "")
299
+ throw new Error(formatAPIError(response.status, errText))
300
+ }
301
+
302
+ const raw = await response.text()
303
+ const chunks = parseSSEStream(raw)
304
+ const { conversationId: cid, text, toolCall, finishReason } = extractFromChunks(chunks)
305
+
306
+ if (cid) conversationId = cid
307
+ if (text) fullText += text
308
+
309
+ if (finishReason === "tool_calls" && toolCall) {
310
+ const toolResult = await executeLocalTool(toolCall.name, toolCall.arguments, directory)
311
+ messages = [
312
+ ...messages,
313
+ {
314
+ role: "assistant",
315
+ content: null,
316
+ tool_calls: [{ id: toolCall.id, type: "function", function: { name: toolCall.name, arguments: toolCall.arguments } }],
317
+ },
318
+ {
319
+ role: "tool",
320
+ tool_call_id: toolCall.id,
321
+ content: toolResult,
322
+ },
323
+ ]
324
+ continue
325
+ }
326
+
327
+ return fullText || "(Cloud agent returned no text)"
328
+ }
329
+
330
+ return fullText || `Cloud agent exhausted ${MAX_TOOL_ROUNDS} tool rounds without completing.`
331
+ }
332
+
333
+ // ── System Prompt Builder ───────────────────────────────────────────
334
+
335
+ function buildSystemPrompt(agents) {
336
+ const grouped = {}
337
+ for (const a of agents) {
338
+ const cat = a.category || "other"
339
+ if (!grouped[cat]) grouped[cat] = []
340
+ grouped[cat].push(a)
341
+ }
342
+
343
+ const categoryLabels = {
344
+ coding: "Coding & Review",
345
+ security: "Security",
346
+ research: "Research & Analysis",
347
+ productivity: "Productivity",
348
+ creative: "Creative",
349
+ other: "Other",
350
+ }
351
+
352
+ let table = ""
353
+ for (const [cat, label] of Object.entries(categoryLabels)) {
354
+ const items = grouped[cat]
355
+ if (!items?.length) continue
356
+ table += `\n**${label}**\n`
357
+ for (const a of items) {
358
+ table += `| ${a.id} | ${a.description} |\n`
359
+ }
360
+ }
361
+
362
+ return `## MegaRouter Cloud Agents
363
+
364
+ You have access to MegaRouter cloud agents — domain-trained specialist AIs with dedicated skills and sandboxed execution. They produce better, more focused results than you for their specialty.
365
+
366
+ ### How to invoke
367
+
368
+ Cloud agents are registered as native subagents. Use \`task()\`:
369
+ \`\`\`
370
+ task(subagent_type="code-reviewer", prompt="Review backend/internal/auth/session.go for security issues.")
371
+ \`\`\`
372
+
373
+ If \`task()\` is unavailable, use the \`cloud_agent\` tool as fallback:
374
+ \`\`\`
375
+ cloud_agent(agent="code-reviewer", message="Review backend/internal/auth/session.go for security issues.")
376
+ \`\`\`
377
+
378
+ ### Available agents
379
+
380
+ | Agent | Specialty |
381
+ |-------|-----------|${table}
382
+
383
+ ### Tips
384
+ - **Be specific**: Include file paths, code snippets, and exactly what you need.
385
+ - **One task per call**: Don't bundle unrelated tasks.
386
+ - Cloud agents can read files, run commands, and search your local codebase.`
387
+ }
388
+
389
+ // ── Tool Creator ────────────────────────────────────────────────────
390
+
391
+ function createCloudAgentTool() {
392
+ const fallbackDesc = FALLBACK_AGENTS.map(a => `${a.id} (${a.description})`).join("; ")
393
+
394
+ return tool({
395
+ description: `Delegate a task to a MegaRouter cloud agent — a domain-trained specialist AI with dedicated skills and sandboxed execution. Cloud agents can read files, run commands, and search your codebase. This is a fallback — prefer using task(subagent_type="agent-name") when available.
396
+
397
+ Available agents: ${fallbackDesc}
398
+
399
+ Use when: code review, security audit, frontend advice, debugging, documentation, DevOps, research, data analysis, API testing, or creative writing.`,
400
+
401
+ args: {
402
+ agent: tool.schema.string().describe(
403
+ `Cloud agent name. One of: ${FALLBACK_AGENTS.map(a => a.id).join(", ")}`
404
+ ),
405
+ message: tool.schema.string().describe(
406
+ "The task to send. Be specific — include file paths, code context, and what you need."
407
+ ),
408
+ },
409
+
410
+ async execute(args, ctx) {
411
+ const agentName = args.agent.trim().toLowerCase()
412
+ const agents = agentList()
413
+ const known = agents.find(a => a.id === agentName)
414
+
415
+ if (!known) {
416
+ const available = agents.map(a => a.id)
417
+ const closest = available.find(id => id.includes(agentName) || agentName.includes(id))
418
+ const hint = closest ? ` Did you mean "${closest}"?` : ""
419
+ return `Unknown cloud agent "${args.agent}".${hint} Available: ${available.join(", ")}`
420
+ }
421
+
422
+ const config = resolveConfig()
423
+ try {
424
+ const result = await callCloudAgent(
425
+ config.baseUrl,
426
+ config.apiKey,
427
+ agentName,
428
+ args.message,
429
+ ctx.abort,
430
+ ctx.directory || process.cwd(),
431
+ )
432
+ return `[Cloud Agent: ${agentName}]\n\n${result}`
433
+ } catch (err) {
434
+ return `Cloud agent "${agentName}" error: ${err.message || err}`
435
+ }
436
+ },
437
+ })
438
+ }
439
+
440
+ // ── Plugin Entry Point ──────────────────────────────────────────────
441
+
442
+ const plugin = async (ctx) => {
443
+ projectDir = ctx?.directory || ctx?.worktree || null
444
+
445
+ return {
446
+ config: async (cfg) => {
447
+ providerConfigCache = null
448
+ discoveredAgents = []
449
+
450
+ if (cfg?.provider?.[PROVIDER_KEY]) {
451
+ providerConfigCache = cfg.provider
452
+ }
453
+
454
+ const config = resolveConfig()
455
+ if (config.baseUrl) {
456
+ const agents = await discoverAgents(config.baseUrl, config.apiKey)
457
+ if (agents && agents.length > 0) {
458
+ discoveredAgents = agents
459
+ await writeCachedManifest(projectDir, agents)
460
+ }
461
+ }
462
+
463
+ if (discoveredAgents.length === 0) {
464
+ const cached = await readCachedManifest(projectDir)
465
+ if (cached) discoveredAgents = cached
466
+ }
467
+
468
+ const agents = agentList()
469
+
470
+ if (!cfg.agent) cfg.agent = {}
471
+ for (const agent of agents) {
472
+ if (cfg.agent[agent.id]) continue
473
+ cfg.agent[agent.id] = {
474
+ model: `${PROVIDER_KEY}/${MODEL_PREFIX}${agent.id}`,
475
+ mode: "subagent",
476
+ description: agent.description,
477
+ }
478
+ }
479
+
480
+ if (cfg.provider?.[PROVIDER_KEY]) {
481
+ if (!cfg.provider[PROVIDER_KEY].models) cfg.provider[PROVIDER_KEY].models = {}
482
+ for (const agent of agents) {
483
+ const modelKey = `${MODEL_PREFIX}${agent.id}`
484
+ if (cfg.provider[PROVIDER_KEY].models[modelKey]) continue
485
+ cfg.provider[PROVIDER_KEY].models[modelKey] = {
486
+ name: agent.name || agent.id,
487
+ tool_call: true,
488
+ limit: { context: 200000, output: 32000 },
489
+ }
490
+ }
491
+ }
492
+ },
493
+
494
+ tool: {
495
+ cloud_agent: createCloudAgentTool(),
496
+ },
497
+
498
+ "experimental.chat.system.transform": async (_input, output) => {
499
+ output.system.push(buildSystemPrompt(agentList()))
500
+ },
501
+ }
502
+ }
503
+
504
+ export default plugin
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "opencode-mega-agent",
3
+ "version": "0.1.0",
4
+ "description": "OpenCode plugin for MegaRouter cloud agents — auto-discovers and registers specialist AI agents (code review, security audit, debugging, etc.) as native subagents",
5
+ "main": "index.mjs",
6
+ "type": "module",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "opencode",
10
+ "opencode-plugin",
11
+ "megarouter",
12
+ "cloud-agent",
13
+ "ai-agent",
14
+ "code-review",
15
+ "security-audit"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/kiyoakii/mega-router.git",
20
+ "directory": "plugins/opencode-cloud-agent"
21
+ },
22
+ "homepage": "https://github.com/kiyoakii/mega-router/tree/main/plugins/opencode-cloud-agent#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/kiyoakii/mega-router/issues"
25
+ },
26
+ "files": [
27
+ "index.mjs",
28
+ "README.md"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "@opencode-ai/plugin": "^1.3.2"
35
+ },
36
+ "devDependencies": {
37
+ "vitest": "^3.1.1"
38
+ }
39
+ }