@te-river/opencode-team-mode 1.5.15 → 1.6.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.
Files changed (196) hide show
  1. package/README.md +853 -690
  2. package/README.zh-CN.md +761 -631
  3. package/dist/agents.d.ts +5 -3
  4. package/dist/agents.d.ts.map +1 -1
  5. package/dist/agents.js +19 -8
  6. package/dist/agents.js.map +1 -1
  7. package/dist/capabilities.d.ts +1 -1
  8. package/dist/capabilities.js +2 -2
  9. package/dist/capabilities.js.map +1 -1
  10. package/dist/host/note.d.ts +7 -0
  11. package/dist/host/note.d.ts.map +1 -0
  12. package/dist/host/note.js +28 -0
  13. package/dist/host/note.js.map +1 -0
  14. package/dist/host/v1.d.ts +14 -0
  15. package/dist/host/v1.d.ts.map +1 -0
  16. package/dist/host/v1.js +340 -0
  17. package/dist/host/v1.js.map +1 -0
  18. package/dist/host/v2-browser-gate.d.ts +45 -0
  19. package/dist/host/v2-browser-gate.d.ts.map +1 -0
  20. package/dist/host/v2-browser-gate.js +319 -0
  21. package/dist/host/v2-browser-gate.js.map +1 -0
  22. package/dist/host/v2-capabilities.d.ts +68 -0
  23. package/dist/host/v2-capabilities.d.ts.map +1 -0
  24. package/dist/host/v2-capabilities.js +146 -0
  25. package/dist/host/v2-capabilities.js.map +1 -0
  26. package/dist/host/v2-client.d.ts +53 -0
  27. package/dist/host/v2-client.d.ts.map +1 -0
  28. package/dist/host/v2-client.js +193 -0
  29. package/dist/host/v2-client.js.map +1 -0
  30. package/dist/host/v2-events.d.ts +56 -0
  31. package/dist/host/v2-events.d.ts.map +1 -0
  32. package/dist/host/v2-events.js +158 -0
  33. package/dist/host/v2-events.js.map +1 -0
  34. package/dist/host/v2-guard.d.ts +101 -0
  35. package/dist/host/v2-guard.d.ts.map +1 -0
  36. package/dist/host/v2-guard.js +199 -0
  37. package/dist/host/v2-guard.js.map +1 -0
  38. package/dist/host/v2-offload.d.ts +148 -0
  39. package/dist/host/v2-offload.d.ts.map +1 -0
  40. package/dist/host/v2-offload.js +288 -0
  41. package/dist/host/v2-offload.js.map +1 -0
  42. package/dist/host/v2-permissions.d.ts +123 -0
  43. package/dist/host/v2-permissions.d.ts.map +1 -0
  44. package/dist/host/v2-permissions.js +190 -0
  45. package/dist/host/v2-permissions.js.map +1 -0
  46. package/dist/host/v2-probe.d.ts +85 -0
  47. package/dist/host/v2-probe.d.ts.map +1 -0
  48. package/dist/host/v2-probe.js +268 -0
  49. package/dist/host/v2-probe.js.map +1 -0
  50. package/dist/host/v2-scope.d.ts +51 -0
  51. package/dist/host/v2-scope.d.ts.map +1 -0
  52. package/dist/host/v2-scope.js +58 -0
  53. package/dist/host/v2-scope.js.map +1 -0
  54. package/dist/host/v2-session-client.d.ts +74 -0
  55. package/dist/host/v2-session-client.d.ts.map +1 -0
  56. package/dist/host/v2-session-client.js +171 -0
  57. package/dist/host/v2-session-client.js.map +1 -0
  58. package/dist/host/v2-session.d.ts +85 -0
  59. package/dist/host/v2-session.d.ts.map +1 -0
  60. package/dist/host/v2-session.js +171 -0
  61. package/dist/host/v2-session.js.map +1 -0
  62. package/dist/host/v2-subagent.d.ts +157 -0
  63. package/dist/host/v2-subagent.d.ts.map +1 -0
  64. package/dist/host/v2-subagent.js +286 -0
  65. package/dist/host/v2-subagent.js.map +1 -0
  66. package/dist/host/v2-tool.d.ts +67 -0
  67. package/dist/host/v2-tool.d.ts.map +1 -0
  68. package/dist/host/v2-tool.js +209 -0
  69. package/dist/host/v2-tool.js.map +1 -0
  70. package/dist/host/v2-transcript.d.ts +73 -0
  71. package/dist/host/v2-transcript.d.ts.map +1 -0
  72. package/dist/host/v2-transcript.js +151 -0
  73. package/dist/host/v2-transcript.js.map +1 -0
  74. package/dist/host/v2-types.d.ts +229 -0
  75. package/dist/host/v2-types.d.ts.map +1 -0
  76. package/dist/host/v2-types.js +17 -0
  77. package/dist/host/v2-types.js.map +1 -0
  78. package/dist/host/v2.d.ts +42 -0
  79. package/dist/host/v2.d.ts.map +1 -0
  80. package/dist/host/v2.js +706 -0
  81. package/dist/host/v2.js.map +1 -0
  82. package/dist/host-hooks.d.ts +13 -2
  83. package/dist/host-hooks.d.ts.map +1 -1
  84. package/dist/host-hooks.js +18 -5
  85. package/dist/host-hooks.js.map +1 -1
  86. package/dist/index.d.ts +23 -17
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +29 -326
  89. package/dist/index.js.map +1 -1
  90. package/dist/prompts/lead.d.ts +1 -1
  91. package/dist/prompts/lead.d.ts.map +1 -1
  92. package/dist/prompts/lead.js +452 -359
  93. package/dist/prompts/lead.js.map +1 -1
  94. package/dist/prompts/shared.d.ts +2 -2
  95. package/dist/prompts/shared.d.ts.map +1 -1
  96. package/dist/prompts/shared.js +89 -17
  97. package/dist/prompts/shared.js.map +1 -1
  98. package/dist/prompts/specialists.d.ts +2 -2
  99. package/dist/prompts/specialists.d.ts.map +1 -1
  100. package/dist/prompts/specialists.js +44 -10
  101. package/dist/prompts/specialists.js.map +1 -1
  102. package/dist/task-offload.d.ts +83 -0
  103. package/dist/task-offload.d.ts.map +1 -0
  104. package/dist/task-offload.js +174 -0
  105. package/dist/task-offload.js.map +1 -0
  106. package/dist/tm/args-schema.d.ts +4 -0
  107. package/dist/tm/args-schema.d.ts.map +1 -1
  108. package/dist/tm/args-schema.js +36 -3
  109. package/dist/tm/args-schema.js.map +1 -1
  110. package/dist/tm/board.d.ts +113 -0
  111. package/dist/tm/board.d.ts.map +1 -0
  112. package/dist/tm/board.js +275 -0
  113. package/dist/tm/board.js.map +1 -0
  114. package/dist/tm/browser.d.ts +325 -6
  115. package/dist/tm/browser.d.ts.map +1 -1
  116. package/dist/tm/browser.js +1360 -135
  117. package/dist/tm/browser.js.map +1 -1
  118. package/dist/tm/config.d.ts +36 -25
  119. package/dist/tm/config.d.ts.map +1 -1
  120. package/dist/tm/config.js +29 -7
  121. package/dist/tm/config.js.map +1 -1
  122. package/dist/tm/dispatch.d.ts +133 -53
  123. package/dist/tm/dispatch.d.ts.map +1 -1
  124. package/dist/tm/dispatch.js +481 -279
  125. package/dist/tm/dispatch.js.map +1 -1
  126. package/dist/tm/dupe-guard.d.ts +16 -3
  127. package/dist/tm/dupe-guard.d.ts.map +1 -1
  128. package/dist/tm/dupe-guard.js +27 -7
  129. package/dist/tm/dupe-guard.js.map +1 -1
  130. package/dist/tm/egress.d.ts +44 -0
  131. package/dist/tm/egress.d.ts.map +1 -0
  132. package/dist/tm/egress.js +191 -0
  133. package/dist/tm/egress.js.map +1 -0
  134. package/dist/tm/guard.js +1 -1
  135. package/dist/tm/guard.js.map +1 -1
  136. package/dist/tm/index.d.ts +67 -2
  137. package/dist/tm/index.d.ts.map +1 -1
  138. package/dist/tm/index.js +228 -20
  139. package/dist/tm/index.js.map +1 -1
  140. package/dist/tm/ledger.d.ts +114 -0
  141. package/dist/tm/ledger.d.ts.map +1 -0
  142. package/dist/tm/ledger.js +308 -0
  143. package/dist/tm/ledger.js.map +1 -0
  144. package/dist/tm/perm-ask.d.ts +45 -5
  145. package/dist/tm/perm-ask.d.ts.map +1 -1
  146. package/dist/tm/perm-ask.js +107 -7
  147. package/dist/tm/perm-ask.js.map +1 -1
  148. package/dist/tm/pipelines.d.ts.map +1 -1
  149. package/dist/tm/pipelines.js +24 -2
  150. package/dist/tm/pipelines.js.map +1 -1
  151. package/dist/tm/preview.d.ts +55 -0
  152. package/dist/tm/preview.d.ts.map +1 -1
  153. package/dist/tm/preview.js +225 -0
  154. package/dist/tm/preview.js.map +1 -1
  155. package/dist/tm/ptc/tool.d.ts.map +1 -1
  156. package/dist/tm/ptc/tool.js +2 -4
  157. package/dist/tm/ptc/tool.js.map +1 -1
  158. package/dist/tm/pty.d.ts.map +1 -1
  159. package/dist/tm/pty.js +6 -3
  160. package/dist/tm/pty.js.map +1 -1
  161. package/dist/tm/search.d.ts +5 -0
  162. package/dist/tm/search.d.ts.map +1 -1
  163. package/dist/tm/search.js +40 -8
  164. package/dist/tm/search.js.map +1 -1
  165. package/dist/tm/serp-loop.d.ts +45 -0
  166. package/dist/tm/serp-loop.d.ts.map +1 -0
  167. package/dist/tm/serp-loop.js +80 -0
  168. package/dist/tm/serp-loop.js.map +1 -0
  169. package/dist/tm/stats.d.ts +26 -0
  170. package/dist/tm/stats.d.ts.map +1 -1
  171. package/dist/tm/stats.js +194 -4
  172. package/dist/tm/stats.js.map +1 -1
  173. package/dist/tm/webfetch.d.ts +56 -0
  174. package/dist/tm/webfetch.d.ts.map +1 -1
  175. package/dist/tm/webfetch.js +187 -19
  176. package/dist/tm/webfetch.js.map +1 -1
  177. package/dist/tool-coerce.d.ts +24 -0
  178. package/dist/tool-coerce.d.ts.map +1 -0
  179. package/dist/tool-coerce.js +63 -0
  180. package/dist/tool-coerce.js.map +1 -0
  181. package/dist/types.d.ts +7 -1
  182. package/dist/types.d.ts.map +1 -1
  183. package/docs/installation-v2.md +436 -0
  184. package/docs/installation.md +81 -2
  185. package/docs/research/agent-data-exchange.md +117 -0
  186. package/docs/research/browser-pane.md +89 -0
  187. package/docs/research/host-http-api.md +132 -0
  188. package/docs/research/host-subagent-injection.md +187 -0
  189. package/docs/research/plugin-loader-contract.md +261 -0
  190. package/docs/research/v2-builtin-tools.md +447 -0
  191. package/docs/v2-port-notes.md +315 -0
  192. package/index.js +7 -0
  193. package/package.json +4 -1
  194. package/scripts/gen-v2-config.mjs +304 -0
  195. package/scripts/lib/config-surgery.cjs +239 -0
  196. package/scripts/lib/fake-ctx.mjs +146 -0
@@ -0,0 +1,261 @@
1
+ # OpenCode 2.0.16 — how the host actually loads a plugin from `config.plugins`
2
+
3
+ **Scope:** read-only forensics. The strings below were cut out of
4
+ `resources/opencode-cli.exe` with a windowed byte scan; the live confirmations were
5
+ run against a sandboxed `OPENCODE_CONFIG_DIR` + temp store roots. Nothing in the host
6
+ was modified, and no path, URL, command line or env value of the user's appears here.
7
+
8
+ **Why this page exists.** The official v2 plugin docs document a `"plugins"` key
9
+ (a list of package names), and for a while our answer to "can the installer just write
10
+ that key?" was "no, it does not load". That answer was **wrong**, and the wrong answer
11
+ was the reason the installers carried a cache-purge/re-resolve story that v2 does not
12
+ need. This page is the corrected contract, with the host's own code as the evidence.
13
+
14
+ ## 0. Where each claim comes from
15
+
16
+ | Tag | Source | Strength |
17
+ |---|---|---|
18
+ | `[B]` | Verbatim from the embedded Effect/JS in `resources/opencode-cli.exe` (`opencode-cli.version` reads `2.0.16`) | strongest offline |
19
+ | `[L]` | Our own live boot probe against a sandboxed config dir (`--standalone`, `--print-logs`, `v2-boot` trajectory line) | strongest overall |
20
+ | `[R]` | Our repo (`index.js`, `package.json#files`) | ours, verified |
21
+ | `[D]` | Official v2 docs | corroborating |
22
+
23
+ ## 1. The entry: `config.plugins` is read, mapped, then resolved
24
+
25
+ The code blocks below are the host's own minified text **reformatted for reading**:
26
+ identifiers, strings and control flow are untouched, whitespace is not. The unmodified
27
+ bytes are in the extract window quoted per line.
28
+
29
+ `[B]` `ConfigPluginSource.scan` (minified `BN`) takes `l.info.plugins ?? []` from every
30
+ parsed config **document** and maps each entry through the operation builder `DN`:
31
+
32
+ ```js
33
+ function DN(e){
34
+ if (typeof e !== "string") return {type:"add", target:e.package, options:e.options ?? {}}
35
+ if (!e.startsWith("-")) return {type:"add", target:e, options:{}}
36
+ if (e.length === 1) throw Error("Plugin remove operation requires a target")
37
+ return {type:"remove", target:e.slice(1)}
38
+ }
39
+ ```
40
+
41
+ So an entry is either a bare string, an object `{package, options}`, or a
42
+ **`-<target>` string that removes** a previously-declared plugin. Then the target is
43
+ resolved relative to the file that declared it:
44
+
45
+ ```js
46
+ let g = l.path ? ln.dirname(l.path) : a.directory
47
+ let v = c.target.startsWith("file://") ? dm(c.target)
48
+ : (c.target.startsWith("./") || c.target.startsWith("../"))
49
+ ? ln.resolve(g, c.target)
50
+ : c.target
51
+ ```
52
+
53
+ `[B]` Three branches follow, and they are the whole answer to the installer question:
54
+
55
+ 1. **Not absolute** (a plain npm/git specifier) → handed to the package installer
56
+ (`PluginModule.load` calls `Bun.add(target)` unless `options.install === false`),
57
+ i.e. `"@te-river/opencode-team-mode@latest"` works exactly as documented.
58
+ 2. **Absolute and a FILE** → `Dt("configured plugin path must be a directory", {target})`
59
+ (a warning) and the entry is dropped (`Ie()`).
60
+ 3. **Absolute and a DIRECTORY**, or a relative path that resolved to one → the entrypoint
61
+ is resolved for the directory, and **if that resolution yields no server the entry
62
+ returns `[]` — no log, no warning, no error**:
63
+
64
+ ```js
65
+ let c = yield*e.isDir(l.target)
66
+ let g = c ? yield*y(()=>U0({directory: l.target})) : {server: UN(l.target).href}
67
+ if (!g.server) return [] // <-- silent
68
+ if (c) { let k = resolve(l.target), f = resolve(dm(g.server))
69
+ if (!contains(k, f)) return [] } // entrypoint must live inside the dir
70
+ ```
71
+
72
+ `[L]` This is the branch that ate our afternoon: `plugins: ["C:/…/team-mode"]` pointing at
73
+ a directory that declared only `package.json#exports` produced **one line of output —
74
+ nothing** — and a perfectly healthy-looking host with no Team tools.
75
+
76
+ ## 2. What a directory must contain to be loadable
77
+
78
+ `[B]` `PluginModule.load` (minified `SB`) resolves the entrypoint and only then logs:
79
+
80
+ ```js
81
+ // PluginModule.load (SB): `r` = target is absolute, `i.install === false` skips the
82
+ // package installer, `U0` is the entrypoint resolver, `rL` is the host's log call.
83
+ let p = r ? void 0 : (i?.install === false ? await resolve(e.target) : await add(e.target))
84
+ let l = r && (await stat(e.target)).isFile()
85
+ ? {server: fileURL(e.target).href}
86
+ : await U0(p ?? {directory: e.target})
87
+ let c = l.server
88
+ if (!r && i?.install === false && !c) return {pending: true}
89
+ if (!c) return new LoadError({message: `Plugin entrypoint not found: ${e.target}`})
90
+ yield* log({msg: "loading plugin", id: e.target, entrypoint: c})
91
+ ```
92
+
93
+ and the module must then validate as a definition:
94
+
95
+ ```
96
+ Plugin must export a default definition with an id and an effect or setup function.
97
+ ```
98
+
99
+ `[L]` For a **directory** target, the entrypoint that `U0` returns on 2.0.16 is
100
+ `<dir>/index.js`. Measured with `--print-logs`:
101
+
102
+ ```
103
+ msg="loading plugin" id=…\vendor\team-mode entrypoint=file:///…/vendor/team-mode/index.js
104
+ ```
105
+
106
+ A directory holding only `package.json` (`exports` → `./dist/index.js`, `main` likewise)
107
+ is **not** resolved to `dist/index.js` — it falls into the `if (!g.server) return []`
108
+ branch above and is skipped with no message at all. That is why the package now ships a
109
+ root [`index.js`](../../index.js) `[R]` re-exporting `./dist/index.js`, and why it is in
110
+ `package.json#files`: it is part of the host contract, not a convenience shim.
111
+
112
+ ## 3. Consequences for the installer
113
+
114
+ - `"plugins"` **is** the right key to write, and the documented npm-specifier form works
115
+ without a cache purge: the host's own `opencode plugin add` installs and writes that
116
+ field (`[D]`+`[L]`). v1's purge machinery has nothing to do on v2.
117
+ - A **local** install (this repo, or a copy on disk) must point at a **directory that
118
+ contains `index.js`**, and the value must be relative (`"./vendor/team-mode"`, resolved
119
+ against the config file's own directory) or an absolute directory. An absolute path to
120
+ `index.js` itself is refused as "must be a directory".
121
+ - Whatever path style is used, the only proof of success is the host's own
122
+ `msg="loading plugin"` line or our `v2-boot` trajectory row — never the config write
123
+ returning 0. The silent `return []` has no diagnostic to fail on.
124
+ - Dedup matters: the same plugin reachable both as an npm cache copy and as a local
125
+ directory is loaded twice, and the second registration is the one that wins the hooks.
126
+ `PluginModule.watch` keeps a per-path set, so two *different* paths are two watchers.
127
+
128
+ ## 5. The entrypoint resolver, in full (`U0`)
129
+
130
+ `[B]` Everything above funnels into one small function, and it is the reason a local
131
+ install has exactly one legal shape. Reformatted for reading; identifiers untouched:
132
+
133
+ ```js
134
+ function U0(r){
135
+ let n = (t) => {
136
+ for (let o of t) {
137
+ let i = r.name ? [r.name, o].filter(Boolean).join("/") : s.resolve(r.directory, o || "index")
138
+ try { return R0(i, r.directory) } // resolve as a module specifier
139
+ catch (e) { if (!["ENOENT","ENOTDIR","MODULE_NOT_FOUND","ERR_MODULE_NOT_FOUND",
140
+ "ERR_PACKAGE_PATH_NOT_EXPORTED","ERR_UNSUPPORTED_DIR_IMPORT"]
141
+ .includes(String(e.code))) throw e }
142
+ }
143
+ return // nothing resolved → undefined
144
+ }
145
+ return { server: n(["server",""]), tui: n(["tui"]), rpc: n(["rpc"]) }
146
+ }
147
+ ```
148
+
149
+ Read it as a decision table:
150
+
151
+ | target | what `U0` is called with | what resolves |
152
+ |---|---|---|
153
+ | an installed package | `{name:"@te-river/opencode-team-mode", directory:<cache>}` | the specifier `@te-river/opencode-team-mode/server`, then the package root — i.e. `package.json#exports`, so `dist/index.js` arrives |
154
+ | a directory path | `{directory:<path>}` (no `name`) | `resolve(dir, "index")` → **`<dir>/index.js`** and nothing else |
155
+
156
+ So the asymmetry is not a documentation gap, it is the code: a package may declare its
157
+ entrypoint, a directory may not. A directory that resolves nothing returns
158
+ `{server: undefined}`, and `ConfigPluginSource.scan` then drops the entry with
159
+ `return []` — the silent skip in §1. `[L]` Measured both ways on 2.0.18: with a root
160
+ `index.js` the host logs `entrypoint=file:///…/vendor/team-mode/index.js`; without it,
161
+ the boot is clean and the plugin is simply absent.
162
+
163
+ `features.tui` / `features.rpc` in the load result come from the same call, which is why
164
+ a TUI-only plugin is recorded somewhere else — see §6.
165
+
166
+ ## 6. What `opencode plugin add` actually does
167
+
168
+ `[B]` The subcommand (`cli.plugin.add`) is four steps, and each one closes an option the
169
+ installer might otherwise reach for:
170
+
171
+ ```js
172
+ if (!(yield* ne(()=>pP(e.package))))
173
+ return h(Error("Plugin target must be an npm registry package or Git package specifier"))
174
+ let r = yield* (yield* ga).add(e.package) // install
175
+ let l = U0(r), o = k(l.server, l.tui) // configurationTarget: "server" | "tui"
176
+ if (!o) return h(Error(`Plugin package has no server or TUI entrypoint: ${e.package}`))
177
+ ```
178
+
179
+ - **A local directory is refused**, by name. Vendoring a tree is therefore a config edit,
180
+ never a `plugin add`.
181
+ - A **server** plugin is written by `writePluginConfig` into the global config the Config
182
+ service names (`i.config`). `[L]` Measured with a redirected `HOME`: it created and
183
+ edited `~/.config/opencode/opencode.json` — not the `.jsonc` — and printed the file it
184
+ touched.
185
+ - Its dedupe is `d(t, n)`: `r === n || (typeof r === "object" && r.package === n)`, i.e.
186
+ **the identical string only**. Two spellings of one plugin are two entries.
187
+ - It parses with `allowTrailingComma`, edits the AST (`Ru(doc, ["plugins"], [...t, n])`,
188
+ tab size 2) and swaps the file through `<file>.tmp` + rename, mode `0o600`. So a
189
+ trailing comma in a user's array is legal to the host — and the reason our own writer
190
+ must not leave one dangling is that the NEXT hand edit may not be a lenient parser.
191
+ - A **TUI-only** plugin goes to a different service (`Il`) with its own `plugins` array.
192
+ Our package has a `server` entrypoint, so it always takes the first branch.
193
+
194
+ ## 7. Why two entries really do load twice
195
+
196
+ `[B]` `PluginSupervisor.resolve` iterates the operations and, before loading, asks only
197
+ whether the target matches an already-known plugin **id**:
198
+
199
+ ```js
200
+ let l = (N,O) => N === "*" || (N.endsWith(".*") ? O.startsWith(N.slice(0,-1)) : N === O)
201
+ let O = S().filter((ee) => l(N.target, ee.id))
202
+ if (O.length > 0 || N.target === "*" || …) { O.forEach(ee => g.add(ee.id)); continue }
203
+ let Q = yield* e.load(N, { install: s }) // otherwise: load it
204
+ ```
205
+
206
+ `N.target` is the config string (`@te-river/opencode-team-mode@latest`, or a path) and
207
+ `ee.id` is the plugin's own exported id (`team-mode`). They are never equal, so that
208
+ guard is a no-op for package entries, and a second operation for the same package
209
+ reaches `e.load` again. `[L]` Confirmed: the same spec present in `opencode.json` and
210
+ `opencode.jsonc` produced two `msg="loading plugin"` lines for one id, with no warning —
211
+ two personalities, the same tools and hooks bound twice.
212
+
213
+ A load failure is loud, unlike the directory skip: `failed to load plugin` carries
214
+ `target`, a `ref`, and the cause, and `PluginModule.LoadError` is what `@opencode-aide`
215
+ 1.6.0 produces on a 2.x host —
216
+
217
+ ```
218
+ Plugin must export a default definition with an id and an effect or setup function.
219
+ (cause: SchemaError(Missing key at ["default"]["effect"] / ["default"]["setup"]))
220
+ ```
221
+
222
+ because the accepted shape is a union, `bB = U([ {id, effect:fn}, {id, setup:fn} ])`, and
223
+ a v1-only `{id, server}` default satisfies neither. That is the whole reason this package
224
+ ships one barrel exporting both personalities.
225
+
226
+ ## 8. What the Extensions panel's row name tells you (and what it does not)
227
+
228
+ `[L]` Observed on the user's desktop, before and after a config change: a plugin loaded
229
+ from `~/.config/opencode/plugins/team-mode.js` (the directory-scan path the docs
230
+ describe) is listed as `team-mode`, which is that FILE's basename, while an npm entry is
231
+ listed verbatim as `@te-river/opencode-team-mode@latest`. A `plugins` entry pointing at a
232
+ local directory also listed itself as `team-mode` — the loaded plugin's own id.
233
+
234
+ The mechanism is inferred, the consequence is not: **the row name cannot tell you which
235
+ spelling is installed**, because a file named `team-mode.js` and a package whose id is
236
+ `team-mode` print identically. Two rows that look like one plugin may be two loads, and
237
+ one row may be either a loader file or a directory entry. The only channel that
238
+ distinguishes them is `msg="loading plugin" id=<target> entrypoint=<url>`, which names
239
+ the config string and the resolved file, and our own `v2-boot` trajectory line.
240
+
241
+ **How the installers use this:** `scripts/lib/config-surgery.cjs` keeps exactly one Team
242
+ entry across `opencode.jsonc` and `opencode.json`, matching our own entries by
243
+ `/opencode[-_]?team[-_]?mode|vendor[/\\]team-mode/i` — case- and hyphen-tolerant, because
244
+ a working tree is `D:/Github/Opencode-TeamMode` and an entry it fails to recognise is one
245
+ it would add beside the existing one.
246
+
247
+
248
+ ## 9. What this page does NOT establish
249
+
250
+ `[U]` Whether an object entry's `options` carries anything beyond `install:false` — the
251
+ scanned code reads only that field, which is not a claim that nothing else exists.
252
+ `[U]` Whether the `<dir>/index` resolution survives a future host: it is read off 2.0.16
253
+ and 2.0.18, and the shipped `package.json#exports` stays as the belt while the root
254
+ `index.js` is the part this contract requires. `[U]` Which file the Config service names
255
+ as `i.config` when both `opencode.json` and `opencode.jsonc` exist — measured only in the
256
+ case where the `.jsonc` was absent, so the installers do not depend on it: they write the
257
+ `.jsonc` and reclaim their own entry from the `.json`. `[U]` Whether the host prunes the
258
+ timestamped `~/.cache/opencode/npm/<pkg>@latest/<ts>/` directories: one existed after
259
+ several installs on this machine, which is an observation, not a garbage-collection
260
+ guarantee.
261
+