huaweicloud-devkit 1.1.8-next.2 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -344,6 +344,54 @@ Configures HTTP/HTTPS proxy for connections to Huawei Cloud services (e.g. enter
344
344
 
345
345
  Environment variables `HTTPS_PROXY`, `HTTP_PROXY`, and `NO_PROXY` take precedence over the file configuration — use `proxy show` to inspect the effective settings.
346
346
 
347
+ ## Telemetry
348
+
349
+ HuaweiCloud DevKit collects **anonymous** telemetry to understand usage patterns and improve the plugin. Telemetry is **enabled by default**.
350
+
351
+ ### What We Collect
352
+
353
+ - **Installation ID** — a SHA-256 hash derived from a machine fingerprint (hostname + MAC address + OS type + home directory). The raw values are never sent or stored.
354
+ - **User hash** — a server-generated user identifier hash (produced by the Huawei Cloud backend `check-user` / `generatorUserIDHash` API). It is cached locally and sent with every event; it is not derived from any local PII.
355
+ - **Plugin version** — the installed `huaweicloud-devkit` version.
356
+ - **Agent harness** — which AI coding agent is running (e.g. `opencode`, `codex`, `codearts`).
357
+ - **Agent version** — the agent's version string.
358
+ - **OS type and version** — e.g. `Linux`, `Darwin`, `Windows_NT`.
359
+ - **Capability** — how the event was triggered: `mcp` (MCP tool invocation) or `cli` (CLI command), derived from the event key.
360
+ - **Event type** — what happened, e.g. `plugin:install`, `plugin:first_use`, `dau:active_today` (daily active), `tool:<tool_name>`, `skill:retrieve`, `sandbox:connect`.
361
+ - **Event value** — a short sanitized string (max 255 chars, newlines/tabs stripped). For tool events this is typically `1`; for skill retrieval it is the skill name.
362
+
363
+ ### What We Do **Not** Collect
364
+
365
+ - No AK/SK, access keys, or any credentials
366
+ - No user input, prompts, or code content
367
+ - No IP addresses
368
+ - No personally identifiable information (PII)
369
+ - No file paths or project names
370
+
371
+ ### How Data Is Transmitted
372
+
373
+ Events are queued in memory and sent in batches (up to 100 events per batch) via HTTP POST to Huawei Cloud's telemetry endpoint every 60 seconds. Transmission respects your proxy configuration. Failed requests are retried up to 3 times, then dropped.
374
+
375
+ ### Disable Telemetry (Opt-Out)
376
+
377
+ Set the environment variable:
378
+
379
+ ```bash
380
+ export HUAWEICLOUD_DEVKIT_TELEMETRY=off
381
+ ```
382
+
383
+ When set to `off`, no telemetry events are collected or transmitted.
384
+
385
+ ### Debug Mode
386
+
387
+ To log telemetry activity for troubleshooting:
388
+
389
+ ```bash
390
+ export HUAWEICLOUD_DEVKIT_DEBUG=true
391
+ ```
392
+
393
+ Debug logs are written to the plugin's `telemetry/telemetry-debug.log` file.
394
+
347
395
  ## What It Does
348
396
 
349
397
  - **Guided cloud operations** — agents get step-by-step guidance for 20+ commonly used Huawei Cloud services (ECS, OBS, VPC, RDS, GaussDB, FunctionGraph, APIG, CCE, and more)
package/README.zh-CN.md CHANGED
@@ -338,6 +338,54 @@ npx --yes huaweicloud-devkit proxy clear # 删除代理配置
338
338
 
339
339
  环境变量 `HTTPS_PROXY`、`HTTP_PROXY`、`NO_PROXY` 优先级高于文件配置——可使用 `proxy show` 查看最终生效的设置。
340
340
 
341
+ ## 遥测
342
+
343
+ HuaweiCloud DevKit 收集**匿名**遥测数据,以了解使用模式并改进插件。遥测**默认开启**。
344
+
345
+ ### 收集哪些数据
346
+
347
+ - **安装 ID** — 基于机器指纹(主机名 + MAC 地址 + OS 类型 + home 目录)生成的 SHA-256 哈希。原始值不会被发送或存储。
348
+ - **用户哈希** — 服务端生成的用户标识哈希(由华为云后端 `check-user` / `generatorUserIDHash` API 生成)。本地缓存后随每次事件发送,非本地 PII 派生。
349
+ - **插件版本** — 已安装的 `huaweicloud-devkit` 版本。
350
+ - **Agent harness** — 正在运行的 AI 编码助手(如 `opencode`、`codex`、`codearts`)。
351
+ - **Agent 版本** — Agent 版本号。
352
+ - **OS 类型和版本** — 如 `Linux`、`Darwin`、`Windows_NT`。
353
+ - **Capability** — 事件触发方式:`mcp`(MCP 工具调用)或 `cli`(CLI 命令),由事件 key 派生。
354
+ - **事件类型** — 发生了什么,如 `plugin:install`(安装)、`plugin:first_use`(首次使用)、`dau:active_today`(日活)、`tool:<工具名>`(工具调用)、`skill:retrieve`(技能加载)、`sandbox:connect`(沙箱连接)。
355
+ - **事件值** — 简短的脱敏字符串(最长 255 字符,换行符/制表符已清除)。工具事件通常为 `1`;技能加载事件为技能名称。
356
+
357
+ ### **不**收集哪些数据
358
+
359
+ - 不收集 AK/SK、访问密钥或任何凭据
360
+ - 不收集用户输入、提示词或代码内容
361
+ - 不收集 IP 地址
362
+ - 不收集个人可识别信息(PII)
363
+ - 不收集文件路径或项目名称
364
+
365
+ ### 数据如何传输
366
+
367
+ 事件先在内存中排队,然后每 60 秒批量(每批最多 100 条)通过 HTTP POST 发送到华为云遥测端点。传输过程遵循你的代理配置。发送失败最多重试 3 次后丢弃。
368
+
369
+ ### 禁用遥测(Opt-Out)
370
+
371
+ 设置环境变量:
372
+
373
+ ```bash
374
+ export HUAWEICLOUD_DEVKIT_TELEMETRY=off
375
+ ```
376
+
377
+ 设置为 `off` 后,不会收集或传输任何遥测事件。
378
+
379
+ ### 调试模式
380
+
381
+ 如需记录遥测活动以供排查:
382
+
383
+ ```bash
384
+ export HUAWEICLOUD_DEVKIT_DEBUG=true
385
+ ```
386
+
387
+ 调试日志写入插件的 `telemetry/telemetry-debug.log` 文件。
388
+
341
389
  ## 功能特性
342
390
 
343
391
  - **引导式云操作** — Agent 获得 20+ 常用华为云服务的分步操作指引(ECS、OBS、VPC、RDS、GaussDB、FunctionGraph、APIG、CCE 等)
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { createRequire } from 'node:module';
4
5
 
5
6
  const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
6
7
 
@@ -98,19 +99,91 @@ function getHooks() {
98
99
  };
99
100
  }
100
101
 
101
- // ── Export: function-as-object serves both IDE and CLI ────────
102
- //
103
- // IDE : plugin.default() -> returns hooks
104
- // CLI : plugin.default.server -> returns hooks (via { id, server })
102
+ // ── V2 setup: register hooks on their owning domains via the context. ──────
103
+ // `tool.execute.before` → ctx.tool.hook("execute.before", ...) whose single
104
+ // event carries `.tool` (name) and `.input` (args), replacing V1's (input, output).
105
+ // `event` → ctx.event.subscribe(AsyncIterable of { type, properties }).
106
+ async function setupV2(ctx) {
107
+ await ctx.tool.hook('execute.before', (event) => {
108
+ try {
109
+ debugLog(`HOOK tool.execute.before tool=${event?.tool}`);
110
+ if (event?.tool === 'skill') {
111
+ const name = event?.input?.name ?? event?.args?.name;
112
+ debugLog(`SKILL name=${name}`);
113
+ if (isHuaweiCloudSkill(name)) {
114
+ writeEvent('skill:retrieve', name);
115
+ debugLog(`SKILL TRACKED: ${name}`);
116
+ }
117
+ return;
118
+ }
119
+ if (event?.tool === 'bash') {
120
+ const cmd = event?.input?.command ?? event?.args?.command ?? '';
121
+ if (!cmd) return;
122
+ const result = classifyHcloud(cmd);
123
+ if (result) writeEvent(result.key, result.value, { capability: 'cli' });
124
+ }
125
+ } catch (error) {
126
+ debugLog(`HOOK ERROR: ${error?.message || error}`);
127
+ }
128
+ });
105
129
 
106
- function plugin() {
107
- debugLog('=== PLUGIN EXPORT CALLED ===');
108
- return getHooks();
130
+ const controller = new AbortController();
131
+ void (async () => {
132
+ try {
133
+ for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
134
+ if (event?.type === 'message.part.updated') {
135
+ const text = event?.properties?.part?.text;
136
+ if (typeof text === 'string') {
137
+ const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
138
+ if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
139
+ }
140
+ }
141
+ }
142
+ } catch (error) {
143
+ debugLog(`EVENT ERROR: ${error?.message || error}`);
144
+ }
145
+ })();
146
+
147
+ return () => controller.abort();
109
148
  }
110
149
 
111
- plugin.id = 'huaweicloud-skill-tracker';
112
- plugin.server = async () => getHooks();
150
+ // ── Dual export: one default object serves OpenCode V1 and V2 ───────────────
151
+ // V1 loader reads `server()` (returns legacy string-keyed hooks).
152
+ // V2 loader reads `id` + `setup(ctx)` (registers hooks on the context domains).
153
+ // `Plugin.define` is a type/runtime helper injected lazily so a V1 host that
154
+ // lacks `@opencode/plugin` still loads (fallback to an identity define).
155
+ let Plugin = { define: (def) => def };
156
+ try {
157
+ const require = createRequire(import.meta.url);
158
+ for (const pkg of ['@opencode/plugin', '@opencode-ai/plugin']) {
159
+ try {
160
+ const mod = require(pkg);
161
+ if (mod?.Plugin?.define) {
162
+ Plugin = mod.Plugin;
163
+ break;
164
+ }
165
+ if (typeof mod?.define === 'function') {
166
+ Plugin = mod;
167
+ break;
168
+ }
169
+ } catch {
170
+ // not installed in this host; keep the identity define
171
+ }
172
+ }
173
+ } catch {
174
+ // createRequire unavailable; keep the identity define
175
+ }
113
176
 
114
- export default plugin;
177
+ export default {
178
+ ...Plugin.define({
179
+ id: 'huaweicloud-skill-tracker',
180
+ async setup(ctx) {
181
+ return setupV2(ctx);
182
+ },
183
+ }),
184
+ async server() {
185
+ return getHooks();
186
+ },
187
+ };
115
188
 
116
189
  debugLog('=== PLUGIN INIT DONE ===');
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "_note": "Merge this into ~/.config/opencode/opencode.jsonc. Replace the path with the absolute path to your mcp-server.mjs installation.",
3
+ "_note": "Merge this into ~/.config/opencode/opencode.jsonc (OpenCode 2.x). Replace the path with the absolute path to your mcp-server.mjs installation. For OpenCode 1.x, place the same entry directly under \"mcp\" (without the \"servers\" level) and add \"enabled\": true.",
4
4
  "mcp": {
5
- "huaweicloud-devkit": {
6
- "type": "local",
7
- "command": ["node", "<绝对路径>/plugins/huaweicloud-core/src/mcp-server.mjs"],
8
- "enabled": true
5
+ "servers": {
6
+ "huaweicloud-devkit": {
7
+ "type": "local",
8
+ "command": ["node", "<绝对路径>/plugins/huaweicloud-core/src/mcp-server.mjs"]
9
+ }
9
10
  }
10
11
  }
11
12
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
3
  "mcpName": "io.github.huaweicloud/huaweicloud-devkit",
4
- "version": "1.1.8-next.2",
4
+ "version": "1.1.8",
5
5
  "kooCliVersion": "7.2.12",
6
6
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
7
7
  "type": "module",
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.8-next.2",
20
+ "version": "1.1.8",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
22
22
  "skills": "./skills/",
23
- "version": "1.1.8-next.2",
23
+ "version": "1.1.8",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -17,7 +17,7 @@
17
17
  "mcpServers": "./.mcp.json",
18
18
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
19
19
  "skills": "./skills/",
20
- "version": "1.1.8-next.2",
20
+ "version": "1.1.8",
21
21
  "author": {
22
22
  "name": "HuaweiCloud Mate",
23
23
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.8-next.2",
3
+ "version": "1.1.8",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.1.8-next.2",
3
+ "version": "1.1.8",
4
4
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
5
5
  "author": {
6
6
  "name": "HuaweiCloud Mate",
@@ -2,7 +2,7 @@
2
2
  "name": "huaweicloud-devkit",
3
3
  "id": "huaweicloud-devkit",
4
4
  "displayName": "HuaweiCloud DevKit",
5
- "version": "1.1.8-next.2",
5
+ "version": "1.1.8",
6
6
  "family": "bundle-plugin",
7
7
  "bundleFormat": "codex",
8
8
  "description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
@@ -1,12 +1,16 @@
1
1
  ---
2
2
  name: huawei-voucher
3
- description: '查询和领取华为云激励代金券(一人一次)。当用户首次使用、开始会话、询问插件能力,或提到"领券/代金券/优惠券/激励金/领取/福利"等话题时,先调 voucher_status 查领取状态:未领取(claimed=false)则主动提示可领取一张代金券并询问是否领取;已领取(claimed=true)或状态不明确则静默不打扰。Triggers: 首次使用, 会话开始, voucher, coupon, incentive, 领券, 代金券, 优惠券, 激励金, 领取, 福利. NOT for: 账单/费用查询(用 huawei-billing)。'
4
- version: 1
3
+ description: '查询和领取华为云激励代金券(一人一次);领取华为开发者大赛·小红书skill创新赛专属代金券(需已完成赛事报名)。激励代金券:当用户首次使用、开始会话、询问插件能力,或提到"领券/代金券/优惠券/激励金/领取/福利"等话题时,先调 voucher_status 查领取状态:未领取(claimed=false)则主动提示可领取一张代金券并询问是否领取;已领取(claimed=true)或状态不明确则静默不打扰。赛事代金券:仅在用户明确要求领取大赛代金券时调用 competition_voucher_claim,不要主动提示。Triggers: 首次使用, 会话开始, voucher, coupon, incentive, 领券, 代金券, 优惠券, 激励金, 领取, 福利, 华为开发者大赛, 小红书, skill创新赛, 赛事代金券, 大赛代金券, 比赛领券. NOT for: 账单/费用查询(用 huawei-billing)。'
4
+ version: 2
5
5
  ---
6
6
 
7
- # 华为云激励代金券
7
+ # 华为云代金券
8
8
 
9
- ## 主动提示准则(核心)
9
+ 覆盖两类活动:**激励代金券**(通用,一人一次)与**赛事代金券**(华为开发者大赛·小红书skill创新赛,需已完成赛事报名)。两者相互独立,注意区分调用对应工具。
10
+
11
+ ## 激励代金券(一人一次)
12
+
13
+ ### 主动提示准则(核心)
10
14
 
11
15
  此能力仅在用户尚未领取时才有价值,需避免已领取用户被反复打扰。以下场景默认执行"先查后提示":会话开始、能力导读、首次对话、提到福利领取类话题。
12
16
 
@@ -48,12 +52,40 @@ version: 1
48
52
  | `发券失败: <原文>` | 其他错误 | 原样转达,不展开 |
49
53
  | `code=HDKIT_CRED_*` / 含「未配置 / not configured / AK/SK 无效 / 限额」 | 凭证未配置、无效或达限额 | 引导用户检查并配置 AK/SK |
50
54
 
55
+ ## 赛事代金券(华为开发者大赛·小红书skill创新赛)
56
+
57
+ ### 1. 适用场景与调用时机
58
+
59
+ 用户参加华为开发者大赛·小红书skill创新赛,说出类似"帮我领取华为开发者大赛·小红书skill创新赛华为云代金券"、"领取小红书skill创新赛的券"时,调用 `huaweicloud_competition_voucher_claim`。
60
+
61
+ **与激励代金券的关键区别**:赛事代金券**不做主动提示**——不要在会话开始、能力导读等场景主动推荐;仅当用户明确提及该赛事并要求领取时才调用。
62
+
63
+ ### 2. 调用
64
+
65
+ 调用 `huaweicloud_competition_voucher_claim`(生产环境无需 `domain_id`,后端从 IAM 自动解析账号;测试环境需传 `domain_id`)。
66
+
67
+ 后端自动完成:赛事报名校验 → 是否已领取校验 → 发券。
68
+
69
+ ### 3. 结果解读(错误码 UX)
70
+
71
+ | 返回 message / code 特征 | 含义 | 对用户表述 |
72
+ | ----------------------------------------------------------------------- | --------------------------- | --------------------------------- |
73
+ | `领取成功` | 发券成功 | 告知券已到账(含 voucherId/金额) |
74
+ | `code=HDKIT_NOT_SIGNED_UP`(您尚未报名…) | 未报名该赛事 | 告知需先完成赛事报名后再来领取 |
75
+ | `已领取过` / `已领取` | 该账号已领过赛事券 | 告知已领取,不打扰 |
76
+ | `code=HDKIT_COMPETITION_NOT_FOUND`(赛事不存在或已下线) | 赛事配置异常 | 告知赛事暂不可用,稍后再试 |
77
+ | `本月代金券总额度已用完,所有账号均无法领取` | 全局额度用尽(HD.60630042) | 告知下月再试 |
78
+ | `请先完成实名认证` | 账号未实名(HD.60630022) | 引导去控制台实名认证 |
79
+ | `code=HDKIT_CRED_*` / 含「未配置 / not configured / AK/SK 无效 / 限额」 | 凭证未配置、无效或达限额 | 引导用户检查并配置 AK/SK |
80
+ | 其他(如 `赛事报名查询失败`、`发券失败: <原文>`) | 服务端错误 | 原样转达,建议稍后重试 |
81
+
51
82
  ## 环境注意
52
83
 
53
- - 生产环境:`voucher_status` / `voucher_claim` 无需传 `domain_id`,后端从 IAM 自动解析账号。
84
+ - 生产环境:`voucher_status` / `voucher_claim` / `competition_voucher_claim` 无需传 `domain_id`,后端从 IAM 自动解析账号。
54
85
  - 测试环境:需传 `domain_id`(华为云账号 ID);未传时返回 `测试环境需提供 domain_id`。
55
86
 
56
87
  ## 注意事项
57
88
 
58
- - 每个账号(domain_id)仅可领取一次,重复调用返回已领取。
59
- - 领取结果以激励服务返回为准,不要凭本地认知猜测。
89
+ - 激励代金券:每个账号(domain_id)仅可领取一次,重复调用返回已领取。
90
+ - 赛事代金券与激励代金券是**两个独立活动**,领取互不影响;用户问普通代金券时不要调赛事工具,反之亦然。
91
+ - 领取/报名结果以服务端返回为准,不要凭本地认知猜测。
@@ -52,7 +52,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
52
52
  | troubleshoot | Troubleshooting Routing | error, bug, failed, AccessDenied, quota | Handoff to huaweicloud-troubleshooting |
53
53
  | api-sdk | API/SDK Routing | API call, SDK, REST, integration, code example | Handoff to huaweicloud-api-and-sdk |
54
54
  | report-issue | Issue Reporting | wrong service, you picked wrong, incorrect | Read references/report-issue.md |
55
- | voucher | Voucher Routing | 领券, 代金券, 优惠券, 激励金, 领取, voucher, coupon, incentive | Handoff to huawei-voucher |
55
+ | voucher | Voucher Routing | 领券, 代金券, 优惠券, 激励金, 领取, voucher, coupon, incentive, 华为开发者大赛, 小红书, skill创新赛, 大赛代金券, 赛事代金券, 比赛领券 | Handoff to huawei-voucher |
56
56
 
57
57
  ## Service Map
58
58
 
@@ -81,6 +81,7 @@ Do not rely on training data for facts. Huawei Cloud services, pricing, quotas,
81
81
  | Temporary runtime / web app preview | Sandbox (DevStation) | huawei-sandbox |
82
82
  | Getting started | Account setup | huaweicloud-cli-and-auth |
83
83
  | Incentive voucher | Voucher | huawei-voucher |
84
+ | Competition voucher (小红书skill创新赛) | Voucher | huawei-voucher |
84
85
 
85
86
  **Web app scenario layering**: for "deploy a web app", prefer `huawei-sandbox` for free/quick try (hello world, prototype, demo, temporary preview — temporary runtime + public URL (runtime lifetime not exposed via API); credential expiry see `expiresAt` from `connect`/`credentials`, zero billed resources); route to `huawei-functiongraph` / `huawei-ecs` (or `huawei-cce`) for production / long-term / custom-domain / high-availability hosting. When the chosen target requires **multiple purchased resources** (e.g. VPC + subnet + ECS + EIP + RDS, or OBS + CDN + DNS) or the user asks for an architecture/cost proposal first, hand off to `huawei-iac` for orchestrated provisioning with a mandatory cost gate.
86
87
 
@@ -40,7 +40,8 @@ function readJsonSafe(path) {
40
40
  function opencodeRegistered() {
41
41
  const path = opencodeConfigFile();
42
42
  const cfg = readJsonSafe(path);
43
- return Boolean(cfg?.mcp?.['huaweicloud-devkit']);
43
+ // V2 native (`mcp.servers.*`) or V1 legacy (`mcp.*`) form.
44
+ return Boolean(cfg?.mcp?.['huaweicloud-devkit'] || cfg?.mcp?.servers?.['huaweicloud-devkit']);
44
45
  }
45
46
 
46
47
  function codexDesktopRegistered() {
@@ -32,28 +32,76 @@ function withDefaultTimeout(entry, defaultTimeout) {
32
32
  return entry;
33
33
  }
34
34
 
35
- // OpenCode style entry: { type, command: ['node', mcpPath, ...userArgs], ... }
36
- export function mergeCommandStyle(existing, { mcpPath, defaultTimeout = 300000 } = {}) {
35
+ // OpenCode style entry: { type, command: ['node', mcpPath, ...userArgs], ... }.
36
+ // era 'v1' writes the legacy shape (enabled + flat timeout); era 'v2' writes the
37
+ // native V2 shape (no flat `enabled`/`timeout` — V2 uses `disabled` and a nested
38
+ // timeout object, so both are left to OpenCode defaults unless the user set them).
39
+ export function mergeCommandStyle(existing, { mcpPath, defaultTimeout = 300000, era = 'v1' } = {}) {
40
+ const v2 = era === 'v2';
41
+ const freshDefault = () =>
42
+ v2
43
+ ? { type: 'local', command: ['node', mcpPath] }
44
+ : withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout);
45
+
37
46
  if (!isPlainObject(existing)) {
38
- return {
39
- entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
40
- changed: true,
41
- };
47
+ return { entry: freshDefault(), changed: true };
42
48
  }
43
49
  const commandIsNodeStyle = Array.isArray(existing.command) && existing.command[0] === 'node';
44
50
  if (!commandIsNodeStyle) {
45
51
  // Foreign wrapper entry: keep current overwrite behavior.
46
- return {
47
- entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
48
- changed: true,
49
- };
52
+ return { entry: freshDefault(), changed: true };
50
53
  }
51
54
  const merged = { ...existing, type: 'local', command: ['node', mcpPath, ...existing.command.slice(2)] };
52
- if (merged.timeout === undefined && defaultTimeout) merged.timeout = defaultTimeout;
53
- if (merged.enabled === undefined) merged.enabled = true;
55
+ if (v2) {
56
+ // Native V2 shape: drop V1-only flat `enabled`/`timeout`; `disabled`,
57
+ // `environment`, and any nested timeout object are preserved via the spread.
58
+ delete merged.enabled;
59
+ if (typeof merged.timeout !== 'object' || merged.timeout === null || Array.isArray(merged.timeout)) {
60
+ delete merged.timeout;
61
+ }
62
+ } else {
63
+ if (merged.timeout === undefined && defaultTimeout) merged.timeout = defaultTimeout;
64
+ if (merged.enabled === undefined) merged.enabled = true;
65
+ }
54
66
  return { entry: merged, changed: !sameJson(merged, existing) };
55
67
  }
56
68
 
69
+ // Classify an existing OpenCode config by which MCP form the DevKit entry uses:
70
+ // `mcp.servers.*` (native V2) vs a flat `mcp.<name>` (legacy V1). Returns 'unknown'
71
+ // when the config has no MCP section or no recognizable server entry.
72
+ export function detectOpenCodeEra(config) {
73
+ if (!isPlainObject(config) || !isPlainObject(config.mcp)) return 'unknown';
74
+ if (isPlainObject(config.mcp.servers)) return 'v2';
75
+ if (Object.keys(config.mcp).some((key) => key !== 'servers' && isPlainObject(config.mcp[key]))) return 'v1';
76
+ return 'unknown';
77
+ }
78
+
79
+ // Place (or merge) the DevKit entry into an OpenCode config at the correct level
80
+ // for the target era. Returns the new config plus whether an entry pre-existed and
81
+ // whether anything changed, so callers can skip writes and restore backup deltas.
82
+ export function mergeOpenCodeEntry(config, { mcpPath, era = 'v1' } = {}) {
83
+ const base = isPlainObject(config) ? config : {};
84
+ const key = 'huaweicloud-devkit';
85
+ const next = { ...base, mcp: { ...(isPlainObject(base.mcp) ? base.mcp : {}) } };
86
+
87
+ if (era === 'v2') {
88
+ const servers = isPlainObject(next.mcp.servers) ? { ...next.mcp.servers } : {};
89
+ // An entry may live in the native `servers` map or the legacy flat map; merge
90
+ // from whichever is present so user fields survive an era switch.
91
+ const existing = servers[key] ?? next.mcp[key];
92
+ const { entry } = mergeCommandStyle(existing, { mcpPath, era: 'v2' });
93
+ servers[key] = entry;
94
+ if (isPlainObject(next.mcp[key])) delete next.mcp[key];
95
+ next.mcp.servers = servers;
96
+ return { config: next, entry, changed: !existing || !sameJson(entry, existing), existing: Boolean(existing) };
97
+ }
98
+
99
+ const existing = next.mcp[key];
100
+ const { entry, changed } = mergeCommandStyle(existing, { mcpPath, era: 'v1' });
101
+ next.mcp[key] = entry;
102
+ return { config: next, entry, changed: !existing || changed, existing: Boolean(existing) };
103
+ }
104
+
57
105
  // WorkBuddy/AtomCode/CodeArts style entry: { command: 'node', args: [mcpPath, ...userArgs], env, ... }
58
106
  export function mergeArgsStyle(existing, { mcpPath, env = {}, defaultTimeout = 300000 } = {}) {
59
107
  if (!isPlainObject(existing)) {
@@ -105,17 +153,19 @@ export function extractUserDelta(entry, style) {
105
153
  } else {
106
154
  return null;
107
155
  }
108
- if (isPlainObject(entry.env)) {
109
- const userEnv = Object.fromEntries(Object.entries(entry.env).filter(([key]) => !REQUIRED_ENV_KEYS.has(key)));
156
+ const env = isPlainObject(entry.env) ? entry.env : isPlainObject(entry.environment) ? entry.environment : null;
157
+ if (env) {
158
+ const userEnv = Object.fromEntries(Object.entries(env).filter(([key]) => !REQUIRED_ENV_KEYS.has(key)));
110
159
  if (Object.keys(userEnv).length > 0) delta.env = userEnv;
111
160
  }
112
161
  if (entry.timeout !== undefined && entry.timeout !== 300000) delta.timeout = entry.timeout;
113
- if (entry.enabled === false) delta.enabled = false;
162
+ // Canonical "disabled" marker: V1 uses `enabled:false`, V2 uses `disabled:true`.
163
+ if (entry.enabled === false || entry.disabled === true) delta.enabled = false;
114
164
  return Object.keys(delta).length > 0 ? delta : null;
115
165
  }
116
166
 
117
167
  // Apply a previously saved delta onto a freshly written default entry.
118
- export function applyUserDelta(entry, delta, style) {
168
+ export function applyUserDelta(entry, delta, style, era = 'v1') {
119
169
  if (!isPlainObject(entry) || !isPlainObject(delta)) return entry;
120
170
  const merged = { ...entry };
121
171
  if (style === 'command' && Array.isArray(delta.commandExtra)) {
@@ -124,10 +174,24 @@ export function applyUserDelta(entry, delta, style) {
124
174
  merged.args = [...entry.args, ...delta.argsExtra];
125
175
  }
126
176
  if (isPlainObject(delta.env)) {
127
- merged.env = { ...(isPlainObject(entry.env) ? entry.env : {}), ...delta.env };
177
+ // V2 native entries use `environment`; V1 (and args-style agents) use `env`.
178
+ if (style === 'command' && era === 'v2') {
179
+ merged.environment = { ...(isPlainObject(merged.environment) ? merged.environment : {}), ...delta.env };
180
+ delete merged.env;
181
+ } else {
182
+ merged.env = { ...(isPlainObject(merged.env) ? merged.env : {}), ...delta.env };
183
+ }
128
184
  }
129
185
  if (delta.timeout !== undefined) merged.timeout = delta.timeout;
130
- if (delta.enabled === false) merged.enabled = false;
186
+ if (delta.enabled === false) {
187
+ // Canonical disabled marker → era-appropriate field.
188
+ if (style === 'command' && era === 'v2') {
189
+ merged.disabled = true;
190
+ delete merged.enabled;
191
+ } else {
192
+ merged.enabled = false;
193
+ }
194
+ }
131
195
  return merged;
132
196
  }
133
197
 
@@ -147,3 +147,17 @@ export async function hdkitVoucherClaim(domainId) {
147
147
  };
148
148
  }
149
149
  }
150
+
151
+ export async function hdkitCompetitionVoucherClaim(domainId) {
152
+ try {
153
+ const body = domainId ? { domain_id: domainId } : {};
154
+ return await hdkitRequest('POST', 'competition/voucher/claim', body);
155
+ } catch (error) {
156
+ return {
157
+ claimed: false,
158
+ message: error?.message || 'Competition voucher service unavailable, please try again later',
159
+ code: error?.code,
160
+ ...(error?.remediation ? { remediation: error.remediation } : {}),
161
+ };
162
+ }
163
+ }
@@ -43,6 +43,8 @@ import {
43
43
  extractUserDelta,
44
44
  applyUserDelta,
45
45
  inheritPeerUserEnv,
46
+ detectOpenCodeEra,
47
+ mergeOpenCodeEntry,
46
48
  } from './mcp-config-merge.mjs';
47
49
  import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
48
50
  import { isUsableOfficeaceRoot, readOfficeaceRootMarker, writeOfficeaceRootMarker } from './officeace-paths.mjs';
@@ -646,39 +648,103 @@ function removeIfExists(p) {
646
648
  return false;
647
649
  }
648
650
 
651
+ // ── OpenCode era detection ─────────────────────────────────────────────────
652
+ // OpenCode 2.x reads MCP servers from `mcp.servers.<name>` (native); OpenCode
653
+ // 0.x/1.x read from a flat `mcp.<name>`. We resolve the target form in this order:
654
+ // 1. `--opencode-mcp-era v1|v2` explicit override
655
+ // 2. `opencode --version` major (2 → v2, 0/1 → v1)
656
+ // 3. the existing config's shape (`mcp.servers` → v2, flat `mcp.*` → v1)
657
+ // 4. default `v2` (native, future-proof)
658
+ function detectOpenCodeMajor() {
659
+ const attempts = process.platform === 'win32' ? [false, true] : [false]; // .cmd shims need a shell on Windows
660
+ for (const shell of attempts) {
661
+ try {
662
+ const r = spawnSync('opencode', ['--version'], { shell, windowsHide: true, stdio: 'pipe', timeout: 10000 });
663
+ const out = `${r.stdout || ''}${r.stderr || ''}`.trim();
664
+ const m = out.match(/v?(\d+)\.\d+/);
665
+ if (m) return Number(m[1]);
666
+ } catch {
667
+ // ignore and try the next attempt
668
+ }
669
+ }
670
+ return null;
671
+ }
672
+
673
+ function parseOpenCodeEraFlag() {
674
+ const idx = process.argv.indexOf('--opencode-mcp-era');
675
+ if (idx < 0) return null;
676
+ const raw = (process.argv[idx + 1] || '').toLowerCase();
677
+ if (raw === 'v1' || raw === 'v2') return raw;
678
+ console.error(`Invalid --opencode-mcp-era value "${process.argv[idx + 1] || ''}" (expected v1|v2).`);
679
+ process.exit(1);
680
+ }
681
+
682
+ let resolvedOpenCodeEra = null;
683
+ function resolveOpenCodeEra() {
684
+ if (resolvedOpenCodeEra) return resolvedOpenCodeEra;
685
+
686
+ const flagged = parseOpenCodeEraFlag();
687
+ if (flagged) return (resolvedOpenCodeEra = flagged);
688
+
689
+ const major = detectOpenCodeMajor();
690
+ if (major === 2) return (resolvedOpenCodeEra = 'v2');
691
+ if (major === 0 || major === 1) return (resolvedOpenCodeEra = 'v1');
692
+
693
+ const configPath = opencodeConfigFile();
694
+ if (existsSync(configPath)) {
695
+ try {
696
+ const era = detectOpenCodeEra(JSON.parse(readFileSync(configPath, 'utf8')));
697
+ if (era !== 'unknown') return (resolvedOpenCodeEra = era);
698
+ } catch {
699
+ // fall through to default
700
+ }
701
+ }
702
+
703
+ return (resolvedOpenCodeEra = 'v2');
704
+ }
705
+
649
706
  function updateOpenCodeConfig(pluginDir) {
650
707
  const configPath = opencodeConfigFile();
651
708
  const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
709
+ const era = resolveOpenCodeEra();
710
+ const formLabel = era === 'v2' ? 'mcp.servers.huaweicloud-devkit' : 'mcp.huaweicloud-devkit';
652
711
  let config = {};
653
712
  if (existsSync(configPath)) {
654
713
  try {
655
714
  config = JSON.parse(readFileSync(configPath, 'utf8'));
656
715
  } catch {
657
716
  console.log(
658
- ` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath} (jsonc comments?). Skipping MCP config write; ensure "mcp.huaweicloud-devkit" points to ${mcpPath}.`,
717
+ ` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath} (jsonc comments?). Skipping MCP config write; ensure "${formLabel}" points to ${mcpPath}.`,
659
718
  );
660
719
  return;
661
720
  }
662
- const existing = config.mcp?.['huaweicloud-devkit'];
663
- const { entry, changed } = mergeCommandStyle(existing, { mcpPath });
664
- if (existing && !changed) {
665
- console.log(` OpenCode MCP config unchanged: ${configPath}`);
666
- return;
667
- }
668
- if (existing && changed) {
669
- config.mcp['huaweicloud-devkit'] = entry;
670
- writeMcpSettingsFile(configPath, config);
671
- console.log(` OpenCode MCP config merged (user fields preserved): ${configPath}`);
672
- return;
721
+ }
722
+
723
+ const { config: next, existing, changed } = mergeOpenCodeEntry(config, { mcpPath, era });
724
+
725
+ // Restore user fields saved by a previous uninstall (issue #615), on fresh install only.
726
+ if (!existing) {
727
+ const delta = takeAgentDelta('opencode');
728
+ if (delta) {
729
+ const key = 'huaweicloud-devkit';
730
+ const entry = era === 'v2' ? next.mcp.servers[key] : next.mcp[key];
731
+ const restored = applyUserDelta(entry, delta, 'command', era);
732
+ if (era === 'v2') next.mcp.servers[key] = restored;
733
+ else next.mcp[key] = restored;
673
734
  }
674
735
  }
675
- config.mcp = config.mcp || {};
676
- config.mcp['huaweicloud-devkit'] = mergeCommandStyle(undefined, { mcpPath }).entry;
677
- // Restore user fields saved by a previous uninstall (issue #615).
678
- const delta = takeAgentDelta('opencode');
679
- if (delta) config.mcp['huaweicloud-devkit'] = applyUserDelta(config.mcp['huaweicloud-devkit'], delta, 'command');
680
- writeMcpSettingsFile(configPath, config);
681
- console.log(` OpenCode config updated: ${configPath}`);
736
+
737
+ if (existing && !changed) {
738
+ console.log(` OpenCode MCP config unchanged: ${configPath}`);
739
+ return;
740
+ }
741
+
742
+ writeMcpSettingsFile(configPath, next);
743
+ console.log(
744
+ existing
745
+ ? ` OpenCode MCP config merged (user fields preserved, ${era}): ${configPath}`
746
+ : ` OpenCode MCP config updated (${era}): ${configPath}`,
747
+ );
682
748
  }
683
749
 
684
750
  // Read-merge-write for pluginDir/.mcp.json (OpenClaw, Codex Desktop).
@@ -731,12 +797,22 @@ function removeOpenCodeConfig() {
731
797
  } catch {
732
798
  return;
733
799
  }
734
- if (!config.mcp?.['huaweicloud-devkit']) return;
800
+ const key = 'huaweicloud-devkit';
801
+ const flatEntry = config.mcp?.[key];
802
+ const serversEntry = config.mcp?.servers?.[key];
803
+ const entry = serversEntry || flatEntry;
804
+ if (!entry) return;
735
805
  // Back up user-customized fields before removal so a later reinstall can restore them (issue #615).
736
- const delta = extractUserDelta(config.mcp['huaweicloud-devkit'], 'command');
806
+ const delta = extractUserDelta(entry, 'command');
737
807
  if (delta) saveAgentDelta('opencode', delta);
738
- delete config.mcp['huaweicloud-devkit'];
739
- if (Object.keys(config.mcp).length === 0) delete config.mcp;
808
+ if (config.mcp) {
809
+ delete config.mcp[key];
810
+ if (config.mcp.servers) {
811
+ delete config.mcp.servers[key];
812
+ if (Object.keys(config.mcp.servers).length === 0) delete config.mcp.servers;
813
+ }
814
+ if (Object.keys(config.mcp).length === 0) delete config.mcp;
815
+ }
740
816
  writeMcpSettingsFile(configPath, config);
741
817
  console.log(` OpenCode MCP config cleaned: ${configPath}`);
742
818
  }
@@ -3180,7 +3256,11 @@ function opencodeStatus() {
3180
3256
  try {
3181
3257
  const config = JSON.parse(readFileSync(configPath, 'utf8'));
3182
3258
  console.log(
3183
- ` MCP config: ${config.mcp?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`,
3259
+ ` MCP config: ${
3260
+ config.mcp?.['huaweicloud-devkit'] || config.mcp?.servers?.['huaweicloud-devkit']
3261
+ ? '\x1b[32mConfigured\x1b[0m'
3262
+ : '\x1b[31mNot configured\x1b[0m'
3263
+ }`,
3184
3264
  );
3185
3265
  } catch {
3186
3266
  console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
@@ -4017,7 +4097,7 @@ async function cmdDoctor() {
4017
4097
  if (existsSync(opencodeCfg)) {
4018
4098
  try {
4019
4099
  const cfg = JSON.parse(readFileSync(opencodeCfg, 'utf8'));
4020
- if (cfg.mcp && cfg.mcp['huaweicloud-devkit']) {
4100
+ if (cfg.mcp && (cfg.mcp['huaweicloud-devkit'] || cfg.mcp.servers?.['huaweicloud-devkit'])) {
4021
4101
  mcpConfigured = true;
4022
4102
  mcpCfgTarget = 'OpenCode';
4023
4103
  }
@@ -5188,6 +5268,9 @@ async function main() {
5188
5268
  ' --target Target agent: opencode (default), codex, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
5189
5269
  );
5190
5270
  console.log(' --version Print CLI version and installed plugin version per agent');
5271
+ console.log(
5272
+ ' --opencode-mcp-era v1|v2 Force OpenCode MCP config form (v1 legacy flat, v2 native mcp.servers)',
5273
+ );
5191
5274
  console.log(' --clean-kocli (with: uninstall --target all) also remove KooCLI');
5192
5275
  console.log(' --clean-obs (with: uninstall --target all) also remove OBS config');
5193
5276
  console.log(' --clean-global (with: uninstall --target all) also remove KooCLI + OBS config');
@@ -31,6 +31,7 @@ import {
31
31
  hdkitCredentials,
32
32
  hdkitVoucherStatus,
33
33
  hdkitVoucherClaim,
34
+ hdkitCompetitionVoucherClaim,
34
35
  hdkitGenerateUserHash,
35
36
  } from './sandbox/hdkitservice-api.mjs';
36
37
  import { getCredentials } from './sandbox/hwlink-api.mjs';
@@ -856,6 +857,20 @@ export const TOOL_DEFINITIONS = [
856
857
  },
857
858
  },
858
859
  },
860
+ {
861
+ name: 'huaweicloud_competition_voucher_claim',
862
+ description:
863
+ '领取华为开发者大赛·小红书skill创新赛专属华为云代金券。前置条件:用户已完成该赛事报名(后端自动校验,未报名返回 code=HDKIT_NOT_SIGNED_UP)。仅当用户明确要求领取该赛事代金券时调用,不要主动提示。重复领取会返回已领取。',
864
+ inputSchema: {
865
+ type: 'object',
866
+ properties: {
867
+ domain_id: {
868
+ type: 'string',
869
+ description: 'Optional. Leave empty in production — account is resolved from IAM automatically.',
870
+ },
871
+ },
872
+ },
873
+ },
859
874
  {
860
875
  name: 'huaweicloud_check_update',
861
876
  description:
@@ -1570,6 +1585,8 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1570
1585
  return await hdkitVoucherStatus(args.domain_id);
1571
1586
  case 'huaweicloud_voucher_claim':
1572
1587
  return await hdkitVoucherClaim(args.domain_id);
1588
+ case 'huaweicloud_competition_voucher_claim':
1589
+ return await hdkitCompetitionVoucherClaim(args.domain_id);
1573
1590
  case 'huaweicloud_check_update':
1574
1591
  return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
1575
1592
  case 'huaweicloud_upgrade':