feat: publish BJD CMS Content 0.7.1
This commit is contained in:
@@ -1,159 +0,0 @@
|
||||
# Agent Install Or Upgrade Contract
|
||||
|
||||
This is the normative, host-neutral procedure for both first installation and
|
||||
upgrade of the public BJD CMS Content distribution. Complete it in order. Do
|
||||
not weaken a failed check.
|
||||
|
||||
## 1. Establish the local target
|
||||
|
||||
1. Detect Windows, macOS, Linux, or WSL.
|
||||
2. Identify the current Agent host and its supported local stdio MCP and skill
|
||||
registration mechanisms. Do not assume Codex.
|
||||
3. Require Python 3.10 or newer. No third-party Python dependency is needed.
|
||||
4. Preserve all unrelated existing Agent/MCP configuration. Before editing a
|
||||
shared configuration file, use the host's supported update mechanism or
|
||||
create a recoverable backup.
|
||||
5. Detect any existing bjd-cms-content installation and registration. Record
|
||||
its installation prefix, version, exact plugin and skill paths, current
|
||||
pointer, and host registration as the rollback target. Do not read or alter
|
||||
the private helper configuration or CMS Token.
|
||||
|
||||
Stop and report if the host cannot run a local stdio MCP process or if its
|
||||
configuration location cannot be determined safely.
|
||||
|
||||
## 2. Select and verify an immutable release
|
||||
|
||||
Repository:
|
||||
|
||||
```text
|
||||
https://git.justconn.top/gitea-admin/bjd-cms-content
|
||||
```
|
||||
|
||||
1. Resolve the latest stable, non-draft, non-prerelease Gitea Release.
|
||||
2. Download `SHA256SUMS` and exactly one platform-appropriate archive:
|
||||
`.tar.gz` for POSIX systems or `.zip` for Windows.
|
||||
3. Verify the archive byte-for-byte against its entry in `SHA256SUMS` before
|
||||
extraction.
|
||||
4. Extract into a newly created temporary directory. Reject archive traversal,
|
||||
absolute members, symlinks, or a checksum mismatch.
|
||||
5. Read `RELEASE.json` and require `transport=stdio`, `skill_required=true`,
|
||||
`python_requires` compatible with the installed Python, and positive
|
||||
server/client versions and tool count.
|
||||
|
||||
Never install from the floating `main` branch. Never continue after a missing
|
||||
asset, ambiguous latest release, or failed checksum.
|
||||
|
||||
## 3. Install or upgrade side by side
|
||||
|
||||
From the extracted release root, run the bundled installer:
|
||||
|
||||
```text
|
||||
POSIX: sh install/install.sh
|
||||
Windows: powershell -ExecutionPolicy RemoteSigned -File install/install.ps1
|
||||
```
|
||||
|
||||
Use the default per-user prefix for a first installation. An upgrade must reuse
|
||||
the existing absolute prefix. The installer creates a new versioned directory
|
||||
and atomically switches its current pointer; it must not overwrite or delete an
|
||||
installed version. If the selected exact version already exists, verify and
|
||||
reuse it instead of rerunning the installer.
|
||||
|
||||
Record the exact plugin and skill paths printed by the installer.
|
||||
|
||||
## 4. Verify the new pair without credentials
|
||||
|
||||
Before changing the Agent host registration, run the `client.py` adjacent to
|
||||
the newly installed `server.py`:
|
||||
|
||||
```text
|
||||
python3 <absolute-plugin-path>/scripts/client.py native-smoke
|
||||
```
|
||||
|
||||
Require all of the following:
|
||||
|
||||
- the reported server version equals `RELEASE.json.server_version`;
|
||||
- the reported client version equals `RELEASE.json.client_version`;
|
||||
- the reported tool count equals `RELEASE.json.tool_count`;
|
||||
- `bundled_pair_verified=true`;
|
||||
- initialization, ping, and tool listing succeed.
|
||||
|
||||
Stop on a mismatch. Do not substitute a global or cached client/server. During
|
||||
an upgrade, restore the recorded previous current pointer before stopping.
|
||||
|
||||
## 5. Register the host-neutral runtime
|
||||
|
||||
Read `install/generic-mcp.md` and the matching host adapter when present. Add or
|
||||
update only the `bjd-cms-content` registration with:
|
||||
|
||||
```text
|
||||
transport: stdio
|
||||
command: the platform's Python 3 launcher
|
||||
args: <absolute-plugin-path>/scripts/server.py
|
||||
cwd: <absolute-plugin-path>
|
||||
env.BJD_CMS_SKILL_ROOT: only for a split/nonstandard installation
|
||||
```
|
||||
|
||||
Do not put a bearer Token in MCP configuration. If the host supports local
|
||||
skills, register the complete matching skill directory, not only `SKILL.md`.
|
||||
If it does not, retain the directory and ensure the Agent reads its `README.md`
|
||||
and `SKILL.md` before CMS work. Preserve unrelated host configuration.
|
||||
|
||||
Restart or reload the Agent host using its supported lifecycle, then confirm
|
||||
that the host reports the same server/client versions and tool count. If that
|
||||
check fails during an upgrade, restore the previous registration and current
|
||||
pointer, restart the host, and stop.
|
||||
|
||||
## 6. Configure credentials without exposing them
|
||||
|
||||
From the complete installed skill directory, first run the redacted offline
|
||||
doctor. On an upgrade, reuse an existing valid private helper configuration
|
||||
unchanged and do not run `configure` merely because the software changed. On a
|
||||
first installation or when the helper reports that configuration is genuinely
|
||||
missing, use the second sequence:
|
||||
|
||||
```text
|
||||
python3 scripts/cms_api.py doctor
|
||||
```
|
||||
|
||||
Only when configuration is missing:
|
||||
|
||||
```text
|
||||
python3 scripts/cms_api.py configure
|
||||
python3 scripts/cms_api.py doctor
|
||||
```
|
||||
|
||||
`doctor` is redacted and makes no network request. Do not read, print, or copy
|
||||
the configuration file. If `token_configured=false`, report the exact path
|
||||
printed by the helper and pause. Tell the user to fill only
|
||||
`AI_CONTENT_API_TOKEN=` locally with the independently issued credential.
|
||||
Never ask for the Token in chat and never put it in a command, prompt, source
|
||||
repository, MCP configuration, or log.
|
||||
|
||||
After a new explicit user message confirms the Token was saved, run only the
|
||||
read-only connection check:
|
||||
|
||||
```text
|
||||
python3 scripts/cms_api.py GET /api/ai/content/schema/news
|
||||
```
|
||||
|
||||
Require a successful top-level envelope with no errors. A `401`, `403`, or
|
||||
`429` is an authoritative server boundary; stop and report it. Do not try a
|
||||
different credential or site.
|
||||
|
||||
## 7. Completion report
|
||||
|
||||
Report only:
|
||||
|
||||
- whether this was a first install, upgrade, or already-current verification;
|
||||
- previous and installed release versions, immutable release URL, and retained
|
||||
rollback path;
|
||||
- plugin and skill paths;
|
||||
- Agent host registration result;
|
||||
- redacted `native-smoke` identity and tool count;
|
||||
- whether the private Token is configured, without reading or displaying it;
|
||||
- read-only schema-check result, if the user had already saved a Token.
|
||||
|
||||
Do not exercise production write tools as an installation test. The first
|
||||
production mutation must be a reviewed real business draft following the
|
||||
skill's schema, validation, version, confirmation, verification, and audit
|
||||
rules.
|
||||
+26
-36
@@ -2,57 +2,47 @@
|
||||
"files": [
|
||||
{
|
||||
"path": "RELEASE-NOTES.md",
|
||||
"sha256": "f3becae1d5103d215a937437a17877216a4e1e3870ee5de961da76c77b831adf",
|
||||
"sha256": "e3b7d313155af620f5fe52ebd49de3e89ae9dcfb3a22ec5ec102b28032d26b71",
|
||||
"size": 225
|
||||
},
|
||||
{
|
||||
"path": "RELEASE.json",
|
||||
"sha256": "63c594c91d85f8ac4410d68d7f6ddc22eaf5196f6b22a5654a311fd8de973fc0",
|
||||
"sha256": "e4331711091ed3e08774bf736be194ac7f050e3b78e73655ef48e9f7e5610a42",
|
||||
"size": 411
|
||||
},
|
||||
{
|
||||
"path": "VERSION",
|
||||
"sha256": "967d9afb101346667166f2e76e81910bc190488d7d41d50ca0072e9d92f00e32",
|
||||
"sha256": "902311c7bb38fa1658ff403def6158ee7378fe9d6e2e284fa1c38735c31bd30b",
|
||||
"size": 6
|
||||
},
|
||||
{
|
||||
"path": "install/agent-install-prompt.md",
|
||||
"sha256": "34c022ee32d899c9d85d72ab8f0b702e9ddce37f5bb4313adfafaa94de77324f",
|
||||
"size": 2933
|
||||
},
|
||||
{
|
||||
"path": "install/claude-desktop.md",
|
||||
"sha256": "e6693749f7b67252e57951bcf1ce3e5ec8e1895c62260ff5b25ddd7bb848c019",
|
||||
"size": 1058
|
||||
},
|
||||
{
|
||||
"path": "install/codex.md",
|
||||
"sha256": "b32ca5ba7c6da708faacf6a82b50bfa7db22b53658a2c00485ad7a796f8f505f",
|
||||
"size": 1099
|
||||
"sha256": "0310b6bd3e8b5829e5a2ca2ff5a794abe20edb9a2c9f29dfe0090a1a4daf63cf",
|
||||
"size": 2433
|
||||
},
|
||||
{
|
||||
"path": "install/generic-mcp.md",
|
||||
"sha256": "6b0dc5f93966a98566fbdbb294fe38470f86d58f83cfdb58de5bf4e926f1f3f8",
|
||||
"size": 3461
|
||||
"sha256": "4fd945eae25d8e8ab7cb19699a5510f5b984e3789eb275c49863c87280623357",
|
||||
"size": 2092
|
||||
},
|
||||
{
|
||||
"path": "install/install.ps1",
|
||||
"sha256": "1c319a90db477cf209f25164eb39b36c43964b42e3a67db0daa9742cf76e4b85",
|
||||
"size": 2880
|
||||
"sha256": "c1927c22264f8673fb8dcc7406198a9a29774249031760940b82cbfc8f0fe7c3",
|
||||
"size": 909
|
||||
},
|
||||
{
|
||||
"path": "install/install.py",
|
||||
"sha256": "979f5502935964c05c83708dd46c1729f42519f2009bfe1553dc211a45d44574",
|
||||
"size": 8296
|
||||
},
|
||||
{
|
||||
"path": "install/install.sh",
|
||||
"sha256": "256dcf1cee20f2514fbcfe3e53a457e387f55575d9da4c3254aa2d70a44f1107",
|
||||
"size": 2810
|
||||
},
|
||||
{
|
||||
"path": "install/opencode.md",
|
||||
"sha256": "831a98ba45176439c2980dc64e2ae1976af2ffd02516827b70416a12fc6d240b",
|
||||
"size": 814
|
||||
"sha256": "afab9750e496351b28e066b88e39ccc4890b03f3bb448a27cd51b31883c90bd6",
|
||||
"size": 673
|
||||
},
|
||||
{
|
||||
"path": "plugin/bjd-cms-content/.codex-plugin/plugin.json",
|
||||
"sha256": "114443fa033cdee0e04e9856e48c196352781dd9bb1c6f9f07738288c312afdb",
|
||||
"sha256": "f2c098d02812eb7c767bda6c0f41e9ff2bed02fa6201a71e729b475350ca1776",
|
||||
"size": 1709
|
||||
},
|
||||
{
|
||||
@@ -62,27 +52,27 @@
|
||||
},
|
||||
{
|
||||
"path": "plugin/bjd-cms-content/README.md",
|
||||
"sha256": "68b91b18f898adb8570e4a533e5789b215ec88516aa6db70f458a8c0db7a7176",
|
||||
"size": 19312
|
||||
"sha256": "0eaed25e9b64954d35a02625c70e0a2f732da96cfc20068b842940bba7fb671e",
|
||||
"size": 19322
|
||||
},
|
||||
{
|
||||
"path": "plugin/bjd-cms-content/scripts/client.py",
|
||||
"sha256": "c3870c9a3557638d080d5be340436c210dd3d48750bd3b154ee861c95fdd11a8",
|
||||
"sha256": "cade4c926f72522baf837ad8d99c9ae08793297410e340343b9cfc1c6e108cae",
|
||||
"size": 54254
|
||||
},
|
||||
{
|
||||
"path": "plugin/bjd-cms-content/scripts/server.py",
|
||||
"sha256": "4a8cb1689d3f136fd2c14480afe36c71e36cdb563104ec72344079f84582df15",
|
||||
"sha256": "274355235788178840f2137c170c577ec805426dcf5a10c68cf57ad5cab502f4",
|
||||
"size": 215521
|
||||
},
|
||||
{
|
||||
"path": "skill/bjd-cms-content/README.md",
|
||||
"sha256": "b473276d5bb86b69caa2a1954395782b9c48015ca1367c9904b2d10ce3b477f4",
|
||||
"sha256": "328abe94dc0f1a5854884fa2103d02d785cbce1f0347fbb9771b0bc72db695ec",
|
||||
"size": 6923
|
||||
},
|
||||
{
|
||||
"path": "skill/bjd-cms-content/SKILL.md",
|
||||
"sha256": "bf279d40f2e98b0bab87ac2ae42f50254d5544c4089e704a5565804bbb5dac83",
|
||||
"sha256": "a38e12a9c328cf48001a450c4396a8584b8054709ff799eaa85f4a36eb086ad2",
|
||||
"size": 10629
|
||||
},
|
||||
{
|
||||
@@ -92,7 +82,7 @@
|
||||
},
|
||||
{
|
||||
"path": "skill/bjd-cms-content/references/api.md",
|
||||
"sha256": "a7838cff1c9296cd554cb20b79dae30d6ed5eeed702adeb13244abd06a61f7c8",
|
||||
"sha256": "4c8be2a116c183f397f130e26a8707336294ec57557e7a93ad6370616ab1a2f7",
|
||||
"size": 42769
|
||||
},
|
||||
{
|
||||
@@ -122,8 +112,8 @@
|
||||
},
|
||||
{
|
||||
"path": "skill/bjd-cms-content/references/mcp-fallback.md",
|
||||
"sha256": "e572ccb9c61df454c00d8800152702b2765ac8e2b1faeae4a516a704c6c987af",
|
||||
"size": 2999
|
||||
"sha256": "01ae5e86141baf344451de2145d9aee822f732cd19e71b183706e14c45a10ce2",
|
||||
"size": 2995
|
||||
},
|
||||
{
|
||||
"path": "skill/bjd-cms-content/references/media.md",
|
||||
|
||||
@@ -1,107 +1,74 @@
|
||||
# BJD CMS Content
|
||||
|
||||
Public, host-neutral distribution of the BJD CMS Content skill and its local
|
||||
stdio MCP server. It works with MCP-capable Agent hosts such as Codex, Claude
|
||||
Desktop, OpenCode, and other standard stdio MCP clients.
|
||||
BJD CMS Content 的公共、宿主无关发行仓库。发行包同时包含标准 Python
|
||||
stdio MCP 服务与完整配套 Skill,适用于任何能够注册本地 stdio MCP 的宿主。
|
||||
|
||||
Current stable release: `0.7.0`
|
||||
当前稳定版:`0.7.1`
|
||||
|
||||
- MCP server: `0.7.0`
|
||||
- Bundled fallback client: `1.9.0`
|
||||
- Python: `3.10+`
|
||||
- Tools: `23`
|
||||
- Runtime dependency: Python standard library only
|
||||
- MCP server:`0.7.1`
|
||||
- fallback client:`1.9.0`
|
||||
- 工具数:`23`
|
||||
- Python:`3.10+`
|
||||
- 第三方运行依赖:无
|
||||
|
||||
Download immutable release assets from the
|
||||
[Releases](https://git.justconn.top/gitea-admin/bjd-cms-content/releases)
|
||||
page. Do not install the floating `main` branch.
|
||||
请只从 [Releases](https://git.justconn.top/gitea-admin/bjd-cms-content/releases)
|
||||
下载不可变发行资产,不要从浮动的 `main` 分支安装。
|
||||
|
||||
## One prompt to install or update
|
||||
## 安装、更新共用一个 Prompt
|
||||
|
||||
Copy the complete prompt below into your Agent for either a first installation
|
||||
or any later upgrade. The prompt itself does not change between releases:
|
||||
下面这段 Prompt 无需填写版本或校验和,以后更新时仍然原样复用:
|
||||
|
||||
```text
|
||||
请从公共仓库 https://git.justconn.top/gitea-admin/bjd-cms-content 安装或更新 BJD CMS Content。先完整阅读仓库根目录的 AGENT_INSTALL.md,并严格按它执行。检测当前操作系统、Python 版本、Agent 宿主,以及是否已经安装或注册 bjd-cms-content;如果已安装,先记录原安装前缀、版本、plugin/skill 路径、current 指针和宿主注册,保留旧版本作为回滚目标,不读取或改写私有 CMS 凭证。只选择最新稳定、非草稿、非预发布的 Gitea Release;解析后将本次版本、标签、Release URL 和 SHA256SUMS 固定到本次操作,不从浮动 main 安装,也不在执行途中切换版本。下载正确平台的单个归档和 SHA256SUMS,校验 SHA-256 后解压到新临时目录;首次安装使用默认用户前缀,升级必须复用原绝对前缀并通过随包 POSIX 或 Windows 安装器并排安装,绝不覆盖或删除旧版本;如果目标版本已经存在,则校验并复用,不重复安装。先用新版本相邻的 client.py 运行 native-smoke,确认结果与 RELEASE.json 的 server/client 版本和 tool_count 完全一致且 bundled_pair_verified=true;失败时恢复旧 current 指针和宿主注册并停止。验证通过后,只新增或更新 bjd-cms-content 的本地 stdio MCP 注册和完整 skill 注册,保留所有无关宿主配置,按宿主要求重载或重启并再次核对运行身份。升级时原样复用现有私有 helper 配置,不要仅因升级而运行 configure;首次安装或确实缺少配置时才运行 skill 自带 helper 的 configure,再运行不联网且脱敏的 doctor。若 AI_CONTENT_API_TOKEN 尚未配置,只告诉我私有配置文件的准确路径并暂停,让我在本机填写;绝不要要求我把 Token 发到聊天、命令、MCP 配置或日志里。已有 Token 或等我确认已保存后,只执行 GET /api/ai/content/schema/news 作为连接验收,不创建测试内容、不调用写工具。最后报告本次是首次安装、升级还是已为最新版校验,并报告旧/新版本、绝对路径、宿主配置变更、校验和、冒烟结果及保留的回滚路径。任何下载、校验、版本、宿主配置或验收不一致都停止并报告,不绕过检查。
|
||||
请从 https://git.justconn.top/gitea-admin/bjd-cms-content 安装或更新 BJD CMS Content。
|
||||
|
||||
1. 解析最新的非草稿、非预发布稳定 Release,并在本次任务内固定其 tag 与资产 URL。只下载 SHA256SUMS 和一个适合当前操作系统的发行归档,按 SHA256SUMS 校验一次;不一致立即停止。不得从分支或仓库源码归档安装。
|
||||
2. 把归档解压到临时目录,在 POSIX 上运行随包 install/install.sh,在 Windows 上运行 install/install.ps1。若已经注册过 BJD CMS,复用原来的绝对安装前缀;能取得当前宿主所用 Python 3 可执行文件时把它传给安装脚本,否则让脚本自动探测。安装脚本输出的单行 JSON 是唯一验收依据:它已完成版本检查、不可变安装或复用、失败不切换的 current 指针更新及 native MCP smoke;不要手工重复这些检查。
|
||||
3. 按 JSON 返回值注册本地 stdio MCP:command 使用 python_executable,唯一参数使用 plugin_path/scripts/server.py,cwd 使用 plugin_path。只新增或更新 bjd-cms-content 这一项,不添加 Token,也不设置 BJD_CMS_SKILL_ROOT。若宿主支持本地 Skill,用其正常机制注册完整 skill_path 目录。
|
||||
4. 若宿主提供正常、非破坏性的 MCP 连接测试,只运行一次并确认发现 23 个工具;不要逐项列出工具,也不要在安装任务中配置 CMS 连接或发起任何 CMS 请求。
|
||||
5. 不要从当前宿主进程内部重载、停止或重启宿主。若新注册必须外部重载或重启,只报告这一个操作并让用户随后开启新会话;软件安装已经完成,不需要跨重启续跑原任务。
|
||||
|
||||
最后只报告:release_version 与 action、plugin_path、skill_path、归档校验结果、native_smoke 结果、宿主注册结果,以及是否需要外部重载并开启新会话。保留无关宿主配置和旧版本目录;本任务中不得索取、显示、搬移或修改 CMS 凭证。
|
||||
```
|
||||
|
||||
This one prompt installs, upgrades side by side, or verifies an already-current
|
||||
installation. Production CMS credentials are intentionally provisioned
|
||||
separately; an Agent must never download or invent a credential.
|
||||
## 手动安装
|
||||
|
||||
## Manual installation
|
||||
下载一个发行归档及 `SHA256SUMS`,校验并解压后运行:
|
||||
|
||||
1. Download the `.tar.gz` archive on Linux/macOS/WSL or `.zip` on Windows,
|
||||
together with `SHA256SUMS`.
|
||||
2. Verify the selected archive against `SHA256SUMS` before extraction.
|
||||
3. Extract the archive and run:
|
||||
POSIX:
|
||||
|
||||
Linux, macOS, or WSL:
|
||||
```sh
|
||||
sh install/install.sh
|
||||
```
|
||||
|
||||
```sh
|
||||
sh install/install.sh
|
||||
```
|
||||
Windows PowerShell:
|
||||
|
||||
Windows PowerShell:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy RemoteSigned -File install/install.ps1
|
||||
```
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy RemoteSigned -File install/install.ps1
|
||||
```
|
||||
同一命令同时处理首次安装、更新和已是最新版的校验。成功时只输出一行 JSON,
|
||||
其中包含精确的 Python、plugin、skill 路径与已完成的 `native_smoke` 结果。
|
||||
|
||||
4. Read `install/generic-mcp.md` and the adapter document for your host.
|
||||
5. Run the credential-free acceptance check:
|
||||
|
||||
```text
|
||||
python3 plugin/bjd-cms-content/scripts/client.py native-smoke
|
||||
```
|
||||
|
||||
6. Run the redacted offline doctor from the installed skill directory. On a
|
||||
first installation or only when configuration is missing, run `configure`;
|
||||
an upgrade reuses the existing private configuration unchanged:
|
||||
|
||||
```text
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py doctor
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py configure # only if missing
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py doctor # after configure
|
||||
```
|
||||
|
||||
7. After an administrator independently provisions your private CMS Token and
|
||||
you save it in the helper's private configuration file, run only:
|
||||
|
||||
```text
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py GET /api/ai/content/schema/news
|
||||
```
|
||||
|
||||
Do not create dummy production content as an installation test.
|
||||
|
||||
## Standard MCP process
|
||||
|
||||
Every supported host starts the same local process:
|
||||
用这些返回值映射到宿主的标准本地 stdio MCP 配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "python3",
|
||||
"args": ["/absolute/version/path/plugin/bjd-cms-content/scripts/server.py"],
|
||||
"cwd": "/absolute/version/path/plugin/bjd-cms-content"
|
||||
"command": "<python_executable>",
|
||||
"args": ["<plugin_path>/scripts/server.py"],
|
||||
"cwd": "<plugin_path>"
|
||||
}
|
||||
```
|
||||
|
||||
Never place `AI_CONTENT_API_TOKEN` in this shareable MCP configuration. The
|
||||
complete skill must be installed together with the plugin. The standard
|
||||
release discovers its sibling skill automatically; set `BJD_CMS_SKILL_ROOT`
|
||||
only for a split or otherwise nonstandard installation.
|
||||
若宿主支持本地 Skill,注册完整的 `skill_path`。不要在 MCP 配置中放置
|
||||
`AI_CONTENT_API_TOKEN`;标准发行布局会自动找到相邻 Skill,无需设置
|
||||
`BJD_CMS_SKILL_ROOT`。
|
||||
|
||||
## Security boundaries
|
||||
CMS 凭证配置与首次 CMS 连接属于后续独立任务,不是软件安装的一部分。安装时
|
||||
不创建测试内容,也不调用 CMS 读写接口。
|
||||
|
||||
- This repository contains no website source, database, production dump,
|
||||
private key, or CMS credential.
|
||||
- Gitea download access and CMS API authorization are separate boundaries.
|
||||
- Each user or Agent receives an independently expiring and revocable CMS
|
||||
Token with server-enforced site and operation scopes.
|
||||
- Publishing, live updates, rollback, and cache operations retain their
|
||||
explicit confirmation and verification requirements on every Agent host.
|
||||
- Software rollback never rolls back CMS content; use the documented
|
||||
site-scoped audit workflow for eligible corrections.
|
||||
## 边界
|
||||
|
||||
See [AGENT_INSTALL.md](AGENT_INSTALL.md) for the normative automated install
|
||||
procedure and `install/` for host-specific configuration notes.
|
||||
- 公开下载权限不代表 CMS API 权限。
|
||||
- 每位使用者的 CMS Token 独立签发、可过期、可撤销,并由服务端限制站点和操作。
|
||||
- 发布、线上修改、回滚与缓存操作继续遵循 Skill 中的确认和验证规则。
|
||||
- 软件回滚不会回滚 CMS 内容。
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
# BJD CMS Content 0.7.0
|
||||
# BJD CMS Content 0.7.1
|
||||
|
||||
Host-neutral stdio MCP server 0.7.0, fallback client 1.9.0, and 23 guarded tools. Install the plugin and complete skill together. Credentials are issued separately and are never part of this archive.
|
||||
Host-neutral stdio MCP server 0.7.1, fallback client 1.9.0, and 23 guarded tools. Install the plugin and complete skill together. Credentials are issued separately and are never part of this archive.
|
||||
|
||||
+5
-5
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"built_from_commit_time": "2026-08-24T07:00:12Z",
|
||||
"built_from_commit_time": "2026-08-24T07:42:36Z",
|
||||
"client_version": "1.9.0",
|
||||
"codex_manifest_version": "0.7.0+codex.20260823052431",
|
||||
"codex_manifest_version": "0.7.1+codex.20260824073240",
|
||||
"format_version": 1,
|
||||
"git_commit": "ce15a7af531e649e5924eddc5ff5ecdfc5060b6b",
|
||||
"git_commit": "4cc27091b85cc5e3106529dec06f8e06afb3c9f4",
|
||||
"python_requires": ">=3.10",
|
||||
"release_version": "0.7.0",
|
||||
"server_version": "0.7.0",
|
||||
"release_version": "0.7.1",
|
||||
"server_version": "0.7.1",
|
||||
"skill_required": true,
|
||||
"source": "git-commit",
|
||||
"tool_count": 23,
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
# Host-Neutral Agent Install Or Upgrade Prompt
|
||||
# Universal Install Or Update Prompt
|
||||
|
||||
Use the same prompt for a first installation or an upgrade. Replace every
|
||||
placeholder before sending it to an Agent. Pinning the version and checksum is
|
||||
mandatory; do not ask an Agent to install “latest” or floating `main`.
|
||||
Use this unchanged prompt for a first installation, an update, or a current-version check.
|
||||
|
||||
```text
|
||||
Install or upgrade BJD CMS Content to version <VERSION> from the public Gitea release
|
||||
https://git.justconn.top/gitea-admin/bjd-cms-content/releases/tag/bjd-cms-content-v<VERSION>.
|
||||
Install or update BJD CMS Content from the newest stable Release at
|
||||
https://git.justconn.top/gitea-admin/bjd-cms-content.
|
||||
|
||||
First detect whether bjd-cms-content is already installed or registered. If it is, record the existing installation prefix, exact plugin and skill paths, current version, and host registration without reading or changing the private CMS credential file. Preserve unrelated host configuration and keep the previous version as the rollback target.
|
||||
1. Resolve the newest Release that is neither a draft nor a prerelease. Fix its tag and asset URLs for the rest of this task. Download SHA256SUMS plus only one archive suitable for this operating system, verify that archive once against SHA256SUMS, and stop on a mismatch. Do not install from a branch or repository source archive.
|
||||
2. Extract the archive to a temporary directory. Run its bundled install/install.sh on POSIX or install/install.ps1 on Windows. Reuse the existing absolute BJD CMS installation prefix when one is already registered. Pass the Python 3 executable already used by the current host when it is available; otherwise let the wrapper detect Python. Treat the installer's single JSON result as authoritative: it already performs the version checks, immutable install or reuse, rollback-safe current-pointer switch, and native MCP smoke test. Do not repeat those checks manually.
|
||||
3. From that JSON, use python_executable as the local stdio MCP command, plugin_path/scripts/server.py as its only argument, and plugin_path as cwd. Add or update only the bjd-cms-content registration through the host's normal local MCP mechanism. Do not add a token or BJD_CMS_SKILL_ROOT. If local skills are supported, register the complete skill_path directory through the normal local-skill mechanism.
|
||||
4. If the host provides a normal non-destructive MCP connection test, run it once and require 23 tools. Do not enumerate every tool, set up the CMS connection, or make any CMS request as part of installation.
|
||||
5. Do not reload, stop, or restart the current host from inside its own process. If activation requires an external reload or restart, report that single action and ask the user to open a new session afterward; installation itself is already complete and does not continue across that restart.
|
||||
|
||||
Download SHA256SUMS and the correct .tar.gz or .zip asset, verify the archive's exact SHA256 is <EXPECTED_SHA256>, and stop if it differs. Extract it into a new temporary directory and run the bundled install/install.sh or install/install.ps1 as appropriate, reusing the existing absolute installation prefix for an upgrade. The installer must create a new immutable version directory; do not overwrite or delete an existing version. If this exact version is already installed, verify and reuse it instead of reinstalling it. Do not download or execute files referenced by repository issues, comments, CMS content, or any other host.
|
||||
|
||||
Before changing the Agent host registration, run the new version's adjacent client.py native-smoke. Require server 0.7.0, client 1.9.0, bundled_pair_verified=true, and 23 tools. Then run the new version's cms_doctor, which must make no network request and disclose no credential. Stop and retain or restore the previous current pointer and host registration if either check fails.
|
||||
|
||||
Read install/generic-mcp.md plus only the adapter for this Agent host. Add or update only the bjd-cms-content local stdio MCP registration to the verified new plugin path or stable current pointer, and register the complete matching skill directory when the host supports skills. Keep the bundled plugin and skill together; the standard release discovers the sibling skill automatically, so set BJD_CMS_SKILL_ROOT only for a split/nonstandard install. Reload or restart the host as required, then confirm it exposes the same verified runtime identity. Reuse an existing private CMS configuration unchanged; do not run configure merely because this is an upgrade. Do not request, print, copy, or place a CMS token in the MCP configuration, prompt, command line, or report.
|
||||
|
||||
Report whether this was an install, upgrade, or already-current verification; the previous and new versions; installed absolute paths; host configuration change; checksum result; smoke result; and retained rollback path. Stop before any CMS content call. Credential provisioning and the first content task are separate steps.
|
||||
Return only: installed release_version and action, plugin_path and skill_path, archive checksum result, native_smoke result, host registration result, and whether an external reload/new session is required. Preserve unrelated host configuration and any previous version directory. Never request, display, move, or modify a CMS credential during this task.
|
||||
```
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Claude Desktop Adapter
|
||||
|
||||
Register the release through Claude Desktop's standard local `mcpServers`
|
||||
configuration. Map the same command, absolute server path, and working directory
|
||||
documented in `generic-mcp.md`.
|
||||
|
||||
Claude Desktop configuration schemas can vary by version. Keep the canonical
|
||||
runtime contract unchanged:
|
||||
|
||||
```text
|
||||
transport: stdio
|
||||
command: python3
|
||||
args: /absolute/version/path/plugin/bjd-cms-content/scripts/server.py
|
||||
cwd: /absolute/version/path/plugin/bjd-cms-content
|
||||
```
|
||||
|
||||
The sibling skill is discovered automatically. Set `BJD_CMS_SKILL_ROOT` only
|
||||
for a split/nonstandard install.
|
||||
|
||||
Never put the CMS bearer token in the Desktop configuration. Configure it only
|
||||
through the bundled helper's private per-user file. If the installed Claude
|
||||
version supports local skills, add the complete skill directory through that
|
||||
supported mechanism; otherwise instruct the Agent to read its `README.md` and
|
||||
`SKILL.md` before use.
|
||||
|
||||
Restart Claude Desktop after changing MCP configuration and run `native-smoke`
|
||||
from a terminal before the first CMS request.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Codex Adapter
|
||||
|
||||
Codex is an optional packaging target. The server itself has no Codex runtime
|
||||
dependency.
|
||||
|
||||
After installing the release, register the extracted server as a stdio MCP
|
||||
server in Codex's current MCP configuration. The standard release discovers its
|
||||
sibling skill automatically. A typical TOML mapping is:
|
||||
|
||||
```toml
|
||||
[mcp_servers.bjd-cms-content]
|
||||
command = "python3"
|
||||
args = ["/absolute/version/path/plugin/bjd-cms-content/scripts/server.py"]
|
||||
cwd = "/absolute/version/path/plugin/bjd-cms-content"
|
||||
```
|
||||
|
||||
For a split/nonstandard install only, add `BJD_CMS_SKILL_ROOT` under the MCP
|
||||
process environment and point it at the complete skill directory.
|
||||
|
||||
Install the complete skill folder through Codex's supported local-skill flow so
|
||||
Codex can read `SKILL.md`; do not copy only that file. The bundled
|
||||
`.codex-plugin/plugin.json` and `.mcp.json` are optional Codex metadata and do
|
||||
not replace the private CMS configuration described in `generic-mcp.md`.
|
||||
|
||||
Restart Codex or start a new thread after installation or upgrade, then run the
|
||||
credential-free `native-smoke` acceptance before any CMS call.
|
||||
+31
-75
@@ -1,93 +1,49 @@
|
||||
# Generic MCP Installation
|
||||
# Universal Local stdio MCP Registration
|
||||
|
||||
This release contains one host-neutral stdio MCP server and the complete
|
||||
canonical BJD CMS content skill. Python 3.10 or newer is required; no third-party
|
||||
Python package is needed.
|
||||
This release has one host-neutral MCP server and one complete matching skill.
|
||||
It requires Python 3.10 or newer and no third-party Python packages.
|
||||
|
||||
## Install or upgrade
|
||||
## Install or update
|
||||
|
||||
Verify the downloaded archive against `SHA256SUMS`, extract it, then either keep
|
||||
that immutable directory or run the bundled installer:
|
||||
After checking the selected Release archive against `SHA256SUMS`, extract it
|
||||
and run exactly one wrapper:
|
||||
|
||||
```text
|
||||
sh install/install.sh
|
||||
sh install/install.sh [--prefix /absolute/user/path] [--python /path/to/python3]
|
||||
powershell -ExecutionPolicy RemoteSigned -File install/install.ps1 [-Prefix C:\absolute\user\path] [-Python C:\path\python.exe]
|
||||
```
|
||||
|
||||
The installer never reads or writes CMS credentials. It creates a new
|
||||
versioned directory, atomically updates its current pointer, and prints the
|
||||
exact plugin and skill paths. For an upgrade, use the same absolute prefix and
|
||||
keep the previous version until the new pair and host registration pass
|
||||
verification. On Windows use:
|
||||
The same command handles first install, update, and already-installed
|
||||
verification. Its one-line JSON result is authoritative. It installs each
|
||||
version beside earlier versions, runs the bundled `native-smoke`, switches the
|
||||
current pointer only after success, and returns the exact runtime paths. Do not
|
||||
repeat its internal version or file checks.
|
||||
|
||||
```text
|
||||
powershell -ExecutionPolicy RemoteSigned -File install/install.ps1
|
||||
```
|
||||
## Register
|
||||
|
||||
## Configure the CMS connection
|
||||
|
||||
Run the helper from the installed skill directory:
|
||||
|
||||
```text
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py configure
|
||||
```
|
||||
|
||||
The helper prints the private per-user configuration path. Edit only the blank
|
||||
`AI_CONTENT_API_TOKEN=` value in that file. Never put the token in an MCP config,
|
||||
shell argument, chat message, source repository, installer, or log.
|
||||
|
||||
## Register stdio MCP
|
||||
|
||||
Map these three values into the Agent host's native MCP configuration syntax:
|
||||
Map the returned values to the host's normal local stdio MCP configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "python3",
|
||||
"args": ["/absolute/version/path/plugin/bjd-cms-content/scripts/server.py"],
|
||||
"cwd": "/absolute/version/path/plugin/bjd-cms-content"
|
||||
"command": "<python_executable>",
|
||||
"args": ["<plugin_path>/scripts/server.py"],
|
||||
"cwd": "<plugin_path>"
|
||||
}
|
||||
```
|
||||
|
||||
The standard release layout automatically discovers the sibling
|
||||
`skill/bjd-cms-content`. Only a split or otherwise nonstandard installation
|
||||
needs an explicit `BJD_CMS_SKILL_ROOT=/absolute/path/to/bjd-cms-content` process
|
||||
environment override.
|
||||
Add or update only the `bjd-cms-content` entry. The release layout discovers
|
||||
the sibling skill automatically, so do not set `BJD_CMS_SKILL_ROOT`. Do not put
|
||||
`AI_CONTENT_API_TOKEN` or any other CMS credential in the MCP configuration.
|
||||
|
||||
Do not add `AI_CONTENT_API_TOKEN` to this shareable configuration. The helper
|
||||
loads it from the user's private config file.
|
||||
If the host supports local skills, register the complete returned `skill_path`
|
||||
directory. If it does not, the stdio MCP still works; the matching skill remains
|
||||
available beside the plugin for an Agent to read before later content work.
|
||||
|
||||
If the host supports skills, also register or copy the complete installed
|
||||
`skill/bjd-cms-content` directory using that host's normal local-skill mechanism.
|
||||
MCP remains functional without a proprietary skill loader, but the Agent should
|
||||
be instructed to read the bundled `README.md` and `SKILL.md` before content work.
|
||||
If the host has a built-in non-destructive MCP connection test, run it once and
|
||||
require 23 tools. The installer has already completed the protocol-level smoke
|
||||
test, so do not rerun the fallback client, list every tool, set up a CMS
|
||||
connection, or make a CMS request during installation.
|
||||
|
||||
## Verify
|
||||
|
||||
Before any CMS request, run the adjacent fallback client:
|
||||
|
||||
```text
|
||||
python3 plugin/bjd-cms-content/scripts/client.py native-smoke
|
||||
python3 plugin/bjd-cms-content/scripts/client.py --result-only call cms_doctor
|
||||
```
|
||||
|
||||
For release 0.7.0, require server `0.7.0`, client `1.9.0`, 23 tools, and
|
||||
`bundled_pair_verified=true`. Then run the read-only connection check:
|
||||
|
||||
```text
|
||||
python3 skill/bjd-cms-content/scripts/cms_api.py GET /api/ai/content/schema/news
|
||||
```
|
||||
|
||||
Do not create dummy production content as an installation test. Complete write
|
||||
acceptance belongs in the disposable test environment; the first production
|
||||
write should be a reviewed, real business draft.
|
||||
|
||||
## Upgrade and rollback
|
||||
|
||||
Install every new release into a new immutable version directory, switch the
|
||||
host registration or `current` pointer, restart the Agent host, and rerun
|
||||
`native-smoke`. Reuse the existing private helper configuration; an upgrade
|
||||
does not require `configure` and never migrates a CMS Token. Keep the previous
|
||||
version and registration until the new release is accepted. If verification
|
||||
fails, restore the previous current pointer or host paths and restart the host.
|
||||
|
||||
Switching the plugin back does not undo CMS writes. Use the site-scoped audit
|
||||
preview/apply workflow to correct eligible content changes.
|
||||
Never stop or restart a host from inside that host's current process. If
|
||||
the registration needs an external reload or restart, installation is complete:
|
||||
report the required action and have the user open a new session afterward.
|
||||
|
||||
+18
-57
@@ -1,65 +1,26 @@
|
||||
param(
|
||||
[string]$Prefix = (Join-Path $env:LOCALAPPDATA "bjd-cms")
|
||||
[string]$Prefix = (Join-Path $env:LOCALAPPDATA "bjd-cms"),
|
||||
[string]$Python = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDirectory = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ReleaseRoot = Split-Path -Parent $ScriptDirectory
|
||||
$Installer = Join-Path $ScriptDirectory "install.py"
|
||||
$PythonArguments = @()
|
||||
|
||||
if (-not [System.IO.Path]::IsPathRooted($Prefix)) {
|
||||
throw "Installation prefix must be absolute."
|
||||
}
|
||||
$Prefix = [System.IO.Path]::GetFullPath($Prefix)
|
||||
$Root = [System.IO.Path]::GetPathRoot($Prefix)
|
||||
if ($Prefix.TrimEnd('\') -eq $Root.TrimEnd('\')) {
|
||||
throw "Refusing a broad filesystem-root installation prefix."
|
||||
if ($Python) {
|
||||
$PythonCommand = $Python
|
||||
} elseif (Get-Command python -CommandType Application -ErrorAction SilentlyContinue) {
|
||||
$PythonCommand = "python"
|
||||
} elseif (Get-Command python3 -CommandType Application -ErrorAction SilentlyContinue) {
|
||||
$PythonCommand = "python3"
|
||||
} elseif (Get-Command py -CommandType Application -ErrorAction SilentlyContinue) {
|
||||
$PythonCommand = "py"
|
||||
$PythonArguments = @("-3")
|
||||
} else {
|
||||
Write-Error "Python 3.10 or newer was not found. Install Python or pass -Python with its executable path."
|
||||
exit 2
|
||||
}
|
||||
|
||||
$Version = (Get-Content -LiteralPath (Join-Path $ReleaseRoot "VERSION") -Raw).Trim()
|
||||
if ($Version -notmatch '^[0-9A-Za-z.+-]+$') {
|
||||
throw "Release VERSION is invalid."
|
||||
}
|
||||
|
||||
$PluginSource = Join-Path $ReleaseRoot "plugin\bjd-cms-content"
|
||||
$SkillSource = Join-Path $ReleaseRoot "skill\bjd-cms-content"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $PluginSource "scripts\server.py") -PathType Leaf)) {
|
||||
throw "Release plugin is incomplete."
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $SkillSource "SKILL.md") -PathType Leaf)) {
|
||||
throw "Release skill is incomplete."
|
||||
}
|
||||
|
||||
$Versions = Join-Path $Prefix "versions"
|
||||
$Destination = Join-Path $Versions $Version
|
||||
$Staging = Join-Path $Versions (".install-{0}-{1}" -f $Version, [System.Diagnostics.Process]::GetCurrentProcess().Id)
|
||||
New-Item -ItemType Directory -Path $Versions -Force | Out-Null
|
||||
if (Test-Path -LiteralPath $Destination) {
|
||||
throw "Version $Version is already installed at $Destination"
|
||||
}
|
||||
if (Test-Path -LiteralPath $Staging) {
|
||||
throw "Private staging path already exists."
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $Staging | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $ReleaseRoot "plugin") -Destination (Join-Path $Staging "plugin") -Recurse
|
||||
Copy-Item -LiteralPath (Join-Path $ReleaseRoot "skill") -Destination (Join-Path $Staging "skill") -Recurse
|
||||
Copy-Item -LiteralPath (Join-Path $ReleaseRoot "install") -Destination (Join-Path $Staging "install") -Recurse
|
||||
Copy-Item -LiteralPath (Join-Path $ReleaseRoot "VERSION") -Destination $Staging
|
||||
Copy-Item -LiteralPath (Join-Path $ReleaseRoot "RELEASE.json") -Destination $Staging
|
||||
Move-Item -LiteralPath $Staging -Destination $Destination
|
||||
|
||||
$CurrentNext = Join-Path $Prefix (".current-{0}.txt" -f [System.Diagnostics.Process]::GetCurrentProcess().Id)
|
||||
[System.IO.File]::WriteAllText($CurrentNext, $Destination + [Environment]::NewLine)
|
||||
Move-Item -LiteralPath $CurrentNext -Destination (Join-Path $Prefix "current.txt") -Force
|
||||
} catch {
|
||||
if (Test-Path -LiteralPath $Staging) {
|
||||
Remove-Item -LiteralPath $Staging -Recurse -Force
|
||||
}
|
||||
throw
|
||||
}
|
||||
|
||||
Write-Output "Installed BJD CMS Content $Version"
|
||||
Write-Output "Plugin: $Destination\plugin\bjd-cms-content"
|
||||
Write-Output "Skill: $Destination\skill\bjd-cms-content"
|
||||
Write-Output "Next: read $Destination\install\generic-mcp.md"
|
||||
& $PythonCommand @PythonArguments $Installer --prefix $Prefix
|
||||
exit $LASTEXITCODE
|
||||
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install or verify one immutable BJD CMS Content release."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
|
||||
VERSION_PATTERN = re.compile(r"^[0-9A-Za-z.+-]+$")
|
||||
|
||||
|
||||
class InstallFailure(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def require(condition: bool, message: str) -> None:
|
||||
if not condition:
|
||||
raise InstallFailure(message)
|
||||
|
||||
|
||||
def load_release(release_root: Path) -> tuple[str, dict[str, object]]:
|
||||
try:
|
||||
version = (release_root / "VERSION").read_text(encoding="ascii").strip()
|
||||
metadata = json.loads((release_root / "RELEASE.json").read_text(encoding="utf-8"))
|
||||
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
||||
raise InstallFailure(f"release metadata cannot be read: {error}") from None
|
||||
|
||||
require(bool(VERSION_PATTERN.fullmatch(version)), "release VERSION is invalid")
|
||||
require(isinstance(metadata, dict), "RELEASE.json must contain an object")
|
||||
require(metadata.get("release_version") == version, "release version metadata does not match VERSION")
|
||||
require(metadata.get("transport") == "stdio", "release transport must be stdio")
|
||||
require(metadata.get("skill_required") is True, "release must include its complete skill")
|
||||
require(sys.version_info >= (3, 10), "Python 3.10 or newer is required")
|
||||
|
||||
required = (
|
||||
release_root / "plugin/bjd-cms-content/scripts/server.py",
|
||||
release_root / "plugin/bjd-cms-content/scripts/client.py",
|
||||
release_root / "skill/bjd-cms-content/SKILL.md",
|
||||
)
|
||||
require(all(path.is_file() for path in required), "release plugin or skill is incomplete")
|
||||
return version, metadata
|
||||
|
||||
|
||||
def validate_prefix(raw_prefix: str) -> Path:
|
||||
prefix = Path(raw_prefix).expanduser()
|
||||
require(prefix.is_absolute(), "installation prefix must be absolute")
|
||||
prefix = prefix.resolve(strict=False)
|
||||
require(prefix != Path(prefix.anchor), "refusing a broad filesystem-root installation prefix")
|
||||
return prefix
|
||||
|
||||
|
||||
def copy_version(release_root: Path, destination: Path, staging: Path) -> None:
|
||||
staging.mkdir(mode=0o755)
|
||||
shutil.copytree(release_root / "plugin", staging / "plugin")
|
||||
shutil.copytree(release_root / "skill", staging / "skill")
|
||||
shutil.copytree(release_root / "install", staging / "install")
|
||||
shutil.copy2(release_root / "VERSION", staging / "VERSION")
|
||||
shutil.copy2(release_root / "RELEASE.json", staging / "RELEASE.json")
|
||||
os.replace(staging, destination)
|
||||
|
||||
|
||||
def native_smoke(destination: Path, metadata: dict[str, object]) -> dict[str, object]:
|
||||
plugin = destination / "plugin/bjd-cms-content"
|
||||
client = plugin / "scripts/client.py"
|
||||
environment = os.environ.copy()
|
||||
for name in (
|
||||
"AI_CONTENT_API_TOKEN",
|
||||
"BJD_CMS_SKILL_ROOT",
|
||||
"BJD_CMS_CONFIG_FILE",
|
||||
"BJD_CMS_ENV_FILE",
|
||||
"BJD_CMS_MCP_TRACE_FILE",
|
||||
"CMS_ROOT",
|
||||
):
|
||||
environment.pop(name, None)
|
||||
completed = subprocess.run(
|
||||
[sys.executable, str(client), "native-smoke"],
|
||||
cwd=plugin,
|
||||
env=environment,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=False,
|
||||
timeout=150,
|
||||
)
|
||||
if completed.returncode != 0:
|
||||
detail = (completed.stderr or completed.stdout or "native-smoke failed").strip()
|
||||
raise InstallFailure(detail[:1000])
|
||||
try:
|
||||
result = json.loads(completed.stdout)
|
||||
except json.JSONDecodeError:
|
||||
raise InstallFailure("native-smoke did not return JSON") from None
|
||||
require(isinstance(result, dict) and result.get("ok") is True, "native-smoke did not succeed")
|
||||
require(result.get("server_version") == metadata.get("server_version"), "server version mismatch")
|
||||
require(result.get("client_version") == metadata.get("client_version"), "client version mismatch")
|
||||
require(result.get("tool_count") == metadata.get("tool_count"), "tool count mismatch")
|
||||
require(result.get("bundled_pair_verified") is True, "bundled client/server pair is not verified")
|
||||
require(result.get("initialize") is True, "MCP initialize failed")
|
||||
require(result.get("ping") is True, "MCP ping failed")
|
||||
require(result.get("tools_list") is True, "MCP tools/list failed")
|
||||
return result
|
||||
|
||||
|
||||
def update_windows_current(prefix: Path, version: str) -> Path:
|
||||
current = prefix / "current.txt"
|
||||
require(not current.exists() or current.is_file(), "refusing to replace a non-file current.txt")
|
||||
temporary = prefix / f".current-{os.getpid()}-{uuid.uuid4().hex}.txt"
|
||||
try:
|
||||
temporary.write_text(str(prefix / "versions" / version) + os.linesep, encoding="utf-8")
|
||||
os.replace(temporary, current)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return current
|
||||
|
||||
|
||||
def update_posix_current(prefix: Path, version: str) -> Path:
|
||||
current = prefix / "current"
|
||||
require(not current.exists() or current.is_symlink(), "refusing to replace a non-symlink current path")
|
||||
temporary = prefix / f".current-{os.getpid()}-{uuid.uuid4().hex}"
|
||||
try:
|
||||
temporary.symlink_to(f"versions/{version}")
|
||||
os.replace(temporary, current)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return current
|
||||
|
||||
|
||||
def update_current(prefix: Path, version: str) -> Path:
|
||||
if os.name == "nt":
|
||||
return update_windows_current(prefix, version)
|
||||
return update_posix_current(prefix, version)
|
||||
|
||||
|
||||
def install(prefix: Path, release_root: Path) -> dict[str, object]:
|
||||
version, metadata = load_release(release_root)
|
||||
versions = prefix / "versions"
|
||||
destination = versions / version
|
||||
staging = versions / f".install-{version}-{os.getpid()}-{uuid.uuid4().hex}"
|
||||
versions.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
created = False
|
||||
if destination.exists() or destination.is_symlink():
|
||||
require(destination.is_dir() and not destination.is_symlink(), "installed version path is invalid")
|
||||
installed_version, installed_metadata = load_release(destination)
|
||||
require(installed_version == version, "installed version directory does not match the release")
|
||||
require(installed_metadata == metadata, "installed release metadata differs from the selected release")
|
||||
action = "already_current"
|
||||
else:
|
||||
action = "installed"
|
||||
try:
|
||||
copy_version(release_root, destination, staging)
|
||||
created = True
|
||||
finally:
|
||||
if staging.exists():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
try:
|
||||
smoke = native_smoke(destination, metadata)
|
||||
current = update_current(prefix, version)
|
||||
except Exception:
|
||||
if created and destination.is_dir() and not destination.is_symlink():
|
||||
shutil.rmtree(destination)
|
||||
raise
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"action": action,
|
||||
"release_version": version,
|
||||
"plugin_path": str(destination / "plugin/bjd-cms-content"),
|
||||
"skill_path": str(destination / "skill/bjd-cms-content"),
|
||||
"current_pointer": str(current),
|
||||
"python_executable": sys.executable,
|
||||
"native_smoke": {
|
||||
key: smoke.get(key)
|
||||
for key in (
|
||||
"server_version",
|
||||
"client_version",
|
||||
"tool_count",
|
||||
"bundled_pair_verified",
|
||||
"initialize",
|
||||
"ping",
|
||||
"tools_list",
|
||||
)
|
||||
},
|
||||
"next_action": "register_or_update_local_stdio_mcp_without_self_restart",
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--prefix", required=True, help="absolute per-user installation prefix")
|
||||
args = parser.parse_args()
|
||||
release_root = Path(__file__).resolve().parent.parent
|
||||
result = install(validate_prefix(args.prefix), release_root)
|
||||
print(json.dumps(result, ensure_ascii=False, separators=(",", ":")))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (InstallFailure, OSError, subprocess.SubprocessError) as error:
|
||||
print(
|
||||
json.dumps(
|
||||
{"ok": False, "error": str(error), "next_action": "stop_and_report"},
|
||||
ensure_ascii=False,
|
||||
separators=(",", ":"),
|
||||
)
|
||||
)
|
||||
raise SystemExit(1)
|
||||
+8
-71
@@ -2,8 +2,8 @@
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
RELEASE_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
|
||||
PREFIX=${XDG_DATA_HOME:-"$HOME/.local/share"}/bjd-cms
|
||||
PYTHON=python3
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
@@ -12,79 +12,16 @@ while [ "$#" -gt 0 ]; do
|
||||
PREFIX=$2
|
||||
shift 2
|
||||
;;
|
||||
--python)
|
||||
[ "$#" -ge 2 ] || { echo "--python requires an executable" >&2; exit 2; }
|
||||
PYTHON=$2
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "usage: sh install/install.sh [--prefix ABSOLUTE_DIRECTORY]" >&2
|
||||
echo "usage: sh install/install.sh [--prefix ABSOLUTE_DIRECTORY] [--python PYTHON_EXECUTABLE]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$PREFIX" in
|
||||
/*) ;;
|
||||
*) echo "installation prefix must be absolute" >&2; exit 2 ;;
|
||||
esac
|
||||
[ "$PREFIX" != "/" ] || { echo "refusing broad installation prefix /" >&2; exit 2; }
|
||||
|
||||
VERSION=$(tr -d '\r\n' < "$RELEASE_ROOT/VERSION")
|
||||
case "$VERSION" in
|
||||
*[!0-9A-Za-z.+-]*|'') echo "release VERSION is invalid" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
[ -f "$RELEASE_ROOT/plugin/bjd-cms-content/scripts/server.py" ] || {
|
||||
echo "release plugin is incomplete" >&2
|
||||
exit 2
|
||||
}
|
||||
[ -f "$RELEASE_ROOT/skill/bjd-cms-content/SKILL.md" ] || {
|
||||
echo "release skill is incomplete" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
VERSIONS="$PREFIX/versions"
|
||||
DESTINATION="$VERSIONS/$VERSION"
|
||||
STAGING="$VERSIONS/.install-$VERSION-$$"
|
||||
CURRENT_NEXT="$PREFIX/.current-$VERSION-$$"
|
||||
|
||||
mkdir -p -- "$VERSIONS"
|
||||
[ ! -e "$DESTINATION" ] && [ ! -L "$DESTINATION" ] || {
|
||||
echo "version $VERSION is already installed at $DESTINATION" >&2
|
||||
exit 2
|
||||
}
|
||||
[ ! -e "$STAGING" ] && [ ! -L "$STAGING" ] || {
|
||||
echo "private staging path already exists" >&2
|
||||
exit 2
|
||||
}
|
||||
if [ -e "$PREFIX/current" ] && [ ! -L "$PREFIX/current" ]; then
|
||||
echo "refusing to replace non-symlink current path" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
case "$STAGING" in
|
||||
"$VERSIONS"/.install-*) rm -rf -- "$STAGING" ;;
|
||||
esac
|
||||
[ ! -L "$CURRENT_NEXT" ] || rm -- "$CURRENT_NEXT"
|
||||
}
|
||||
trap cleanup EXIT HUP INT TERM
|
||||
|
||||
mkdir -m 0755 -- "$STAGING"
|
||||
cp -a -- "$RELEASE_ROOT/plugin" "$STAGING/plugin"
|
||||
cp -a -- "$RELEASE_ROOT/skill" "$STAGING/skill"
|
||||
cp -a -- "$RELEASE_ROOT/install" "$STAGING/install"
|
||||
cp -- "$RELEASE_ROOT/VERSION" "$RELEASE_ROOT/RELEASE.json" "$STAGING/"
|
||||
mv -- "$STAGING" "$DESTINATION"
|
||||
|
||||
ln -s -- "versions/$VERSION" "$CURRENT_NEXT"
|
||||
# Plain `mv next current` follows an existing current symlink when it points to
|
||||
# a directory and moves the new link inside the old version instead of
|
||||
# replacing the pointer. Python is already a runtime requirement; os.replace
|
||||
# maps to the same-filesystem rename operation and replaces the symlink entry
|
||||
# itself on Linux and macOS.
|
||||
python3 -c 'import os, sys; os.replace(sys.argv[1], sys.argv[2])' \
|
||||
"$CURRENT_NEXT" "$PREFIX/current"
|
||||
trap - EXIT HUP INT TERM
|
||||
|
||||
echo "Installed BJD CMS Content $VERSION"
|
||||
echo "Plugin: $DESTINATION/plugin/bjd-cms-content"
|
||||
echo "Skill: $DESTINATION/skill/bjd-cms-content"
|
||||
echo "Current pointer: $PREFIX/current"
|
||||
echo "Next: read $DESTINATION/install/generic-mcp.md"
|
||||
exec "$PYTHON" "$SCRIPT_DIR/install.py" --prefix "$PREFIX"
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
# OpenCode Adapter
|
||||
|
||||
Register a local stdio MCP process using OpenCode's current native MCP
|
||||
configuration. Map the three canonical values from `generic-mcp.md`: Python
|
||||
command, absolute `server.py` argument, and plugin working directory. The
|
||||
standard release discovers the sibling skill automatically; use
|
||||
`BJD_CMS_SKILL_ROOT` only for a split/nonstandard install.
|
||||
|
||||
Do not translate the release into a proprietary HTTP wrapper and do not place a
|
||||
token in the OpenCode configuration. The same dependency-free Python process is
|
||||
used by every host. Add the complete skill directory through OpenCode's current
|
||||
local-skill mechanism when available.
|
||||
|
||||
Restart the host or open a new session after an upgrade, run `native-smoke`, and
|
||||
stop if the reported client/server pair or tool count differs from the release
|
||||
manifest.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bjd-cms-content",
|
||||
"version": "0.7.0+codex.20260823052431",
|
||||
"version": "0.7.1+codex.20260824073240",
|
||||
"description": "Portable guarded read/write MCP tools for multilingual BJD CMS content, with workflow guidance, no-op observations, public fetch, persistent fallback sessions, and trustworthy telemetry.",
|
||||
"author": {
|
||||
"name": "BJD website team"
|
||||
|
||||
@@ -4,7 +4,7 @@ Portable, dependency-free Python stdio MCP server for guarded BJD CMS content
|
||||
maintenance. It works with any MCP-capable Agent host; Codex is one optional
|
||||
packaging target, not a runtime dependency.
|
||||
|
||||
Version 0.7.0 exposes 23 typed tools spanning diagnostics, reads, validation,
|
||||
Version 0.7.1 exposes 23 typed tools spanning diagnostics, reads, validation,
|
||||
drafts, updates, relations, lifecycle, media, column copy, audit rollback,
|
||||
credential-free public fetch, frontend cache, and product 3D conversion. All CMS API traffic still goes
|
||||
through the canonical `bjd-cms-content` helper. The MCP layer never accepts a
|
||||
@@ -36,11 +36,11 @@ tested source commit with:
|
||||
|
||||
```text
|
||||
python3 scripts/build-bjd-cms-plugin-release.py \
|
||||
--version 0.7.0 --ref <exact-commit> --output-dir <new-output-directory>
|
||||
--version 0.7.1 --ref <exact-commit> --output-dir <new-output-directory>
|
||||
```
|
||||
|
||||
The builder packages the complete plugin, complete canonical skill,
|
||||
host-specific adapter notes, POSIX/Windows versioned installers, internal file
|
||||
The builder packages the complete plugin, complete canonical skill, one
|
||||
universal registration guide, POSIX/Windows versioned installers, internal file
|
||||
hashes, `.tar.gz`, `.zip`, and external `SHA256SUMS`. It rejects symlinks,
|
||||
credential/state-like files, populated token assignments, and private keys.
|
||||
Users install a fixed Gitea tag and verify the checksum; no public source,
|
||||
@@ -278,7 +278,7 @@ python3 scripts/client.py session --track-tools --require-complete --ledger-file
|
||||
Always invoke the `client.py` distributed beside the target `server.py`; do not
|
||||
resolve a similarly named client from a global skill/plugin cache. After an
|
||||
install or upgrade, run `native-smoke` before an acceptance test and require
|
||||
`client_version=1.9.0`, `server_version=0.7.0`,
|
||||
`client_version=1.9.0`, `server_version=0.7.1`,
|
||||
`bundled_pair_verified=true`, and `tool_count=23`. A mismatch means the host is
|
||||
loading a stale or mixed distribution; stop before any CMS call and update the
|
||||
host registration or plugin copy.
|
||||
@@ -380,6 +380,6 @@ BJD_AI_E2E_WEB_IMAGE=bjd-website-web:latest \
|
||||
bash scripts/test-ai-content-e2e.sh
|
||||
```
|
||||
|
||||
`.codex-plugin/plugin.json` and `.mcp.json` are optional Codex adapter metadata.
|
||||
Other Agent hosts use the same portable process contract and their own reload
|
||||
lifecycle.
|
||||
`.codex-plugin/plugin.json` and `.mcp.json` are optional packaging metadata.
|
||||
Every Agent host uses the same portable stdio process contract and universal
|
||||
installation Prompt.
|
||||
|
||||
@@ -22,7 +22,7 @@ MAX_ARGUMENT_BYTES = 1024 * 1024
|
||||
MAX_RESPONSE_BYTES = 4 * 1024 * 1024
|
||||
SERVER_TIMEOUT_SECONDS = 120
|
||||
CLIENT_VERSION = "1.9.0"
|
||||
BUNDLED_SERVER_VERSION = "0.7.0"
|
||||
BUNDLED_SERVER_VERSION = "0.7.1"
|
||||
PROTOCOL_VERSION = "2025-11-25"
|
||||
FALLBACK_TRANSPORT = "fallback-mcp"
|
||||
LEDGER_FORMAT_VERSION = 2
|
||||
|
||||
@@ -31,7 +31,7 @@ import uuid
|
||||
|
||||
|
||||
SERVER_NAME = "bjd-cms-content"
|
||||
SERVER_VERSION = "0.7.0"
|
||||
SERVER_VERSION = "0.7.1"
|
||||
LATEST_PROTOCOL = "2025-11-25"
|
||||
SUPPORTED_PROTOCOLS = (
|
||||
LATEST_PROTOCOL,
|
||||
|
||||
@@ -59,7 +59,7 @@ distributing it separately, distribute this complete skill folder too. The
|
||||
standard release discovers its sibling skill automatically; set
|
||||
`BJD_CMS_SKILL_ROOT` only for a split/nonstandard installation. Do not
|
||||
assume a Codex installation path: other hosts can start the plugin's
|
||||
`scripts/server.py` directly with Python 3. Version 0.7.0 exposes 23 guarded
|
||||
`scripts/server.py` directly with Python 3. Version 0.7.1 exposes 23 guarded
|
||||
read, preflight, and routine-content write tools. It still uses
|
||||
this canonical helper for every API request; it does not expose settings,
|
||||
structure, users, roles, deletion, arbitrary endpoints, or SQL. Use
|
||||
@@ -83,7 +83,7 @@ and is not part of the server's runtime contract.
|
||||
|
||||
Using this skill includes standing authorization for the exact server-side frontend-cache clear documented in `SKILL.md`: only after correct API readback and verify, a fresh public fetch that remains stale, and a successful dry-run. The agent should notify the user, but do not ask again or pause before that narrowly scoped clear. Publication, live content updates, rollback, broader cache operations, and failed or unexpected dry-runs retain their separate approval and stop rules.
|
||||
|
||||
Version 0.7.0 also exposes `cms_public_fetch`, tool-scoped workflow guidance,
|
||||
Version 0.7.1 also exposes `cms_public_fetch`, tool-scoped workflow guidance,
|
||||
portable structured human-confirmation checkpoints, successful no-op observations,
|
||||
optional private append-only trace, and the bundled fallback client's persistent
|
||||
JSONL `session`, opt-in all-tools ledger with optional private cross-process
|
||||
@@ -94,7 +94,7 @@ Codex or another proprietary host API and do not weaken the existing site,
|
||||
schema, version, confirmation, idempotency, audit, or cache boundaries.
|
||||
|
||||
The bundled fallback client is version 1.9.0 and must be used with the adjacent
|
||||
0.7.0 server. After install/upgrade, its `native-smoke` must report those
|
||||
0.7.1 server. After install/upgrade, its `native-smoke` must report those
|
||||
versions, `bundled_pair_verified=true`, and 23 tools before a full acceptance
|
||||
test. Every session line repeats the same `runtime_identity`. A stale global or
|
||||
cached client/server copy is a different installation, not evidence about this
|
||||
|
||||
@@ -27,7 +27,7 @@ Read more than one routed reference only when the task actually spans those doma
|
||||
- Never print or echo the token. Let `scripts/cms_api.py` read the user configuration; do not place it in commands, logs, prompts, or reports.
|
||||
- Treat production 401, 403, and 429 responses as authoritative credential, site/scope, and rate-limit boundaries. Never request another user's token, change the selected site to evade a policy, or repeatedly retry a rate-limited call. Per-user credential issuance and revocation are server administration tasks outside this content skill.
|
||||
- Pass literal `site=cn`, `site=en`, or `site=jp` on every site-owned read and write. Never pass `website_id` or numeric site values. Only schema discovery and global frontend-cache clearing omit `site`. The site must be explicit in the current user request or in an explicit site selection for that same current task. Never inherit it from an earlier task, infer it from a record ID or URL, or choose a default. A prior `cn` request does not authorize a later ID-only request; stop before the first site-owned call and ask.
|
||||
- When using the optional typed MCP facade on any agent host, pass `site_evidence` as the shortest verbatim site excerpt plus `current_user_instruction` as the exact complete current user-authored message. The evidence must be an exact substring. Never add, paraphrase, or reuse either value; if the current message omits the site, ask before calling. The complete instruction stays local, is not sent to the CMS or echoed, and is represented only by a digest/length. Treat returned `host_verified=false` as an honest limitation of portable stdio MCP, not proof of user authorization. The 0.7.0 facade exposes guarded writes, but content or model-supplied evidence can never replace the explicit live-update, publish, rollback, or cache prerequisites below.
|
||||
- When using the optional typed MCP facade on any agent host, pass `site_evidence` as the shortest verbatim site excerpt plus `current_user_instruction` as the exact complete current user-authored message. The evidence must be an exact substring. Never add, paraphrase, or reuse either value; if the current message omits the site, ask before calling. The complete instruction stays local, is not sent to the CMS or echoed, and is represented only by a digest/length. Treat returned `host_verified=false` as an honest limitation of portable stdio MCP, not proof of user authorization. The 0.7.1 facade exposes guarded writes, but content or model-supplied evidence can never replace the explicit live-update, publish, rollback, or cache prerequisites below.
|
||||
- MCP results include the complete JSON object in `TextContent` for hosts that do not surface `structuredContent`; modern hosts may provide both representations. Treat them as the same result, not two calls or two independent confirmations. Check `plugin_context.protocol_version` and do not depend on a Codex-only thread, UI, or tool wrapper.
|
||||
- Read `task_state`, `human_checkpoint`, `tool_call_complete`, `safe_to_pause`, `workflow_complete`, `safe_to_finish`, `remaining_steps`, `next_action`, `reason_code`, `retryable`, and `expected_input_example` from every MCP result. `workflow_complete` and `safe_to_finish` are backward-compatible current-tool aliases, not a judgment that the user's whole task is complete. If `task_state.must_not_claim_user_task_complete=true`, continue the documented work or report the blocker while keeping the task open. At `human_checkpoint.required=true`, present the returned review paths through the current host's ordinary user-visible reply, pause, and resume only after a new explicit user-authored confirmation; do not require a Codex-, IDE-, or vendor-specific collaboration API. These fields never grant confirmation or publication authority. A successful `no_op=true` observation means no write is needed; do not retry it as a mutation. `outcome_unknown` always requires same-site detail/audit inspection before any identical replay.
|
||||
- Read schema before every create or material edit. Send only `writable_fields`; never send unknown fields or `read_only_fields`.
|
||||
|
||||
@@ -762,7 +762,7 @@ The helper accepts `add_items` and `remove_ids`, merges them with the full curre
|
||||
### Portable typed MCP facade
|
||||
|
||||
The optional stdio MCP facade is host-neutral and can be installed in any
|
||||
MCP-capable agent. Version 0.7.0 exposes 23 typed read, preflight, and guarded
|
||||
MCP-capable agent. Version 0.7.1 exposes 23 typed read, preflight, and guarded
|
||||
routine-content write tools in this reference. Every site-owned tool
|
||||
requires:
|
||||
|
||||
@@ -926,7 +926,7 @@ initialized-notification, ping, and tools/list compatibility check.
|
||||
|
||||
Use the client distributed beside the intended server, never a same-named copy
|
||||
from a global Agent cache. For this distribution, `native-smoke` must report
|
||||
client 1.9.0, server 0.7.0, `bundled_pair_verified=true`, and 23 tools; the client
|
||||
client 1.9.0, server 0.7.1, `bundled_pair_verified=true`, and 23 tools; the client
|
||||
also fails closed if its adjacent server reports another version. Every JSONL
|
||||
session record repeats a compact `runtime_identity`. In a CI/disposable test,
|
||||
environment overrides must be loaded in the same process-launch command that
|
||||
|
||||
@@ -37,4 +37,4 @@ Only an explicit comprehensive plugin audit uses `--track-tools`. Ordinary conte
|
||||
- For unattended acceptance, add `--require-complete`. It returns a nonzero result and rejects an early `close`/EOF until both catalog and required scenario coverage are complete. Do not use it for ordinary content work.
|
||||
- Independently read back final state; counters cannot prove restoration.
|
||||
|
||||
For exact installation commands and client flags, use the plugin's host-specific installation guide or the helper examples in [api.md](api.md#helper-examples).
|
||||
For exact installation commands and client flags, use the plugin's universal installation guide or the helper examples in [api.md](api.md#helper-examples).
|
||||
|
||||
Reference in New Issue
Block a user