huaweicloud-devkit 1.1.0-next.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +27 -1
  2. package/README.zh-CN.md +26 -1
  3. package/package.json +3 -1
  4. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  5. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  6. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  7. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  8. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  9. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -12
  10. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  11. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +136 -24
  12. package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +15 -14
  13. package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +1 -1
  14. package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +5 -1
  15. package/plugins/huaweicloud-core/skills/huawei-ecs/references/hc-activity.md +27 -0
  16. package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +11 -11
  17. package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +6 -5
  18. package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +71 -7
  19. package/plugins/huaweicloud-core/skills/huawei-sandbox/references/nginx-templates.md +4 -0
  20. package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +19 -12
  21. package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +26 -26
  22. package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +12 -6
  23. package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +7 -4
  24. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +7 -7
  25. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +14 -0
  26. package/plugins/huaweicloud-core/src/auth/credentials.mjs +30 -1
  27. package/plugins/huaweicloud-core/src/auth/service.mjs +33 -0
  28. package/plugins/huaweicloud-core/src/detect-framework.mjs +25 -3
  29. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +67 -5
  30. package/plugins/huaweicloud-core/src/mcp-server.mjs +23 -0
  31. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +11 -3
  32. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +118 -23
  33. package/plugins/huaweicloud-core/src/search-market.mjs +1 -0
  34. package/plugins/huaweicloud-core/src/setup-cli.mjs +491 -64
  35. package/plugins/huaweicloud-core/src/tools.mjs +37 -15
@@ -105,6 +105,17 @@ function codeartsPluginsDir() {
105
105
  return join(homedir(), '.codeartsdoer', 'huaweicloud-plugins');
106
106
  }
107
107
 
108
+ // CodeArts Work (CodeArts Space) — user-level only
109
+ function codeartsWorkSkillsDir() {
110
+ return join(homedir(), '.codeartswork', 'skills');
111
+ }
112
+ function codeartsWorkMcpSettingsFile() {
113
+ return join(homedir(), '.codeartswork', 'mcp', 'mcp_settings.json');
114
+ }
115
+ function codeartsWorkPluginsDir() {
116
+ return join(homedir(), '.codeartswork', 'huaweicloud-plugins');
117
+ }
118
+
108
119
  function workbuddySkillsDir() {
109
120
  return join(homedir(), '.workbuddy', 'skills');
110
121
  }
@@ -463,6 +474,7 @@ function installRuntimeDeps(pluginsDir) {
463
474
  };
464
475
  let r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
465
476
  const isRetryable = (res) => {
477
+ if (res.status === 0) return false;
466
478
  const stderr = (res.stderr || '').toString();
467
479
  return res.error?.code === 'EPERM' || res.error?.code === 'EBUSY' || /EPERM|EBUSY/.test(stderr);
468
480
  };
@@ -471,6 +483,11 @@ function installRuntimeDeps(pluginsDir) {
471
483
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
472
484
  r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
473
485
  }
486
+ if (r.status !== 0 && !isRetryable(r)) {
487
+ console.log(` \x1b[33m[RETRY]\x1b[0m npm install failed (exit ${r.status}), retrying in 3s...`);
488
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 3000);
489
+ r = spawnSync('npm', ['install', '--omit=dev'], spawnOpts);
490
+ }
474
491
  const undiciDir = join(pluginsDir, 'node_modules', 'undici');
475
492
  if (r.status === 0 && existsSync(undiciDir)) {
476
493
  console.log(` Runtime deps installed -> ${join(pluginsDir, 'node_modules')}`);
@@ -478,12 +495,14 @@ function installRuntimeDeps(pluginsDir) {
478
495
  const errCode = r.error?.code;
479
496
  const err = (r.stderr || '').toString().trim().split(/\r?\n/).slice(-2).join(' ');
480
497
  const hint = errCode === 'ENOENT' ? ' (npm not found — ensure Node.js/npm is in PATH)' : '';
481
- console.log(` \x1b[33m[WARN]\x1b[0m npm install failed in ${pluginsDir}${err ? `: ${err}` : ''}${hint}`);
498
+ const msg = `npm install failed in ${pluginsDir}${err ? `: ${err}` : ''}${hint}`;
499
+ console.log(` \x1b[31m[ERROR]\x1b[0m ${msg}`);
482
500
  console.log(' Manual fix: cd %s && npm install', pluginsDir);
483
501
  if (!existsSync(undiciDir)) {
484
502
  console.log(` \x1b[31m[ERROR]\x1b[0m undici is NOT installed. MCP server will fail to start.`);
485
503
  console.log(` Run manually: cd "${pluginsDir}" && npm install undici@^8.10.0`);
486
504
  }
505
+ throw new Error(msg);
487
506
  }
488
507
  }
489
508
 
@@ -1248,6 +1267,146 @@ function codeartsStatus() {
1248
1267
  }
1249
1268
  }
1250
1269
 
1270
+ // --- CodeArts Work (CodeArts Space) ---
1271
+
1272
+ function registerCodeartsWorkMcp() {
1273
+ const configPath = codeartsWorkMcpSettingsFile();
1274
+ const mcpPath = join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
1275
+ const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
1276
+ const hcloudBin = findHcloudBin();
1277
+ if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
1278
+ let config = {};
1279
+ if (existsSync(configPath)) {
1280
+ try {
1281
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
1282
+ } catch {
1283
+ console.log(
1284
+ ` \x1b[33m[WARN]\x1b[0m Could not parse ${configPath}. Skipping MCP config write; ensure "mcpServers.huaweicloud-devkit" points to ${mcpPath}.`,
1285
+ );
1286
+ return;
1287
+ }
1288
+ const existing = config.mcpServers?.['huaweicloud-devkit'];
1289
+ if (
1290
+ existing &&
1291
+ existing.command === 'node' &&
1292
+ Array.isArray(existing.args) &&
1293
+ existing.args[0] === mcpPath &&
1294
+ existing.timeout === 300000
1295
+ ) {
1296
+ console.log(` MCP config unchanged: ${configPath}`);
1297
+ return;
1298
+ }
1299
+ }
1300
+ config.mcpServers = config.mcpServers || {};
1301
+ config.mcpServers['huaweicloud-devkit'] = {
1302
+ command: 'node',
1303
+ args: [mcpPath],
1304
+ env,
1305
+ enabled: true,
1306
+ timeout: 300000,
1307
+ };
1308
+ mkdirSync(dirname(configPath), { recursive: true });
1309
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1310
+ console.log(` MCP config updated: ${configPath}`);
1311
+ }
1312
+
1313
+ async function installCodeArtsWork() {
1314
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1315
+ const srcDir = join(PLUGIN_ROOT, 'src');
1316
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1317
+
1318
+ copyDir(skillsSrc, codeartsWorkSkillsDir());
1319
+ console.log(` Skills -> ${codeartsWorkSkillsDir()}`);
1320
+
1321
+ const pluginDest = codeartsWorkPluginsDir();
1322
+ copyDir(srcDir, join(pluginDest, 'src'));
1323
+ console.log(` MCP Server -> ${join(pluginDest, 'src')}`);
1324
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1325
+ console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
1326
+
1327
+ registerCodeartsWorkMcp();
1328
+ installRuntimeDeps(pluginDest);
1329
+ }
1330
+
1331
+ async function updateCodeArtsWork() {
1332
+ const skillsSrc = join(PLUGIN_ROOT, 'skills');
1333
+ const srcDir = join(PLUGIN_ROOT, 'src');
1334
+ const safetyDir = join(PLUGIN_ROOT, 'safety');
1335
+ const pluginDest = codeartsWorkPluginsDir();
1336
+
1337
+ copyDir(skillsSrc, codeartsWorkSkillsDir());
1338
+ console.log(` Skills updated -> ${codeartsWorkSkillsDir()}`);
1339
+ copyDir(srcDir, join(pluginDest, 'src'));
1340
+ console.log(` MCP Server updated -> ${join(pluginDest, 'src')}`);
1341
+ copyDir(safetyDir, join(pluginDest, 'safety'));
1342
+ console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
1343
+ registerCodeartsWorkMcp();
1344
+ mkdirSync(pluginDest, { recursive: true });
1345
+ writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
1346
+ installRuntimeDeps(pluginDest);
1347
+ }
1348
+
1349
+ function uninstallCodeArtsWork() {
1350
+ const skillsDir = codeartsWorkSkillsDir();
1351
+ if (existsSync(skillsDir)) {
1352
+ let removed = 0;
1353
+ for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
1354
+ if (entry.name.startsWith('huawei')) {
1355
+ removeIfExists(join(skillsDir, entry.name));
1356
+ removed++;
1357
+ }
1358
+ }
1359
+ if (removed > 0) console.log(` Removed ${removed} skills`);
1360
+ }
1361
+
1362
+ if (removeIfExists(codeartsWorkPluginsDir())) {
1363
+ console.log(' Removed MCP server and safety policy');
1364
+ }
1365
+
1366
+ const configPath = codeartsWorkMcpSettingsFile();
1367
+ if (existsSync(configPath)) {
1368
+ let config = {};
1369
+ try {
1370
+ config = JSON.parse(readFileSync(configPath, 'utf8'));
1371
+ } catch {}
1372
+ if (config.mcpServers?.['huaweicloud-devkit']) {
1373
+ delete config.mcpServers['huaweicloud-devkit'];
1374
+ if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
1375
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
1376
+ console.log(` Config cleaned: ${configPath}`);
1377
+ }
1378
+ }
1379
+ }
1380
+
1381
+ function codeartsWorkStatus() {
1382
+ const pluginDir = codeartsWorkPluginsDir();
1383
+ console.log(
1384
+ ` MCP Server: ${existsSync(join(pluginDir, 'src', 'mcp-server.mjs')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1385
+ );
1386
+ console.log(
1387
+ ` Safety Policy: ${existsSync(join(pluginDir, 'safety', 'policy.json')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
1388
+ );
1389
+ let skillCount = 0;
1390
+ if (existsSync(codeartsWorkSkillsDir())) {
1391
+ skillCount = readdirSync(codeartsWorkSkillsDir(), { withFileTypes: true }).filter(
1392
+ (d) => d.isDirectory() && d.name.startsWith('huawei'),
1393
+ ).length;
1394
+ }
1395
+ console.log(
1396
+ ` Skills: ${skillCount > 0 ? `\x1b[32m${skillCount} installed\x1b[0m` : '\x1b[31mNot installed\x1b[0m'}`,
1397
+ );
1398
+ if (existsSync(codeartsWorkMcpSettingsFile())) {
1399
+ try {
1400
+ const config = JSON.parse(readFileSync(codeartsWorkMcpSettingsFile(), 'utf8'));
1401
+ console.log(
1402
+ ` MCP config: ${config.mcpServers?.['huaweicloud-devkit'] ? '\x1b[32mConfigured\x1b[0m' : '\x1b[31mNot configured\x1b[0m'}`,
1403
+ );
1404
+ } catch {
1405
+ console.log(` MCP config: \x1b[31mInvalid\x1b[0m`);
1406
+ }
1407
+ }
1408
+ }
1409
+
1251
1410
  // Returns true when the config file was written, false when it was already correct.
1252
1411
  function ensureWorkbuddyMcpConfig() {
1253
1412
  const configPath = workbuddyMcpConfigFile();
@@ -1957,6 +2116,14 @@ function hermesPluginsDir() {
1957
2116
  return join(hermesHomeDir(), 'huaweicloud-plugins');
1958
2117
  }
1959
2118
 
2119
+ function hermesPythonPluginsDir() {
2120
+ return join(hermesHomeDir(), 'plugins');
2121
+ }
2122
+
2123
+ function hermesSafetyPluginDir() {
2124
+ return join(hermesPythonPluginsDir(), 'huaweicloud-safety');
2125
+ }
2126
+
1960
2127
  function hermesConfigFile() {
1961
2128
  return join(hermesHomeDir(), 'config.yaml');
1962
2129
  }
@@ -2035,17 +2202,26 @@ function removeHermesMcpConfigBlock() {
2035
2202
  console.log(' MCP config cleaned');
2036
2203
  }
2037
2204
 
2205
+ function hermesHookScript() {
2206
+ return join(hermesPluginsDir(), 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
2207
+ }
2208
+
2209
+ function hermesHookCommand() {
2210
+ const pythonBin = process.platform === 'win32' ? 'python' : 'python3';
2211
+ return `${pythonBin} ${hermesHookScript()}`;
2212
+ }
2213
+
2038
2214
  function ensureHermesHooksConfig() {
2039
2215
  const configPath = hermesConfigFile();
2040
- const pluginDest = hermesPluginsDir();
2041
- const hookScript = join(pluginDest, 'hooks', 'huaweicloud-safety.py').replace(/\\/g, '/');
2216
+ const hookCommand = hermesHookCommand();
2042
2217
 
2043
2218
  const blockLines = [
2044
2219
  'hooks:',
2045
2220
  ' pre_tool_call:',
2046
2221
  ' - matcher: "terminal"',
2047
- ` command: "python3 ${hookScript}"`,
2222
+ ` command: "${hookCommand}"`,
2048
2223
  ' timeout: 5',
2224
+ ' fail_closed: true',
2049
2225
  ];
2050
2226
  const block = blockLines.join('\n');
2051
2227
 
@@ -2054,10 +2230,21 @@ function ensureHermesHooksConfig() {
2054
2230
  try {
2055
2231
  existing = readFileSync(configPath, 'utf8');
2056
2232
  } catch {}
2057
- if (existing.includes('hooks:') && existing.includes('huaweicloud-safety.py')) {
2233
+ if (
2234
+ existing.includes('hooks:') &&
2235
+ existing.includes(`command: "${hookCommand}"`) &&
2236
+ existing.includes('fail_closed: true')
2237
+ ) {
2058
2238
  console.log(` Hooks config unchanged: ${configPath}`);
2059
2239
  return false;
2060
2240
  }
2241
+ removeHermesHooksConfigBlock();
2242
+ existing = '';
2243
+ if (existsSync(configPath)) {
2244
+ try {
2245
+ existing = readFileSync(configPath, 'utf8');
2246
+ } catch {}
2247
+ }
2061
2248
  }
2062
2249
  mkdirSync(dirname(configPath), { recursive: true });
2063
2250
  const newContent = existing ? `${existing.trimEnd()}\n\n${block}\n` : `${block}\n`;
@@ -2066,30 +2253,171 @@ function ensureHermesHooksConfig() {
2066
2253
  return true;
2067
2254
  }
2068
2255
 
2256
+ function hermesHookScriptMtime() {
2257
+ const scriptPath = hermesHookScript();
2258
+ try {
2259
+ return statSync(scriptPath).mtime.toISOString();
2260
+ } catch {
2261
+ return null;
2262
+ }
2263
+ }
2264
+
2265
+ function hermesAllowlistPath() {
2266
+ return join(hermesHomeDir(), 'shell-hooks-allowlist.json');
2267
+ }
2268
+
2269
+ function ensureHermesHookAllowlist() {
2270
+ const hookCommand = hermesHookCommand();
2271
+ const allowlistPath = hermesAllowlistPath();
2272
+
2273
+ let data = { approvals: [] };
2274
+ if (existsSync(allowlistPath)) {
2275
+ try {
2276
+ const parsed = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2277
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
2278
+ data = parsed;
2279
+ }
2280
+ } catch {}
2281
+ }
2282
+ const approvals = Array.isArray(data.approvals) ? data.approvals : [];
2283
+ const already = approvals.some(
2284
+ (a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === hookCommand,
2285
+ );
2286
+ if (already) {
2287
+ console.log(` Hook allowlist unchanged: ${allowlistPath}`);
2288
+ return false;
2289
+ }
2290
+
2291
+ const entry = {
2292
+ event: 'pre_tool_call',
2293
+ command: hookCommand,
2294
+ approved_at: new Date().toISOString(),
2295
+ script_mtime_at_approval: hermesHookScriptMtime(),
2296
+ };
2297
+ data.approvals = [...approvals, entry];
2298
+ mkdirSync(dirname(allowlistPath), { recursive: true });
2299
+ writeFileSync(allowlistPath, JSON.stringify(data, null, 2));
2300
+ console.log(` Hook allowlist updated: ${allowlistPath}`);
2301
+ return true;
2302
+ }
2303
+
2304
+ function hermesHookAllowlisted() {
2305
+ const allowlistPath = hermesAllowlistPath();
2306
+ if (!existsSync(allowlistPath)) return false;
2307
+ try {
2308
+ const data = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2309
+ const cmd = hermesHookCommand();
2310
+ return (
2311
+ Array.isArray(data?.approvals) &&
2312
+ data.approvals.some((a) => a && typeof a === 'object' && a.event === 'pre_tool_call' && a.command === cmd)
2313
+ );
2314
+ } catch {
2315
+ return false;
2316
+ }
2317
+ }
2318
+
2319
+ function hermesHookPluginInit() {
2320
+ const script = hermesHookScript();
2321
+ return [
2322
+ 'import importlib.util',
2323
+ '',
2324
+ `SAFETY_SCRIPT = ${JSON.stringify(script)}`,
2325
+ '',
2326
+ '',
2327
+ 'def _load_safety():',
2328
+ ' spec = importlib.util.spec_from_file_location("huaweicloud_safety_hook", SAFETY_SCRIPT)',
2329
+ ' module = importlib.util.module_from_spec(spec)',
2330
+ ' spec.loader.exec_module(module)',
2331
+ ' return module',
2332
+ '',
2333
+ '',
2334
+ '_safety = _load_safety()',
2335
+ '',
2336
+ '',
2337
+ 'def register(ctx):',
2338
+ ' ctx.register_hook("pre_tool_call", _check)',
2339
+ '',
2340
+ '',
2341
+ 'def _check(tool_name=None, args=None, **kwargs):',
2342
+ ' if tool_name != "terminal":',
2343
+ ' return None',
2344
+ ' if not (isinstance(args, dict) and "command" in args):',
2345
+ ' return None',
2346
+ ' reason = _safety.evaluate(tool_name, args)',
2347
+ ' if reason:',
2348
+ ' return {"action": "block", "message": _safety.DENY_PREFIX + reason}',
2349
+ ' return None',
2350
+ '',
2351
+ ].join('\n');
2352
+ }
2353
+
2354
+ function ensureHermesHookPlugin() {
2355
+ const pluginDir = hermesSafetyPluginDir();
2356
+ const manifestPath = join(pluginDir, 'plugin.yaml');
2357
+ const initPath = join(pluginDir, '__init__.py');
2358
+
2359
+ const manifest = [
2360
+ 'name: huaweicloud-safety',
2361
+ `version: "${pkgVersion}"`,
2362
+ 'description: Huawei Cloud safety pre_tool_call hook',
2363
+ 'author: HuaweiCloud Mate',
2364
+ 'provides_hooks:',
2365
+ ' - pre_tool_call',
2366
+ '',
2367
+ ].join('\n');
2368
+ const init = hermesHookPluginInit();
2369
+
2370
+ const manifestCurrent = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : '';
2371
+ const initCurrent = existsSync(initPath) ? readFileSync(initPath, 'utf8') : '';
2372
+ if (manifestCurrent === manifest && initCurrent === init) {
2373
+ console.log(` Hook plugin unchanged: ${pluginDir}`);
2374
+ return false;
2375
+ }
2376
+
2377
+ mkdirSync(pluginDir, { recursive: true });
2378
+ writeFileSync(manifestPath, manifest);
2379
+ writeFileSync(initPath, init);
2380
+ console.log(` Hook plugin installed: ${pluginDir}`);
2381
+ return true;
2382
+ }
2383
+
2384
+ function removeHermesHookPlugin() {
2385
+ const pluginDir = hermesSafetyPluginDir();
2386
+ if (!existsSync(pluginDir)) return false;
2387
+ rmSync(pluginDir, { recursive: true, force: true });
2388
+ console.log(` Removed hook plugin: ${pluginDir}`);
2389
+ const parent = hermesPythonPluginsDir();
2390
+ try {
2391
+ if (existsSync(parent) && readdirSync(parent).length === 0) {
2392
+ rmSync(parent, { recursive: true, force: true });
2393
+ }
2394
+ } catch {}
2395
+ return true;
2396
+ }
2397
+
2398
+ function hermesHookPluginInstalled() {
2399
+ return (
2400
+ existsSync(join(hermesSafetyPluginDir(), 'plugin.yaml')) && existsSync(join(hermesSafetyPluginDir(), '__init__.py'))
2401
+ );
2402
+ }
2403
+
2069
2404
  function removeHermesHooksConfigBlock() {
2070
2405
  const configPath = hermesConfigFile();
2071
2406
  if (!existsSync(configPath)) return;
2072
2407
  const lines = readFileSync(configPath, 'utf8').split(/\r?\n/);
2408
+ let inHooksBlock = false;
2073
2409
  const out = [];
2074
- let skip = false;
2075
2410
  for (const line of lines) {
2076
- if (/^\s*huaweicloud-safety\.py/.test(line.trim())) {
2077
- skip = true;
2078
- continue;
2079
- }
2080
- if (skip) {
2081
- if (line.trim() === '') continue;
2082
- if (/^\s{2,}/.test(line) && line.trim() !== '') continue;
2083
- skip = false;
2084
- }
2085
- if (line.trim() === 'hooks:') {
2086
- continue;
2087
- }
2088
- if (line.trim() === 'pre_tool_call:') {
2411
+ const trimmed = line.trim();
2412
+ if (trimmed === 'hooks:' && !inHooksBlock) {
2413
+ inHooksBlock = true;
2089
2414
  continue;
2090
2415
  }
2091
- if (/^\s*- matcher:\s*"terminal"/.test(line)) {
2092
- continue;
2416
+ if (inHooksBlock) {
2417
+ if (trimmed === '' || line.startsWith(' ') || line.startsWith('\t')) {
2418
+ continue;
2419
+ }
2420
+ inHooksBlock = false;
2093
2421
  }
2094
2422
  out.push(line);
2095
2423
  }
@@ -2155,6 +2483,8 @@ async function installHermes() {
2155
2483
 
2156
2484
  if (!skipMcp) ensureHermesMcpConfig();
2157
2485
  ensureHermesHooksConfig();
2486
+ ensureHermesHookAllowlist();
2487
+ ensureHermesHookPlugin();
2158
2488
  if (!skipMcp) installRuntimeDeps(pluginDest);
2159
2489
  if (!skipMcp) ensureHermesMcpSdk();
2160
2490
  }
@@ -2177,6 +2507,8 @@ async function updateHermes() {
2177
2507
  console.log(` Safety Hooks updated -> ${join(pluginDest, 'hooks')}`);
2178
2508
  ensureHermesMcpConfig();
2179
2509
  ensureHermesHooksConfig();
2510
+ ensureHermesHookAllowlist();
2511
+ ensureHermesHookPlugin();
2180
2512
  ensureHermesMcpSdk();
2181
2513
  mkdirSync(pluginDest, { recursive: true });
2182
2514
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
@@ -2186,6 +2518,40 @@ async function updateHermes() {
2186
2518
  function uninstallHermes() {
2187
2519
  const skillsDir = hermesSkillsDir();
2188
2520
  let removed = 0;
2521
+
2522
+ // 1. Remove hook config from config.yaml (before deleting script files)
2523
+ removeHermesHooksConfigBlock();
2524
+ console.log(' Hooks config removed');
2525
+
2526
+ // 1b. Remove the Python safety hook plugin (v0.9.0+ enforcement path)
2527
+ removeHermesHookPlugin();
2528
+
2529
+ // 2. Clean shell-hooks-allowlist.json (remove approved hook references)
2530
+ const hermesHome = hermesHomeDir();
2531
+ const allowlistPath = join(hermesHome, 'shell-hooks-allowlist.json');
2532
+ if (existsSync(allowlistPath)) {
2533
+ try {
2534
+ const allowlist = JSON.parse(readFileSync(allowlistPath, 'utf8'));
2535
+ const before = (allowlist.approvals || []).length;
2536
+ allowlist.approvals = (allowlist.approvals || []).filter((a) => {
2537
+ const cmd = typeof a === 'string' ? a : a?.command;
2538
+ return !(typeof cmd === 'string' && cmd.includes('huaweicloud-safety.py'));
2539
+ });
2540
+ if (allowlist.approvals.length < before) {
2541
+ writeFileSync(allowlistPath, JSON.stringify(allowlist, null, 2));
2542
+ console.log(` Removed ${before - allowlist.approvals.length} hook approvals from allowlist`);
2543
+ }
2544
+ } catch {
2545
+ removeIfExists(allowlistPath);
2546
+ console.log(' Removed hook allowlist file');
2547
+ }
2548
+ }
2549
+
2550
+ // 3. Remove MCP config from config.yaml
2551
+ removeHermesMcpConfigBlock();
2552
+ console.log(' MCP config removed');
2553
+
2554
+ // 4. Remove skills (file deletion comes after config cleanup)
2189
2555
  if (existsSync(skillsDir)) {
2190
2556
  for (const entry of readdirSync(skillsDir, { withFileTypes: true })) {
2191
2557
  if (entry.name.startsWith('huawei')) {
@@ -2202,11 +2568,10 @@ function uninstallHermes() {
2202
2568
  } catch {}
2203
2569
  }
2204
2570
 
2571
+ // 5. Remove plugin directory (hook script files deleted last)
2205
2572
  if (removeIfExists(hermesPluginsDir())) {
2206
2573
  console.log(' Removed MCP server, safety policy and hooks');
2207
2574
  }
2208
- removeHermesMcpConfigBlock();
2209
- removeHermesHooksConfigBlock();
2210
2575
  }
2211
2576
 
2212
2577
  function hermesStatus() {
@@ -2221,6 +2586,12 @@ function hermesStatus() {
2221
2586
  console.log(
2222
2587
  ` Safety Hooks: ${existsSync(join(pluginDir, 'hooks', 'huaweicloud-safety.py')) ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2223
2588
  );
2589
+ console.log(
2590
+ ` Hook allowlist: ${hermesHookAllowlisted() ? '\x1b[32mApproved\x1b[0m' : '\x1b[31mNot allowlisted\x1b[0m'}`,
2591
+ );
2592
+ console.log(
2593
+ ` Hook plugin: ${hermesHookPluginInstalled() ? '\x1b[32mInstalled\x1b[0m' : '\x1b[31mNot installed\x1b[0m'}`,
2594
+ );
2224
2595
  console.log(` MCP Python SDK: ${hermesMcpSdkOk() ? '\x1b[32mReady\x1b[0m' : '\x1b[31mMissing\x1b[0m'}`);
2225
2596
  let skillCount = 0;
2226
2597
  if (existsSync(skillsDir)) {
@@ -2285,6 +2656,7 @@ function autoDetectTarget() {
2285
2656
  ['opencode', () => existsSync(join(homedir(), '.config', 'opencode'))],
2286
2657
  ['codex-desktop', () => existsSync(join(homedir(), '.codex'))],
2287
2658
  ['codearts', () => existsSync(join(homedir(), '.codeartsdoer'))],
2659
+ ['codearts-work', () => existsSync(join(homedir(), '.codeartswork'))],
2288
2660
  ['workbuddy', () => existsSync(join(homedir(), '.workbuddy'))],
2289
2661
  [
2290
2662
  'dsh',
@@ -2337,6 +2709,10 @@ async function cmdInstall() {
2337
2709
  console.log('\n[CodeArts]');
2338
2710
  await installCodeArts();
2339
2711
  }
2712
+ if (target === 'codearts-work' || target === 'all') {
2713
+ console.log('\n[CodeArts Work]');
2714
+ await installCodeArtsWork();
2715
+ }
2340
2716
  if (target === 'workbuddy' || target === 'all') {
2341
2717
  console.log('\n[WorkBuddy]');
2342
2718
  await installWorkBuddy();
@@ -2388,23 +2764,25 @@ async function cmdInstall() {
2388
2764
  const appName =
2389
2765
  target === 'codearts'
2390
2766
  ? 'CodeArts'
2391
- : target === 'codex-desktop'
2392
- ? 'Codex Desktop'
2393
- : target === 'codex'
2394
- ? 'Codex'
2395
- : target === 'workbuddy'
2396
- ? 'WorkBuddy'
2397
- : target === 'dsh'
2398
- ? 'DSH'
2399
- : target === 'officeace'
2400
- ? 'OfficeAce'
2401
- : target === 'hermes'
2402
- ? 'Hermes Agent'
2403
- : target === 'openclaw'
2404
- ? 'OpenClaw'
2405
- : target === 'atomcode'
2406
- ? 'AtomCode'
2407
- : '当前 agent';
2767
+ : target === 'codearts-work'
2768
+ ? 'CodeArts Work'
2769
+ : target === 'codex-desktop'
2770
+ ? 'Codex Desktop'
2771
+ : target === 'codex'
2772
+ ? 'Codex'
2773
+ : target === 'workbuddy'
2774
+ ? 'WorkBuddy'
2775
+ : target === 'dsh'
2776
+ ? 'DSH'
2777
+ : target === 'officeace'
2778
+ ? 'OfficeAce'
2779
+ : target === 'hermes'
2780
+ ? 'Hermes Agent'
2781
+ : target === 'openclaw'
2782
+ ? 'OpenClaw'
2783
+ : target === 'atomcode'
2784
+ ? 'AtomCode'
2785
+ : '当前 agent';
2408
2786
  const pad = ' '.repeat(24 - appName.length);
2409
2787
  if (target === 'officeace') {
2410
2788
  console.log(`\n\x1b[1m\x1b[33m╔══════════════════════════════════════════════════════════╗`);
@@ -2458,17 +2836,19 @@ async function cmdInstall() {
2458
2836
  ? dshPluginsDir()
2459
2837
  : target === 'codearts'
2460
2838
  ? codeartsPluginsDir()
2461
- : target === 'workbuddy'
2462
- ? workbuddyPluginsDir()
2463
- : target === 'officeace'
2464
- ? officeacePluginsDir()
2465
- : target === 'openclaw'
2466
- ? codexDesktopPluginsDir()
2467
- : target === 'atomcode'
2468
- ? atomcodePluginsDir()
2469
- : target === 'codex-desktop'
2470
- ? codexDesktopPluginsDir()
2471
- : opencodePluginsDir();
2839
+ : target === 'codearts-work'
2840
+ ? codeartsWorkPluginsDir()
2841
+ : target === 'workbuddy'
2842
+ ? workbuddyPluginsDir()
2843
+ : target === 'officeace'
2844
+ ? officeacePluginsDir()
2845
+ : target === 'openclaw'
2846
+ ? codexDesktopPluginsDir()
2847
+ : target === 'atomcode'
2848
+ ? atomcodePluginsDir()
2849
+ : target === 'codex-desktop'
2850
+ ? codexDesktopPluginsDir()
2851
+ : opencodePluginsDir();
2472
2852
  mkdirSync(markerDir, { recursive: true });
2473
2853
  writeFileSync(join(markerDir, '.installed'), new Date().toISOString());
2474
2854
  if (target === 'opencode' || target === 'all') {
@@ -2477,6 +2857,9 @@ async function cmdInstall() {
2477
2857
  if (target === 'codearts' || target === 'all') {
2478
2858
  console.log('Or describe your Huawei Cloud task in CodeArts');
2479
2859
  }
2860
+ if (target === 'codearts-work' || target === 'all') {
2861
+ console.log('Or describe your Huawei Cloud task in CodeArts Work');
2862
+ }
2480
2863
  if (target === 'codex' || target === 'all') {
2481
2864
  console.log('Or mention @huaweicloud-core in Codex');
2482
2865
  }
@@ -2510,6 +2893,10 @@ async function cmdUninstall() {
2510
2893
  console.log('\n[CodeArts]');
2511
2894
  uninstallCodeArts();
2512
2895
  }
2896
+ if (target === 'codearts-work' || target === 'all') {
2897
+ console.log('\n[CodeArts Work]');
2898
+ uninstallCodeArtsWork();
2899
+ }
2513
2900
  if (target === 'workbuddy' || target === 'all') {
2514
2901
  console.log('\n[WorkBuddy]');
2515
2902
  uninstallWorkBuddy();
@@ -2574,6 +2961,10 @@ async function cmdStatus() {
2574
2961
  console.log('\n[CodeArts]');
2575
2962
  codeartsStatus();
2576
2963
  }
2964
+ if (target === 'codearts-work' || target === 'all') {
2965
+ console.log('\n[CodeArts Work]');
2966
+ codeartsWorkStatus();
2967
+ }
2577
2968
  if (target === 'workbuddy' || target === 'all') {
2578
2969
  console.log('\n[WorkBuddy]');
2579
2970
  workbuddyStatus();
@@ -2652,6 +3043,7 @@ async function cmdDoctor() {
2652
3043
  const opencodePluginDir = opencodePluginsDir();
2653
3044
  const codexPluginDir = codexDesktopPluginsDir();
2654
3045
  const codeartsPluginDir = codeartsPluginsDir();
3046
+ const codeartsWorkPluginDir = codeartsWorkPluginsDir();
2655
3047
  const workbuddyPluginDir = workbuddyPluginsDir();
2656
3048
  const dshPluginDir = dshPluginsDir();
2657
3049
  const officeacePluginDir = officeacePluginsDir();
@@ -2661,6 +3053,7 @@ async function cmdDoctor() {
2661
3053
  existsSync(join(opencodePluginDir, 'src', 'mcp-server.mjs')) ||
2662
3054
  existsSync(join(codexPluginDir, 'src', 'mcp-server.mjs')) ||
2663
3055
  existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs')) ||
3056
+ existsSync(join(codeartsWorkPluginDir, 'src', 'mcp-server.mjs')) ||
2664
3057
  existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs')) ||
2665
3058
  existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs')) ||
2666
3059
  existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs')) ||
@@ -2672,17 +3065,19 @@ async function cmdDoctor() {
2672
3065
  ? 'Codex Desktop'
2673
3066
  : existsSync(join(codeartsPluginDir, 'src', 'mcp-server.mjs'))
2674
3067
  ? 'CodeArts'
2675
- : existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs'))
2676
- ? 'WorkBuddy'
2677
- : existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs'))
2678
- ? 'DSH'
2679
- : existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs'))
2680
- ? 'OfficeAce'
2681
- : existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'))
2682
- ? 'Hermes Agent'
2683
- : existsSync(join(atomcodePluginDir, 'src', 'mcp-server.mjs'))
2684
- ? 'AtomCode'
2685
- : '';
3068
+ : existsSync(join(codeartsWorkPluginDir, 'src', 'mcp-server.mjs'))
3069
+ ? 'CodeArts Work'
3070
+ : existsSync(join(workbuddyPluginDir, 'src', 'mcp-server.mjs'))
3071
+ ? 'WorkBuddy'
3072
+ : existsSync(join(dshPluginDir, 'src', 'mcp-server.mjs'))
3073
+ ? 'DSH'
3074
+ : existsSync(join(officeacePluginDir, 'src', 'mcp-server.mjs'))
3075
+ ? 'OfficeAce'
3076
+ : existsSync(join(hermesPluginDir, 'src', 'mcp-server.mjs'))
3077
+ ? 'Hermes Agent'
3078
+ : existsSync(join(atomcodePluginDir, 'src', 'mcp-server.mjs'))
3079
+ ? 'AtomCode'
3080
+ : '';
2686
3081
  check('MCP server installed', mcpOk, 'Run: npx huaweicloud-devkit install');
2687
3082
 
2688
3083
  if (mcpOk) {
@@ -2693,6 +3088,7 @@ async function cmdDoctor() {
2693
3088
  opencodePluginDir,
2694
3089
  codexPluginDir,
2695
3090
  codeartsPluginDir,
3091
+ codeartsWorkPluginDir,
2696
3092
  workbuddyPluginDir,
2697
3093
  dshPluginDir,
2698
3094
  officeacePluginDir,
@@ -2705,6 +3101,7 @@ async function cmdDoctor() {
2705
3101
  existsSync(join(opencodePluginDir, 'safety', 'policy.json')) ||
2706
3102
  existsSync(join(codexPluginDir, 'safety', 'policy.json')) ||
2707
3103
  existsSync(join(codeartsPluginDir, 'safety', 'policy.json')) ||
3104
+ existsSync(join(codeartsWorkPluginDir, 'safety', 'policy.json')) ||
2708
3105
  existsSync(join(workbuddyPluginDir, 'safety', 'policy.json')) ||
2709
3106
  existsSync(join(dshPluginDir, 'safety', 'policy.json')) ||
2710
3107
  existsSync(join(officeacePluginDir, 'safety', 'policy.json')) ||
@@ -2745,6 +3142,16 @@ async function cmdDoctor() {
2745
3142
  }
2746
3143
  } catch {}
2747
3144
  }
3145
+ const codeartsWorkCfg = codeartsWorkMcpSettingsFile();
3146
+ if (!mcpConfigured && existsSync(codeartsWorkCfg)) {
3147
+ try {
3148
+ const cfg = JSON.parse(readFileSync(codeartsWorkCfg, 'utf8'));
3149
+ if (cfg.mcpServers && cfg.mcpServers['huaweicloud-devkit']) {
3150
+ mcpConfigured = true;
3151
+ mcpCfgTarget = 'CodeArts Work';
3152
+ }
3153
+ } catch {}
3154
+ }
2748
3155
  const workbuddyCfg = workbuddyMcpConfigFile();
2749
3156
  if (!mcpConfigured && existsSync(workbuddyCfg)) {
2750
3157
  try {
@@ -2853,6 +3260,7 @@ async function cmdDoctor() {
2853
3260
  opencodeSkillsDir(),
2854
3261
  codexDesktopSkillsDir(),
2855
3262
  codeartsSkillsDir(),
3263
+ codeartsWorkSkillsDir(),
2856
3264
  workbuddySkillsDir(),
2857
3265
  dshSkillsDir(),
2858
3266
  officeaceSkillsDir(),
@@ -2912,6 +3320,7 @@ async function cmdDoctor() {
2912
3320
  { path: join(codexDesktopPluginsDir(), '.installed'), name: 'Codex Desktop' },
2913
3321
  { path: join(workbuddyPluginsDir(), '.installed'), name: 'WorkBuddy' },
2914
3322
  { path: join(codeartsPluginsDir(), '.installed'), name: 'CodeArts' },
3323
+ { path: join(codeartsWorkPluginsDir(), '.installed'), name: 'CodeArts Work' },
2915
3324
  { path: join(dshPluginsDir(), '.installed'), name: 'DSH' },
2916
3325
  { path: join(officeacePluginsDir(), '.installed'), name: 'OfficeAce' },
2917
3326
  { path: join(atomcodePluginsDir(), '.installed'), name: 'AtomCode' },
@@ -2984,6 +3393,18 @@ async function cmdUpdate() {
2984
3393
  return;
2985
3394
  }
2986
3395
 
3396
+ if (target === 'codearts-work') {
3397
+ if (!existsSync(join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs'))) {
3398
+ console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
3399
+ return;
3400
+ }
3401
+ console.log('[CodeArts Work]');
3402
+ await updateCodeArtsWork();
3403
+ console.log(`\n\x1b[32mUpdate complete.\x1b[0m`);
3404
+ console.log(`\x1b[33mMCP 工具在重启 CodeArts Work 会话后才生效。\x1b[0m`);
3405
+ return;
3406
+ }
3407
+
2987
3408
  if (target === 'workbuddy') {
2988
3409
  if (!existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
2989
3410
  console.log('\x1b[33mNot installed. Use "install" command first.\x1b[0m');
@@ -3073,6 +3494,11 @@ async function cmdUpdate() {
3073
3494
  await updateCodeArts();
3074
3495
  updatedAny = true;
3075
3496
  }
3497
+ if (existsSync(join(codeartsWorkPluginsDir(), 'src', 'mcp-server.mjs'))) {
3498
+ console.log('\n[CodeArts Work]');
3499
+ await updateCodeArtsWork();
3500
+ updatedAny = true;
3501
+ }
3076
3502
  if (existsSync(join(workbuddyPluginsDir(), 'src', 'mcp-server.mjs'))) {
3077
3503
  console.log('\n[WorkBuddy]');
3078
3504
  await updateWorkBuddy();
@@ -3598,7 +4024,7 @@ async function main() {
3598
4024
  default:
3599
4025
  console.log(BANNER);
3600
4026
  console.log(
3601
- 'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|workbuddy|dsh|officeace|hermes|openclaw|atomcode|all>]\n',
4027
+ 'Usage: npx huaweicloud-devkit <command> [--target <opencode|codex|codearts|codearts-work|workbuddy|dsh|officeace|hermes|openclaw|atomcode|all>]\n',
3602
4028
  );
3603
4029
  console.log('Commands:');
3604
4030
  console.log(' install Install skills, MCP server, safety policy');
@@ -3613,12 +4039,13 @@ async function main() {
3613
4039
  console.log(' help Show this help');
3614
4040
  console.log('\nOptions:');
3615
4041
  console.log(
3616
- ' --target Target agent: opencode (default), codex, codearts, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
4042
+ ' --target Target agent: opencode (default), codex, codearts, codearts-work, workbuddy, dsh, officeace, hermes, openclaw, atomcode, all',
3617
4043
  );
3618
4044
  console.log('\nExamples:');
3619
4045
  console.log(' npx huaweicloud-devkit install');
3620
4046
  console.log(' npx huaweicloud-devkit install --target codex');
3621
4047
  console.log(' npx huaweicloud-devkit install --target codearts');
4048
+ console.log(' npx huaweicloud-devkit install --target codearts-work');
3622
4049
  console.log(' npx huaweicloud-devkit install --target workbuddy');
3623
4050
  console.log(' npx huaweicloud-devkit install --target dsh');
3624
4051
  console.log(' npx huaweicloud-devkit install --target officeace');