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 CHANGED
@@ -1,14 +1,7 @@
1
1
  MIT License
2
2
 
3
3
  Copyright (c) 2026 xiaozhou26
4
- Copyright (c) 2026 opencode-zen-free contributors
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, and exits non-zero saying
292
- nothing was asked if it cannot tell the two apart.
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 比这份指纹声称的更新)或 `unreachable`(根本联系不上)。只要还有一代的 `floor` 被应答,退出码就是 0。某个 `floor` 被拒、而它自己的 `full` 被应答时会单独点出来:那意味着带着自己那套 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` 和 `anthropic-beta` 也会发出去,但只在走 Messages 时,而且只在宿主自己设了它们的时候。
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)。版权署名见 [LICENSE](https://github.com/tkhs101/OpenCode-Zen-FreeX/blob/main/LICENSE):`Copyright (c) 2026 xiaozhou26` 是上游原作者,本分支的贡献者另有署名。
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 captured = JSON.parse(readFileSync(new URL("./fingerprint.json", import.meta.url), "utf8"));
23
+ const CAPTURE = new URL("./fingerprint.json", import.meta.url);
23
24
 
24
- export const PROFILES = captured.profiles;
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((tools ?? []).map((tool) => tool?.function?.name ?? tool?.name));
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 = PROFILES[generation].agent;
45
- return agent.providerUtils
46
- ? `opencode/${agent.opencode} ai-sdk/provider-utils/${agent.providerUtils[protocol]} ${agent.runtime}`
47
- : `opencode/${agent.channel}/${agent.opencode}/${agent.client}`;
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 Windows PowerShell (5.1) 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# Windows PowerShell (5.1) shell notes\n- Use `cmd1; if ($?) { cmd2 }` to chain dependent commands.\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 opera",
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
- // Only the `server` factory is forwarded: that is the hook a magpie host builds
4
- // a plugin from, so the provider's `id` is deliberately not repeated here. Say
5
- // so where the mismatch is, rather than handing the host an undefined plugin to
6
- // fail with something opaque later.
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