huaweicloud-devkit 1.0.2-next.9 → 1.0.2

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 (36) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/README.md +85 -51
  3. package/README.zh-CN.md +85 -49
  4. package/package.json +11 -5
  5. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +4 -17
  6. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +4 -17
  7. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +4 -17
  8. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +30 -0
  9. package/plugins/huaweicloud-core/.mcp.json +1 -2
  10. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +4 -18
  11. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +32 -9
  12. package/plugins/huaweicloud-core/openclaw.plugin.json +20 -0
  13. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +67 -20
  15. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +11 -0
  16. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +16 -0
  17. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +27 -24
  18. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +12 -0
  19. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +103 -2
  20. package/plugins/huaweicloud-core/src/auth/credentials.mjs +65 -0
  21. package/plugins/huaweicloud-core/src/mcp-server.mjs +31 -4
  22. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +6 -1
  23. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +7 -2
  24. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +14 -4
  25. package/plugins/huaweicloud-core/src/sandbox/sandbox-file-server.py +45 -0
  26. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +432 -10
  27. package/plugins/huaweicloud-core/src/setup-cli.mjs +823 -22
  28. package/plugins/huaweicloud-core/src/tools.mjs +239 -27
  29. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +18 -4
  30. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +9 -0
  31. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +9 -0
  32. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +9 -0
  33. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +9 -0
  34. package/plugins/huaweicloud-core/src/ws-exec/hwlink-tunnel-channel.mjs +267 -0
  35. package/plugins/huaweicloud-core/src/ws-exec/index.js +3 -0
  36. package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +9 -5
@@ -13,6 +13,8 @@ import { fileURLToPath } from 'node:url';
13
13
  import { homedir, platform } from 'node:os';
14
14
  import { createInterface } from 'node:readline';
15
15
  import { spawnSync } from 'node:child_process';
16
+ import { randomUUID } from 'node:crypto';
17
+ import Database from 'better-sqlite3';
16
18
  import { getAuthStatus, syncAuth } from './auth/service.mjs';
17
19
  import { SUPPORTED_AGENT_TARGETS } from './auth/agent-registration.mjs';
18
20
  import {
@@ -126,6 +128,242 @@ function dshPluginsDir() {
126
128
  return join(dshRoot(), 'huaweicloud-plugins');
127
129
  }
128
130
 
131
+ function readOfficeaceRegistryInstallDir() {
132
+ if (platform() !== 'win32') return null;
133
+ try {
134
+ const r = spawnSync('reg', ['query', 'HKCU\\SOFTWARE\\OfficeAce\\OfficeAce', '/v', 'InstallDir'], {
135
+ encoding: 'utf8',
136
+ windowsHide: true,
137
+ timeout: 5000,
138
+ });
139
+ if (r.status === 0) {
140
+ const m = r.stdout.match(/InstallDir\s+REG_SZ\s+(.+)/);
141
+ if (m) return m[1].trim();
142
+ }
143
+ } catch {}
144
+ return null;
145
+ }
146
+
147
+ function officeaceCapabilitiesDir() {
148
+ const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
149
+ if (configRoot && existsSync(join(configRoot, 'capabilities.json'))) return configRoot;
150
+ const regDir = readOfficeaceRegistryInstallDir();
151
+ if (regDir) {
152
+ const dir = join(regDir, '.office-claw');
153
+ if (existsSync(join(dir, 'capabilities.json'))) return dir;
154
+ }
155
+ if (platform() === 'win32') {
156
+ const bases = [process.env.ProgramFiles, 'C:\\Program Files', 'D:\\Program Files'];
157
+ if (process.env.LOCALAPPDATA) bases.push(join(process.env.LOCALAPPDATA, 'Programs'));
158
+ for (const base of bases) {
159
+ if (!base) continue;
160
+ const dir = join(base, 'OfficeAce', '.office-claw');
161
+ if (existsSync(join(dir, 'capabilities.json'))) return dir;
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ function officeaceCapabilitiesDirSafe() {
168
+ return officeaceCapabilitiesDir() || join(homedir(), '.office-claw');
169
+ }
170
+ function officeaceCapabilitiesFile() {
171
+ return join(officeaceCapabilitiesDirSafe(), 'capabilities.json');
172
+ }
173
+ function officeaceSkillsDir() {
174
+ return join(officeaceCapabilitiesDirSafe(), 'skills');
175
+ }
176
+ function officeacePluginsDir() {
177
+ return join(officeaceCapabilitiesDirSafe(), 'huaweicloud-plugins');
178
+ }
179
+
180
+ function officeaceSqlitePath() {
181
+ const capDir = officeaceCapabilitiesDirSafe();
182
+ return join(resolve(capDir, '..'), 'data', 'mcp-connectors.sqlite');
183
+ }
184
+
185
+ function openOfficeaceDb() {
186
+ return new Database(officeaceSqlitePath());
187
+ }
188
+
189
+ function officeaceGetOwnerUserId() {
190
+ if (!existsSync(officeaceSqlitePath())) return null;
191
+ try {
192
+ const db = openOfficeaceDb();
193
+ const row = db.prepare('SELECT owner_user_id FROM mcp_connectors WHERE owner_user_id IS NOT NULL LIMIT 1').get();
194
+ db.close();
195
+ return row?.owner_user_id || null;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ function ensureOfficeaceMcpInSqlite() {
202
+ const dbPath = officeaceSqlitePath();
203
+ if (!existsSync(dbPath)) {
204
+ console.log(` \x1b[31mOfficeAce database not found: ${dbPath}\x1b[0m`);
205
+ console.log(` \x1b[33mPlease ensure OfficeAce is installed and has been launched at least once.\x1b[0m`);
206
+ return false;
207
+ }
208
+
209
+ const mcpPath = join(officeacePluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
210
+ const env = [{ key: 'HUAWEICLOUD_AGENT_TOOLKIT_MODE', value: 'local', sensitive: false }];
211
+ const hcloudBin = findHcloudBin();
212
+ if (hcloudBin) env.push({ key: 'HCLOUD_BIN', value: hcloudBin.replace(/\\/g, '/'), sensitive: false });
213
+
214
+ const now = Date.now();
215
+ let db;
216
+ try {
217
+ db = openOfficeaceDb();
218
+
219
+ const existing = db
220
+ .prepare("SELECT id, command, args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'")
221
+ .get();
222
+
223
+ const argsJson = JSON.stringify([mcpPath]);
224
+ const envJson = JSON.stringify(env);
225
+
226
+ if (existing) {
227
+ if (existing.command === 'node' && existing.args_json === argsJson) {
228
+ console.log(` MCP config unchanged: ${dbPath}`);
229
+ db.close();
230
+ return true;
231
+ }
232
+ db.prepare(
233
+ 'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
234
+ ).run('node', argsJson, envJson, now, 'disconnected', existing.id);
235
+ console.log(` MCP config updated: ${dbPath}`);
236
+ } else {
237
+ const ownerUserId = officeaceGetOwnerUserId();
238
+ if (!ownerUserId) {
239
+ console.log(` \x1b[31mCannot determine owner_user_id from database\x1b[0m`);
240
+ db.close();
241
+ return false;
242
+ }
243
+ db.prepare(
244
+ `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)
245
+ VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
246
+ ).run(randomUUID(), ownerUserId, argsJson, envJson, now, now);
247
+ console.log(` MCP config created: ${dbPath}`);
248
+ }
249
+ db.close();
250
+ return true;
251
+ } catch (err) {
252
+ if (db) {
253
+ try {
254
+ db.close();
255
+ } catch {}
256
+ }
257
+ console.log(` \x1b[31mFailed to write MCP config: ${err.message}\x1b[0m`);
258
+ return false;
259
+ }
260
+ }
261
+
262
+ function removeOfficeaceMcpFromSqlite() {
263
+ const dbPath = officeaceSqlitePath();
264
+ if (!existsSync(dbPath)) return;
265
+ let db;
266
+ try {
267
+ db = openOfficeaceDb();
268
+ db.prepare(
269
+ "DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
270
+ ).run();
271
+ const result2 = db.prepare("DELETE FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").run();
272
+ if (result2.changes > 0) {
273
+ console.log(` MCP config removed: ${dbPath}`);
274
+ }
275
+ db.close();
276
+ } catch (err) {
277
+ if (db) {
278
+ try {
279
+ db.close();
280
+ } catch {}
281
+ }
282
+ console.log(` \x1b[31mFailed to remove MCP config: ${err.message}\x1b[0m`);
283
+ }
284
+ }
285
+
286
+ function readCapabilitiesJson() {
287
+ const capFile = officeaceCapabilitiesFile();
288
+ if (!existsSync(capFile)) return { capabilities: [] };
289
+ try {
290
+ return JSON.parse(readFileSync(capFile, 'utf8'));
291
+ } catch {
292
+ return { capabilities: [] };
293
+ }
294
+ }
295
+
296
+ function writeCapabilitiesJson(config) {
297
+ mkdirSync(officeaceCapabilitiesDirSafe(), { recursive: true });
298
+ writeFileSync(officeaceCapabilitiesFile(), JSON.stringify(config, null, 2));
299
+ }
300
+
301
+ function removeOfficeaceSkillCapabilities() {
302
+ const capFile = officeaceCapabilitiesFile();
303
+ if (!existsSync(capFile)) return;
304
+ let config;
305
+ try {
306
+ config = JSON.parse(readFileSync(capFile, 'utf8'));
307
+ } catch {
308
+ return;
309
+ }
310
+ if (!Array.isArray(config.capabilities)) return;
311
+ const origLen = config.capabilities.length;
312
+ config.capabilities = config.capabilities.filter(
313
+ (c) =>
314
+ !(
315
+ (c.id === 'huaweicloud-core' && c.type === 'skill') ||
316
+ (c.source === 'custom' && c.id && c.id.startsWith('huawei'))
317
+ ),
318
+ );
319
+ if (config.capabilities.length !== origLen) {
320
+ writeCapabilitiesJson(config);
321
+ console.log(` Skill capabilities cleaned: ${capFile}`);
322
+ }
323
+ }
324
+
325
+ function registerOfficeaceSkillEntries() {
326
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
327
+ if (!existsSync(skillsSrc)) return;
328
+ const skillNames = readdirSync(skillsSrc, { withFileTypes: true })
329
+ .filter((d) => d.isDirectory() && d.name.startsWith('huawei'))
330
+ .map((d) => d.name);
331
+
332
+ const config = readCapabilitiesJson();
333
+ let changed = false;
334
+
335
+ for (const name of skillNames) {
336
+ const existingIdx = config.capabilities.findIndex((c) => c.id === name && c.type === 'skill');
337
+ if (existingIdx >= 0) continue;
338
+ config.capabilities.push({
339
+ id: name,
340
+ type: 'skill',
341
+ enabled: true,
342
+ source: 'custom',
343
+ selfEvolution: 'suggest',
344
+ followGlobalSelfEvolution: true,
345
+ });
346
+ changed = true;
347
+ }
348
+
349
+ if (!config.capabilities.some((c) => c.id === 'huaweicloud-core' && c.type === 'skill')) {
350
+ config.capabilities.push({
351
+ id: 'huaweicloud-core',
352
+ type: 'skill',
353
+ enabled: true,
354
+ source: 'custom',
355
+ selfEvolution: 'suggest',
356
+ followGlobalSelfEvolution: true,
357
+ });
358
+ changed = true;
359
+ }
360
+
361
+ if (changed) {
362
+ writeCapabilitiesJson(config);
363
+ console.log(` Skill entries registered: ${officeaceCapabilitiesFile()}`);
364
+ }
365
+ }
366
+
129
367
  const DSH_MCP_PATCH_START = '# HuaweiCloud DevKit DSH integration start';
130
368
  const DSH_MCP_PATCH_END = '# HuaweiCloud DevKit DSH integration end';
131
369
 
@@ -164,14 +402,14 @@ function printSandboxWarning(reason) {
164
402
  console.log(`\x1b[31m 请任选其一继续:\x1b[0m`);
165
403
  console.log(`\x1b[31m A. 在码道外的终端安装并使用 KooCLI (推荐):`);
166
404
  console.log(`\x1b[31m https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`);
167
- console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 权限 → Bash 模式)`);
405
+ console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 对话流 → 智能体 终端命令运行模式 → 自动运行)`);
168
406
  console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
169
407
  }
170
408
 
171
409
  function checkNode() {
172
410
  const v = process.versions.node.split('.').map(Number);
173
- if (v[0] < 20) {
174
- console.error(`\x1b[31mNode.js >= 20 required (current: ${process.version})\x1b[0m`);
411
+ if (v[0] < 22) {
412
+ console.error(`\x1b[31mNode.js >= 22 required (current: ${process.version})\x1b[0m`);
175
413
  process.exit(1);
176
414
  }
177
415
  console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
@@ -191,6 +429,35 @@ function copyDir(src, dest) {
191
429
  }
192
430
  }
193
431
 
432
+ function installRuntimeDeps(pluginsDir) {
433
+ const pkgJson = {
434
+ name: 'huaweicloud-devkit',
435
+ version: pkgVersion,
436
+ type: 'module',
437
+ dependencies: { undici: '^8.10.0' },
438
+ };
439
+ mkdirSync(pluginsDir, { recursive: true });
440
+ writeFileSync(join(pluginsDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
441
+ const r = spawnSync('npm', ['install', '--omit=dev'], {
442
+ cwd: pluginsDir,
443
+ windowsHide: true,
444
+ stdio: 'pipe',
445
+ timeout: 120000,
446
+ });
447
+ const undiciDir = join(pluginsDir, 'node_modules', 'undici');
448
+ if (r.status === 0 && existsSync(undiciDir)) {
449
+ console.log(` Runtime deps installed -> ${join(pluginsDir, 'node_modules')}`);
450
+ } else {
451
+ const err = (r.stderr || '').toString().trim().split(/\r?\n/).slice(-2).join(' ');
452
+ console.log(` \x1b[33m[WARN]\x1b[0m npm install failed in ${pluginsDir}${err ? `: ${err}` : ''}`);
453
+ console.log(' Manual fix: cd %s && npm install', pluginsDir);
454
+ if (!existsSync(undiciDir)) {
455
+ console.log(` \x1b[31m[ERROR]\x1b[0m undici is NOT installed. MCP server will fail to start.`);
456
+ console.log(` Run manually: cd "${pluginsDir}" && npm install undici@^8.10.0`);
457
+ }
458
+ }
459
+ }
460
+
194
461
  function removeIfExists(p) {
195
462
  if (existsSync(p)) {
196
463
  try {
@@ -223,7 +490,8 @@ function updateOpenCodeConfig(pluginDir) {
223
490
  existing.type === 'local' &&
224
491
  Array.isArray(existing.command) &&
225
492
  existing.command[0] === 'node' &&
226
- existing.command[1] === mcpPath
493
+ existing.command[1] === mcpPath &&
494
+ existing.timeout === 300000
227
495
  ) {
228
496
  console.log(` OpenCode MCP config unchanged: ${configPath}`);
229
497
  return;
@@ -234,6 +502,7 @@ function updateOpenCodeConfig(pluginDir) {
234
502
  type: 'local',
235
503
  command: ['node', mcpPath],
236
504
  enabled: true,
505
+ timeout: 300000,
237
506
  };
238
507
  writeFileSync(configPath, JSON.stringify(config, null, 2));
239
508
  console.log(` OpenCode MCP config updated: ${configPath}`);
@@ -373,6 +642,7 @@ async function installOpenCode() {
373
642
  copyDir(safetyDir, join(pluginDest, 'safety'));
374
643
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
375
644
  updateOpenCodeConfig(pluginDest);
645
+ installRuntimeDeps(pluginDest);
376
646
  }
377
647
 
378
648
  function uninstallOpenCode() {
@@ -445,6 +715,7 @@ async function updateOpenCode() {
445
715
  updateOpenCodeConfig(pluginDest);
446
716
  mkdirSync(pluginDest, { recursive: true });
447
717
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
718
+ installRuntimeDeps(pluginDest);
448
719
  }
449
720
 
450
721
  function codexMcpServerPath() {
@@ -549,6 +820,7 @@ async function installCodexDesktop() {
549
820
  }
550
821
 
551
822
  ensureCodexConfigSection(mcpServerAbsPath);
823
+ installRuntimeDeps(codexDesktopPluginsDir());
552
824
  }
553
825
 
554
826
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -596,6 +868,7 @@ async function updateCodexDesktop() {
596
868
 
597
869
  ensureCodexConfigSection(mcpServerAbsPath);
598
870
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
871
+ installRuntimeDeps(pluginDest);
599
872
  }
600
873
 
601
874
  function uninstallCodexDesktop() {
@@ -645,7 +918,13 @@ function registerCodeartsMcp(configPath) {
645
918
  return;
646
919
  }
647
920
  const existing = config.mcpServers?.['huaweicloud-devkit'];
648
- if (existing && existing.command === 'node' && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
921
+ if (
922
+ existing &&
923
+ existing.command === 'node' &&
924
+ Array.isArray(existing.args) &&
925
+ existing.args[0] === mcpPath &&
926
+ existing.timeout === 300000
927
+ ) {
649
928
  console.log(` MCP config unchanged: ${configPath}`);
650
929
  return;
651
930
  }
@@ -656,6 +935,7 @@ function registerCodeartsMcp(configPath) {
656
935
  args: [mcpPath],
657
936
  env,
658
937
  enabled: true,
938
+ timeout: 300000,
659
939
  };
660
940
  mkdirSync(dirname(configPath), { recursive: true });
661
941
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -680,6 +960,7 @@ async function installCodeArts() {
680
960
 
681
961
  registerCodeartsMcp(codeartsMcpSettingsFile());
682
962
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
963
+ installRuntimeDeps(pluginDest);
683
964
  }
684
965
 
685
966
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -702,6 +983,7 @@ async function updateCodeArts() {
702
983
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
703
984
  mkdirSync(pluginDest, { recursive: true });
704
985
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
986
+ installRuntimeDeps(pluginDest);
705
987
  }
706
988
 
707
989
  function uninstallCodeArts() {
@@ -782,7 +1064,13 @@ function ensureWorkbuddyMcpConfig() {
782
1064
  return false;
783
1065
  }
784
1066
  const existing = config.mcpServers?.['huaweicloud-devkit'];
785
- if (existing && existing.command === 'node' && Array.isArray(existing.args) && existing.args[0] === mcpPath) {
1067
+ if (
1068
+ existing &&
1069
+ existing.command === 'node' &&
1070
+ Array.isArray(existing.args) &&
1071
+ existing.args[0] === mcpPath &&
1072
+ existing.timeout === 300000
1073
+ ) {
786
1074
  console.log(` MCP config unchanged: ${configPath}`);
787
1075
  return false;
788
1076
  }
@@ -792,6 +1080,7 @@ function ensureWorkbuddyMcpConfig() {
792
1080
  command: 'node',
793
1081
  args: [mcpPath],
794
1082
  env,
1083
+ timeout: 300000,
795
1084
  };
796
1085
  mkdirSync(dirname(configPath), { recursive: true });
797
1086
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -814,6 +1103,7 @@ async function installWorkBuddy() {
814
1103
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
815
1104
 
816
1105
  ensureWorkbuddyMcpConfig();
1106
+ installRuntimeDeps(pluginDest);
817
1107
  }
818
1108
 
819
1109
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -833,6 +1123,7 @@ async function updateWorkBuddy() {
833
1123
  ensureWorkbuddyMcpConfig();
834
1124
  mkdirSync(pluginDest, { recursive: true });
835
1125
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1126
+ installRuntimeDeps(pluginDest);
836
1127
  }
837
1128
 
838
1129
  function uninstallWorkBuddy() {
@@ -1097,6 +1388,7 @@ async function installDsh() {
1097
1388
  tryInstallDshMcpClient();
1098
1389
  mkdirSync(pluginDest, { recursive: true });
1099
1390
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1391
+ installRuntimeDeps(pluginDest);
1100
1392
  }
1101
1393
 
1102
1394
  async function updateDsh() {
@@ -1116,6 +1408,7 @@ async function updateDsh() {
1116
1408
  tryInstallDshMcpClient();
1117
1409
  mkdirSync(pluginDest, { recursive: true });
1118
1410
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1411
+ installRuntimeDeps(pluginDest);
1119
1412
  }
1120
1413
 
1121
1414
  function uninstallDsh() {
@@ -1166,6 +1459,335 @@ function dshStatus() {
1166
1459
  );
1167
1460
  }
1168
1461
 
1462
+ async function promptOfficeaceInstallDir() {
1463
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1464
+ const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
1465
+ while (true) {
1466
+ const path = (await ask(' Install directory: ')).trim();
1467
+ if (!path) {
1468
+ console.log(' \x1b[33mPath cannot be empty.\x1b[0m');
1469
+ continue;
1470
+ }
1471
+ const capFile = join(path, '.office-claw', 'capabilities.json');
1472
+ if (existsSync(capFile)) {
1473
+ rl.close();
1474
+ return path;
1475
+ }
1476
+ console.log(` \x1b[33mcapabilities.json not found at: ${capFile}\x1b[0m`);
1477
+ console.log(' Please verify the path and try again.');
1478
+ }
1479
+ }
1480
+
1481
+ async function installOfficeAce() {
1482
+ if (!officeaceCapabilitiesDir()) {
1483
+ if (process.stdin.isTTY) {
1484
+ console.log(' \x1b[33mOfficeAce install directory not found automatically.\x1b[0m');
1485
+ console.log(' Please enter the OfficeAce install directory.');
1486
+ const entered = await promptOfficeaceInstallDir();
1487
+ process.env.OFFICE_CLAW_CONFIG_ROOT = join(entered, '.office-claw');
1488
+ } else {
1489
+ console.log(
1490
+ ' \x1b[31mOfficeAce install directory not found. Please set OFFICE_CLAW_CONFIG_ROOT env var and retry.\x1b[0m',
1491
+ );
1492
+ return;
1493
+ }
1494
+ }
1495
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1496
+ const srcDir = join(PLUGIN_ROOT, 'src');
1497
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1498
+ const pluginDest = officeacePluginsDir();
1499
+
1500
+ copyDir(skillsSrc, officeaceSkillsDir());
1501
+ console.log(` Skills -> ${officeaceSkillsDir()}`);
1502
+
1503
+ copyDir(srcDir, join(pluginDest, 'src'));
1504
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
1505
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1506
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
1507
+
1508
+ installRuntimeDeps(pluginDest);
1509
+ ensureOfficeaceMcpInSqlite();
1510
+ registerOfficeaceSkillEntries();
1511
+ }
1512
+
1513
+ async function updateOfficeAce() {
1514
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1515
+ const srcDir = join(PLUGIN_ROOT, 'src');
1516
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1517
+ const pluginDest = officeacePluginsDir();
1518
+
1519
+ copyDir(skillsSrc, officeaceSkillsDir());
1520
+ const stale = pruneStale(officeaceSkillsDir(), skillsSrc);
1521
+ console.log(` Skills updated -> ${officeaceSkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
1522
+ copyDir(srcDir, join(pluginDest, 'src'));
1523
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
1524
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1525
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
1526
+ installRuntimeDeps(pluginDest);
1527
+ ensureOfficeaceMcpInSqlite();
1528
+ registerOfficeaceSkillEntries();
1529
+ mkdirSync(pluginDest, { recursive: true });
1530
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1531
+ }
1532
+
1533
+ function uninstallOfficeAce() {
1534
+ const skillsDir = officeaceSkillsDir();
1535
+ let removed = 0;
1536
+ if (existsSync(skillsDir)) {
1537
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1538
+ if (entry.name.startsWith('huawei')) {
1539
+ removeIfExists(join(skillsDir, entry.name));
1540
+ removed++;
1541
+ }
1542
+ }
1543
+ if (removed > 0) console.log(` Removed ${removed} skills`);
1544
+ try {
1545
+ if (readdirSync(skillsDir).length === 0) {
1546
+ rmSync(skillsDir, { recursive: true, force: true });
1547
+ console.log(` Removed empty skills directory: ${skillsDir}`);
1548
+ }
1549
+ } catch {}
1550
+ }
1551
+
1552
+ if (removeIfExists(officeacePluginsDir())) {
1553
+ console.log(' Removed MCP server and safety policy');
1554
+ }
1555
+ removeOfficeaceSkillCapabilities();
1556
+ removeOfficeaceMcpFromSqlite();
1557
+ }
1558
+
1559
+ function officeaceStatus() {
1560
+ const pluginDir = officeacePluginsDir();
1561
+ const skillsDir = officeaceSkillsDir();
1562
+ console.log(
1563
+ ` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1564
+ );
1565
+ console.log(
1566
+ ` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1567
+ );
1568
+ let skillCount = 0;
1569
+ if (existsSync(skillsDir)) {
1570
+ skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
1571
+ (d) => d.isDirectory() && d.name.startsWith('huawei'),
1572
+ ).length;
1573
+ }
1574
+ console.log(
1575
+ ` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
1576
+ );
1577
+ const dbPath = officeaceSqlitePath();
1578
+ if (existsSync(dbPath)) {
1579
+ try {
1580
+ const db = openOfficeaceDb();
1581
+ const row = db.prepare("SELECT enabled, status FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").get();
1582
+ db.close();
1583
+ if (row) {
1584
+ const statusIcon = row.status === 'connected' ? '\x1b[32m' : '\x1b[33m';
1585
+ console.log(` MCP config: ${statusIcon}${row.status}\x1b[0m (enabled: ${row.enabled ? 'yes' : 'no'})`);
1586
+ } else {
1587
+ console.log(` MCP config: \x1b[31mNot configured\x1b[0m`);
1588
+ }
1589
+ } catch {
1590
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
1591
+ }
1592
+ } else {
1593
+ console.log(` MCP config: \x1b[31mDatabase not found\x1b[0m`);
1594
+ }
1595
+ }
1596
+
1597
+ // ── Hermes Agent ──
1598
+
1599
+ function hermesHomeDir() {
1600
+ if (process.env.HERMES_HOME) return process.env.HERMES_HOME;
1601
+ // Hermes on Windows stores under LOCALAPPDATA, not ~/.hermes
1602
+ if (platform() === 'win32' && process.env.LOCALAPPDATA) {
1603
+ return join(process.env.LOCALAPPDATA, 'hermes');
1604
+ }
1605
+ return join(homedir(), '.hermes');
1606
+ }
1607
+
1608
+ function hermesSkillsDir() {
1609
+ return join(hermesHomeDir(), 'skills');
1610
+ }
1611
+
1612
+ function hermesPluginsDir() {
1613
+ return join(hermesHomeDir(), 'huaweicloud-plugins');
1614
+ }
1615
+
1616
+ function hermesConfigFile() {
1617
+ return join(hermesHomeDir(), 'config.yaml');
1618
+ }
1619
+
1620
+ // Returns true when the config file was written, false when it was already correct.
1621
+ function ensureHermesMcpConfig() {
1622
+ const configPath = hermesConfigFile();
1623
+ const mcpPath = join(hermesPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
1624
+ const hcloudBin = findHcloudBin();
1625
+
1626
+ const blockLines = [
1627
+ 'mcp_servers:',
1628
+ ' huaweicloud-devkit:',
1629
+ ' command: "node"',
1630
+ ` args: ["${mcpPath}"]`,
1631
+ ' env:',
1632
+ ' HUAWEICLOUD_AGENT_TOOLKIT_MODE: "local"',
1633
+ ];
1634
+ if (hcloudBin) {
1635
+ blockLines.push(` HCLOUD_BIN: "${hcloudBin.replace(/\\/g, '/')}"`);
1636
+ }
1637
+ const block = blockLines.join('\n');
1638
+
1639
+ let existing = '';
1640
+ if (existsSync(configPath)) {
1641
+ try {
1642
+ existing = readFileSync(configPath, 'utf8');
1643
+ } catch {}
1644
+ if (existing.includes('mcp_servers:') && existing.includes('huaweicloud-devkit')) {
1645
+ if (existing.includes(`args: ["${mcpPath}"]`)) {
1646
+ console.log(` MCP config unchanged: ${configPath}`);
1647
+ return false;
1648
+ }
1649
+ removeHermesMcpConfigBlock();
1650
+ existing = '';
1651
+ if (existsSync(configPath)) {
1652
+ try {
1653
+ existing = readFileSync(configPath, 'utf8');
1654
+ } catch {}
1655
+ }
1656
+ }
1657
+ }
1658
+ mkdirSync(dirname(configPath), { recursive: true });
1659
+ const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
1660
+ writeFileSync(configPath, newContent);
1661
+ console.log(` MCP config updated: ${configPath}`);
1662
+ return true;
1663
+ }
1664
+
1665
+ function removeHermesMcpConfigBlock() {
1666
+ const configPath = hermesConfigFile();
1667
+ if (!existsSync(configPath)) return;
1668
+ const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
1669
+ const out = [];
1670
+ let skip = false;
1671
+ for (const line of lines) {
1672
+ if (/^\s*huaweicloud-devkit\s*:/.test(line)) {
1673
+ skip = true;
1674
+ continue;
1675
+ }
1676
+ if (skip) {
1677
+ // Continue skipping indented lines (the server config block)
1678
+ if (/^\s{2,}/.test(line) && line.trim() !== '') continue;
1679
+ // Stop skipping at a top-level key or empty line
1680
+ skip = false;
1681
+ }
1682
+ // If we just skipped the only server under mcp_servers, remove the key too
1683
+ if (line.trim() === 'mcp_servers:') {
1684
+ // Peek ahead to check if this mcp_servers block only has huaweicloud-devkit
1685
+ const remaining = out.join('\n');
1686
+ // Remove trailing mcp_servers: if it's the last meaningful line
1687
+ continue; // skip the mcp_servers key for now, re-add if other servers exist
1688
+ }
1689
+ out.push(line);
1690
+ }
1691
+ // Reconstruct: add mcp_servers back if there are other servers
1692
+ const cleaned = out.join('\n').trimEnd();
1693
+ writeFileSync(configPath, cleaned ? `${cleaned}\n` : '');
1694
+ console.log(' MCP config cleaned');
1695
+ }
1696
+
1697
+ async function installHermes() {
1698
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1699
+ const srcDir = join(PLUGIN_ROOT, 'src');
1700
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1701
+ const pluginDest = hermesPluginsDir();
1702
+
1703
+ copyDir(skillsSrc, hermesSkillsDir());
1704
+ console.log(` Skills -> ${hermesSkillsDir()}`);
1705
+
1706
+ copyDir(srcDir, join(pluginDest, 'src'));
1707
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
1708
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1709
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
1710
+
1711
+ ensureHermesMcpConfig();
1712
+ installRuntimeDeps(pluginDest);
1713
+ }
1714
+
1715
+ async function updateHermes() {
1716
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1717
+ const srcDir = join(PLUGIN_ROOT, 'src');
1718
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1719
+ const pluginDest = hermesPluginsDir();
1720
+
1721
+ copyDir(skillsSrc, hermesSkillsDir());
1722
+ const stale = pruneStale(hermesSkillsDir(), skillsSrc);
1723
+ console.log(` Skills updated -> ${hermesSkillsDir()}${stale > 0 ? ` (removed ${stale} stale)` : ''}`);
1724
+ copyDir(srcDir, join(pluginDest, 'src'));
1725
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
1726
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1727
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
1728
+ ensureHermesMcpConfig();
1729
+ mkdirSync(pluginDest, { recursive: true });
1730
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1731
+ installRuntimeDeps(pluginDest);
1732
+ }
1733
+
1734
+ function uninstallHermes() {
1735
+ const skillsDir = hermesSkillsDir();
1736
+ let removed = 0;
1737
+ if (existsSync(skillsDir)) {
1738
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1739
+ if (entry.name.startsWith('huawei')) {
1740
+ removeIfExists(join(skillsDir, entry.name));
1741
+ removed++;
1742
+ }
1743
+ }
1744
+ if (removed > 0) console.log(` Removed ${removed} skills`);
1745
+ try {
1746
+ if (readdirSync(skillsDir).length === 0) {
1747
+ rmSync(skillsDir, { recursive: true, force: true });
1748
+ console.log(` Removed empty skills directory: ${skillsDir}`);
1749
+ }
1750
+ } catch {}
1751
+ }
1752
+
1753
+ if (removeIfExists(hermesPluginsDir())) {
1754
+ console.log(' Removed MCP server and safety policy');
1755
+ }
1756
+ removeHermesMcpConfigBlock();
1757
+ }
1758
+
1759
+ function hermesStatus() {
1760
+ const pluginDir = hermesPluginsDir();
1761
+ const skillsDir = hermesSkillsDir();
1762
+ console.log(
1763
+ ` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1764
+ );
1765
+ console.log(
1766
+ ` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1767
+ );
1768
+ let skillCount = 0;
1769
+ if (existsSync(skillsDir)) {
1770
+ skillCount = readdirSync(skillsDir, { withFileTypes: true }).filter(
1771
+ (d) => d.isDirectory() && d.name.startsWith('huawei'),
1772
+ ).length;
1773
+ }
1774
+ console.log(
1775
+ ` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
1776
+ );
1777
+ const configPath = hermesConfigFile();
1778
+ if (existsSync(configPath)) {
1779
+ try {
1780
+ const config = readFileSync(configPath, 'utf8');
1781
+ const configured = config.includes('mcp_servers:') && config.includes('huaweicloud-devkit');
1782
+ console.log(` MCP config: ${configured ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
1783
+ } catch {
1784
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
1785
+ }
1786
+ } else {
1787
+ console.log(` MCP config: \x1b[31mNot found\x1b[0m`);
1788
+ }
1789
+ }
1790
+
1169
1791
  function opencodeStatus() {
1170
1792
  const pluginDir = opencodePluginsDir();
1171
1793
  const skillsDir = opencodeSkillsDir();
@@ -1210,6 +1832,9 @@ function autoDetectTarget() {
1210
1832
  return existsSync(dsh);
1211
1833
  },
1212
1834
  ],
1835
+ ['officeace', () => existsSync(officeaceCapabilitiesDir())],
1836
+ ['hermes', () => existsSync(hermesHomeDir())],
1837
+ ['openclaw', () => existsSync(join(homedir(), '.openclaw'))],
1213
1838
  ];
1214
1839
  const detected = checks.filter(([, check]) => check()).map(([name]) => name);
1215
1840
  if (detected.length === 0) {
@@ -1258,6 +1883,18 @@ async function cmdInstall() {
1258
1883
  console.log('\n[DSH]');
1259
1884
  await installDsh();
1260
1885
  }
1886
+ if (target === 'officeace' || target === 'all') {
1887
+ console.log('\n[OfficeAce]');
1888
+ await installOfficeAce();
1889
+ }
1890
+ if (target === 'hermes' || target === 'all') {
1891
+ console.log('\n[Hermes Agent]');
1892
+ await installHermes();
1893
+ }
1894
+ if (target === 'openclaw' || target === 'all') {
1895
+ console.log('\n[OpenClaw]');
1896
+ await installCodexDesktop();
1897
+ }
1261
1898
  if (target === 'codex' || target === 'all') {
1262
1899
  console.log('\n[Codex]');
1263
1900
  if (!hasCodexCLI()) {
@@ -1293,13 +1930,26 @@ async function cmdInstall() {
1293
1930
  ? 'WorkBuddy'
1294
1931
  : target === 'dsh'
1295
1932
  ? 'DSH'
1296
- : 'OpenCode';
1933
+ : target === 'officeace'
1934
+ ? 'OfficeAce'
1935
+ : target === 'hermes'
1936
+ ? 'Hermes Agent'
1937
+ : target === 'openclaw'
1938
+ ? 'OpenClaw'
1939
+ : '当前 agent';
1297
1940
  const pad = ' '.repeat(24 - appName.length);
1298
- console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
1299
- console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
1300
- console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 → 重新打开,直接描述华为云任务即可 ║`);
1301
- console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
1302
- console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
1941
+ if (target === 'officeace') {
1942
+ console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
1943
+ console.log(`\x1b[1m\x1b[33m║ 打开连接器 → 我的连接器 → huaweicloud-devkit ║`);
1944
+ console.log(`\x1b[1m\x1b[33m║ → 连接 → 回到对话 → 输入框开启连接器 ║`);
1945
+ console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
1946
+ } else {
1947
+ console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
1948
+ console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
1949
+ console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 → 重新打开,直接描述华为云任务即可 ║`);
1950
+ console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
1951
+ console.log(`\x1b[1m\x1b[33m╚══════════════════════════════════════════════════════╝\x1b[0m`);
1952
+ }
1303
1953
 
1304
1954
  const hcloudOk = checkHcloud();
1305
1955
  if (!hcloudOk) {
@@ -1312,7 +1962,11 @@ async function cmdInstall() {
1312
1962
  console.log(`\n\x1b[1m下一步:\x1b[0m`);
1313
1963
  console.log(` 1. 配置统一凭据:npx huaweicloud-devkit auth init`);
1314
1964
  console.log(` 2. 配置代理(企业内网):npx huaweicloud-devkit proxy init`);
1315
- console.log(` 3. 重启 ${appName} 会话(MCP 工具重启后生效)`);
1965
+ if (target === 'officeace') {
1966
+ console.log(` 3. 打开连接器 → 我的连接器 → huaweicloud-devkit → 连接 → 回到对话 → 输入框开启连接器`);
1967
+ } else {
1968
+ console.log(` 3. 重启 ${appName} 会话(MCP 工具重启后生效)`);
1969
+ }
1316
1970
  console.log(` 4. 运行自检:npx huaweicloud-devkit doctor`);
1317
1971
 
1318
1972
  // Write install marker for doctor to detect
@@ -1323,9 +1977,13 @@ async function cmdInstall() {
1323
1977
  ? codeartsPluginsDir()
1324
1978
  : target === 'workbuddy'
1325
1979
  ? workbuddyPluginsDir()
1326
- : target === 'codex-desktop'
1327
- ? codexDesktopPluginsDir()
1328
- : opencodePluginsDir();
1980
+ : target === 'officeace'
1981
+ ? officeacePluginsDir()
1982
+ : target === 'openclaw'
1983
+ ? codexDesktopPluginsDir()
1984
+ : target === 'codex-desktop'
1985
+ ? codexDesktopPluginsDir()
1986
+ : opencodePluginsDir();
1329
1987
  mkdirSync(markerDir, { recursive: true });
1330
1988
  writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
1331
1989
  if (target === 'opencode' || target === 'all') {
@@ -1343,6 +2001,12 @@ async function cmdInstall() {
1343
2001
  if (target === 'dsh' || target === 'all') {
1344
2002
  console.log('Or describe your Huawei Cloud task in DSH');
1345
2003
  }
2004
+ if (target === 'officeace' || target === 'all') {
2005
+ console.log('Or describe your Huawei Cloud task in OfficeAce');
2006
+ }
2007
+ if (target === 'openclaw' || target === 'all') {
2008
+ console.log('Or describe your Huawei Cloud task in OpenClaw');
2009
+ }
1346
2010
  }
1347
2011
 
1348
2012
  async function cmdUninstall() {
@@ -1366,6 +2030,18 @@ async function cmdUninstall() {
1366
2030
  console.log('\n[DSH]');
1367
2031
  uninstallDsh();
1368
2032
  }
2033
+ if (target === 'officeace' || target === 'all') {
2034
+ console.log('\n[OfficeAce]');
2035
+ uninstallOfficeAce();
2036
+ }
2037
+ if (target === 'hermes' || target === 'all') {
2038
+ console.log('\n[Hermes Agent]');
2039
+ uninstallHermes();
2040
+ }
2041
+ if (target === 'openclaw' || target === 'all') {
2042
+ console.log('\n[OpenClaw]');
2043
+ uninstallCodexDesktop();
2044
+ }
1369
2045
  if (target === 'codex-desktop' || target === 'codex' || target === 'all') {
1370
2046
  console.log('\n[Codex]');
1371
2047
  uninstallCodexDesktop();
@@ -1414,6 +2090,34 @@ async function cmdStatus() {
1414
2090
  console.log('\n[DSH]');
1415
2091
  dshStatus();
1416
2092
  }
2093
+ if (target === 'officeace' || target === 'all') {
2094
+ console.log('\n[OfficeAce]');
2095
+ officeaceStatus();
2096
+ }
2097
+ if (target === 'hermes' || target === 'all') {
2098
+ console.log('\n[Hermes Agent]');
2099
+ hermesStatus();
2100
+ }
2101
+ if (target === 'openclaw' || target === 'all') {
2102
+ console.log('\n[OpenClaw]');
2103
+ const cdPluginDir = codexDesktopPluginsDir();
2104
+ const cdSkillsDir = codexDesktopSkillsDir();
2105
+ console.log(
2106
+ ` MCP Server: ${existsSync(join(cdPluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2107
+ );
2108
+ console.log(
2109
+ ` Safety Policy: ${existsSync(join(cdPluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2110
+ );
2111
+ let cdSkillCount = 0;
2112
+ if (existsSync(cdSkillsDir)) {
2113
+ cdSkillCount = readdirSync(cdSkillsDir, { withFileTypes: true }).filter(
2114
+ (d) => d.isDirectory() && d.name.startsWith('huawei'),
2115
+ ).length;
2116
+ }
2117
+ console.log(
2118
+ ` Skills: ${cdSkillCount > 0 ? `\x1b[32m${cdSkillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
2119
+ );
2120
+ }
1417
2121
  if (target === 'codex' || target === 'all') {
1418
2122
  console.log('\n[Codex]');
1419
2123
  if (!hasCodexCLI()) {
@@ -1446,7 +2150,7 @@ async function cmdDoctor() {
1446
2150
  }
1447
2151
 
1448
2152
  // Node.js
1449
- check('Node.js >= 20', process.versions.node.split('.')[0] >= 20, 'Run: nvm install 20 && nvm use 20');
2153
+ check('Node.js >= 22', process.versions.node.split('.')[0] >= 22, 'Run: nvm install 22 && nvm use 22');
1450
2154
 
1451
2155
  // MCP server — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH paths
1452
2156
  const opencodePluginDir = opencodePluginsDir();
@@ -1454,12 +2158,16 @@ async function cmdDoctor() {
1454
2158
  const codeartsPluginDir = codeartsPluginsDir();
1455
2159
  const workbuddyPluginDir = workbuddyPluginsDir();
1456
2160
  const dshPluginDir = dshPluginsDir();
2161
+ const officeacePluginDir = officeacePluginsDir();
2162
+ const hermesPluginDir = hermesPluginsDir();
1457
2163
  const mcpOk =
1458
2164
  existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ||
1459
2165
  existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ||
1460
2166
  existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs')) ||
1461
2167
  existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ||
1462
- existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs'));
2168
+ existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs')) ||
2169
+ existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs')) ||
2170
+ existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'));
1463
2171
  const mcpTarget = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs'))
1464
2172
  ? 'OpenCode'
1465
2173
  : existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs'))
@@ -1470,7 +2178,11 @@ async function cmdDoctor() {
1470
2178
  ? 'WorkBuddy'
1471
2179
  : existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs'))
1472
2180
  ? 'DSH'
1473
- : '';
2181
+ : existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs'))
2182
+ ? 'OfficeAce'
2183
+ : existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'))
2184
+ ? 'Hermes Agent'
2185
+ : '';
1474
2186
  check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
1475
2187
 
1476
2188
  if (mcpOk) {
@@ -1482,7 +2194,9 @@ async function cmdDoctor() {
1482
2194
  existsSync(join(codexPluginDir, 'safety', 'policy.json')) ||
1483
2195
  existsSync(join(codeartsPluginDir, 'safety', 'policy.json')) ||
1484
2196
  existsSync(join(workbuddyPluginDir, 'safety', 'policy.json')) ||
1485
- existsSync(join(dshPluginDir, 'safety', 'policy.json'));
2197
+ existsSync(join(dshPluginDir, 'safety', 'policy.json')) ||
2198
+ existsSync(join(officeacePluginDir, 'safety', 'policy.json')) ||
2199
+ existsSync(join(hermesPluginDir, 'safety', 'policy.json'));
1486
2200
  check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
1487
2201
 
1488
2202
  // MCP config — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH
@@ -1532,6 +2246,33 @@ async function cmdDoctor() {
1532
2246
  mcpConfigured = true;
1533
2247
  mcpCfgTarget = 'DSH';
1534
2248
  }
2249
+ if (!mcpConfigured) {
2250
+ const officeaceCfg = officeaceCapabilitiesFile();
2251
+ if (existsSync(officeaceCfg)) {
2252
+ try {
2253
+ const cfg = JSON.parse(readFileSync(officeaceCfg, 'utf8'));
2254
+ if (
2255
+ Array.isArray(cfg.capabilities) &&
2256
+ cfg.capabilities.some((c) => c.id === 'huaweicloud-devkit' && c.type === 'mcp')
2257
+ ) {
2258
+ mcpConfigured = true;
2259
+ mcpCfgTarget = 'OfficeAce';
2260
+ }
2261
+ } catch {}
2262
+ }
2263
+ }
2264
+ if (!mcpConfigured) {
2265
+ const hermesCfg = hermesConfigFile();
2266
+ if (existsSync(hermesCfg)) {
2267
+ try {
2268
+ const cfg = readFileSync(hermesCfg, 'utf8');
2269
+ if (cfg.includes('mcp_servers:') && cfg.includes('huaweicloud-devkit')) {
2270
+ mcpConfigured = true;
2271
+ mcpCfgTarget = 'Hermes Agent';
2272
+ }
2273
+ } catch {}
2274
+ }
2275
+ }
1535
2276
  check(
1536
2277
  'MCP configured',
1537
2278
  mcpConfigured,
@@ -1581,6 +2322,8 @@ async function cmdDoctor() {
1581
2322
  codeartsSkillsDir(),
1582
2323
  workbuddySkillsDir(),
1583
2324
  dshSkillsDir(),
2325
+ officeaceSkillsDir(),
2326
+ hermesSkillsDir(),
1584
2327
  ];
1585
2328
  let skillCount = 0;
1586
2329
  const missingSkills = [];
@@ -1636,6 +2379,7 @@ async function cmdDoctor() {
1636
2379
  { path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
1637
2380
  { path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
1638
2381
  { path: join(dshPluginsDir(), '.installed'), name: 'DSH' },
2382
+ { path: join(officeacePluginsDir(), '.installed'), name: 'OfficeAce' },
1639
2383
  ];
1640
2384
  for (const marker of installedMarkers) {
1641
2385
  if (existsSync(marker.path)) {
@@ -1729,6 +2473,42 @@ async function cmdUpdate() {
1729
2473
  return;
1730
2474
  }
1731
2475
 
2476
+ if (target === 'officeace') {
2477
+ if (!existsSync(join(officeacePluginsDir(), 'src', 'mcp-server.mjs'))) {
2478
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
2479
+ return;
2480
+ }
2481
+ console.log('[OfficeAce]');
2482
+ await updateOfficeAce();
2483
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
2484
+ console.log(`\x1b[33m打开连接器 → 我的连接器 → huaweicloud-devkit → 连接 → 回到对话 → 输入框开启连接器\x1b[0m`);
2485
+ return;
2486
+ }
2487
+
2488
+ if (target === 'hermes') {
2489
+ if (!existsSync(join(hermesPluginsDir(), 'src', 'mcp-server.mjs'))) {
2490
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
2491
+ return;
2492
+ }
2493
+ console.log('[Hermes Agent]');
2494
+ await updateHermes();
2495
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
2496
+ console.log(`\x1b[33mRestart Hermes Agent for changes to take effect.\x1b[0m`);
2497
+ return;
2498
+ }
2499
+
2500
+ if (target === 'openclaw') {
2501
+ if (!existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
2502
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
2503
+ return;
2504
+ }
2505
+ console.log('[OpenClaw]');
2506
+ await updateCodexDesktop();
2507
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
2508
+ console.log(`\x1b[33mRestart OpenClaw for changes to take effect.\x1b[0m`);
2509
+ return;
2510
+ }
2511
+
1732
2512
  if (target === 'all') {
1733
2513
  let updatedAny = false;
1734
2514
  if (existsSync(join(opencodePluginsDir(), 'src', 'mcp-server.mjs'))) {
@@ -1756,6 +2536,21 @@ async function cmdUpdate() {
1756
2536
  await updateDsh();
1757
2537
  updatedAny = true;
1758
2538
  }
2539
+ if (existsSync(join(officeacePluginsDir(), 'src', 'mcp-server.mjs'))) {
2540
+ console.log('\n[OfficeAce]');
2541
+ await updateOfficeAce();
2542
+ updatedAny = true;
2543
+ }
2544
+ if (existsSync(join(hermesPluginsDir(), 'src', 'mcp-server.mjs'))) {
2545
+ console.log('\n[Hermes Agent]');
2546
+ await updateHermes();
2547
+ updatedAny = true;
2548
+ }
2549
+ if (existsSync(join(codexDesktopPluginsDir(), 'src', 'mcp-server.mjs'))) {
2550
+ console.log('\n[OpenClaw]');
2551
+ await updateCodexDesktop();
2552
+ updatedAny = true;
2553
+ }
1759
2554
  if (codexStatus()) {
1760
2555
  console.log('\n[Codex]');
1761
2556
  installCodex();
@@ -2250,7 +3045,9 @@ async function main() {
2250
3045
  case '-h':
2251
3046
  default:
2252
3047
  console.log(BANNER);
2253
- console.log('Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|all>]\n');
3048
+ console.log(
3049
+ 'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|officeace|hermes|openclaw|all>]\n',
3050
+ );
2254
3051
  console.log('Commands:');
2255
3052
  console.log(' install Install skills, MCP server, safety policy');
2256
3053
  console.log(' uninstall Remove installed files');
@@ -2263,13 +3060,17 @@ async function main() {
2263
3060
  console.log(' proxy Manage proxy config: init | show | clear');
2264
3061
  console.log(' help Show this help');
2265
3062
  console.log('\nOptions:');
2266
- console.log(' --target Target agent: opencode (default), codex, codearts, workbuddy, dsh, all');
3063
+ console.log(
3064
+ ' --target Target agent: opencode (default), codex, codearts, workbuddy, dsh, officeace, hermes, openclaw, all',
3065
+ );
2267
3066
  console.log('\nExamples:');
2268
3067
  console.log(' npx huaweicloud-devkit install');
2269
3068
  console.log(' npx huaweicloud-devkit install --target codex');
2270
3069
  console.log(' npx huaweicloud-devkit install --target codearts');
2271
3070
  console.log(' npx huaweicloud-devkit install --target workbuddy');
2272
3071
  console.log(' npx huaweicloud-devkit install --target dsh');
3072
+ console.log(' npx huaweicloud-devkit install --target officeace');
3073
+ console.log(' npx huaweicloud-devkit install --target hermes');
2273
3074
  console.log(' npx huaweicloud-devkit install --target all');
2274
3075
  console.log(' npx huaweicloud-devkit auth init');
2275
3076
  console.log(' npx huaweicloud-devkit auth sync --target all');