Skip to content

Releases: xhqing/zcode-cli

zcode-cli 3.8.1-32

Choose a tag to compare

@xhqing xhqing released this 09 Sep 15:15

Fixes the 3.8.1-31 known issue: while signed out (custom-provider config only, no stored login), picking any model from the /model list connected to nothing. Model switching now resolves credential-less official-slot references to the same provider's custom-env slot. The model pickers also drop the internal env- prefix, the signed-out list is restricted to custom.env models, and the custom-provider config file is renamed to custom.env with automatic legacy migration.

Fix: signed-out /model selection was unusable (3.8.1-31 known issue)

Why: In 3.8.1-31 the deduplication kept the official entry and dropped its env-slot twin. The official bigmodel slot is managed by /login and carries no credential while signed out, so setModel("bigmodel/glm-5.3") reached the runtime with no API key — while the credentialed env-bigmodel entry was exactly the one deduplication removed.

What changed:

  • src/identity.ts adds resolveModelSlotRef(): an official slot ref (<provider>/<model>) is returned unchanged when the provider has a vault login or an official-slot key; with neither, it falls back to the env-<provider> slot when that slot carries a key and declares the model. The fallback is judged per provider (signing in to zai does not affect picking a bigmodel model). Env-slot refs, models not declared in the env slot, and providers without a keyed env slot pass through unchanged.
  • All three model-switching entry points in the TUI — switchTransientModel (shared by the /model list, manually typed /model <provider>/<model>, and the quick cycle toggle) and the /settings → Model providers session apply — pass through resolveModelSlotRef() before sending to the bridge. /settings persistence writes the resolved slot ref, so a signed-out save points directly at the usable env slot (the post-login switchModelBlockToOfficialProvider migration is unchanged).
  • selectors.ts current marking now matches both forms (the internal slot id env-<provider>/<model> and its prefix-free display form), fixing the lost "current" marker and /settings preselection that 3.8.1-31 introduced.

Compatibility: The 3.8.1-31 dedup direction (each model listed once, official entry wins) is unchanged — the fix does not touch withoutEnvSlotTwins(); it extends the fact that the env slot is the only usable path while signed out from the display layer to the switching layer. Env-only entries and signed-in behavior are identical to 3.8.1-31; the 3.8.1-26 "the env file is the model-block authority while signed out" semantics are unaffected.

Verification: tsc --noEmit passes; 7 new identity unit tests (signed-out fallback, undeclared model passes through, no keyed env slot passes through, official-slot key passes through, vault token passes through, per-provider independence, env ref and no-slash alias pass through) and 2 new selector unit tests (current marker matches the env-slot form of an official twin in the flat picker and the provider cascade); full bun test green; all 5 TUI smoke tests pass.

Model pickers: no more env- prefix; signed-out list shows only custom.env models

Why: The internal env-<provider> slot ids leaked into the display layer — env-only entries showed up prefixed in the flat /model list, and /settings → Model providers split one provider into duplicate official + env groups. After the 3.8.1-31 incident the list layer is also tightened: while signed out, only entries that actually carry credentials are listed, because nothing else can connect.

What changed:

  • modelPicker() / providerModelPicker() gain a third signedIn?: boolean parameter: undefined keeps the full list (backwards compatible), false keeps only env-slot entries, true shows everything. Entry values, labels, and generated /model commands all go through displayModelRef(); cascade grouping keys switch to displayProviderId(), merging official and env slots of the same provider into one group with twin-deduplicated entries; group-name fallbacks drop the prefix; current marking and preselection match both forms. With signedIn === false, both the flat and cascade views filter out non-env-slot entries.
  • All three call sites (showModelPicker, showModelProviderSettings, the quick cycle switchModel) pass the sign-in state from readSignedInProvider(). An empty /model list no longer opens a silent empty picker — it explains per sign-in state (signed out: "sign in (/login) or configure ~/.zcode/cli/custom.env").
  • Selection still resolves through resolveModelSlotRef() to a credentialed slot, so every listed entry is actually usable.

Verification: the acceptance group (11 cases, including selection-path guards R7/R8 reproducing the 3.8.1-31 incident and signed-out filtering R9/R10) is green; tsc --noEmit passes.

Config file rename: custom-provider.env → custom.env (automatic migration)

Why: Per the project naming convention, provider in custom-provider.env is a redundant modifier — the name simplifies to custom.env. Existing machines had the old file, so automatic migration was required.

What changed:

  • src/env-config.ts: the filename constant is renamed to customEnvFileName with value custom.env (single source of truth). migrateLegacyEnvFile() grows from one legacy layer (.env) to a two-layer chain — custom-provider.env first, .env as fallback; migration is skipped when custom.env already exists (coexisting old files are kept untouched) or when ZCODE_ENV_FILE overrides; at most one rename per launch.
  • The migration notice in src/launcher.ts reports the actual destination path instead of hardcoding .env; the signed-out hint in src/identity.ts uses the new name.
  • The template is renamed custom-provider.env.example → custom.env.example; docs/CONFIGURATION.md and the install sections of all three READMEs are updated.
  • Tests: path assertions use the new name; the migration suite covers four cases (long-name migration, direct .env migration, coexistence prefers the long name and keeps .env, override skips).

Verification: the acceptance group (5 cases) is green; tsc --noEmit passes.

v3.8.1-31 marked as pre-release

The previous release carried the signed-out selection defect above; it was marked pre-release so the Latest pointer falls back to 3.8.1-30, and a known-issue warning was added at the top of its notes pointing to the fixed version. The release itself is kept for history. With this release published as Latest, latest/download links point at the fixed version again.

Install links are now tag-pinned

All three READMEs and docs/RELEASING.md switched from releases/latest/download/... to tag-pinned URLs — the latest pointer moves on every publish while the asset name carries the version, so historical latest/download links go 404. Tag-pinned URLs always resolve to that version's asset.

Tests & CI

  • Two acceptance-case groups from Hopper (TestEngineerAgent) were synced, implemented against, and archived: model-picker-env-prefix (11 cases) and custom-env-rename (5 cases).
  • Regression-net gap filling: six new test files with 46 cases covering previously untested modules (tool-group-view, command, protocol-part-view, renderable, plugin-protocol, clipboard-text); one known defect is locked with test.failing (captureCommand throws ERR_STREAM_PREMATURE_CLOSE instead of returning {code: 1, stderr} when the target binary is missing — affects the update fallback for users without gh and the browser-opening fallback in the OAuth flows).
  • CI gate slimmed: the validate job dropped the release build (90% of its runtime) in favor of a vendor-runtime cache keyed on zcode-runtime.lock.json + scripts/sync-runtime.ts, and no longer re-runs on pushes to main; timeout 45 → 20 minutes; cold-cache full run ~3.5 minutes.
  • Housekeeping: subproject paths in .claude/CLAUDE.md updated from ~/Documents/Projects/ to ~/Developer/ after the project directory migration.

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-32/zcode-cli-3.8.1-32.tgz

zcode-cli 3.8.1-30

Choose a tag to compare

@xhqing xhqing released this 06 Sep 03:07

The TUI footer no longer falls back to the literal "default" for the model name: startup now resolves the configured <provider>/<model_id> form.

Why: A user reported (with a screenshot) that after running zcode, the footer showed ◇ default — ◉ yolo — … and asked for the model name to always display <provider>/<model_id>. Root cause: the official runtime's login gate only checks the official zai/bigmodel slots, so boots with model access configured in an env-file slot (env-<id>, custom-provider.env) are marked loginRequired and receive no startup model metadata. The TUI constructor fell back to the literal "default" for a missing initialModel, and the run() startup sequence had no config-based fallback — the existing readConfiguredModelAccess() fallback in handleResult only triggered after the first message was submitted, so the footer showed "default" from first paint until the first message.

What changed:

  • The TUI run() startup sequence gains resolveStartupModel() (awaited before the first frame is drawn): when this.model is "default" (the marker for missing startup metadata), it first consults readConfiguredModelAccess() — on a hit (the slot pointed to by model.main declares the model and carries a key) it backfills displayModelRef(access.model) for display and corrects loginRequired to false under the existing semantics (consistent with the re-check semantics of handleResult / handleLocalLogin); on a miss it falls back to the new readConfiguredMainModel() (src/model-access.ts, which only reads model.main from config.json without requiring a key in the slot) — so loginRequired boots without configured access also show the configured model (e.g. the default zai/glm-5.2) instead of "default".
  • Display goes through displayModelRef(), which uniformly strips the env- prefix: the env slot env-bigmodel/glm-5.3 displays as bigmodel/glm-5.3.
  • Smoke tests (scripts/smoke-tui.ts): the main flow gains an assertion that before login (pristine HOME, no key) the footer already shows ◈ zai/glm-5.2 (covering the main-fallback path), plus a new standalone verifyEnvSlotModelDisplay() section that pre-writes an env- slot + key + model.main config to replicate the user's scenario and asserts the footer shows bigmodel/glm-5.3 with no ◈ default anywhere (covering the access-hit path); the lenient ◈ default fallback branch of the /mode plan assertion is tightened to a concrete model form so regressions cannot hide.
  • model-access unit tests gain two readConfiguredMainModel cases: main is readable even without a key; missing / blank / unreadable JSON returns null.
  • Version bumped to 3.8.1-30 (VERSION, package.json, test/update.test.ts, version badges and install URLs in all three READMEs).

Compatibility assessment: 3.8.1-25 established the re-check semantics for loginRequired (an env slot with configured access is not treated as unconfigured; the warning was removed in 3.8.1-28 but the re-check semantics stayed in handleResult). This change simply moves that re-check from "after the first message" to "at startup" — an addition, not a weakening. The identity semantics of 3.8.1-27 (env-slot access counts as "not signed in"; the banner identity only recognizes official slots / OAuth) are untouched — in the env-slot scenario the banner still shows "Not signed in" while the footer shows the model; the two coexist without contradiction (sign-in identity and model availability are separate concerns). The existing handleResult fallback and modelLabel() behavior are unchanged; normally signed-in boots (official slot / OAuth) still get model metadata directly from the runtime and never go through the new path.

Verification: tsc --noEmit passes; full bun test 693 pass / 0 fail (78 files); all 5 TUI smoke tests pass (smoke-tui with the two new assertions and the env-slot section; features / clear / pressure / widths without regression); after build:tui the vendored @zcode/tui copy was synced manually following the same steps as installLocalTui (no runtime changes).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/latest/download/zcode-cli-3.8.1-30.tgz

zcode-cli 3.8.1-29

Choose a tag to compare

@xhqing xhqing released this 06 Sep 02:40

Always show the sign-in state in the TUI welcome banner

  • Always show the sign-in state in the TUI welcome banner: every boot state now renders an identity status line, no more blank space (packages/zcode-tui/src/index.ts, packages/zcode-tui/src/login-identity.ts, packages/zcode-tui/src/welcome-banner.ts, test/welcome-banner.test.ts, scripts/smoke-tui.ts; version bumped to 3.8.1-29 across VERSION, package.json, test/update.test.ts, and the three READMEs' badges and install URLs).
    • Why: the user reported that running zcode showed no sign-in information in the welcome banner — you could not tell whether the session was Signed in or Not signed in. Signed-in (OAuth / key) and custom-provider boots already had their identity line (the system built in 3.8.1-26 / 27); only the unconfigured-model-access loginRequired boot was blank — the identity was hard-set to undefined in that state so the banner rendered no identity line, and after 3.8.1-28 removed the persistent warnings there was nothing left at all.
    • What changed: the TUI adds readBannerIdentity() — when loginRequired=true (the unauthenticated boot where the runtime skips model loading) or the identity snapshot read fails / returns undefined (nothing configured at all), it falls back to a signedOut identity so the banner shows "Not signed in"; both the first paint (run()) and the identity refresh (refreshLoginIdentity()) now go through it. Status-bar metadata behavior is unchanged (signedOut still emits no user / key fields); the snapshot function readLoginIdentitySnapshot() and its undefined semantics stay untouched (other callers such as zcode identity are unaffected — the undefined fallback exists only at the banner layer).
    • Traceability & regression assessment: what 3.8.1-28 removed was guiding warning copy ("Model access is not configured. / Run /login..."), while this change adds a factual state line ("Not signed in" / "Signed in as ..." / "API key ...") — different in nature, and this was an explicit user request ("please add the default sign-in state"), so it is not a regression; the identity priority established in 3.8.1-26 / 27 (OAuth account name > key mapping name > masked key > signedOut) is fully unchanged, only adding the display fallback for the undefined case.
    • Verification: tsc --noEmit passes; full bun test 691 pass / 0 fail (78 files; welcome-banner adds two cases for the signedOut identity line in wide / compact layouts); TUI smoke all 5 green (smoke-tui's first screen gains a "Not signed in" assertion — a pristine HOME's loginRequired boot shows the line from the very first paint); after build:tui the vendored @zcode/tui copy was synced manually following the installLocalTui step (the runtime itself is unchanged).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-29/zcode-cli-3.8.1-29.tgz

zcode-cli 3.8.1-28

Choose a tag to compare

@xhqing xhqing released this 06 Sep 02:18

Remove the persistent two-line model-access warning from the TUI banner

  • Remove the persistent two-line "Model access is not configured. / Run /login, or configure a custom provider in ~/.zcode/cli/config.json." warning from the top of the TUI (packages/zcode-tui/src/index.ts; version bumped to 3.8.1-28 across VERSION, package.json, test/update.test.ts, and the three READMEs' badges and install URLs).
    • Why: the user ruled this hint out. With model access unconfigured, two yellow warning lines sat permanently under the welcome banner — long-standing screen space whose guidance duplicated the /login wizard.
    • What changed: the loginWarning / loginHelp Text components, their layout mounts, and the updateLoginWarning() method are deleted; the loginRequired flag stays untouched (it still drives runtime behaviors such as skipping model loading, Goal / Session usage refresh, and identity refresh timing). The "Model access is not configured yet." status note inside the first-run /login guide dialog is unaffected — that is in-wizard interactive copy, not the persistent banner.
    • Traceability & regression assessment: the warning had shipped in 3.8.1-25 with compensating logic (when loginRequired=true, re-check via readConfiguredModelAccess() and hide the warning when access exists) guarding against the false positive "warning shown although an env slot is configured" — with the warning removed wholesale, that false positive can no longer occur, so this is a superset rather than a regression; the trade-off is that a genuinely unconfigured user no longer gets any guidance hint after startup (the first-run wizard remains, with its in-wizard status note visible) — the user-ruled intended behavior.
    • Verification: tsc --noEmit passes; full bun test 689 pass / 0 fail (78 files); TUI smoke all 5 green (smoke-tui / features / clear / pressure / widths); after build:tui the vendored @zcode/tui copy inside the locked runtime was synced manually (that day sync:locked could not run the full chain due to a CDN download interruption; the runtime itself was unchanged — only the new dist was copied, equivalent to the installLocalTui step).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-28/zcode-cli-3.8.1-28.tgz

zcode-cli 3.8.1-31

zcode-cli 3.8.1-31 Pre-release
Pre-release

Choose a tag to compare

@xhqing xhqing released this 06 Sep 03:59

⚠️ Known issue in this version: while signed out (no stored login, custom-provider.env only), picking a model from the /model list fails to connect to the upstream — the deduplicated list keeps official-slot entries that carry no credential in that state. Fixed in 3.8.1-32; until then, use 3.8.1-30.

The /model picker no longer lists env-file slot entries (env-<provider>/<model>) when the official prefix-free twin (<provider>/<model>) is also listed.

Why: A user reported (with a screenshot) that the /model list showed six entries: the three prefixed ones (env-bigmodel/glm-5.3, env-bigmodel/glm-5-turbo, env-bigmodel/glm-5.3-flash) duplicated the three prefix-free ones (bigmodel/glm-5.3, …), pointing at the same models; the user ruled the prefixed variants redundant. Root cause: the user was signed in through the official bigmodel slot (API key) while the same provider was also configured in custom-provider.env — the launcher writes the latter into the env-bigmodel slot of config.json, and the upstream runtime's listModels() reports both slots, so every model appeared twice in the picker.

What changed:

  • selectors.ts gains officialTwinId() (computes the prefix-free twin id of an env-file slot entry) and withoutEnvSlotTwins() (drops env-file slot entries whose official twin is listed; env-only entries without a twin stay — they are the only selectable path while signed out with just a custom provider).
  • Both modelPicker() (shared by the /model list and the quick cycle toggle) and providerModelPicker() (the /settings → Model providers cascade) pass through this filter; in the cascade, a provider group whose env-slot models are all shadowed disappears entirely.
  • The dual-slot config.json structure, the sign-in / sign-out flows (switchModelBlockToOfficialProvider etc.), and runtime behavior are untouched.
  • Version bumped to 3.8.1-31 (VERSION, package.json, test/update.test.ts, version badges and install URLs in all three READMEs).

Compatibility assessment: Since 3.8.1-26, displayProviderId() / displayModelRef() have only affected the display of the current model name (status bar, banner); /model list entries still used the raw ids. This change extends the same "the env- prefix is configuration plumbing, not for users to see" principle to the picker — same direction, not a regression. Env-only entries (no official twin) are kept, so the /model list of signed-out custom-provider users is unaffected (pinned by a unit test). The existing same-id dedup of modelPicker and the per-group dedup of providerModelPicker are unchanged — entries are deduplicated first, then twins filtered.

Verification: tsc --noEmit passes; three new selector unit tests (dual-slot listing drops env entries with the current marker on the official entry, env-only entries are kept, a fully shadowed env group disappears from the cascade); full bun test 696 pass / 0 fail (78 files); all 5 TUI smoke tests pass (after build:tui, the vendored @zcode/tui copy was synced manually following the same steps as installLocalTui).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-31/zcode-cli-3.8.1-31.tgz

zcode-cli 3.8.1-27

Choose a tag to compare

@xhqing xhqing released this 05 Sep 16:50

Treat official-slot API key sign-in as login (vault token first, key second), clear official-slot keys on logout, and force-collect a user name on BigModel login bound to the key mapping

  • API key sign-in counts as signed-in: sign-in detection upgrades from "vault OAuth token only" to a two-layer check (vault token first, official-slot key second), and logout clears official-slot keys as well (src/identity.ts, src/launcher.ts, packages/zcode-tui/src/index.ts, test/identity.test.ts, test/login-identity.test.ts, README.md, README_zh_hans.md, README_zh_hant.md, docs/CONFIGURATION.md).
    • Why: 3.8.1-26 anchored sign-in state solely to the vault oauth:<provider>:access_token. A user who completed /login by pasting an API key (the wizard reported success, the key landed in the official slot, the model switched to the official slot) still saw a "Not signed in" banner and no identity field in the status bar — the login flow said success while the identity display said signed out, a contradiction; it was also a display regression versus 3.8.1-25 (an official slot holding a key already showed a key identity) and violated the 3.8.1-26 principle that "a user action must produce correct feedback". The product ruling: a key pasted via /login is a login; only access through a custom-provider file (env- slots) counts as signed out.
    • What changed: (1) layered sign-in detection (new readSignedInProvider() in src/identity.ts): a vault OAuth token wins; with no token, a key in the official zai/bigmodel slots also counts as signed in (the official slot model.main points to is checked first, otherwise scan zai-first); (2) key identity restored for key sign-ins (readLoginIdentitySnapshot()): a key sign-in shows the key identity while an OAuth sign-in still shows the account name with absolute priority; signedOut narrows to env--slot-only access; (3) mapping names never impersonate account identity (a second same-day ruling): names in bigmodel-users.json are user-chosen key aliases and two accounts may share a name, so the "Signed in as " phrasing is reserved for OAuth account sign-ins (the account name the system actually read); key sign-ins always use the "API key" phrasing — with a mapping: API key <name> (<masked key>) (the snapshot carries a new keyMasked field), without: API key <masked key> — switching accounts (changing keys) necessarily changes the display, with one consistent reading across the banner / status bar / zcode identity / the zcode login already-signed-in notice; (4) the launcher adopts the new detection (two places in src/launcher.ts): the startup-sync skipModelBlock (a signed-in user's model block is not taken over by the env file) and the bare zcode login "already signed in" gate both cover key sign-ins — after pasting a key and restarting, the model is no longer pinned back to an env- slot; (5) logout clears official-slot keys (clearOAuthLoginCredentials() extended): beyond the vault it also clears the config official slots' apiKey (env- slots are kept to serve the signed-out path) — a key sign-out now truly returns to signed-out instead of "logged out but the identity lingers"; (6) TUI status bar fix (index.ts): a signedOut identity no longer emits a status-bar field (the old logic rendered a stray empty user label); a named identity's prefix changes from user to key and carries the masked key; (7) zcode identity set is refused in a key sign-in state and points to the bigmodel-users.json mapping (a key's display name belongs to the mapping file; only OAuth account names belong to identity set); (8) BigModel login force-collects a user name (user ruling: "however you sign in, running /login first forces a user-name entry"): any TUI /login that goes through a BigModel option (OAuth or pasting a key) — whether picked from the plain /login menu or typed as the command — pops a name input before the login runs (non-empty enforced, Esc cancels the login; the placeholder previews the key's existing mapping name); on success the name is automatically upserted into bigmodel-users.json bound to the landed key (new writeBigmodelUserName() in src/bigmodel-users.ts; hand-edited and login-collected entries are equivalent and freely mixable), and the display immediately refreshes to API key <name> (<masked key>). Z.AI sign-ins are not asked (the OAuth flow itself writes back the account name); a failed or cancelled login writes nothing; (9) docs synced (the sign-in permission sections of all three READMEs, the Sign-in identity and custom-provider sections, CONFIGURATION.md's login definition, logout behavior, and key-mapping sections).
    • Behavior map: /login with a pasted key → the key lands in the official slot + the model switches to the official slot + the banner immediately shows "API key ()" or "API key " + the status bar shows a key <name> (<masked>) field; restarting in a key sign-in state keeps the model on the official slot (the env file no longer takes over); /logout → vault + official-slot keys fully cleared, the banner returns to "Not signed in", and the env file takes the model back on the next start. OAuth sign-ins keep showing "Signed in as " exactly as in 3.8.1-26.
    • Verification: tsc --noEmit passes; full bun test 689 pass / 0 fail (78 files; identity gains 4 key-sign-in identity + 3 readSignedInProvider + 1 identity-set-in-key-state + 2 writeBigmodelUserName cases, the logout cases now assert official-slot keys are cleared, login-identity gains 3 key sign-in + 2 shouldPromptForLoginUserName cases); the release build's TUI smoke (scripts/smoke-tui.ts) adds live coverage of the user-name prompt — in the BigModel paste-key flow it waits for the name prompt after the key enter, types smoke to continue the login, and adds a banner API key smoke (<masked>) assertion plus a bigmodel-users.json binding assertion (the mapping file is addressed by the raw key, permission 0600, sits beside config.json as a sanctioned key store, joins the smoke leak whitelist, and is positively asserted).
    • Upstream research (2026-09-05, no code changes): confirmed upstream 3.11.2 (the official stable, three minors ahead of this project's locked 3.8.1) still fails to obtain the BigModel login user name — reversing the 3.11.2 runtime shows the shared credential store is structurally identical to 3.8.1 (only zai has a user_info slot) and loginBigmodelCodingPlan is structure-for-structure isomorphic (authorize → exchangeCode → exchange key → write config, never fetching a user name); the official changelog corroborates (the only login-related entries after 3.8.1 are three stability fixes: expired sign-in state / authorization callback failure / mcp oauth failure). Incidental finding: during the key exchange the runtime calls GET bigmodel.cn/api/biz/customer/getCustomerInfo, whose response carries account / organization info but only the ID is kept and then discarded, and the accessToken is never persisted nor callable outside the flow — this spawned two follow-ups (T3: evaluate upgrading the runtime to 3.11.2; T4: research the key → account info API).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-27/zcode-cli-3.8.1-27.tgz

zcode-cli 3.8.1-26

Choose a tag to compare

@xhqing xhqing released this 05 Sep 09:40

Make sign-in state first-class: instant identity switch after /login, custom-provider.env as the signed-out fallback with automatic hand-off

  • Make sign-in state first-class: after /login completes browser OAuth the banner and status bar switch to the signed-in account immediately, and the custom-provider env file is repositioned as the signed-out-only channel with automatic hand-off in both directions (src/identity.ts, src/env-config.ts, src/launcher.ts, packages/zcode-tui/src/login-identity.ts, packages/zcode-tui/src/index.ts, test/identity.test.ts, test/login-identity.test.ts, test/env-config.test.ts, README.md, README_zh_hans.md, README_zh_hant.md, docs/CONFIGURATION.md, .env.example renamed to custom-provider.env.example).
    • Why: after completing OAuth in the TUI (/login), the banner kept showing the masked API key from the custom-provider file — a successful login with zero visible feedback. The root cause sits in the 3.8.1-25 decoupling design: identity display followed the model.main prefix, which the custom-provider file pinned to its env-<id> slot on every start, so the official slots and the credential vault written by OAuth never got a turn; and under the old design, seeing your login account required deleting the file and re-creating it after logout — unusable as a product. The ruling product principle: a user action must produce correct feedback — signed in shows the signed-in account, signed out shows Not signed in, and the two states must hand off automatically without manual file juggling.
    • What changed: (1) sign-in detection (new readStoredOAuthLogin() in src/identity.ts): the presence of oauth:<provider>:access_token in the credential vault ~/.zcode/v2/credentials.json is the sign-in state (the oauth:active_provider marker takes priority; a stale marker falls back to scanning both providers' tokens); (2) login-first identity display (readLoginIdentitySnapshot() rewritten): signed in → show the login provider's account identity (vault user_info snapshot → BigModel key-name mapping → masked key), regardless of which slot model.main points to — visible on the refresh right after login; signed out with model access → new kind signedOut, banner / status bar show "Not signed in" and the provider displays the declared value with the env- prefix stripped; no access at all → the sign-in wizard warning stays. The TUI side (login-identity.ts) no longer mirrors the logic — it reuses the src snapshot functions (single implementation, no duplicate drift); (3) automatic model-ownership switch (env-config.ts): while signed in, the startup sync sets skipModelBlock (refresh only the env- slot data, never rewrite the model block); a leftover model block pointing at an env- slot is switched to the login provider's official slot by the new switchModelBlockToOfficialProvider() (model ID kept if the official slot already declares it, otherwise its first declared model); after logout the next start is signed out and the file takes the model block back — both directions seamless, the file never needs manual removal or restore; (4) file rename .env → custom-provider.env (template renamed to custom-provider.env.example with its header rewritten to the new semantics): the first start automatically renames a legacy ~/.zcode/cli/.env and prints one notice line (no migration when ZCODE_ENV_FILE pins an explicit path); (5) prefix-stripped display (new displayProviderId() / displayModelRef() in env-config.ts): the TUI model display, the zcode identity Provider line, and <provider-id>/<model-id> all show the value declared in the file (env-bigmodel/glm-5.3 → bigmodel/glm-5.3); the env- prefix lives only in config.json internal slot names (the isolation mechanism is unchanged); (6) the bare zcode login gate now blocks only when already signed in: signed in it prints "Already signed in as " and exits (--oauth forces a re-login), signed out it lets OAuth proceed even when a custom-provider file is configured — "running login means the user wants to log in"; (7) zcode identity set is refused while signed out (display names follow the login account; nothing to set when signed out); (8) zcode identity while signed out prints Identity: not signed in (model access via custom provider).
    • Behavior map (login / logout feedback loop): signed out + file → model via the env- slot, banner "Not signed in", model shown as <declared id>/<model>; /login OAuth completes → vault + official slot written, TUI refreshes instantly to "Signed in as ", next start the model block belongs to the official slot; /logout → vault cleared, banner flips back to "Not signed in" instantly, next start the file takes the model back over.
    • Migration note: the first start after upgrading renames ~/.zcode/cli/.env → custom-provider.env automatically (one console notice); set ZCODE_ENV_FILE to the old path to opt out. Signed-in users' model.main is moved from env-<id>/... back to the official slot on the next start.
    • Verification: tsc --noEmit passes; full bun test 674 pass / 0 fail (78 files; identity gains 3 sign-in-detection + 3 snapshot cases, login-identity re-signs signedOut semantics + 2 new cases, env-config gains 2 migration + 1 display + 1 skipModelBlock + 3 model-block-switch cases).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-26/zcode-cli-3.8.1-26.tgz

zcode-cli 3.8.1-25

Choose a tag to compare

@xhqing xhqing released this 05 Sep 07:42

Add BigModel key-name mapping, fix logout leaving BigModel credentials, and isolate env-file config into env- slots

  • Add a BigModel API key → display-name mapping file ~/.zcode/cli/bigmodel-users.json: per-key custom sign-in display names (a user name, a key name, or any label — purely local display text), so account switches no longer need re-pinning; a hint is printed after login when no mapping exists yet (new src/bigmodel-users.ts, src/identity.ts, src/launcher.ts, packages/zcode-tui/src/login-identity.ts, packages/zcode-tui/src/index.ts, test/identity.test.ts, test/login-identity.test.ts, README.md, README_zh_hans.md, README_zh_hant.md, docs/CONFIGURATION.md).
    • Why: BigModel logins (OAuth and API-key variants) only write the exchanged / pasted API key into config.json and never learn the account name, so the identity display falls back to the masked key; multi-account users cannot tell who is signed in after a switch, and the existing zcode identity set mechanism is a provider-level snapshot that must be manually re-pinned on every switch — miss it and the wrong name shows. With a per-key mapping, every key carries its own display name and switches are correct by construction.
    • What changed: (1) new src/bigmodel-users.ts — bigmodelUsersPath() (~/.zcode/cli/bigmodel-users.json, owned by the BigModel login channel and deliberately kept out of the model-access .env), readBigmodelUserNames() (fault-tolerant read: a missing file, bad JSON, or a non-object all yield an empty map; non-string and blank entries are skipped), resolveBigmodelUserName(); (2) identity resolution (readLoginIdentitySnapshot in src/identity.ts and its TUI mirror readLoginIdentity in login-identity.ts): priority is vault user_info snapshot → BigModel key lookup in the mapping (a hit returns the new kind named; the banner / status bar show Signed in as <name>, status-bar prefix user) → masked-key fallback; the mapping only applies to provider bigmodel (zai / custom providers are not looked up); (3) hints in three places: readBigModelKeyNameHint() detects "bigmodel + identity resolved to the masked key + no mapping"; the TUI prints it via suggestBigModelKeyName() after the non-attributable /login variants (bigmodel-coding-plan and the two api-key variants), the bare CLI zcode login prints it via printBigModelKeyNameHint() after success, and zcode identity appends a Tip line when showing a masked key; hints contain only the masked key and the file path, never the full key; (4) 13 new test cases (8 src-side: named resolution, oauth priority, provider restriction, read fault tolerance, hint positive/negative cases, identity-command Tip presence; 5 TUI-side: named / not covered / bad file / non-bigmodel / oauth priority, plus a loginIdentityText assertion for the named wording).
    • Scope note: the mapped value is entirely up to the user — a user name, a key remark, an account name, or any custom label works; neither the implementation nor the documentation constrains its semantics (purely local display text, no effect on authentication or requests). The .env channel's env-bigmodel slot is not looked up (the established boundary keeping it decoupled from /login). The zcode identity set mechanism is kept unchanged (it suits renaming within the same account; the mapping suits multiple keys / multiple accounts — the two complement each other).
  • Fix /logout failing to remove BigModel credentials: intercepted on both the CLI and TUI sides with a complete deletion list (zai + bigmodel + shared markers) (src/identity.ts, src/launcher.ts, packages/zcode-tui/src/index.ts, test/identity.test.ts).
    • Why: users reported that after /logout in the TUI the model still worked and the account username kept showing at the bottom. Reverse engineering vendor/zcode.cjs confirmed the root cause — the runtime's logout ultimately calls clearZaiLoginCredentials(), hardcoded to delete only 4 fixed keys (oauth:zai:access_token / oauth:zai:refresh_token / oauth:zai:user_info / zcodejwttoken, plus oauth:active_provider only when it decrypts to zai), while the keys written by the BigModel OAuth flow — oauth:bigmodel:access_token, oauth:bigmodel:user_info, oauth:login_attribution — are not on the list: the CLI zcode logout really prints "Logged out from Z.AI" and the credential file is indeed written, yet not a single vault entry is removed. The direct cause of the lingering username is oauth:bigmodel:user_info never being deleted (the TUI identity display reads it first).
    • What changed: (1) src/identity.ts adds clearOAuthLoginCredentials() — the deletion list covers both providers' full credential sets plus shared markers (the zai trio, bigmodel access/refresh/user_info, oauth:login_attribution, oauth:active_provider, zcodejwttoken), preserving unrelated entries (e.g. the zcodefeedbackclientid telemetry ID), idempotent (a missing or already-empty vault both succeed); runLogoutCommand() / isLogoutInvocation() form the CLI command entry; (2) launcher.ts intercepts zcode logout after the identity routing instead of passing it through to the runtime (the runtime's deletion list is a subset of this implementation, so the intercepted behavior is a superset); (3) the TUI submit() intercepts /logout locally (same layer as the suspended /login zai-coding-plan) with the new handleLocalLogout(): clear credentials → notice feedback → re-check the login state via readConfiguredModelAccess() (the loginRequired warning is only set when no model access is configured; .env / manual-config users keep model access after logout, matching the semantic boundary that an API key is model-access configuration, not a login state) → setLoginRequired triggers an inline identity refresh (user_info is gone, the display falls back to the masked key or disappears).
    • Scope note: logout only clears vault login credentials, not the API key in config.json (consistent with official runtime semantics — the OAuth-exchanged key is an independently valid credential; to cut off model access, delete .env or edit config).
  • .env configuration now writes its own env-<provider-id> slot, fully decoupled from the /login / /logout OAuth system (src/env-config.ts, packages/zcode-tui/src/index.ts, test/env-config.test.ts).
    • Why: the intent of .env is a custom-provider channel (universal for z.ai / bigmodel.cn / deepseek or any provider, without affecting normal login/logout), but the old implementation wrote the .env key directly into the config slot of the declared ID (ZCODE_PROVIDER_ID=bigmodel wrote provider.bigmodel) — the same official slot OAuth logins write to — creating a three-way tangle: the .env key was treated as an official login artifact (the TUI showed the OAuth username), an OAuth login would overwrite the .env configuration, and zcode login reported "no login needed" because the official slot had a key. The default was traced back to a deliberate design (borrowing the official slot to satisfy the upstream login gate — the runtime's hasConfiguredCodingPlanApiKey only recognizes the zai/bigmodel slots), but the cost was exactly the tangle above, conflicting with .env's positioning as a universal channel for any provider.
    • What changed: (1) buildProviderConfig() in env-config.ts now always outputs the env-<declared id> slot (ZCODE_PROVIDER_ID=bigmodel → writes provider["env-bigmodel"], model.main = "env-bigmodel/glm-5.2"; the declared id is still used for the base-URL default table and the provider display name); the envProviderSlotPrefix constant is exported; the official zai/bigmodel slots now belong exclusively to the OAuth flow — .env and /login never overwrite each other; (2) the TUI's handleResult() re-checks a runtime-pushed loginRequired=true with readConfiguredModelAccess() (slot-agnostic: only checks whether the provider pointed to by model.main has a non-empty key) and suppresses the "Model access is not configured" warning when configured — compensating for the runtime login gate only recognizing the two official slots; env-* slots and manually configured custom providers both benefit; (3) the identity display needed no changes and degrades automatically (login-identity.ts only consults the vault's user_info for zai/bigmodel; env-* slots show the masked key).
    • Migration note: on the first start after upgrading, the .env sync switches to the env-* slot; keys left behind in the old official slots are kept (harmless residue an OAuth login can overwrite normally). Existing users will see the model identifier change from bigmodel/glm-5.3 to env-bigmodel/glm-5.3 (semantically more accurate: this is .env-channel configuration).
    • Verification: tsc --noEmit passes; full bun test 644 pass / 0 fail (78 files; env-config slot assertions updated plus a strengthened "official slots are never touched by .env" case; 5 new identity logout cases).

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-25/zcode-cli-3.8.1-25.tgz

zcode-cli 3.8.1-24

Choose a tag to compare

@xhqing xhqing released this 05 Sep 04:28

Show the workspace directory beside the turn timer in the TUI status footer

  • Show the workspace directory beside the turn timer in the TUI status footer (tell concurrently open sessions apart) (packages/zcode-tui/src/turn-status.ts, packages/zcode-tui/src/index.ts, packages/zcode-tui/src/welcome-banner.ts, test/turn-status.test.ts).

    • Why: the current directory was previously shown only in the welcome banner; once a long session scrolls the banner off screen, several concurrently open zcode TUI instances can no longer be told apart. The directory now lives on the turn status line (the line that carries the turn timer), always visible right next to the timer and refreshing along with it.
    • What changed: (1) welcome-banner.ts exports the home-prefix-abbreviating displayWorkspace() logic as abbreviateWorkspaceDirectory() (banner and timer line now share one abbreviation rule); (2) turn-status.ts adds turnStatusDirectoryText() -- the workspace directory abbreviated to ~ form, and when it exceeds the 24-column budget (TURN_STATUS_DIRECTORY_MAX_COLUMNS) it is truncated from the head while keeping the tail (the distinguishing part of concurrently open sessions is mostly the project name at the end of the path); the 24-column budget leaves room for the right-side Goal status on 80-column terminals (measured: timer + directory + compact Goal coexist at 80 columns; narrower terminals prioritize timer + directory); (3) index.ts updateTurnStatus() appends the directory text (muted style, ─ separator) to the right of the timer text, and the directory shows alone when there is no timer content; the directory text is sanitized via sanitizeTerminalText and cached (cwd does not change within a process lifetime, ??= computes it once); (4) 3 new test cases (same abbreviation as the banner, head-truncation keeping the tail over budget, wide-character directory truncated without breaking characters).
    • Verification: full bun test 639 pass / 0 fail (78 files); tsc --noEmit passes; multi-width rendering measured at 100 / 80 / 60 columns confirms the expected layout; after the build:tui rebuild, the @zcode/tui copy inside vendor is refreshed in sync (matches the build output).
  • Fix the TUI showing the previous account's username after switching accounts: the identity display now refreshes unconditionally after login, and logins whose identity cannot be attributed clear stale snapshots when the API key changes (src/identity.ts, packages/zcode-tui/src/index.ts, test/identity.test.ts, test/login-flow.test.ts, README.md, README_zh_hans.md, README_zh_hant.md).

    • Why: users reported that after switching accounts and logging in again, the TUI banner and status bar still showed the old account's username. Investigation confirmed two root causes -- (1) session layer: setLoginRequired refreshed the identity display only when loginRequired flipped from true to false; when switching accounts while already logged in, that state stays false, so refreshLoginIdentity never fired and the old name persisted until the TUI was restarted; (2) data layer: the display name is read from the vault's oauth:<provider>:user_info snapshot; the Z.AI OAuth login flow rewrites that snapshot with the new account's user object on every login (runtime saveZaiLoginCredentials, confirmed by reverse engineering), but BigModel OAuth and the two API-key login variants only write the new API key into config.json and never touch the snapshot (reverse engineering confirmed the runtime has zero references to oauth:bigmodel:* keys; that entry in the local vault is a leftover from the uninstalled ZCode Desktop) -- after switching to another BigModel account, the name in the snapshot belongs to the old account and a restart does not fix it, and the CLI has no way to obtain the new account's username (the token exchange happens entirely inside the runtime process, and the live fetch endpoint is blocked by a WAF, see the previous entry).
    • What changed: (1) src/identity.ts adds readProviderApiKeySnapshot() (snapshots the zai / bigmodel providers' config API keys before login) and clearIdentitiesWithChangedKeys(before) (after login, a provider whose key changed has its user_info snapshot deleted because it can no longer be attributed to the current account; returns the cleared list); (2) the TUI's setLoginRequired now calls refreshLoginIdentity() on every invocation (no longer only on state flips -- switching accounts while logged in also refreshes), and refreshLoginIdentity compares values (early-returns when kind + label are unchanged, avoiding a redraw on every result); (3) new isLoginWithoutIdentityRefresh() detecting the three non-attributable login commands (/login bigmodel-coding-plan and the two -api-key variants; Z.AI OAuth excluded because it rewrites the snapshot itself); submit() snapshots the keys before submitPrompt for such commands and calls the new clearStaleIdentityAfterLogin() after handleResult: key changed -> clear the snapshot, refresh the display, and hint that zcode identity set <name> can pin the new name; key unchanged (re-login to the same account) -> keep the current name; (4) 7 new test cases (5 in identity: snapshot only includes vault-backed providers, a key change clears the snapshot while preserving neighboring entries, an unchanged key keeps it, a zai key change also clears, no snapshot is a no-op; 2 in login-flow: positive and negative cases of the predicate); full bun test 636 pass / 0 fail (78 files), typecheck passes; rebuilt with build:launcher + build:tui, and the @zcode/tui copy inside vendor is refreshed in sync.
    • Scope note: this fix guarantees "the old account's name no longer shows after a switch" (a Z.AI switch shows the new name directly; a BigModel / API key switch falls back to the masked API key with a hint to pin the new name manually) -- obtaining the new BigModel username during login is a structural limitation of the upstream runtime (the token never touches disk); showing it automatically requires upstream support and is out of scope for this fix.
  • New zcode identity subcommand: view / manually sync the signed-in identity display name (fixes the TUI showing a stale username after a rename) (new src/identity.ts, src/usage.ts, src/launcher.ts, new test/identity.test.ts).

    • Why: the user renamed their account on bigmodel.cn, but the TUI banner and status bar kept the old name. Investigation confirmed the root cause -- the display name is read from the encrypted oauth:bigmodel:user_info snapshot (containing username / displayName) in the machine-wide shared credential vault ~/.zcode/v2/credentials.json; that snapshot was written when ZCode Desktop (which shares the vault with the CLI) logged in via OAuth, and nothing has refreshed it since: Desktop is uninstalled, and the official runtime's bigmodel login flow (reverse engineered from vendor/zcode.cjs) only writes the exchanged API key into ~/.zcode/cli/config.json and never the vault's user_info -- so even re-running zcode login cannot clear the old name. Fetching the latest username from the server in real time is not dependable: the primary endpoint GET bigmodel.cn/api/biz/customer/getCustomerInfo (the same one used by the web profile page and the runtime login chain Ghr->Uhr) is currently blocked by an Aliyun WAF challenge from this machine both directly and via proxy (200 + empty body + acw_tc cookie), and the monitor domain has no user-info endpoint. Hence a manual sync command as the only reliable refresh path.
    • What changed: (1) src/usage.ts adds encryptCredential() -- the dual of decryptCredential() (enc:v1: AES-256-GCM, random 12-byte IV, 16-byte tag, three base64url segments, key derivation identical to the runtime), letting the CLI write back credentials the runtime can read; (2) new src/identity.ts -- zcode identity shows the active provider's identity (OAuth account name or masked API key, with the same read priority as the TUI's login-identity.ts mirror; the package dependency direction forbids importing it the other way, hence the standalone implementation); zcode identity set <name> rewrites the active provider's user_info snapshot username + displayName (the oauth:active_provider marker first, falling back to the config.model.main prefix, default bigmodel), preserving other fields such as id / avatarUrl and neighboring vault entries (login_attribution etc.), and creating the entry when no snapshot exists; zcode identity clear deletes the snapshot and falls back to the API-key display; the name is capped at 64 characters, set is rejected for non-OAuth providers, and the vault is written back with 0600 permissions; (3) launcher.ts wires the identity route after the stats route; (4) new test/identity.test.ts with 13 cases (encrypt/decrypt round trip + random IV, command argument dispatch, the three snapshot read priorities, set preserving fields / creating an entry / two rejection classes, show / clear / no-op / missing state). Rebuilt with bun run build:launcher; full bun test 630 pass / 0 fail (78 files). Live check: zcode identity outputs Provider: bigmodel / Identity: signed in as <old name> (reproducing the issue); the vault is shared globally, and after set a new TUI session picks it up immediately.
    • Scope note: identity set is a local display-name sync and does not change the server-side account; if the WAF ever allows the live endpoint, automatic refresh could be filed separately (not filed now -- live fetching is unusable in production today, and automatic refresh would silently fail and fall back to the stale snapshot, adding complexity for nothing).
    • Version note: these changes were originally recorded under the 3.8.1-23 entry, but v3.8.1-23 had already been published as a GitHub Release (2026-09-04, whose notes do not include these two items); af...
Read more

zcode-cli 3.8.1-23

Choose a tag to compare

@xhqing xhqing released this 04 Sep 06:38

Enable workspace-hook trust system in the TUI session factory

  • TUI session factory now enables the workspace-hook trust system: bootstrap passes workspaceHookTrustEnabled: true (TODO T1 completed) (scripts/sync-runtime.ts, scripts/check-runtime.ts, vendor/zcode.cjs, test/sync-runtime.test.ts, TODO.md, new TODO-archive.md).

    • Why: the runtime already ships a complete project-level trust system for workspace hooks (pending_trust -> trusted_persistent state machine, declaration sha256 + bundle digest binding, persistent trust store at ~/.zcode/security/workspace-hook-trust-v1.json, and the zcode hooks trust status/grant/revoke command family); the headless protocol path (--prompt) already sets workspaceHookTrustEnabled:!0; only zcode-cli's TUI session factory never passed it, so the runtime side was permanently false and project-level hooks were disabled as a whole (a CLI grant had no effect, and every session logged workspace_hook.feature_disabled). The consequence was that DayTradingAgent had to fall back to two security hooks under the user-level ~/.zcode/cli/config.json, and on 2026-09-04 that path was measured to be overwritten wholesale by a stale snapshot when the client saves settings -- the project-level single source is the stable state.
    • What changed: (1) sync-runtime.ts gains patchRuntimeWorkspaceHookTrust(runtime) -- it anchors on the unique anchor of the TUI session factory (...,onWorkflowEvent:b.onWorkflowEvent})) and injects ,workspaceHookTrustEnabled:!0; idempotent (returns the input unchanged when already patched) and throws with incompatible when the anchor is missing; wired into the installTuiBridge patch chain (between patchRuntimeHttpNoContent and patchRuntimeAgentAutoBackground). (2) The idempotent check chain in check-runtime.ts gained the matching line. (3) The patch was actually written into vendor/zcode.cjs (net +29 bytes, at offset ~12365683) and node --check passes. (4) New cases in test/sync-runtime.test.ts (injection assertion, idempotence, incompatible throw; the fixture cannot be executed with new Function, so these are pure string assertions).
    • End-to-end verification (tmp/smoke-hook-trust.ts, reproducible, gitignored): temporary HOME plus a project-level SessionStart hook through the whole flow -- before granting, a new session log contains no workspace_hook.feature_disabled (patch effective), config load logs config.project_hooks.pending_trust, and the hook is blocked by the trust gate (no marker file); after zcode hooks trust grant (CLI path, status becomes workspace_hooks_trusted_persistent) the hook actually runs in a new session (marker file appears) and the log still contains no feature_disabled. Two findings worth keeping: SessionStart hooks run on the first turn (the first user input triggers runSessionStartHooks("startup")), not at TUI startup, so a self-test must send a message; on macOS the /tmp -> /private/tmp symlink makes the workspaceIdentity recorded by the CLI differ from the one the TUI session resolves from cwd (the grant never reaches the session), so smoke sandboxes must live on a symlink-free path (under the project's tmp/). Real workspace paths are stable and unaffected.
    • Verification: bun test test/sync-runtime.test.ts 26 pass; full bun test 617 pass / 0 fail across 77 files; bun run check passes (zcode-cli 3.8.1-23 / zcode-runtime 0.16.3). Follow-up on the DayTradingAgent side (its TODO T140 2 and 3): rerun the credential probe in a new ZCode session (should still be blocked, now by the project-level hook), then withdraw the two user-level hooks and switch back to the project-level single source.
    • Follow-up closure: T1 is archived into the newly created TODO-archive.md; the same-origin risk (the client saving settings rewrites config.json wholesale from an in-memory stale snapshot, wiping external edits to the hooks section) is filed as TODO T2 (orange urgency).
  • Added the project-root TODO.md and registered T1 (session bootstrap should pass workspaceHookTrustEnabled: true) (new TODO.md).

    • Why: verification on the DayTradingAgent side (2026-09-03~04) found that zcode-cli never passes this parameter when constructing a session, so the project-level workspace hooks trust system already built into the runtime was disabled by the host capability switch (the CLI grant was in place and the trust store persisted -- only this switch was missing). Per the cross-project split, this work item was transferred from DayTradingAgent TODO T140 (1) and registered here.
    • What changed: created TODO.md (a four-level urgency section framework plus a numbered timestamp convention) and registered T1 (orange) -- task description, three vendor/zcode.cjs offset references (construction site ~11703354, bootstrap consumer ~11762913, !0 sample ~11922028), post-completion verification criteria (rerun the credential probe in a new DayTradingAgent session, no feature_disabled in the log, then withdraw the user-level fallback and switch to the project-level single source), and a note on the same-origin risk (measured on 2026-09-04: the client saving settings rewrites config.json wholesale from a stale snapshot, wiping external edits to the hooks section -- whether to file it separately was left undecided). This project had no TODO-archive.md yet (first item, nothing to archive); it was created along with the first archive.

Install

npm install -g https://github.com/xhqing/zcode-cli/releases/download/v3.8.1-23/zcode-cli-3.8.1-23.tgz