huaweicloud-devkit 1.0.2-next.8 → 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 (38) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/LICENSE +201 -201
  3. package/README.md +71 -52
  4. package/README.zh-CN.md +71 -50
  5. package/bin/setup.cjs +0 -0
  6. package/package.json +9 -5
  7. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +2 -2
  8. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +2 -2
  9. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +2 -2
  10. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +30 -0
  11. package/plugins/huaweicloud-core/.mcp.json +1 -2
  12. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +2 -3
  13. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +159 -136
  14. package/plugins/huaweicloud-core/openclaw.plugin.json +20 -0
  15. package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +1 -1
  16. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +67 -20
  17. package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +11 -0
  18. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +16 -0
  19. package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +27 -24
  20. package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +12 -0
  21. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +79 -11
  22. package/plugins/huaweicloud-core/src/auth/credentials.mjs +65 -0
  23. package/plugins/huaweicloud-core/src/mcp-server.mjs +31 -4
  24. package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +6 -1
  25. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +7 -2
  26. package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +14 -4
  27. package/plugins/huaweicloud-core/src/sandbox/sandbox-file-server.py +45 -0
  28. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +432 -10
  29. package/plugins/huaweicloud-core/src/setup-cli.mjs +581 -87
  30. package/plugins/huaweicloud-core/src/tools.mjs +225 -33
  31. package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +18 -4
  32. package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +9 -0
  33. package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +9 -0
  34. package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +9 -0
  35. package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +9 -0
  36. package/plugins/huaweicloud-core/src/ws-exec/hwlink-tunnel-channel.mjs +267 -0
  37. package/plugins/huaweicloud-core/src/ws-exec/index.js +3 -0
  38. 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,93 +128,177 @@ 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
+
129
147
  function officeaceCapabilitiesDir() {
130
- if (process.env.OFFICEACE_HOME) return process.env.OFFICEACE_HOME;
131
- const dotDir = join(homedir(), '.office-claw');
132
- const dotCapFile = join(dotDir, 'capabilities.json');
133
- if (existsSync(dotCapFile)) return dotDir;
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
+ }
134
155
  if (platform() === 'win32') {
135
- for (const base of [process.env.ProgramFiles, 'C:\\Program Files', 'D:\\Program Files']) {
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) {
136
159
  if (!base) continue;
137
160
  const dir = join(base, 'OfficeAce', '.office-claw');
138
161
  if (existsSync(join(dir, 'capabilities.json'))) return dir;
139
162
  }
140
163
  }
141
- return dotDir;
164
+ return null;
165
+ }
166
+
167
+ function officeaceCapabilitiesDirSafe() {
168
+ return officeaceCapabilitiesDir() || join(homedir(), '.office-claw');
142
169
  }
143
170
  function officeaceCapabilitiesFile() {
144
- return join(officeaceCapabilitiesDir(), 'capabilities.json');
171
+ return join(officeaceCapabilitiesDirSafe(), 'capabilities.json');
145
172
  }
146
173
  function officeaceSkillsDir() {
147
- return join(officeaceCapabilitiesDir(), 'skills');
174
+ return join(officeaceCapabilitiesDirSafe(), 'skills');
148
175
  }
149
176
  function officeacePluginsDir() {
150
- return join(officeaceCapabilitiesDir(), 'huaweicloud-plugins');
177
+ return join(officeaceCapabilitiesDirSafe(), 'huaweicloud-plugins');
151
178
  }
152
179
 
153
- function readCapabilitiesJson() {
154
- const capFile = officeaceCapabilitiesFile();
155
- if (!existsSync(capFile)) return { capabilities: [] };
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;
156
191
  try {
157
- return JSON.parse(readFileSync(capFile, 'utf8'));
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;
158
196
  } catch {
159
- return { capabilities: [] };
197
+ return null;
160
198
  }
161
199
  }
162
200
 
163
- function writeCapabilitiesJson(config) {
164
- mkdirSync(officeaceCapabilitiesDir(), { recursive: true });
165
- writeFileSync(officeaceCapabilitiesFile(), JSON.stringify(config, null, 2));
166
- }
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
+ }
167
208
 
168
- function ensureOfficeaceCapabilities() {
169
- const capFile = officeaceCapabilitiesFile();
170
209
  const mcpPath = join(officeacePluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
171
- const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
210
+ const env = [{ key: 'HUAWEICLOUD_AGENT_TOOLKIT_MODE', value: 'local', sensitive: false }];
172
211
  const hcloudBin = findHcloudBin();
173
- if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
212
+ if (hcloudBin) env.push({ key: 'HCLOUD_BIN', value: hcloudBin.replace(/\\/g, '/'), sensitive: false });
174
213
 
175
- const config = readCapabilitiesJson();
176
- let changed = false;
214
+ const now = Date.now();
215
+ let db;
216
+ try {
217
+ db = openOfficeaceDb();
177
218
 
178
- // Ensure MCP server entry
179
- const mcpEntry = {
180
- id: 'huaweicloud-devkit',
181
- type: 'mcp',
182
- enabled: true,
183
- source: 'custom',
184
- mcpServer: {
185
- command: 'node',
186
- args: [mcpPath],
187
- env,
188
- },
189
- };
190
- const existingMcpIdx = config.capabilities.findIndex((c) => c.id === 'huaweicloud-devkit' && c.type === 'mcp');
191
- if (existingMcpIdx >= 0) {
192
- const existing = config.capabilities[existingMcpIdx];
193
- if (
194
- existing.mcpServer?.command === 'node' &&
195
- Array.isArray(existing.mcpServer?.args) &&
196
- existing.mcpServer.args[0] === mcpPath
197
- ) {
198
- console.log(` MCP config unchanged: ${capFile}`);
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}`);
199
236
  } else {
200
- config.capabilities[existingMcpIdx] = mcpEntry;
201
- changed = true;
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}`);
202
248
  }
203
- } else {
204
- config.capabilities.push(mcpEntry);
205
- changed = true;
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;
206
259
  }
260
+ }
207
261
 
208
- if (changed) {
209
- writeCapabilitiesJson(config);
210
- console.log(` MCP config updated: ${capFile}`);
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: [] };
211
293
  }
212
- return changed;
213
294
  }
214
295
 
215
- function removeOfficeaceCapabilities() {
296
+ function writeCapabilitiesJson(config) {
297
+ mkdirSync(officeaceCapabilitiesDirSafe(), { recursive: true });
298
+ writeFileSync(officeaceCapabilitiesFile(), JSON.stringify(config, null, 2));
299
+ }
300
+
301
+ function removeOfficeaceSkillCapabilities() {
216
302
  const capFile = officeaceCapabilitiesFile();
217
303
  if (!existsSync(capFile)) return;
218
304
  let config;
@@ -226,14 +312,13 @@ function removeOfficeaceCapabilities() {
226
312
  config.capabilities = config.capabilities.filter(
227
313
  (c) =>
228
314
  !(
229
- (c.id === 'huaweicloud-devkit' && c.type === 'mcp') ||
230
315
  (c.id === 'huaweicloud-core' && c.type === 'skill') ||
231
316
  (c.source === 'custom' && c.id && c.id.startsWith('huawei'))
232
317
  ),
233
318
  );
234
319
  if (config.capabilities.length !== origLen) {
235
320
  writeCapabilitiesJson(config);
236
- console.log(` Capabilities cleaned: ${capFile}`);
321
+ console.log(` Skill capabilities cleaned: ${capFile}`);
237
322
  }
238
323
  }
239
324
 
@@ -317,14 +402,14 @@ function printSandboxWarning(reason) {
317
402
  console.log(`\x1b[31m 请任选其一继续:\x1b[0m`);
318
403
  console.log(`\x1b[31m A. 在码道外的终端安装并使用 KooCLI (推荐):`);
319
404
  console.log(`\x1b[31m https://support.huaweicloud.com/qs-hcli/hcli_02_003.html`);
320
- console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 权限 → Bash 模式)`);
405
+ console.log(`\x1b[31m B. 在码道设置中关闭沙箱模式后重试 (设置 → 对话流 → 智能体 终端命令运行模式 → 自动运行)`);
321
406
  console.log(`\x1b[31m 关闭沙箱后重新运行: npx huaweicloud-devkit install-hcloud\x1b[0m`);
322
407
  }
323
408
 
324
409
  function checkNode() {
325
410
  const v = process.versions.node.split('.').map(Number);
326
- if (v[0] < 20) {
327
- 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`);
328
413
  process.exit(1);
329
414
  }
330
415
  console.log(` Node.js ${process.version} \x1b[32mOK\x1b[0m`);
@@ -344,6 +429,35 @@ function copyDir(src, dest) {
344
429
  }
345
430
  }
346
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
+
347
461
  function removeIfExists(p) {
348
462
  if (existsSync(p)) {
349
463
  try {
@@ -376,7 +490,8 @@ function updateOpenCodeConfig(pluginDir) {
376
490
  existing.type === 'local' &&
377
491
  Array.isArray(existing.command) &&
378
492
  existing.command[0] === 'node' &&
379
- existing.command[1] === mcpPath
493
+ existing.command[1] === mcpPath &&
494
+ existing.timeout === 300000
380
495
  ) {
381
496
  console.log(` OpenCode MCP config unchanged: ${configPath}`);
382
497
  return;
@@ -387,6 +502,7 @@ function updateOpenCodeConfig(pluginDir) {
387
502
  type: 'local',
388
503
  command: ['node', mcpPath],
389
504
  enabled: true,
505
+ timeout: 300000,
390
506
  };
391
507
  writeFileSync(configPath, JSON.stringify(config, null, 2));
392
508
  console.log(` OpenCode MCP config updated: ${configPath}`);
@@ -526,6 +642,7 @@ async function installOpenCode() {
526
642
  copyDir(safetyDir, join(pluginDest, 'safety'));
527
643
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
528
644
  updateOpenCodeConfig(pluginDest);
645
+ installRuntimeDeps(pluginDest);
529
646
  }
530
647
 
531
648
  function uninstallOpenCode() {
@@ -598,6 +715,7 @@ async function updateOpenCode() {
598
715
  updateOpenCodeConfig(pluginDest);
599
716
  mkdirSync(pluginDest, { recursive: true });
600
717
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
718
+ installRuntimeDeps(pluginDest);
601
719
  }
602
720
 
603
721
  function codexMcpServerPath() {
@@ -702,6 +820,7 @@ async function installCodexDesktop() {
702
820
  }
703
821
 
704
822
  ensureCodexConfigSection(mcpServerAbsPath);
823
+ installRuntimeDeps(codexDesktopPluginsDir());
705
824
  }
706
825
 
707
826
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -749,6 +868,7 @@ async function updateCodexDesktop() {
749
868
 
750
869
  ensureCodexConfigSection(mcpServerAbsPath);
751
870
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
871
+ installRuntimeDeps(pluginDest);
752
872
  }
753
873
 
754
874
  function uninstallCodexDesktop() {
@@ -798,7 +918,13 @@ function registerCodeartsMcp(configPath) {
798
918
  return;
799
919
  }
800
920
  const existing = config.mcpServers?.['huaweicloud-devkit'];
801
- 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
+ ) {
802
928
  console.log(` MCP config unchanged: ${configPath}`);
803
929
  return;
804
930
  }
@@ -809,6 +935,7 @@ function registerCodeartsMcp(configPath) {
809
935
  args: [mcpPath],
810
936
  env,
811
937
  enabled: true,
938
+ timeout: 300000,
812
939
  };
813
940
  mkdirSync(dirname(configPath), { recursive: true });
814
941
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -833,6 +960,7 @@ async function installCodeArts() {
833
960
 
834
961
  registerCodeartsMcp(codeartsMcpSettingsFile());
835
962
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
963
+ installRuntimeDeps(pluginDest);
836
964
  }
837
965
 
838
966
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -855,6 +983,7 @@ async function updateCodeArts() {
855
983
  registerCodeartsMcp(codeartsProjectMcpSettingsFile());
856
984
  mkdirSync(pluginDest, { recursive: true });
857
985
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
986
+ installRuntimeDeps(pluginDest);
858
987
  }
859
988
 
860
989
  function uninstallCodeArts() {
@@ -935,7 +1064,13 @@ function ensureWorkbuddyMcpConfig() {
935
1064
  return false;
936
1065
  }
937
1066
  const existing = config.mcpServers?.['huaweicloud-devkit'];
938
- 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
+ ) {
939
1074
  console.log(` MCP config unchanged: ${configPath}`);
940
1075
  return false;
941
1076
  }
@@ -945,6 +1080,7 @@ function ensureWorkbuddyMcpConfig() {
945
1080
  command: 'node',
946
1081
  args: [mcpPath],
947
1082
  env,
1083
+ timeout: 300000,
948
1084
  };
949
1085
  mkdirSync(dirname(configPath), { recursive: true });
950
1086
  writeFileSync(configPath, JSON.stringify(config, null, 2));
@@ -967,6 +1103,7 @@ async function installWorkBuddy() {
967
1103
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
968
1104
 
969
1105
  ensureWorkbuddyMcpConfig();
1106
+ installRuntimeDeps(pluginDest);
970
1107
  }
971
1108
 
972
1109
  // Incremental update: overwrite copied files, prune stale ones, and only touch the config when necessary.
@@ -986,6 +1123,7 @@ async function updateWorkBuddy() {
986
1123
  ensureWorkbuddyMcpConfig();
987
1124
  mkdirSync(pluginDest, { recursive: true });
988
1125
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1126
+ installRuntimeDeps(pluginDest);
989
1127
  }
990
1128
 
991
1129
  function uninstallWorkBuddy() {
@@ -1250,6 +1388,7 @@ async function installDsh() {
1250
1388
  tryInstallDshMcpClient();
1251
1389
  mkdirSync(pluginDest, { recursive: true });
1252
1390
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1391
+ installRuntimeDeps(pluginDest);
1253
1392
  }
1254
1393
 
1255
1394
  async function updateDsh() {
@@ -1269,6 +1408,7 @@ async function updateDsh() {
1269
1408
  tryInstallDshMcpClient();
1270
1409
  mkdirSync(pluginDest, { recursive: true });
1271
1410
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1411
+ installRuntimeDeps(pluginDest);
1272
1412
  }
1273
1413
 
1274
1414
  function uninstallDsh() {
@@ -1319,7 +1459,39 @@ function dshStatus() {
1319
1459
  );
1320
1460
  }
1321
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
+
1322
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
+ }
1323
1495
  const skillsSrc = join(PLUGIN_ROOT, 'skills');
1324
1496
  const srcDir = join(PLUGIN_ROOT, 'src');
1325
1497
  const safetyDir = join(PLUGIN_ROOT, 'safety');
@@ -1333,7 +1505,8 @@ async function installOfficeAce() {
1333
1505
  copyDir(safetyDir, join(pluginDest, 'safety'));
1334
1506
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
1335
1507
 
1336
- ensureOfficeaceCapabilities();
1508
+ installRuntimeDeps(pluginDest);
1509
+ ensureOfficeaceMcpInSqlite();
1337
1510
  registerOfficeaceSkillEntries();
1338
1511
  }
1339
1512
 
@@ -1350,7 +1523,8 @@ async function updateOfficeAce() {
1350
1523
  console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
1351
1524
  copyDir(safetyDir, join(pluginDest, 'safety'));
1352
1525
  console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
1353
- ensureOfficeaceCapabilities();
1526
+ installRuntimeDeps(pluginDest);
1527
+ ensureOfficeaceMcpInSqlite();
1354
1528
  registerOfficeaceSkillEntries();
1355
1529
  mkdirSync(pluginDest, { recursive: true });
1356
1530
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
@@ -1378,7 +1552,8 @@ function uninstallOfficeAce() {
1378
1552
  if (removeIfExists(officeacePluginsDir())) {
1379
1553
  console.log(' Removed MCP server and safety policy');
1380
1554
  }
1381
- removeOfficeaceCapabilities();
1555
+ removeOfficeaceSkillCapabilities();
1556
+ removeOfficeaceMcpFromSqlite();
1382
1557
  }
1383
1558
 
1384
1559
  function officeaceStatus() {
@@ -1399,15 +1574,217 @@ function officeaceStatus() {
1399
1574
  console.log(
1400
1575
  ` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
1401
1576
  );
1402
- const capFile = officeaceCapabilitiesFile();
1403
- if (existsSync(capFile)) {
1577
+ const dbPath = officeaceSqlitePath();
1578
+ if (existsSync(dbPath)) {
1404
1579
  try {
1405
- const config = JSON.parse(readFileSync(capFile, 'utf8'));
1406
- const hasMcp = (config.capabilities || []).some((c) => c.id === 'huaweicloud-devkit' && c.type === 'mcp');
1407
- console.log(` MCP config: ${hasMcp ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`);
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
+ }
1408
1589
  } catch {
1409
1590
  console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
1410
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`);
1411
1788
  }
1412
1789
  }
1413
1790
 
@@ -1456,6 +1833,8 @@ function autoDetectTarget() {
1456
1833
  },
1457
1834
  ],
1458
1835
  ['officeace', () => existsSync(officeaceCapabilitiesDir())],
1836
+ ['hermes', () => existsSync(hermesHomeDir())],
1837
+ ['openclaw', () => existsSync(join(homedir(), '.openclaw'))],
1459
1838
  ];
1460
1839
  const detected = checks.filter(([, check]) => check()).map(([name]) => name);
1461
1840
  if (detected.length === 0) {
@@ -1508,6 +1887,14 @@ async function cmdInstall() {
1508
1887
  console.log('\n[OfficeAce]');
1509
1888
  await installOfficeAce();
1510
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
+ }
1511
1898
  if (target === 'codex' || target === 'all') {
1512
1899
  console.log('\n[Codex]');
1513
1900
  if (!hasCodexCLI()) {
@@ -1545,13 +1932,24 @@ async function cmdInstall() {
1545
1932
  ? 'DSH'
1546
1933
  : target === 'officeace'
1547
1934
  ? 'OfficeAce'
1548
- : 'OpenCode';
1935
+ : target === 'hermes'
1936
+ ? 'Hermes Agent'
1937
+ : target === 'openclaw'
1938
+ ? 'OpenClaw'
1939
+ : '当前 agent';
1549
1940
  const pad = ' '.repeat(24 - appName.length);
1550
- console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════╗`);
1551
- console.log(`\x1b[1m\x1b[33m║ MCP 工具在重启 ${appName} 会话后才生效${pad}║`);
1552
- console.log(`\x1b[1m\x1b[33m║ 关闭当前会话 → 重新打开,直接描述华为云任务即可 ║`);
1553
- console.log(`\x1b[1m\x1b[33m║ 重启前请勿执行 hcloud 命令,避免 AK/SK 泄露 ║`);
1554
- 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
+ }
1555
1953
 
1556
1954
  const hcloudOk = checkHcloud();
1557
1955
  if (!hcloudOk) {
@@ -1564,7 +1962,11 @@ async function cmdInstall() {
1564
1962
  console.log(`\n\x1b[1m下一步:\x1b[0m`);
1565
1963
  console.log(` 1. 配置统一凭据:npx huaweicloud-devkit auth init`);
1566
1964
  console.log(` 2. 配置代理(企业内网):npx huaweicloud-devkit proxy init`);
1567
- console.log(` 3. 重启 ${appName} 会话(MCP 工具重启后生效)`);
1965
+ if (target === 'officeace') {
1966
+ console.log(` 3. 打开连接器 → 我的连接器 → huaweicloud-devkit → 连接 → 回到对话 → 输入框开启连接器`);
1967
+ } else {
1968
+ console.log(` 3. 重启 ${appName} 会话(MCP 工具重启后生效)`);
1969
+ }
1568
1970
  console.log(` 4. 运行自检:npx huaweicloud-devkit doctor`);
1569
1971
 
1570
1972
  // Write install marker for doctor to detect
@@ -1577,9 +1979,11 @@ async function cmdInstall() {
1577
1979
  ? workbuddyPluginsDir()
1578
1980
  : target === 'officeace'
1579
1981
  ? officeacePluginsDir()
1580
- : target === 'codex-desktop'
1982
+ : target === 'openclaw'
1581
1983
  ? codexDesktopPluginsDir()
1582
- : opencodePluginsDir();
1984
+ : target === 'codex-desktop'
1985
+ ? codexDesktopPluginsDir()
1986
+ : opencodePluginsDir();
1583
1987
  mkdirSync(markerDir, { recursive: true });
1584
1988
  writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
1585
1989
  if (target === 'opencode' || target === 'all') {
@@ -1600,6 +2004,9 @@ async function cmdInstall() {
1600
2004
  if (target === 'officeace' || target === 'all') {
1601
2005
  console.log('Or describe your Huawei Cloud task in OfficeAce');
1602
2006
  }
2007
+ if (target === 'openclaw' || target === 'all') {
2008
+ console.log('Or describe your Huawei Cloud task in OpenClaw');
2009
+ }
1603
2010
  }
1604
2011
 
1605
2012
  async function cmdUninstall() {
@@ -1627,6 +2034,14 @@ async function cmdUninstall() {
1627
2034
  console.log('\n[OfficeAce]');
1628
2035
  uninstallOfficeAce();
1629
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
+ }
1630
2045
  if (target === 'codex-desktop' || target === 'codex' || target === 'all') {
1631
2046
  console.log('\n[Codex]');
1632
2047
  uninstallCodexDesktop();
@@ -1679,6 +2094,30 @@ async function cmdStatus() {
1679
2094
  console.log('\n[OfficeAce]');
1680
2095
  officeaceStatus();
1681
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
+ }
1682
2121
  if (target === 'codex' || target === 'all') {
1683
2122
  console.log('\n[Codex]');
1684
2123
  if (!hasCodexCLI()) {
@@ -1711,7 +2150,7 @@ async function cmdDoctor() {
1711
2150
  }
1712
2151
 
1713
2152
  // Node.js
1714
- 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');
1715
2154
 
1716
2155
  // MCP server — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH paths
1717
2156
  const opencodePluginDir = opencodePluginsDir();
@@ -1720,13 +2159,15 @@ async function cmdDoctor() {
1720
2159
  const workbuddyPluginDir = workbuddyPluginsDir();
1721
2160
  const dshPluginDir = dshPluginsDir();
1722
2161
  const officeacePluginDir = officeacePluginsDir();
2162
+ const hermesPluginDir = hermesPluginsDir();
1723
2163
  const mcpOk =
1724
2164
  existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ||
1725
2165
  existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ||
1726
2166
  existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs')) ||
1727
2167
  existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ||
1728
2168
  existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs')) ||
1729
- existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs'));
2169
+ existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs')) ||
2170
+ existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'));
1730
2171
  const mcpTarget = existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs'))
1731
2172
  ? 'OpenCode'
1732
2173
  : existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs'))
@@ -1739,7 +2180,9 @@ async function cmdDoctor() {
1739
2180
  ? 'DSH'
1740
2181
  : existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs'))
1741
2182
  ? 'OfficeAce'
1742
- : '';
2183
+ : existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'))
2184
+ ? 'Hermes Agent'
2185
+ : '';
1743
2186
  check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
1744
2187
 
1745
2188
  if (mcpOk) {
@@ -1752,7 +2195,8 @@ async function cmdDoctor() {
1752
2195
  existsSync(join(codeartsPluginDir, 'safety', 'policy.json')) ||
1753
2196
  existsSync(join(workbuddyPluginDir, 'safety', 'policy.json')) ||
1754
2197
  existsSync(join(dshPluginDir, 'safety', 'policy.json')) ||
1755
- existsSync(join(officeacePluginDir, 'safety', 'policy.json'));
2198
+ existsSync(join(officeacePluginDir, 'safety', 'policy.json')) ||
2199
+ existsSync(join(hermesPluginDir, 'safety', 'policy.json'));
1756
2200
  check('Safety policy installed', safetyOk, 'Run: npx huaweicloud-devkit install');
1757
2201
 
1758
2202
  // MCP config — check OpenCode, Codex Desktop, CodeArts, WorkBuddy, and DSH
@@ -1817,6 +2261,18 @@ async function cmdDoctor() {
1817
2261
  } catch {}
1818
2262
  }
1819
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
+ }
1820
2276
  check(
1821
2277
  'MCP configured',
1822
2278
  mcpConfigured,
@@ -1867,6 +2323,7 @@ async function cmdDoctor() {
1867
2323
  workbuddySkillsDir(),
1868
2324
  dshSkillsDir(),
1869
2325
  officeaceSkillsDir(),
2326
+ hermesSkillsDir(),
1870
2327
  ];
1871
2328
  let skillCount = 0;
1872
2329
  const missingSkills = [];
@@ -2024,7 +2481,31 @@ async function cmdUpdate() {
2024
2481
  console.log('[OfficeAce]');
2025
2482
  await updateOfficeAce();
2026
2483
  console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
2027
- console.log(`\x1b[33mRestart OfficeAce for changes to take effect.\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`);
2028
2509
  return;
2029
2510
  }
2030
2511
 
@@ -2060,6 +2541,16 @@ async function cmdUpdate() {
2060
2541
  await updateOfficeAce();
2061
2542
  updatedAny = true;
2062
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
+ }
2063
2554
  if (codexStatus()) {
2064
2555
  console.log('\n[Codex]');
2065
2556
  installCodex();
@@ -2555,7 +3046,7 @@ async function main() {
2555
3046
  default:
2556
3047
  console.log(BANNER);
2557
3048
  console.log(
2558
- 'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|officeace|all>]\n',
3049
+ 'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|officeace|hermes|openclaw|all>]\n',
2559
3050
  );
2560
3051
  console.log('Commands:');
2561
3052
  console.log(' install Install skills, MCP server, safety policy');
@@ -2569,7 +3060,9 @@ async function main() {
2569
3060
  console.log(' proxy Manage proxy config: init | show | clear');
2570
3061
  console.log(' help Show this help');
2571
3062
  console.log('\nOptions:');
2572
- 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
+ );
2573
3066
  console.log('\nExamples:');
2574
3067
  console.log(' npx huaweicloud-devkit install');
2575
3068
  console.log(' npx huaweicloud-devkit install --target codex');
@@ -2577,6 +3070,7 @@ async function main() {
2577
3070
  console.log(' npx huaweicloud-devkit install --target workbuddy');
2578
3071
  console.log(' npx huaweicloud-devkit install --target dsh');
2579
3072
  console.log(' npx huaweicloud-devkit install --target officeace');
3073
+ console.log(' npx huaweicloud-devkit install --target hermes');
2580
3074
  console.log(' npx huaweicloud-devkit install --target all');
2581
3075
  console.log(' npx huaweicloud-devkit auth init');
2582
3076
  console.log(' npx huaweicloud-devkit auth sync --target all');