@mirrowel/opencode-subagent-explorer 0.1.0-dev.1

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.
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@mirrowel/opencode-subagent-explorer",
3
+ "version": "0.1.0-dev.1",
4
+ "type": "module",
5
+ "description": "OpenCode plugin for exploring and cleaning up subagent sessions of the current session",
6
+ "license": "MIT",
7
+ "author": "Mirrowel",
8
+ "homepage": "https://github.com/Mirrowel/opencode-subagent-explorer#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Mirrowel/opencode-subagent-explorer.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Mirrowel/opencode-subagent-explorer/issues"
15
+ },
16
+ "main": "dist/server.js",
17
+ "types": "dist/server.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/server.d.ts",
21
+ "import": "./dist/server.js"
22
+ },
23
+ "./server": {
24
+ "types": "./dist/server.d.ts",
25
+ "import": "./dist/server.js"
26
+ },
27
+ "./tui": {
28
+ "types": "./dist/tui.d.ts",
29
+ "import": "./dist/tui.js"
30
+ },
31
+ "./wizard": {
32
+ "types": "./dist/wizard.d.ts",
33
+ "import": "./dist/wizard.js"
34
+ },
35
+ "./sessions": {
36
+ "types": "./dist/sessions.d.ts",
37
+ "import": "./dist/sessions.js"
38
+ },
39
+ "./tui-host": {
40
+ "types": "./dist/tui-host.d.ts",
41
+ "import": "./dist/tui-host.js"
42
+ }
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "src",
47
+ "tsconfig.json",
48
+ "README.md",
49
+ "LICENSE"
50
+ ],
51
+ "keywords": [
52
+ "opencode",
53
+ "opencode-plugin",
54
+ "subagent",
55
+ "session",
56
+ "cleanup"
57
+ ],
58
+ "oc-plugin": [
59
+ "server",
60
+ "tui"
61
+ ],
62
+ "publishConfig": {
63
+ "registry": "https://registry.npmjs.org/",
64
+ "access": "public"
65
+ },
66
+ "scripts": {
67
+ "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
68
+ "build": "npm run clean && tsc --outDir dist --declaration && npm run build:tui",
69
+ "build:tui": "bun scripts/build-tui.mjs",
70
+ "ci": "npm run release:intent:check && npm run ci:package",
71
+ "ci:package": "npm run typecheck && npm run build && npm run test:unit && npm run test:tui-reactivity && npm run test:tui-startup && npm run pack:dry-run && npm run test:tui-package",
72
+ "commit:check": "npm run ci",
73
+ "hooks:install": "node scripts/install-git-hooks.mjs",
74
+ "pack:dry-run": "node scripts/pack-dry-run.mjs",
75
+ "release:intent": "node scripts/set-release-intent.mjs",
76
+ "release:intent:check": "node scripts/check-release-intent.mjs",
77
+ "test:unit": "node scripts/unit-tests.mjs",
78
+ "test:tui-reactivity": "bun --conditions=browser scripts/tui-reactivity-smoke.mjs",
79
+ "test:tui-package": "node scripts/tui-package-smoke.mjs",
80
+ "test:tui-startup": "node scripts/tui-startup-smoke.mjs",
81
+ "typecheck": "tsc --noEmit",
82
+ "prepublishOnly": "npm run typecheck && npm run build"
83
+ },
84
+ "dependencies": {
85
+ "@opencode-ai/plugin": ">=1.14.0",
86
+ "@opentui/solid": "^0.4.1",
87
+ "comment-json": "^4.2.5",
88
+ "solid-js": "1.9.12"
89
+ },
90
+ "devDependencies": {
91
+ "@types/node": "^22.0.0",
92
+ "typescript": "^5.8.0"
93
+ },
94
+ "peerDependencies": {
95
+ "@opencode-ai/plugin": ">=1.14.0"
96
+ }
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Subagent Explorer server plugin (v1).
3
+ *
4
+ * The explorer is a TUI-driven tool: the server side exists purely so a
5
+ * single opencode.json registration loads the plugin (and self-wires the
6
+ * matching tui.json entry - see selfwire.ts). No hooks are registered; the
7
+ * plugin never intercepts runtime behavior.
8
+ */
9
+
10
+ import type { Plugin } from "@opencode-ai/plugin"
11
+ import { ensureTuiRegistration, isSubagentExplorerSpec } from "./selfwire.js"
12
+
13
+ export const plugin: Plugin = async (input) => {
14
+ // File-scan only, wrapped in try/catch: never blocks or breaks startup.
15
+ try {
16
+ ensureTuiRegistration({ directory: input.directory, worktree: input.worktree, env: process.env })
17
+ } catch {
18
+ /* self-wiring is best-effort */
19
+ }
20
+ return {}
21
+ }
22
+
23
+ export const __testInternals = { ensureTuiRegistration, isSubagentExplorerSpec }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shared palette category for Mirrowel OpenCode plugins loaded in the same
3
+ * session. Every family plugin declares its own label into a process-wide
4
+ * registry at TUI activation; a delayed reconciler joins all declared labels
5
+ * (alphabetical, deterministic) and writes the join into every registered
6
+ * command object, so the palette shows one combined section. A plugin loaded
7
+ * alone keeps its own name; third-party plugins never declare and stay out.
8
+ *
9
+ * Labels are defined only in each plugin's entry point - this module carries
10
+ * no per-plugin data. Keep the logic copies in sync across family repos.
11
+ */
12
+
13
+ const REGISTRY_KEY = Symbol.for("mirrowel.opencode.paletteCategory")
14
+
15
+ // v1 command objects group via `category`; v2 keymap commands use `group`.
16
+ // Stamping both keeps one registry meaningful on both hosts.
17
+ type CategorizedCommand = { category?: string; group?: string }
18
+ type RegistryEntry = { label: string; commands: CategorizedCommand[] }
19
+ type Registry = { entries: RegistryEntry[] }
20
+
21
+ function registry(): Registry {
22
+ const globals = globalThis as { [key: symbol]: Registry | undefined }
23
+ if (!globals[REGISTRY_KEY]) globals[REGISTRY_KEY] = { entries: [] }
24
+ return globals[REGISTRY_KEY]!
25
+ }
26
+
27
+ function joinLabels(labels: string[]): string {
28
+ if (labels.length <= 1) return labels[0] ?? ""
29
+ return `${labels.slice(0, -1).join(", ")} & ${labels[labels.length - 1]!}`
30
+ }
31
+
32
+ function joinedCategory(): string {
33
+ const labels = [...new Set(registry().entries.map((entry) => entry.label))].sort((a, b) => a.localeCompare(b))
34
+ return joinLabels(labels)
35
+ }
36
+
37
+ /**
38
+ * Declares this plugin's category label and attaches command objects whose
39
+ * `category` field is kept in sync. Returns the current join, so the very
40
+ * first registration already renders correctly even before reconcilers run.
41
+ */
42
+ export function declarePaletteCategory(label: string, ...commands: CategorizedCommand[]): string {
43
+ const reg = registry()
44
+ let entry = reg.entries.find((item) => item.label === label)
45
+ if (!entry) {
46
+ entry = { label, commands: [] }
47
+ reg.entries.push(entry)
48
+ }
49
+ for (const command of commands) {
50
+ if (command && !entry.commands.includes(command)) entry.commands.push(command)
51
+ }
52
+ reconcilePaletteCategories()
53
+ return joinedCategory()
54
+ }
55
+
56
+ /** Live join for callback-style command registration (reads fresh each call). */
57
+ export function currentPaletteCategory(): string {
58
+ return joinedCategory()
59
+ }
60
+
61
+ /** Writes the current join into every declared command (idempotent). */
62
+ export function reconcilePaletteCategories(): void {
63
+ const category = joinedCategory()
64
+ for (const entry of registry().entries) {
65
+ for (const command of entry.commands) {
66
+ command.category = category
67
+ command.group = category
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Delayed reconciles so later-activating siblings join before the user opens the palette. */
73
+ export function schedulePaletteReconcile(delaysMs: readonly number[] = [2000, 6000]): void {
74
+ for (const delay of delaysMs) {
75
+ const timer = setTimeout(() => reconcilePaletteCategories(), delay)
76
+ ;(timer as { unref?: () => void }).unref?.()
77
+ }
78
+ }
79
+
80
+ /** Tests only: drop all declarations. */
81
+ export function __resetPaletteRegistry(): void {
82
+ ;(globalThis as { [key: symbol]: Registry | undefined })[REGISTRY_KEY] = undefined
83
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Self-wiring for the subagent-explorer TUI (v1 hosts only).
3
+ *
4
+ * OpenCode loads server plugins from the `plugin` array in opencode.json
5
+ * layers but TUI plugins only from tui.json layers. When subagent-explorer
6
+ * is registered standalone (opencode.json) without a matching tui.json
7
+ * entry, the explorer command never loads. ensureTuiRegistration() mirrors
8
+ * the server registration into tui.json AT THE SAME CONFIG LEVEL
9
+ * (global -> global, project -> project) with the same auto-correct
10
+ * semantics as Config Studio's selfwire:
11
+ *
12
+ * - identity matching: any checkout counts (repo folder named plain
13
+ * `subagent-explorer` or `opencode-subagent-explorer`), not just this
14
+ * running instance's directory
15
+ * - the tui entry must MATCH the server registration; local checkouts win
16
+ * over npm installs when both are registered
17
+ * - stale mirrors (tui entries at levels without a server registration) are
18
+ * removed; duplicates are deduped
19
+ *
20
+ * When Config Studio is registered anywhere (it can embed subagent-explorer
21
+ * and provides the Tools menu entry), selfwiring is skipped entirely so the
22
+ * two never fight over the TUI slot. v2 hosts never run this module: they
23
+ * auto-discover ./tui from server-declared plugins, and the v2 setup does
24
+ * not import it.
25
+ */
26
+
27
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
28
+ import { basename, dirname, join } from "node:path"
29
+ import { parse, stringify } from "comment-json"
30
+
31
+ const EXPLORER_NPM = "@mirrowel/opencode-subagent-explorer"
32
+ const STUDIO_NPM = "@mirrowel/opencode-config-studio"
33
+
34
+ function normalizePath(value: string): string {
35
+ return value.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase()
36
+ }
37
+
38
+ function specPathSegments(spec: string): string | undefined {
39
+ if (!spec.startsWith("file:")) return undefined
40
+ try {
41
+ let url = spec
42
+ if (!url.startsWith("file:///") && url.startsWith("file://")) url = `file:///${url.slice("file://".length)}`
43
+ return normalizePath(new URL(url).pathname)
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ function isLocalSpec(spec: string): boolean {
50
+ return spec.startsWith("file:") || /^([a-zA-Z]:[\\/]|\/)/.test(spec)
51
+ }
52
+
53
+ export function isSubagentExplorerSpec(spec: unknown): spec is string {
54
+ if (typeof spec !== "string" || spec.length === 0) return false
55
+ if (spec === EXPLORER_NPM || spec.startsWith(`${EXPLORER_NPM}@`)) return true
56
+ const path = specPathSegments(spec)
57
+ if (!path) return false
58
+ if (path.endsWith("/opencode-subagent-explorer") || path.endsWith("/subagent-explorer")) return true
59
+ // Cache/wrapper layouts: <base>/@mirrowel/opencode-subagent-explorer@<tag>[/node_modules/...]
60
+ return path.includes("/@mirrowel/opencode-subagent-explorer") || path.includes("/node_modules/@mirrowel/opencode-subagent-explorer")
61
+ }
62
+
63
+ export function isConfigStudioSpec(spec: unknown): spec is string {
64
+ if (typeof spec !== "string" || spec.length === 0) return false
65
+ if (spec === STUDIO_NPM || spec.startsWith(`${STUDIO_NPM}@`)) return true
66
+ const path = specPathSegments(spec)
67
+ if (!path) return false
68
+ return path.endsWith("/opencode-config-studio")
69
+ }
70
+
71
+ function specStrings(data: Record<string, unknown>): string[] {
72
+ const result: string[] = []
73
+ for (const key of ["plugin", "plugins"] as const) {
74
+ const array = data[key]
75
+ if (!Array.isArray(array)) continue
76
+ for (const entry of array) {
77
+ const spec =
78
+ typeof entry === "string"
79
+ ? entry
80
+ : Array.isArray(entry) && typeof entry[0] === "string"
81
+ ? (entry[0] as string)
82
+ : entry && typeof entry === "object" && typeof (entry as { package?: unknown }).package === "string"
83
+ ? (entry as { package: string }).package
84
+ : undefined
85
+ if (spec !== undefined) result.push(spec)
86
+ }
87
+ }
88
+ return result
89
+ }
90
+
91
+ function readJsonc(path: string): Record<string, unknown> {
92
+ try {
93
+ if (!existsSync(path)) return {}
94
+ return parse(readFileSync(path, "utf8")) as unknown as Record<string, unknown>
95
+ } catch {
96
+ return {}
97
+ }
98
+ }
99
+
100
+ function sameDir(a: string, b: string): boolean {
101
+ return normalizePath(a) === normalizePath(b)
102
+ }
103
+
104
+ function levelDirOf(configPath: string): string {
105
+ let dir = dirname(configPath)
106
+ if (basename(dir) === ".opencode") dir = dirname(dir)
107
+ return dir
108
+ }
109
+
110
+ function globalConfigDir(env?: NodeJS.ProcessEnv): string {
111
+ if (env?.["OPENCODE_CONFIG_DIR"]) return env["OPENCODE_CONFIG_DIR"]
112
+ if (env?.["XDG_CONFIG_HOME"]) return join(env["XDG_CONFIG_HOME"], "opencode")
113
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? ""
114
+ return join(home, ".config", "opencode")
115
+ }
116
+
117
+ function opencodeLayerPaths(input: { directory?: string; worktree?: string; env?: NodeJS.ProcessEnv }): string[] {
118
+ const paths: string[] = []
119
+ const pushDir = (dir: string) => {
120
+ for (const candidate of [join(dir, "opencode.json"), join(dir, "opencode.jsonc"), join(dir, ".opencode", "opencode.json"), join(dir, ".opencode", "opencode.jsonc")]) {
121
+ if (existsSync(candidate) && !paths.includes(candidate)) paths.push(candidate)
122
+ }
123
+ }
124
+ const globalDir = globalConfigDir(input.env)
125
+ if (input.env?.["OPENCODE_CONFIG"]) {
126
+ if (existsSync(input.env["OPENCODE_CONFIG"])) paths.push(input.env["OPENCODE_CONFIG"])
127
+ } else {
128
+ pushDir(globalDir)
129
+ }
130
+ if (input.directory && input.worktree && input.directory.startsWith(input.worktree)) {
131
+ let current: string | undefined = input.directory
132
+ for (let guard = 0; guard < 64 && current; guard++) {
133
+ pushDir(current)
134
+ if (current === input.worktree || current === dirname(current)) break
135
+ current = dirname(current)
136
+ }
137
+ }
138
+ return paths
139
+ }
140
+
141
+ function tuiLayerPaths(input: { directory?: string; worktree?: string; env?: NodeJS.ProcessEnv }): string[] {
142
+ const paths: string[] = []
143
+ const globalDir = globalConfigDir(input.env)
144
+ const globalTui = join(globalDir, "tui.json")
145
+ if (existsSync(globalTui)) paths.push(globalTui)
146
+ const envTui = input.env?.["OPENCODE_TUI_CONFIG"]
147
+ if (envTui && existsSync(envTui)) paths.push(envTui)
148
+ if (input.directory && input.worktree && input.directory.startsWith(input.worktree)) {
149
+ let current: string | undefined = input.directory
150
+ for (let guard = 0; guard < 64 && current; guard++) {
151
+ for (const candidate of [join(current, "tui.json"), join(current, ".opencode", "tui.json")]) {
152
+ if (existsSync(candidate) && !paths.includes(candidate)) paths.push(candidate)
153
+ }
154
+ if (current === input.worktree || current === dirname(current)) break
155
+ current = dirname(current)
156
+ }
157
+ }
158
+ return paths
159
+ }
160
+
161
+ function ownIndices(tuiPath: string): number[] {
162
+ const data = readJsonc(tuiPath)
163
+ const plugin = data["plugin"]
164
+ if (!Array.isArray(plugin)) return []
165
+ const indices: number[] = []
166
+ plugin.forEach((entry, index) => {
167
+ const spec = typeof entry === "string" ? entry : Array.isArray(entry) && typeof entry[0] === "string" ? (entry[0] as string) : undefined
168
+ if (spec !== undefined && isSubagentExplorerSpec(spec)) indices.push(index)
169
+ })
170
+ return indices
171
+ }
172
+
173
+ function preferLocal(specs: string[]): string[] {
174
+ const local = specs.filter(isLocalSpec)
175
+ const npm = specs.filter((spec) => !local.includes(spec))
176
+ return [...local, ...npm]
177
+ }
178
+
179
+ function atomicWrite(path: string, content: string, stateDir: string): boolean {
180
+ try {
181
+ const backupDir = join(stateDir, "selfwire-backups")
182
+ mkdirSync(backupDir, { recursive: true })
183
+ if (existsSync(path)) {
184
+ const backup = join(backupDir, `${basename(path)}.${Date.now()}.bak`)
185
+ writeFileSync(backup, readFileSync(path, "utf8"), "utf8")
186
+ }
187
+ const temp = `${path}.se-tmp`
188
+ writeFileSync(temp, content, "utf8")
189
+ renameSync(temp, path)
190
+ return true
191
+ } catch {
192
+ return false
193
+ }
194
+ }
195
+
196
+ export type WireResult =
197
+ | { status: "skipped-studio" }
198
+ | { status: "already-wired"; spec: string }
199
+ | { status: "wired"; spec: string; target: string }
200
+ | { status: "corrected"; spec: string; target: string }
201
+ | { status: "not-registered" }
202
+ | { status: "failed"; error: string }
203
+
204
+ /**
205
+ * Mirrors the standalone subagent-explorer server registration into
206
+ * tui.json (same config level). No-op when Config Studio is registered
207
+ * anywhere - it embeds subagent-explorer and provides the Tools entry
208
+ * itself.
209
+ */
210
+ export function ensureTuiRegistration(input: { directory?: string; worktree?: string; env?: NodeJS.ProcessEnv } = {}): WireResult {
211
+ try {
212
+ const opencodeLayers = opencodeLayerPaths(input)
213
+ const tuiLayers = tuiLayerPaths(input)
214
+
215
+ // Config Studio present anywhere (server or tui layers) -> stand down.
216
+ for (const layer of [...opencodeLayers, ...tuiLayers]) {
217
+ if (specStrings(readJsonc(layer)).some((spec) => isConfigStudioSpec(spec))) {
218
+ return { status: "skipped-studio" }
219
+ }
220
+ }
221
+
222
+ const registrations = opencodeLayers
223
+ .map((path) => ({
224
+ path,
225
+ level: levelDirOf(path),
226
+ specs: specStrings(readJsonc(path)).filter((spec) => isSubagentExplorerSpec(spec)),
227
+ }))
228
+ .filter((layer) => layer.specs.length > 0)
229
+ if (registrations.length === 0) return { status: "not-registered" }
230
+
231
+ const preferAt = (layers: typeof registrations): string | undefined => {
232
+ const ordered = [...layers].sort((a, b) => {
233
+ const aLocal = a.specs.some(isLocalSpec)
234
+ const bLocal = b.specs.some(isLocalSpec)
235
+ if (aLocal !== bLocal) return aLocal ? -1 : 1
236
+ return 0
237
+ })
238
+ const first = ordered[0]
239
+ return first ? preferLocal(first.specs)[0] : undefined
240
+ }
241
+ const wanted = preferAt(registrations)
242
+ if (!wanted) return { status: "not-registered" }
243
+ const wantedLevel = registrations.find((layer) => layer.specs.includes(wanted))?.level ?? globalConfigDir(input.env)
244
+ const target = sameDir(wantedLevel, globalConfigDir(input.env))
245
+ ? join(globalConfigDir(input.env), "tui.json")
246
+ : join(wantedLevel, "tui.json")
247
+
248
+ const stateDir = join(globalConfigDir(input.env), "subagent-explorer")
249
+ let wired = false
250
+ let corrected = false
251
+
252
+ const seen = new Set<string>()
253
+ const fixTuiFile = (tuiPath: string, level: string) => {
254
+ if (seen.has(tuiPath)) return
255
+ seen.add(tuiPath)
256
+ const indices = ownIndices(tuiPath)
257
+ if (indices.length === 0) return
258
+ const levelLayers = registrations.filter((layer) => sameDir(layer.level, level))
259
+ const data = readJsonc(tuiPath)
260
+ const plugin = data["plugin"]
261
+ if (!Array.isArray(plugin)) return
262
+ if (levelLayers.length === 0) {
263
+ // Stale mirror: no server registration at this level.
264
+ for (const index of [...indices].sort((a, b) => b - a)) plugin.splice(index, 1)
265
+ corrected = true
266
+ } else {
267
+ const wantedHere = preferAt(levelLayers) ?? wanted
268
+ const keepIndex = indices[0]!
269
+ let changed = false
270
+ if (indices.length > 1) {
271
+ for (const index of [...indices].slice(1).sort((a, b) => b - a)) plugin.splice(index, 1)
272
+ changed = true
273
+ }
274
+ const entry = plugin[keepIndex]
275
+ const spec = typeof entry === "string" ? entry : Array.isArray(entry) && typeof entry[0] === "string" ? (entry[0] as string) : undefined
276
+ if (spec !== wantedHere) {
277
+ if (Array.isArray(entry)) entry[0] = wantedHere
278
+ else plugin[keepIndex] = wantedHere
279
+ changed = true
280
+ }
281
+ if (changed) corrected = true
282
+ else return
283
+ }
284
+ if (!atomicWrite(tuiPath, stringify(data, null, 2) + "\n", stateDir)) {
285
+ throw new Error(`failed to write ${tuiPath}`)
286
+ }
287
+ }
288
+
289
+ for (const layer of tuiLayers) fixTuiFile(layer, levelDirOf(layer))
290
+
291
+ // Ensure the registration level carries its mirror (independent of the
292
+ // correction pass above - an empty plugin array still needs the entry).
293
+ const targetIndices = existsSync(target) ? ownIndices(target) : []
294
+ if (targetIndices.length === 0) {
295
+ const data = readJsonc(target)
296
+ const plugin = Array.isArray(data["plugin"]) ? (data["plugin"] as unknown[]) : []
297
+ plugin.push(wanted)
298
+ data["plugin"] = plugin
299
+ if (!atomicWrite(target, stringify(data, null, 2) + "\n", stateDir)) {
300
+ return { status: "failed", error: `failed to write ${target}` }
301
+ }
302
+ wired = true
303
+ }
304
+
305
+ if (wired) return { status: "wired", spec: wanted, target }
306
+ if (corrected) return { status: "corrected", spec: wanted, target }
307
+ return { status: "already-wired", spec: wanted }
308
+ } catch (error) {
309
+ return { status: "failed", error: error instanceof Error ? error.message : String(error) }
310
+ }
311
+ }
package/src/server.ts ADDED
@@ -0,0 +1,17 @@
1
+ import { plugin } from "./index.js"
2
+ import { createV2ServerSetup } from "./v2-server.js"
3
+
4
+ /**
5
+ * Dual-target server entry.
6
+ *
7
+ * - OpenCode v1 resolves `exports["./server"]`, reads `mod.default`, and
8
+ * invokes the `server` factory (excess keys are never validated).
9
+ * - OpenCode v2 imports the same module, validates `mod.default` as
10
+ * `{ id, setup }`, and ignores the legacy `server` key - so one default
11
+ * export satisfies both loaders.
12
+ */
13
+ export default {
14
+ id: "subagent-explorer",
15
+ server: plugin,
16
+ setup: createV2ServerSetup(),
17
+ }