huaweicloud-devkit 1.0.2-dev.2 → 1.0.2-dev.4

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
@@ -9,6 +9,10 @@ Help AI coding agents use Huawei Cloud safely and accurately — a single integr
9
9
 
10
10
  Supports OpenCode, Codex, CodeArts Agent, and WorkBuddy.
11
11
 
12
+ ## Prerequisites
13
+
14
+ - Node.js >= 22
15
+
12
16
  ## Quick Start
13
17
 
14
18
  ### OpenCode
@@ -34,6 +38,15 @@ npx --yes huaweicloud-devkit install --target codex
34
38
 
35
39
  > The Codex CLI must be installed first.
36
40
 
41
+ ```bash
42
+ npx --yes huaweicloud-devkit doctor # self-check
43
+ npx --yes huaweicloud-devkit status --target codex
44
+ npx --yes huaweicloud-devkit update --target codex # update
45
+ npx --yes huaweicloud-devkit uninstall --target codex
46
+ ```
47
+
48
+ > **Codex Desktop** (Windows): use `--target codex-desktop` with the same commands.
49
+
37
50
  ### CodeArts Agent
38
51
 
39
52
  ```bash
@@ -46,12 +59,13 @@ npx --yes huaweicloud-devkit install --target codearts
46
59
  npx --yes huaweicloud-devkit install-hcloud # install KooCLI
47
60
  npx --yes huaweicloud-devkit doctor # self-check
48
61
  npx --yes huaweicloud-devkit status --target codearts
62
+ npx --yes huaweicloud-devkit update --target codearts
49
63
  npx --yes huaweicloud-devkit uninstall --target codearts
50
64
  ```
51
65
 
52
66
  > **Sandbox mode**: CodeArts defaults to sandbox mode which blocks KooCLI. `install-hcloud` detects this and shows how to resolve it — install KooCLI outside the sandbox terminal, or disable sandbox mode in CodeArts settings (Settings → Permissions → Bash mode).
53
67
  >
54
- > **Authentication**: Run `hcloud configure init` to configure AK/SK and region.
68
+ > **Authentication**: Run `npx huaweicloud-devkit auth init` to configure unified AK/SK credentials for KooCLI, OBS, and sandbox APIs.
55
69
 
56
70
  ### WorkBuddy
57
71
 
@@ -61,6 +75,15 @@ npx --yes huaweicloud-devkit install --target workbuddy
61
75
 
62
76
  **Restart the session** after installation.
63
77
 
78
+ ```bash
79
+ npx --yes huaweicloud-devkit doctor
80
+ npx --yes huaweicloud-devkit status --target workbuddy
81
+ npx --yes huaweicloud-devkit update --target workbuddy # update
82
+ npx --yes huaweicloud-devkit uninstall --target workbuddy
83
+ ```
84
+
85
+ > **Updating**: `update` is incremental per agent — it refreshes only the installed files and leaves your config untouched. Use `update --target all` to update every installed agent at once.
86
+
64
87
  ### Other Agents
65
88
 
66
89
  For agents that support the Model Context Protocol (MCP):
@@ -83,7 +106,7 @@ Then install:
83
106
  npx --yes huaweicloud-devkit install
84
107
  ```
85
108
 
86
- > **Prerequisites:** [KooCLI](https://support.huaweicloud.com/qs-hcli/hcli_02_003.html) (`hcloud`) installed and authenticated. Node.js >= 20 required.
109
+ > **Prerequisite:** Node.js >= 22. Run `npx huaweicloud-devkit auth init` for unified credentials.
87
110
 
88
111
  ## What It Does
89
112
 
package/README.zh-CN.md CHANGED
@@ -9,6 +9,10 @@
9
9
 
10
10
  支持 OpenCode、Codex、码道(CodeArts Agent)、WorkBuddy。
11
11
 
12
+ ## 前置条件
13
+
14
+ - Node.js >= 22
15
+
12
16
  ## 快速开始
13
17
 
14
18
  ### OpenCode
@@ -34,6 +38,15 @@ npx --yes huaweicloud-devkit install --target codex
34
38
 
35
39
  > 需要先安装 Codex CLI。
36
40
 
41
+ ```bash
42
+ npx --yes huaweicloud-devkit doctor # 自检
43
+ npx --yes huaweicloud-devkit status --target codex
44
+ npx --yes huaweicloud-devkit update --target codex # 更新
45
+ npx --yes huaweicloud-devkit uninstall --target codex
46
+ ```
47
+
48
+ > **Codex Desktop**(Windows):将 `--target codex` 换成 `--target codex-desktop`,命令相同。
49
+
37
50
  ### CodeArts Agent(码道)
38
51
 
39
52
  ```bash
@@ -46,12 +59,13 @@ npx --yes huaweicloud-devkit install --target codearts
46
59
  npx --yes huaweicloud-devkit install-hcloud # 安装 KooCLI
47
60
  npx --yes huaweicloud-devkit doctor # 自检
48
61
  npx --yes huaweicloud-devkit status --target codearts
62
+ npx --yes huaweicloud-devkit update --target codearts
49
63
  npx --yes huaweicloud-devkit uninstall --target codearts
50
64
  ```
51
65
 
52
66
  > **沙箱模式**:码道默认沙箱模式会阻止 KooCLI 运行。`install-hcloud` 自动检测并给出指引——请在码道外终端安装使用 KooCLI,或在码道设置中关闭沙箱模式(设置 → 权限 → Bash 模式)。
53
67
  >
54
- > **认证**:执行 `hcloud configure init` 配置 AK/SK 与区域。
68
+ > **认证**:执行 `npx huaweicloud-devkit auth init`,统一配置 KooCLI、OBS 和沙箱接口所需的 AK/SK
55
69
 
56
70
  ### WorkBuddy
57
71
 
@@ -61,6 +75,15 @@ npx --yes huaweicloud-devkit install --target workbuddy
61
75
 
62
76
  安装后**重启会话**。
63
77
 
78
+ ```bash
79
+ npx --yes huaweicloud-devkit doctor
80
+ npx --yes huaweicloud-devkit status --target workbuddy
81
+ npx --yes huaweicloud-devkit update --target workbuddy # 更新
82
+ npx --yes huaweicloud-devkit uninstall --target workbuddy
83
+ ```
84
+
85
+ > **更新机制**:`update` 按 agent 增量更新,只刷新必要文件、不动你的配置文件。`update --target all` 可一次更新所有已安装的 agent。
86
+
64
87
  ### 其他 Agent
65
88
 
66
89
  支持 MCP 协议的 Agent,手动配置:
@@ -83,7 +106,7 @@ npx --yes huaweicloud-devkit install --target workbuddy
83
106
  npx --yes huaweicloud-devkit install
84
107
  ```
85
108
 
86
- > **前置条件:** 需要安装 [KooCLI](https://support.huaweicloud.com/qs-hcli/hcli_02_003.html)(`hcloud`)并完成认证。Node.js >= 20。
109
+ > **前置条件:** Node.js >= 22。执行 `npx huaweicloud-devkit auth init` 完成统一认证。
87
110
 
88
111
  ## 功能特性
89
112
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-devkit",
3
- "version": "1.0.2-dev.2",
3
+ "version": "1.0.2-dev.4",
4
4
  "description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
5
5
  "type": "module",
6
6
  "files": [
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
22
22
  "skills": "./skills/",
23
- "version": "1.0.0",
23
+ "version": "1.0.2-dev.4",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "huaweicloud-core",
3
- "version": "1.0.0",
3
+ "version": "1.0.2-dev.4",
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",
@@ -20,7 +20,7 @@
20
20
  "mcpServers": "./.mcp.json",
21
21
  "description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
22
22
  "skills": "./skills/",
23
- "version": "1.0.0",
23
+ "version": "1.0.2-dev.4",
24
24
  "author": {
25
25
  "name": "HuaweiCloud Mate",
26
26
  "url": "https://github.com/huaweicloud"
@@ -33,7 +33,6 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
33
33
 
34
34
  | Tool | Purpose |
35
35
  |------|---------|
36
- | `huaweicloud_sandbox_exec` | One-shot command execution (no session reuse) |
37
36
  | `huaweicloud_sandbox_exec_with_session` | Session-based execution (state persists) |
38
37
  | `huaweicloud_sandbox_close_session` | Close a persistent terminal session |
39
38
 
@@ -43,7 +42,7 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
43
42
  2. **Sign agreement** (if needed): `huaweicloud_sandbox_sign_agreement` — when `agreement_signed=false`
44
43
  3. **Connect**: `huaweicloud_sandbox_connect` — returns `session_id`, `dev_stage_id`, `connection_id`, `connection_address`
45
44
  4. **Inject credentials** (optional): `huaweicloud_sandbox_credentials` — enables cloud API access from sandbox
46
- 5. **Execute commands**: `huaweicloud_sandbox_exec_with_session` for interactive work, `huaweicloud_sandbox_exec` for one-shot
45
+ 5. **Execute commands**: `huaweicloud_sandbox_exec_with_session` for interactive work
47
46
  6. **Release**: `huaweicloud_sandbox_release` — cleans up sandbox and session
48
47
 
49
48
  ## Critical Warnings
@@ -52,7 +51,6 @@ Domain expertise for Huawei Cloud Sandbox (DevStation) instances and workspace t
52
51
  |------|-----|
53
52
  | Agreement required first | `sandbox_connect` fails if user hasn't signed agreements; run `sandbox_check_user` first |
54
53
  | Session state persists | `exec_with_session` preserves `cd`, env vars, aliases between calls |
55
- | No session reuse in `exec` | Each `exec` call creates a new connection; previous state is lost |
56
54
  | Destructive commands blocked | `rm -rf /`, `mkfs`, `dd if=`, fork bombs are denied by safety policy |
57
55
  | Workspace ID = dev_stage_id | Use `dev_stage_id` from `sandbox_connect` as `workspace_id` for terminal exec |
58
56
  | Node.js >= 22 required | Sandbox terminal uses built-in WebSocket (globalThis.WebSocket) |
@@ -2,7 +2,7 @@ import { getCredentials } from './hwlink-api.mjs';
2
2
 
3
3
  const HDKIT_BASE_URL =
4
4
  process.env.HDKITSERVICE_ENDPOINT ||
5
- 'http://113.44.143.91:3002/rest/developer/server/hdkitservice/';
5
+ 'https://devkit.huaweicloud.com/rest/developer/server/hdkitservice/';
6
6
 
7
7
  async function hdkitRequest(method, path, body, timeoutMs = 300000) {
8
8
  const { ak, sk, securitytoken } = getCredentials();
@@ -1,10 +1,10 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { join, dirname } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
4
  import { createConnection, getCredentials } from './hwlink-api.mjs';
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
- const WS_EXEC_SRC = join(__dirname, '..', 'ws-exec');
7
+ export const WS_EXEC_INDEX_URL = pathToFileURL(join(__dirname, '..', 'ws-exec', 'index.js')).href;
8
8
 
9
9
  const DEFAULT_WORKSPACE_ID = process.env.HW_WORKSPACE_ID || '0107bd9997aa4287bd2b4890b49af07d';
10
10
 
@@ -55,7 +55,7 @@ async function getSession(workspaceId, username, timeoutMs) {
55
55
  const { ak, sk, securitytoken } = getCredentials();
56
56
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
57
57
 
58
- const { connectHwlinkTerminalSession } = await import(join(WS_EXEC_SRC, 'index.js'));
58
+ const { connectHwlinkTerminalSession } = await import(WS_EXEC_INDEX_URL);
59
59
  const session = await connectHwlinkTerminalSession({
60
60
  url: wsUrl,
61
61
  source,
@@ -71,7 +71,7 @@ export async function execOneShot(workspaceId, command, username, timeoutMs) {
71
71
  const { ak, sk, securitytoken } = getCredentials();
72
72
  const { wsUrl, source } = await createConnection(workspaceId, ak, sk, securitytoken);
73
73
 
74
- const { executeHwlinkCommand } = await import(join(WS_EXEC_SRC, 'index.js'));
74
+ const { executeHwlinkCommand } = await import(WS_EXEC_INDEX_URL);
75
75
  return await executeHwlinkCommand({
76
76
  url: wsUrl,
77
77
  source,
@@ -129,11 +129,21 @@ function removeIfExists(p) {
129
129
 
130
130
  function updateOpenCodeConfig(pluginDir) {
131
131
  const configPath = opencodeConfigFile();
132
+ const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
132
133
  let config = {};
133
134
  if (existsSync(configPath)) {
134
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
135
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
136
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath} (jsonc comments?). Skipping MCP config write; ensure "mcp.huaweicloud-devkit" points to ${mcpPath}.`);
137
+ return;
138
+ }
139
+ const existing = config.mcp?.['huaweicloud-devkit'];
140
+ if (existing && existing.type === 'local'
141
+ && Array.isArray(existing.command) && existing.command[0] === 'node'
142
+ && existing.command[1] === mcpPath) {
143
+ console.log(` OpenCode MCP config unchanged: ${configPath}`);
144
+ return;
145
+ }
135
146
  }
136
- const mcpPath = join(pluginDir, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
137
147
  config.mcp = config.mcp || {};
138
148
  config.mcp['huaweicloud-devkit'] = {
139
149
  type: 'local',
@@ -298,6 +308,101 @@ function uninstallOpenCode() {
298
308
  removeOpenCodeConfig();
299
309
  }
300
310
 
311
+ // Remove huawei* entries in targetDir that no longer exist in sourceDir (stale files from an older version).
312
+ function pruneStale(targetDir, sourceDir) {
313
+ if (!existsSync(targetDir) || !existsSync(sourceDir)) return 0;
314
+ const sourceNames = new Set(readdirSync(sourceDir));
315
+ let removed = 0;
316
+ for (const entry of readdirSync(targetDir, { withFileTypes: true })) {
317
+ if (!entry.name.startsWith('huawei')) continue;
318
+ if (!sourceNames.has(entry.name)) {
319
+ removeIfExists(join(targetDir, entry.name));
320
+ removed++;
321
+ }
322
+ }
323
+ return removed;
324
+ }
325
+
326
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
327
+ async function updateOpenCode() {
328
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
329
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
330
+ const srcDir = join(PLUGIN_ROOT, 'src');
331
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
332
+ const pluginDest = opencodePluginsDir();
333
+
334
+ copyDir(skillsSrc, opencodeSkillsDir());
335
+ const staleSkills = pruneStale(opencodeSkillsDir(), skillsSrc);
336
+ console.log(` Skills updated -> ${opencodeSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
337
+ copyDir(commandsSrc, opencodeCommandsDir());
338
+ const staleCommands = pruneStale(opencodeCommandsDir(), commandsSrc);
339
+ console.log(` Commands updated -> ${opencodeCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
340
+ copyDir(srcDir, join(pluginDest, 'src'));
341
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
342
+ copyDir(safetyDir, join(pluginDest, 'safety'));
343
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
344
+ updateOpenCodeConfig(pluginDest);
345
+ mkdirSync(pluginDest, { recursive: true });
346
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
347
+ }
348
+
349
+ function codexMcpServerPath() {
350
+ return join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
351
+ }
352
+
353
+ function codexConfigSectionText(mcpPath) {
354
+ return [
355
+ '[mcp_servers.huaweicloud-devkit]',
356
+ 'command = "node"',
357
+ `args = ["${mcpPath}"]`,
358
+ '',
359
+ '[mcp_servers.huaweicloud-devkit.env]',
360
+ 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
361
+ '',
362
+ ].join('\n');
363
+ }
364
+
365
+ // Returns true when the config file was written, false when it was already correct.
366
+ function ensureCodexConfigSection(mcpPath) {
367
+ const configPath = codexConfigToml();
368
+ let existing = '';
369
+ if (existsSync(configPath)) {
370
+ try { existing = readFileSync(configPath, 'utf8'); } catch {}
371
+ if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
372
+ if (existing.includes(`args = ["${mcpPath}"]`)) {
373
+ console.log(` Config unchanged: ${configPath}`);
374
+ return false;
375
+ }
376
+ removeCodexConfigSection();
377
+ existing = '';
378
+ if (existsSync(configPath)) {
379
+ try { existing = readFileSync(configPath, 'utf8'); } catch {}
380
+ }
381
+ }
382
+ }
383
+ mkdirSync(dirname(configPath), { recursive: true });
384
+ if (existing && !existing.endsWith('\n')) existing += '\n';
385
+ writeFileSync(configPath, existing + codexConfigSectionText(mcpPath));
386
+ console.log(` Config updated: ${configPath}`);
387
+ return true;
388
+ }
389
+
390
+ function removeCodexConfigSection() {
391
+ const configPath = codexConfigToml();
392
+ if (!existsSync(configPath)) return;
393
+ const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
394
+ const out = [];
395
+ let skip = false;
396
+ for (const line of lines) {
397
+ if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
398
+ if (skip && line.startsWith('[')) skip = false;
399
+ if (!skip) out.push(line);
400
+ }
401
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
402
+ writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
403
+ console.log(' Config cleaned');
404
+ }
405
+
301
406
  async function installCodexDesktop() {
302
407
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
303
408
  const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
@@ -315,7 +420,7 @@ async function installCodexDesktop() {
315
420
  console.log(` Safety Policy -> ${join(codexDesktopPluginsDir(), 'safety')}`);
316
421
 
317
422
  // Generate .mcp.json with absolute paths for Codex Desktop MCP server discovery
318
- const mcpServerAbsPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
423
+ const mcpServerAbsPath = codexMcpServerPath();
319
424
  const mcpConfig = {
320
425
  mcpServers: {
321
426
  'huaweicloud-devkit': {
@@ -335,29 +440,50 @@ async function installCodexDesktop() {
335
440
  console.log(` Plugin Manifest -> ${join(codexDesktopPluginsDir(), '.codex-plugin')}`);
336
441
  }
337
442
 
338
- const mcpPath = join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
339
- const configPath = codexConfigToml();
340
- const section = [
341
- '[mcp_servers.huaweicloud-devkit]',
342
- 'command = "node"',
343
- `args = ["${mcpPath}"]`,
344
- '',
345
- '[mcp_servers.huaweicloud-devkit.env]',
346
- 'HUAWEICLOUD_AGENT_TOOLKIT_MODE = "local"',
347
- '',
348
- ].join('\n');
349
- let existing = '';
350
- if (existsSync(configPath)) {
351
- try { existing = readFileSync(configPath, 'utf8'); } catch {}
352
- if (existing.includes('[mcp_servers.huaweicloud-devkit]')) {
353
- console.log(` Config already configured: ${configPath}`);
354
- return;
355
- }
443
+ ensureCodexConfigSection(mcpServerAbsPath);
444
+ }
445
+
446
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
447
+ async function updateCodexDesktop() {
448
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
449
+ const commandsSrc = join(PACKAGE_ROOT, 'integrations', 'opencode', 'commands');
450
+ const srcDir = join(PLUGIN_ROOT, 'src');
451
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
452
+ const pluginDest = codexDesktopPluginsDir();
453
+
454
+ copyDir(skillsSrc, codexDesktopSkillsDir());
455
+ const staleSkills = pruneStale(codexDesktopSkillsDir(), skillsSrc);
456
+ console.log(` Skills updated -> ${codexDesktopSkillsDir()}${staleSkills > 0 ? ` (removed ${staleSkills} stale)` : ''}`);
457
+ copyDir(commandsSrc, codexDesktopCommandsDir());
458
+ const staleCommands = pruneStale(codexDesktopCommandsDir(), commandsSrc);
459
+ console.log(` Commands updated -> ${codexDesktopCommandsDir()}${staleCommands > 0 ? ` (removed ${staleCommands} stale)` : ''}`);
460
+ mkdirSync(pluginDest, { recursive: true });
461
+ copyDir(srcDir, join(pluginDest, 'src'));
462
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
463
+ copyDir(safetyDir, join(pluginDest, 'safety'));
464
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
465
+
466
+ const mcpServerAbsPath = codexMcpServerPath();
467
+ const mcpConfig = {
468
+ mcpServers: {
469
+ 'huaweicloud-devkit': {
470
+ command: 'node',
471
+ args: [mcpServerAbsPath],
472
+ env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
473
+ },
474
+ },
475
+ };
476
+ writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
477
+ console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
478
+
479
+ const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
480
+ if (existsSync(codexPluginSrc)) {
481
+ copyDir(codexPluginSrc, join(pluginDest, '.codex-plugin'));
482
+ console.log(` Plugin Manifest updated -> ${join(pluginDest, '.codex-plugin')}`);
356
483
  }
357
- mkdirSync(dirname(configPath), { recursive: true });
358
- if (existing && !existing.endsWith('\n')) existing += '\n';
359
- writeFileSync(configPath, existing + section);
360
- console.log(` Config updated: ${configPath}`);
484
+
485
+ ensureCodexConfigSection(mcpServerAbsPath);
486
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
361
487
  }
362
488
 
363
489
  function uninstallCodexDesktop() {
@@ -388,32 +514,28 @@ function uninstallCodexDesktop() {
388
514
  if (removeIfExists(codexDesktopPluginsDir())) {
389
515
  console.log(' Removed MCP server and safety policy');
390
516
  }
391
- const configPath = codexConfigToml();
392
- if (existsSync(configPath)) {
393
- const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
394
- const out = [];
395
- let skip = false;
396
- for (const line of lines) {
397
- if (/^\[mcp_servers\.huaweicloud-devkit(\]|\.)/.test(line)) { skip = true; continue; }
398
- if (skip && line.startsWith('[')) skip = false;
399
- if (!skip) out.push(line);
400
- }
401
- while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
402
- writeFileSync(configPath, out.join('\n') + (out.length > 0 ? '\n' : ''));
403
- console.log(' Config cleaned');
404
- }
517
+ removeCodexConfigSection();
405
518
  }
406
519
 
407
520
  function registerCodeartsMcp(configPath) {
408
521
  const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
522
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
523
+ const hcloudBin = findHcloudBin();
524
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
409
525
  let config = {};
410
526
  if (existsSync(configPath)) {
411
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
527
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
528
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
529
+ return;
530
+ }
531
+ const existing = config.mcpServers?.['huaweicloud-devkit'];
532
+ if (existing && existing.command === 'node'
533
+ && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
534
+ console.log(` MCP config unchanged: ${configPath}`);
535
+ return;
536
+ }
412
537
  }
413
538
  config.mcpServers = config.mcpServers || {};
414
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
415
- const hcloudBin = findHcloudBin();
416
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
417
539
  config.mcpServers['huaweicloud-devkit'] = {
418
540
  command: 'node',
419
541
  args: [mcpPath],
@@ -445,6 +567,28 @@ async function installCodeArts() {
445
567
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
446
568
  }
447
569
 
570
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
571
+ async function updateCodeArts() {
572
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
573
+ const srcDir = join(PLUGIN_ROOT, 'src');
574
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
575
+ const pluginDest = codeartsPluginsDir();
576
+
577
+ for (const dir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
578
+ copyDir(skillsSrc, dir);
579
+ const stale = pruneStale(dir, skillsSrc);
580
+ console.log(` Skills updated -> ${dir}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
581
+ }
582
+ copyDir(srcDir, join(pluginDest, 'src'));
583
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
584
+ copyDir(safetyDir, join(pluginDest, 'safety'));
585
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
586
+ registerCodeartsMcp(codeartsMcpSettingsFile());
587
+ registerCodeartsMcp(codeartsProjectMcpSettingsFile());
588
+ mkdirSync(pluginDest, { recursive: true });
589
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
590
+ }
591
+
448
592
  function uninstallCodeArts() {
449
593
  let removed = 0;
450
594
  for (const skillsDir of [codeartsSkillsDir(), codeartsProjectSkillsDir()]) {
@@ -494,6 +638,38 @@ function codeartsStatus() {
494
638
  }
495
639
  }
496
640
 
641
+ // Returns true when the config file was written, false when it was already correct.
642
+ function ensureWorkbuddyMcpConfig() {
643
+ const configPath = workbuddyMcpConfigFile();
644
+ const mcpPath = join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
645
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
646
+ const hcloudBin = findHcloudBin();
647
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
648
+ let config = {};
649
+ if (existsSync(configPath)) {
650
+ try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {
651
+ console.log(` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`);
652
+ return false;
653
+ }
654
+ const existing = config.mcpServers?.['huaweicloud-devkit'];
655
+ if (existing && existing.command === 'node'
656
+ && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
657
+ console.log(` MCP config unchanged: ${configPath}`);
658
+ return false;
659
+ }
660
+ }
661
+ config.mcpServers = config.mcpServers || {};
662
+ config.mcpServers['huaweicloud-devkit'] = {
663
+ command: 'node',
664
+ args: [mcpPath],
665
+ env,
666
+ };
667
+ mkdirSync(dirname(configPath), { recursive: true });
668
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
669
+ console.log(` MCP config updated: ${configPath}`);
670
+ return true;
671
+ }
672
+
497
673
  async function installWorkBuddy() {
498
674
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
499
675
  const srcDir = join(PLUGIN_ROOT, 'src');
@@ -508,25 +684,26 @@ async function installWorkBuddy() {
508
684
  copyDir(safetyDir, join(pluginDest, 'safety'));
509
685
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
510
686
 
511
- // Write MCP config to ~/.workbuddy/mcp.json
512
- const mcpPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
513
- const configPath = workbuddyMcpConfigFile();
514
- let config = {};
515
- if (existsSync(configPath)) {
516
- try { config = JSON.parse(readFileSync(configPath, 'utf8')); } catch {}
517
- }
518
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
519
- const hcloudBin = findHcloudBin();
520
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
521
- config.mcpServers = config.mcpServers || {};
522
- config.mcpServers['huaweicloud-devkit'] = {
523
- command: 'node',
524
- args: [mcpPath],
525
- env,
526
- };
527
- mkdirSync(dirname(configPath), { recursive: true });
528
- writeFileSync(configPath, JSON.stringify(config, null, 2));
529
- console.log(` MCP config updated: ${configPath}`);
687
+ ensureWorkbuddyMcpConfig();
688
+ }
689
+
690
+ // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
691
+ async function updateWorkBuddy() {
692
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
693
+ const srcDir = join(PLUGIN_ROOT, 'src');
694
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
695
+ const pluginDest = workbuddyPluginsDir();
696
+
697
+ copyDir(skillsSrc, workbuddySkillsDir());
698
+ const stale = pruneStale(workbuddySkillsDir(), skillsSrc);
699
+ console.log(` Skills updated -> ${workbuddySkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
700
+ copyDir(srcDir, join(pluginDest, 'src'));
701
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
702
+ copyDir(safetyDir, join(pluginDest, 'safety'));
703
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
704
+ ensureWorkbuddyMcpConfig();
705
+ mkdirSync(pluginDest, { recursive: true });
706
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
530
707
  }
531
708
 
532
709
  function uninstallWorkBuddy() {
@@ -905,13 +1082,105 @@ async function cmdUpdate() {
905
1082
  console.log(BANNER);
906
1083
  const target = parseTarget();
907
1084
 
908
- if (target === 'opencode' || target === 'all') {
909
- if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))
910
- && !existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))
911
- && !codexStatus()) {
1085
+ if (target === 'opencode') {
1086
+ if (!existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1087
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1088
+ return;
1089
+ }
1090
+ console.log('[OpenCode]');
1091
+ await updateOpenCode();
1092
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1093
+ console.log(`\x1b[33mMCP 工具在重启 OpenCode 会话后才生效。\x1b[0m`);
1094
+ return;
1095
+ }
1096
+
1097
+ if (target === 'codex-desktop') {
1098
+ if (!existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1099
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1100
+ return;
1101
+ }
1102
+ console.log('[Codex Desktop]');
1103
+ await updateCodexDesktop();
1104
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1105
+ console.log(`\x1b[33mMCP 工具在重启 Codex Desktop 会话后才生效。\x1b[0m`);
1106
+ return;
1107
+ }
1108
+
1109
+ if (target === 'codex') {
1110
+ if (!hasCodexCLI()) {
1111
+ console.log(` \x1b[31mCodex CLI not found.\x1b[0m`);
1112
+ if (process.platform === 'win32') {
1113
+ console.log(` \x1b[33mTip: use --target codex-desktop for Codex Desktop on Windows\x1b[0m`);
1114
+ }
1115
+ console.log(` \x1b[31mInstall Codex CLI: https://github.com/openai/codex-cli\x1b[0m`);
1116
+ process.exitCode = 1;
1117
+ return;
1118
+ }
1119
+ console.log('[Codex]');
1120
+ installCodex();
1121
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1122
+ console.log(`\x1b[33mRestart the Codex session for changes to take effect.\x1b[0m`);
1123
+ return;
1124
+ }
1125
+
1126
+ if (target === 'codearts') {
1127
+ if (!existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1128
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1129
+ return;
1130
+ }
1131
+ console.log('[CodeArts]');
1132
+ await updateCodeArts();
1133
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1134
+ console.log(`\x1b[33mMCP 工具在重启 CodeArts 会话后才生效。\x1b[0m`);
1135
+ return;
1136
+ }
1137
+
1138
+ if (target === 'workbuddy') {
1139
+ if (!existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
912
1140
  console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
913
1141
  return;
914
1142
  }
1143
+ console.log('[WorkBuddy]');
1144
+ await updateWorkBuddy();
1145
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1146
+ console.log(`\x1b[33mMCP 工具在重启 WorkBuddy 会话后才生效。\x1b[0m`);
1147
+ return;
1148
+ }
1149
+
1150
+ if (target === 'all') {
1151
+ let updatedAny = false;
1152
+ if (existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
1153
+ console.log('[OpenCode]');
1154
+ await updateOpenCode();
1155
+ updatedAny = true;
1156
+ }
1157
+ if (existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
1158
+ console.log('\n[Codex Desktop]');
1159
+ await updateCodexDesktop();
1160
+ updatedAny = true;
1161
+ }
1162
+ if (existsSync(join(codeartsPluginsDir(), 'src', 'mcp-server.mjs'))) {
1163
+ console.log('\n[CodeArts]');
1164
+ await updateCodeArts();
1165
+ updatedAny = true;
1166
+ }
1167
+ if (existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
1168
+ console.log('\n[WorkBuddy]');
1169
+ await updateWorkBuddy();
1170
+ updatedAny = true;
1171
+ }
1172
+ if (codexStatus()) {
1173
+ console.log('\n[Codex]');
1174
+ installCodex();
1175
+ updatedAny = true;
1176
+ }
1177
+ if (!updatedAny) {
1178
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
1179
+ return;
1180
+ }
1181
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
1182
+ console.log(`\x1b[33mMCP 工具在重启各 agent 会话后才生效。\x1b[0m`);
1183
+ return;
915
1184
  }
916
1185
 
917
1186
  await cmdUninstall();
@@ -6,7 +6,7 @@ import { join, dirname } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { homedir } from 'node:os';
8
8
  import { searchMarketplace } from './search-market.mjs';
9
- import { execOneShot, execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
9
+ import { execWithSession, closeSession, DEFAULT_WORKSPACE_ID } from './sandbox/session-manager.mjs';
10
10
  import { hdkitCheckUser, hdkitSignAgreement, hdkitConnect, hdkitCredentials, hdkitRelease } from './sandbox/hdkitservice-api.mjs';
11
11
  import { getAuthStatus, syncAuth } from './auth/service.mjs';
12
12
  import { readGlobalCredentials, writeObsConfig as writeObsConfigFile } from './auth/credentials.mjs';
@@ -309,20 +309,6 @@ export const TOOL_DEFINITIONS = [
309
309
  },
310
310
  },
311
311
  },
312
- {
313
- name: 'huaweicloud_sandbox_exec',
314
- description: 'Execute a command on a Huawei Cloud workspace terminal via hwlink (one-shot, no session reuse). Each call creates a new connection. Shell state does NOT persist across calls.',
315
- inputSchema: {
316
- type: 'object',
317
- required: ['command'],
318
- properties: {
319
- command: { type: 'string', description: 'The shell command to execute on the remote workspace' },
320
- workspace_id: { type: 'string', description: 'The workspace ID' },
321
- username: { type: 'string', description: 'Login username for the remote terminal (default: root)' },
322
- timeout_ms: { type: 'number', description: 'Execution timeout in milliseconds (default: 30000)' },
323
- },
324
- },
325
- },
326
312
  {
327
313
  name: 'huaweicloud_sandbox_exec_with_session',
328
314
  description: 'Execute a command on a workspace terminal with session reuse (state persists across calls). Shell state (cd, env vars, aliases) carries over between calls.',
@@ -458,13 +444,6 @@ export async function callTool(name, args = {}) {
458
444
  return getAuthStatus(args.target || 'all');
459
445
  case 'huaweicloud_auth_sync':
460
446
  return syncAuth(args.target || 'all');
461
- case 'huaweicloud_sandbox_exec': {
462
- const sandboxWsId1 = args.workspace_id || DEFAULT_WORKSPACE_ID;
463
- const sandboxUser1 = args.username || 'root';
464
- const sandboxTimeout1 = args.timeout_ms || 30000;
465
- const sandboxResult1 = await execOneShot(sandboxWsId1, args.command, sandboxUser1, sandboxTimeout1);
466
- return { stdout: sandboxResult1.stdout, exitCode: sandboxResult1.exitCode };
467
- }
468
447
  case 'huaweicloud_sandbox_exec_with_session': {
469
448
  const sandboxWsId2 = args.workspace_id || DEFAULT_WORKSPACE_ID;
470
449
  const sandboxUser2 = args.username || 'root';