@te-river/opencode-team-mode 1.7.6 → 1.7.8

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 (59) hide show
  1. package/README.md +1010 -1009
  2. package/README.zh-CN.md +907 -906
  3. package/dist/host/v2-browser-gate.js +2 -2
  4. package/dist/host/v2-guard.d.ts +135 -0
  5. package/dist/host/v2-guard.d.ts.map +1 -1
  6. package/dist/host/v2-guard.js +227 -2
  7. package/dist/host/v2-guard.js.map +1 -1
  8. package/dist/host/v2-offload.js +6 -6
  9. package/dist/host/v2-retry.d.ts +9 -1
  10. package/dist/host/v2-retry.d.ts.map +1 -1
  11. package/dist/host/v2-retry.js +10 -2
  12. package/dist/host/v2-retry.js.map +1 -1
  13. package/dist/host/v2-subagent.d.ts +21 -0
  14. package/dist/host/v2-subagent.d.ts.map +1 -1
  15. package/dist/host/v2-subagent.js +55 -9
  16. package/dist/host/v2-subagent.js.map +1 -1
  17. package/dist/host/v2.d.ts.map +1 -1
  18. package/dist/host/v2.js +77 -1
  19. package/dist/host/v2.js.map +1 -1
  20. package/dist/prompts/specialists.js +286 -286
  21. package/dist/task-offload.js +1 -1
  22. package/dist/tm/config-layers.d.ts +5 -2
  23. package/dist/tm/config-layers.d.ts.map +1 -1
  24. package/dist/tm/config-layers.js +10 -6
  25. package/dist/tm/config-layers.js.map +1 -1
  26. package/dist/tm/config.d.ts +3 -0
  27. package/dist/tm/config.d.ts.map +1 -1
  28. package/dist/tm/config.js +7 -1
  29. package/dist/tm/config.js.map +1 -1
  30. package/dist/tm/index.d.ts +5 -1
  31. package/dist/tm/index.d.ts.map +1 -1
  32. package/dist/tm/index.js +1 -1
  33. package/dist/tm/index.js.map +1 -1
  34. package/dist/tm/ledger.js +5 -5
  35. package/dist/tm/memory.js +9 -9
  36. package/dist/tm/search.d.ts +3 -0
  37. package/dist/tm/search.d.ts.map +1 -1
  38. package/dist/tm/search.js +67 -15
  39. package/dist/tm/search.js.map +1 -1
  40. package/dist/tm/stats.d.ts.map +1 -1
  41. package/dist/tm/stats.js +23 -0
  42. package/dist/tm/stats.js.map +1 -1
  43. package/dist/tm/tools.js +3 -3
  44. package/dist/tm/webfetch.d.ts +94 -5
  45. package/dist/tm/webfetch.d.ts.map +1 -1
  46. package/dist/tm/webfetch.js +285 -64
  47. package/dist/tm/webfetch.js.map +1 -1
  48. package/docs/installation-v2.md +2 -2
  49. package/docs/installation.md +505 -505
  50. package/docs/research/agent-data-exchange.md +117 -117
  51. package/docs/research/browser-pane.md +94 -94
  52. package/docs/research/host-http-api.md +132 -132
  53. package/docs/research/host-subagent-injection.md +187 -187
  54. package/docs/research/plugin-loader-contract.md +261 -261
  55. package/docs/research/v2-builtin-tools.md +447 -447
  56. package/docs/v2-port-notes.md +121 -78
  57. package/index.js +7 -7
  58. package/package.json +64 -64
  59. package/scripts/lib/config-surgery.cjs +239 -239
@@ -1,132 +1,132 @@
1
- # OpenCode 2.0.18 — the host's own HTTP API, and what it changes for a plugin
2
-
3
- **Status: mapped, not called.** Every route below was read out of the installed host's
4
- embedded JS (the SDK client factory: `method:"GET", path:"/api/…"` entries, both literal and
5
- template-literal), cross-checked against the official v2 API page for the plugin endpoints. **No
6
- request has been made to any of them** — so "exists in the client" is not the same claim as
7
- "works from a plugin process", and nothing here is a verified capability.
8
-
9
- **Why it matters.** The v2 plugin context is thin: no SDK client, no `session.children`, no
10
- message domain, no dialog. Several conclusions this repo recorded as host limitations are
11
- actually *ctx* limitations — the server behind the ctx has a much larger surface. This page
12
- separates the two so a future change does not re-accept a limitation that is really a missing
13
- transport.
14
-
15
- ## 1. Discovery and authentication
16
-
17
- `[B]`+`[D]` `GET /api/info` returns "the server identity, connection URLs, paths, and
18
- readiness status". The CLI reaches a server through a helper that yields `{url, password}`, and
19
- the transport is HTTP Basic with a fixed username:
20
-
21
- ```js
22
- { type: "basic", username: "opencode", password: e.password }
23
- await fetch(new URL("/api/info", e.url), { headers: X(i), signal: AbortSignal.timeout(r) })
24
- ```
25
-
26
- `[B]` The password is **not** per-process random-only: `<global-config>/service.json` holds a
27
- single `password` string (43 chars on this machine). The port is dynamic (`serve --stdio
28
- --port 0`), which is why `/api/info` exists.
29
-
30
- `[U]` Whether a plugin can obtain `{url, password}` from its own context instead of reading
31
- that file is the open question, and it is the whole cost of every item below: **reading the
32
- user's credential file to call back into the host is a new trust boundary**, and a plugin that
33
- does it silently would be doing exactly what goal #6 forbids. Nothing in this repo does it
34
- today, and `src/host/v2-session-client.ts` says so where the temptation is documented.
35
-
36
- ## 2. The routes that touch our recorded conclusions
37
-
38
- | route family (`[B]`) | what it offers | which claim it reopens |
39
- |---|---|---|
40
- | `GET /api/plugin`, `POST /api/plugin/check`, `POST /api/plugin/update` | the enabled server plugins **with live status**, plus update checking | the install-verification story: this is a cleaner arrival check than grepping `msg="loading plugin"` (which is what `docs/installation-v2.md` currently tells an agent to do) |
41
- | `GET /api/session/{id}/message`, `GET /api/session/{id}/message/{messageID}`, `GET /api/session/{id}/context` | read a session's messages — including a CHILD session | `tm_join`'s "the body arrives by injection, we cannot read it" limit. The in-process `ctx.session.context` is tried first (see §4); the API is the fallback with a price |
42
- | `GET /api/permission/request`, `POST /api/session/{id}/permission`, `GET /api/permission/saved`, `DELETE /api/permission/saved/{id}` | the permission queue, answering it, and the saved-rule store | **"a plugin cannot raise a dialog on v2"** — that was measured against the ctx (`options.permission` on our own tool produced no evaluation). The server clearly has a dialog surface; whether a plugin may drive it is unmeasured, and driving it *for* the user would be self-allowing, which this project never does |
43
- | `GET /api/pty`, `POST /api/pty`, `GET /api/pty/{id}/output?`, `GET /api/experimental/persistent-pty/{id}`, `.../snapshot`, `.../connect-token`, `POST /api/experimental/persistent-pty/{id}/connect-token` | terminal sessions with **snapshot/read**, i.e. exactly the capture capability `tm_pty` lacked | task #21 ("v2 has no `client.pty` → retire `tm_pty`") was decided on the ctx surface. The host has a persistent-pty API. Retirement still looks right (a plugin spawning terminals the user cannot see is its own problem), but the reason is now "no in-process seam + trust boundary", not "the host has no such thing" |
44
- | `GET /api/fs/read/{path}`, `GET /api/fs/list`, `GET /api/fs/find`, `POST /api/experimental/fs/write` | file access through the host's own permission layer | reinforces the 1.7.x decision to retire `tm_read`/`tm_grep`/`tm_bash` in favour of the native ladder — the native path is where the host's `external_directory` dialog lives |
45
- | `POST /api/websearch`, `GET /api/websearch/provider` | server-side search | all four native providers need a key/`/connect`, so `tm_search`'s zero-key front stays (unchanged conclusion, now with the reason visible) |
46
- | `POST /api/session/{id}/wait`, `POST /api/experimental/session/{id}/wait`, `GET /api/experimental/session/stats`, `GET /api/experimental/session/{id}/export`, `POST /api/session/{id}/synthetic`, `.../interrupt`, `.../compact`, `.../fork` | waiting on and injecting into sessions | the shape of what a *session-owning* API looks like — useful for judging whether `tm_join`'s bounded wait is fighting the host or duplicating it |
47
- | `GET /api/event` | the SSE feed | `ctx.event.subscribe()` is this stream, in-process. No reason to prefer HTTP |
48
-
49
- ## 3. The question the user actually asked: is there a todo list?
50
-
51
- **No.** `[B]` Across **136 distinct `METHOD /path` pairs** in the scanned window (55 literal +
52
- 81 template-literal paths, deduplicated — a lower bound, since the scan covered 2 MB of the
53
- embedded JS rather than the whole client factory) there is no `todo`, no `plan`, no `task`
54
- resource; the only `message` routes are `GET /api/session/{id}/message` and
55
- `GET /api/session/{id}/message/{messageID}`. `[D]` The official API page lists none either. The
56
- v1 REST endpoint `GET /session/{id}/todo` that `tm_join`'s goal tripwire was written against
57
- does not exist on 2.x, which is the same fact as "the host has no `todowrite` tool" seen from
58
- the other side. So `tm_ledger` is not a workaround we settled for: it is the only ledger there
59
- is. What *is* available for the same purpose is `GET /api/session/{id}/context` (in-process)
60
- and `/message` (HTTP) — reading what the model was told, rather than a list the model
61
- maintains.
62
-
63
- ## 4. What was verified instead, in-process
64
-
65
- `[B]` The host's own call sites give the plugin-facing argument shape, and that fixed a bridge
66
- that had never worked:
67
-
68
- ```
69
- te = (schema, fn) => (input) => decode(schema, input ?? {}) → fn(decoded)
70
- await e.session.get({ sessionID: n })
71
- await e.message.list({ sessionID: n, limit: 200, cursor: r })
72
- ```
73
-
74
- `ctx.session.get` therefore takes a **single flat object `{sessionID}`** — not v1's
75
- `{path:{id}}`, and not an array. `src/host/v2-session-client.ts` had both wrong, which is why
76
- every lookup failed silently for a release cycle.
77
-
78
- `[L]` **The full ctx domain set, measured 2026-09-26 with zero model tokens** (a plugin that
79
- only inspects its own context, booted under `--standalone --model nope/nope`; every method
80
- called with `{}` so the host's own decode error names the required key):
81
-
82
- | domain | what the host gives a plugin |
83
- |---|---|
84
- | `session` | `hook create get switchAgent switchModel prompt generate command synthetic interrupt update move wait context` |
85
- | `permission` | `hook list get reply` — all of `list/get/reply` decode as `{sessionID}` |
86
- | `experimental` | `terminal.read` (decodes as `{sessionID}`; a terminal-id spelling has not been found) |
87
- | `storage` | `get set remove scan` |
88
- | `agent` / `model` / `command` / `mcp` / `worktree` | `list/get/transform/reload` families |
89
- | `event` | `subscribe` (an async iterable, already used by `v2-events.ts`) |
90
- | `rpc` | a single function of arity 1 — its calling convention has NOT been read out |
91
-
92
- `[L]` **`ctx.session.context({sessionID})` answers with an ARRAY of flat items**, measured
93
- against a real background child from a live round:
94
-
95
- ```
96
- array(5) < { id: string, time: { created: number }, text: string, type: string } // type "user" …
97
- ```
98
-
99
- That is NOT v1's `[{info:{role},parts:[…]}]`, and it is the reason a child's report was lost for
100
- a release: `v2-session-client.ts` already called `context`, then handed the array to
101
- `lastAssistantMessage`, which tolerates an unknown shape by returning no text. The seam
102
- ANSWERED and the body was discarded, while `tm_join` told the lead 正文不经本工具 — our decode
103
- miss reported as the host's limit. The flat shape is now normalised at the seam
104
- (`normaliseContextMessages`), and no `time.completed` is invented there, because that field *is*
105
- the settle verdict.
106
-
107
- ## 5. Deliberate non-goals
108
-
109
- `[U]` No HTTP call is made by this plugin today. The reasons are recorded rather than implied:
110
- the credential read is a trust boundary the user has not been asked about; a plugin answering
111
- `/api/permission/request` would be self-allowing; and a plugin that can `POST
112
- /api/experimental/fs/write` bypasses the very `session.hook("context")` trim that keeps a
113
- denied role from being offered a tool. If any of these is ever pursued, it goes through the
114
- same gate as everything else here: Team-scoped, counted, and visible in `tm_stats`.
115
-
116
- ## 6. The #32 decision, and what closed it
117
-
118
- The user authorised the HTTP API on one condition — 保证不会被泄露. That condition is the reason
119
- §4's probe happened first, and the probe made the condition moot:
120
-
121
- - **子会话正文**: `ctx.session.context` in process (§4). No credential exists to leak.
122
- - **权限队列**: `ctx.permission.list({sessionID})` answers in process — verified as a seam,
123
- NOT yet used. Reading it to tell the user "N dialogs are waiting" is legitimate; calling
124
- `reply` is not, because that is self-allowing, and `reply` stays uncalled.
125
- - **pty 捕获**: `ctx.experimental.terminal.read({sessionID})` exists but its second argument
126
- has not been found, so `tm_pty`'s retirement decision (§21) is unchanged for now — the
127
- reason is updated from "the host has no such thing" to "the plugin seam is unread".
128
-
129
- What the scan still settles on its own: there is **no todo/plan/task resource anywhere**, so
130
- `tm_ledger` is not a compromise. And `~/.config/opencode/service.json` has never been opened by
131
- this plugin, which `test-v2-adapter.mjs` group 12 asserts against the shipped source.
132
-
1
+ # OpenCode 2.0.18 — the host's own HTTP API, and what it changes for a plugin
2
+
3
+ **Status: mapped, not called.** Every route below was read out of the installed host's
4
+ embedded JS (the SDK client factory: `method:"GET", path:"/api/…"` entries, both literal and
5
+ template-literal), cross-checked against the official v2 API page for the plugin endpoints. **No
6
+ request has been made to any of them** — so "exists in the client" is not the same claim as
7
+ "works from a plugin process", and nothing here is a verified capability.
8
+
9
+ **Why it matters.** The v2 plugin context is thin: no SDK client, no `session.children`, no
10
+ message domain, no dialog. Several conclusions this repo recorded as host limitations are
11
+ actually *ctx* limitations — the server behind the ctx has a much larger surface. This page
12
+ separates the two so a future change does not re-accept a limitation that is really a missing
13
+ transport.
14
+
15
+ ## 1. Discovery and authentication
16
+
17
+ `[B]`+`[D]` `GET /api/info` returns "the server identity, connection URLs, paths, and
18
+ readiness status". The CLI reaches a server through a helper that yields `{url, password}`, and
19
+ the transport is HTTP Basic with a fixed username:
20
+
21
+ ```js
22
+ { type: "basic", username: "opencode", password: e.password }
23
+ await fetch(new URL("/api/info", e.url), { headers: X(i), signal: AbortSignal.timeout(r) })
24
+ ```
25
+
26
+ `[B]` The password is **not** per-process random-only: `<global-config>/service.json` holds a
27
+ single `password` string (43 chars on this machine). The port is dynamic (`serve --stdio
28
+ --port 0`), which is why `/api/info` exists.
29
+
30
+ `[U]` Whether a plugin can obtain `{url, password}` from its own context instead of reading
31
+ that file is the open question, and it is the whole cost of every item below: **reading the
32
+ user's credential file to call back into the host is a new trust boundary**, and a plugin that
33
+ does it silently would be doing exactly what goal #6 forbids. Nothing in this repo does it
34
+ today, and `src/host/v2-session-client.ts` says so where the temptation is documented.
35
+
36
+ ## 2. The routes that touch our recorded conclusions
37
+
38
+ | route family (`[B]`) | what it offers | which claim it reopens |
39
+ |---|---|---|
40
+ | `GET /api/plugin`, `POST /api/plugin/check`, `POST /api/plugin/update` | the enabled server plugins **with live status**, plus update checking | the install-verification story: this is a cleaner arrival check than grepping `msg="loading plugin"` (which is what `docs/installation-v2.md` currently tells an agent to do) |
41
+ | `GET /api/session/{id}/message`, `GET /api/session/{id}/message/{messageID}`, `GET /api/session/{id}/context` | read a session's messages — including a CHILD session | `tm_join`'s "the body arrives by injection, we cannot read it" limit. The in-process `ctx.session.context` is tried first (see §4); the API is the fallback with a price |
42
+ | `GET /api/permission/request`, `POST /api/session/{id}/permission`, `GET /api/permission/saved`, `DELETE /api/permission/saved/{id}` | the permission queue, answering it, and the saved-rule store | **"a plugin cannot raise a dialog on v2"** — that was measured against the ctx (`options.permission` on our own tool produced no evaluation). The server clearly has a dialog surface; whether a plugin may drive it is unmeasured, and driving it *for* the user would be self-allowing, which this project never does |
43
+ | `GET /api/pty`, `POST /api/pty`, `GET /api/pty/{id}/output?`, `GET /api/experimental/persistent-pty/{id}`, `.../snapshot`, `.../connect-token`, `POST /api/experimental/persistent-pty/{id}/connect-token` | terminal sessions with **snapshot/read**, i.e. exactly the capture capability `tm_pty` lacked | task #21 ("v2 has no `client.pty` → retire `tm_pty`") was decided on the ctx surface. The host has a persistent-pty API. Retirement still looks right (a plugin spawning terminals the user cannot see is its own problem), but the reason is now "no in-process seam + trust boundary", not "the host has no such thing" |
44
+ | `GET /api/fs/read/{path}`, `GET /api/fs/list`, `GET /api/fs/find`, `POST /api/experimental/fs/write` | file access through the host's own permission layer | reinforces the 1.7.x decision to retire `tm_read`/`tm_grep`/`tm_bash` in favour of the native ladder — the native path is where the host's `external_directory` dialog lives |
45
+ | `POST /api/websearch`, `GET /api/websearch/provider` | server-side search | all four native providers need a key/`/connect`, so `tm_search`'s zero-key front stays (unchanged conclusion, now with the reason visible) |
46
+ | `POST /api/session/{id}/wait`, `POST /api/experimental/session/{id}/wait`, `GET /api/experimental/session/stats`, `GET /api/experimental/session/{id}/export`, `POST /api/session/{id}/synthetic`, `.../interrupt`, `.../compact`, `.../fork` | waiting on and injecting into sessions | the shape of what a *session-owning* API looks like — useful for judging whether `tm_join`'s bounded wait is fighting the host or duplicating it |
47
+ | `GET /api/event` | the SSE feed | `ctx.event.subscribe()` is this stream, in-process. No reason to prefer HTTP |
48
+
49
+ ## 3. The question the user actually asked: is there a todo list?
50
+
51
+ **No.** `[B]` Across **136 distinct `METHOD /path` pairs** in the scanned window (55 literal +
52
+ 81 template-literal paths, deduplicated — a lower bound, since the scan covered 2 MB of the
53
+ embedded JS rather than the whole client factory) there is no `todo`, no `plan`, no `task`
54
+ resource; the only `message` routes are `GET /api/session/{id}/message` and
55
+ `GET /api/session/{id}/message/{messageID}`. `[D]` The official API page lists none either. The
56
+ v1 REST endpoint `GET /session/{id}/todo` that `tm_join`'s goal tripwire was written against
57
+ does not exist on 2.x, which is the same fact as "the host has no `todowrite` tool" seen from
58
+ the other side. So `tm_ledger` is not a workaround we settled for: it is the only ledger there
59
+ is. What *is* available for the same purpose is `GET /api/session/{id}/context` (in-process)
60
+ and `/message` (HTTP) — reading what the model was told, rather than a list the model
61
+ maintains.
62
+
63
+ ## 4. What was verified instead, in-process
64
+
65
+ `[B]` The host's own call sites give the plugin-facing argument shape, and that fixed a bridge
66
+ that had never worked:
67
+
68
+ ```
69
+ te = (schema, fn) => (input) => decode(schema, input ?? {}) → fn(decoded)
70
+ await e.session.get({ sessionID: n })
71
+ await e.message.list({ sessionID: n, limit: 200, cursor: r })
72
+ ```
73
+
74
+ `ctx.session.get` therefore takes a **single flat object `{sessionID}`** — not v1's
75
+ `{path:{id}}`, and not an array. `src/host/v2-session-client.ts` had both wrong, which is why
76
+ every lookup failed silently for a release cycle.
77
+
78
+ `[L]` **The full ctx domain set, measured 2026-09-26 with zero model tokens** (a plugin that
79
+ only inspects its own context, booted under `--standalone --model nope/nope`; every method
80
+ called with `{}` so the host's own decode error names the required key):
81
+
82
+ | domain | what the host gives a plugin |
83
+ |---|---|
84
+ | `session` | `hook create get switchAgent switchModel prompt generate command synthetic interrupt update move wait context` |
85
+ | `permission` | `hook list get reply` — all of `list/get/reply` decode as `{sessionID}` |
86
+ | `experimental` | `terminal.read` (decodes as `{sessionID}`; a terminal-id spelling has not been found) |
87
+ | `storage` | `get set remove scan` |
88
+ | `agent` / `model` / `command` / `mcp` / `worktree` | `list/get/transform/reload` families |
89
+ | `event` | `subscribe` (an async iterable, already used by `v2-events.ts`) |
90
+ | `rpc` | a single function of arity 1 — its calling convention has NOT been read out |
91
+
92
+ `[L]` **`ctx.session.context({sessionID})` answers with an ARRAY of flat items**, measured
93
+ against a real background child from a live round:
94
+
95
+ ```
96
+ array(5) < { id: string, time: { created: number }, text: string, type: string } // type "user" …
97
+ ```
98
+
99
+ That is NOT v1's `[{info:{role},parts:[…]}]`, and it is the reason a child's report was lost for
100
+ a release: `v2-session-client.ts` already called `context`, then handed the array to
101
+ `lastAssistantMessage`, which tolerates an unknown shape by returning no text. The seam
102
+ ANSWERED and the body was discarded, while `tm_join` told the lead 正文不经本工具 — our decode
103
+ miss reported as the host's limit. The flat shape is now normalised at the seam
104
+ (`normaliseContextMessages`), and no `time.completed` is invented there, because that field *is*
105
+ the settle verdict.
106
+
107
+ ## 5. Deliberate non-goals
108
+
109
+ `[U]` No HTTP call is made by this plugin today. The reasons are recorded rather than implied:
110
+ the credential read is a trust boundary the user has not been asked about; a plugin answering
111
+ `/api/permission/request` would be self-allowing; and a plugin that can `POST
112
+ /api/experimental/fs/write` bypasses the very `session.hook("context")` trim that keeps a
113
+ denied role from being offered a tool. If any of these is ever pursued, it goes through the
114
+ same gate as everything else here: Team-scoped, counted, and visible in `tm_stats`.
115
+
116
+ ## 6. The #32 decision, and what closed it
117
+
118
+ The user authorised the HTTP API on one condition — 保证不会被泄露. That condition is the reason
119
+ §4's probe happened first, and the probe made the condition moot:
120
+
121
+ - **子会话正文**: `ctx.session.context` in process (§4). No credential exists to leak.
122
+ - **权限队列**: `ctx.permission.list({sessionID})` answers in process — verified as a seam,
123
+ NOT yet used. Reading it to tell the user "N dialogs are waiting" is legitimate; calling
124
+ `reply` is not, because that is self-allowing, and `reply` stays uncalled.
125
+ - **pty 捕获**: `ctx.experimental.terminal.read({sessionID})` exists but its second argument
126
+ has not been found, so `tm_pty`'s retirement decision (§21) is unchanged for now — the
127
+ reason is updated from "the host has no such thing" to "the plugin seam is unread".
128
+
129
+ What the scan still settles on its own: there is **no todo/plan/task resource anywhere**, so
130
+ `tm_ledger` is not a compromise. And `~/.config/opencode/service.json` has never been opened by
131
+ this plugin, which `test-v2-adapter.mjs` group 12 asserts against the shipped source.
132
+