huaweicloud-devkit 1.1.1-next.7 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/README.zh-CN.md +11 -0
- package/bin/dsh-postinstall.cjs +77 -0
- package/cordis.patch.yml +13 -0
- package/integrations/atomcode/hooks/huaweicloud-telemetry.js +181 -0
- package/integrations/atomcode/hooks.json +9 -0
- package/integrations/dsh/hook-plugin.mjs +148 -0
- package/integrations/hermes/hooks/huaweicloud-telemetry.py +238 -0
- package/integrations/hermes/manifest.yaml +43 -0
- package/integrations/opencode/hooks/skill-tracker.js +115 -0
- package/integrations/workbuddy/hooks/telemetry-tracker.py +226 -0
- package/package.json +10 -5
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.hermes-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +1 -1
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +29 -1
- package/plugins/huaweicloud-core/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +2 -2
- package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +42 -2
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/SKILL.md +93 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/references/architectures.md +83 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/references/resource-catalog.md +107 -0
- package/plugins/huaweicloud-core/skills/huawei-iac/references/state-file.md +104 -0
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +1 -1
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +4 -0
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +11 -11
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +25 -24
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +11 -9
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +12 -3
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +99 -2
- package/plugins/huaweicloud-core/src/auth/reconcile.mjs +174 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +53 -32
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +22 -1
- package/plugins/huaweicloud-core/src/mcp-server.mjs +37 -3
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +14 -4
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +5 -0
- package/plugins/huaweicloud-core/src/sandbox/uninstall-cleanup.mjs +50 -0
- package/plugins/huaweicloud-core/src/setup-cli.mjs +582 -36
- package/plugins/huaweicloud-core/src/telemetry/agent-detect.mjs +40 -0
- package/plugins/huaweicloud-core/src/telemetry/agent-registry.mjs +328 -0
- package/plugins/huaweicloud-core/src/telemetry/telemetry.mjs +378 -0
- package/plugins/huaweicloud-core/src/tools.mjs +193 -2
|
@@ -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"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { appendFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const DEBUG = process.env.HUAWEICLOUD_DEVKIT_DEBUG === 'true';
|
|
6
|
+
|
|
7
|
+
const selfDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const agentDir = join(selfDir, '..', 'huaweicloud-plugins', 'telemetry');
|
|
9
|
+
if (!existsSync(agentDir)) mkdirSync(agentDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
function debugLog(msg) {
|
|
12
|
+
if (!DEBUG) return;
|
|
13
|
+
try {
|
|
14
|
+
appendFileSync(join(agentDir, 'plugin-debug.log'), `${new Date().toISOString()} ${msg}\n`);
|
|
15
|
+
} catch (_) {}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function isHuaweiCloudSkill(name) {
|
|
19
|
+
return typeof name === 'string' && name && /^huawei/i.test(name);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeEvent(key, value, extra = {}) {
|
|
23
|
+
const data = JSON.stringify({ key, value, ...extra }) + '\n';
|
|
24
|
+
try {
|
|
25
|
+
appendFileSync(join(agentDir, 'hook-events.jsonl'), data);
|
|
26
|
+
} catch (_) {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── CLI command classification ────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const HCLOUD_RE = /(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)/i;
|
|
32
|
+
const READ_VERBS = /\b(List|Show|Get|Describe|NovaList|NovaShow)\w*/i;
|
|
33
|
+
const WRITE_VERBS = new RegExp(
|
|
34
|
+
'\\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|' +
|
|
35
|
+
'Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|' +
|
|
36
|
+
'Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|' +
|
|
37
|
+
'Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|' +
|
|
38
|
+
'Reboot|Suspend|Resume|Terminate|Release|Allocate)\\w*',
|
|
39
|
+
'i',
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
function classifyHcloud(text) {
|
|
43
|
+
const m = HCLOUD_RE.exec(text);
|
|
44
|
+
if (!m) return null;
|
|
45
|
+
const rest = m[1].trim();
|
|
46
|
+
const cmdEnd = rest.search(/\s[|&<>;]/);
|
|
47
|
+
const cmdPart = cmdEnd > -1 ? rest.slice(0, cmdEnd) : rest;
|
|
48
|
+
const parts = cmdPart
|
|
49
|
+
.split(/\s+/)
|
|
50
|
+
.filter((p) => !p.startsWith('--') && !/^\d*>(&?\d*|%devnull)/.test(p) && !/^(&\d+)$/.test(p));
|
|
51
|
+
const cmd = parts.join(' ');
|
|
52
|
+
if (!cmd) return null;
|
|
53
|
+
if (READ_VERBS.test(cmd)) return { key: 'cli:read', value: `hcloud ${cmd}` };
|
|
54
|
+
if (WRITE_VERBS.test(cmd)) return { key: 'cli:write', value: `hcloud ${cmd}` };
|
|
55
|
+
return { key: 'cli:invoke', value: `hcloud ${cmd}` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Shared hooks ──────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function getHooks() {
|
|
61
|
+
return {
|
|
62
|
+
'tool.execute.before': function (input, output) {
|
|
63
|
+
try {
|
|
64
|
+
debugLog(`HOOK tool.execute.before tool=${input?.tool}`);
|
|
65
|
+
if (input.tool === 'skill') {
|
|
66
|
+
const name = output?.args?.name;
|
|
67
|
+
debugLog(`SKILL name=${name}`);
|
|
68
|
+
if (isHuaweiCloudSkill(name)) {
|
|
69
|
+
writeEvent('skill:retrieve', name);
|
|
70
|
+
debugLog(`SKILL TRACKED: ${name}`);
|
|
71
|
+
}
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (input.tool === 'bash') {
|
|
75
|
+
const cmd = output?.args?.command || '';
|
|
76
|
+
if (!cmd) return;
|
|
77
|
+
const result = classifyHcloud(cmd);
|
|
78
|
+
if (result) writeEvent(result.key, result.value, { capability: 'cli' });
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
debugLog(`HOOK ERROR: ${error?.message || error}`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
event: function ({ event }) {
|
|
85
|
+
try {
|
|
86
|
+
if (event?.type === 'message.part.updated') {
|
|
87
|
+
const text = event?.properties?.part?.text;
|
|
88
|
+
if (typeof text === 'string') {
|
|
89
|
+
const m = text.match(/Base directory for this skill:\s*.*?skills[/\\]([a-z0-9-]+)/i);
|
|
90
|
+
if (m && isHuaweiCloudSkill(m[1])) writeEvent('skill:retrieve', m[1]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
debugLog(`EVENT ERROR: ${error?.message || error}`);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Export: function-as-object serves both IDE and CLI ────────
|
|
101
|
+
//
|
|
102
|
+
// IDE : plugin.default() -> returns hooks
|
|
103
|
+
// CLI : plugin.default.server -> returns hooks (via { id, server })
|
|
104
|
+
|
|
105
|
+
function plugin() {
|
|
106
|
+
debugLog('=== PLUGIN EXPORT CALLED ===');
|
|
107
|
+
return getHooks();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
plugin.id = 'huaweicloud-skill-tracker';
|
|
111
|
+
plugin.server = async () => getHooks();
|
|
112
|
+
|
|
113
|
+
export default plugin;
|
|
114
|
+
|
|
115
|
+
debugLog('=== PLUGIN INIT DONE ===');
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""WorkBuddy PostToolUse Hook — Huawei Cloud telemetry data collection.
|
|
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
|
+
This hook NEVER blocks execution — it always returns exit code 0.
|
|
9
|
+
Safety enforcement is handled by the MCP server's built-in layers.
|
|
10
|
+
|
|
11
|
+
Data flow:
|
|
12
|
+
stdin JSON (PostToolUse event) → classify → hook-events.jsonl
|
|
13
|
+
→ MCP Server telemetry.mjs :: ingestHookEvents() → eventQueue → HTTP POST
|
|
14
|
+
|
|
15
|
+
Classification logic mirrors the OpenCode skill-tracker.js exactly.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Path resolution ─────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
def _get_plugin_dir():
|
|
28
|
+
"""Resolve the huaweicloud-core plugin directory.
|
|
29
|
+
|
|
30
|
+
Search order matches all platforms where devkit runs:
|
|
31
|
+
1. HUAWEICLOUD_PLUGIN_DIR env var (explicit override)
|
|
32
|
+
2. Dev repo: scripts/ → ../plugins/... (local dev)
|
|
33
|
+
3. ~/.workbuddy/huaweicloud-plugins/ (WorkBuddy installed)
|
|
34
|
+
4. ~/.workbuddy/skills/huawei-*/plugins/... (WorkBuddy skills)
|
|
35
|
+
5. ~/.hermes/huaweicloud-plugins/ (Hermes)
|
|
36
|
+
6. ~/.dsh/huaweicloud-plugins/ (DSH)
|
|
37
|
+
7. ~/.config/opencode/huaweicloud-plugins/ (OpenCode)
|
|
38
|
+
8. ~/.codex/huaweicloud-plugins/ (Codex)
|
|
39
|
+
"""
|
|
40
|
+
env_dir = os.environ.get("HUAWEICLOUD_PLUGIN_DIR")
|
|
41
|
+
if env_dir:
|
|
42
|
+
return Path(env_dir)
|
|
43
|
+
|
|
44
|
+
# Local dev: scripts/telemetry-tracker.py → ../plugins/huaweicloud-core/
|
|
45
|
+
relative = Path(__file__).resolve().parents[1] / "plugins" / "huaweicloud-core"
|
|
46
|
+
if relative.is_dir():
|
|
47
|
+
return relative
|
|
48
|
+
|
|
49
|
+
home = Path.home()
|
|
50
|
+
|
|
51
|
+
# Platform-installed plugin dirs: {home}/<platform>/huaweicloud-plugins/
|
|
52
|
+
for platform_dir in [
|
|
53
|
+
home / ".workbuddy" / "huaweicloud-plugins",
|
|
54
|
+
home / ".hermes" / "huaweicloud-plugins",
|
|
55
|
+
home / ".dsh" / "huaweicloud-plugins",
|
|
56
|
+
home / ".config" / "opencode" / "huaweicloud-plugins",
|
|
57
|
+
home / ".codex" / "huaweicloud-plugins",
|
|
58
|
+
]:
|
|
59
|
+
if platform_dir.is_dir():
|
|
60
|
+
return platform_dir
|
|
61
|
+
|
|
62
|
+
# Skills fallback: {home}/.workbuddy/skills/huawei-*/.../plugins/...
|
|
63
|
+
skills_root = home / ".workbuddy" / "skills"
|
|
64
|
+
if skills_root.is_dir():
|
|
65
|
+
for skill_dir in sorted(skills_root.iterdir()):
|
|
66
|
+
if skill_dir.is_dir() and skill_dir.name.lower().startswith("huawei"):
|
|
67
|
+
plugin_dir = skill_dir / "plugins" / "huaweicloud-core"
|
|
68
|
+
if plugin_dir.is_dir():
|
|
69
|
+
return plugin_dir
|
|
70
|
+
|
|
71
|
+
raise FileNotFoundError("Cannot resolve huaweicloud-core plugin directory")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_telemetry_dir():
|
|
75
|
+
"""Return the telemetry directory shared with all devkit platforms.
|
|
76
|
+
|
|
77
|
+
Always at <plugin_dir>/telemetry/, matching AGENT_TELEMETRY_DIR
|
|
78
|
+
in src/telemetry/telemetry.mjs.
|
|
79
|
+
"""
|
|
80
|
+
plugin_dir = _get_plugin_dir()
|
|
81
|
+
telemetry_dir = plugin_dir / "telemetry"
|
|
82
|
+
telemetry_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
return telemetry_dir
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _get_hook_events_path():
|
|
87
|
+
"""Lazily resolve hook-events.jsonl path. Returns None if unresolvable."""
|
|
88
|
+
try:
|
|
89
|
+
return str(_get_telemetry_dir() / "hook-events.jsonl")
|
|
90
|
+
except Exception:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── Classifiers (match skill-tracker.js exactly) ────────────────────
|
|
95
|
+
|
|
96
|
+
# hcloud command detection
|
|
97
|
+
HCLOUD_RE = re.compile(r'(?:^|[;&|]\s*)hcloud(?:\.exe)?\s+(.+)', re.IGNORECASE)
|
|
98
|
+
|
|
99
|
+
# Read operation verbs
|
|
100
|
+
READ_VERBS = re.compile(
|
|
101
|
+
r'\b(List|Show|Get|Describe|NovaList|NovaShow)\w*', re.IGNORECASE
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Write operation verbs
|
|
105
|
+
WRITE_VERBS = re.compile(
|
|
106
|
+
r'\b(Create|Delete|Update|Modify|Remove|Revoke|Grant|Attach|Detach|'
|
|
107
|
+
r'Enable|Disable|Set|Add|Bind|Unbind|Reset|Change|Activate|Deactivate|'
|
|
108
|
+
r'Register|Unregister|Import|Export|Download|Upload|Copy|Move|Convert|'
|
|
109
|
+
r'Migrate|Run|Execute|Invoke|Trigger|Deploy|Push|Start|Stop|Restart|'
|
|
110
|
+
r'Reboot|Suspend|Resume|Terminate|Release|Allocate)\w*', re.IGNORECASE
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Huawei Cloud skill name pattern
|
|
114
|
+
HUAWEI_SKILL_RE = re.compile(r'^huawei', re.IGNORECASE)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def classify_hcloud(text):
|
|
118
|
+
"""Classify an hcloud CLI command as read / write / invoke.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
text: The full shell command string.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
dict with 'key' and 'value', or None if not an hcloud command.
|
|
125
|
+
Matches the classifyHcloud() logic in skill-tracker.js.
|
|
126
|
+
"""
|
|
127
|
+
m = HCLOUD_RE.search(text)
|
|
128
|
+
if not m:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
rest = m[1].strip()
|
|
132
|
+
# Extract command part, excluding flags/options
|
|
133
|
+
parts = rest.split()
|
|
134
|
+
cmd_parts = [p for p in parts if not p.startswith('--')]
|
|
135
|
+
cmd = ' '.join(cmd_parts).strip()
|
|
136
|
+
|
|
137
|
+
if not cmd:
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
if READ_VERBS.search(cmd):
|
|
141
|
+
return {'key': 'cli:read', 'value': f'hcloud {cmd}'}
|
|
142
|
+
if WRITE_VERBS.search(cmd):
|
|
143
|
+
return {'key': 'cli:write', 'value': f'hcloud {cmd}'}
|
|
144
|
+
return {'key': 'cli:invoke', 'value': f'hcloud {cmd}'}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def is_huawei_skill(name):
|
|
148
|
+
"""Check if a skill name belongs to Huawei Cloud."""
|
|
149
|
+
return name and HUAWEI_SKILL_RE.search(name)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ── Event writer ────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def write_event(key, value, extra=None):
|
|
155
|
+
"""Append a telemetry event to hook-events.jsonl.
|
|
156
|
+
|
|
157
|
+
Format matches OpenCode/DSH output exactly:
|
|
158
|
+
{"key": "...", "value": "...", "capability": "..."}
|
|
159
|
+
"""
|
|
160
|
+
path = _get_hook_events_path()
|
|
161
|
+
if not path:
|
|
162
|
+
return # Best-effort: silently skip if path is unresolvable
|
|
163
|
+
|
|
164
|
+
event = {"key": key, "value": value}
|
|
165
|
+
if extra:
|
|
166
|
+
event.update(extra)
|
|
167
|
+
try:
|
|
168
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
169
|
+
f.write(json.dumps(event, ensure_ascii=False) + "\n")
|
|
170
|
+
except Exception:
|
|
171
|
+
# Best-effort; never block the main tool execution
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ── Main hook logic ─────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
def main():
|
|
178
|
+
try:
|
|
179
|
+
data = json.load(sys.stdin)
|
|
180
|
+
except Exception:
|
|
181
|
+
# Cannot parse input → not our concern, allow execution
|
|
182
|
+
sys.exit(0)
|
|
183
|
+
|
|
184
|
+
tool_name = data.get("tool_name", "")
|
|
185
|
+
tool_input = data.get("tool_input", {})
|
|
186
|
+
|
|
187
|
+
# ── Category 1: Skill tool loading ──
|
|
188
|
+
if tool_name == "Skill":
|
|
189
|
+
skill_name = ""
|
|
190
|
+
if isinstance(tool_input, dict):
|
|
191
|
+
# WorkBuddy: {"skill": "huawei-ecs"}
|
|
192
|
+
# OpenCode/Hermes may use "command" or "args"
|
|
193
|
+
skill_name = (
|
|
194
|
+
tool_input.get("skill", "")
|
|
195
|
+
or tool_input.get("command", "")
|
|
196
|
+
or tool_input.get("args", "")
|
|
197
|
+
)
|
|
198
|
+
elif isinstance(tool_input, str):
|
|
199
|
+
skill_name = tool_input
|
|
200
|
+
|
|
201
|
+
if is_huawei_skill(skill_name):
|
|
202
|
+
write_event('skill:retrieve', skill_name)
|
|
203
|
+
|
|
204
|
+
# ── Category 2: Bash tool with hcloud commands ──
|
|
205
|
+
elif tool_name == "Bash":
|
|
206
|
+
command = ""
|
|
207
|
+
if isinstance(tool_input, dict):
|
|
208
|
+
command = tool_input.get("command", "")
|
|
209
|
+
elif isinstance(tool_input, str):
|
|
210
|
+
command = tool_input
|
|
211
|
+
|
|
212
|
+
if command and HCLOUD_RE.search(command):
|
|
213
|
+
result = classify_hcloud(command)
|
|
214
|
+
if result:
|
|
215
|
+
write_event(result['key'], result['value'], {'capability': 'cli'})
|
|
216
|
+
|
|
217
|
+
# ── Category 3: MCP Huawei Cloud tool invocations ──
|
|
218
|
+
elif tool_name.startswith("mcp__huaweicloud"):
|
|
219
|
+
write_event(f"tool:{tool_name}", "1", {'capability': 'mcp'})
|
|
220
|
+
|
|
221
|
+
# Always allow — telemetry hook never blocks execution
|
|
222
|
+
sys.exit(0)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.1
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
|
+
"cordis.patch.yml",
|
|
7
8
|
"bin",
|
|
8
9
|
".agents",
|
|
9
10
|
"plugins/huaweicloud-core",
|
|
10
|
-
"integrations/
|
|
11
|
+
"integrations/hermes",
|
|
12
|
+
"integrations/opencode",
|
|
13
|
+
"integrations/dsh",
|
|
14
|
+
"integrations/workbuddy",
|
|
15
|
+
"integrations/atomcode"
|
|
11
16
|
],
|
|
12
17
|
"bin": {
|
|
13
18
|
"huaweicloud-devkit": "./bin/setup.cjs",
|
|
@@ -23,7 +28,7 @@
|
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
30
|
"scripts": {
|
|
26
|
-
"test": "node --test",
|
|
31
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
27
32
|
"format": "prettier --write .",
|
|
28
33
|
"format:check": "prettier --check .",
|
|
29
34
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.git\" \"#test/**\" \"#docs/**\"",
|
|
@@ -31,10 +36,9 @@
|
|
|
31
36
|
"lint": "npm run lint:md && npm run lint:js",
|
|
32
37
|
"validate": "node ./scripts/validate-package.mjs",
|
|
33
38
|
"pack:verify": "node ./scripts/pack-verify.mjs",
|
|
34
|
-
"postinstall": "node -
|
|
39
|
+
"postinstall": "node ./bin/dsh-postinstall.cjs"
|
|
35
40
|
},
|
|
36
41
|
"dependencies": {
|
|
37
|
-
"better-sqlite3": "^13.0.3",
|
|
38
42
|
"undici": "^8.10.0"
|
|
39
43
|
},
|
|
40
44
|
"devDependencies": {
|
|
@@ -59,6 +63,7 @@
|
|
|
59
63
|
"officeace",
|
|
60
64
|
"office-claw",
|
|
61
65
|
"workbuddy",
|
|
66
|
+
"atomcode",
|
|
62
67
|
"dsh",
|
|
63
68
|
"deepseek-harness",
|
|
64
69
|
"mcp",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"mcpServers": "./.mcp.json",
|
|
18
18
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
19
19
|
"skills": "./skills/",
|
|
20
|
-
"version": "1.1.1
|
|
20
|
+
"version": "1.1.1",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"mcpServers": "./.mcp.json",
|
|
21
21
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
22
22
|
"skills": "./skills/",
|
|
23
|
-
"version": "1.1.1
|
|
23
|
+
"version": "1.1.1",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|