huaweicloud-devkit 1.1.0-next.8 → 1.1.0

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.
Files changed (35) hide show
  1. package/README.md +46 -2
  2. package/README.zh-CN.md +45 -2
  3. package/package.json +3 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -12
  10. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  11. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
  12. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
  13. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
  16. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
  17. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
  18. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +291 -53
  19. package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
  20. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
  21. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
  22. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
  23. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
  24. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
  25. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
  26. package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
  27. package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
  28. package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
  29. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
  30. package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
  31. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
  32. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +365 -11
  33. package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
  34. package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
  35. package/plugins/huaweicloud-core/src/tools.mjs +184 -14
@@ -18,19 +18,19 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
18
18
 
19
19
  ## Critical Warnings
20
20
 
21
- | Trap | Why |
22
- | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | VPC CIDR cannot change | Once set, VPC CIDR block is immutable |
24
- | Security group stateful | SG rules are stateful. Return traffic auto-allowed |
25
- | Network ACL stateless | ACL rules must allow both inbound AND outbound |
26
- | EIP bills when idle | Unbound EIP still charges. Release when unused |
27
- | Subnet AZ binding | Subnet tied to single AZ. Cross-AZ needs multiple subnets |
28
- | EIP PER type needs `--bandwidth.name` | PER bandwidth requires explicit name; `--help` marks it optional but it's required |
29
- | **VPC params need nested prefix** | KooCLI 7.x VPC API uses `--vpc.<param>`, `--subnet.<param>`, `--security_group.<param>`. Example: `--vpc.name=xxx` NOT `--name=xxx` |
30
- | **Security group needs no vpc_id** | VPC v3 API `CreateSecurityGroup` does NOT accept `vpc_id`. Security groups are region-level, not VPC-bound |
31
- | Subnet DNS empty → ECS no DNS | DNS params (`--subnet.primary_dns`, `--subnet.secondary_dns`) marked optional but empty default breaks cloud-init domain resolution — `yum`/`apt` installs fail silently. Always set both. Common DNS IPs: cn-north-4 (100.125.1.250 / 100.125.1.251), cn-north-1 (100.125.1.250 / 100.125.129.250), cn-east-3 (100.125.1.250 / 100.125.129.250), ap-southeast-3 (100.125.1.250 / 100.125.128.250) |
32
- | SCP blocks 0.0.0.0/0 SG rules | If `CreateSecurityGroupRule` with `--remote_ip_prefix=0.0.0.0/0` fails with `SYS.0403`, an org-level SCP policy is denying wide-open rules. Narrow to a specific CIDR range (e.g., your office IP) instead |
33
- | **VPC tags use `*` separator** | VPC tag format: `--vpc.tags.1=env*test` (asterisk between key and value). NOT `--vpc.tags.1.key=env` (ECS-style). This is different from ECS `--server.metadata.key=value` |
21
+ | Trap | Why |
22
+ | ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | VPC CIDR cannot change | Once set, VPC CIDR block is immutable |
24
+ | Security group stateful | SG rules are stateful. Return traffic auto-allowed |
25
+ | Network ACL stateless | ACL rules must allow both inbound AND outbound |
26
+ | EIP bills when idle | Unbound EIP still charges. Release when unused |
27
+ | Subnet AZ binding | Subnet tied to single AZ. Cross-AZ needs multiple subnets |
28
+ | EIP PER type needs `--bandwidth.name` | PER bandwidth requires explicit name; `--help` marks it optional but it's required |
29
+ | **VPC params need nested prefix** | KooCLI 7.x VPC API uses `--vpc.<param>`, `--subnet.<param>`, `--security_group.<param>`. Example: `--vpc.name=xxx` NOT `--name=xxx` |
30
+ | **Security group needs no vpc_id** | VPC v3 API `CreateSecurityGroup` does NOT accept `vpc_id`. Security groups are region-level, not VPC-bound |
31
+ | Subnet DNS empty → ECS no DNS | DNS params (`--subnet.primary_dns`, `--subnet.secondary_dns`) marked optional but empty default breaks cloud-init domain resolution — `yum`/`apt` installs fail silently. Always set both. Common DNS (primary / secondary): cn-north-4 (100.125.1.250 / 100.125.129.250), cn-north-1 (100.125.1.250 / 100.125.21.250), cn-east-3 (100.125.1.250 / 100.125.64.250), cn-south-1 (100.125.1.250 / 100.125.136.29), ap-southeast-3 (100.125.1.250 / 100.125.128.250). Authoritative per region: `hcloud DNS ListNameServers --type=private --region=<r>` |
32
+ | SCP blocks 0.0.0.0/0 SG rules | If `CreateSecurityGroupRule` with `--remote_ip_prefix=0.0.0.0/0` fails with `SYS.0403`, an org-level SCP policy is denying wide-open rules. Narrow to a specific CIDR range (e.g., your office IP) instead |
33
+ | **VPC tags use `*` separator** | VPC tag format: `--vpc.tags.1=env*test` (asterisk between key and value). NOT `--vpc.tags.1.key=env` (ECS-style). This is different from ECS `--server.metadata.key=value` |
34
34
 
35
35
  ## Common Workflows
36
36
 
@@ -43,25 +43,25 @@ Domain expertise for Huawei Cloud Virtual Private Cloud (VPC). Covers VPC/subnet
43
43
  | SG rule | hcloud VPC CreateSecurityGroupRule --security_group_rule.security_group_id=<id> --security_group_rule.direction=<direction> --security_group_rule.protocol=<protocol> --security_group_rule.multiport=<port> --security_group_rule.remote_ip_prefix=<cidr> | references/security-group.md |
44
44
  | Create EIP | hcloud EIP CreatePublicip --publicip.type=<type> --bandwidth.size=<size> --bandwidth.share_type=<share-type> --bandwidth.name=<name> | Run `hcloud EIP CreatePublicip --help` to confirm valid type values per region |
45
45
  | Bind EIP to ECS | hcloud EIP AssociatePublicips --publicip_id=<id> --publicip.associate_instance_id=<port-id> --publicip.associate_instance_type=PORT | Get port ID from `hcloud ECS ListServersDetails --server_id=<id>` → `OS-EXT-IPS:port_id` |
46
- | Unbind EIP | hcloud EIP DisassociatePublicip --publicip_id=<id> | references/eip.md |
46
+ | Unbind EIP | hcloud EIP DisassociatePublicips --publicip_id=<id> | references/eip.md |
47
47
  | Delete EIP | hcloud EIP DeletePublicip --publicip_id=<id> | references/eip.md |
48
48
  | List EIPs | hcloud EIP ListPublicips | |
49
49
  | NAT gateway | hcloud NAT CreateNatGateway --nat.name=<name> --nat.spec=<spec> --router_id=<vpc-id> --internal_network_id=<subnet-id> | Run `hcloud NAT CreateNatGateway --help` for available spec values |
50
50
 
51
51
  ## Troubleshooting
52
52
 
53
- | Error | Root Cause -> Fix |
54
- | -------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
- | Cannot reach instance | SG missing rule or no EIP -> Add SG rule / Bind EIP |
56
- | Subnet CIDR conflict | Overlapping with existing subnets -> Choose non-overlapping CIDR |
57
- | NAT gateway no internet | Route table missing default route -> Add 0.0.0.0/0 via NAT |
58
- | EIP quota exceeded | Check actual quota: `hcloud EIP ListPublicips --cli-region=<r>` to see current usage. Default varies by account (typically 5-10) |
59
- | EIP.7905 | Run `hcloud EIP ListPublicips --cli-region=<r>` first to check current usage |
60
- | VPC.0301: Bandwidth name invalid | PER type requires `--bandwidth.name`, even though `--help` marks it optional |
61
- | EIP has no public IP after binding | May need AddIngressEipV2 for ELB-type resources (see huawei-apig) |
62
- | ECS cloud-init fails silently (port 80/443 closed) | Subnet likely has no DNS. Check `hcloud VPC ShowSubnet --subnet_id=<id>` → `dnsList` empty? Rebuild subnet with `--subnet.primary_dns=<dns1> --subnet.secondary_dns=<dns2>`. DNS addresses per region: `hcloud VPC CreateSubnet --help` |
63
- | VPC.0209: Subnet still used | Subnet has dependent resources (ECS/RDS) — delete instances first, then subnet |
64
- | SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction. If SSH is blocked, bootstrap via cloud-init user_data instead: see `huawei-ecs` — no SSH needed |
53
+ | Error | Root Cause -> Fix |
54
+ | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | Cannot reach instance | SG missing rule or no EIP -> Add SG rule / Bind EIP |
56
+ | Subnet CIDR conflict | Overlapping with existing subnets -> Choose non-overlapping CIDR |
57
+ | NAT gateway no internet | Route table missing default route -> Add 0.0.0.0/0 via NAT |
58
+ | EIP quota exceeded | Check actual quota: `hcloud EIP ListPublicips --cli-region=<r>` to see current usage. Default varies by account (typically 5-10) |
59
+ | EIP.7905 | Run `hcloud EIP ListPublicips --cli-region=<r>` first to check current usage |
60
+ | VPC.0301: Bandwidth name invalid | PER type requires `--bandwidth.name`, even though `--help` marks it optional |
61
+ | EIP has no public IP after binding | May need AddIngressEipV2 for ELB-type resources (see huawei-apig) |
62
+ | ECS cloud-init fails silently (port 80/443 closed) | Subnet likely has no DNS. Check `hcloud VPC ShowSubnet --subnet_id=<id>` → `dnsList` empty? Rebuild subnet with `--subnet.primary_dns=<dns1> --subnet.secondary_dns=<dns2>`. DNS addresses per region: `hcloud DNS ListNameServers --type=private --region=<r>` |
63
+ | VPC.0209: Subnet still used | Subnet has dependent resources (ECS/RDS) — delete instances first, then subnet |
64
+ | SYS.0403 / SCP deny | Service Control Policy explicitly denies this operation — contact org admin to adjust SCP, or use an account/region without the restriction. If SSH is blocked, bootstrap via cloud-init user_data instead: see `huawei-ecs` — no SSH needed |
65
65
 
66
66
  ## Security Considerations
67
67
 
@@ -6,11 +6,17 @@ Subnets without DNS configuration will fail cloud-init. Use these DNS addresses:
6
6
 
7
7
  | Region | Primary DNS | Secondary DNS |
8
8
  | -------------- | ------------- | --------------- |
9
- | cn-north-1 | 100.125.1.250 | 100.125.129.250 |
9
+ | cn-north-1 | 100.125.1.250 | 100.125.21.250 |
10
10
  | cn-north-4 | 100.125.1.250 | 100.125.129.250 |
11
- | cn-east-3 | 100.125.1.250 | 100.125.129.250 |
12
- | cn-south-1 | 100.125.1.250 | 100.125.129.250 |
13
- | ap-southeast-3 | 100.125.1.250 | 100.125.129.250 |
11
+ | cn-east-3 | 100.125.1.250 | 100.125.64.250 |
12
+ | cn-south-1 | 100.125.1.250 | 100.125.136.29 |
13
+ | ap-southeast-3 | 100.125.1.250 | 100.125.128.250 |
14
+
15
+ Get the authoritative values for any region at runtime (preferred over this table):
16
+
17
+ ```bash
18
+ hcloud DNS ListNameServers --type=private --region=<region>
19
+ ```
14
20
 
15
21
  **Subnet create with DNS**:
16
22
 
@@ -21,8 +27,8 @@ hcloud VPC CreateSubnet \
21
27
  --subnet.cidr=10.50.1.0/24 \
22
28
  --subnet.gateway_ip=10.50.1.1 \
23
29
  --subnet.availability_zone=<az> \
24
- --subnet.dnsList.1=100.125.1.250 \
25
- --subnet.dnsList.2=100.125.129.250
30
+ --subnet.dnsList.1=<dns1> \
31
+ --subnet.dnsList.2=<dns2>
26
32
  ```
27
33
 
28
34
  ## Nested Prefix Summary
@@ -29,8 +29,8 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
29
29
 
30
30
  | Task | Operation |
31
31
  | ------------------- | --------------------------------------------------------------- |
32
- | List WAF instances | `ListInstances --cli-region=<r> --project_id=<p>` |
33
- | List policies | `ListPolicies --cli-region=<r> --project_id=<p>` |
32
+ | List WAF instances | `ListCompositeHosts --cli-region=<r> --project_id=<p>` |
33
+ | List policies | `ListPolicy --cli-region=<r> --project_id=<p>` |
34
34
  | Create custom rule | `BatchCreateCustomRule --cli-region=<r> --project_id=<p>` |
35
35
  | Create IP blacklist | `BatchCreateWhiteblackipRule --cli-region=<r> --project_id=<p>` |
36
36
  | Create CC rule | `BatchCreateCcRule --cli-region=<r> --project_id=<p>` |
@@ -39,8 +39,11 @@ Domain expertise for WAF and Anti-DDoS. Covers WAF policy/rules, AAD protection,
39
39
  ### AAD
40
40
 
41
41
  ```bash
42
- hcloud AAD ListInstances --cli-region=<r> --project_id=<p>
43
- hcloud AAD CreateInstance --cli-region=<r> --project_id=<p>
42
+ # List AAD instances (requires --cli-region=cn-north-4 or ap-southeast-1)
43
+ hcloud AAD ListInstance --cli-region=<r> --project_id=<p>
44
+
45
+ # List DDoS attack events
46
+ hcloud AAD ListDDoSAttackEvent --cli-region=<r> --project_id=<p>
44
47
  ```
45
48
 
46
49
  ## Troubleshooting
@@ -125,13 +125,13 @@ hcloud <Service> <Op> --cli-debug=true
125
125
 
126
126
  Credentials are resolved in this order (highest priority first):
127
127
 
128
- | Priority | Source | Mechanism | Persistence |
129
- | -------- | ----------------------- | ---------------------------------------------------------- | ------------------------------- |
130
- | 1 | Runtime credentials | `huaweicloud_auth_init` tool | Memory (cleared on MCP restart) |
131
- | 2 | Environment variables | `HW_ACCESS_KEY` / `HW_SECRET_KEY` | MCP process lifetime |
132
- | 3 | CodeArts project config | `.codeartsdoer/mcp/mcp_settings.json` (project, then user) | File |
133
- | 4 | Global config file | `~/.config/huaweicloud/credentials.json` | Permanent |
134
- | 5 | KooCLI profile | `~/.hcloud/config.json` (KooCLI only) | Permanent |
128
+ | Priority | Source | Mechanism | Persistence |
129
+ | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------- |
130
+ | 1 | Runtime credentials | `huaweicloud_auth_init` tool | Memory (cleared on MCP restart) |
131
+ | 2 | Environment variables | `HW_ACCESS_KEY` / `HW_SECRET_KEY` | MCP process lifetime |
132
+ | 3 | CodeArts / CodeArts Work | `.codeartsdoer/mcp/mcp_settings.json` (project → user) or `.codeartswork/mcp/mcp_settings.json` (user) | File |
133
+ | 4 | Global config file | `~/.config/huaweicloud/credentials.json` | Permanent |
134
+ | 5 | KooCLI profile | `~/.hcloud/config.json` (KooCLI only) | Permanent |
135
135
 
136
136
  When switching accounts within the same Agent session, use `huaweicloud_auth_init` to set runtime credentials. This overrides all other sources for the current MCP process.
137
137
 
@@ -9,6 +9,7 @@ export const SUPPORTED_AGENT_TARGETS = [
9
9
  'codex',
10
10
  'codex-desktop',
11
11
  'codearts',
12
+ 'codearts-work',
12
13
  'workbuddy',
13
14
  'dsh',
14
15
  'officeace',
@@ -78,6 +79,12 @@ function codeartsRegistered() {
78
79
  });
79
80
  }
80
81
 
82
+ function codeartsWorkRegistered() {
83
+ const path = join(baseHome(), '.codeartswork', 'mcp', 'mcp_settings.json');
84
+ const cfg = readJsonSafe(path);
85
+ return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
86
+ }
87
+
81
88
  function workbuddyRegistered() {
82
89
  const cfg = readJsonSafe(join(baseHome(), '.workbuddy', 'mcp.json'));
83
90
  return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
@@ -179,6 +186,11 @@ function atomcodeRegistered() {
179
186
  return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
180
187
  }
181
188
 
189
+ function openclawRegistered() {
190
+ const cfg = readJsonSafe(join(baseHome(), '.agents', 'huaweicloud-plugins', '.mcp.json'));
191
+ return Boolean(cfg?.mcpServers?.['huaweicloud-devkit']);
192
+ }
193
+
182
194
  function hermesHome() {
183
195
  if (process.env.HERMES_HOME) return process.env.HERMES_HOME;
184
196
  // Hermes on Windows stores under LOCALAPPDATA, not ~/.hermes
@@ -208,10 +220,12 @@ export function getAgentRegistrationStatuses(target = 'all') {
208
220
  if (agent === 'codex-desktop') configured = codexDesktopRegistered();
209
221
  if (agent === 'codex') configured = codexCliRegistered();
210
222
  if (agent === 'codearts') configured = codeartsRegistered();
223
+ if (agent === 'codearts-work') configured = codeartsWorkRegistered();
211
224
  if (agent === 'workbuddy') configured = workbuddyRegistered();
212
225
  if (agent === 'dsh') configured = dshRegistered();
213
226
  if (agent === 'officeace') configured = officeaceRegistered();
214
227
  if (agent === 'hermes') configured = hermesRegistered();
228
+ if (agent === 'openclaw') configured = openclawRegistered();
215
229
  if (agent === 'atomcode') configured = atomcodeRegistered();
216
230
  result.agents[agent] = { configured };
217
231
  }
@@ -123,7 +123,11 @@ export function getParentCwd() {
123
123
  }
124
124
 
125
125
  function isCodeArtsContext() {
126
- return existsSync(join(process.cwd(), '.codeartsdoer')) || existsSync(join(homedir(), '.codeartsdoer'));
126
+ return (
127
+ existsSync(join(process.cwd(), '.codeartsdoer')) ||
128
+ existsSync(join(homedir(), '.codeartsdoer')) ||
129
+ existsSync(join(homedir(), '.codeartswork'))
130
+ );
127
131
  }
128
132
 
129
133
  function readCodeArtsCredentials() {
@@ -154,6 +158,31 @@ function readCodeArtsCredentials() {
154
158
  }
155
159
  }
156
160
 
161
+ // CodeArts Work — user-level only
162
+ {
163
+ const path = join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
164
+ try {
165
+ if (existsSync(path)) {
166
+ const config = JSON.parse(readFileSync(path, 'utf8'));
167
+ const server = config?.mcpServers?.['huaweicloud-devkit'];
168
+ if (server?.env) {
169
+ const ak = server.env.HW_ACCESS_KEY;
170
+ const sk = server.env.HW_SECRET_KEY;
171
+ if (ak && sk) {
172
+ return {
173
+ ak,
174
+ sk,
175
+ securityToken: server.env.HW_SECURITY_TOKEN || '',
176
+ region: server.env.HW_REGION || server.env.HUAWEICLOUD_REGION || '',
177
+ };
178
+ }
179
+ }
180
+ }
181
+ } catch {
182
+ // mcp_settings.json missing or invalid — skip
183
+ }
184
+ }
185
+
157
186
  return null;
158
187
  }
159
188
 
@@ -54,9 +54,42 @@ export function syncAuth(target = 'all') {
54
54
  };
55
55
  }
56
56
 
57
+ let hcloud = { ok: false, message: 'KooCLI not installed' };
58
+ if (hcloudInstalled()) {
59
+ const bin = process.env.HCLOUD_BIN || 'hcloud';
60
+ const r = spawnSync(
61
+ bin,
62
+ [
63
+ 'configure',
64
+ 'set',
65
+ `--cli-access-key=${credentials.ak}`,
66
+ `--cli-secret-key=${credentials.sk}`,
67
+ `--cli-region=${credentials.region || ''}`,
68
+ ],
69
+ {
70
+ shell: false,
71
+ windowsHide: true,
72
+ stdio: 'pipe',
73
+ timeout: 30000,
74
+ },
75
+ );
76
+ if (r.status === 0) {
77
+ hcloud = { ok: true, message: 'KooCLI config synced' };
78
+ } else {
79
+ hcloud = {
80
+ ok: false,
81
+ message: 'KooCLI config sync failed',
82
+ error: String(r.stderr || '')
83
+ .trim()
84
+ .slice(0, 240),
85
+ };
86
+ }
87
+ }
88
+
57
89
  return {
58
90
  ok: true,
59
91
  obs: { configured: true, path: obs.path, endpoint: obs.endpoint },
92
+ hcloud,
60
93
  credentialsConfigured: true,
61
94
  agents: getAgentRegistrationStatuses(target).agents,
62
95
  note: 'OBS credentials were synced from the global credential vault. Agent MCP registration is managed by "npx huaweicloud-devkit install --target <agent>".',
@@ -157,10 +157,13 @@ function readPkg(projectPath) {
157
157
  }
158
158
  }
159
159
 
160
- function detectPackageManager(projectPath) {
160
+ function detectPackageManager(projectPath, pkg) {
161
161
  if (existsSync(join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm';
162
162
  if (existsSync(join(projectPath, 'yarn.lock'))) return 'yarn';
163
163
  if (existsSync(join(projectPath, 'package-lock.json'))) return 'npm';
164
+ if (existsSync(join(projectPath, 'bun.lockb'))) return 'bun';
165
+ const declared = (pkg?.packageManager || '').split('@')[0];
166
+ if (declared && ['pnpm', 'yarn', 'npm', 'bun'].includes(declared)) return declared;
164
167
  return 'npm';
165
168
  }
166
169
 
@@ -234,10 +237,24 @@ function frameworkResult(fw, pm, projectPath) {
234
237
  return patchCommands({ ...fw, packageManager: pm, rootDir: projectPath }, pm);
235
238
  }
236
239
 
240
+ function readVitepressOutDir(projectPath) {
241
+ const configFiles = ['config.mts', 'config.ts', 'config.mjs', 'config.js'];
242
+ for (const cf of configFiles) {
243
+ const configPath = join(projectPath, '.vitepress', cf);
244
+ if (!existsSync(configPath)) continue;
245
+ try {
246
+ const content = readFileSync(configPath, 'utf8');
247
+ const match = content.match(/outDir\s*:\s*['"]([^'"]+)['"]/);
248
+ if (match) return match[1];
249
+ } catch {}
250
+ }
251
+ return null;
252
+ }
253
+
237
254
  export function detectFramework(projectPath) {
238
255
  const pkg = readPkg(projectPath);
239
256
  const deps = collectDeps(pkg);
240
- const pm = detectPackageManager(projectPath);
257
+ const pm = detectPackageManager(projectPath, pkg);
241
258
 
242
259
  if (
243
260
  existsSync(join(projectPath, 'pnpm-workspace.yaml')) ||
@@ -287,7 +304,12 @@ export function detectFramework(projectPath) {
287
304
  }
288
305
 
289
306
  if (existsSync(join(projectPath, '.vitepress'))) {
290
- return frameworkResult(FRAMEWORKS.vitepress, pm, projectPath);
307
+ const outDir = readVitepressOutDir(projectPath);
308
+ return frameworkResult(
309
+ outDir ? { ...FRAMEWORKS.vitepress, outputDir: outDir } : FRAMEWORKS.vitepress,
310
+ pm,
311
+ projectPath,
312
+ );
291
313
  }
292
314
 
293
315
  if (
@@ -1,4 +1,8 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
2
6
 
3
7
  import { classifyHcloudArgs, redactSecrets, assertAllowed } from './safety-policy.mjs';
4
8
  import { getProxySettings } from './proxy/proxy-config.mjs';
@@ -6,6 +10,46 @@ import { getProxySettings } from './proxy/proxy-config.mjs';
6
10
  const DEFAULT_TIMEOUT_MS = 60_000;
7
11
  const DEFAULT_FORCE_KILL_AFTER_MS = 2_000;
8
12
  const DEFAULT_MAX_RETRIES = 1;
13
+ const APPROVAL_TTL_MS = 5 * 60_000;
14
+ const LARGE_OUTPUT_THRESHOLD = 50_000;
15
+ const OUTPUT_DIR = join('/tmp', 'huaweicloud-devkit');
16
+
17
+ const approvalStore = new Map();
18
+
19
+ export function createApprovalToken(rawArgs) {
20
+ const token = randomUUID();
21
+ approvalStore.set(token, { rawArgs, createdAt: Date.now() });
22
+ if (approvalStore.size % 20 === 0) {
23
+ const now = Date.now();
24
+ for (const [k, v] of approvalStore) {
25
+ if (now - v.createdAt > APPROVAL_TTL_MS) approvalStore.delete(k);
26
+ }
27
+ }
28
+ return token;
29
+ }
30
+
31
+ export function consumeApprovalToken(token) {
32
+ const entry = approvalStore.get(token);
33
+ if (!entry) return null;
34
+ if (Date.now() - entry.createdAt > APPROVAL_TTL_MS) {
35
+ approvalStore.delete(token);
36
+ return null;
37
+ }
38
+ approvalStore.delete(token);
39
+ return entry.rawArgs;
40
+ }
41
+
42
+ function saveLargeOutput(rawStdout) {
43
+ if (rawStdout.length <= LARGE_OUTPUT_THRESHOLD) return null;
44
+ try {
45
+ mkdirSync(OUTPUT_DIR, { recursive: true });
46
+ const filePath = join(OUTPUT_DIR, `output-${Date.now()}.json`);
47
+ writeFileSync(filePath, rawStdout, { encoding: 'utf8' });
48
+ return filePath;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
9
53
 
10
54
  export function planHcloudCommand(args, options = {}) {
11
55
  const normalizedArgs = Array.isArray(args) ? args.map(String) : [];
@@ -26,6 +70,7 @@ export function planHcloudCommand(args, options = {}) {
26
70
  executableBlock: redactOutput(command),
27
71
  warnings,
28
72
  classification,
73
+ approvalToken: createApprovalToken(normalizedArgs),
29
74
  safeToRun: classification.decision === 'allow',
30
75
  };
31
76
  }
@@ -53,12 +98,22 @@ export async function runHcloud(args, options = {}) {
53
98
  throw new Error('Unreachable retry state.');
54
99
  }
55
100
 
101
+ function discoverHcloudPath() {
102
+ if (process.env.HCLOUD_BIN && existsSync(process.env.HCLOUD_BIN)) return process.env.HCLOUD_BIN;
103
+ const candidates =
104
+ process.platform === 'win32'
105
+ ? [join(homedir(), 'hcloud', 'hcloud.exe')]
106
+ : [join(homedir(), '.local', 'bin', 'hcloud'), join(homedir(), 'hcloud', 'hcloud')];
107
+ return candidates.find((c) => existsSync(c)) || null;
108
+ }
109
+
56
110
  function runHcloudOnce(plan, options) {
57
111
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
58
112
  const forceKillAfterMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_AFTER_MS;
59
- const executable = options.executable || options.env?.HCLOUD_BIN || process.env.HCLOUD_BIN || 'hcloud';
113
+ const executable = options.executable || options.env?.HCLOUD_BIN || discoverHcloudPath() || 'hcloud';
60
114
  const executableArgs = Array.isArray(options.executableArgs) ? options.executableArgs.map(String) : [];
61
115
  const cwd = options.cwd || undefined;
116
+ const stdin = options.stdin ?? 'y\n';
62
117
 
63
118
  return new Promise((resolve) => {
64
119
  const proxySettings = getProxySettings();
@@ -78,11 +133,11 @@ function runHcloudOnce(plan, options) {
78
133
  ...options.env,
79
134
  },
80
135
  });
81
- if (options.stdin) {
82
- if (typeof options.stdin === 'function') {
83
- options.stdin(child.stdin);
136
+ if (stdin) {
137
+ if (typeof stdin === 'function') {
138
+ stdin(child.stdin);
84
139
  } else {
85
- child.stdin.write(String(options.stdin));
140
+ child.stdin.write(String(stdin));
86
141
  child.stdin.end();
87
142
  }
88
143
  }
@@ -99,6 +154,13 @@ function runHcloudOnce(plan, options) {
99
154
  clearTimeout(timer);
100
155
  clearTimeout(forceTimer);
101
156
  clearTimeout(settleTimer);
157
+ if (result.stdout && String(result.stdout).length > LARGE_OUTPUT_THRESHOLD) {
158
+ const outputFile = saveLargeOutput(stdout);
159
+ if (outputFile) {
160
+ result.outputFile = outputFile;
161
+ result.stdout = String(result.stdout).slice(0, 2000) + `\n...(truncated, full output saved to ${outputFile})`;
162
+ }
163
+ }
102
164
  resolve(result);
103
165
  }
104
166
 
@@ -49,6 +49,29 @@ for (const base of [pluginRoot, packageRoot]) {
49
49
  let buffer = Buffer.alloc(0);
50
50
  let useContentLengthFraming = true;
51
51
 
52
+ // Keep the event loop alive after stdin is closed (Windows Hermes workaround).
53
+ // Node.js exits when no active handles remain; the stdin 'data' listener is
54
+ // the only handle. On Windows, Hermes may close the stdin pipe after the
55
+ // initial handshake, causing the process to exit silently (exit 0).
56
+ //
57
+ // When stdin closes, start a keepalive timer. When stdout also closes (normal
58
+ // shutdown signal from OfficeAce or other agents), clear the timer and exit.
59
+ let keepAlive = null;
60
+ function onStdinClose() {
61
+ if (keepAlive) return;
62
+ keepAlive = setInterval(() => {}, 60000);
63
+ }
64
+ function onStdoutClose() {
65
+ if (keepAlive) {
66
+ clearInterval(keepAlive);
67
+ keepAlive = null;
68
+ }
69
+ process.exitCode = 0;
70
+ }
71
+ stdin.on('close', onStdinClose);
72
+ stdin.on('end', onStdinClose);
73
+ stdout.on('close', onStdoutClose);
74
+
52
75
  stdin.on('data', (chunk) => {
53
76
  buffer = Buffer.concat([buffer, chunk]);
54
77
  readFrames();
@@ -7,6 +7,12 @@ const HDKIT_BASE_URL =
7
7
  async function hdkitRequest(method, path, body, timeoutMs = 300000) {
8
8
  const { ak, sk, securitytoken } = getCredentials();
9
9
 
10
+ if (!ak || !sk) {
11
+ throw new Error(
12
+ 'Huawei Cloud credentials are not configured. ' +
13
+ 'Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
14
+ );
15
+ }
10
16
  const headers = {
11
17
  'Content-Type': 'application/json',
12
18
  'X-HW-AK': ak,
@@ -49,10 +55,12 @@ async function hdkitRequest(method, path, body, timeoutMs = 300000) {
49
55
  }
50
56
 
51
57
  if (!resp.ok) {
52
- const err = new Error(data.message || `hdkitservice error: ${data.code || resp.status}`);
53
- err.code = data.code;
58
+ const code = data.code || `HTTP_${resp.status}`;
59
+ const trace = data.traceId ? ` [trace: ${data.traceId}]` : '';
60
+ const err = new Error(`${code}: ${data.message || 'hdkitservice error'}${trace}`);
61
+ err.code = code;
54
62
  err.status = resp.status;
55
- err.traceId = data.traceId; // 后端实际返回驼峰 traceId
63
+ err.traceId = data.traceId;
56
64
  throw err;
57
65
  }
58
66