huaweicloud-devkit 1.1.5 → 1.1.6-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.5-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.6-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **[中文](README.zh-CN.md) | English**
10
10
 
package/README.zh-CN.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)
5
5
  [![CI](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml/badge.svg)](https://github.com/huaweicloud/huaweicloud-devkit/actions/workflows/ci.yml)
6
6
  [![npm version](https://img.shields.io/npm/v/huaweicloud-devkit)](https://www.npmjs.com/package/huaweicloud-devkit)
7
- [![Beta](https://img.shields.io/badge/beta-v1.1.5-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
7
+ [![Beta](https://img.shields.io/badge/beta-v1.1.6-orange)](https://github.com/huaweicloud/huaweicloud-devkit)
8
8
 
9
9
  **中文 | [English](README.md)**
10
10
 
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.5",
4
+ "version": "1.1.6-next.0",
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",
@@ -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.5",
20
+ "version": "1.1.6-next.0",
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.5",
23
+ "version": "1.1.6-next.0",
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.5",
20
+ "version": "1.1.6-next.0",
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.5",
3
+ "version": "1.1.6-next.0",
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.5",
3
+ "version": "1.1.6-next.0",
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.5",
5
+ "version": "1.1.6-next.0",
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.",
@@ -1,25 +1,9 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve, dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
-
5
1
  import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
6
- import { peekCachedUpdateInfo, applyUpdateHint } from './update-check.mjs';
2
+ import { peekCachedUpdateInfo, applyUpdateHint, readInstalledVersion } from './update-check.mjs';
7
3
  import { initTelemetry } from './telemetry/telemetry.mjs';
8
4
  import { detectAgent } from './telemetry/agent-detect.mjs';
9
5
 
10
- const __dirname = dirname(fileURLToPath(import.meta.url));
11
- const pluginRoot = resolve(__dirname, '..');
12
- const packageRoot = resolve(pluginRoot, '..', '..');
13
- let pkgVersion = '0.0.0';
14
- for (const base of [pluginRoot, packageRoot]) {
15
- try {
16
- const version = JSON.parse(readFileSync(join(base, 'package.json'), 'utf8')).version;
17
- if (version) {
18
- pkgVersion = version;
19
- break;
20
- }
21
- } catch {}
22
- }
6
+ const pkgVersion = readInstalledVersion() || '0.0.0';
23
7
 
24
8
  // 会话内首个非 check/upgrade 工具调用附加 _updateInfo,只消费一次。
25
9
  // 按会话隔离:同进程内不同会话(A/B)各自首次提示;stdio 用固定 'stdin'。
@@ -0,0 +1,51 @@
1
+ // Shared OfficeAce config-root resolution used by both the installer
2
+ // (setup-cli.mjs) and the MCP runtime (tools.mjs), so skills are installed
3
+ // to and read from the same location (#559).
4
+ //
5
+ // OfficeAce exposes no API for the plugin to query the current user or
6
+ // install directory, so the root is located by probing. The probe result is
7
+ // persisted in a marker file so a non-standard directory (entered via prompt,
8
+ // whose OFFICE_CLAW_CONFIG_ROOT env does not survive across processes) is
9
+ // still found by later uninstall/update runs.
10
+
11
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
12
+ import { join, resolve, dirname } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+
15
+ function markerPath() {
16
+ return join(process.env.HUAWEICLOUD_HOME || homedir(), '.config', 'huaweicloud', 'devkit-officeace-root.json');
17
+ }
18
+
19
+ function sqliteSibling(dir) {
20
+ return join(resolve(dir, '..'), 'data', 'mcp-connectors.sqlite');
21
+ }
22
+
23
+ // A directory is a usable OfficeAce config root when it exists and carries
24
+ // OfficeAce evidence — capabilities.json, the connector DB, or DevKit's own
25
+ // installed artifacts. Deliberately more lenient than "capabilities.json must
26
+ // exist" so uninstall/update still find the root after it was removed.
27
+ export function isUsableOfficeaceRoot(dir) {
28
+ if (!dir || !existsSync(dir)) return false;
29
+ return (
30
+ existsSync(join(dir, 'capabilities.json')) ||
31
+ existsSync(sqliteSibling(dir)) ||
32
+ existsSync(join(dir, 'skills')) ||
33
+ existsSync(join(dir, 'huaweicloud-plugins'))
34
+ );
35
+ }
36
+
37
+ export function readOfficeaceRootMarker() {
38
+ try {
39
+ const marker = JSON.parse(readFileSync(markerPath(), 'utf8'));
40
+ return isUsableOfficeaceRoot(marker.root) ? marker.root : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ export function writeOfficeaceRootMarker(root) {
47
+ try {
48
+ mkdirSync(dirname(markerPath()), { recursive: true });
49
+ writeFileSync(markerPath(), JSON.stringify({ root, savedAt: new Date().toISOString() }, null, 2));
50
+ } catch {}
51
+ }
@@ -108,15 +108,57 @@ function evaluate(stage, inputs, options = {}) {
108
108
  };
109
109
  }
110
110
 
111
+ function isNonEmptyString(value) {
112
+ return typeof value === 'string' && value.trim().length > 0;
113
+ }
114
+
115
+ function isNonEmptyContainer(value) {
116
+ if (Array.isArray(value)) return value.length > 0;
117
+ if (value && typeof value === 'object') return Object.keys(value).length > 0;
118
+ return false;
119
+ }
120
+
121
+ // A risk check that could not inspect its input must never report "allow"
122
+ // (fail-open) — it reports deny with an `invalid` risk and a synthetic finding
123
+ // so the reason is visible through the tool response (#564).
124
+ function invalidRiskResult(stage, reason) {
125
+ return {
126
+ decision: 'deny',
127
+ risk: 'invalid',
128
+ findings: [
129
+ {
130
+ ruleId: 'invalid-input',
131
+ title: 'Invalid input for risk check',
132
+ category: 'invalid',
133
+ severity: 'deny',
134
+ message: reason,
135
+ remediation: 'Provide a valid, non-empty input before re-running the check.',
136
+ source: stage,
137
+ evidence: '',
138
+ },
139
+ ],
140
+ };
141
+ }
142
+
111
143
  export function evaluateCommandRisk(command, options = {}) {
144
+ if (!isNonEmptyString(command)) {
145
+ return invalidRiskResult('command', 'command must be a non-empty string.');
146
+ }
112
147
  return evaluate('command', { command }, options);
113
148
  }
114
149
 
115
150
  export function evaluateArtifacts(artifacts, options = {}) {
116
- return evaluate('artifact', Array.isArray(artifacts) ? artifacts : [], options);
151
+ if (!Array.isArray(artifacts) || artifacts.length === 0) {
152
+ return invalidRiskResult('artifact', 'artifacts must be a non-empty array.');
153
+ }
154
+ return evaluate('artifact', artifacts, options);
117
155
  }
118
156
 
119
157
  export function evaluateDeployPlan(plan, options = {}) {
158
+ const valid = isNonEmptyString(plan) || (plan != null && typeof plan === 'object' && isNonEmptyContainer(plan));
159
+ if (!valid) {
160
+ return invalidRiskResult('deploy_plan', 'plan must be a non-empty object, array, or string.');
161
+ }
120
162
  return evaluate('deploy_plan', { plan }, options);
121
163
  }
122
164
 
@@ -37,7 +37,8 @@ import {
37
37
  } from './proxy/proxy-config.mjs';
38
38
  import { removeKooCli, removeObsConfig } from './sandbox/uninstall-cleanup.mjs';
39
39
  import { mergeCommandStyle, mergeArgsStyle, extractUserDelta, applyUserDelta } from './mcp-config-merge.mjs';
40
- import { saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
40
+ import { readAgentDelta, saveAgentDelta, takeAgentDelta, purgeBackup } from './mcp-config-backup.mjs';
41
+ import { isUsableOfficeaceRoot, readOfficeaceRootMarker, writeOfficeaceRootMarker } from './officeace-paths.mjs';
41
42
  import { queryDistTagsFetch, determineTarget, semverCompare } from './update-check.mjs';
42
43
  import { getKooCliVersion, compareVersion, kooCliDownloadBase, KOO_CLI_BASE } from './koocli-version.mjs';
43
44
  import { findHcloudBin, hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
@@ -185,11 +186,13 @@ function readOfficeaceRegistryInstallDir() {
185
186
 
186
187
  function officeaceCapabilitiesDir() {
187
188
  const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
188
- if (configRoot && existsSync(join(configRoot, 'capabilities.json'))) return configRoot;
189
+ if (isUsableOfficeaceRoot(configRoot)) return configRoot;
190
+ const markerRoot = readOfficeaceRootMarker();
191
+ if (markerRoot) return markerRoot;
189
192
  const regDir = readOfficeaceRegistryInstallDir();
190
193
  if (regDir) {
191
194
  const dir = join(regDir, '.office-claw');
192
- if (existsSync(join(dir, 'capabilities.json'))) return dir;
195
+ if (isUsableOfficeaceRoot(dir)) return dir;
193
196
  }
194
197
  if (platform() === 'win32') {
195
198
  const bases = [process.env.ProgramFiles, 'C:\\Program Files', 'D:\\Program Files'];
@@ -197,7 +200,7 @@ function officeaceCapabilitiesDir() {
197
200
  for (const base of bases) {
198
201
  if (!base) continue;
199
202
  const dir = join(base, 'OfficeAce', '.office-claw');
200
- if (existsSync(join(dir, 'capabilities.json'))) return dir;
203
+ if (isUsableOfficeaceRoot(dir)) return dir;
201
204
  }
202
205
  }
203
206
  return null;
@@ -242,7 +245,7 @@ function ensureOfficeaceMcpInSqlite() {
242
245
  if (!existsSync(dbPath)) {
243
246
  console.log(` \x1b[31mOfficeAce database not found: ${dbPath}\x1b[0m`);
244
247
  console.log(` \x1b[33mPlease ensure OfficeAce is installed and has been launched at least once.\x1b[0m`);
245
- return false;
248
+ return 'db-missing';
246
249
  }
247
250
 
248
251
  const mcpPath = join(officeacePluginsDir(), 'src', 'mcp-server.mjs').replace(/\\/g, '/');
@@ -281,21 +284,27 @@ function ensureOfficeaceMcpInSqlite() {
281
284
  if (existing.command === 'node' && existing.args_json === nextArgsJson && existing.env_json === nextEnvJson) {
282
285
  console.log(` MCP config unchanged: ${dbPath}`);
283
286
  db.close();
284
- return true;
287
+ return 'ok';
285
288
  }
286
289
  db.prepare(
287
290
  'UPDATE mcp_connectors SET command = ?, args_json = ?, env_json = ?, updated_at = ?, status = ?, enabled = 1 WHERE id = ?',
288
291
  ).run('node', nextArgsJson, nextEnvJson, now, 'disconnected', existing.id);
289
292
  console.log(` MCP config merged (user fields preserved): ${dbPath}`);
290
293
  } else {
291
- const ownerUserId = officeaceGetOwnerUserId();
294
+ // Restore user identity + args saved by a previous uninstall (#559).
295
+ // owner_user_id has no queryable source: we copy it from an existing
296
+ // row, and fall back to the value backed up before a prior uninstall so
297
+ // an emptied table does not make registration permanently impossible.
298
+ const officeaceDelta = readAgentDelta('officeace');
299
+ const ownerUserId = officeaceGetOwnerUserId() || (officeaceDelta && officeaceDelta.ownerUserId);
292
300
  if (!ownerUserId) {
293
- console.log(` \x1b[31mCannot determine owner_user_id from database\x1b[0m`);
301
+ console.log(` \x1b[31mCannot determine owner_user_id for the huaweicloud-devkit connector.\x1b[0m`);
302
+ console.log(
303
+ ` \x1b[33mThe OfficeAce connector database has no rows to copy the user id from and no prior registration is backed up. Open OfficeAce, add/enable any connector once, then re-run the install.\x1b[0m`,
304
+ );
294
305
  db.close();
295
- return false;
306
+ return 'owner-missing';
296
307
  }
297
- // Restore user args saved by a previous uninstall (issue #615).
298
- const officeaceDelta = takeAgentDelta('officeace');
299
308
  const insertArgsJson =
300
309
  officeaceDelta && Array.isArray(officeaceDelta.argsExtra)
301
310
  ? JSON.stringify([mcpPath, ...officeaceDelta.argsExtra])
@@ -304,10 +313,12 @@ function ensureOfficeaceMcpInSqlite() {
304
313
  `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)
305
314
  VALUES (?, ?, 'custom', 'huaweicloud-devkit', 'huaweicloud-devkit', 'stdio', 60000, 'node', ?, ?, 1, 'disconnected', ?, ?, 1, 0)`,
306
315
  ).run(randomUUID(), ownerUserId, insertArgsJson, envJson, now, now);
316
+ // Consume the backup only after a successful insert so a DB failure keeps it for the next retry.
317
+ takeAgentDelta('officeace');
307
318
  console.log(` MCP config created: ${dbPath}`);
308
319
  }
309
320
  db.close();
310
- return true;
321
+ return 'ok';
311
322
  } catch (error) {
312
323
  if (db) {
313
324
  try {
@@ -315,7 +326,7 @@ function ensureOfficeaceMcpInSqlite() {
315
326
  } catch {}
316
327
  }
317
328
  console.log(` \x1b[31mFailed to write MCP config: ${error.message}\x1b[0m`);
318
- return false;
329
+ return 'error';
319
330
  }
320
331
  }
321
332
 
@@ -325,17 +336,28 @@ function removeOfficeaceMcpFromSqlite() {
325
336
  let db;
326
337
  try {
327
338
  db = openOfficeaceDb();
328
- // Back up user args before deleting the connector row (issue #615).
339
+ // Back up user identity + args before deleting the connector row:
340
+ // owner_user_id is copied from existing rows on reinstall, so an empty
341
+ // table after this delete would make registration impossible (#559).
329
342
  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
- }
343
+ const row = db
344
+ .prepare("SELECT owner_user_id, args_json FROM mcp_connectors WHERE name = 'huaweicloud-devkit'")
345
+ .get();
346
+ const delta = {};
347
+ if (row?.owner_user_id) delta.ownerUserId = row.owner_user_id;
348
+ try {
349
+ const parsed = row ? JSON.parse(row.args_json || '[]') : [];
350
+ if (Array.isArray(parsed) && parsed.length > 1) delta.argsExtra = parsed.slice(1);
351
+ } catch {}
352
+ if (Object.keys(delta).length > 0) saveAgentDelta('officeace', delta);
353
+ } catch {}
354
+ // mcp_connector_tools may not exist on minimal installs — deleting it must
355
+ // not abort deleting the connector row itself.
356
+ try {
357
+ db.prepare(
358
+ "DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
359
+ ).run();
335
360
  } catch {}
336
- db.prepare(
337
- "DELETE FROM mcp_connector_tools WHERE connector_id IN (SELECT id FROM mcp_connectors WHERE name = 'huaweicloud-devkit')",
338
- ).run();
339
361
  const result2 = db.prepare("DELETE FROM mcp_connectors WHERE name = 'huaweicloud-devkit'").run();
340
362
  if (result2.changes > 0) {
341
363
  console.log(` MCP config removed: ${dbPath}`);
@@ -2377,8 +2399,17 @@ async function installOfficeAce() {
2377
2399
  console.log(` Safety Policy -> ${join(pluginDest, 'safety')}`);
2378
2400
 
2379
2401
  installRuntimeDeps(pluginDest);
2380
- ensureOfficeaceMcpInSqlite();
2402
+ if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
2403
+ throw new Error(
2404
+ 'OfficeAce MCP connector registration failed (owner_user_id unavailable). ' +
2405
+ 'Open OfficeAce, add/enable any connector once, then re-run install.',
2406
+ );
2407
+ }
2381
2408
  registerOfficeaceSkillEntries();
2409
+ // Persist the resolved root so a later uninstall/update (new process, no
2410
+ // OFFICE_CLAW_CONFIG_ROOT env) still targets the same directory (#559).
2411
+ const resolvedRoot = officeaceCapabilitiesDir();
2412
+ if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
2382
2413
  }
2383
2414
 
2384
2415
  async function updateOfficeAce() {
@@ -2395,8 +2426,15 @@ async function updateOfficeAce() {
2395
2426
  copyDir(safetyDir, join(pluginDest, 'safety'));
2396
2427
  console.log(` Safety Policy updated -> ${join(pluginDest, 'safety')}`);
2397
2428
  installRuntimeDeps(pluginDest);
2398
- ensureOfficeaceMcpInSqlite();
2429
+ if (ensureOfficeaceMcpInSqlite() === 'owner-missing') {
2430
+ console.log(
2431
+ ` \x1b[31m[WARN]\x1b[0m OfficeAce MCP connector registration failed (owner_user_id unavailable). ` +
2432
+ `The connector may stay disconnected until you add/enable any connector once in OfficeAce and re-run update.`,
2433
+ );
2434
+ }
2399
2435
  registerOfficeaceSkillEntries();
2436
+ const resolvedRoot = officeaceCapabilitiesDir();
2437
+ if (resolvedRoot) writeOfficeaceRootMarker(resolvedRoot);
2400
2438
  mkdirSync(pluginDest, { recursive: true });
2401
2439
  writeFileSync(join(pluginDest, '.installed'), new Date().toISOString());
2402
2440
  }
@@ -63,6 +63,7 @@ import {
63
63
  upgradePackage,
64
64
  } from './update-check.mjs';
65
65
  import { hcloudProbeNextStep, probeHcloud } from './hcloud-probe.mjs';
66
+ import { isUsableOfficeaceRoot, readOfficeaceRootMarker } from './officeace-paths.mjs';
66
67
 
67
68
  const __dirname = dirname(fileURLToPath(import.meta.url));
68
69
  const SKILLS_ROOT_DEV = join(__dirname, '..', 'skills');
@@ -104,7 +105,14 @@ function readOfficeaceRegistryInstallDir() {
104
105
 
105
106
  function officeaceSkillsRoot() {
106
107
  const configRoot = process.env.OFFICE_CLAW_CONFIG_ROOT;
107
- if (configRoot && existsSync(join(configRoot, 'capabilities.json'))) return join(configRoot, 'skills');
108
+ // Same lenient probe as the installer (isUsableOfficeaceRoot) so env pointing
109
+ // at "skills present but capabilities.json absent" resolves identically at
110
+ // install and at runtime (#559 review).
111
+ if (configRoot && isUsableOfficeaceRoot(configRoot)) return join(configRoot, 'skills');
112
+ // Same persisted root used by the installer keeps install-time and
113
+ // runtime skill discovery consistent (#559).
114
+ const markerRoot = readOfficeaceRootMarker();
115
+ if (markerRoot) return join(markerRoot, 'skills');
108
116
  const regDir = readOfficeaceRegistryInstallDir();
109
117
  if (regDir) {
110
118
  const dir = join(regDir, '.office-claw', 'skills');
@@ -135,6 +135,19 @@ function selfDir() {
135
135
  return dirname(fileURLToPath(import.meta.url));
136
136
  }
137
137
 
138
+ // Plugin manifests also carry the version and are kept in sync with package.json
139
+ // by `npm run validate`. Codex marketplace installs (`.codex/plugins/cache/...`)
140
+ // contain only the plugin directory — no package.json — so fall back to these
141
+ // manifests when neither candidate package.json exists (#576).
142
+ const VERSION_MANIFEST_RELS = [
143
+ '.codex-plugin/plugin.json',
144
+ '.claude-plugin/plugin.json',
145
+ '.cursor-plugin/plugin.json',
146
+ '.workbuddy-plugin/plugin.json',
147
+ '.hermes-plugin/plugin.json',
148
+ 'openclaw.plugin.json',
149
+ ];
150
+
138
151
  export function readInstalledVersion() {
139
152
  const pluginRoot = resolve(selfDir(), '..');
140
153
  const packageRoot = resolve(pluginRoot, '..', '..');
@@ -144,6 +157,12 @@ export function readInstalledVersion() {
144
157
  if (typeof version === 'string' && version) return version;
145
158
  } catch {}
146
159
  }
160
+ for (const rel of VERSION_MANIFEST_RELS) {
161
+ try {
162
+ const version = JSON.parse(readFileSync(join(pluginRoot, rel), 'utf8')).version;
163
+ if (typeof version === 'string' && version) return version;
164
+ } catch {}
165
+ }
147
166
  return null;
148
167
  }
149
168