@magpie-community/opencode-zcode-auth 0.1.6 → 0.1.7
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/README.md +8 -1
- package/index.mjs +140 -15
- package/package.json +2 -2
- package/prompt.mjs +34 -0
package/README.md
CHANGED
|
@@ -17,7 +17,14 @@ over Anthropic's Messages API. Provider id: `zcode`.
|
|
|
17
17
|
- If the account has no plan of its own, it looks for a seat on a team's
|
|
18
18
|
plan and uses that team project's `zcode-team-api-key`.
|
|
19
19
|
- Failing both, it uses ZCode's free **Start Plan**, served by zcode.z.ai
|
|
20
|
-
with ZCode's session token
|
|
20
|
+
with ZCode's session token.
|
|
21
|
+
- zcode.z.ai serves the Start Plan only to requests that look like the
|
|
22
|
+
ZCode client's own, and refuses others with HTTP 405 "request has been
|
|
23
|
+
blocked due to unusual activity". So a Start Plan request goes as
|
|
24
|
+
ZCode's does: ZCode's headers, ZCode's system prompt ahead of the
|
|
25
|
+
agent's, the day in a `<system-reminder>` turn before the agent's
|
|
26
|
+
turns, and the sign-in's device id in `metadata.user_id`. GLM Coding
|
|
27
|
+
Plan and team requests go as the agent sent them.
|
|
21
28
|
- The Start Plan's token can't be refreshed. When it runs out, sign in
|
|
22
29
|
again.
|
|
23
30
|
- **ZCode app's sign-in**
|
package/index.mjs
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
import { createDecipheriv, createHash } from "node:crypto"
|
|
20
20
|
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
21
|
-
import { homedir, userInfo } from "node:os"
|
|
22
|
-
import { join } from "node:path"
|
|
21
|
+
import { arch, homedir, release, userInfo } from "node:os"
|
|
22
|
+
import { basename, join } from "node:path"
|
|
23
|
+
import PROMPT from "./prompt.mjs"
|
|
23
24
|
|
|
24
25
|
const PROVIDER = "zcode"
|
|
25
26
|
const ZCODE = "https://zcode.z.ai"
|
|
@@ -1014,6 +1015,128 @@ function model(m, url) {
|
|
|
1014
1015
|
}
|
|
1015
1016
|
}
|
|
1016
1017
|
|
|
1018
|
+
// ---- the Start Plan's requests ----------------------------------------------------
|
|
1019
|
+
// zcode.z.ai serves the Start Plan only to what looks like ZCode's own
|
|
1020
|
+
// request, and turns others away with 405 "request has been blocked due to
|
|
1021
|
+
// unusual activity" (code 3012). A Start Plan request goes as the plugin
|
|
1022
|
+
// the plan serves (ARNO's "Freeflow", provider zcode-start) sends it: its
|
|
1023
|
+
// headers (startHeaders) and its body (dress). The texts are prompt.mjs.
|
|
1024
|
+
|
|
1025
|
+
const osVersion = () => `${process.platform} ${release()} ${arch()}`
|
|
1026
|
+
const osCategory = () => (process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")
|
|
1027
|
+
|
|
1028
|
+
function intl() {
|
|
1029
|
+
try {
|
|
1030
|
+
return Intl.DateTimeFormat().resolvedOptions()
|
|
1031
|
+
} catch {
|
|
1032
|
+
return {}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// startHeaders puts ZCode's headers on a Start Plan request, jwt its key.
|
|
1037
|
+
function startHeaders(h, jwt) {
|
|
1038
|
+
h.delete("x-api-key")
|
|
1039
|
+
h.delete("authorization")
|
|
1040
|
+
h.delete("X-Device-Mid")
|
|
1041
|
+
h.set("Authorization", "Bearer " + jwt)
|
|
1042
|
+
h.set("anthropic-version", "2023-06-01")
|
|
1043
|
+
h.set("HTTP-Referer", ZCODE)
|
|
1044
|
+
h.set("User-Agent", UA + " ai-sdk/anthropic/3.0.81")
|
|
1045
|
+
h.set("X-ZCode-App-Version", APP_VERSION)
|
|
1046
|
+
h.set("X-Title", "Z Code@cli")
|
|
1047
|
+
h.set("X-Release-Channel", "production")
|
|
1048
|
+
const o = intl()
|
|
1049
|
+
h.set("X-Client-Language", o.locale || "unknown")
|
|
1050
|
+
if (o.timeZone) h.set("X-Client-Timezone", o.timeZone)
|
|
1051
|
+
h.set("X-ZCode-Agent", "glm")
|
|
1052
|
+
h.set("X-Platform", platform())
|
|
1053
|
+
h.set("X-Os-Category", osCategory())
|
|
1054
|
+
h.set("X-Os-Version", osVersion())
|
|
1055
|
+
h.set("x-request-id", uuid())
|
|
1056
|
+
h.set("x-zcode-session-type", "main")
|
|
1057
|
+
h.set("x-zcode-trace-id", uuid())
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// environment is the prompt's environment section; provider and model
|
|
1061
|
+
// name the powered-by line, left out with no model.
|
|
1062
|
+
function environment(provider, model) {
|
|
1063
|
+
const e = PROMPT.environment
|
|
1064
|
+
const sh = process.env.SHELL || process.env.ComSpec
|
|
1065
|
+
const lines = [
|
|
1066
|
+
e.heading,
|
|
1067
|
+
e.invokedLine,
|
|
1068
|
+
`- ${e.cwdLabel}: ${process.cwd()}`,
|
|
1069
|
+
`- ${e.gitLabel}: ${e.gitNo}`,
|
|
1070
|
+
`- ${e.platformLabel}: ${process.platform}`,
|
|
1071
|
+
`- ${e.shellLabel}: ${sh ? basename(sh) : "unknown"}`,
|
|
1072
|
+
`- ${e.osVersionLabel}: ${osVersion()}`,
|
|
1073
|
+
]
|
|
1074
|
+
if (model) lines.push(e.poweredByLine.replace("{provider}", provider).replace("{model}", model))
|
|
1075
|
+
return lines.join("\n")
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// today is the local date, YYYY-MM-DD.
|
|
1079
|
+
function today(now = new Date()) {
|
|
1080
|
+
const p = (n) => String(n).padStart(2, "0")
|
|
1081
|
+
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// contextPrefix is the user turn ZCode puts before the agent's: the day in
|
|
1085
|
+
// a <system-reminder>, the tags hugging the text.
|
|
1086
|
+
function contextPrefix() {
|
|
1087
|
+
const c = PROMPT.context
|
|
1088
|
+
const body = [c.intro, c.currentDateHeading + "\n" + c.currentDateLine.replace("{date}", today()), "", c.outro].join("\n")
|
|
1089
|
+
return { role: "user", content: [{ type: "text", text: "<system-reminder>" + body + "</system-reminder>" }] }
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// dress is a Messages request body as ZCode sends it: ZCode's three cached
|
|
1093
|
+
// system blocks, then the agent's own system text uncached; the context
|
|
1094
|
+
// prefix as the first turn; no cache mark in the turns but on the last
|
|
1095
|
+
// block of the last; none on the tools; metadata.user_id naming the
|
|
1096
|
+
// sign-in's device. site names the powered-by line's provider. A body that
|
|
1097
|
+
// isn't a Messages request goes as it is.
|
|
1098
|
+
function dress(text, site, device) {
|
|
1099
|
+
if (typeof text !== "string" || !text.includes("messages")) return text
|
|
1100
|
+
let b
|
|
1101
|
+
try {
|
|
1102
|
+
b = JSON.parse(text)
|
|
1103
|
+
} catch {
|
|
1104
|
+
return text
|
|
1105
|
+
}
|
|
1106
|
+
if (!b || typeof b !== "object" || !Array.isArray(b.messages)) return text
|
|
1107
|
+
const cached = (t) => ({ type: "text", text: t, cache_control: { type: "ephemeral" } })
|
|
1108
|
+
const provider = site === "bigmodel" ? "bigmodel-api" : "zai-api"
|
|
1109
|
+
const model = typeof b.model === "string" ? b.model : ""
|
|
1110
|
+
const own = []
|
|
1111
|
+
if (typeof b.system === "string") {
|
|
1112
|
+
if (b.system.trim()) own.push({ type: "text", text: b.system })
|
|
1113
|
+
} else if (Array.isArray(b.system)) {
|
|
1114
|
+
for (const x of b.system) if (x?.type === "text" && typeof x.text === "string" && x.text) own.push({ type: "text", text: x.text })
|
|
1115
|
+
}
|
|
1116
|
+
b.system = [
|
|
1117
|
+
cached(PROMPT.cliPrefix),
|
|
1118
|
+
cached(PROMPT.stableSections.join("\n\n")),
|
|
1119
|
+
cached("\n\n" + [PROMPT.beforeEnvironment, environment(provider, model), PROMPT.afterEnvironment].join("\n\n")),
|
|
1120
|
+
...own,
|
|
1121
|
+
]
|
|
1122
|
+
b.messages = [contextPrefix(), ...b.messages]
|
|
1123
|
+
let last = null
|
|
1124
|
+
for (const m of b.messages) {
|
|
1125
|
+
if (!m || m.role === "system") continue
|
|
1126
|
+
last = m
|
|
1127
|
+
if (Array.isArray(m.content)) for (const x of m.content) if (x && typeof x === "object") delete x.cache_control
|
|
1128
|
+
}
|
|
1129
|
+
if (last && typeof last.content === "string") last.content = [cached(last.content)]
|
|
1130
|
+
else if (last && Array.isArray(last.content) && last.content.length) {
|
|
1131
|
+
const x = last.content[last.content.length - 1]
|
|
1132
|
+
if (x && typeof x === "object" && !x.cache_control) x.cache_control = { type: "ephemeral" }
|
|
1133
|
+
}
|
|
1134
|
+
if (Array.isArray(b.tools)) for (const t of b.tools) if (t && typeof t === "object") delete t.cache_control
|
|
1135
|
+
const meta = b.metadata && typeof b.metadata === "object" ? b.metadata : {}
|
|
1136
|
+
b.metadata = { ...meta, user_id: JSON.stringify({ device_id: device, account_uuid: "", session_id: "" }) }
|
|
1137
|
+
return JSON.stringify(b)
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1017
1140
|
// ---- the plugin ------------------------------------------------------------------
|
|
1018
1141
|
|
|
1019
1142
|
export async function ZCodeAuthPlugin({ client }) {
|
|
@@ -1092,19 +1215,21 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1092
1215
|
const h = new Headers(opts.headers)
|
|
1093
1216
|
if (start) {
|
|
1094
1217
|
if (jwtExpired(s.jwt)) throw new Error(EXPIRED)
|
|
1095
|
-
|
|
1096
|
-
//
|
|
1097
|
-
h
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1218
|
+
if (!s.jwt) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1219
|
+
// the Start Plan is served only to what looks like ZCode's own request
|
|
1220
|
+
startHeaders(h, s.jwt)
|
|
1221
|
+
if (opts.body != null) {
|
|
1222
|
+
const text = typeof opts.body === "string" ? opts.body : await new Response(opts.body).text()
|
|
1223
|
+
opts.body = dress(text, s.site, s.device)
|
|
1224
|
+
delete opts.duplex
|
|
1225
|
+
h.delete("content-length")
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1229
|
+
h.delete("authorization")
|
|
1230
|
+
h.set("x-api-key", key)
|
|
1231
|
+
h.set("Authorization", "Bearer " + key)
|
|
1103
1232
|
}
|
|
1104
|
-
if (!key) throw new Error("ZCode's sign-in has no key; sign in again")
|
|
1105
|
-
h.delete("authorization")
|
|
1106
|
-
h.set("x-api-key", key)
|
|
1107
|
-
h.set("Authorization", "Bearer " + key)
|
|
1108
1233
|
const res = await fetch(url, { ...opts, headers: h })
|
|
1109
1234
|
// the plan's answer goes on as it came, the account kept: the
|
|
1110
1235
|
// built-in never marked a ZCode account lapsed nor cleared one
|
|
@@ -1175,4 +1300,4 @@ export async function ZCodeAuthPlugin({ client }) {
|
|
|
1175
1300
|
}
|
|
1176
1301
|
|
|
1177
1302
|
// for tests
|
|
1178
|
-
export const _internal = { entry, limitWindows, termOf, startUsage, routes, teamKeys, ownSignIn, stateOf }
|
|
1303
|
+
export const _internal = { entry, limitWindows, termOf, startUsage, routes, teamKeys, ownSignIn, stateOf, dress, PROMPT }
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-zcode-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "OpenCode provider plugin: signs in to Z.ai's or BigModel's (智谱) GLM Coding Plan as ZCode does, including team seats and ZCode's Start Plan",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
|
-
"files": ["index.mjs", "README.md"],
|
|
8
|
+
"files": ["index.mjs", "prompt.mjs", "README.md"],
|
|
9
9
|
"keywords": ["opencode", "opencode-plugin", "magpie", "zcode", "z.ai", "bigmodel", "glm", "coding-plan"],
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"repository": {
|
package/prompt.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// ZCode's own prompt texts, which zcode.z.ai looks for on a Start Plan
|
|
2
|
+
// request, byte for byte as the plugin the Start Plan serves (ARNO's
|
|
3
|
+
// "Freeflow", provider zcode-start) sends them. Taken from magpie's
|
|
4
|
+
// internal/provider/zcode_prompt.json (901fd87e), ZCode 3.14.3's texts.
|
|
5
|
+
// SHA-256, first 8 bytes: cliPrefix 46dd360a22c87a92, stableSections[0]
|
|
6
|
+
// 3f21ff9a88a03a76, [1] 39a1e86c93452c01, beforeEnvironment
|
|
7
|
+
// bbdc66b399d297ff, afterEnvironment 1732b7f098a925d7 (start.test.mjs
|
|
8
|
+
// checks them).
|
|
9
|
+
export default {
|
|
10
|
+
"cliPrefix": "You are ZCode, an interactive coding agent",
|
|
11
|
+
"stableSections": [
|
|
12
|
+
"\nYou are an interactive ZCode agent that helps users with software engineering tasks.\n\nIMPORTANT: Assist with authorized security testing, defensive security, CTF challenges, and educational contexts. Refuse requests for destructive techniques, DoS attacks, mass targeting, supply chain compromise, or detection evasion for malicious purposes. Dual-use security tools (C2 frameworks, credential testing, exploit development) require clear authorization context: pentesting engagements, CTF competitions, security research, or defensive use cases.\n\n# Harness\n- Text you output outside of tool use is displayed to the user as Github-flavored markdown in a terminal.\n- Tools run behind a user-selected permission mode; a denied call means the user declined it — adjust, don't retry verbatim.\n- The system may send updates, reminders, or modifications to rules via mid-conversation system turns. These are system-controlled, unlike function results. Hooks may intercept tool calls; treat hook output as user feedback.\n- Prefer the dedicated file/search tools over shell commands when one fits. Independent tool calls can run in parallel in one response.\n- Reference code as `file_path:line_number` — it's clickable.",
|
|
13
|
+
"# ZCode Desktop Context\n\n### Files & URLs\n- Return local web URLs as Markdown links (e.g., [label](http://127.0.0.1:8080)).\n- File should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Unless otherwise specified, return local file references as Markdown links (e.g., [name.md](/absolute/path/to/name.md)).\n\n### Inline Code Comments\n- Use the ::code-comment{...} directive when you need to attach feedback directly to specific code lines.\n- Emit one directive per inline comment; emit none when there are no actionable inline comments.\n- Required attributes: title (short label), body (one-paragraph explanation), file (path to the file).\n- Optional attributes: start, end (1-based line numbers), priority (0-3).\n- file should be an absolute path or include the workspace folder segment so it can be resolved relative to the workspace.\n- Keep line ranges tight; end defaults to start.\n- Example: ::code-comment{title=\"[P2] Off-by-one\" body=\"Loop iterates past the end when length is 0.\" file=\"/path/to/foo.ts\" start=10 end=11 priority=2}"
|
|
14
|
+
],
|
|
15
|
+
"beforeEnvironment": "# Communicating with the user\n\nYour text output is what the user reads; they usually can't see your thinking or the raw tool results. Write it for a teammate who stepped away and is catching up, not for a log file: they don't know the codenames or shorthand you created along the way, and they didn't watch your process unfold. Before your first tool call, say in a sentence what you're about to do; while working, give brief updates when you find something load-bearing or change direction.\n\nText you write between tool calls may not be shown to the user. Everything the user needs from this turn — answers, summaries, findings, conclusions, deliverables — must be in the final text message of your turn, with no tool calls after it. Keep text between tool calls to brief status notes. If something important appeared only mid-turn or in your thinking, restate it in that final message.\n\nLead with the outcome. Your first sentence after finishing should answer \"what happened\" or \"what did you find\" — the thing the user would ask for if they said \"just give me the TLDR.\" Supporting detail and reasoning come after, for readers who want them.\n\nBeing readable and being concise are different things, and readable matters more. If the user has to reread your summary or ask you to explain, any time saved by brevity is gone. The way to keep output short is to be selective about what you include (drop details that don't change what the reader would do next), not to compress the writing into fragments, abbreviations, arrow chains like `A → B → fails`, or jargon. What you do include, write in complete sentences with the technical terms spelled out. Don't make the reader cross-reference labels or numbering you invented earlier; say what you mean in place.\n\nMatch the response to the question: a simple question gets a direct answer in prose, not headers and sections. Use tables only for short enumerable facts, with explanations in the surrounding prose rather than the cells. Calibrate to the user — a bit tighter for an expert, more explanatory for someone newer.\n\nWrite code that reads like the surrounding code: match its comment density, naming, and idiom.\nOnly write a code comment to state a constraint the code itself can't show — never to say where it came from, what the next line does, or why your change is correct; that's you talking to the reviewer, not the next reader, and it's noise the moment the PR merges.\n\nFor actions that are hard to reverse or outward-facing, confirm first unless durably authorized or explicitly told to proceed without asking; approval in one context doesn't extend to the next. Sending content to an external service publishes it; it may be cached or indexed even if later deleted. Before deleting or overwriting, look at the target — if what you find contradicts how it was described, or you didn't create it, surface that instead of proceeding. Report outcomes faithfully: if tests fail, say so with the output; if a step was skipped, say that; when something is done and verified, state it plainly without hedging.",
|
|
16
|
+
"afterEnvironment": "# Context management\nWhen the conversation grows long, some or all of the current context is summarized; the summary, along with any remaining unsummarized context, is provided in the next context window so work can continue — you don't need to wrap up early or hand off mid-task.\n\nWhen you have enough information to act, act. Do not re-derive facts already established in the conversation, re-litigate a decision the user has already made, or narrate options you will not pursue. If you are weighing a choice, give a recommendation, not an exhaustive survey\n\nYou are operating autonomously. The user is not watching in real time and cannot answer questions mid-task, so asking 'Want me to…?' or 'Shall I…?' will block the work. For reversible actions that follow from the original request, proceed without asking. Stop only for destructive actions or genuine scope changes the user must decide. Offering follow-ups after the task is done is fine; asking permission before doing the work is not.\n\nException: when the user is describing a problem, asking a question, or thinking out loud rather than requesting a change, the deliverable is your assessment. Report your findings and stop. Don't apply a fix until they ask for one.\n\nBefore ending your turn, check your last paragraph. If it is a plan, an analysis, a question, a list of next steps, or a promise about work you have not done ('I'll…', 'let me know when…'), do that work now with tool calls. That includes retrying after errors and gathering missing information yourself. Do not stop because the context or session is long. End your turn only when the task is complete or you are blocked on input only the user can provide.\n\nBefore running a command that changes system state — restarts, deletes, config edits — check that the evidence actually supports that specific action. A signal that pattern-matches to a known failure may have a different cause.",
|
|
17
|
+
"environment": {
|
|
18
|
+
"heading": "# Environment",
|
|
19
|
+
"invokedLine": "You have been invoked in the following environment:",
|
|
20
|
+
"cwdLabel": "Primary working directory",
|
|
21
|
+
"gitLabel": "Is a git repository",
|
|
22
|
+
"gitNo": "no",
|
|
23
|
+
"platformLabel": "Platform",
|
|
24
|
+
"shellLabel": "Shell",
|
|
25
|
+
"osVersionLabel": "OS Version",
|
|
26
|
+
"poweredByLine": "- You are powered by the model named {provider}/{model}."
|
|
27
|
+
},
|
|
28
|
+
"context": {
|
|
29
|
+
"intro": "As you answer the user's questions, you can use the following context:",
|
|
30
|
+
"outro": " IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.",
|
|
31
|
+
"currentDateHeading": "# currentDate",
|
|
32
|
+
"currentDateLine": "Today's date is {date}."
|
|
33
|
+
}
|
|
34
|
+
}
|