huaweicloud-devkit 1.1.4-next.4 → 1.1.4-next.6

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/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.4-next.4",
4
+ "version": "1.1.4-next.6",
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.4-next.4",
20
+ "version": "1.1.4-next.6",
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.4-next.4",
23
+ "version": "1.1.4-next.6",
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.4-next.4",
20
+ "version": "1.1.4-next.6",
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.4-next.4",
3
+ "version": "1.1.4-next.6",
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.4-next.4",
3
+ "version": "1.1.4-next.6",
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.4-next.4",
5
+ "version": "1.1.4-next.6",
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.",
@@ -41,7 +41,7 @@ Domain expertise for Huawei Cloud Object Storage Service (OBS). Covers bucket/ob
41
41
  | Three-layer permissions | IAM > Bucket Policy > ACL. Most restrictive wins |
42
42
  | Versioning is irreversible | Once enabled, cannot be disabled, only suspended |
43
43
  | OBS uses AK/SK directly | NOT IAM tokens. Auth errors mean check AK/SK validity |
44
- | Static website via CLI missing | KooCLI OBS lacks website config. Use REST API or console |
44
+ | Static website via CLI missing | KooCLI OBS lacks website config. Use `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally) or console |
45
45
  | **OBS needs separate cred config** | `hcloud configure` is NOT enough for OBS. Before any OBS operation, call `huaweicloud_setup_obs_config` to sync credentials from hcloud profile. |
46
46
  | **obsutil interactive prompts** | `cp`/`rm` without `-f` causes "Please input (y/n)" → Agent hangs (TIMEOUT). Always use `-f` for non-interactive. |
47
47
  | **Directory upload adds prefix** | `cp <dir>/ obs://<bucket>/ -r` puts files under `bucket/<dir>/...`. Use `-flat` for root-level files (static sites). Preview with `-dryRun` first. |
@@ -83,7 +83,7 @@ hcloud OBS config -e=<endpoint> -i=<AK> -k=<SK> -t=token
83
83
  See `references/static-website.md` for the full end-to-end workflow:
84
84
  Build → Create bucket → Upload → Set bucket ACL → Set object ACL → Configure website (REST API/console)
85
85
 
86
- > KooCLI OBS does NOT support `SetBucketWebsite`. Configure static website hosting via REST API (`PUT /?website`) or the Huawei Cloud console.
86
+ > KooCLI OBS does NOT support `SetBucketWebsite`. Use the `huaweicloud_obs_set_website_config` MCP tool (handles AWS4 signing internally, no manual signature needed) or the Huawei Cloud console.
87
87
 
88
88
  ## Single-File Quick Share
89
89
 
@@ -31,13 +31,9 @@ hcloud OBS chattri obs://my-static-site -acl=public-read
31
31
  hcloud OBS chattri obs://my-static-site/ -r -f -acl=public-read
32
32
 
33
33
  # 5. Configure static website hosting
34
- # KooCLI OBS does NOT support this. Use one of:
35
- # Option A — REST API:
36
- # PUT /?website HTTP/1.1
37
- # Host: my-static-site.obs.cn-north-4.myhuaweicloud.com
38
- # Body: {"IndexDocument": {"Suffix": "index.html"}, "ErrorDocument": {"Key": "error.html"}}
39
- # (requires AK/SK signature)
40
- # Option B — Huawei Cloud Console:
34
+ # KooCLI OBS does NOT support this. Use the MCP tool (handles AWS4 signing internally):
35
+ # action=set, bucket=<bucket>, region=<region>, indexDocument=index.html, errorDocument=404.html
36
+ # Or use Huawei Cloud Console:
41
37
  # Console → OBS → Bucket → Basic Settings → Static Website Hosting
42
38
 
43
39
  # 6. Verify
@@ -0,0 +1,70 @@
1
+ // Backup/restore of user-customized MCP config fields across uninstall/reinstall (issue #615).
2
+ // Uninstall saves the user delta keyed by agent; a later install/update consumes and applies it.
3
+ //
4
+ // Residual semantics: deltas are namespaced per agent and only that agent's
5
+ // next install/update consumes them (take-once). If the user never reinstalls
6
+ // an agent, its delta simply stays in the backup file — this is expected and
7
+ // harmless (no cross-agent contamination). `uninstall --target all` purges
8
+ // the whole file explicitly.
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+ import { homedir } from 'node:os';
13
+
14
+ export function mcpBackupFilePath(base = process.env.HUAWEICLOUD_HOME || homedir()) {
15
+ return join(base, '.config', 'huaweicloud', 'devkit-mcp-backup.json');
16
+ }
17
+
18
+ function readBackup(file) {
19
+ if (!existsSync(file)) return {};
20
+ try {
21
+ const data = JSON.parse(readFileSync(file, 'utf8'));
22
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
23
+ } catch {
24
+ return {};
25
+ }
26
+ }
27
+
28
+ export function readAgentDelta(agentKey, file = mcpBackupFilePath()) {
29
+ const delta = readBackup(file)[agentKey];
30
+ return delta && typeof delta === 'object' ? delta : null;
31
+ }
32
+
33
+ export function saveAgentDelta(agentKey, delta, file = mcpBackupFilePath()) {
34
+ if (!agentKey || !delta || typeof delta !== 'object') return false;
35
+ const backup = readBackup(file);
36
+ backup[agentKey] = { ...delta, savedAt: new Date().toISOString() };
37
+ try {
38
+ mkdirSync(dirname(file), { recursive: true });
39
+ writeFileSync(file, JSON.stringify(backup, null, 2));
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ // Consume the delta for an agent: returns it and removes it from the backup file.
47
+ export function takeAgentDelta(agentKey, file = mcpBackupFilePath()) {
48
+ const delta = readAgentDelta(agentKey, file);
49
+ if (!delta) return null;
50
+ const backup = readBackup(file);
51
+ delete backup[agentKey];
52
+ try {
53
+ if (Object.keys(backup).length === 0) {
54
+ rmSync(file, { force: true });
55
+ } else {
56
+ writeFileSync(file, JSON.stringify(backup, null, 2));
57
+ }
58
+ } catch {}
59
+ return delta;
60
+ }
61
+
62
+ export function purgeBackup(file = mcpBackupFilePath()) {
63
+ if (!existsSync(file)) return false;
64
+ try {
65
+ rmSync(file, { force: true });
66
+ return true;
67
+ } catch {
68
+ return false;
69
+ }
70
+ }
@@ -0,0 +1,132 @@
1
+ // Pure helpers for merging HuaweiCloud DevKit MCP config entries without
2
+ // dropping user-customized fields (extra command args, env, timeout, enabled).
3
+ // Program-owned fields (the node executable + mcp-server.mjs path, required env
4
+ // keys) are corrected by the installer; everything else belongs to the user.
5
+
6
+ function isPlainObject(value) {
7
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
8
+ }
9
+
10
+ function sameJson(a, b) {
11
+ return JSON.stringify(a) === JSON.stringify(b);
12
+ }
13
+
14
+ // Merge env maps: our required keys are only set when absent; user-set values
15
+ // always win. Returns null when nothing needs to change.
16
+ function mergeEnv(userEnv = {}, requiredEnv = {}) {
17
+ const merged = { ...userEnv };
18
+ let changed = false;
19
+ for (const [key, value] of Object.entries(requiredEnv)) {
20
+ if (merged[key] === undefined) {
21
+ merged[key] = value;
22
+ changed = true;
23
+ }
24
+ }
25
+ if (!changed && sameJson(merged, userEnv)) return null;
26
+ return merged;
27
+ }
28
+
29
+ function withDefaultTimeout(entry, defaultTimeout) {
30
+ // Pass null explicitly to omit the timeout field (defaults only apply to undefined).
31
+ if (defaultTimeout) entry.timeout = defaultTimeout;
32
+ return entry;
33
+ }
34
+
35
+ // OpenCode style entry: { type, command: ['node', mcpPath, ...userArgs], ... }
36
+ export function mergeCommandStyle(existing, { mcpPath, defaultTimeout = 300000 } = {}) {
37
+ if (!isPlainObject(existing)) {
38
+ return {
39
+ entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
40
+ changed: true,
41
+ };
42
+ }
43
+ const commandIsNodeStyle = Array.isArray(existing.command) && existing.command[0] === 'node';
44
+ if (!commandIsNodeStyle) {
45
+ // Foreign wrapper entry: keep current overwrite behavior.
46
+ return {
47
+ entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
48
+ changed: true,
49
+ };
50
+ }
51
+ 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;
54
+ return { entry: merged, changed: !sameJson(merged, existing) };
55
+ }
56
+
57
+ // WorkBuddy/AtomCode/CodeArts style entry: { command: 'node', args: [mcpPath, ...userArgs], env, ... }
58
+ export function mergeArgsStyle(existing, { mcpPath, env = {}, defaultTimeout = 300000 } = {}) {
59
+ if (!isPlainObject(existing)) {
60
+ return {
61
+ entry: withDefaultTimeout({ command: 'node', args: [mcpPath], env: { ...env } }, defaultTimeout),
62
+ changed: true,
63
+ };
64
+ }
65
+ const commandIsNodeStyle = existing.command === 'node';
66
+ if (!commandIsNodeStyle) {
67
+ return {
68
+ entry: withDefaultTimeout({ command: 'node', args: [mcpPath], env: { ...env } }, defaultTimeout),
69
+ changed: true,
70
+ };
71
+ }
72
+ const merged = {
73
+ ...existing,
74
+ command: 'node',
75
+ args: [mcpPath, ...(Array.isArray(existing.args) ? existing.args.slice(1) : [])],
76
+ };
77
+ const envMerged = mergeEnv(isPlainObject(existing.env) ? existing.env : {}, env);
78
+ if (envMerged) merged.env = envMerged;
79
+ if (merged.timeout === undefined && defaultTimeout) merged.timeout = defaultTimeout;
80
+ return { entry: merged, changed: !sameJson(merged, existing) };
81
+ }
82
+
83
+ // Codex Desktop / OpenClaw .mcp.json style: { mcpServers: { 'huaweicloud-devkit': entry } }
84
+ // Fresh entries keep the historical shape (no timeout field); user-set timeouts are preserved.
85
+ export function mergeMcpServersFile(config, { mcpPath, env = {} } = {}) {
86
+ const existing = isPlainObject(config) ? config.mcpServers?.['huaweicloud-devkit'] : undefined;
87
+ const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env, defaultTimeout: null });
88
+ const next = isPlainObject(config) ? { ...config } : {};
89
+ next.mcpServers = { ...(isPlainObject(config) ? config.mcpServers : {}), 'huaweicloud-devkit': entry };
90
+ return { entry, config: next, changed: changed || !isPlainObject(config) || !existing };
91
+ }
92
+
93
+ // Env keys the installer manages itself (recomputed on every install) — never
94
+ // treated as user assets for backup purposes.
95
+ const REQUIRED_ENV_KEYS = new Set(['HUAWEICLOUD_AGENT_TOOLKIT_MODE', 'HCLOUD_BIN']);
96
+
97
+ // Extract the user-owned delta of an entry for backup before uninstall removes it.
98
+ export function extractUserDelta(entry, style) {
99
+ if (!isPlainObject(entry)) return null;
100
+ const delta = {};
101
+ if (style === 'command') {
102
+ if (Array.isArray(entry.command) && entry.command.length > 2) delta.commandExtra = entry.command.slice(2);
103
+ } else if (style === 'args') {
104
+ if (Array.isArray(entry.args) && entry.args.length > 1) delta.argsExtra = entry.args.slice(1);
105
+ } else {
106
+ return null;
107
+ }
108
+ if (isPlainObject(entry.env)) {
109
+ const userEnv = Object.fromEntries(Object.entries(entry.env).filter(([key]) => !REQUIRED_ENV_KEYS.has(key)));
110
+ if (Object.keys(userEnv).length > 0) delta.env = userEnv;
111
+ }
112
+ if (entry.timeout !== undefined && entry.timeout !== 300000) delta.timeout = entry.timeout;
113
+ if (entry.enabled === false) delta.enabled = false;
114
+ return Object.keys(delta).length > 0 ? delta : null;
115
+ }
116
+
117
+ // Apply a previously saved delta onto a freshly written default entry.
118
+ export function applyUserDelta(entry, delta, style) {
119
+ if (!isPlainObject(entry) || !isPlainObject(delta)) return entry;
120
+ const merged = { ...entry };
121
+ if (style === 'command' && Array.isArray(delta.commandExtra)) {
122
+ merged.command = [...entry.command, ...delta.commandExtra];
123
+ } else if (style === 'args' && Array.isArray(delta.argsExtra)) {
124
+ merged.args = [...entry.args, ...delta.argsExtra];
125
+ }
126
+ if (isPlainObject(delta.env)) {
127
+ merged.env = { ...(isPlainObject(entry.env) ? entry.env : {}), ...delta.env };
128
+ }
129
+ if (delta.timeout !== undefined) merged.timeout = delta.timeout;
130
+ if (delta.enabled === false) merged.enabled = false;
131
+ return merged;
132
+ }
@@ -36,6 +36,8 @@ import {
36
36
  getProxySettings,
37
37
  } from './proxy/proxy-config.mjs';
38
38
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
39
+ import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
40
+ import { saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
39
41
  import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
40
42
  import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
41
43
  import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
@@ -261,15 +263,30 @@ function ensureOfficeaceMcpInSqlite() {
261
263
  const envJson = JSON.stringify(env);
262
264
 
263
265
  if (existing) {
264
- if (existing.command === 'node' && existing.args_json === argsJson && existing.env_json === envJson) {
266
+ // Merge: keep user's extra args/env values, only correct program-owned fields (issue #615).
267
+ let existingArgs = [];
268
+ let existingEnv = [];
269
+ try {
270
+ existingArgs = JSON.parse(existing.args_json || '[]');
271
+ existingEnv = JSON.parse(existing.env_json || '[]');
272
+ } catch {}
273
+ const userArgs = Array.isArray(existingArgs) ? existingArgs.slice(1) : [];
274
+ // Start from the user's env and add our required keys only when absent (user values win).
275
+ const envMerged = [...(Array.isArray(existingEnv) ? existingEnv : [])];
276
+ for (const e of env) {
277
+ if (e && e.key && !envMerged.some((x) => x && x.key === e.key)) envMerged.push(e);
278
+ }
279
+ const nextArgsJson = JSON.stringify([mcpPath, ...userArgs]);
280
+ const nextEnvJson = JSON.stringify(envMerged);
281
+ if (existing.command === 'node' && existing.args_json === nextArgsJson && existing.env_json === nextEnvJson) {
265
282
  console.log(` MCP config unchanged: ${dbPath}`);
266
283
  db.close();
267
284
  return true;
268
285
  }
269
286
  db.prepare(
270
287
  'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
271
- ).run('node', argsJson, envJson, now, 'disconnected', existing.id);
272
- console.log(` MCP config updated: ${dbPath}`);
288
+ ).run('node', nextArgsJson, nextEnvJson, now, 'disconnected', existing.id);
289
+ console.log(` MCP config merged (user fields preserved): ${dbPath}`);
273
290
  } else {
274
291
  const ownerUserId = officeaceGetOwnerUserId();
275
292
  if (!ownerUserId) {
@@ -277,10 +294,16 @@ function ensureOfficeaceMcpInSqlite() {
277
294
  db.close();
278
295
  return false;
279
296
  }
297
+ // Restore user args saved by a previous uninstall (issue #615).
298
+ const officeaceDelta = takeAgentDelta('officeace');
299
+ const insertArgsJson =
300
+ officeaceDelta && Array.isArray(officeaceDelta.argsExtra)
301
+ ? JSON.stringify([mcpPath, ...officeaceDelta.argsExtra])
302
+ : argsJson;
280
303
  db.prepare(
281
304
  `INSERT INTO mcp_connectors (id, owner_user_id, type, name, normalized_name, transport, timeout_ms, command, args_json, env_json, enabled, status, created_at, updated_at, version, seeded)
282
305
  VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
283
- ).run(randomUUID(), ownerUserId, argsJson, envJson, now, now);
306
+ ).run(randomUUID(), ownerUserId, insertArgsJson, envJson, now, now);
284
307
  console.log(` MCP config created: ${dbPath}`);
285
308
  }
286
309
  db.close();
@@ -302,6 +325,14 @@ function removeOfficeaceMcpFromSqlite() {
302
325
  let db;
303
326
  try {
304
327
  db = openOfficeaceDb();
328
+ // Back up user args before deleting the connector row (issue #615).
329
+ try {
330
+ const row = db.prepare("SELECT args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").get();
331
+ const parsed = row ? JSON.parse(row.args_json || '[]') : [];
332
+ if (Array.isArray(parsed) && parsed.length > 1) {
333
+ saveAgentDelta('officeace', { argsExtra: parsed.slice(1) });
334
+ }
335
+ } catch {}
305
336
  db.prepare(
306
337
  "DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
307
338
  ).run();
@@ -558,29 +589,68 @@ function updateOpenCodeConfig(pluginDir) {
558
589
  return;
559
590
  }
560
591
  const existing = config.mcp?.['huaweicloud-devkit'];
561
- if (
562
- existing &&
563
- existing.type === 'local' &&
564
- Array.isArray(existing.command) &&
565
- existing.command[0] === 'node' &&
566
- existing.command[1] === mcpPath &&
567
- existing.timeout === 300000
568
- ) {
592
+ const { entry, changed } = mergeCommandStyle(existing, { mcpPath });
593
+ if (existing && !changed) {
569
594
  console.log(` OpenCode MCP config unchanged: ${configPath}`);
570
595
  return;
571
596
  }
597
+ if (existing && changed) {
598
+ config.mcp['huaweicloud-devkit'] = entry;
599
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
600
+ console.log(` OpenCode MCP config merged (user fields preserved): ${configPath}`);
601
+ return;
602
+ }
572
603
  }
573
604
  config.mcp = config.mcp || {};
574
- config.mcp['huaweicloud-devkit'] = {
575
- type: 'local',
576
- command: ['node', mcpPath],
577
- enabled: true,
578
- timeout: 300000,
579
- };
605
+ config.mcp['huaweicloud-devkit'] = mergeCommandStyle(undefined, { mcpPath }).entry;
606
+ // Restore user fields saved by a previous uninstall (issue #615).
607
+ const delta = takeAgentDelta('opencode');
608
+ if (delta) config.mcp['huaweicloud-devkit'] = applyUserDelta(config.mcp['huaweicloud-devkit'], delta, 'command');
580
609
  writeFileSync(configPath, JSON.stringify(config, null, 2));
581
610
  console.log(` OpenCode config updated: ${configPath}`);
582
611
  }
583
612
 
613
+ // Read-merge-write for pluginDir/.mcp.json (OpenClaw, Codex Desktop).
614
+ // Preserves user extra args/env; restores a prior uninstall's backup on fresh install (issue #615).
615
+ function writeMcpServersFile(pluginDest, mcpPath, agentKey) {
616
+ const configPath = join(pluginDest, '.mcp.json');
617
+ let config;
618
+ try {
619
+ config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : undefined;
620
+ } catch {
621
+ config = undefined;
622
+ }
623
+ const existing = config && typeof config === 'object' ? config.mcpServers?.['huaweicloud-devkit'] : undefined;
624
+ const { entry, changed } = mergeArgsStyle(existing, {
625
+ mcpPath,
626
+ env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
627
+ defaultTimeout: null,
628
+ });
629
+ if (existing && !changed) {
630
+ console.log(` MCP Config unchanged: ${configPath}`);
631
+ return;
632
+ }
633
+ const next = config && typeof config === 'object' && !Array.isArray(config) ? { ...config } : {};
634
+ next.mcpServers = { ...(config && typeof config === 'object' ? config.mcpServers : {}), 'huaweicloud-devkit': entry };
635
+ if (!existing) {
636
+ const delta = takeAgentDelta(agentKey);
637
+ if (delta)
638
+ next.mcpServers['huaweicloud-devkit'] = applyUserDelta(next.mcpServers['huaweicloud-devkit'], delta, 'args');
639
+ }
640
+ writeFileSync(configPath, JSON.stringify(next, null, 2));
641
+ console.log(` MCP Config -> ${configPath}`);
642
+ }
643
+
644
+ // Back up the user delta from a pluginDir/.mcp.json before the directory is removed (issue #615).
645
+ function backupDeltaFromMcpServersFile(pluginDest, agentKey) {
646
+ const configPath = join(pluginDest, '.mcp.json');
647
+ try {
648
+ const config = JSON.parse(readFileSync(configPath, 'utf8'));
649
+ const delta = extractUserDelta(config?.mcpServers?.['huaweicloud-devkit'], 'args');
650
+ if (delta) saveAgentDelta(agentKey, delta);
651
+ } catch {}
652
+ }
653
+
584
654
  function removeOpenCodeConfig() {
585
655
  const configPath = opencodeConfigFile();
586
656
  if (!existsSync(configPath)) return;
@@ -591,6 +661,9 @@ function removeOpenCodeConfig() {
591
661
  return;
592
662
  }
593
663
  if (!config.mcp?.['huaweicloud-devkit']) return;
664
+ // Back up user-customized fields before removal so a later reinstall can restore them (issue #615).
665
+ const delta = extractUserDelta(config.mcp['huaweicloud-devkit'], 'command');
666
+ if (delta) saveAgentDelta('opencode', delta);
594
667
  delete config.mcp['huaweicloud-devkit'];
595
668
  if (Object.keys(config.mcp).length === 0) delete config.mcp;
596
669
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -920,17 +993,7 @@ async function installOpenClaw() {
920
993
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
921
994
 
922
995
  const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
923
- const mcpConfig = {
924
- mcpServers: {
925
- 'huaweicloud-devkit': {
926
- command: 'node',
927
- args: [mcpServerAbsPath],
928
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
929
- },
930
- },
931
- };
932
- writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
933
- console.log(` MCP Config -> ${join(pluginDest, '.mcp.json')}`);
996
+ writeMcpServersFile(pluginDest, mcpServerAbsPath, 'openclaw');
934
997
 
935
998
  const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
936
999
  if (existsSync(codexPluginSrc)) {
@@ -967,6 +1030,7 @@ function uninstallOpenClaw() {
967
1030
  if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
968
1031
  }
969
1032
 
1033
+ backupDeltaFromMcpServersFile(openclawPluginsDir(), 'openclaw');
970
1034
  if (removeIfExists(openclawPluginsDir())) {
971
1035
  console.log(' Removed MCP server and safety policy');
972
1036
  }
@@ -991,17 +1055,7 @@ async function updateOpenClaw() {
991
1055
  console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
992
1056
 
993
1057
  const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
994
- const mcpConfig = {
995
- mcpServers: {
996
- 'huaweicloud-devkit': {
997
- command: 'node',
998
- args: [mcpServerAbsPath],
999
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
1000
- },
1001
- },
1002
- };
1003
- writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
1004
- console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
1058
+ writeMcpServersFile(pluginDest, mcpServerAbsPath, 'openclaw');
1005
1059
 
1006
1060
  const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
1007
1061
  if (existsSync(codexPluginSrc)) {
@@ -1039,17 +1093,7 @@ async function installCodexDesktop() {
1039
1093
 
1040
1094
  // Generate .mcp.json for Codex plugin MCP server discovery
1041
1095
  const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
1042
- const mcpConfig = {
1043
- mcpServers: {
1044
- 'huaweicloud-devkit': {
1045
- command: 'node',
1046
- args: [mcpServerAbsPath],
1047
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
1048
- },
1049
- },
1050
- };
1051
- writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
1052
- console.log(` MCP Config -> ${join(pluginDest, '.mcp.json')}`);
1096
+ writeMcpServersFile(pluginDest, mcpServerAbsPath, 'codex-desktop');
1053
1097
 
1054
1098
  // Copy .codex-plugin manifest for Codex Desktop plugin registration
1055
1099
  const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
@@ -1106,17 +1150,7 @@ async function updateCodexDesktop() {
1106
1150
  }
1107
1151
 
1108
1152
  const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
1109
- const mcpConfig = {
1110
- mcpServers: {
1111
- 'huaweicloud-devkit': {
1112
- command: 'node',
1113
- args: [mcpServerAbsPath],
1114
- env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
1115
- },
1116
- },
1117
- };
1118
- writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
1119
- console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
1153
+ writeMcpServersFile(pluginDest, mcpServerAbsPath, 'codex-desktop');
1120
1154
 
1121
1155
  const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
1122
1156
  if (existsSync(codexPluginSrc)) {
@@ -1155,6 +1189,7 @@ function uninstallCodexDesktop() {
1155
1189
  if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
1156
1190
  }
1157
1191
 
1192
+ backupDeltaFromMcpServersFile(pluginDest, 'codex-desktop');
1158
1193
  if (removeIfExists(pluginDest)) {
1159
1194
  console.log(' Removed MCP server and safety policy');
1160
1195
  }
@@ -1185,10 +1220,13 @@ function uninstallCodexDesktop() {
1185
1220
  }
1186
1221
  }
1187
1222
 
1188
- function registerCodeartsMcp(configPath) {
1223
+ function registerCodeartsMcp(configPath, agentKey = 'codearts') {
1189
1224
  const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
1190
1225
  const hcloudBin = findHcloudBin();
1226
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
1227
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
1191
1228
  let config = {};
1229
+ let existing;
1192
1230
  if (existsSync(configPath)) {
1193
1231
  try {
1194
1232
  config = JSON.parse(readFileSync(configPath, 'utf8'));
@@ -1198,51 +1236,27 @@ function registerCodeartsMcp(configPath) {
1198
1236
  );
1199
1237
  return;
1200
1238
  }
1201
- const existing = config.mcpServers?.['huaweicloud-devkit'];
1202
- if (
1203
- existing &&
1204
- existing.command === 'node' &&
1205
- Array.isArray(existing.args) &&
1206
- existing.args[0] === mcpPath &&
1207
- existing.timeout === 300000
1208
- ) {
1209
- let changed = false;
1210
- if (!existing.env) {
1211
- existing.env = {};
1212
- changed = true;
1213
- }
1214
- if (!existing.env.HUAWEICLOUD_AGENT_TOOLKIT_MODE) {
1215
- existing.env.HUAWEICLOUD_AGENT_TOOLKIT_MODE = 'local';
1216
- changed = true;
1217
- }
1218
- if (hcloudBin && !existing.env.HCLOUD_BIN) {
1219
- existing.env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
1220
- changed = true;
1221
- }
1222
- if (existing.enabled !== true) {
1223
- existing.enabled = true;
1224
- changed = true;
1225
- }
1226
- if (changed) {
1227
- mkdirSync(dirname(configPath), { recursive: true });
1228
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1229
- console.log(` MCP config refreshed: ${configPath}`);
1230
- } else {
1239
+ existing = config.mcpServers?.['huaweicloud-devkit'];
1240
+ if (existing) {
1241
+ const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
1242
+ if (!changed) {
1231
1243
  console.log(` MCP config unchanged: ${configPath}`);
1244
+ return;
1232
1245
  }
1246
+ config.mcpServers['huaweicloud-devkit'] = entry;
1247
+ mkdirSync(dirname(configPath), { recursive: true });
1248
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1249
+ console.log(` MCP config merged (user fields preserved): ${configPath}`);
1233
1250
  return;
1234
1251
  }
1235
1252
  }
1236
1253
  config.mcpServers = config.mcpServers || {};
1237
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
1238
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
1239
- config.mcpServers['huaweicloud-devkit'] = {
1240
- command: 'node',
1241
- args: [mcpPath],
1242
- env,
1243
- enabled: true,
1244
- timeout: 300000,
1245
- };
1254
+ let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
1255
+ entry.enabled = true;
1256
+ // Restore user fields saved by a previous uninstall (issue #615).
1257
+ const delta = takeAgentDelta(agentKey);
1258
+ if (delta) entry = applyUserDelta(entry, delta, 'args');
1259
+ config.mcpServers['huaweicloud-devkit'] = entry;
1246
1260
  mkdirSync(dirname(configPath), { recursive: true });
1247
1261
  writeFileSync(configPath, JSON.stringify(config, null, 2));
1248
1262
  console.log(` MCP config updated: ${configPath}`);
@@ -1336,6 +1350,8 @@ function uninstallCodeArts() {
1336
1350
  config = JSON.parse(readFileSync(configPath, 'utf8'));
1337
1351
  } catch {}
1338
1352
  if (config.mcpServers?.['huaweicloud-devkit']) {
1353
+ const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
1354
+ if (delta) saveAgentDelta('codearts', delta);
1339
1355
  delete config.mcpServers['huaweicloud-devkit'];
1340
1356
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1341
1357
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -1392,31 +1408,44 @@ function registerCodeartsWorkMcp() {
1392
1408
  return;
1393
1409
  }
1394
1410
  const existing = config.mcp?.['huaweicloud-devkit'];
1395
- if (
1396
- existing &&
1397
- existing.type === 'local' &&
1398
- Array.isArray(existing.command) &&
1399
- existing.command[0] === 'node' &&
1400
- existing.command[1] === mcpPath &&
1401
- existing.timeout === 300000
1402
- ) {
1403
- console.log(` MCP config unchanged: ${configPath}`);
1411
+ if (existing) {
1412
+ // codearts-work uses `command` array + `environment` naming; adapt via mergeCommandStyle.
1413
+ const { entry, changed } = mergeCommandStyle(existing, { mcpPath });
1414
+ if (!changed) {
1415
+ console.log(` MCP config unchanged: ${configPath}`);
1416
+ return;
1417
+ }
1418
+ const merged = { ...entry };
1419
+ merged.environment = {
1420
+ ...(isPlainLocal(existing.environment) ? existing.environment : {}),
1421
+ HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local',
1422
+ };
1423
+ if (hcloudBin && merged.environment.HCLOUD_BIN === undefined) {
1424
+ merged.environment.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
1425
+ }
1426
+ config.mcp['huaweicloud-devkit'] = merged;
1427
+ mkdirSync(dirname(configPath), { recursive: true });
1428
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1429
+ console.log(` MCP config merged (user fields preserved): ${configPath}`);
1404
1430
  return;
1405
1431
  }
1406
1432
  }
1407
1433
  config.mcp = config.mcp || {};
1408
- config.mcp['huaweicloud-devkit'] = {
1409
- type: 'local',
1410
- command: ['node', mcpPath],
1411
- environment,
1412
- enabled: true,
1413
- timeout: 300000,
1414
- };
1434
+ let entry = mergeCommandStyle(undefined, { mcpPath }).entry;
1435
+ entry.environment = { ...environment };
1436
+ // Restore user fields saved by a previous uninstall (issue #615).
1437
+ const delta = takeAgentDelta('codearts-work');
1438
+ if (delta) entry = applyUserDelta(entry, delta, 'command');
1439
+ config.mcp['huaweicloud-devkit'] = entry;
1415
1440
  mkdirSync(dirname(configPath), { recursive: true });
1416
1441
  writeFileSync(configPath, JSON.stringify(config, null, 2));
1417
1442
  console.log(` MCP config updated: ${configPath}`);
1418
1443
  }
1419
1444
 
1445
+ function isPlainLocal(value) {
1446
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
1447
+ }
1448
+
1420
1449
  async function installCodeArtsWork() {
1421
1450
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
1422
1451
  const srcDir = join(PLUGIN_ROOT, 'src');
@@ -1477,6 +1506,8 @@ function uninstallCodeArtsWork() {
1477
1506
  config = JSON.parse(readFileSync(configPath, 'utf8'));
1478
1507
  } catch {}
1479
1508
  if (config.mcp?.['huaweicloud-devkit']) {
1509
+ const delta = extractUserDelta(config.mcp['huaweicloud-devkit'], 'command');
1510
+ if (delta) saveAgentDelta('codearts-work', delta);
1480
1511
  delete config.mcp['huaweicloud-devkit'];
1481
1512
  if (Object.keys(config.mcp).length === 0) delete config.mcp;
1482
1513
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -1532,24 +1563,25 @@ function ensureWorkbuddyMcpConfig() {
1532
1563
  return false;
1533
1564
  }
1534
1565
  const existing = config.mcpServers?.['huaweicloud-devkit'];
1535
- if (
1536
- existing &&
1537
- existing.command === 'node' &&
1538
- Array.isArray(existing.args) &&
1539
- existing.args[0] === mcpPath &&
1540
- existing.timeout === 300000
1541
- ) {
1542
- console.log(` MCP config unchanged: ${configPath}`);
1543
- return false;
1566
+ if (existing) {
1567
+ const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
1568
+ if (!changed) {
1569
+ console.log(` MCP config unchanged: ${configPath}`);
1570
+ return false;
1571
+ }
1572
+ config.mcpServers['huaweicloud-devkit'] = entry;
1573
+ mkdirSync(dirname(configPath), { recursive: true });
1574
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1575
+ console.log(` MCP config merged (user fields preserved): ${configPath}`);
1576
+ return true;
1544
1577
  }
1545
1578
  }
1546
1579
  config.mcpServers = config.mcpServers || {};
1547
- config.mcpServers['huaweicloud-devkit'] = {
1548
- command: 'node',
1549
- args: [mcpPath],
1550
- env,
1551
- timeout: 300000,
1552
- };
1580
+ let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
1581
+ // Restore user fields saved by a previous uninstall (issue #615).
1582
+ const delta = takeAgentDelta('workbuddy');
1583
+ if (delta) entry = applyUserDelta(entry, delta, 'args');
1584
+ config.mcpServers['huaweicloud-devkit'] = entry;
1553
1585
  mkdirSync(dirname(configPath), { recursive: true });
1554
1586
  writeFileSync(configPath, JSON.stringify(config, null, 2));
1555
1587
  console.log(` MCP config updated: ${configPath}`);
@@ -1762,6 +1794,8 @@ function uninstallWorkBuddy() {
1762
1794
  config = JSON.parse(readFileSync(configPath, 'utf8'));
1763
1795
  } catch {}
1764
1796
  if (config.mcpServers?.['huaweicloud-devkit']) {
1797
+ const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
1798
+ if (delta) saveAgentDelta('workbuddy', delta);
1765
1799
  delete config.mcpServers['huaweicloud-devkit'];
1766
1800
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1767
1801
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -1861,24 +1895,25 @@ function ensureAtomcodeMcpConfig() {
1861
1895
  return false;
1862
1896
  }
1863
1897
  const existing = config.mcpServers?.['huaweicloud-devkit'];
1864
- if (
1865
- existing &&
1866
- existing.command === 'node' &&
1867
- Array.isArray(existing.args) &&
1868
- existing.args[0] === mcpPath &&
1869
- existing.timeout === 300000
1870
- ) {
1871
- console.log(` MCP config unchanged: ${configPath}`);
1872
- return false;
1898
+ if (existing) {
1899
+ const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
1900
+ if (!changed) {
1901
+ console.log(` MCP config unchanged: ${configPath}`);
1902
+ return false;
1903
+ }
1904
+ config.mcpServers['huaweicloud-devkit'] = entry;
1905
+ mkdirSync(dirname(configPath), { recursive: true });
1906
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1907
+ console.log(` MCP config merged (user fields preserved): ${configPath}`);
1908
+ return true;
1873
1909
  }
1874
1910
  }
1875
1911
  config.mcpServers = config.mcpServers || {};
1876
- config.mcpServers['huaweicloud-devkit'] = {
1877
- command: 'node',
1878
- args: [mcpPath],
1879
- env,
1880
- timeout: 300000,
1881
- };
1912
+ let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
1913
+ // Restore user fields saved by a previous uninstall (issue #615).
1914
+ const delta = takeAgentDelta('atomcode');
1915
+ if (delta) entry = applyUserDelta(entry, delta, 'args');
1916
+ config.mcpServers['huaweicloud-devkit'] = entry;
1882
1917
  mkdirSync(dirname(configPath), { recursive: true });
1883
1918
  writeFileSync(configPath, JSON.stringify(config, null, 2));
1884
1919
  console.log(` MCP config updated: ${configPath}`);
@@ -1951,6 +1986,8 @@ function uninstallAtomCode() {
1951
1986
  config = JSON.parse(readFileSync(configPath, 'utf8'));
1952
1987
  } catch {}
1953
1988
  if (config.mcpServers?.['huaweicloud-devkit']) {
1989
+ const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
1990
+ if (delta) saveAgentDelta('atomcode', delta);
1954
1991
  delete config.mcpServers['huaweicloud-devkit'];
1955
1992
  if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1956
1993
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -2478,7 +2515,6 @@ function ensureHermesMcpConfig() {
2478
2515
  if (hcloudBin) {
2479
2516
  blockLines.push(` HCLOUD_BIN: "${hcloudBin.replace(/\\/g, '/')}"`);
2480
2517
  }
2481
- const block = blockLines.join('\n');
2482
2518
 
2483
2519
  let existing = '';
2484
2520
  if (existsSync(configPath)) {
@@ -2486,10 +2522,15 @@ function ensureHermesMcpConfig() {
2486
2522
  existing = readFileSync(configPath, 'utf8');
2487
2523
  } catch {}
2488
2524
  if (existing.includes('mcp_servers:') && existing.includes('huaweicloud-devkit')) {
2489
- if (existing.includes(`args: ["${mcpPath}"]`)) {
2525
+ // Preserve user's extra args (e.g. --hdkitservice-endpoint) when rewriting the block (issue #615).
2526
+ const argsMatch = existing.match(/^\s*huaweicloud-devkit\s*:[\s\S]*?^\s*args:\s*\[[^\]]*\]\s*$/m);
2527
+ const extrasMatch = argsMatch ? argsMatch[0].match(/args:\s*\[".*?"(.*)\]/) : null;
2528
+ const extras = extrasMatch && extrasMatch[1].trim() ? extrasMatch[1].replace(/,\s*$/, '') : '';
2529
+ if (existing.includes(`args: ["${mcpPath}"${extras}]`)) {
2490
2530
  console.log(` MCP config unchanged: ${configPath}`);
2491
2531
  return false;
2492
2532
  }
2533
+ if (extras) blockLines[3] = ` args: ["${mcpPath}"${extras}]`;
2493
2534
  removeHermesMcpConfigBlock();
2494
2535
  existing = '';
2495
2536
  if (existsSync(configPath)) {
@@ -2500,7 +2541,14 @@ function ensureHermesMcpConfig() {
2500
2541
  }
2501
2542
  }
2502
2543
  mkdirSync(dirname(configPath), { recursive: true });
2503
- const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
2544
+ // Restore user args saved by a previous uninstall (issue #615).
2545
+ const hermesDelta = takeAgentDelta('hermes');
2546
+ if (hermesDelta && Array.isArray(hermesDelta.argsExtra) && hermesDelta.argsExtra.length > 0) {
2547
+ const extras = hermesDelta.argsExtra.map((a) => `"${String(a)}"`).join(', ');
2548
+ blockLines[3] = ` args: ["${mcpPath}", ${extras}]`;
2549
+ }
2550
+ const newBlock = blockLines.join('\n');
2551
+ const newContent = existing ? `${existing.trimEnd()}\n\n${newBlock}\n` : `${newBlock}\n`;
2504
2552
  writeFileSync(configPath, newContent);
2505
2553
  console.log(` MCP config updated: ${configPath}`);
2506
2554
  return true;
@@ -2917,7 +2965,25 @@ function uninstallHermes() {
2917
2965
  }
2918
2966
  }
2919
2967
 
2920
- // 3. Remove MCP config from config.yaml
2968
+ // 3. Remove MCP config from config.yaml (back up user args first, issue #615)
2969
+ const hermesConfig = hermesConfigFile();
2970
+ if (existsSync(hermesConfig)) {
2971
+ try {
2972
+ const text = readFileSync(hermesConfig, 'utf8');
2973
+ const argsLine = text.match(/^\s*args:\s*\[([^\]]*)\]\s*$/m);
2974
+ if (argsLine) {
2975
+ let parsed;
2976
+ try {
2977
+ parsed = JSON.parse(`[${argsLine[1].replace(/'/g, '"')}]`);
2978
+ } catch {
2979
+ parsed = null;
2980
+ }
2981
+ if (Array.isArray(parsed) && parsed.length > 1) {
2982
+ saveAgentDelta('hermes', { argsExtra: parsed.slice(1) });
2983
+ }
2984
+ }
2985
+ } catch {}
2986
+ }
2921
2987
  removeHermesMcpConfigBlock();
2922
2988
  console.log(' MCP config removed');
2923
2989
 
@@ -3583,6 +3649,10 @@ async function cmdUninstall() {
3583
3649
  if (removeIfExists(vaultPath)) {
3584
3650
  console.log(' Removed credential vault');
3585
3651
  }
3652
+ // Full uninstall also discards any saved MCP config backups (issue #615).
3653
+ if (purgeBackup()) {
3654
+ console.log(' Removed MCP config backup');
3655
+ }
3586
3656
  const vaultDir = dirname(vaultPath);
3587
3657
  try {
3588
3658
  if (existsSync(vaultDir) && readdirSync(vaultDir).length === 0) {
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { homedir, tmpdir } from 'node:os';
5
5
  import { spawnSync, execFile } from 'node:child_process';
6
6
  import { promisify } from 'node:util';
7
+ import { createHmac, createHash } from 'node:crypto';
7
8
 
8
9
  import { evaluateArtifacts, evaluateCommandRisk, evaluateDeployPlan } from './risk-rule-engine.mjs';
9
10
  import { classifyTextCommand, redactSecrets } from './safety-policy.mjs';
@@ -48,6 +49,7 @@ import {
48
49
  resolveCredentialsWithRuntime,
49
50
  } from './auth/credentials.mjs';
50
51
  import { trackToolInvoke, trackSkillRetrieve, clearUserHash } from './telemetry/telemetry.mjs';
52
+ import { fetchWithProxy } from './proxy/proxy-agent.mjs';
51
53
  import { fingerprint, runHcloudConfigure, resolveManagedProfile } from './auth/reconcile.mjs';
52
54
  import {
53
55
  getCachedUpdateInfo,
@@ -846,6 +848,32 @@ export const TOOL_DEFINITIONS = [
846
848
  },
847
849
  },
848
850
  },
851
+ {
852
+ name: 'huaweicloud_obs_set_website_config',
853
+ description:
854
+ '配置 OBS 桶的静态网站托管。KooCLI OBS 不支持 SetBucketWebsite API,此工具内部实现 AWS4 签名调用 OBS REST API,屏蔽签名细节。支持 set(配置)、get(查询)、delete(删除)三种操作。操作前需确保桶已创建且已设置 public-read ACL。',
855
+ inputSchema: {
856
+ type: 'object',
857
+ properties: {
858
+ action: {
859
+ type: 'string',
860
+ enum: ['set', 'get', 'delete'],
861
+ description: '操作类型:set=配置静态网站托管,get=查询当前配置,delete=删除配置',
862
+ },
863
+ bucket: { type: 'string', description: 'OBS 桶名称' },
864
+ region: { type: 'string', description: 'OBS 桶所在区域,如 cn-north-4' },
865
+ indexDocument: {
866
+ type: 'string',
867
+ description: '首页文件名(action=set 时必填),如 index.html',
868
+ },
869
+ errorDocument: {
870
+ type: 'string',
871
+ description: '错误页面文件名(action=set 时可选),如 404.html 或 error.html',
872
+ },
873
+ },
874
+ required: ['action', 'bucket', 'region'],
875
+ },
876
+ },
849
877
  ];
850
878
 
851
879
  function toolInvokeValue(name, args) {
@@ -1448,19 +1476,26 @@ export async function callTool(name, rawArgs = {}, opts = {}) {
1448
1476
  case 'huaweicloud_voucher_claim':
1449
1477
  return await hdkitVoucherClaim(args.domain_id);
1450
1478
  case 'huaweicloud_check_update':
1451
- return await handleCheckUpdate(args, { sessionId: opts?.sessionId });
1479
+ return await handleCheckUpdate(args, { sessionId: opts?.sessionId, doQuery: opts?.doQuery });
1452
1480
  case 'huaweicloud_upgrade':
1453
1481
  return await handleUpgrade(args, { sessionId: opts?.sessionId });
1482
+ case 'huaweicloud_obs_set_website_config':
1483
+ return await handleObsWebsiteConfig(args);
1454
1484
  default:
1455
1485
  throw new Error(`Unknown tool: ${name}`);
1456
1486
  }
1457
1487
  }
1458
1488
 
1459
1489
  async function handleCheckUpdate(args = {}, opts = {}) {
1460
- const sessionId = opts?.sessionId || null;
1490
+ const { sessionId = null, doQuery } = opts;
1461
1491
  const current = readInstalledVersion() || '0.0.0';
1462
1492
  if (args.dismiss === true) {
1463
- const distTags = await getUpdateDistTags(current, { sessionId });
1493
+ const distTags = await getUpdateDistTags(current, { sessionId, doQuery });
1494
+ if (!distTags) {
1495
+ // 查询失败:不降级为 current 冷却、不写 skip,返回 check_failed。
1496
+ // 不调 invalidateUpdateCache(),以保留 failedAt 的 5 分钟失败节流。
1497
+ return judgeUpdate(current, null, null);
1498
+ }
1464
1499
  const target = determineTarget(current, distTags);
1465
1500
  const dismissedVersion =
1466
1501
  typeof args.dismissVersion === 'string' && args.dismissVersion ? args.dismissVersion : target || current;
@@ -1468,7 +1503,7 @@ async function handleCheckUpdate(args = {}, opts = {}) {
1468
1503
  invalidateUpdateCache();
1469
1504
  return judgeUpdate(current, distTags, state);
1470
1505
  }
1471
- return getCachedUpdateInfo(current, { sessionId });
1506
+ return getCachedUpdateInfo(current, { sessionId, doQuery });
1472
1507
  }
1473
1508
 
1474
1509
  async function handleUpgrade(args = {}, opts = {}) {
@@ -2255,3 +2290,112 @@ async function getRegionalAvailability(service, region) {
2255
2290
  export function classifyRawCommand(command) {
2256
2291
  return classifyTextCommand(command);
2257
2292
  }
2293
+
2294
+ // ── OBS Static Website Hosting (AWS4 signed REST API) ──
2295
+
2296
+ async function handleObsWebsiteConfig(args) {
2297
+ const { action, bucket, region, indexDocument, errorDocument } = args;
2298
+ if (!bucket || !region) {
2299
+ throw new Error('bucket and region are required');
2300
+ }
2301
+ const creds = resolveCredentialsWithRuntime({});
2302
+ if (!creds?.ak || !creds?.sk) {
2303
+ throw new Error('OBS website config requires AK/SK credentials. Run huaweicloud_auth_init first.');
2304
+ }
2305
+
2306
+ const host = `${bucket}.obs.${region}.myhuaweicloud.com`;
2307
+ const endpoint = `https://${host}`;
2308
+
2309
+ if (action === 'get') {
2310
+ const res = await obsSignedRequest('GET', endpoint, '/?website', '', creds, region);
2311
+ return { ok: res.status === 200, status: res.status, body: res.body };
2312
+ }
2313
+
2314
+ if (action === 'delete') {
2315
+ const res = await obsSignedRequest('DELETE', endpoint, '/?website', '', creds, region);
2316
+ return { ok: res.status === 204, status: res.status };
2317
+ }
2318
+
2319
+ if (action === 'set') {
2320
+ if (!indexDocument) {
2321
+ throw new Error('indexDocument is required for action=set');
2322
+ }
2323
+ const xmlParts = ['<WebsiteConfiguration>', ` <IndexDocument><Suffix>${indexDocument}</Suffix></IndexDocument>`];
2324
+ if (errorDocument) {
2325
+ xmlParts.push(` <ErrorDocument><Key>${errorDocument}</Key></ErrorDocument>`);
2326
+ }
2327
+ xmlParts.push('</WebsiteConfiguration>');
2328
+ const body = xmlParts.join('\n');
2329
+ const res = await obsSignedRequest('PUT', endpoint, '/?website', body, creds, region);
2330
+ const websiteUrl = `http://${bucket}.obs-website.${region}.myhuaweicloud.com`;
2331
+ return {
2332
+ ok: res.status === 200,
2333
+ status: res.status,
2334
+ websiteUrl,
2335
+ message:
2336
+ res.status === 200
2337
+ ? `Static website hosting configured. Website URL: ${websiteUrl} (may take ~1 min to propagate)`
2338
+ : `Failed to configure website: HTTP ${res.status}`,
2339
+ };
2340
+ }
2341
+
2342
+ throw new Error(`Unknown action: ${action}. Use set, get, or delete.`);
2343
+ }
2344
+
2345
+ async function obsSignedRequest(method, endpoint, pathAndQuery, body, creds, region) {
2346
+ const now = new Date();
2347
+ const dateStamp = now.toISOString().slice(0, 10).replace(/-/g, '');
2348
+ const amzDate = dateStamp + 'T' + now.toISOString().slice(11, 19).replace(/:/g, '') + 'Z';
2349
+ const payloadHash = createHash('sha256').update(body).digest('hex');
2350
+
2351
+ const url = new URL(endpoint + pathAndQuery);
2352
+ const canonicalUri = '/';
2353
+ const canonicalQueryString = 'website=';
2354
+ const canonicalHeaders = `host:${url.host}\n` + `x-amz-content-sha256:${payloadHash}\n` + `x-amz-date:${amzDate}\n`;
2355
+ const signedHeaders = 'host;x-amz-content-sha256;x-amz-date';
2356
+ const canonicalRequest = [
2357
+ method,
2358
+ canonicalUri,
2359
+ canonicalQueryString,
2360
+ canonicalHeaders,
2361
+ signedHeaders,
2362
+ payloadHash,
2363
+ ].join('\n');
2364
+
2365
+ const credentialScope = `${dateStamp}/${region}/s3/aws4_request`;
2366
+ const stringToSign = [
2367
+ 'AWS4-HMAC-SHA256',
2368
+ amzDate,
2369
+ credentialScope,
2370
+ createHash('sha256').update(canonicalRequest).digest('hex'),
2371
+ ].join('\n');
2372
+
2373
+ const kDate = createHmac('sha256', 'AWS4' + creds.sk)
2374
+ .update(dateStamp)
2375
+ .digest();
2376
+ const kRegion = createHmac('sha256', kDate).update(region).digest();
2377
+ const kService = createHmac('sha256', kRegion).update('s3').digest();
2378
+ const kSigning = createHmac('sha256', kService).update('aws4_request').digest();
2379
+ const signature = createHmac('sha256', kSigning).update(stringToSign).digest('hex');
2380
+
2381
+ const authorization =
2382
+ `AWS4-HMAC-SHA256 Credential=${creds.ak}/${credentialScope}, ` +
2383
+ `SignedHeaders=${signedHeaders}, Signature=${signature}`;
2384
+
2385
+ const headers = {
2386
+ Host: url.host,
2387
+ 'x-amz-content-sha256': payloadHash,
2388
+ 'x-amz-date': amzDate,
2389
+ Authorization: authorization,
2390
+ };
2391
+ if (body) headers['Content-Type'] = 'application/xml';
2392
+ if (creds.securityToken) headers['x-amz-security-token'] = creds.securityToken;
2393
+
2394
+ const res = await fetchWithProxy(endpoint + pathAndQuery, {
2395
+ method,
2396
+ headers,
2397
+ body: body || undefined,
2398
+ });
2399
+ const resBody = await res.text();
2400
+ return { status: res.status, body: resBody };
2401
+ }
@@ -114,6 +114,10 @@ export function judgeUpdate(current, distTags, skipState, now = Date.now()) {
114
114
  if (!distTags) {
115
115
  return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
116
116
  }
117
+ // truthy 但无有效版本(如 failure 折叠后的 { latest:null, next:null })同样视为检测失败
118
+ if (!distTags.latest && !distTags.next) {
119
+ return buildResult(current, null, { result: 'check_failed', note: '检测失败,不影响使用' });
120
+ }
117
121
  const target = determineTarget(current, distTags);
118
122
  if (!target || semverCompare(target, current) <= 0) {
119
123
  return buildResult(current, distTags, { result: 'up_to_date', target: target ?? null });
@@ -348,9 +352,9 @@ export function peekCachedUpdateInfo() {
348
352
  return lastHint && lastHint.updateAvailable && lastHint.targetVersion ? lastHint : null;
349
353
  }
350
354
 
351
- export async function getUpdateDistTags(current, { sessionId = null } = {}) {
352
- const result = await getCachedUpdateInfo(current, { sessionId });
353
- if (!result) return null;
355
+ export async function getUpdateDistTags(current, { sessionId = null, doQuery } = {}) {
356
+ const result = await getCachedUpdateInfo(current, { sessionId, doQuery });
357
+ if (!result || result.result === 'check_failed') return null;
354
358
  return { latest: result.latestStable ?? null, next: result.latestNext ?? null };
355
359
  }
356
360