opencode-zen-freex 0.2.8 → 0.2.10
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/LICENSE +2 -9
- package/MAINTAINING.md +55 -3
- package/NOTICE +18 -0
- package/README.md +6 -4
- package/core-tools.mjs +85 -7
- package/fingerprint.json +32 -32
- package/index.mjs +14 -4
- package/limits.mjs +221 -57
- package/models.mjs +27 -3
- package/package.json +14 -6
- package/provider.mjs +111 -22
- package/stream.mjs +119 -21
- package/tool-response.mjs +56 -11
- package/capture.mjs +0 -231
- package/limits-audit.mjs +0 -340
- package/tools-smoke.mjs +0 -234
package/LICENSE
CHANGED
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2026 xiaozhou26
|
|
4
|
-
Copyright (c) 2026
|
|
5
|
-
|
|
6
|
-
This is a maintained fork of the OpenCode Zen Free plugin from
|
|
7
|
-
magpie-community/plugins, which is MIT licensed and carries the notice above:
|
|
8
|
-
the `Copyright (c) 2026 xiaozhou26` line is upstream's and has to be kept with
|
|
9
|
-
it, and the `opencode-zen-free contributors` line covers this fork.
|
|
10
|
-
The fork is maintained separately because the free tier's request fingerprint
|
|
11
|
-
moves faster than that repository follows it.
|
|
4
|
+
Copyright (c) 2026 OpenCode-Zen-FreeX contributors
|
|
12
5
|
|
|
13
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
7
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -26,4 +19,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
26
19
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
20
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
21
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
-
SOFTWARE.
|
|
22
|
+
SOFTWARE.
|
package/MAINTAINING.md
CHANGED
|
@@ -156,13 +156,21 @@ It reports each request as:
|
|
|
156
156
|
| `refused` | Zen did not recognise it (403) |
|
|
157
157
|
| `superseded` | Zen wants a newer OpenCode than the profile claims (426) |
|
|
158
158
|
| `failed` | Zen answered with something other than a 403, so it never judged the fingerprint |
|
|
159
|
+
| `malformed` | the request could not be built: a name in `required` has no tool behind it in `fingerprint.json`. Zen was never asked, and the line names the generation, the shape and the name |
|
|
159
160
|
| `unreachable` | Zen could not be asked at all |
|
|
160
161
|
|
|
162
|
+
A `malformed` row is a `fingerprint.json` problem, not a network one, and the
|
|
163
|
+
summary says so rather than sending the reader to `capture`. It is deliberately
|
|
164
|
+
not counted as `answered`: a run whose floors are all `malformed` exits non-zero.
|
|
165
|
+
|
|
161
166
|
The `floor` decides the exit code, because it is the shape this plugin exists to
|
|
162
167
|
send. The `full` is reported for what it says about a wording that has gone
|
|
163
168
|
stale. Asking only the full shape left a real gap: `read` alone is `answered` and
|
|
164
169
|
everything but `read` is `refused`, so a Zen that began insisting on one more
|
|
165
|
-
name would keep `full` passing while the shape actually sent was refused.
|
|
170
|
+
name would keep `full` passing while the shape actually sent was refused. The
|
|
171
|
+
diagnosis at the end reads the floor rows only: a stray 5xx on a `full` request
|
|
172
|
+
says something about that request and nothing about whether the wording is
|
|
173
|
+
current, so it must not turn "nothing was served" into "check the network".
|
|
166
174
|
|
|
167
175
|
Only a 403 and a 426 are Zen answering about the fingerprint. A 5xx, a 429 or a
|
|
168
176
|
401 says nothing about the wording, and the gate says so rather than sending the
|
|
@@ -173,6 +181,17 @@ part of a run answered and part of it did not.
|
|
|
173
181
|
locally and reads the request it makes, so nothing is sent to Zen and no key is
|
|
174
182
|
involved.
|
|
175
183
|
|
|
184
|
+
The wording it reads is the host's, so the host's own details are put back to
|
|
185
|
+
something neutral before the capture is written: the OS banner, the scratch
|
|
186
|
+
directory, and now the shell's version. That last one was live — opencode 1.19.0
|
|
187
|
+
words `bash`'s description after the machine it runs on, one host saying
|
|
188
|
+
"Executes a given Windows PowerShell (5.1) command…" and another "PowerShell
|
|
189
|
+
(7+)…", and Zen reads those bytes out of the request, so two machines disagreed
|
|
190
|
+
about a description that belongs to neither. The version is dropped, which is
|
|
191
|
+
also what a capture on Linux already says; the live gate answered the neutral
|
|
192
|
+
wording four times running on 2026-10-16. What any wording is worth is settled by
|
|
193
|
+
`check:gate`, never by this file.
|
|
194
|
+
|
|
176
195
|
## Reading a failure
|
|
177
196
|
|
|
178
197
|
**Every `floor` `refused`.** OpenCode reworded its tools, or moved the endpoint.
|
|
@@ -195,6 +214,13 @@ name is served either way, which is why only the `floor` shows this.
|
|
|
195
214
|
"your client is too old", which is a different problem from 403 and is not fixed
|
|
196
215
|
by changing tools.
|
|
197
216
|
|
|
217
|
+
**`malformed`.** `fingerprint.json` names a tool in `required` that the same
|
|
218
|
+
profile does not carry under `tools`, so the gate could not build the request it
|
|
219
|
+
wants to ask with. This is not a network problem and not something Zen said: the
|
|
220
|
+
line names the generation, the shape and the missing name. Fix the file — a
|
|
221
|
+
capture from a current OpenCode is the usual way — and ask again. The run exits
|
|
222
|
+
non-zero, and the summary says the same thing in one sentence.
|
|
223
|
+
|
|
198
224
|
**`unreachable`.** Not a fingerprint problem. Check the network, and whether
|
|
199
225
|
repeated refusals from this address have put it in a cooldown — they can, and
|
|
200
226
|
the cooldown answers like a refusal.
|
|
@@ -261,9 +287,18 @@ that passes under the stricter of them, since the looser one will not last.
|
|
|
261
287
|
| `models.mjs` | which models are free, and what protocol each speaks |
|
|
262
288
|
| `limits.mjs` | what a model's limits are, where the catalogue under-reports them |
|
|
263
289
|
| `stream.mjs`, `tool-response.mjs` | replies Zen cuts short, and tool calls |
|
|
290
|
+
| `limits-audit.mjs` | asks the route which ids it still offers and which look under-declared (`limits:audit`) |
|
|
264
291
|
| `capture.mjs` | takes a new capture |
|
|
265
292
|
| `tools-smoke.mjs` | asks Zen what it accepts (`check:gate`) |
|
|
266
293
|
|
|
294
|
+
`capture.mjs`, `limits-audit.mjs` and `tools-smoke.mjs` are maintenance tools for
|
|
295
|
+
this repository, not part of what a host loads: they are not in `package.json`'s
|
|
296
|
+
`files`, so a published tarball carries only the modules a host imports plus
|
|
297
|
+
`fingerprint.json`. `package.json`'s `exports` names those public modules one by
|
|
298
|
+
one rather than mapping `./*`, so a file that was never meant to be imported
|
|
299
|
+
cannot be reached as a subpath — `capture.mjs` in particular starts a server and
|
|
300
|
+
rewrites `fingerprint.json` the moment it is imported.
|
|
301
|
+
|
|
267
302
|
## Which copy of the icon and the concurrency is authoritative
|
|
268
303
|
|
|
269
304
|
`icon` and `maxConcurrency` are declared twice, and neither copy can be read by
|
|
@@ -288,8 +323,18 @@ about the commit rather than about whatever Bun was newest that morning.
|
|
|
288
323
|
Nothing here is installed with Bun — there are no dependencies — so Bun is the
|
|
289
324
|
tools only. `import.meta.main` is what decides whether `tools-smoke.mjs` runs its
|
|
290
325
|
gate or is being imported by a test, and a Node older than 22.18 does not define
|
|
291
|
-
it: the gate falls back to comparing the entry path
|
|
292
|
-
|
|
326
|
+
it: the gate falls back to comparing the entry path. That comparison resolves
|
|
327
|
+
both paths and then their real paths, so a symlink or a junction naming this file
|
|
328
|
+
is still recognised as this file, and a hard link is compared by device and inode
|
|
329
|
+
(only where the filesystem reports an inode at all). A question it cannot answer
|
|
330
|
+
exits non-zero saying nothing was asked — never a silent success, which is the
|
|
331
|
+
one outcome a release gate must not have.
|
|
332
|
+
|
|
333
|
+
Both scripts read their endpoint and model from the environment when the
|
|
334
|
+
defaults are wrong: `ZEN_BASE_URL`, `ZEN_CHECK_MODEL` (gate), `ZEN_CATALOG_URL`,
|
|
335
|
+
`ZEN_GENERATION` (audit) and `CAPTURE_PORT` (capture). `.env.example` lists them
|
|
336
|
+
with their defaults. Nothing loads a file by itself, so export them or copy that
|
|
337
|
+
file to `.env` and export from it.
|
|
293
338
|
|
|
294
339
|
## Publishing
|
|
295
340
|
|
|
@@ -305,6 +350,13 @@ that order, so the tarball, the commit the tag names, and the tree both were
|
|
|
305
350
|
built from are one thing. `magpie plugin add opencode-zen-freex` installs it by
|
|
306
351
|
that name.
|
|
307
352
|
|
|
353
|
+
What ships is `package.json`'s `files` list, and it is deliberately narrower than
|
|
354
|
+
the repository: the three maintenance scripts (`capture.mjs`, `tools-smoke.mjs`,
|
|
355
|
+
`limits-audit.mjs`) stay here, because a host never loads them and one of them
|
|
356
|
+
rewrites the shipped `fingerprint.json` if it is ever imported. `LICENSE` is
|
|
357
|
+
verbatim MIT; the fork's provenance is in `NOTICE`, so a licence scanner reads a
|
|
358
|
+
standard MIT file and a reader still finds why this fork exists.
|
|
359
|
+
|
|
308
360
|
**The second factor is a browser, not a password.** Run `npm publish` in a
|
|
309
361
|
terminal. When the account has 2FA on, the command stops and offers the whole
|
|
310
362
|
exchange at once:
|
package/NOTICE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# NOTICE
|
|
2
|
+
|
|
3
|
+
This package is a maintained fork of the OpenCode Zen Free plugin from
|
|
4
|
+
[magpie-community/plugins](https://github.com/magpie-community/plugins), which
|
|
5
|
+
is MIT licensed. Upstream's notice, `Copyright (c) 2026 xiaozhou26`, is kept in
|
|
6
|
+
[LICENSE](./LICENSE) alongside this fork's own.
|
|
7
|
+
|
|
8
|
+
The fork is maintained separately from upstream under the name
|
|
9
|
+
`opencode-zen-freex` (https://github.com/tkhs101/OpenCode-Zen-FreeX) because the
|
|
10
|
+
free tier's request fingerprint moves faster than that repository follows it:
|
|
11
|
+
the tool wording and User-Agent Zen accepts can be recaptured here with
|
|
12
|
+
`bun run capture`, without waiting for a release upstream. `MAINTAINING.md`
|
|
13
|
+
describes that loop.
|
|
14
|
+
|
|
15
|
+
The provider id this package registers, `opencode-zen-free`, is deliberately the
|
|
16
|
+
same one upstream's plugin registers — a model is written
|
|
17
|
+
`opencode-zen-free/<model>` either way — so the two are mutually exclusive;
|
|
18
|
+
install one.
|
package/README.md
CHANGED
|
@@ -66,7 +66,9 @@ bun run check:gate
|
|
|
66
66
|
| `floor` | `required` 里的名字 | 宿主一个都没有时,插件替它补上的那几个。**这才是插件真实发出的形状,退出码由它决定** |
|
|
67
67
|
| `full` | 这一代携带的全部工具 | 宿主本来就是 OpenCode 时发出来的形状。用来看措辞有没有过期,不决定退出码 |
|
|
68
68
|
|
|
69
|
-
每一代的结论是 `answered`(正常应答)、`refused`(被拒,403)、`superseded`(426——Zen 要求的 OpenCode
|
|
69
|
+
每一代的结论是 `answered`(正常应答)、`refused`(被拒,403)、`superseded`(426——Zen 要求的 OpenCode 比这份指纹声称的更新)、`failed`(Zen 答了,但不是 403,所以它没有对指纹表态)、`malformed`(请求根本没法构造:`required` 里的某个名字在 `fingerprint.json` 里没有对应的工具。Zen 没被问到,那一行会点名是哪一代、哪种形状、缺的是哪个名字)或 `unreachable`(根本联系不上)。只要还有一代的 `floor` 被应答,退出码就是 0;`malformed` 不算应答,所以整轮都是它的时候退出码非 0,并且会说明这是指纹文件的问题、不是网络的问题。
|
|
70
|
+
|
|
71
|
+
某个 `floor` 被拒、而它自己的 `full` 被应答时会单独点出来:那意味着带着自己那套 OpenCode 工具来的宿主能用,而插件真正要服务的那种宿主会被拒。全部联系不上时它不会说指纹过期,只会说问不到——这两件事得分开,而且最后的判断只读 `floor` 那几行:某个 `full` 请求拿到 5xx 说明的是那次请求,不足以把「没有一代被服务」说成「去查网络」。
|
|
70
72
|
|
|
71
73
|
闸门问的时候用的就是插件自己那份请求:请求头的名字来自 `provider.mjs` 里的 `outboundHeaders`,工具来自插件所顶替的同一份 `required`。所以它绿了是在说「插件真发的那个请求」被应答了,而不是在说「照抄的一份」被应答了——照抄的那一份曾经少发一个 `accept`,而且没有任何东西因此报错。
|
|
72
74
|
|
|
@@ -105,7 +107,7 @@ Zen 的免费层读请求体,也读请求头,只有当整个请求都属于
|
|
|
105
107
|
| `x-opencode-client` | `cli` |
|
|
106
108
|
| `authorization` | `Bearer public`;走 Messages 时是 `x-api-key: public` |
|
|
107
109
|
|
|
108
|
-
`anthropic-version`
|
|
110
|
+
`anthropic-version` 在走 Messages 时总是发出去(宿主设了就用它的,没设就是 `2023-06-01`);`anthropic-beta` 只在宿主自己设了的时候才发。
|
|
109
111
|
|
|
110
112
|
`x-opencode-request`、`x-session-affinity` 和 `x-session-id` 都在 OpenCode 自己的请求头集合里,但没有一个是必需的,所以都不发——不过进来的同名请求头仍然会被读,用来判断这次请求属于哪个会话。这不是「一个个试过不行」得出的结论,2026-10-15 实测(1.x,其余不动):
|
|
111
113
|
|
|
@@ -119,7 +121,7 @@ Zen 的免费层读请求体,也读请求头,只有当整个请求都属于
|
|
|
119
121
|
|
|
120
122
|
两个各自无害的头放在一起会被拒,所以「某个头能不能加」不能靠单个试出来。测试按名字钉住了发出的这组请求头,因为多加一个请求头是弄坏这个插件最容易的方式。
|
|
121
123
|
|
|
122
|
-
Zen 的免费层会检查 session id 是否是 OpenCode 能生成的那种:时间戳加每毫秒计数器截成十二位十六进制,后面接十四个随机的 base62 字符;session 的那一种还会把低位取反。宿主给的 id 如果本来就是这个形状,就原样传出去;否则按会话的首条消息生成一个并记住,这样同一会话的各轮请求会共用一个 session id
|
|
124
|
+
Zen 的免费层会检查 session id 是否是 OpenCode 能生成的那种:时间戳加每毫秒计数器截成十二位十六进制,后面接十四个随机的 base62 字符;session 的那一种还会把低位取反。宿主给的 id 如果本来就是这个形状,就原样传出去;否则按会话的首条消息生成一个并记住,这样同一会话的各轮请求会共用一个 session id。宿主既不声明会话、又没有可作依据的用户消息时,这一轮就拿一个自己的 id——没有东西可以据以记住它,所以它不会被并进另一个会话里去。
|
|
123
125
|
|
|
124
126
|
偶尔 Zen 会在发出 `[DONE]` 之前就结束一个流式的 Chat Completions 回复。如果回复已经说了内容、且其中没有未完成的工具调用,就以普通回答结束,正文里会说明它被截断了;再问一次就会接着往下走。如果是带着未完成的工具调用被截断、或者什么都没说就断了、或者被折叠给一个不支持流式的客户端,请求就会失败,错误信息里写明这个回复的 id 和收到了多少。走 Anthropic Messages 或 Responses 的回复被截断同样会失败:magpie 能把它和完整的回复区分开,并且在该模型支持的情况下继续往下走。
|
|
125
127
|
|
|
@@ -140,4 +142,4 @@ Zen 的免费层会检查 session id 是否是 OpenCode 能生成的那种:时
|
|
|
140
142
|
|
|
141
143
|
## 许可
|
|
142
144
|
|
|
143
|
-
MIT,分叉自 [zen-free](https://github.com/magpie-community/plugins/tree/main/packages/zen-free)
|
|
145
|
+
MIT,分叉自 [zen-free](https://github.com/magpie-community/plugins/tree/main/packages/zen-free)。版权署名:`Copyright (c) 2026 xiaozhou26` 是上游原作者,本分支的贡献者另有署名。`LICENSE` 是原样的 MIT 正文,本分支的来源与维护缘由写在 [NOTICE](https://github.com/tkhs101/OpenCode-Zen-FreeX/blob/main/NOTICE),这样许可证扫描器读到的是一份标准的 MIT 文件,而想知道这个分支为什么存在的人仍然找得到答案。
|
package/core-tools.mjs
CHANGED
|
@@ -18,31 +18,109 @@
|
|
|
18
18
|
// because those belong to the machine the capture ran on.
|
|
19
19
|
|
|
20
20
|
import { readFileSync } from "node:fs";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
+
const CAPTURE = new URL("./fingerprint.json", import.meta.url);
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
// Read once, here, because every answer in this module comes out of it. Guarded
|
|
26
|
+
// because the failure this replaces is a bare ENOENT or SyntaxError thrown out of
|
|
27
|
+
// whichever module imported this one, which reads as *that* module being broken
|
|
28
|
+
// and never names the file to fix. One message covers both, since the remedy is
|
|
29
|
+
// the same for either: only a capture writes this file.
|
|
30
|
+
function readProfiles() {
|
|
31
|
+
let captured;
|
|
32
|
+
try {
|
|
33
|
+
captured = JSON.parse(readFileSync(CAPTURE, "utf8"));
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`core-tools: could not load the capture at ${fileURLToPath(CAPTURE)}; ` +
|
|
37
|
+
"missing or invalid JSON — run `bun run capture` to write a new one",
|
|
38
|
+
{ cause: error },
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
// An object carrying a non-empty map of profiles, and nothing more specific:
|
|
42
|
+
// which generations those are is the capture's to say — capture.mjs writes
|
|
43
|
+
// next.profiles[generation] — so the keys are never named here. A capture that
|
|
44
|
+
// lost every profile cannot tell one generation from another and fails now
|
|
45
|
+
// rather than at the first request.
|
|
46
|
+
const profiles = captured?.profiles;
|
|
47
|
+
if (
|
|
48
|
+
typeof profiles !== "object" ||
|
|
49
|
+
profiles === null ||
|
|
50
|
+
Array.isArray(profiles) ||
|
|
51
|
+
!Object.keys(profiles).length
|
|
52
|
+
) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
`core-tools: the capture at ${fileURLToPath(CAPTURE)} has no profiles; ` +
|
|
55
|
+
"run `bun run capture` to write a new one",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return profiles;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const PROFILES = readProfiles();
|
|
25
62
|
|
|
26
63
|
/**
|
|
27
64
|
* The generation a request belongs to, told by the tools it carries. Tool names
|
|
28
65
|
* move far less often than descriptions do, so this survives a rewording that a
|
|
29
66
|
* pinned User-Agent would not.
|
|
67
|
+
*
|
|
68
|
+
* `undefined` is a normal return: it says neither generation's names were there,
|
|
69
|
+
* and the callers default it themselves (`provider.mjs` uses `?? DEFAULT_GENERATION`).
|
|
70
|
+
* A body whose `tools` is not an array is the same answer as an empty one — the
|
|
71
|
+
* body is client-supplied, so it can carry any shape at all, and "no generation
|
|
72
|
+
* recognised" is the only honest reading of one that is not a list of tools.
|
|
30
73
|
*/
|
|
31
74
|
export function generationOf(tools) {
|
|
32
|
-
const names = new Set(
|
|
75
|
+
const names = new Set(
|
|
76
|
+
(Array.isArray(tools) ? tools : []).map((tool) => tool?.function?.name ?? tool?.name),
|
|
77
|
+
);
|
|
33
78
|
if (names.has("shell")) return "2.x";
|
|
34
79
|
if (names.has("bash")) return "1.x";
|
|
35
80
|
return undefined;
|
|
36
81
|
}
|
|
37
82
|
|
|
83
|
+
// The profile a generation was captured into, or an Error naming both what was
|
|
84
|
+
// asked for and what the capture holds. Ownership rather than truthiness: a
|
|
85
|
+
// plain object answers `constructor` and `toString` out of Object.prototype, so
|
|
86
|
+
// `PROFILES[generation]` alone would hand back a function and then fail on its
|
|
87
|
+
// `.agent` with a TypeError that names nothing.
|
|
88
|
+
function profileFor(generation) {
|
|
89
|
+
if (!Object.hasOwn(PROFILES, generation)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`core-tools: no profile for generation ${JSON.stringify(generation)}; ` +
|
|
92
|
+
`the capture holds ${Object.keys(PROFILES).join(", ")}`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return PROFILES[generation];
|
|
96
|
+
}
|
|
97
|
+
|
|
38
98
|
/**
|
|
39
99
|
* The User-Agent that generation of OpenCode sends, for a protocol. Each
|
|
40
100
|
* protocol's ai-sdk package bundles its own provider-utils, so 1.x differs by
|
|
41
101
|
* protocol and 2.x does not.
|
|
42
102
|
*/
|
|
43
103
|
export function agentFor(generation, protocol = "chat") {
|
|
44
|
-
const agent =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
104
|
+
const agent = profileFor(generation).agent;
|
|
105
|
+
if (typeof agent !== "object" || agent === null) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`core-tools: the ${JSON.stringify(generation)} profile in the capture has no agent; ` +
|
|
108
|
+
"run `bun run capture` to write a new one",
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
// The release shape carries no provider-utils at all, and is composed the same
|
|
112
|
+
// way whatever protocol was asked for.
|
|
113
|
+
if (!agent.providerUtils) return `opencode/${agent.channel}/${agent.opencode}/${agent.client}`;
|
|
114
|
+
// `?? "undefined"` used to be interpolated here for a protocol the capture has
|
|
115
|
+
// no provider-utils for, and a prototype member (`toString`) as the version.
|
|
116
|
+
// Zen reads this header, so a malformed one is rejected upstream with an error
|
|
117
|
+
// that says nothing about where it came from; name the miss instead.
|
|
118
|
+
if (!Object.hasOwn(agent.providerUtils, protocol)) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`core-tools: generation ${JSON.stringify(generation)} composes its User-Agent ` +
|
|
121
|
+
`per protocol, and the capture has no provider-utils for ${JSON.stringify(protocol)}; ` +
|
|
122
|
+
`it holds ${Object.keys(agent.providerUtils).join(", ")}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return `opencode/${agent.opencode} ai-sdk/provider-utils/${agent.providerUtils[protocol]} ${agent.runtime}`;
|
|
48
126
|
}
|
package/fingerprint.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"tools": {
|
|
21
21
|
"bash": {
|
|
22
22
|
"name": "bash",
|
|
23
|
-
"description": "Executes a given
|
|
23
|
+
"description": "Executes a given PowerShell command with optional timeout, ensuring proper handling and security measures.\n\nBe aware: OS: linux, Shell: bash\n\nAll commands run in the current working directory by default. Use the `workdir` parameter if you need to run a command in a different directory. AVOID changing directories inside the command - use `workdir` instead.\n\nUse `/tmp` for temporary work outside the workspace. This directory has already been created, already exists, and is pre-approved for external directory access.\n\nIMPORTANT: This tool is for terminal operations like git, npm, docker, etc. DO NOT use it for file operations (reading, writing, editing, searching, finding files) - use the specialized tools for this instead.\n\n# PowerShell shell notes\n- This cross-platform shell supports pipeline chain operators (`&&` and `||`).\n- Use double quotes for interpolated strings (`\"Hello $name\"`), single quotes for verbatim strings.\n- Prefer full cmdlet names like `Get-ChildItem`, `Set-Content`, `Remove-Item`, and `New-Item` over aliases.\n- Use `$(...)` for subexpressions. Use `@(...)` for array expressions.\n- To call a native executable whose path contains spaces, use the call operator: `& \"",
|
|
24
24
|
"parameters": {
|
|
25
25
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
26
26
|
"type": "object",
|
|
@@ -46,6 +46,37 @@
|
|
|
46
46
|
]
|
|
47
47
|
}
|
|
48
48
|
},
|
|
49
|
+
"edit": {
|
|
50
|
+
"name": "edit",
|
|
51
|
+
"description": "Performs exact string replacements in files. \n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. \n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `oldString` is not found in the file with an error \"oldString not found in content\".\n- The edit will FAIL if `oldString` is found multiple times in the file with an error \"Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.\" Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every ",
|
|
52
|
+
"parameters": {
|
|
53
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
54
|
+
"type": "object",
|
|
55
|
+
"properties": {
|
|
56
|
+
"filePath": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "The absolute path to the file to modify"
|
|
59
|
+
},
|
|
60
|
+
"oldString": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "The text to replace"
|
|
63
|
+
},
|
|
64
|
+
"newString": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "The text to replace it with (must be different from oldString)"
|
|
67
|
+
},
|
|
68
|
+
"replaceAll": {
|
|
69
|
+
"type": "boolean",
|
|
70
|
+
"description": "Replace all occurrences of oldString (default false)"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"required": [
|
|
74
|
+
"filePath",
|
|
75
|
+
"oldString",
|
|
76
|
+
"newString"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
},
|
|
49
80
|
"glob": {
|
|
50
81
|
"name": "glob",
|
|
51
82
|
"description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths\n- Use this tool when you need to find files by name patterns\n- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead\n- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.\n",
|
|
@@ -121,37 +152,6 @@
|
|
|
121
152
|
]
|
|
122
153
|
}
|
|
123
154
|
},
|
|
124
|
-
"edit": {
|
|
125
|
-
"name": "edit",
|
|
126
|
-
"description": "Performs exact string replacements in files. \n\nUsage:\n- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file. \n- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: line number + colon + space (e.g., `1: `). Everything after that space is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.\n- The edit will FAIL if `oldString` is not found in the file with an error \"oldString not found in content\".\n- The edit will FAIL if `oldString` is found multiple times in the file with an error \"Found multiple matches for oldString. Provide more surrounding lines in oldString to identify the correct match.\" Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every ",
|
|
127
|
-
"parameters": {
|
|
128
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
129
|
-
"type": "object",
|
|
130
|
-
"properties": {
|
|
131
|
-
"filePath": {
|
|
132
|
-
"type": "string",
|
|
133
|
-
"description": "The absolute path to the file to modify"
|
|
134
|
-
},
|
|
135
|
-
"oldString": {
|
|
136
|
-
"type": "string",
|
|
137
|
-
"description": "The text to replace"
|
|
138
|
-
},
|
|
139
|
-
"newString": {
|
|
140
|
-
"type": "string",
|
|
141
|
-
"description": "The text to replace it with (must be different from oldString)"
|
|
142
|
-
},
|
|
143
|
-
"replaceAll": {
|
|
144
|
-
"type": "boolean",
|
|
145
|
-
"description": "Replace all occurrences of oldString (default false)"
|
|
146
|
-
}
|
|
147
|
-
},
|
|
148
|
-
"required": [
|
|
149
|
-
"filePath",
|
|
150
|
-
"oldString",
|
|
151
|
-
"newString"
|
|
152
|
-
]
|
|
153
|
-
}
|
|
154
|
-
},
|
|
155
155
|
"write": {
|
|
156
156
|
"name": "write",
|
|
157
157
|
"description": "Writes a file to the local filesystem.\n\nUsage:\n- This tool will overwrite the existing file if there is one at the provided path.\n- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.\n- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.\n- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.\n- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.\n",
|
package/index.mjs
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import plugin from "./provider.mjs"
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// A host resolves a plugin module and reads the factory off it. Which shape it
|
|
4
|
+
// reads is the host's business and not this package's to narrow: magpie builds
|
|
5
|
+
// one from `default.server`, which is the shape provider.mjs already exports, so
|
|
6
|
+
// that object is forwarded whole rather than the factory being renamed into a
|
|
7
|
+
// shape nothing else in the package uses. The `id` comes with it, because a
|
|
8
|
+
// loader that keys plugins by id needs the same one the provider registers — and
|
|
9
|
+
// it is the provider's own id, not a second copy of it: it is read from the
|
|
10
|
+
// module, so the two cannot drift.
|
|
11
|
+
//
|
|
12
|
+
// The named export stays for anything that already imported it by name, and
|
|
13
|
+
// `server` is offered as a name too, so `mod.default.server` and `mod.server`
|
|
14
|
+
// both resolve.
|
|
7
15
|
if (typeof plugin?.server !== "function") {
|
|
8
16
|
throw new TypeError("provider.mjs must default-export a plugin with a `server` factory")
|
|
9
17
|
}
|
|
10
18
|
|
|
11
19
|
export const ZenFreePlugin = plugin.server
|
|
20
|
+
export const server = plugin.server
|
|
21
|
+
export default plugin
|