huaweicloud-devkit 1.1.4-next.5 → 1.1.4
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/package.json +2 -2
- 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/openclaw.plugin.json +1 -1
- package/plugins/huaweicloud-core/safety/policy.json +1 -0
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +34 -0
- package/plugins/huaweicloud-core/src/auth/service.mjs +135 -0
- package/plugins/huaweicloud-core/src/mcp-config-backup.mjs +70 -0
- package/plugins/huaweicloud-core/src/mcp-config-merge.mjs +132 -0
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +12 -1
- package/plugins/huaweicloud-core/src/setup-cli.mjs +222 -152
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"mcpName": "io.github.huaweicloud/huaweicloud-devkit",
|
|
4
|
-
"version": "1.1.4
|
|
4
|
+
"version": "1.1.4",
|
|
5
5
|
"kooCliVersion": "7.2.12",
|
|
6
6
|
"description": "Agent toolkit that helps coding agents use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities safely and accurately.",
|
|
7
7
|
"type": "module",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
|
-
"test": "node --test \"test/*.test.mjs\"",
|
|
33
|
+
"test": "node --experimental-test-module-mocks --test \"test/*.test.mjs\"",
|
|
34
34
|
"format": "prettier --write .",
|
|
35
35
|
"format:check": "prettier --check .",
|
|
36
36
|
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\" \"#.git\" \"#test/**\" \"#docs/**\" \"#.superpowers/**\"",
|
|
@@ -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.4
|
|
20
|
+
"version": "1.1.4",
|
|
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.4
|
|
23
|
+
"version": "1.1.4",
|
|
24
24
|
"author": {
|
|
25
25
|
"name": "HuaweiCloud Mate",
|
|
26
26
|
"url": "https://github.com/huaweicloud"
|
|
@@ -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.4
|
|
20
|
+
"version": "1.1.4",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "HuaweiCloud Mate",
|
|
23
23
|
"url": "https://github.com/huaweicloud"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.4
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
|
-
"version": "1.1.4
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Guide coding agents to use Huawei Cloud Skills, KooCLI, APIs, SDKs, and future MCP capabilities with safer execution and less context.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "HuaweiCloud Mate",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "huaweicloud-devkit",
|
|
3
3
|
"id": "huaweicloud-devkit",
|
|
4
4
|
"displayName": "HuaweiCloud DevKit",
|
|
5
|
-
"version": "1.1.4
|
|
5
|
+
"version": "1.1.4",
|
|
6
6
|
"family": "bundle-plugin",
|
|
7
7
|
"bundleFormat": "codex",
|
|
8
8
|
"description": "Guide coding agents to use Huawei Cloud safely — KooCLI, APIs, SDKs, 28 MCP tools, skills, and safety guardrails.",
|
|
@@ -177,6 +177,40 @@ export function resolveCredentials(options = {}) {
|
|
|
177
177
|
'Huawei Cloud credentials are not configured. Run "npx huaweicloud-devkit auth init" or set HW_ACCESS_KEY/HW_SECRET_KEY.',
|
|
178
178
|
);
|
|
179
179
|
err.code = 'HDKIT_CRED_MISSING';
|
|
180
|
+
// Lightweight onboarding hint. HDKIT_CRED_MISSING fires when no credential
|
|
181
|
+
// resolved: with env empty/masked and S1 absent, the scenario is 3 by
|
|
182
|
+
// default. Scenario 4 (import a config-provided account) is only reachable
|
|
183
|
+
// in the narrow case where CodeArts S4 offers real creds that the resolver
|
|
184
|
+
// still couldn't adopt (e.g. CODEARTS_PROJECT_DIR set but the marker dir is
|
|
185
|
+
// absent). Full guidance comes from getAuthStatus(); keep this branch cheap.
|
|
186
|
+
const codeartsCreds = readCodeArtsCredentials();
|
|
187
|
+
const injectedAvailable = present(codeartsCreds?.ak) && present(codeartsCreds?.sk);
|
|
188
|
+
err.onboarding = injectedAvailable
|
|
189
|
+
? {
|
|
190
|
+
scenario: 4,
|
|
191
|
+
reason: 'import-injected',
|
|
192
|
+
message: '检测到配置中已有有效账号,可导入为正式凭证。',
|
|
193
|
+
steps: isCodeArtsContext()
|
|
194
|
+
? [{ action: 'auth_switch', args: { mode: 'mcp-config', action: 'persist' }, label: '导入配置中的账号' }]
|
|
195
|
+
: [
|
|
196
|
+
{
|
|
197
|
+
action: 'auth_switch',
|
|
198
|
+
args: { mode: 'import', action: 'persist' },
|
|
199
|
+
label: '通过 creds-import.json 导入',
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
}
|
|
203
|
+
: {
|
|
204
|
+
scenario: 3,
|
|
205
|
+
reason: 's1-missing',
|
|
206
|
+
message: '未配置华为云凭证,需要完成登录后才能使用云能力。',
|
|
207
|
+
steps: isCodeArtsContext()
|
|
208
|
+
? [
|
|
209
|
+
{ action: 'write-import', target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json') },
|
|
210
|
+
{ action: 'auth_switch', args: { mode: 'import', action: 'persist' } },
|
|
211
|
+
]
|
|
212
|
+
: [{ action: 'auth-init', args: {} }],
|
|
213
|
+
};
|
|
180
214
|
throw err;
|
|
181
215
|
}
|
|
182
216
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
|
|
3
5
|
import { getAgentRegistrationStatuses } from './agent-registration.mjs';
|
|
4
6
|
import { resolveAndApplyProjectId } from './project-id.mjs';
|
|
5
7
|
import {
|
|
6
8
|
globalCredentialsPath,
|
|
9
|
+
isPlaceholder,
|
|
7
10
|
obsConfigPath,
|
|
8
11
|
readGlobalCredentials,
|
|
9
12
|
writeLastSync,
|
|
@@ -18,10 +21,141 @@ import {
|
|
|
18
21
|
} from './reconcile.mjs';
|
|
19
22
|
import { hcloudProbeNextStep, probeHcloud } from '../hcloud-probe.mjs';
|
|
20
23
|
|
|
24
|
+
function isCodeArtsHome() {
|
|
25
|
+
return (
|
|
26
|
+
existsSync(join(process.cwd(), '.codeartsdoer')) ||
|
|
27
|
+
existsSync(join(homedir(), '.codeartsdoer')) ||
|
|
28
|
+
existsSync(join(homedir(), '.codeartswork'))
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function envHasRealTriplet() {
|
|
33
|
+
return (
|
|
34
|
+
!isPlaceholder(process.env.HW_ACCESS_KEY) &&
|
|
35
|
+
Boolean(process.env.HW_ACCESS_KEY) &&
|
|
36
|
+
!isPlaceholder(process.env.HW_SECRET_KEY) &&
|
|
37
|
+
Boolean(process.env.HW_SECRET_KEY) &&
|
|
38
|
+
!isPlaceholder(process.env.HW_SECURITY_TOKEN) &&
|
|
39
|
+
Boolean(process.env.HW_SECURITY_TOKEN)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Onboarding guidance (scenarios 1-4) for credential setup.
|
|
44
|
+
// scenario: 1=S1 exists, no env/…; 2=conflict (S1 vs injected); 3=everything
|
|
45
|
+
// missing (fresh user); 4=S1 empty but injected creds exist (import candidate).
|
|
46
|
+
export function computeOnboarding({ credentials, reconciled } = {}) {
|
|
47
|
+
const creds = credentials ?? readGlobalCredentials();
|
|
48
|
+
const scan = reconciled ?? exportStateForStatus();
|
|
49
|
+
const s1Has = Boolean(creds?.ak && creds?.sk && !isPlaceholder(creds.ak) && !isPlaceholder(creds.sk));
|
|
50
|
+
const injected = envHasRealTriplet();
|
|
51
|
+
// env has real non-triplet creds that are NOT placeholders (e.g. devspace AK/SK w/o token)
|
|
52
|
+
const envRealAk = !isPlaceholder(process.env.HW_ACCESS_KEY) && Boolean(process.env.HW_ACCESS_KEY);
|
|
53
|
+
const envRealSk = !isPlaceholder(process.env.HW_SECRET_KEY) && Boolean(process.env.HW_SECRET_KEY);
|
|
54
|
+
const envHasCreds = envRealAk && envRealSk;
|
|
55
|
+
const codeArts = isCodeArtsHome();
|
|
56
|
+
const accountHint = s1Has ? fingerprint(creds.ak, creds.sk) : null;
|
|
57
|
+
|
|
58
|
+
let scenario;
|
|
59
|
+
let reason;
|
|
60
|
+
let message;
|
|
61
|
+
let steps;
|
|
62
|
+
|
|
63
|
+
if (scan.hasRuntime) {
|
|
64
|
+
scenario = 0;
|
|
65
|
+
reason = 'runtime-active';
|
|
66
|
+
message = 'Runtime credentials are active; no setup needed.';
|
|
67
|
+
return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
|
|
68
|
+
}
|
|
69
|
+
if (envHasRealTriplet() && !s1Has) {
|
|
70
|
+
scenario = 0;
|
|
71
|
+
reason = 'platform-injected';
|
|
72
|
+
message = 'Platform credentials are active; nothing to configure.';
|
|
73
|
+
return { needsSetup: false, scenario, reason, message, steps: [], accountHint };
|
|
74
|
+
}
|
|
75
|
+
if (s1Has && !envHasCreds) {
|
|
76
|
+
scenario = 1;
|
|
77
|
+
reason = 's1-only';
|
|
78
|
+
message = `已保存账号(指纹 ${accountHint})可直接使用。`;
|
|
79
|
+
steps = [
|
|
80
|
+
{ order: 1, action: 'use-s1', args: {}, label: '直接使用已保存账号' },
|
|
81
|
+
{ order: 2, action: 'switch-new', args: { mode: 'import', action: 'persist' }, label: '改用新账号(导入)' },
|
|
82
|
+
];
|
|
83
|
+
} else if (s1Has && envHasCreds) {
|
|
84
|
+
// Covers both plain env creds (conflict) AND platform triplet when S1
|
|
85
|
+
// exists. resolveCredentials' STS gate keeps S1 in front of a triplet,
|
|
86
|
+
// so the effective account is S1 → scenario 1 semantics plus a note.
|
|
87
|
+
if (injected) {
|
|
88
|
+
scenario = 1;
|
|
89
|
+
reason = 's1-with-platform-injected';
|
|
90
|
+
message = `已保存账号(指纹 ${accountHint})优先于平台注入,可直接使用。`;
|
|
91
|
+
steps = [
|
|
92
|
+
{ order: 1, action: 'use-s1', args: {}, label: '使用已保存账号' },
|
|
93
|
+
{
|
|
94
|
+
order: 2,
|
|
95
|
+
action: 'switch-platform',
|
|
96
|
+
args: { mode: 'mcp-config', action: 'persist' },
|
|
97
|
+
label: '改用平台注入账号',
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
} else {
|
|
101
|
+
scenario = 2;
|
|
102
|
+
reason = 'conflict';
|
|
103
|
+
message = '检测到两套账号:已保存 与 环境注入,请选择其一。';
|
|
104
|
+
steps = [
|
|
105
|
+
{
|
|
106
|
+
order: 1,
|
|
107
|
+
action: 'switch-persist',
|
|
108
|
+
args: { mode: 'memory', action: 'persist' },
|
|
109
|
+
label: '使用已保存账号覆盖',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
order: 2,
|
|
113
|
+
action: 'switch-env',
|
|
114
|
+
args: { mode: 'mcp-config', action: 'persist' },
|
|
115
|
+
label: '使用环境注入账号导入',
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
} else if (!s1Has && envHasCreds && !injected) {
|
|
120
|
+
scenario = 4;
|
|
121
|
+
reason = 'import-injected';
|
|
122
|
+
message = '检测到配置中已有有效账号,可导入为正式凭证。';
|
|
123
|
+
steps = [
|
|
124
|
+
{
|
|
125
|
+
order: 1,
|
|
126
|
+
action: 'switch-persist',
|
|
127
|
+
args: { mode: codeArts ? 'mcp-config' : 'import', action: 'persist' },
|
|
128
|
+
label: '导入该账号',
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
} else {
|
|
132
|
+
scenario = 3;
|
|
133
|
+
reason = 's1-missing';
|
|
134
|
+
message = '未配置华为云凭证,需要完成登录后才能使用云能力。';
|
|
135
|
+
steps = [
|
|
136
|
+
{ order: 1, action: 'obtain-aksk', target: 'console', label: '获取 AK/SK(华为云控制台)' },
|
|
137
|
+
...(codeArts
|
|
138
|
+
? [
|
|
139
|
+
{
|
|
140
|
+
order: 2,
|
|
141
|
+
action: 'write-import',
|
|
142
|
+
target: join(homedir(), '.config', 'huaweicloud', 'creds-import.json'),
|
|
143
|
+
label: '把 AK/SK 写入 creds-import.json',
|
|
144
|
+
},
|
|
145
|
+
{ order: 3, action: 'auth_switch', args: { mode: 'import', action: 'persist' }, label: '执行导入完成配置' },
|
|
146
|
+
]
|
|
147
|
+
: [{ order: 2, action: 'auth-init', args: {}, label: '运行 npx huaweicloud-devkit auth init' }]),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return { needsSetup: true, scenario, reason, message, steps, accountHint };
|
|
152
|
+
}
|
|
153
|
+
|
|
21
154
|
export function getAuthStatus(target = 'all') {
|
|
22
155
|
const credentials = readGlobalCredentials();
|
|
23
156
|
const reconciled = { ...exportStateForStatus(), runtimeActive: hasRuntimeCredentials() };
|
|
24
157
|
const hcloud = probeHcloud();
|
|
158
|
+
const onboarding = computeOnboarding({ credentials, reconciled });
|
|
25
159
|
return {
|
|
26
160
|
target,
|
|
27
161
|
credentialsConfigured: Boolean(credentials?.ak && credentials?.sk),
|
|
@@ -32,6 +166,7 @@ export function getAuthStatus(target = 'all') {
|
|
|
32
166
|
kooCliStatus: hcloud.status,
|
|
33
167
|
kooCliNextStep: hcloudProbeNextStep(hcloud),
|
|
34
168
|
reconciled,
|
|
169
|
+
onboarding,
|
|
35
170
|
agents: getAgentRegistrationStatuses(target).agents,
|
|
36
171
|
};
|
|
37
172
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Backup/restore of user-customized MCP config fields across uninstall/reinstall (issue #615).
|
|
2
|
+
// Uninstall saves the user delta keyed by agent; a later install/update consumes and applies it.
|
|
3
|
+
//
|
|
4
|
+
// Residual semantics: deltas are namespaced per agent and only that agent's
|
|
5
|
+
// next install/update consumes them (take-once). If the user never reinstalls
|
|
6
|
+
// an agent, its delta simply stays in the backup file — this is expected and
|
|
7
|
+
// harmless (no cross-agent contamination). `uninstall --target all` purges
|
|
8
|
+
// the whole file explicitly.
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { homedir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
export function mcpBackupFilePath(base = process.env.HUAWEICLOUD_HOME || homedir()) {
|
|
15
|
+
return join(base, '.config', 'huaweicloud', 'devkit-mcp-backup.json');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readBackup(file) {
|
|
19
|
+
if (!existsSync(file)) return {};
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(readFileSync(file, 'utf8'));
|
|
22
|
+
return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
|
|
23
|
+
} catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readAgentDelta(agentKey, file = mcpBackupFilePath()) {
|
|
29
|
+
const delta = readBackup(file)[agentKey];
|
|
30
|
+
return delta && typeof delta === 'object' ? delta : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function saveAgentDelta(agentKey, delta, file = mcpBackupFilePath()) {
|
|
34
|
+
if (!agentKey || !delta || typeof delta !== 'object') return false;
|
|
35
|
+
const backup = readBackup(file);
|
|
36
|
+
backup[agentKey] = { ...delta, savedAt: new Date().toISOString() };
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
39
|
+
writeFileSync(file, JSON.stringify(backup, null, 2));
|
|
40
|
+
return true;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Consume the delta for an agent: returns it and removes it from the backup file.
|
|
47
|
+
export function takeAgentDelta(agentKey, file = mcpBackupFilePath()) {
|
|
48
|
+
const delta = readAgentDelta(agentKey, file);
|
|
49
|
+
if (!delta) return null;
|
|
50
|
+
const backup = readBackup(file);
|
|
51
|
+
delete backup[agentKey];
|
|
52
|
+
try {
|
|
53
|
+
if (Object.keys(backup).length === 0) {
|
|
54
|
+
rmSync(file, { force: true });
|
|
55
|
+
} else {
|
|
56
|
+
writeFileSync(file, JSON.stringify(backup, null, 2));
|
|
57
|
+
}
|
|
58
|
+
} catch {}
|
|
59
|
+
return delta;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function purgeBackup(file = mcpBackupFilePath()) {
|
|
63
|
+
if (!existsSync(file)) return false;
|
|
64
|
+
try {
|
|
65
|
+
rmSync(file, { force: true });
|
|
66
|
+
return true;
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Pure helpers for merging HuaweiCloud DevKit MCP config entries without
|
|
2
|
+
// dropping user-customized fields (extra command args, env, timeout, enabled).
|
|
3
|
+
// Program-owned fields (the node executable + mcp-server.mjs path, required env
|
|
4
|
+
// keys) are corrected by the installer; everything else belongs to the user.
|
|
5
|
+
|
|
6
|
+
function isPlainObject(value) {
|
|
7
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function sameJson(a, b) {
|
|
11
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Merge env maps: our required keys are only set when absent; user-set values
|
|
15
|
+
// always win. Returns null when nothing needs to change.
|
|
16
|
+
function mergeEnv(userEnv = {}, requiredEnv = {}) {
|
|
17
|
+
const merged = { ...userEnv };
|
|
18
|
+
let changed = false;
|
|
19
|
+
for (const [key, value] of Object.entries(requiredEnv)) {
|
|
20
|
+
if (merged[key] === undefined) {
|
|
21
|
+
merged[key] = value;
|
|
22
|
+
changed = true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (!changed && sameJson(merged, userEnv)) return null;
|
|
26
|
+
return merged;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function withDefaultTimeout(entry, defaultTimeout) {
|
|
30
|
+
// Pass null explicitly to omit the timeout field (defaults only apply to undefined).
|
|
31
|
+
if (defaultTimeout) entry.timeout = defaultTimeout;
|
|
32
|
+
return entry;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// OpenCode style entry: { type, command: ['node', mcpPath, ...userArgs], ... }
|
|
36
|
+
export function mergeCommandStyle(existing, { mcpPath, defaultTimeout = 300000 } = {}) {
|
|
37
|
+
if (!isPlainObject(existing)) {
|
|
38
|
+
return {
|
|
39
|
+
entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
|
|
40
|
+
changed: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
const commandIsNodeStyle = Array.isArray(existing.command) && existing.command[0] === 'node';
|
|
44
|
+
if (!commandIsNodeStyle) {
|
|
45
|
+
// Foreign wrapper entry: keep current overwrite behavior.
|
|
46
|
+
return {
|
|
47
|
+
entry: withDefaultTimeout({ type: 'local', command: ['node', mcpPath], enabled: true }, defaultTimeout),
|
|
48
|
+
changed: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const merged = { ...existing, type: 'local', command: ['node', mcpPath, ...existing.command.slice(2)] };
|
|
52
|
+
if (merged.timeout === undefined && defaultTimeout) merged.timeout = defaultTimeout;
|
|
53
|
+
if (merged.enabled === undefined) merged.enabled = true;
|
|
54
|
+
return { entry: merged, changed: !sameJson(merged, existing) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// WorkBuddy/AtomCode/CodeArts style entry: { command: 'node', args: [mcpPath, ...userArgs], env, ... }
|
|
58
|
+
export function mergeArgsStyle(existing, { mcpPath, env = {}, defaultTimeout = 300000 } = {}) {
|
|
59
|
+
if (!isPlainObject(existing)) {
|
|
60
|
+
return {
|
|
61
|
+
entry: withDefaultTimeout({ command: 'node', args: [mcpPath], env: { ...env } }, defaultTimeout),
|
|
62
|
+
changed: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const commandIsNodeStyle = existing.command === 'node';
|
|
66
|
+
if (!commandIsNodeStyle) {
|
|
67
|
+
return {
|
|
68
|
+
entry: withDefaultTimeout({ command: 'node', args: [mcpPath], env: { ...env } }, defaultTimeout),
|
|
69
|
+
changed: true,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const merged = {
|
|
73
|
+
...existing,
|
|
74
|
+
command: 'node',
|
|
75
|
+
args: [mcpPath, ...(Array.isArray(existing.args) ? existing.args.slice(1) : [])],
|
|
76
|
+
};
|
|
77
|
+
const envMerged = mergeEnv(isPlainObject(existing.env) ? existing.env : {}, env);
|
|
78
|
+
if (envMerged) merged.env = envMerged;
|
|
79
|
+
if (merged.timeout === undefined && defaultTimeout) merged.timeout = defaultTimeout;
|
|
80
|
+
return { entry: merged, changed: !sameJson(merged, existing) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Codex Desktop / OpenClaw .mcp.json style: { mcpServers: { 'huaweicloud-devkit': entry } }
|
|
84
|
+
// Fresh entries keep the historical shape (no timeout field); user-set timeouts are preserved.
|
|
85
|
+
export function mergeMcpServersFile(config, { mcpPath, env = {} } = {}) {
|
|
86
|
+
const existing = isPlainObject(config) ? config.mcpServers?.['huaweicloud-devkit'] : undefined;
|
|
87
|
+
const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env, defaultTimeout: null });
|
|
88
|
+
const next = isPlainObject(config) ? { ...config } : {};
|
|
89
|
+
next.mcpServers = { ...(isPlainObject(config) ? config.mcpServers : {}), 'huaweicloud-devkit': entry };
|
|
90
|
+
return { entry, config: next, changed: changed || !isPlainObject(config) || !existing };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Env keys the installer manages itself (recomputed on every install) — never
|
|
94
|
+
// treated as user assets for backup purposes.
|
|
95
|
+
const REQUIRED_ENV_KEYS = new Set(['HUAWEICLOUD_AGENT_TOOLKIT_MODE', 'HCLOUD_BIN']);
|
|
96
|
+
|
|
97
|
+
// Extract the user-owned delta of an entry for backup before uninstall removes it.
|
|
98
|
+
export function extractUserDelta(entry, style) {
|
|
99
|
+
if (!isPlainObject(entry)) return null;
|
|
100
|
+
const delta = {};
|
|
101
|
+
if (style === 'command') {
|
|
102
|
+
if (Array.isArray(entry.command) && entry.command.length > 2) delta.commandExtra = entry.command.slice(2);
|
|
103
|
+
} else if (style === 'args') {
|
|
104
|
+
if (Array.isArray(entry.args) && entry.args.length > 1) delta.argsExtra = entry.args.slice(1);
|
|
105
|
+
} else {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (isPlainObject(entry.env)) {
|
|
109
|
+
const userEnv = Object.fromEntries(Object.entries(entry.env).filter(([key]) => !REQUIRED_ENV_KEYS.has(key)));
|
|
110
|
+
if (Object.keys(userEnv).length > 0) delta.env = userEnv;
|
|
111
|
+
}
|
|
112
|
+
if (entry.timeout !== undefined && entry.timeout !== 300000) delta.timeout = entry.timeout;
|
|
113
|
+
if (entry.enabled === false) delta.enabled = false;
|
|
114
|
+
return Object.keys(delta).length > 0 ? delta : null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Apply a previously saved delta onto a freshly written default entry.
|
|
118
|
+
export function applyUserDelta(entry, delta, style) {
|
|
119
|
+
if (!isPlainObject(entry) || !isPlainObject(delta)) return entry;
|
|
120
|
+
const merged = { ...entry };
|
|
121
|
+
if (style === 'command' && Array.isArray(delta.commandExtra)) {
|
|
122
|
+
merged.command = [...entry.command, ...delta.commandExtra];
|
|
123
|
+
} else if (style === 'args' && Array.isArray(delta.argsExtra)) {
|
|
124
|
+
merged.args = [...entry.args, ...delta.argsExtra];
|
|
125
|
+
}
|
|
126
|
+
if (isPlainObject(delta.env)) {
|
|
127
|
+
merged.env = { ...(isPlainObject(entry.env) ? entry.env : {}), ...delta.env };
|
|
128
|
+
}
|
|
129
|
+
if (delta.timeout !== undefined) merged.timeout = delta.timeout;
|
|
130
|
+
if (delta.enabled === false) merged.enabled = false;
|
|
131
|
+
return merged;
|
|
132
|
+
}
|
|
@@ -3,6 +3,17 @@ import { getProxyUrlForTarget } from './proxy-config.mjs';
|
|
|
3
3
|
let cachedDispatcher = undefined;
|
|
4
4
|
let cachedDispatcherProxyUrl = null;
|
|
5
5
|
|
|
6
|
+
// Enterprise MITM proxies re-sign TLS on BOTH legs: the proxy's own certificate
|
|
7
|
+
// (`proxyTls`) and the tunneled connection to the target server (`requestTls`).
|
|
8
|
+
// Both must be relaxed for such environments. Note `requestTls` is NOT the proxy
|
|
9
|
+
// cert — it is the target-server cert seen through the tunnel. This relaxation is
|
|
10
|
+
// scoped to proxied connections only; direct connections keep strict certificate
|
|
11
|
+
// verification (see the non-proxy branch of `fetchWithProxy`).
|
|
12
|
+
export const PROXY_TLS_OPTIONS = {
|
|
13
|
+
proxyTls: { rejectUnauthorized: false },
|
|
14
|
+
requestTls: { rejectUnauthorized: false },
|
|
15
|
+
};
|
|
16
|
+
|
|
6
17
|
async function importUndici() {
|
|
7
18
|
try {
|
|
8
19
|
/* eslint-disable-next-line n/no-missing-import */
|
|
@@ -21,7 +32,7 @@ export async function getProxyDispatcher(targetUrl) {
|
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
const { ProxyAgent } = await importUndici();
|
|
24
|
-
cachedDispatcher = new ProxyAgent(proxyUrl);
|
|
35
|
+
cachedDispatcher = new ProxyAgent({ uri: proxyUrl, ...PROXY_TLS_OPTIONS });
|
|
25
36
|
cachedDispatcherProxyUrl = proxyUrl;
|
|
26
37
|
return cachedDispatcher;
|
|
27
38
|
}
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
getProxySettings,
|
|
37
37
|
} from './proxy/proxy-config.mjs';
|
|
38
38
|
import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
|
|
39
|
+
import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
|
|
40
|
+
import { saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
|
|
39
41
|
import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
|
|
40
42
|
import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
|
|
41
43
|
import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
|
|
@@ -261,15 +263,30 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
261
263
|
const envJson = JSON.stringify(env);
|
|
262
264
|
|
|
263
265
|
if (existing) {
|
|
264
|
-
|
|
266
|
+
// Merge: keep user's extra args/env values, only correct program-owned fields (issue #615).
|
|
267
|
+
let existingArgs = [];
|
|
268
|
+
let existingEnv = [];
|
|
269
|
+
try {
|
|
270
|
+
existingArgs = JSON.parse(existing.args_json || '[]');
|
|
271
|
+
existingEnv = JSON.parse(existing.env_json || '[]');
|
|
272
|
+
} catch {}
|
|
273
|
+
const userArgs = Array.isArray(existingArgs) ? existingArgs.slice(1) : [];
|
|
274
|
+
// Start from the user's env and add our required keys only when absent (user values win).
|
|
275
|
+
const envMerged = [...(Array.isArray(existingEnv) ? existingEnv : [])];
|
|
276
|
+
for (const e of env) {
|
|
277
|
+
if (e && e.key && !envMerged.some((x) => x && x.key === e.key)) envMerged.push(e);
|
|
278
|
+
}
|
|
279
|
+
const nextArgsJson = JSON.stringify([mcpPath, ...userArgs]);
|
|
280
|
+
const nextEnvJson = JSON.stringify(envMerged);
|
|
281
|
+
if (existing.command === 'node' && existing.args_json === nextArgsJson && existing.env_json === nextEnvJson) {
|
|
265
282
|
console.log(` MCP config unchanged: ${dbPath}`);
|
|
266
283
|
db.close();
|
|
267
284
|
return true;
|
|
268
285
|
}
|
|
269
286
|
db.prepare(
|
|
270
287
|
'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
|
|
271
|
-
).run('node',
|
|
272
|
-
console.log(` MCP config
|
|
288
|
+
).run('node', nextArgsJson, nextEnvJson, now, 'disconnected', existing.id);
|
|
289
|
+
console.log(` MCP config merged (user fields preserved): ${dbPath}`);
|
|
273
290
|
} else {
|
|
274
291
|
const ownerUserId = officeaceGetOwnerUserId();
|
|
275
292
|
if (!ownerUserId) {
|
|
@@ -277,10 +294,16 @@ function ensureOfficeaceMcpInSqlite() {
|
|
|
277
294
|
db.close();
|
|
278
295
|
return false;
|
|
279
296
|
}
|
|
297
|
+
// Restore user args saved by a previous uninstall (issue #615).
|
|
298
|
+
const officeaceDelta = takeAgentDelta('officeace');
|
|
299
|
+
const insertArgsJson =
|
|
300
|
+
officeaceDelta && Array.isArray(officeaceDelta.argsExtra)
|
|
301
|
+
? JSON.stringify([mcpPath, ...officeaceDelta.argsExtra])
|
|
302
|
+
: argsJson;
|
|
280
303
|
db.prepare(
|
|
281
304
|
`INSERT INTO mcp_connectors (id, owner_user_id, type, name, normalized_name, transport, timeout_ms, command, args_json, env_json, enabled, status, created_at, updated_at, version, seeded)
|
|
282
305
|
VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
|
|
283
|
-
).run(randomUUID(), ownerUserId,
|
|
306
|
+
).run(randomUUID(), ownerUserId, insertArgsJson, envJson, now, now);
|
|
284
307
|
console.log(` MCP config created: ${dbPath}`);
|
|
285
308
|
}
|
|
286
309
|
db.close();
|
|
@@ -302,6 +325,14 @@ function removeOfficeaceMcpFromSqlite() {
|
|
|
302
325
|
let db;
|
|
303
326
|
try {
|
|
304
327
|
db = openOfficeaceDb();
|
|
328
|
+
// Back up user args before deleting the connector row (issue #615).
|
|
329
|
+
try {
|
|
330
|
+
const row = db.prepare("SELECT args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").get();
|
|
331
|
+
const parsed = row ? JSON.parse(row.args_json || '[]') : [];
|
|
332
|
+
if (Array.isArray(parsed) && parsed.length > 1) {
|
|
333
|
+
saveAgentDelta('officeace', { argsExtra: parsed.slice(1) });
|
|
334
|
+
}
|
|
335
|
+
} catch {}
|
|
305
336
|
db.prepare(
|
|
306
337
|
"DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
|
|
307
338
|
).run();
|
|
@@ -558,29 +589,68 @@ function updateOpenCodeConfig(pluginDir) {
|
|
|
558
589
|
return;
|
|
559
590
|
}
|
|
560
591
|
const existing = config.mcp?.['huaweicloud-devkit'];
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
existing.type === 'local' &&
|
|
564
|
-
Array.isArray(existing.command) &&
|
|
565
|
-
existing.command[0] === 'node' &&
|
|
566
|
-
existing.command[1] === mcpPath &&
|
|
567
|
-
existing.timeout === 300000
|
|
568
|
-
) {
|
|
592
|
+
const { entry, changed } = mergeCommandStyle(existing, { mcpPath });
|
|
593
|
+
if (existing && !changed) {
|
|
569
594
|
console.log(` OpenCode MCP config unchanged: ${configPath}`);
|
|
570
595
|
return;
|
|
571
596
|
}
|
|
597
|
+
if (existing && changed) {
|
|
598
|
+
config.mcp['huaweicloud-devkit'] = entry;
|
|
599
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
600
|
+
console.log(` OpenCode MCP config merged (user fields preserved): ${configPath}`);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
572
603
|
}
|
|
573
604
|
config.mcp = config.mcp || {};
|
|
574
|
-
config.mcp['huaweicloud-devkit'] = {
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
timeout: 300000,
|
|
579
|
-
};
|
|
605
|
+
config.mcp['huaweicloud-devkit'] = mergeCommandStyle(undefined, { mcpPath }).entry;
|
|
606
|
+
// Restore user fields saved by a previous uninstall (issue #615).
|
|
607
|
+
const delta = takeAgentDelta('opencode');
|
|
608
|
+
if (delta) config.mcp['huaweicloud-devkit'] = applyUserDelta(config.mcp['huaweicloud-devkit'], delta, 'command');
|
|
580
609
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
581
610
|
console.log(` OpenCode config updated: ${configPath}`);
|
|
582
611
|
}
|
|
583
612
|
|
|
613
|
+
// Read-merge-write for pluginDir/.mcp.json (OpenClaw, Codex Desktop).
|
|
614
|
+
// Preserves user extra args/env; restores a prior uninstall's backup on fresh install (issue #615).
|
|
615
|
+
function writeMcpServersFile(pluginDest, mcpPath, agentKey) {
|
|
616
|
+
const configPath = join(pluginDest, '.mcp.json');
|
|
617
|
+
let config;
|
|
618
|
+
try {
|
|
619
|
+
config = existsSync(configPath) ? JSON.parse(readFileSync(configPath, 'utf8')) : undefined;
|
|
620
|
+
} catch {
|
|
621
|
+
config = undefined;
|
|
622
|
+
}
|
|
623
|
+
const existing = config && typeof config === 'object' ? config.mcpServers?.['huaweicloud-devkit'] : undefined;
|
|
624
|
+
const { entry, changed } = mergeArgsStyle(existing, {
|
|
625
|
+
mcpPath,
|
|
626
|
+
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
627
|
+
defaultTimeout: null,
|
|
628
|
+
});
|
|
629
|
+
if (existing && !changed) {
|
|
630
|
+
console.log(` MCP Config unchanged: ${configPath}`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const next = config && typeof config === 'object' && !Array.isArray(config) ? { ...config } : {};
|
|
634
|
+
next.mcpServers = { ...(config && typeof config === 'object' ? config.mcpServers : {}), 'huaweicloud-devkit': entry };
|
|
635
|
+
if (!existing) {
|
|
636
|
+
const delta = takeAgentDelta(agentKey);
|
|
637
|
+
if (delta)
|
|
638
|
+
next.mcpServers['huaweicloud-devkit'] = applyUserDelta(next.mcpServers['huaweicloud-devkit'], delta, 'args');
|
|
639
|
+
}
|
|
640
|
+
writeFileSync(configPath, JSON.stringify(next, null, 2));
|
|
641
|
+
console.log(` MCP Config -> ${configPath}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Back up the user delta from a pluginDir/.mcp.json before the directory is removed (issue #615).
|
|
645
|
+
function backupDeltaFromMcpServersFile(pluginDest, agentKey) {
|
|
646
|
+
const configPath = join(pluginDest, '.mcp.json');
|
|
647
|
+
try {
|
|
648
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
649
|
+
const delta = extractUserDelta(config?.mcpServers?.['huaweicloud-devkit'], 'args');
|
|
650
|
+
if (delta) saveAgentDelta(agentKey, delta);
|
|
651
|
+
} catch {}
|
|
652
|
+
}
|
|
653
|
+
|
|
584
654
|
function removeOpenCodeConfig() {
|
|
585
655
|
const configPath = opencodeConfigFile();
|
|
586
656
|
if (!existsSync(configPath)) return;
|
|
@@ -591,6 +661,9 @@ function removeOpenCodeConfig() {
|
|
|
591
661
|
return;
|
|
592
662
|
}
|
|
593
663
|
if (!config.mcp?.['huaweicloud-devkit']) return;
|
|
664
|
+
// Back up user-customized fields before removal so a later reinstall can restore them (issue #615).
|
|
665
|
+
const delta = extractUserDelta(config.mcp['huaweicloud-devkit'], 'command');
|
|
666
|
+
if (delta) saveAgentDelta('opencode', delta);
|
|
594
667
|
delete config.mcp['huaweicloud-devkit'];
|
|
595
668
|
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
596
669
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -920,17 +993,7 @@ async function installOpenClaw() {
|
|
|
920
993
|
console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
|
|
921
994
|
|
|
922
995
|
const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
923
|
-
|
|
924
|
-
mcpServers: {
|
|
925
|
-
'huaweicloud-devkit': {
|
|
926
|
-
command: 'node',
|
|
927
|
-
args: [mcpServerAbsPath],
|
|
928
|
-
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
929
|
-
},
|
|
930
|
-
},
|
|
931
|
-
};
|
|
932
|
-
writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
933
|
-
console.log(` MCP Config -> ${join(pluginDest, '.mcp.json')}`);
|
|
996
|
+
writeMcpServersFile(pluginDest, mcpServerAbsPath, 'openclaw');
|
|
934
997
|
|
|
935
998
|
const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
|
|
936
999
|
if (existsSync(codexPluginSrc)) {
|
|
@@ -967,6 +1030,7 @@ function uninstallOpenClaw() {
|
|
|
967
1030
|
if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
|
|
968
1031
|
}
|
|
969
1032
|
|
|
1033
|
+
backupDeltaFromMcpServersFile(openclawPluginsDir(), 'openclaw');
|
|
970
1034
|
if (removeIfExists(openclawPluginsDir())) {
|
|
971
1035
|
console.log(' Removed MCP server and safety policy');
|
|
972
1036
|
}
|
|
@@ -991,17 +1055,7 @@ async function updateOpenClaw() {
|
|
|
991
1055
|
console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
|
|
992
1056
|
|
|
993
1057
|
const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
994
|
-
|
|
995
|
-
mcpServers: {
|
|
996
|
-
'huaweicloud-devkit': {
|
|
997
|
-
command: 'node',
|
|
998
|
-
args: [mcpServerAbsPath],
|
|
999
|
-
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
1000
|
-
},
|
|
1001
|
-
},
|
|
1002
|
-
};
|
|
1003
|
-
writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
1004
|
-
console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
|
|
1058
|
+
writeMcpServersFile(pluginDest, mcpServerAbsPath, 'openclaw');
|
|
1005
1059
|
|
|
1006
1060
|
const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
|
|
1007
1061
|
if (existsSync(codexPluginSrc)) {
|
|
@@ -1039,17 +1093,7 @@ async function installCodexDesktop() {
|
|
|
1039
1093
|
|
|
1040
1094
|
// Generate .mcp.json for Codex plugin MCP server discovery
|
|
1041
1095
|
const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
1042
|
-
|
|
1043
|
-
mcpServers: {
|
|
1044
|
-
'huaweicloud-devkit': {
|
|
1045
|
-
command: 'node',
|
|
1046
|
-
args: [mcpServerAbsPath],
|
|
1047
|
-
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
1048
|
-
},
|
|
1049
|
-
},
|
|
1050
|
-
};
|
|
1051
|
-
writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
1052
|
-
console.log(` MCP Config -> ${join(pluginDest, '.mcp.json')}`);
|
|
1096
|
+
writeMcpServersFile(pluginDest, mcpServerAbsPath, 'codex-desktop');
|
|
1053
1097
|
|
|
1054
1098
|
// Copy .codex-plugin manifest for Codex Desktop plugin registration
|
|
1055
1099
|
const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
|
|
@@ -1106,17 +1150,7 @@ async function updateCodexDesktop() {
|
|
|
1106
1150
|
}
|
|
1107
1151
|
|
|
1108
1152
|
const mcpServerAbsPath = join(pluginDest, 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
1109
|
-
|
|
1110
|
-
mcpServers: {
|
|
1111
|
-
'huaweicloud-devkit': {
|
|
1112
|
-
command: 'node',
|
|
1113
|
-
args: [mcpServerAbsPath],
|
|
1114
|
-
env: { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' },
|
|
1115
|
-
},
|
|
1116
|
-
},
|
|
1117
|
-
};
|
|
1118
|
-
writeFileSync(join(pluginDest, '.mcp.json'), JSON.stringify(mcpConfig, null, 2));
|
|
1119
|
-
console.log(` MCP Config updated -> ${join(pluginDest, '.mcp.json')}`);
|
|
1153
|
+
writeMcpServersFile(pluginDest, mcpServerAbsPath, 'codex-desktop');
|
|
1120
1154
|
|
|
1121
1155
|
const codexPluginSrc = join(PLUGIN_ROOT, '.codex-plugin');
|
|
1122
1156
|
if (existsSync(codexPluginSrc)) {
|
|
@@ -1155,6 +1189,7 @@ function uninstallCodexDesktop() {
|
|
|
1155
1189
|
if (cmdRemoved > 0) console.log(` Removed ${cmdRemoved} commands`);
|
|
1156
1190
|
}
|
|
1157
1191
|
|
|
1192
|
+
backupDeltaFromMcpServersFile(pluginDest, 'codex-desktop');
|
|
1158
1193
|
if (removeIfExists(pluginDest)) {
|
|
1159
1194
|
console.log(' Removed MCP server and safety policy');
|
|
1160
1195
|
}
|
|
@@ -1185,10 +1220,13 @@ function uninstallCodexDesktop() {
|
|
|
1185
1220
|
}
|
|
1186
1221
|
}
|
|
1187
1222
|
|
|
1188
|
-
function registerCodeartsMcp(configPath) {
|
|
1223
|
+
function registerCodeartsMcp(configPath, agentKey = 'codearts') {
|
|
1189
1224
|
const mcpPath = join(codeartsPluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
|
|
1190
1225
|
const hcloudBin = findHcloudBin();
|
|
1226
|
+
const env = { HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local' };
|
|
1227
|
+
if (hcloudBin) env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
|
|
1191
1228
|
let config = {};
|
|
1229
|
+
let existing;
|
|
1192
1230
|
if (existsSync(configPath)) {
|
|
1193
1231
|
try {
|
|
1194
1232
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
@@ -1198,51 +1236,27 @@ function registerCodeartsMcp(configPath) {
|
|
|
1198
1236
|
);
|
|
1199
1237
|
return;
|
|
1200
1238
|
}
|
|
1201
|
-
|
|
1202
|
-
if (
|
|
1203
|
-
existing
|
|
1204
|
-
|
|
1205
|
-
Array.isArray(existing.args) &&
|
|
1206
|
-
existing.args[0] === mcpPath &&
|
|
1207
|
-
existing.timeout === 300000
|
|
1208
|
-
) {
|
|
1209
|
-
let changed = false;
|
|
1210
|
-
if (!existing.env) {
|
|
1211
|
-
existing.env = {};
|
|
1212
|
-
changed = true;
|
|
1213
|
-
}
|
|
1214
|
-
if (!existing.env.HUAWEICLOUD_AGENT_TOOLKIT_MODE) {
|
|
1215
|
-
existing.env.HUAWEICLOUD_AGENT_TOOLKIT_MODE = 'local';
|
|
1216
|
-
changed = true;
|
|
1217
|
-
}
|
|
1218
|
-
if (hcloudBin && !existing.env.HCLOUD_BIN) {
|
|
1219
|
-
existing.env.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
|
|
1220
|
-
changed = true;
|
|
1221
|
-
}
|
|
1222
|
-
if (existing.enabled !== true) {
|
|
1223
|
-
existing.enabled = true;
|
|
1224
|
-
changed = true;
|
|
1225
|
-
}
|
|
1226
|
-
if (changed) {
|
|
1227
|
-
mkdirSync(dirname(configPath), { recursive: true });
|
|
1228
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1229
|
-
console.log(` MCP config refreshed: ${configPath}`);
|
|
1230
|
-
} else {
|
|
1239
|
+
existing = config.mcpServers?.['huaweicloud-devkit'];
|
|
1240
|
+
if (existing) {
|
|
1241
|
+
const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
|
|
1242
|
+
if (!changed) {
|
|
1231
1243
|
console.log(` MCP config unchanged: ${configPath}`);
|
|
1244
|
+
return;
|
|
1232
1245
|
}
|
|
1246
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1247
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1248
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1249
|
+
console.log(` MCP config merged (user fields preserved): ${configPath}`);
|
|
1233
1250
|
return;
|
|
1234
1251
|
}
|
|
1235
1252
|
}
|
|
1236
1253
|
config.mcpServers = config.mcpServers || {};
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
enabled: true,
|
|
1244
|
-
timeout: 300000,
|
|
1245
|
-
};
|
|
1254
|
+
let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
|
|
1255
|
+
entry.enabled = true;
|
|
1256
|
+
// Restore user fields saved by a previous uninstall (issue #615).
|
|
1257
|
+
const delta = takeAgentDelta(agentKey);
|
|
1258
|
+
if (delta) entry = applyUserDelta(entry, delta, 'args');
|
|
1259
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1246
1260
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
1247
1261
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1248
1262
|
console.log(` MCP config updated: ${configPath}`);
|
|
@@ -1336,6 +1350,8 @@ function uninstallCodeArts() {
|
|
|
1336
1350
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1337
1351
|
} catch {}
|
|
1338
1352
|
if (config.mcpServers?.['huaweicloud-devkit']) {
|
|
1353
|
+
const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
|
|
1354
|
+
if (delta) saveAgentDelta('codearts', delta);
|
|
1339
1355
|
delete config.mcpServers['huaweicloud-devkit'];
|
|
1340
1356
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
1341
1357
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -1392,31 +1408,44 @@ function registerCodeartsWorkMcp() {
|
|
|
1392
1408
|
return;
|
|
1393
1409
|
}
|
|
1394
1410
|
const existing = config.mcp?.['huaweicloud-devkit'];
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1397
|
-
existing
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1411
|
+
if (existing) {
|
|
1412
|
+
// codearts-work uses `command` array + `environment` naming; adapt via mergeCommandStyle.
|
|
1413
|
+
const { entry, changed } = mergeCommandStyle(existing, { mcpPath });
|
|
1414
|
+
if (!changed) {
|
|
1415
|
+
console.log(` MCP config unchanged: ${configPath}`);
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const merged = { ...entry };
|
|
1419
|
+
merged.environment = {
|
|
1420
|
+
...(isPlainLocal(existing.environment) ? existing.environment : {}),
|
|
1421
|
+
HUAWEICLOUD_AGENT_TOOLKIT_MODE: 'local',
|
|
1422
|
+
};
|
|
1423
|
+
if (hcloudBin && merged.environment.HCLOUD_BIN === undefined) {
|
|
1424
|
+
merged.environment.HCLOUD_BIN = hcloudBin.replace(/\\/g, '/');
|
|
1425
|
+
}
|
|
1426
|
+
config.mcp['huaweicloud-devkit'] = merged;
|
|
1427
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1428
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1429
|
+
console.log(` MCP config merged (user fields preserved): ${configPath}`);
|
|
1404
1430
|
return;
|
|
1405
1431
|
}
|
|
1406
1432
|
}
|
|
1407
1433
|
config.mcp = config.mcp || {};
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
};
|
|
1434
|
+
let entry = mergeCommandStyle(undefined, { mcpPath }).entry;
|
|
1435
|
+
entry.environment = { ...environment };
|
|
1436
|
+
// Restore user fields saved by a previous uninstall (issue #615).
|
|
1437
|
+
const delta = takeAgentDelta('codearts-work');
|
|
1438
|
+
if (delta) entry = applyUserDelta(entry, delta, 'command');
|
|
1439
|
+
config.mcp['huaweicloud-devkit'] = entry;
|
|
1415
1440
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
1416
1441
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1417
1442
|
console.log(` MCP config updated: ${configPath}`);
|
|
1418
1443
|
}
|
|
1419
1444
|
|
|
1445
|
+
function isPlainLocal(value) {
|
|
1446
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1420
1449
|
async function installCodeArtsWork() {
|
|
1421
1450
|
const skillsSrc = join(PLUGIN_ROOT, 'skills');
|
|
1422
1451
|
const srcDir = join(PLUGIN_ROOT, 'src');
|
|
@@ -1477,6 +1506,8 @@ function uninstallCodeArtsWork() {
|
|
|
1477
1506
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1478
1507
|
} catch {}
|
|
1479
1508
|
if (config.mcp?.['huaweicloud-devkit']) {
|
|
1509
|
+
const delta = extractUserDelta(config.mcp['huaweicloud-devkit'], 'command');
|
|
1510
|
+
if (delta) saveAgentDelta('codearts-work', delta);
|
|
1480
1511
|
delete config.mcp['huaweicloud-devkit'];
|
|
1481
1512
|
if (Object.keys(config.mcp).length === 0) delete config.mcp;
|
|
1482
1513
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -1532,24 +1563,25 @@ function ensureWorkbuddyMcpConfig() {
|
|
|
1532
1563
|
return false;
|
|
1533
1564
|
}
|
|
1534
1565
|
const existing = config.mcpServers?.['huaweicloud-devkit'];
|
|
1535
|
-
if (
|
|
1536
|
-
existing
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1566
|
+
if (existing) {
|
|
1567
|
+
const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
|
|
1568
|
+
if (!changed) {
|
|
1569
|
+
console.log(` MCP config unchanged: ${configPath}`);
|
|
1570
|
+
return false;
|
|
1571
|
+
}
|
|
1572
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1573
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1574
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1575
|
+
console.log(` MCP config merged (user fields preserved): ${configPath}`);
|
|
1576
|
+
return true;
|
|
1544
1577
|
}
|
|
1545
1578
|
}
|
|
1546
1579
|
config.mcpServers = config.mcpServers || {};
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
};
|
|
1580
|
+
let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
|
|
1581
|
+
// Restore user fields saved by a previous uninstall (issue #615).
|
|
1582
|
+
const delta = takeAgentDelta('workbuddy');
|
|
1583
|
+
if (delta) entry = applyUserDelta(entry, delta, 'args');
|
|
1584
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1553
1585
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
1554
1586
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1555
1587
|
console.log(` MCP config updated: ${configPath}`);
|
|
@@ -1762,6 +1794,8 @@ function uninstallWorkBuddy() {
|
|
|
1762
1794
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1763
1795
|
} catch {}
|
|
1764
1796
|
if (config.mcpServers?.['huaweicloud-devkit']) {
|
|
1797
|
+
const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
|
|
1798
|
+
if (delta) saveAgentDelta('workbuddy', delta);
|
|
1765
1799
|
delete config.mcpServers['huaweicloud-devkit'];
|
|
1766
1800
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
1767
1801
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -1861,24 +1895,25 @@ function ensureAtomcodeMcpConfig() {
|
|
|
1861
1895
|
return false;
|
|
1862
1896
|
}
|
|
1863
1897
|
const existing = config.mcpServers?.['huaweicloud-devkit'];
|
|
1864
|
-
if (
|
|
1865
|
-
existing
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1898
|
+
if (existing) {
|
|
1899
|
+
const { entry, changed } = mergeArgsStyle(existing, { mcpPath, env });
|
|
1900
|
+
if (!changed) {
|
|
1901
|
+
console.log(` MCP config unchanged: ${configPath}`);
|
|
1902
|
+
return false;
|
|
1903
|
+
}
|
|
1904
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1905
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
1906
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1907
|
+
console.log(` MCP config merged (user fields preserved): ${configPath}`);
|
|
1908
|
+
return true;
|
|
1873
1909
|
}
|
|
1874
1910
|
}
|
|
1875
1911
|
config.mcpServers = config.mcpServers || {};
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
};
|
|
1912
|
+
let entry = mergeArgsStyle(undefined, { mcpPath, env }).entry;
|
|
1913
|
+
// Restore user fields saved by a previous uninstall (issue #615).
|
|
1914
|
+
const delta = takeAgentDelta('atomcode');
|
|
1915
|
+
if (delta) entry = applyUserDelta(entry, delta, 'args');
|
|
1916
|
+
config.mcpServers['huaweicloud-devkit'] = entry;
|
|
1882
1917
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
1883
1918
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
1884
1919
|
console.log(` MCP config updated: ${configPath}`);
|
|
@@ -1951,6 +1986,8 @@ function uninstallAtomCode() {
|
|
|
1951
1986
|
config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
1952
1987
|
} catch {}
|
|
1953
1988
|
if (config.mcpServers?.['huaweicloud-devkit']) {
|
|
1989
|
+
const delta = extractUserDelta(config.mcpServers['huaweicloud-devkit'], 'args');
|
|
1990
|
+
if (delta) saveAgentDelta('atomcode', delta);
|
|
1954
1991
|
delete config.mcpServers['huaweicloud-devkit'];
|
|
1955
1992
|
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
|
1956
1993
|
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
@@ -2478,7 +2515,6 @@ function ensureHermesMcpConfig() {
|
|
|
2478
2515
|
if (hcloudBin) {
|
|
2479
2516
|
blockLines.push(` HCLOUD_BIN: "${hcloudBin.replace(/\\/g, '/')}"`);
|
|
2480
2517
|
}
|
|
2481
|
-
const block = blockLines.join('\n');
|
|
2482
2518
|
|
|
2483
2519
|
let existing = '';
|
|
2484
2520
|
if (existsSync(configPath)) {
|
|
@@ -2486,10 +2522,15 @@ function ensureHermesMcpConfig() {
|
|
|
2486
2522
|
existing = readFileSync(configPath, 'utf8');
|
|
2487
2523
|
} catch {}
|
|
2488
2524
|
if (existing.includes('mcp_servers:') && existing.includes('huaweicloud-devkit')) {
|
|
2489
|
-
|
|
2525
|
+
// Preserve user's extra args (e.g. --hdkitservice-endpoint) when rewriting the block (issue #615).
|
|
2526
|
+
const argsMatch = existing.match(/^\s*huaweicloud-devkit\s*:[\s\S]*?^\s*args:\s*\[[^\]]*\]\s*$/m);
|
|
2527
|
+
const extrasMatch = argsMatch ? argsMatch[0].match(/args:\s*\[".*?"(.*)\]/) : null;
|
|
2528
|
+
const extras = extrasMatch && extrasMatch[1].trim() ? extrasMatch[1].replace(/,\s*$/, '') : '';
|
|
2529
|
+
if (existing.includes(`args: ["${mcpPath}"${extras}]`)) {
|
|
2490
2530
|
console.log(` MCP config unchanged: ${configPath}`);
|
|
2491
2531
|
return false;
|
|
2492
2532
|
}
|
|
2533
|
+
if (extras) blockLines[3] = ` args: ["${mcpPath}"${extras}]`;
|
|
2493
2534
|
removeHermesMcpConfigBlock();
|
|
2494
2535
|
existing = '';
|
|
2495
2536
|
if (existsSync(configPath)) {
|
|
@@ -2500,7 +2541,14 @@ function ensureHermesMcpConfig() {
|
|
|
2500
2541
|
}
|
|
2501
2542
|
}
|
|
2502
2543
|
mkdirSync(dirname(configPath), { recursive: true });
|
|
2503
|
-
|
|
2544
|
+
// Restore user args saved by a previous uninstall (issue #615).
|
|
2545
|
+
const hermesDelta = takeAgentDelta('hermes');
|
|
2546
|
+
if (hermesDelta && Array.isArray(hermesDelta.argsExtra) && hermesDelta.argsExtra.length > 0) {
|
|
2547
|
+
const extras = hermesDelta.argsExtra.map((a) => `"${String(a)}"`).join(', ');
|
|
2548
|
+
blockLines[3] = ` args: ["${mcpPath}", ${extras}]`;
|
|
2549
|
+
}
|
|
2550
|
+
const newBlock = blockLines.join('\n');
|
|
2551
|
+
const newContent = existing ? `${existing.trimEnd()}\n\n${newBlock}\n` : `${newBlock}\n`;
|
|
2504
2552
|
writeFileSync(configPath, newContent);
|
|
2505
2553
|
console.log(` MCP config updated: ${configPath}`);
|
|
2506
2554
|
return true;
|
|
@@ -2917,7 +2965,25 @@ function uninstallHermes() {
|
|
|
2917
2965
|
}
|
|
2918
2966
|
}
|
|
2919
2967
|
|
|
2920
|
-
// 3. Remove MCP config from config.yaml
|
|
2968
|
+
// 3. Remove MCP config from config.yaml (back up user args first, issue #615)
|
|
2969
|
+
const hermesConfig = hermesConfigFile();
|
|
2970
|
+
if (existsSync(hermesConfig)) {
|
|
2971
|
+
try {
|
|
2972
|
+
const text = readFileSync(hermesConfig, 'utf8');
|
|
2973
|
+
const argsLine = text.match(/^\s*args:\s*\[([^\]]*)\]\s*$/m);
|
|
2974
|
+
if (argsLine) {
|
|
2975
|
+
let parsed;
|
|
2976
|
+
try {
|
|
2977
|
+
parsed = JSON.parse(`[${argsLine[1].replace(/'/g, '"')}]`);
|
|
2978
|
+
} catch {
|
|
2979
|
+
parsed = null;
|
|
2980
|
+
}
|
|
2981
|
+
if (Array.isArray(parsed) && parsed.length > 1) {
|
|
2982
|
+
saveAgentDelta('hermes', { argsExtra: parsed.slice(1) });
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
} catch {}
|
|
2986
|
+
}
|
|
2921
2987
|
removeHermesMcpConfigBlock();
|
|
2922
2988
|
console.log(' MCP config removed');
|
|
2923
2989
|
|
|
@@ -3583,6 +3649,10 @@ async function cmdUninstall() {
|
|
|
3583
3649
|
if (removeIfExists(vaultPath)) {
|
|
3584
3650
|
console.log(' Removed credential vault');
|
|
3585
3651
|
}
|
|
3652
|
+
// Full uninstall also discards any saved MCP config backups (issue #615).
|
|
3653
|
+
if (purgeBackup()) {
|
|
3654
|
+
console.log(' Removed MCP config backup');
|
|
3655
|
+
}
|
|
3586
3656
|
const vaultDir = dirname(vaultPath);
|
|
3587
3657
|
try {
|
|
3588
3658
|
if (existsSync(vaultDir) && readdirSync(vaultDir).length === 0) {
|