CLI reference
Day to day, working with Zhin mostly means working with one command, zhin: start the bot, change config, install plugins, diagnose problems. It comes from @zhin.js/cli and is called in a project as npx zhin … or pnpm exec zhin …. Creating a new project uses a separate command, npm create zhin-app.
npx zhin --help # list every commandCommand results (lists, details) go to stdout without a log prefix, so they can be piped; progress and diagnostics go to stderr. The global --verbose option adds the [time][LEVEL] [CLI] prefix to diagnostic lines; --debug shows the log correlation id when a running instance answers with an internal error, and finds the matching line in the instance log.
Running outside a bot project
Inside a project that has @zhin.js/cli installed, npx zhin uses it directly. Outside one (for example creating a standalone plugin, or publishing from a plugin directory), npx downloads the npm package named zhin:
- Since
zhin@4.0,zhinon npm is an alias of@zhin.js/cli:npx zhin <command>is the same asnpx @zhin.js/cli <command>. zhin@3.xis the old, unmaintained framework package, not this CLI. If your npx cache still holds 3.x, or a project pinszhin@^3, you get that one instead, which shows up as missing commands or startup errors. Usenpx @zhin.js/cli(reliable with every version), ornpx zhin@latestto force the new one.
The commands you'll use most
| I want to… | Command |
|---|---|
| Start the bot (dev mode, hot reload on save) | pnpm dev (i.e. zhin start --foreground --watch source config) |
| Run in the background on a server / inspect / stop | npx zhin start / npx zhin status, npx zhin logs -f / npx zhin stop |
| Run under systemd / Docker / PM2 (foreground) | pnpm start (i.e. zhin start --foreground --mode production --no-watch) |
| Create the first Console owner | npx zhin console init (or finish it in the Console with the link printed at startup) |
| Add Console accounts, reset a password | npx zhin console user create <username> --role viewer / npx zhin console user passwd <username> |
| Connect a platform, configure AI or a database | npx zhin setup (optionally --adapters / --ai / --database) |
| Diagnose environment or config problems | npx zhin doctor |
| Check the config file is valid | npx zhin config check |
| Install a community plugin | npx zhin install <plugin> |
| Turn a plugin off / on again without uninstalling it | npx zhin disable <instanceKey> / npx zhin enable <instanceKey> |
| Find plugins and setup steps by purpose | npx zhin use-cases |
| Create a new, publishable plugin | npx @zhin.js/cli new my-plugin --standalone |
| Check a plugin before publishing | npx @zhin.js/cli plugin check |
| Add a command, middleware, tool… file to a plugin | npx zhin create command hello |
| Add a local plugin to an existing bot project | npx zhin new my-plugin |
| Update dependencies (stop instance → install → start, rolled back on failure) | npx zhin update |
| Upgrade Zhin | npx zhin migrate --dry-run, then again without --dry-run |
| Install as a start-on-boot system service | npx zhin service install |
All commands, grouped by purpose, follow.
Running
start / stop / restart / status / logs: Gateway and Instance
zhin start [--foreground] [--mode development|test|production] [--environment <name>]
[--watch source config | --no-watch] [--once] [--open] [--log-file <path>] [--standalone]
zhin stop [--timeout <ms>]
zhin restart [--gateway]
zhin status [--json]
zhin logs [-f] [-n <1-1000>] [--interval <ms>]zhin start starts the management gateway (long-lived; serves HTTP and the Console), which launches the Zhin instance. It runs in the background by default; --foreground (-f) keeps it attached (development, systemd, containers, PM2). Only --foreground, --once and --standalone run attached: --mode production on its own still runs in the background, so process managers must pass --foreground. While the gateway runs, the other commands talk to it over the local control socket .zhin/gateway.sock: stop stops instance and gateway, restart restarts only the instance (--gateway restarts a background gateway), status shows both (exit code 3 when not running), logs reads the instance logs. Without a gateway, stop / restart have nothing to do and logs reads .zhin/logs/instance.log. Run the management commands as the same system user that started the gateway. Options, exit codes and a systemd example: zhin start.
For zhin update, see Update dependencies below.
runtime: Project Tools
zhin runtime start [...] # gateway in the foreground; same as zhin start --foreground
zhin runtime inspect
zhin runtime codegen [dir] [--write] [--json]
zhin runtime build
zhin runtime publish [--execute|--resume] [--tag latest]
zhin runtime migrate [extract|cutover|verify|status] [--check|--write]--watch and the compatibility flag --no-watch are mutually exclusive. Without either, no files are watched.
| Subcommand | Purpose |
|---|---|
start | Run gateway and instance in the foreground (alias of zhin start --foreground); see zhin start |
inspect | Print the project structure as JSON (plugin list, build plan) |
codegen | Scan the convention directories and print static type declarations for the plugin's capabilities plus a capabilityGraph summary; --write writes plugin.capabilities.d.ts into the target directory and --json prints the scan result. dir defaults to the current directory |
build / publish | Build and publish according to the project structure. publish only prints the plan by default; --execute runs it, and --resume continues after an interruption |
migrate | Generate and apply a project-structure migration plan: --check previews, --write writes atomically, status prints a readiness report |
Scaffolded projects already have these conventional scripts:
{
"scripts": {
"dev": "zhin start --foreground --watch source config",
"start": "zhin start --foreground --mode production --no-watch",
"stop": "zhin stop"
}
}service: System Service
zhin service install [--user]
zhin service uninstall [--user]
zhin service status [--user]Installs a start-on-boot / supervised service. --user uses user-level systemd on Linux; status works on Linux and macOS only. The service runs the gateway in the foreground (zhin start --foreground --mode production --no-watch); the gateway restarts the instance.
db: restricted account of the Console SQL editor
zhin db console-role [--print-sql] [--role <name>]On PostgreSQL / MySQL the Console SQL editor runs only on a separately logged-in restricted account (ADR 0059, Chinese). This command creates it with the main account (default name zhin_console_<project hash>), writes a random password to .env (ZHIN_DB_CONSOLE_USER / ZHIN_DB_CONSOLE_PASSWORD) and references it from database.console; running it again rotates the password. When the main account may not create accounts, add --print-sql: nothing is created, the SQL is printed for a DBA (the password is the placeholder <ZHIN_DB_CONSOLE_PASSWORD>, the real value is in .env). It refuses while the project database still holds the old Console account tables — migrate first. Then zhin restart. zhin doctor --fix does the same. SQLite needs none. See configuration: database.console.
console: Setup and User Management
zhin console init [--status] [--reset-store]
zhin console init [--database sqlite|mysql|pg|mongodb] [--database-<field> <value> ...]
[--database-password-env <VAR> | --database-password-stdin]
[--owner <username>] [--display-name <name>] [--password-stdin] [-y]
zhin console user create <username> [--role viewer] [--display-name <name>] [--password-stdin]
zhin console user passwd <username> [--password-stdin]
zhin console user disable <username>
zhin console user enable <username>
zhin console user list
zhin console migrate [--revert]Console users, roles, sessions, API tokens and the audit log live in the gateway's own .zhin/gateway.db (ADR 0049, ADR 0059), not in the project database. These commands do the same as the Console's setup wizard and Access pages — use them on a server, in a container build, or when a password is lost.
- Who runs them: while the project's gateway runs, the commands go to it over the local control socket
.zhin/gateway.sock(a connection counts as the local owner), so a database switch or a new owner takes effect at once. Without a gateway they open.zhin/gateway.dbdirectly (offline setup and rescue), after moving over the account tables an earlier version left in the project database. A socket that belongs to another system user or does not answer is an error; the command never edits files behind the gateway's back. inithas two steps: choose the database (the current configuration, usually SQLite, is kept by default), then create the first owner. Without flags it asks interactively;--statusprints the setup state as JSON. Once an owner exists it fails — usezhin console userinstead.--database <dialect>switches database; pass connection fields as--database-filename,--database-host,--database-port,--database-user,--database-database,--database-url,--database-db-name,--database-modeand so on. The CLI tests the connection first, then writes the configuration and puts secret fields in.env.- There are two recommended ways to pass the database password (use only one source):
--database-password-env <VAR>references an existing environment variable (the config gets${VAR});--database-password-stdinreads the first line of stdin, stores it in.envand writes${VAR}into the config. Together with--password-stdin, the first line is the database password and the second the owner password. --database-password <value>still works but is not recommended: the secret stays in shell history and the process list (ps), and the command prints a warning.--ownerplus--password-stdincreates the owner non-interactively (the password is the first line of stdin, or the second line together with--database-password-stdin; at least 10 characters); with-y, missing flags fail instead of prompting, for scripts.
user:createbinds the globalviewerrole by default,--roletakesowner/admin/operator/vieweror a custom role;passwdresets the password and revokes all of the user's sessions;disable/enablesuspend and restore;listprints users, state and role bindings. Adapter- or account-scoped bindings are set in the Console's Access pages.init --reset-store: when.zhin/gateway.dbwas lost or replaced (the project database's markerzhin_gateway_storenames another store) the Console stays closed; once you accept that the old accounts are gone, this resets it (gateway stopped first, asks for confirmation,--yesskips it), records the reset in the new audit log and continues with setup.migratemoves the Console account tables an earlier version kept in the project database into.zhin/gateway.db(the gateway also does it when it starts; while a gateway runs the command only reports the state).--revertwrites them back into the project database and empties gateway.db — required before downgrading, with the gateway stopped (zhin stop). See the upgrade notes.http.tokenis only a bootstrap token: valid until the first owner exists, refused on every endpoint afterwards.
# Non-interactive setup in a container or on a server: create the owner before opening the port
printf '%s\n' "$OWNER_PASSWORD" | zhin console init -y --owner admin --password-stdin
printf '%s\n' "$VIEWER_PASSWORD" | zhin console user create alice --role viewer --password-stdin
# Switch to PostgreSQL: database password on line 1, owner password on line 2, neither on the command line
printf '%s\n%s\n' "$PG_PASSWORD" "$OWNER_PASSWORD" | zhin console init -y --database pg \
--database-host db --database-user zhin --database-database zhin \
--database-password-stdin --owner admin --password-stdinExit code 0 on success, 1 on failure (already set up, unknown user, database connection failed, missing flags and so on).
Configuration and diagnostics
setup: Interactive Configuration Wizard
zhin setup # full wizard
zhin setup --database # database only
zhin setup --adapters # adapters only (add accounts)
zhin setup --ai # AI only
zhin setup -y --adapter sandbox --enable-ai --install # non-interactive: run only the steps given by flags
zhin setup --decisions # optional TypeSafe Jev decision service (needs existing AI config)
zhin setup --bootstrap # bootstrap files only (SOUL.md / TOOLS.md / BOT.md; an existing AGENTS.md without BOT.md is kept as the bot file)
zhin setup --global # initialize a global instance in ~/.zhin
zhin setup --locale en # set the project language (zh-CN or en); combines with the other optionsThe wizard edits the matching parts of zhin.config.yml and writes secrets to .env. Non-interactive flags: --adapter <name> (repeatable) adds an adapter, --enable-ai enables AI (add model providers later in the Console), --skip-ai / --skip-adapters skip those onboarding steps, --install installs the needed dependencies now (the Console's allowlist), and -y asks nothing and runs only the steps given by flags.
Without --locale the project's existing language is kept; language and time zone are independent. See Language and Localization.
For network databases, the CLI checks the port range, writes connection fields as environment references and puts the real values in .env. Afterwards run npx zhin config check to catch missing database variables before startup; if the values only live in .env.production, use npx zhin config check --environment production.
config: Read and Write Configuration
zhin config list # whole configuration (secrets masked)
zhin config get <key> # dotted path ai.enabled or JSON Pointer /plugin/key.with.dot
zhin config set <key> <value> # value is parsed as JSON, otherwise kept as a string
zhin config unset <key> # aliases: delete / del
zhin config path # print the local config file path
zhin config check [--fix] [--json] [--strict]get / set / unset / list talk to the running Host by default (POST {base}/console/request). A write first reads the config revision, then submits one config:patch-project; the Host performs the CAS, schema validation, save and any live application. The CLI never does its own read-modify-write and never retries.
- Connection: without
--hostthe command only uses this project's gateway over its local control socket (.zhin/gateway.sock), as the local owner (or as theZHIN_CONSOLE_TOKENprincipal when that is set). When this project's gateway is not running the command fails with "本项目的网关未运行" (start it withzhin start, or pass--offline) and does not connect to127.0.0.1:<http.port>, where another project's gateway may be listening. To reach another address pass--host <url>explicitly; the token then comes fromZHIN_CONSOLE_TOKEN(or the variable named by--token-env <name>). A loopback--host(127.0.0.1/localhost/[::1]) is checked first: if its/pub/healthreports a different project id the command refuses without sending the request or the token. A target that reports no project id (an older gateway or a--standaloneinstance) is refused the same way, with a non-zero exit: upgrade that gateway and start this project withzhin start, or, if you are sure of the target, pass--allow-unverified-hostto continue with a warning only (it never accepts a different project id). Sending a token to a non-loopback address requires HTTPS.zhin send,zhin schedule,zhin watchandzhin packages … --livefollow the same rules (the first three also accept--host <url>; all of them accept--allow-unverified-host). - If the Host is not running the command fails; it never falls back to writing the file. For offline edits pass
--offlineexplicitly: the samepatchProjectrules as the Host (revision check, plugin declarations are read-only, per-section schema validation) run locally before saving. No Runtime is started, no pluginsetupruns and the CLI sends no request to the Host, but loading plugin declarations executes module top-level code (which may itself use the network). An offline save is not applied to a running Host until--watch configor Console "Apply configuration". - Writing a secret field (marked secret / writeOnly / password in the schema, or a token / password-like key) stores the value in the project's
.envand keeps or puts a${VAR}reference in the config: the variable the field already referenced (such as${HTTP_TOKEN}), or a name derived from the path (http.token→HTTP_TOKEN,_2on a clash); a variable already defined in.env.<environment>is written there. The output names the variable and never echoes the value. Add--plainto write the literal into the config file on purpose (a warning is printed). Console saves follow the same rule. - Reads are masked by schema:
secret/writeOnly/hidden/ password fields, credential-like keys and unexpanded${...}/env://references print as[redacted]. Pass--revealfor raw values. - With
--json,get/list/set/unsetprint exactly one line of compact JSON, for scripts; without it they print only human-readable text (config pathandconfig checkhave their own formats). - Exit code
6also reportsrequestIdandexpectedRevision(in the result object with--json, otherwise on stderr) so the write can be verified. Other failures show only the reason; the global--debugoption adds both to stderr. - A write that cannot apply explains why: an unknown key lists the closest settings; an array index past the end states the current length and how to append (read the whole array, add the item, write it back). When
http.portin the config is broken and no gateway is running, the CLI says to repair it with--offlinefirst.
Exit codes are a public contract:
| Code | Meaning |
|---|---|
0 | Success: saved and applied (or nothing to apply); read succeeded; an --offline save (not yet applied to a running Host) |
3 | Saved; some settings take effect after a process restart |
4 | Revision conflict: changed elsewhere, not saved; the latest (masked) values are printed (values under schema branches that cannot be proven safe, such as if/not/contains, a nested $id or a non-local $ref, are masked whole) |
5 | Validation failed (including rejected plugin-declaration writes): when not saved, field paths are printed; also returned when the file was saved but failed validation before applying, so check with zhin config get |
6 | Unknown: the write was sent but no definitive receipt arrived (including non-authoritative errors such as a gateway 502/504), or it was saved but the application result is unconfirmed. requestId / expectedRevision are reported; check with zhin config get first; do not blindly retry |
7 | Not connected to a running Host: nothing was sent, the file is unchanged |
1 | Any other error: usage, missing key, authentication (401/403), an authoritative Host rejection (not saved); or saved but not applied (superseded by a newer revision, or a group failed/was blocked; the file was written) |
config check validates the config file, and --fix repairs fields that can be migrated safely. Missing database connection variables are always errors; other warnings become errors with --strict, which suits CI.
doctor: Environment Diagnostics
zhin doctor [--fix] [--upgrade-l4]
zhin doctor --live <host-url> [--json]Checks Node, ports, tokens, config and other environment issues; --fix repairs what it can; --upgrade-l4 diagnoses what's needed to go from a minimal project to the full AI stack. --live reads a running Host's readiness report (needs an API token with status.view in ZHIN_CONSOLE_TOKEN; put a custom http.base in the URL path) and --json prints it as JSON; exit code 0 when ready, 1 otherwise. See Production. When --fix adds dependencies it writes the real version ranges released alongside the CLI (e.g. ^1.1.11), never latest; run pnpm install afterwards. When the project has @zhin.js/gateway, doctor also loads the gateway's node-pty from the project directory: when that fails (usually because pnpm skipped its build script) it warns with the repair command, and the Console's local terminal is unavailable. See Build scripts of the native dependencies.
dev: Develop Against a Local zhin Checkout or a Standalone Plugin
zhin dev link <zhin-path> [--project <dir>] [--no-install]
zhin dev unlink [--project <dir>] [--no-install] [--force]
zhin dev link <plugin-dir> [--project <dir>] [--as <instance>]
zhin dev unlink <instance> [--project <dir>]When <plugin-dir>/package.json has a zhin field, connects the standalone plugin to the current bot: links it under plugins/, adds ./plugins/<instance>, and makes the plugin's @zhin.js/* use the bot's copies, so edits hot-reload. See hot-reload a standalone plugin.
Given a zhin checkout, in an external plugin project it switches zhin.js / @zhin.js/* to a local checkout as a whole (packs the full dependency closure and writes pnpm.overrides, so each package has exactly one instance); unlink restores the original files. The "Zhin 包单实例" check in doctor detects duplicate instances and suggests this command. See the local development guide.
onboard: Project Onboarding
zhin onboard [--quick] [--flow quickstart|full]Reuses existing config, environment variables and data in a project, letting you keep or reconfigure them. --quick (-q) only shows the quick-start hints without the wizard; --flow defaults to full, and quickstart asks fewer questions.
migrate: Project Upgrade
zhin migrate [--dry-run] [--no-install] [--include-pinned]Upgrades an older Zhin project to the latest: dependencies, scripts and folder structure. --dry-run only prints the plan and ends with "nothing was changed"; --no-install skips the final pnpm install. Dependencies move to the newest release on the registry your npm configuration points to (mirrors included): ranges become ^x.y.z, exact versions stay exact, and @zhin.js/* entries in package.json#pnpm.overrides are aligned with the dependencies of the target zhin.js. Dependencies you locked (exact versions such as 1.1.6, link:, file:) are kept unless you pass --include-pinned. Full upgrade flow: Compatibility and migration.
In a project directory (one with zhin.config.*), zhin start needs the CLI and the management gateway in production too, so zhin migrate puts zhin.js, @zhin.js/cli and @zhin.js/gateway together in dependencies: a package listed only in devDependencies moves with its version, a duplicate left in devDependencies is removed, and a missing @zhin.js/gateway is added (the CLI's dist-tag such as latest when it uses one, otherwise the range released together with the running CLI). A --prod install therefore keeps everything startup needs. A Plugin package (no project config) keeps the CLI in devDependencies. The zhin runtime migrate cutover does the same for private (project) packages.
In a pnpm project that declares @zhin.js/gateway, zhin migrate also adds the gateway's native dependencies node-pty, ssh2 and cpu-features to allowBuilds in pnpm-workspace.yaml (creating the file when missing; existing true / false values are kept, the set this to true or false placeholder pnpm 11 leaves is set to true, and an existing onlyBuiltDependencies list is appended to), so the install that follows builds them. See Build scripts of the native dependencies.
pnpm 11 does not install releases younger than one day by default (minimumReleaseAge), so a fresh Zhin release would stay at the old version (Already up to date). zhin migrate therefore lists the upgrade targets as name@version under minimumReleaseAgeExclude in pnpm-workspace.yaml (the form pnpm writes into a new scaffold; entries that already cover a release are not repeated), and after the install reads node_modules to list each package's installed version before and after. When a package did not reach its declared version it exits with an error and the cause (release age, a conflicting overrides entry, …). zhin doctor also warns, with the fix, when an installed version does not satisfy package.json.
zhin migrate only creates the directories the current scaffold creates (data/, plugins/), no longer src/plugins; SOUL.md / TOOLS.md / AGENTS.md are only added to a project that uses AI (AI enabled in the config, or @zhin.js/agent declared); files still on the old Chinese template are upgraded with a .bak.
zhin migrate never edits config files. When startup reports configVersion must be 2, or zhin doctor / zhin config check flags a v1 config, use zhin migrate instances, described below. zhin runtime migrate migrates legacy plugin capability code and is unrelated to the config version.
migrate inference: Migrate legacy AI model configuration
zhin migrate inference [--mapping <file>] --output migration.json # offline preview
zhin migrate inference --apply migration.json --maintenance --backup-confirmed [--with-credentials]
zhin migrate inference --finalize migration.json --maintenance --backup-confirmed
zhin migrate inference --sessions-only --mapping <file> --output <file> # only bind old Sessions to existing ProfilesPreviews and migrates the legacy ai.providers-style model configuration and Sessions offline; it loads no .env and does not start the Bot. --apply and --finalize both require --maintenance (the Bot is stopped) and --backup-confirmed (the configuration, database, and encryption master key are backed up). For a non-SQLite database, --state-snapshot <file> supplies the old metadata and --database-config <file> an offline database connection. Full steps and recovery boundaries: Provider interface upgrade.
Plugins
new: Create a Plugin
zhin new [plugin-name] [--standalone] [--type normal|service|adapter] [--package-name <npm-name>] [--is-official] [--skip-install]This is the single official way to create a plugin. The name must match /^[a-z][a-z0-9-]*$/; without arguments it asks interactively.
Standalone plugin (an external plugin to publish to npm):
npx @zhin.js/cli new my-plugin --standalonegenerates./my-plugin/in the current directory. No bot project is needed and nopackage.jsonis modified. The output has thepackage.json#zhinmanifest, aplugin.tsthat default-exportsdefinePlugin(), convention directories,tests/andREADME.md, and works out of the box withpnpm test/pnpm typecheck/pnpm publish.Local plugin inside a bot project: run
zhin new my-plugin(without--standalone) at the project root. It generatesplugins/<name>/and adds it to the rootpackage.jsondependencies.The package name defaults to
zhin.js-<name>. Use--package-namefor your own name or scope (for example@me/zhin-plugin-x). The@zhin.js/scope is reserved for official plugins (--is-official).zhin.jsand the capability packages (@zhin.js/command,@zhin.js/skill, and so on) are peerDependencies with a^x.y.zrange derived from the installed versions. The same versions go into devDependencies for local tests and type checks.The build output follows the published package layout (publishing guide, Chinese):
src/holds library code.pnpm buildcompiles it intolib/(.js+.d.ts), andmain,typesandexportsall point atlib/.zhin.entryis./plugin.js; the local runtime falls back toplugin.ts. When you pack (pnpm packor publish),prepackruns the build and thenscripts/build-runtime-entries.mjs, which compilesplugin.tsand the convention directories into sibling.jsfiles and rewrites imports ofsrc/tolib/.postpackremoves those generated files again.filesis a whitelist. It ships onlylib/,plugin.js, the.jsfiles in convention directories,schema.json,skills/,agents/and docs. TypeScript sources, tests and configs are not published.
The generated
pnpm test(Vitest) andpnpm typecheck(tsc --noEmit) work as is; noNODE_ENVsetting is needed.--skip-installskipspnpm installin both the plugin directory and the project root.
create: Generate a Convention File
zhin create <command|middleware|handler|tool|component> <name> [--cwd <plugin-dir>] [--force] [--event <event>]Writes one capability file into the current plugin package, so you do not hand-write the convention directory or the package.json#zhin manifest. The plugin package is the first package.json found walking up from the current directory, and it must carry a zhin.type: "plugin" manifest (plugins from zhin new and bot project roots do); --cwd sets another starting point.
| Kind | File | Default export | Feature package |
|---|---|---|---|
command | commands/<path>/index.ts | defineCommand (zhin.js/command) | @zhin.js/command |
middleware | middlewares/<name>/index.ts | defineMiddleware (zhin.js/middleware) | @zhin.js/middleware |
handler | handlers/<path>/index.ts | defineHandler (zhin.js/handler) | @zhin.js/handler |
tool | tools/<name>/index.ts | defineAgentTool (@zhin.js/tool) | @zhin.js/tool |
component | components/<name>/index.tsx | defineComponent (zhin.js/component) | @zhin.js/component |
zhin create command gh/issue # commands/gh/issue/index.ts → /gh issue
zhin create command echo/[text] # trailing [text] is a required parameter; the template declares params.text
zhin create handler message/receive # event inferred from the path: message.receive
zhin create handler welcome --event notice.receive # use --event when the path is not a built-in event
zhin create tool lookup- Every name segment must match
/^[a-z][a-z0-9-]*$/.commandandhandlernames may use/; the last segment of acommandmay be a required parameter directory[name](create[[name]]/[...name]by hand, see Commands). - Templates are the smallest default export that type-checks; edit them from there.
package.json: whenzhin.featuresdoes not list the feature package,{ package, api }is appended; when the plugin does not depend on it yet, it is added topeerDependencies(optional) anddevDependenciesfollowing thezhin newrules, with the range following the plugin's ownzhin.jsspec (workspace:stays a workspace link, otherwise the release range^x.y.z). Existing entries are never duplicated; other fields and the formatting (2-space indent, trailing newline) are kept. When dependencies change it reminds you to runpnpm install.- An existing target file is an error (exit code 1);
--forceoverwrites it. zhin add <package>is the old spelling ofzhin installand still works (with a hint to usezhin install); create a plugin withzhin new.
install: Install a Plugin
zhin install <plugin name or git URL> [--no-enable] [--dry-run]By default it installs with pnpm and writes the package reference into plugins.<instanceKey> in zhin.config.yml. Business settings use the top-level alias block; disable_plugins controls activation (change it with zhin enable / zhin disable). When the plugin schema has required fields without defaults (such as an adapter's endpoints accounts), only the dependency is installed, nothing is declared, and the missing fields are printed: fill them in the Console and declare the plugin there (gateway.plugins.declare), run zhin setup --adapters, or add plugins.<instanceKey> and the instance block to the config file yourself and restart.
| Option | Description |
|---|---|
-D, --save-dev | Write to devDependencies |
-g, --global | Install globally (doesn't enable the plugin) |
--no-enable | Install dependencies only; don't change config |
--dry-run | Print the install and config changes without writing |
While the gateway runs (zhin start), the install goes through the gateway's dependency transaction: the instance is stopped, the package manager runs, then the instance starts and must report ready; on failure package.json, the lockfile and the config are restored and the old version is started again. Progress is streamed to the terminal. Through the gateway only npm registry packages (name or name@range) can be installed; git URLs, local paths and --save-dev need zhin stop first. Without a gateway the install runs directly in the project.
uninstall: Uninstall
zhin uninstall plugin <name> [--remove-pkg]
zhin uninstall adapter <name> [--remove-pkg]name can be an npm package name or an instanceKey; --remove-pkg also removes the dependency from package.json. While the gateway runs, the uninstall goes through it (instance stopped and started, rolled back on failure). Remove the system service with zhin service uninstall.
enable / disable: Turn a Plugin Instance On or Off
zhin disable <instanceKey or package>
zhin enable <instanceKey or package>Adds an installed, declared plugin instance to disable_plugins in the config file, or removes it from that list. Dependencies and the instance's config block are left alone (use zhin uninstall plugin to remove a plugin entirely). The argument is the instance key of plugins.<instanceKey>; a package name works only when that package is declared once. With several instances, pass the instance key.
| Situation | Result |
|---|---|
Gateway running (zhin start) | Runs as a gateway transaction: back up the config → stop the instance → write disable_plugins → start the instance and wait for ready. If it fails to start, the config file is restored and the previous instance started again (the package manager never runs). Needs plugin.manage; the local socket counts as the owner |
| No gateway | Writes the config file directly; the change applies at the next zhin start |
| Already in the requested state | Exits successfully without changing the config or restarting the instance |
Undeclared, a reserved key (http, plugins, …) or the project's root plugin | Error, exit code 1 |
enable when the package isn't installed, its dependencies don't resolve or the instance config fails the plugin schema | Error, exit code 1, config unchanged. disable doesn't require the package to still be installed, so you can switch off a broken plugin first |
update: Update Dependencies
zhin update [package | package@exact-version ...]Without package names every dependency is updated (pnpm update). While the gateway runs the same transaction goes through it (stop instance → update → start instance, rolled back on failure); otherwise it runs directly.
search / info: Discover Plugins
zhin search [keyword] [-c utility|service|game|adapter|admin|ai] [-l 20] [--official]
zhin info <package>Search the plugin marketplace or show one plugin's details. Details include the author (author, or maintainers when absent); when the plugin is installed in the current project they also list its commands, Agent tools and config fields (read statically from the convention directories and schema.json; no plugin code runs).
use-cases: Find a Setup Path by Purpose
zhin use-cases
zhin use-cases reminder
zhin use-cases personal-reminder
zhin use-cases group-welcome
zhin use-cases keyword-replyFind the required plugin, installation preview, steps and current limitations for reminders, welcome messages and keyword replies. This command only prints a guide: it does not install plugins, change configuration, start the bot or send messages. The existing installation, configuration and chat interfaces still own those operations.
The Administrator tasks tab of Console Operations → Scheduled tasks uses the same catalog. Its first creation flow manages administrator reminders. Welcome messages and keyword replies currently link to existing setup paths; Console does not impersonate a chat member.
preview: Preview Components and Pages
zhin preview [dir] [-p 4176] [-H 127.0.0.1] [--open]
zhin preview --export preview.html # export a single-file static HTML snapshot and exit
zhin preview --json # print the scan and render result and exitStarts a local preview workbench: it scans components/* and pages/* in the project or plugin directory (the current directory by default), hot-reloads, and lets you edit Props and adjust styles live. It listens on 127.0.0.1:4176 by default; --open opens the browser after startup.
build: Build a Plugin
zhin build [plugin] [--clean] [--no-production]plugin is a path relative to plugins/ (e.g. adapters/icqq); when omitted it builds the current package from inside a plugin folder, or every plugins/* from the workspace root. src/ is compiled with tsc and client/ bundled with esbuild; --clean clears lib/ and dist/ first; --production (on by default) minifies client output.
plugin check: Check a Plugin Before Publishing
zhin plugin check [plugin-dir] [--no-pack] [--json] [--strict]Checks whether the plugin in a directory (the current one by default) will publish correctly. Run it once before publishing: npx @zhin.js/cli plugin check.
Manifest and layout: checks the
package.jsonnameandzhinmanifest, and thatzhin.entrypoints at JavaScript with a matching source file. It also checks that thefileswhitelist covers the entry, the convention directories (the.jsbuilt fromcommands/,tools/and so on, plusskills/andagents/) andschema.json, and thatprepack/postpackgenerate and clean the runtime entries. A missing README or missing tests only produce warnings.zhin manifest and dependencies:
- the
zhinmanifest (protocol,type,entry,runtime,features,plugins) is validated with the same parser the Runtime uses at load time; zhin.enginemust be a semver range the current Runtime engine satisfies, andfeatures[].apimust be a valid range;- packages referenced by
features/pluginsmust be listed independenciesorpeerDependencies; peerDependenciesmust declarezhin.jswith real semver ranges:latest,*andfile:/link:are errors, and a locally installed version outside the range is a warning;- Zhin stack packages in
dependenciesproduce a warning.
- the
Published package: runs the plugin's real
npm pack(itsprepackbuild included) and unpacks the tarball in a temporary directory. It then checks that:- the entry and
main/exportsare packed; - relative imports in packed JavaScript resolve;
- no tests or build configuration slipped in;
- every
.tsin a convention directory has a packed.js; - the packed
zhin.entrycan be imported: it is loaded in a child process using the plugin's ownnode_modules(skipped with a warning when dependencies are not installed).
These are the same rules as the repository's
pnpm check:pack-imports.--no-packskips this step.- the entry and
Exits with code 1 on errors;
--strictalso fails on warnings;--jsonprints a structured result for CI.
pub: Publish a Plugin
zhin pub [plugin-name|plugin-dir] [--tag latest] [--access public] [--registry <url>] [--dry-run] [--skip-build] [--skip-check] [--yes]- Inside a standalone plugin from
zhin new --standalone, run it without arguments to publish the current directory:npx @zhin.js/cli pub --dry-runto preview,npx @zhin.js/cli pubto publish. (npx @zhin.js/cliis reliable with every version;npx zhinonly means the same CLI fromzhin@4on, see the tip at the top of this page.) - The argument can also be any plugin directory (its
package.jsonhas azhinfield), e.g.zhin pub ../my-plugin. - In a bot project root, the argument is a path relative to
plugins/; without it a single plugin is picked automatically, several are offered interactively. --dry-rungoes through the flow without publishing. Non-interactive shells (CI) need--yesto skip the publish confirmation. Packages withprivate: trueare rejected, and azhin.entrypointing at.tsgets a hint to publish the built.js.- Before publishing it runs
zhin plugin checkautomatically (npm packincluded). Errors stop the publish; warnings alone let it continue. Use--skip-checkto skip it if you really must. - Full release flow: Complete plugin delivery.
packages: Plugin Status and Extension Packages
Manage skill / extension packages (ADR 0010):
zhin packages install <pkg|npm:skill-pack|git:url> [--local]
zhin packages remove <name> [--local]
zhin packages list [--local]
zhin packages update [source] [--local]--local (-l) installs into the project's .zhin/packages/ instead of globally. Without an npm: / git: prefix, it uses the same install flow as zhin install <package> and Console.
Check whether a plugin is really live: installing doesn't mean it's loaded, its platform is online, or its features are verified. Use these to inspect status and run verification:
zhin packages status <instanceKey>
zhin packages status <instanceKey> --live http://127.0.0.1:8068
zhin packages verification-plan <instanceKey> --live http://127.0.0.1:8068
zhin packages apply <instanceKey> --live http://127.0.0.1:8068 --project-revision <revision>
zhin packages verify <instanceKey> <check-id> --live http://127.0.0.1:8068 \
--generation <generation> --project-revision <revision> --check-revision <checkRevision>States returned by status:
| State | Meaning |
|---|---|
installed | The package resolves |
declared | An instance is declared |
missing-config | Required config or environment variables are missing, or values don't match the schema |
pending-restart | The running version hasn't applied it yet |
enabled | Live, with config and package version consistent |
degraded | Declaration and actual installation disagree, or a dependency check failed |
readiness records readiness evidence separately (unknown when an adapter is present but there's no platform health evidence); verification only says whether business checks exist — a successful start is not a business pass.
Live requests, verification flow and concurrency rules
- Token: live requests read the Console token from
ZHIN_CONSOLE_TOKEN, or the variable named by--token-env <name>.verification-plan,applyandverifyneed full permissions. Never put tokens in URLs or command arguments. - HTTPS: remote requests carrying a token must use HTTPS; plain HTTP is allowed only for
localhost,127.0.0.0/8and::1. - Offline mode: offline
statusprovides only file and package-resolution evidence; an offline verification plan has no current version or runnable checks, and never starts the Runtime. - Verification steps: review the scope and side effects in
verification-planfirst, then pass the returnedgeneration,projectRevisionandcheckRevisiontoverify. Changes to config, version or check definitions reject old plans. Plugins without checks returnnot-configured; passing local checks doesn't mean platform delivery is verified.applyonly requests application — confirm with a livestatusafterwards. - Schema: a loaded config schema applies only to the same package version; after an update the on-disk
schema.jsonis read. If the new version declares config only in TypeScript with no readable schema,validation.schemaAvailableisfalseand only environment references are checked; full validation needs a real Runtime pre-check. - Production dependencies: auto-enabled plugins must be production dependencies;
--save-devmust be combined with--no-enable, and this is checked before installing. - Concurrency: project changes use a file lock and revision checks. Concurrent operations or config drift fail explicitly; if install steps already completed, partial-completion evidence is returned and user changes are never overwritten. If the lock's owning process has exited, the error shows the lock path — delete it after confirming no installer is running, then retry.
Working with a running bot
These commands work on a running bot, so start it with zhin start in the project directory first. By default they only use this project's gateway over its local control socket (.zhin/gateway.sock), as the local owner, without a token; when this project's gateway is not running they fail with "本项目的网关未运行" and never connect to the local http.port (another project's gateway may be listening there). To reach another address pass --host <url>: requests then carry the Console API token from ZHIN_CONSOLE_TOKEN (issued in your Console account settings, never more than your own permissions; after Console setup http.token is always refused), and a loopback address is first checked against the project id its /pub/health reports — a different project is refused, and so is a target that reports no project id (an older gateway or a --standalone instance) unless you pass --allow-unverified-host.
send: Send a Message to a Running Bot
zhin send <scene_id> [content...] [-s private|group|channel] [-a <adapter>] [-e <endpoint>] [--host <url>]scene_id is a user ID for DMs or a group number for groups; without content it reads one line from stdin. -a may be omitted only when the instance has a single adapter; -e picks an account ID, defaulting to the adapter's first online account.
A failed send shows the reason (for example no open Sandbox session, an account that is not connected, or a platform refusal). An internal instance error shows only "Internal error"; with --debug the CLI shows its log correlation id and finds that line in the last 1000 lines of zhin logs.
watch: Monitor a Running Instance
zhin watch [-i 3] [--once] [--json] [--no-clear] [--host <url>]Refreshes a status panel in the terminal (stats, accounts, assistant jobs). --once fetches once and exits; --json suits scripts.
schedule: Persistent Scheduled Tasks
Have the AI run a prompt at given times. The command talks to the running instance through the Host API and takes effect immediately; the bot must be started with zhin start and have assistant.enabled.
zhin schedule list [--host <url>]
zhin schedule add "0 0 9 * * *" --prompt "Good morning, summarize today's schedule" [--kind solar|lunar|workday|freeDay]
zhin schedule add --every 30m --prompt "Check RSS"
zhin schedule add --at 2026-08-01T09:00:00Z --prompt "One-off reminder"
zhin schedule remove <id>
zhin schedule pause <id>
zhin schedule resume <id>add option | Description |
|---|---|
--prompt | Required: the prompt sent to the AI when due |
-l, --label | Job name |
--kind | solar (default, Gregorian) / lunar / workday / freeDay |
--notify-channel | im / silent (default; im when --target is given) / log |
--target | IM delivery target <adapter>:<endpoint>:<kind>:<id> (kind: private / group / channel), e.g. sandbox:sandbox-bot:private:u1; defaults to assistant.defaults.notify or the profile's defaults.notify. Without a usable target, or when its account does not exist, the instance refuses the job and says why. An explicit target also needs endpoint.operate on that account (as zhin send / /message/send does; the Console cron:add RPC too), otherwise 403 schedule_target_forbidden |
cron expression, --at, --every | Pick one: a six-field cron (sec min hour day month weekday), a one-off ISO8601 time, or an interval (e.g. 30m, 1h, 1d) |
zhin schedule list shows each job's resolved delivery target (account name + conversation) and the next run of enabled jobs; a target whose account no longer exists, or a missing target, is flagged "⚠ 无效" (invalid), and an enabled job without a next run is flagged "⚠ 无(不会触发)" (it also appears under the Console's attention list).
agent: Agent Diagnostics
Requires the AI stack (@zhin.js/agent + zod + ai).
zhin agent info [--json]| Command | Purpose |
|---|---|
info | List discovered Agent capabilities and named Agents (Tools, Skills, Hooks…) |
trace: Inspect Agent Turns
Requires the AI stack. After the bot finishes each AI conversation turn, it appends that turn's trace to data/agent-traces/<UTC date>.jsonl (one file per day). You no longer need to dig through bot.db and the journal to diagnose a problem:
zhin trace # same as zhin trace last: print the most recent turn
zhin trace <turnId> # a specific turn; an id prefix is enough
zhin trace --list # list recent turns (50 by default)
zhin trace --sessions # list sessions that have traces
zhin trace --session qq:bot:private:12345 --since 2h # one session's turns in the last 2 hours
zhin trace --list --since 2026-10-14 --until 2026-10-15 -n 200
zhin trace last --json
zhin trace --engagements --list # list recent multi-Agent collaborations (Engagements)
zhin trace --engagements # collaboration graph of the latest collaboration
zhin trace <engagementId> # a specific collaboration; an id prefix is enough| Option | Purpose |
|---|---|
-l, --list | One line per turn: time, end status, turn id, session, tool count, tokens |
--sessions | One line per session: turn count, last time and status, total tokens |
--engagements | Read multi-Agent collaboration records instead (the engagement_events database table; requires SQLite, MySQL, or PostgreSQL): with --list, list recent collaborations; otherwise show the participants, mail hops, Blackboard, and budget of the latest or the given collaboration. See Multi-Agent Collaboration and Venues |
-s, --session <key> | Only this session (sessionKey); output switches to the list format |
--since <time> / --until <time> | Time range (--until exclusive): 30m, 2h, 7d (how long ago), an ISO date/time, or epoch ms |
-n, --limit <n> | Show at most the latest n entries, default 50 |
--json | Raw JSON output (snake_case fields) |
--no-timeline | Do not print the step-by-step timeline |
--cwd <dir> | Project root, default current directory |
--dir <dir> | Read a trace directory directly (a failed eval report prints this argument) |
A trace contains: the initiator and role, the session, tokens for each model call, each tool call (argument summary, duration, result size, error or refusing policy), budget extensions and automatic continuation, nested sub-agent turns, per-segment context cost, and the end reason. The output starts by calling out the first failing step.
Triggering message. Since #1250 a trace also records the message that started the turn: a text excerpt (whitespace collapsed, credentials redacted, cut to 120 characters), the sender id and sender name, the conversation (adapter, Endpoint, conversation kind and id), the message id, and when it was received, how long it waited and queued, and how many messages were merged. A trace file therefore holds fragments of what users said plus their identifiers: anyone who can read data/agent-traces, anyone who can run zhin trace, and Console accounts or API tokens holding agent.view (/api/agent/traces, /api/agent/trace-archive/*) can see them. Shorten ZHIN_TRACE_RETENTION_DAYS if you do not want to keep them.
Model calls of sub-agents (collaborating agents reached by ask / delegate) are recorded in their nested turns. The parent's "model calls / tokens" line shows the total with a split ("self N, sub-agents M"), and the trace field subagent_usage holds the sub-agent part; ai.usage logs and usage statistics count every call once. The timeline, requests and model_calls record one provider for one request (the service actually resolved), shown as the service name (service · model), or the model alone when the service configuration cannot be read.
Retention and redaction. Traces are kept for 7 days with a 64 MB total cap by default: cleanup runs at startup and every hour after that, first deleting expired date files and then deleting from the oldest day until the total is under the cap (today's file is never deleted for size). Adjust with the ZHIN_TRACE_RETENTION_DAYS and ZHIN_TRACE_MAX_MB environment variables. A trace stores summaries only: tool results record only their length, not their content; tokens, passwords, Authorization, and similar fields in arguments and error messages are replaced with [redacted], and before writing to disk inline credentials (sk-…, Bearer …, and so on) and secret values from environment variables are scanned once more. The legacy data/agent-traces/recent.json is still read.
Console page. The "Agent Trace" page in Console (/console/agent-trace) reads the same data: session list → turn list → turn detail (model/tool call waterfall, timeline, and raw record), with time filtering, and ?turn=<turnId> opens a turn directly. Add one page to your project to enable it:
// pages/agent-trace/index.tsx
import { TraceArchivePage } from '@zhin.js/client';
import { definePage } from '@zhin.js/console-contract';
export const meta = definePage({ title: 'Agent Trace', icon: 'Activity', order: 31 });
export default TraceArchivePage;Behind the page are the read-only endpoints GET /api/agent/trace-archive/sessions, /turns?sessionKey=&since=&until=&limit=, and /turn?turnId=. They require agent.view; an account or API token without it gets 403.
Eval integration. A failing case in pnpm eval:agent writes its turn trace to ZHIN_EVAL_TRACE_DIR (by default zhin-eval-traces under the system temp directory), and the report prints one ↪ zhin trace <turnId> --dir … line per turn that you can copy and run.
usage report: Real Usage Ledger
Read-only. Reads data/agent-traces/*.jsonl and summarises real model usage per local day, so you can tell whether context optimisations actually saved tokens (ADR 0043 §6). Files are streamed line by line, nothing is written, and malformed lines are counted and reported at the end.
zhin usage report # today
zhin usage report --days 7 # last 7 days, including today
zhin usage report --since 2026-10-28 --until 2026-10-29
zhin usage report --session 'icqq:*:group:*' --json| Option | Purpose |
|---|---|
--days <n> | Last n days, default 1 |
--since <time> / --until <time> | Time range (--until exclusive): 2h, 7d, YYYY-MM-DD (local midnight), ISO time or epoch ms; --since overrides --days |
-s, --session <pattern> | Session key substring, or a glob with * |
--json | JSON output |
--cwd <dir> / --dir <dir> | Project root / trace directory |
--tz <zone> | IANA zone for day boundaries; defaults to the instance zone (TZ, else the system zone) |
Each day also lists model usage merged by "service · model": calls recorded under a service's services: and accounts: ids become one row, with the service name read from the model configuration in the project database (the model alone when it cannot be read). The session column shows the conversation name recorded in the trace (e.g. qq · 群 Test) with a short session key. Input tokens are the provider-reported input of every model call (iterations and sub-agents included), a different basis from the first-request segment estimate below.
Per day: turns, model calls, calls per turn and per-call input tokens (p50 / p90 / max), cache-read share (cache_read_tokens / prompt_tokens), history / fixed / user prompt shares, turns that hit the iteration limit, and the top 5 sessions by input tokens. Segment shares come from each turn's first-request estimate (context_budget); fixed = system + tool_schemas + output_schema + tool_index + skill_catalog + agent_catalog + memory + bootstrap. "Hit the iteration limit" means a last_iteration_nudge was recorded or the turn ended budget_exceeded.
Prompt-cache hint. Every model call records a request-prefix hash (system prompt + tool definitions, trace field prefix_hash). When a service's last 20 or more consecutive calls, across at least 3 sessions, had no cache read (cache_read_tokens 0 or not reported) while more than half of the adjacent same-session requests shared their prefix, the report ends with "this service may not support or pass through prompt caching" and shows the same-prefix share as evidence. zhin doctor runs the same check over the last 7 days of traces, and the Console usage statistics carry it too (promptCacheHints of the usage-stats RPC).
Exit Codes
| Code | Meaning |
|---|---|
0 | Normal exit |
3 | zhin status: the gateway is not running |
130 | Interrupted by a signal |
| other non-zero | The gateway failed to start (port in use, a gateway already runs for the project, unsupported Node version, unreadable config), or a management command failed (socket owned by another user, gateway not answering, transaction rolled back). An instance crash does not end the gateway: the gateway restarts it, and restarts are requested over the control channel, not through exit codes |
zhin config and zhin doctor --live have their own exit codes (above). Process model, exit codes per start mode and hot-reload behavior: zhin start.
Migrating app instance declarations
An older project must stop the Bot first and then migrate to configVersion: 2. The command only previews by default and neither installs dependencies nor starts plugins:
zhin migrate instances
zhin migrate instances --apply
zhin migrate instances --recover continue
zhin migrate instances --recover rollbackThe preview lists changes by key name (the added configVersion, the plugins mapping, moved business blocks, disable_plugins, and the emptied package.json#zhin.plugins) without printing any configuration value, so it can be pasted into an issue; --json prints a machine-readable summary. Before writing, --apply copies the original files to .zhin/backups/config-v2-<timestamp>/ (with a manifest.json) and prints the path. Prefer zhin migrate instances --recover rollback to restore; you can also copy the files back by hand following the manifest. Nesting, optional plugins, and alias collisions need manual handling. See the migration guide.