huaweicloud-devkit 1.1.1-next.14 → 1.1.1-next.16

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 (34) hide show
  1. package/integrations/atomcode/hooks/huaweicloud-telemetry.js +181 -0
  2. package/integrations/atomcode/hooks.json +9 -0
  3. package/integrations/dsh/hook-plugin.mjs +148 -0
  4. package/integrations/hermes/hooks/huaweicloud-telemetry.py +238 -0
  5. package/integrations/hermes/manifest.yaml +43 -0
  6. package/integrations/opencode/hooks/skill-tracker.js +115 -0
  7. package/integrations/workbuddy/hooks/telemetry-tracker.py +226 -0
  8. package/package.json +7 -2
  9. package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
  10. package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
  11. package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
  12. package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
  13. package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
  14. package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -1
  15. package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
  16. package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +2 -2
  17. package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +42 -2
  18. package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +10 -10
  19. package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +11 -10
  20. package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +11 -11
  21. package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +11 -9
  22. package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +11 -2
  23. package/plugins/huaweicloud-core/src/auth/credentials.mjs +82 -2
  24. package/plugins/huaweicloud-core/src/auth/reconcile.mjs +174 -0
  25. package/plugins/huaweicloud-core/src/auth/service.mjs +53 -32
  26. package/plugins/huaweicloud-core/src/hcloud-cli.mjs +22 -1
  27. package/plugins/huaweicloud-core/src/mcp-server.mjs +37 -3
  28. package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +14 -4
  29. package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +5 -0
  30. package/plugins/huaweicloud-core/src/setup-cli.mjs +395 -25
  31. package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +40 -0
  32. package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +328 -0
  33. package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +378 -0
  34. package/plugins/huaweicloud-core/src/tools.mjs +193 -2
@@ -0,0 +1,181 @@
1
+ /**
2
+ * huaweicloud-telemetry.js — AtomCode PreToolUse Hook
3
+ *
4
+ * Captures Skill loading, hcloud CLI commands, and MCP tool invocations,
5
+ * writing structured events to hook-events.jsonl for the devkit telemetry
6
+ * module to ingest and report.
7
+ *
8
+ * Classification logic mirrors the existing four platform hooks exactly.
9
+ * This hook NEVER blocks execution — it always exits 0.
10
+ */
11
+
12
+ import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
13
+ import { join, dirname } from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ // ══════════════════════════════════════════════════════════════════
17
+ // Debug
18
+ // ══════════════════════════════════════════════════════════════════
19
+
20
+ const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
21
+
22
+ function debugLog(msg) {
23
+ if (!DEBUG) return;
24
+ try {
25
+ const logPath = join(TELEMETRY_DIR, 'atomcode-debug.log');
26
+ appendFileSync(logPath, `${new Date().toISOString()} ${msg}\n`);
27
+ } catch (_) {
28
+ /* best-effort */
29
+ }
30
+ }
31
+
32
+ // ══════════════════════════════════════════════════════════════════
33
+ // Paths — hook is installed at <plugin_dir>/hooks/
34
+ // ══════════════════════════════════════════════════════════════════
35
+
36
+ const __filename = fileURLToPath(import.meta.url);
37
+ const HOOK_DIR = dirname(__filename);
38
+ const PLUGIN_DIR = dirname(HOOK_DIR);
39
+ const TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
40
+ const HOOK_EVENTS_PATH = join(TELEMETRY_DIR, 'hook-events.jsonl');
41
+
42
+ // ══════════════════════════════════════════════════════════════════
43
+ // Helpers
44
+ // ══════════════════════════════════════════════════════════════════
45
+
46
+ function ensureDir(dirPath) {
47
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
48
+ }
49
+
50
+ function writeEvent(key, value, extra = {}) {
51
+ try {
52
+ ensureDir(TELEMETRY_DIR);
53
+ const line = JSON.stringify({ key, value, ...extra }) + '\n';
54
+ appendFileSync(HOOK_EVENTS_PATH, line, 'utf8');
55
+ debugLog(`writeEvent key=${key} value=${value}`);
56
+ } catch (error) {
57
+ debugLog(`writeEvent FAILED: ${error.message}`);
58
+ }
59
+ }
60
+
61
+ // ══════════════════════════════════════════════════════════════════
62
+ // Skill identification
63
+ // ══════════════════════════════════════════════════════════════════
64
+
65
+ function isHuaweiCloudSkill(name) {
66
+ return typeof name === 'string' && /^huawei/i.test(name);
67
+ }
68
+
69
+ // ══════════════════════════════════════════════════════════════════
70
+ // CLI command classification (matches skill-tracker.js exactly)
71
+ // ══════════════════════════════════════════════════════════════════
72
+
73
+ const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)/i;
74
+ const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
75
+ const WRITE_VERBS =
76
+ /\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*/i;
77
+
78
+ function classifyHcloud(text) {
79
+ const m = HCLOUD_RE.exec(text);
80
+ if (!m) return null;
81
+ const rest = m[1].trim();
82
+ const parts = rest.split(/\s+/).filter((p) => !p.startsWith('--'));
83
+ const cmd = parts.join(' ');
84
+ if (!cmd) return null;
85
+ if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}` };
86
+ if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}` };
87
+ return { key: 'cli:invoke', value: `hcloud ${cmd}` };
88
+ }
89
+
90
+ // ══════════════════════════════════════════════════════════════════
91
+ // Input parsing — stdin JSON, with env-var fallback
92
+ // ══════════════════════════════════════════════════════════════════
93
+
94
+ function parseInput() {
95
+ // Primary: AtomCode passes data via env vars
96
+ const toolName = process.env.ATOMCODE_TOOL_NAME || '';
97
+ const context = process.env.ATOMCODE_HOOK_CONTEXT || '';
98
+ if (toolName || context) {
99
+ const input = { tool_name: toolName };
100
+ if (context) {
101
+ try {
102
+ input.tool_input = JSON.parse(context);
103
+ } catch (_) {
104
+ input.tool_input = { command: context };
105
+ }
106
+ } else {
107
+ input.tool_input = {};
108
+ }
109
+ return input;
110
+ }
111
+
112
+ // Fallback: stdin JSON (piped for manual testing)
113
+ try {
114
+ const data = readFileSync(0, 'utf8').trim();
115
+ if (data) return JSON.parse(data);
116
+ } catch (_) {
117
+ /* stdin unavailable */
118
+ }
119
+
120
+ return { tool_name: '', tool_input: {} };
121
+ }
122
+
123
+ function extractCommand(toolInput) {
124
+ if (typeof toolInput === 'string') return toolInput;
125
+ if (toolInput && typeof toolInput === 'object') {
126
+ return toolInput.command || toolInput.arguments?.command || '';
127
+ }
128
+ return '';
129
+ }
130
+
131
+ function extractSkillName(toolInput) {
132
+ if (typeof toolInput === 'string') return toolInput;
133
+ if (toolInput && typeof toolInput === 'object') {
134
+ return toolInput.name || toolInput.command || '';
135
+ }
136
+ return '';
137
+ }
138
+
139
+ // ══════════════════════════════════════════════════════════════════
140
+ // Main
141
+ // ══════════════════════════════════════════════════════════════════
142
+
143
+ try {
144
+ const input = parseInput();
145
+ const toolName = input.tool_name || input.tool || '';
146
+ const toolInput = input.tool_input || input.arguments || {};
147
+
148
+ debugLog(`tool=${toolName}`);
149
+
150
+ // ── Category 1: Skill tool calls ──
151
+ if (toolName === 'skill' || toolName === 'use_skill') {
152
+ const skillName = extractSkillName(toolInput);
153
+ debugLog(`skill name=${skillName}`);
154
+ if (isHuaweiCloudSkill(skillName)) {
155
+ writeEvent('skill:retrieve', skillName);
156
+ }
157
+ }
158
+
159
+ // ── Category 2: Bash/pwsh with hcloud commands ──
160
+ if (toolName === 'bash' || toolName === 'pwsh') {
161
+ const command = extractCommand(toolInput);
162
+ if (command && HCLOUD_RE.test(command)) {
163
+ debugLog(`hcloud command: ${command.slice(0, 120)}`);
164
+ const classified = classifyHcloud(command);
165
+ if (classified) {
166
+ writeEvent(classified.key, classified.value, { capability: 'cli' });
167
+ }
168
+ }
169
+ }
170
+
171
+ // ── Category 3: MCP Huawei Cloud tools ──
172
+ if (toolName.startsWith('mcp__huaweicloud')) {
173
+ writeEvent(`tool:${toolName}`, '1', { capability: 'mcp' });
174
+ debugLog(`MCP tool: ${toolName}`);
175
+ }
176
+ } catch (error) {
177
+ debugLog(`FATAL: ${error?.message || error}`);
178
+ }
179
+
180
+ // Always allow — telemetry hook never blocks; the process exits 0 naturally
181
+ // since all I/O is synchronous and nothing holds the event loop open.
@@ -0,0 +1,9 @@
1
+ {
2
+ "hooks": {
3
+ "hw-telemetry": {
4
+ "event": "pre_tool_use",
5
+ "command": "<node_path> <plugin_dir>/hooks/huaweicloud-telemetry.js",
6
+ "timeout_ms": 2000
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,148 @@
1
+ // hook-plugin.mjs — DSH Cordis Hook Plugin
2
+ // Intercepts Huawei Cloud skill calls and hcloud CLI commands,
3
+ // writes events to hook-events.jsonl for the telemetry module to consume.
4
+
5
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
6
+ import { join, dirname } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ // ══════════════════════════════════════════════════════════════════
10
+ // Cordis plugin identity (required by DSH loader)
11
+ // ══════════════════════════════════════════════════════════════════
12
+ export const name = 'huaweicloud-devkit-hook';
13
+ export const inject = ['tools'];
14
+
15
+ // ══════════════════════════════════════════════════════════════════
16
+ // Paths — this file lives in $DSH_HOME/huaweicloud-plugins/
17
+ // ══════════════════════════════════════════════════════════════════
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const PLUGIN_DIR = dirname(__filename);
20
+ // telemetry/ dir is the single shared location for event files
21
+ const TELEMETRY_DIR = join(PLUGIN_DIR, 'telemetry');
22
+ const HOOK_EVENTS_PATH = join(TELEMETRY_DIR, 'hook-events.jsonl');
23
+
24
+ const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
25
+
26
+ // ══════════════════════════════════════════════════════════════════
27
+ // Helpers
28
+ // ══════════════════════════════════════════════════════════════════
29
+
30
+ function ensureDir(dirPath) {
31
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
32
+ }
33
+
34
+ function debugLog(msg) {
35
+ if (!DEBUG) return;
36
+ try {
37
+ const logPath = join(TELEMETRY_DIR, 'telemetry-debug.log');
38
+ ensureDir(TELEMETRY_DIR);
39
+ appendFileSync(logPath, `${new Date().toISOString()} [hook] ${msg}\n`, 'utf8');
40
+ } catch {
41
+ // debug log is best-effort; never fail the plugin
42
+ }
43
+ }
44
+
45
+ function writeEvent(key, value, extra = {}) {
46
+ try {
47
+ ensureDir(TELEMETRY_DIR);
48
+ const line = JSON.stringify({ key, value, ...extra }) + '\n';
49
+ appendFileSync(HOOK_EVENTS_PATH, line, 'utf8');
50
+ debugLog(`writeEvent key=${key} value=${value}`);
51
+ } catch (error) {
52
+ debugLog(`writeEvent FAILED: ${error.message}`);
53
+ }
54
+ }
55
+
56
+ // ══════════════════════════════════════════════════════════════════
57
+ // Huawei Cloud identification
58
+ // ══════════════════════════════════════════════════════════════════
59
+
60
+ function isHuaweiCloudSkill(name) {
61
+ return typeof name === 'string' && /^huawei/i.test(name);
62
+ }
63
+
64
+ // ══════════════════════════════════════════════════════════════════
65
+ // hcloud command classification (mirrors skill-tracker.js)
66
+ // ══════════════════════════════════════════════════════════════════
67
+
68
+ const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)/i;
69
+ const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
70
+ const WRITE_VERBS =
71
+ /\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*/i;
72
+
73
+ function classifyHcloud(text) {
74
+ const m = HCLOUD_RE.exec(text);
75
+ if (!m) return null;
76
+ const rest = m[1].trim();
77
+ const parts = rest.split(/\s+/).filter((p) => !p.startsWith('--'));
78
+ const cmd = parts.join(' ');
79
+ if (!cmd) return null;
80
+ if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}`, capability: 'cli' };
81
+ if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}`, capability: 'cli' };
82
+ return { key: 'cli:invoke', value: `hcloud ${cmd}`, capability: 'cli' };
83
+ }
84
+
85
+ // ══════════════════════════════════════════════════════════════════
86
+ // apply() — Cordis plugin entry point
87
+ // ══════════════════════════════════════════════════════════════════
88
+
89
+ export function apply(ctx) {
90
+ debugLog('=== PLUGIN LOADED ===');
91
+
92
+ // ── Layer 1: tools/pre-execute waterfall ──────────────────────
93
+ ctx.on('tools/pre-execute', async (exec, next) => {
94
+ try {
95
+ debugLog(`pre-execute tool=${exec.name}`);
96
+
97
+ // ── Intercept 1: skill tool calls ──
98
+ if (exec.name === 'skill') {
99
+ const skillName = exec.arguments?.name || '';
100
+ debugLog(`skill name=${skillName}`);
101
+ if (isHuaweiCloudSkill(skillName)) {
102
+ writeEvent('skill:retrieve', skillName);
103
+ ctx.logger.info(`[hw-hook] skill called: ${skillName}`);
104
+ debugLog(`SKILL TRACKED: ${skillName}`);
105
+ }
106
+ }
107
+
108
+ // ── Intercept 2: pwsh/bash with hcloud commands ──
109
+ if (exec.name === 'pwsh' || exec.name === 'bash') {
110
+ const command = exec.arguments?.command || '';
111
+ if (!command) return next();
112
+
113
+ if (HCLOUD_RE.test(command)) {
114
+ debugLog(`hcloud command: ${command.slice(0, 120)}`);
115
+
116
+ // Classify and write event
117
+ const classified = classifyHcloud(command);
118
+ if (classified) {
119
+ writeEvent(classified.key, classified.value, { capability: 'cli' });
120
+ debugLog(`CLI TRACKED: ${classified.key} ${classified.value}`);
121
+ }
122
+ }
123
+ }
124
+
125
+ // ── Intercept 3: MCP Huawei Cloud tools (record only) ──
126
+ if (exec.name.startsWith('mcp__huaweicloud__')) {
127
+ writeEvent(`tool:${exec.name}`, '1', { capability: 'mcp' });
128
+ debugLog(`MCP tool: ${exec.name}`);
129
+ }
130
+ } catch (error) {
131
+ debugLog(`pre-execute ERROR: ${error?.message || error}`);
132
+ ctx.logger.warn(`[hw-hook] pre-execute error: ${error.message}`);
133
+ }
134
+ return next();
135
+ });
136
+
137
+ // ── Layer 2: tools/result observe ─────────────────────────────
138
+ ctx.on('tools/result', (exec, result) => {
139
+ if (exec.name.startsWith('mcp__huaweicloud__') && result.isError) {
140
+ const msg = result.error?.message || 'unknown error';
141
+ debugLog(`tool error: ${exec.name}: ${msg}`);
142
+ ctx.logger.info(`[hw-hook] tool error: ${exec.name}: ${msg}`);
143
+ }
144
+ });
145
+
146
+ ctx.logger.info('[hw-hook] DSH Huawei Cloud hook plugin activated');
147
+ debugLog('=== PLUGIN INIT DONE ===');
148
+ }
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """Hermes pre_tool_call hook — Huawei Cloud telemetry data collection.
3
+
4
+ Captures Skill loading and hcloud CLI commands, writing structured events
5
+ to hook-events.jsonl for the devkit telemetry module to ingest and report.
6
+
7
+ This hook NEVER blocks execution — it always returns exit code 0.
8
+ Safety enforcement is handled by huaweicloud-safety.py.
9
+
10
+ Data flow:
11
+ stdin JSON (pre_tool_call event) → classify → hook-events.jsonl
12
+ → MCP Server telemetry.mjs :: ingestHookEvents() → eventQueue → HTTP POST
13
+
14
+ Classification logic mirrors the OpenCode skill-tracker.js exactly.
15
+ """
16
+
17
+ import json
18
+ import os
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+
24
+ # ── Path resolution ─────────────────────────────────────────────────
25
+
26
+ def _get_plugin_dir():
27
+ """Resolve the huaweicloud-core plugin directory.
28
+
29
+ Search order:
30
+ 1. HUAWEICLOUD_PLUGIN_DIR env var (explicit override)
31
+ 2. Dev repo: hooks/ → ../plugins/... (local dev)
32
+ 3. ~/.hermes/huaweicloud-plugins/ (Hermes installed)
33
+ """
34
+ env_dir = os.environ.get("HUAWEICLOUD_PLUGIN_DIR")
35
+ if env_dir:
36
+ return Path(env_dir)
37
+
38
+ here = Path(__file__).resolve()
39
+
40
+ # Local dev: plugins/huaweicloud-core/hooks/huaweicloud-telemetry.py
41
+ # parents[1] = plugins/huaweicloud-core/
42
+ if (here.parents[1] / "skills").is_dir():
43
+ return here.parents[1]
44
+
45
+ # Local dev (from repo root): hooks/ → ../plugins/huaweicloud-core/
46
+ relative = here.parents[1] / "plugins" / "huaweicloud-core"
47
+ if relative.is_dir():
48
+ return relative
49
+
50
+ # Hermes installed: ~/AppData/Local/hermes/huaweicloud-plugins/
51
+ # hooks/ is directly under the plugin dir, so parents[1] IS the plugin dir.
52
+ if (here.parents[1] / "telemetry").is_dir() or (here.parents[1] / "src").is_dir():
53
+ return here.parents[1]
54
+
55
+ home = Path.home()
56
+
57
+ # Hermes installed plugin dir (legacy ~/.hermes/ path)
58
+ hermes_dir = home / ".hermes" / "huaweicloud-plugins"
59
+ if hermes_dir.is_dir():
60
+ return hermes_dir
61
+
62
+ raise FileNotFoundError("Cannot resolve huaweicloud-core plugin directory")
63
+
64
+
65
+ def _get_telemetry_dir():
66
+ """Return the telemetry directory shared with all devkit platforms.
67
+
68
+ Always at <plugin_dir>/telemetry/, matching AGENT_TELEMETRY_DIR
69
+ in src/telemetry/telemetry.mjs.
70
+ """
71
+ plugin_dir = _get_plugin_dir()
72
+ telemetry_dir = plugin_dir / "telemetry"
73
+ telemetry_dir.mkdir(parents=True, exist_ok=True)
74
+ return telemetry_dir
75
+
76
+
77
+ def _get_hook_events_path():
78
+ """Lazily resolve hook-events.jsonl path. Returns None if unresolvable."""
79
+ try:
80
+ return str(_get_telemetry_dir() / "hook-events.jsonl")
81
+ except Exception:
82
+ return None
83
+
84
+
85
+ # ── Classifiers (match skill-tracker.js exactly) ────────────────────
86
+
87
+ # hcloud command detection
88
+ HCLOUD_RE = re.compile(r'(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)', re.IGNORECASE)
89
+
90
+ # Read operation verbs
91
+ READ_VERBS = re.compile(
92
+ r'\b(List|Show|Get|Describe|NovaList|NovaShow)\w*', re.IGNORECASE
93
+ )
94
+
95
+ # Write operation verbs
96
+ WRITE_VERBS = re.compile(
97
+ r'\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|'
98
+ r'Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|'
99
+ r'Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|'
100
+ r'Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|'
101
+ r'Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*', re.IGNORECASE
102
+ )
103
+
104
+ # Huawei Cloud skill name pattern
105
+ HUAWEI_SKILL_RE = re.compile(r'^huawei', re.IGNORECASE)
106
+
107
+
108
+ def classify_hcloud(text):
109
+ """Classify an hcloud CLI command as read / write / invoke.
110
+
111
+ Returns:
112
+ dict with 'key' and 'value', or None if not an hcloud command.
113
+ Matches the classifyHcloud() logic in skill-tracker.js.
114
+ """
115
+ m = HCLOUD_RE.search(text)
116
+ if not m:
117
+ return None
118
+
119
+ rest = m[1].strip()
120
+ # Extract command part, excluding flags/options
121
+ parts = rest.split()
122
+ cmd_parts = [p for p in parts if not p.startswith('--')]
123
+ cmd = ' '.join(cmd_parts).strip()
124
+
125
+ if not cmd:
126
+ return None
127
+
128
+ if READ_VERBS.search(cmd):
129
+ return {'key': 'cli:read', 'value': f'hcloud {cmd}'}
130
+ if WRITE_VERBS.search(cmd):
131
+ return {'key': 'cli:write', 'value': f'hcloud {cmd}'}
132
+ return {'key': 'cli:invoke', 'value': f'hcloud {cmd}'}
133
+
134
+
135
+ def is_huawei_skill(name):
136
+ """Check if a skill name belongs to Huawei Cloud."""
137
+ return name and HUAWEI_SKILL_RE.search(name)
138
+
139
+
140
+ # ── Event writer ────────────────────────────────────────────────────
141
+
142
+ def write_event(key, value, extra=None):
143
+ """Append a telemetry event to hook-events.jsonl.
144
+
145
+ Format matches OpenCode/DSH/WorkBuddy output exactly:
146
+ {"key": "...", "value": "...", "capability": "..."}
147
+ """
148
+ path = _get_hook_events_path()
149
+ if not path:
150
+ return # Best-effort: silently skip if path is unresolvable
151
+
152
+ event = {"key": key, "value": value}
153
+ if extra:
154
+ event.update(extra)
155
+ try:
156
+ with open(path, "a", encoding="utf-8") as f:
157
+ f.write(json.dumps(event, ensure_ascii=False) + "\n")
158
+ except Exception:
159
+ # Best-effort; never block the main tool execution
160
+ pass
161
+
162
+
163
+ # ── Command text extraction ─────────────────────────────────────────
164
+
165
+ def command_text(tool_input):
166
+ """Extract command text from various tool_input shapes."""
167
+ if isinstance(tool_input, str):
168
+ return tool_input
169
+ if isinstance(tool_input, dict):
170
+ for key in ("command", "cmd", "script", "args", "arguments"):
171
+ value = tool_input.get(key)
172
+ if isinstance(value, list):
173
+ return " ".join(str(item) for item in value)
174
+ if value is not None:
175
+ return str(value)
176
+ return json.dumps(tool_input)
177
+ return json.dumps(tool_input)
178
+
179
+
180
+ # ── Skill name extraction ───────────────────────────────────────────
181
+
182
+ def extract_skill_name(tool_name, tool_input):
183
+ """Extract skill name from tool_input for skill-related tools.
184
+
185
+ Hermes skill tools: skill_view, skills_list, skill_manage.
186
+ The skill name may be in 'name', 'skill', or 'args' field.
187
+ """
188
+ if not isinstance(tool_input, dict):
189
+ if isinstance(tool_input, str):
190
+ return tool_input
191
+ return ""
192
+
193
+ # Common field names across agent platforms
194
+ return (
195
+ tool_input.get("name", "")
196
+ or tool_input.get("skill", "")
197
+ or tool_input.get("command", "")
198
+ or ""
199
+ )
200
+
201
+
202
+ # ── Main hook logic ─────────────────────────────────────────────────
203
+
204
+ def main():
205
+ try:
206
+ data = json.load(sys.stdin)
207
+ except Exception:
208
+ # Cannot parse input → not our concern, allow execution
209
+ sys.exit(0)
210
+
211
+ tool_name = data.get("tool_name", "")
212
+ tool_input = data.get("tool_input", {})
213
+
214
+ # ── Category 1: Skill tool loading ──
215
+ # Hermes skill tools: skill_view, skills_list, skill_manage
216
+ if tool_name in ("skill_view", "skills_list", "skill_manage", "skill"):
217
+ skill_name = extract_skill_name(tool_name, tool_input)
218
+ if is_huawei_skill(skill_name):
219
+ write_event('skill:retrieve', skill_name)
220
+
221
+ # ── Category 2: Bash/terminal tool with hcloud commands ──
222
+ elif tool_name in ("Bash", "terminal", "bash", "pwsh"):
223
+ command = command_text(tool_input)
224
+ if command and HCLOUD_RE.search(command):
225
+ result = classify_hcloud(command)
226
+ if result:
227
+ write_event(result['key'], result['value'], {'capability': 'cli'})
228
+
229
+ # ── Category 3: MCP Huawei Cloud tool invocations ──
230
+ elif tool_name.startswith("mcp__huaweicloud") or tool_name.startswith("huaweicloud_"):
231
+ write_event(f"tool:{tool_name}", "1", {'capability': 'mcp'})
232
+
233
+ # Always allow — telemetry hook never blocks execution
234
+ sys.exit(0)
235
+
236
+
237
+ if __name__ == "__main__":
238
+ main()
@@ -0,0 +1,43 @@
1
+ manifest_version: 1
2
+ name: huaweicloud-devkit
3
+ description: >
4
+ Huawei Cloud DevKit provides Skills, MCP tools, KooCLI guidance,
5
+ and safety guardrails for Huawei Cloud services within Hermes Agent.
6
+ Includes 27 skills covering ECS, OBS, VPC, RDS, CCE, ModelArts, and more.
7
+
8
+ source: https://github.com/huaweicloud/huaweicloud-devkit
9
+
10
+ transport:
11
+ type: stdio
12
+ command: 'node'
13
+ args: ['plugins/huaweicloud-core/src/mcp-server.mjs']
14
+ env:
15
+ HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local'
16
+
17
+ install:
18
+ type: git
19
+ url: https://github.com/huaweicloud/huaweicloud-devkit
20
+ bootstrap:
21
+ - 'npm ci --omit=dev'
22
+ - 'node plugins/huaweicloud-core/src/setup-cli.mjs install --target hermes --skip-mcp-server'
23
+
24
+ auth:
25
+ type: none
26
+
27
+ tools:
28
+ default_enabled: []
29
+
30
+ post_install: |
31
+ Huawei Cloud DevKit installed successfully!
32
+
33
+ What was set up:
34
+ • 27 skills (ECS, OBS, VPC, RDS, CCE, ModelArts, ...) → ~/.hermes/skills/
35
+ • MCP tools (33 tools for CLI, auth, sandbox, safety) → config.yaml
36
+ • Safety hooks (pre_tool_call terminal guard) → config.yaml
37
+ • Safety policy → ~/.hermes/huaweicloud-plugins/safety/
38
+
39
+ Next steps:
40
+ 1. Configure credentials: npx huaweicloud-devkit auth init
41
+ 2. Restart Hermes Agent session (MCP tools load on restart)
42
+ 3. Verify: hermes doctor
43
+ 4. Try: "Help me check my ECS instances" or "Deploy this website to OBS"