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
package/README.md
CHANGED
|
@@ -11,6 +11,12 @@ Help AI coding agents use Huawei Cloud safely and accurately — a single integr
|
|
|
11
11
|
|
|
12
12
|
Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), OfficeAce, Hermes, OpenClaw, and AtomCode.
|
|
13
13
|
|
|
14
|
+
## Cursor Directory Plugin
|
|
15
|
+
|
|
16
|
+
This repository also acts as an [Open Plugins](https://open-plugins.com) package for the [Cursor Directory](https://cursor.directory) marketplace. The root-level `plugin.json`, `mcp.json`, `skills/`, and `rules/` files are used **only** for Cursor Directory repository-scan discovery and are **not** shipped in the npm tarball (the `package.json` `files` whitelist excludes them). The npm package's manifests live under `plugins/huaweicloud-core/`.
|
|
17
|
+
|
|
18
|
+
The `mcp.json` MCP server launches via `npx -y -p huaweicloud-devkit huaweicloud-devkit-mcp`, which tracks the npm `latest` tag. During pre-release this may differ from the manifest `version`; it can be pinned after a stable release.
|
|
19
|
+
|
|
14
20
|
## Prerequisites
|
|
15
21
|
|
|
16
22
|
- Node.js >= 22
|
|
@@ -27,6 +33,11 @@ Supports OpenCode, Codex, CodeArts Agent, WorkBuddy, DeepSeek Harness (DSH), Off
|
|
|
27
33
|
|
|
28
34
|
> If `--target` is omitted, the installer auto-detects agents on your machine. When multiple agents are detected, **all of them** will be installed. Specify `--target` to control which agent receives the install.
|
|
29
35
|
|
|
36
|
+
```bash
|
|
37
|
+
npx --yes huaweicloud-devkit version # print the installed plugin version per agent
|
|
38
|
+
npx --yes huaweicloud-devkit uninstall --target all --clean-global # also remove KooCLI + OBS config
|
|
39
|
+
```
|
|
40
|
+
|
|
30
41
|
### OpenCode
|
|
31
42
|
|
|
32
43
|
```bash
|
package/README.zh-CN.md
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
支持 OpenCode、Codex、码道(CodeArts Agent)、WorkBuddy、DeepSeek Harness(DSH)、OfficeAce、Hermes、OpenClaw、AtomCode。
|
|
13
13
|
|
|
14
|
+
## Cursor Directory 插件
|
|
15
|
+
|
|
16
|
+
本仓库同时作为 [Open Plugins](https://open-plugins.com) 包挂载到 [Cursor Directory](https://cursor.directory) 市场。根目录的 `plugin.json`、`mcp.json`、`skills/`、`rules/` 仅用于 Cursor Directory 的仓库扫描发现,**不会**打入 npm 包(`package.json` 的 `files` 白名单未包含它们)。npm 包的 manifests 位于 `plugins/huaweicloud-core/`。
|
|
17
|
+
|
|
18
|
+
`mcp.json` 中的 MCP 服务通过 `npx -y -p huaweicloud-devkit huaweicloud-devkit-mcp` 启动,跟踪的是 npm 的 `latest` tag。预发布阶段可能与 manifest 中的 `version` 不一致,稳定发版后可以固定版本。
|
|
19
|
+
|
|
14
20
|
## 前置条件
|
|
15
21
|
|
|
16
22
|
- Node.js >= 22
|
|
@@ -27,6 +33,11 @@
|
|
|
27
33
|
|
|
28
34
|
> 省略 `--target` 时,安装器会自动检测机器上的 agent,检测到多个时**全部安装**。建议始终指定 `--target` 以明确安装目标。
|
|
29
35
|
|
|
36
|
+
```bash
|
|
37
|
+
npx --yes huaweicloud-devkit version # 查看各 agent 已安装的插件版本
|
|
38
|
+
npx --yes huaweicloud-devkit uninstall --target all --clean-global # 一并删除 KooCLI 与 OBS 配置
|
|
39
|
+
```
|
|
40
|
+
|
|
30
41
|
### OpenCode
|
|
31
42
|
|
|
32
43
|
```bash
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
|
|
7
|
+
// ── Detect DSH profile context ──
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const dshHome = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
10
|
+
const profilesRoot = path.join(dshHome, 'profiles');
|
|
11
|
+
|
|
12
|
+
if (!cwd.startsWith(profilesRoot)) {
|
|
13
|
+
// Not inside a DSH profile — traditional install, just print message
|
|
14
|
+
console.log('\nHuaweiCloud DevKit installed. Run: npx huaweicloud-devkit install\n');
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Inside a DSH profile: copy skills to ~/.dsh/skills/ ──
|
|
19
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
20
|
+
|
|
21
|
+
// Rewrite the bundled patch's relative MCP server path to the absolute package
|
|
22
|
+
// path. Relative paths resolve against the DSH process cwd and break when DSH
|
|
23
|
+
// is started outside the profile directory. Idempotent: absolute paths are kept.
|
|
24
|
+
const bundledPatch = path.join(packageRoot, 'cordis.patch.yml');
|
|
25
|
+
if (fs.existsSync(bundledPatch)) {
|
|
26
|
+
const relativeArg = './node_modules/huaweicloud-devkit/plugins/huaweicloud-core/src/mcp-server.mjs';
|
|
27
|
+
const absoluteArg = path
|
|
28
|
+
.join(packageRoot, 'plugins', 'huaweicloud-core', 'src', 'mcp-server.mjs')
|
|
29
|
+
.replace(/\\/g, '/')
|
|
30
|
+
.replace(/'/g, "''");
|
|
31
|
+
const patch = fs.readFileSync(bundledPatch, 'utf8');
|
|
32
|
+
const updated = patch.split(`'${relativeArg}'`).join(`'${absoluteArg}'`);
|
|
33
|
+
if (updated !== patch) {
|
|
34
|
+
fs.writeFileSync(bundledPatch, updated);
|
|
35
|
+
console.log('HuaweiCloud DevKit: rewrote DSH patch MCP path to absolute package path');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const skillsSrc = path.join(packageRoot, 'plugins', 'huaweicloud-core', 'skills');
|
|
40
|
+
const skillsDest = path.join(dshHome, 'skills');
|
|
41
|
+
|
|
42
|
+
if (!fs.existsSync(skillsSrc)) {
|
|
43
|
+
console.log('HuaweiCloud DevKit: skills source not found, skipping auto-setup');
|
|
44
|
+
process.exit(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fs.mkdirSync(skillsDest, { recursive: true });
|
|
48
|
+
|
|
49
|
+
function copyDir(src, dest) {
|
|
50
|
+
if (!fs.existsSync(src)) return;
|
|
51
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
52
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
53
|
+
const s = path.join(src, entry.name);
|
|
54
|
+
const d = path.join(dest, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
copyDir(s, d);
|
|
57
|
+
} else {
|
|
58
|
+
fs.copyFileSync(s, d);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let count = 0;
|
|
64
|
+
for (const entry of fs.readdirSync(skillsSrc, { withFileTypes: true })) {
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
copyDir(path.join(skillsSrc, entry.name), path.join(skillsDest, entry.name));
|
|
67
|
+
count++;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log(`\nHuaweiCloud DevKit: ${count} skills installed to ~/.dsh/skills/`);
|
|
72
|
+
console.log('MCP server will be available after DSH restart.');
|
|
73
|
+
console.log('\n\u001b[1m\u001b[36m 首次使用请配置环境:\u001b[0m');
|
|
74
|
+
console.log(' 1. 安装 KooCLI:npx huaweicloud-devkit install-hcloud');
|
|
75
|
+
console.log(' 2. 配置凭证: npx huaweicloud-devkit auth init');
|
|
76
|
+
console.log(' 3. 重启 DSH 会话后即可使用');
|
|
77
|
+
console.log(' 或者直接在 DSH 中对 Agent 说:帮我安装华为云 KooCLI 并配置凭证\n');
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: huaweicloud-devkit
|
|
3
|
+
name: '@deepseek-ai/dsh-mcp-client'
|
|
4
|
+
config:
|
|
5
|
+
serverName: huaweicloud
|
|
6
|
+
transport: stdio
|
|
7
|
+
command: node
|
|
8
|
+
args:
|
|
9
|
+
- './node_modules/huaweicloud-devkit/plugins/huaweicloud-core/src/mcp-server.mjs'
|
|
10
|
+
env:
|
|
11
|
+
HUAWEICLOUD_AGENT_TOOLKIT_MODE: local
|
|
12
|
+
HDKITSERVICE_ENDPOINT: ''
|
|
13
|
+
failOnStartupError: false
|
|
@@ -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,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
|
+
}
|