coding-tool-x 3.6.8 → 3.6.10

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.
@@ -29,6 +29,8 @@ const {
29
29
  const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
30
30
  const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
31
31
  const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
32
+ const CLAUDE_PLUGINS_CACHE_DIR = path.join(CLAUDE_PLUGINS_DIR, 'cache');
33
+ const CLAUDE_MARKETPLACES_DIR = path.join(CLAUDE_PLUGINS_DIR, 'marketplaces');
32
34
  const CODEX_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'plugins');
33
35
  const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
34
36
  const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
@@ -135,6 +137,10 @@ function normalizePluginCacheSegment(value = '', label = 'plugin segment', fallb
135
137
  });
136
138
  }
137
139
 
140
+ function normalizeClaudeMarketplaceSegment(value = '', fallback = 'ctx') {
141
+ return normalizePluginCacheSegment(value, 'Claude marketplace', fallback);
142
+ }
143
+
138
144
  function resolvePluginConfigFile(configDir, name) {
139
145
  const safeName = normalizePluginPathName(name, 'plugin config name');
140
146
  return resolveInsideRoot(configDir, `${safeName}.json`, 'Plugin config path');
@@ -171,6 +177,28 @@ function splitPluginMarketplaceKey(key = '') {
171
177
  };
172
178
  }
173
179
 
180
+ function normalizeMarketplaceSkillPaths(skills = []) {
181
+ if (!Array.isArray(skills)) return [];
182
+ return skills
183
+ .map(skill => normalizeRepoPath(String(skill || '').replace(/^\.\//, '')))
184
+ .filter(Boolean);
185
+ }
186
+
187
+ function hasRepoInstallInfo(repoInfo = null) {
188
+ return Boolean(
189
+ repoInfo &&
190
+ typeof repoInfo === 'object' &&
191
+ (
192
+ Object.prototype.hasOwnProperty.call(repoInfo, 'directory') ||
193
+ repoInfo.localPath ||
194
+ repoInfo.owner ||
195
+ repoInfo.projectPath ||
196
+ repoInfo.repoUrl ||
197
+ repoInfo.url
198
+ )
199
+ );
200
+ }
201
+
174
202
  function isWindowsAbsolutePath(input = '') {
175
203
  return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
176
204
  }
@@ -772,6 +800,219 @@ class PluginsService {
772
800
  fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
773
801
  }
774
802
 
803
+ _readClaudeSettings() {
804
+ const filePath = NATIVE_PATHS.claude.settings;
805
+ if (!fs.existsSync(filePath)) return { filePath, settings: {} };
806
+ try {
807
+ const raw = fs.readFileSync(filePath, 'utf8');
808
+ return { filePath, settings: raw.trim() ? JSON.parse(raw) : {} };
809
+ } catch (err) {
810
+ console.error('[PluginsService] Failed to read Claude settings:', err.message);
811
+ return { filePath, settings: {} };
812
+ }
813
+ }
814
+
815
+ _writeClaudeSettings(settings) {
816
+ const filePath = NATIVE_PATHS.claude.settings;
817
+ this._ensureDir(path.dirname(filePath));
818
+ fs.writeFileSync(filePath, JSON.stringify(settings || {}, null, 2), 'utf8');
819
+ }
820
+
821
+ _setClaudePluginEnabled(name, marketplace, enabled) {
822
+ const { settings } = this._readClaudeSettings();
823
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
824
+ const pluginKey = marketplace ? `${name}@${marketplace}` : name;
825
+ nextSettings.enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
826
+ nextSettings.enabledPlugins[pluginKey] = !!enabled;
827
+ this._writeClaudeSettings(nextSettings);
828
+ }
829
+
830
+ _removeClaudePluginEnabled(name, marketplace = '') {
831
+ const { settings } = this._readClaudeSettings();
832
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
833
+ const enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
834
+ for (const key of Object.keys(enabledPlugins)) {
835
+ const parsed = splitPluginMarketplaceKey(key);
836
+ if (parsed.name === name && (!marketplace || parsed.marketplace === marketplace)) {
837
+ delete enabledPlugins[key];
838
+ }
839
+ }
840
+ nextSettings.enabledPlugins = enabledPlugins;
841
+ this._writeClaudeSettings(nextSettings);
842
+ }
843
+
844
+ _isClaudePluginEnabled(name, marketplace = '') {
845
+ const { settings } = this._readClaudeSettings();
846
+ const enabledPlugins = settings.enabledPlugins || {};
847
+ const pluginKey = marketplace ? `${name}@${marketplace}` : name;
848
+ if (Object.prototype.hasOwnProperty.call(enabledPlugins, pluginKey)) {
849
+ return enabledPlugins[pluginKey] !== false;
850
+ }
851
+ for (const [key, value] of Object.entries(enabledPlugins)) {
852
+ const parsed = splitPluginMarketplaceKey(key);
853
+ if (parsed.name === name) {
854
+ return value !== false;
855
+ }
856
+ }
857
+ return true;
858
+ }
859
+
860
+ _resolveClaudeMarketplaceName(normalizedRepo, explicit = '') {
861
+ if (explicit) return normalizeClaudeMarketplaceSegment(explicit);
862
+ if (normalizedRepo.marketplace) return normalizeClaudeMarketplaceSegment(normalizedRepo.marketplace);
863
+ if (normalizedRepo.provider === 'local') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.localPath, 'local');
864
+ if (normalizedRepo.provider === 'gitlab') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.projectPath, 'gitlab');
865
+ return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.owner || 'ctx');
866
+ }
867
+
868
+ _resolveClaudePluginCachePath(marketplace, pluginName, version) {
869
+ return resolveInsideRoot(
870
+ CLAUDE_PLUGINS_CACHE_DIR,
871
+ path.posix.join(
872
+ normalizeClaudeMarketplaceSegment(marketplace),
873
+ normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin'),
874
+ normalizePluginCacheSegment(String(version || 'local'), 'Claude plugin version', 'local')
875
+ ),
876
+ 'Claude plugin cache path'
877
+ );
878
+ }
879
+
880
+ _resolveClaudeMarketplacePath(marketplace) {
881
+ return resolveInsideRoot(
882
+ CLAUDE_MARKETPLACES_DIR,
883
+ normalizeClaudeMarketplaceSegment(marketplace),
884
+ 'Claude marketplace path'
885
+ );
886
+ }
887
+
888
+ _registerClaudeMarketplace(marketplace, sourceDir, marketplaceInfo = {}) {
889
+ const safeMarketplace = normalizeClaudeMarketplaceSegment(marketplace);
890
+ const marketplaceDir = this._resolveClaudeMarketplacePath(safeMarketplace);
891
+ this._ensureDir(path.join(marketplaceDir, '.claude-plugin'));
892
+
893
+ const plugins = Array.isArray(marketplaceInfo.plugins) ? marketplaceInfo.plugins : [];
894
+ const manifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
895
+ const marketplaceManifest = {
896
+ name: safeMarketplace,
897
+ owner: { name: 'CTX' },
898
+ plugins,
899
+ ...(marketplaceInfo.description ? { description: marketplaceInfo.description } : {})
900
+ };
901
+ fs.writeFileSync(manifestPath, JSON.stringify(marketplaceManifest, null, 2), 'utf8');
902
+
903
+ this._ensureDir(CLAUDE_PLUGINS_DIR);
904
+ let known = {};
905
+ if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
906
+ try {
907
+ known = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
908
+ } catch {
909
+ known = {};
910
+ }
911
+ }
912
+ known[safeMarketplace] = {
913
+ source: {
914
+ source: 'directory',
915
+ path: marketplaceDir
916
+ },
917
+ installLocation: marketplaceDir,
918
+ lastUpdated: new Date().toISOString()
919
+ };
920
+ fs.writeFileSync(CLAUDE_MARKETPLACES_FILE, JSON.stringify(known, null, 2), 'utf8');
921
+
922
+ const { settings } = this._readClaudeSettings();
923
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
924
+ nextSettings.extraKnownMarketplaces = { ...(nextSettings.extraKnownMarketplaces || {}) };
925
+ nextSettings.extraKnownMarketplaces[safeMarketplace] = {
926
+ source: {
927
+ source: 'directory',
928
+ path: marketplaceDir
929
+ }
930
+ };
931
+ this._writeClaudeSettings(nextSettings);
932
+ return marketplaceDir;
933
+ }
934
+
935
+ _upsertClaudeMarketplacePlugin(marketplace, pluginDir, plugin) {
936
+ const marketplaceDir = this._resolveClaudeMarketplacePath(marketplace);
937
+ const marketplaceManifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
938
+ let marketplaceManifest = {
939
+ name: normalizeClaudeMarketplaceSegment(marketplace),
940
+ owner: { name: 'CTX' },
941
+ plugins: []
942
+ };
943
+ if (fs.existsSync(marketplaceManifestPath)) {
944
+ try {
945
+ marketplaceManifest = JSON.parse(fs.readFileSync(marketplaceManifestPath, 'utf8'));
946
+ } catch {
947
+ // recreate malformed marketplace manifest below
948
+ }
949
+ }
950
+ if (!Array.isArray(marketplaceManifest.plugins)) {
951
+ marketplaceManifest.plugins = [];
952
+ }
953
+
954
+ const pluginName = String(plugin.name || '').trim();
955
+ const existingIndex = marketplaceManifest.plugins.findIndex(item => item?.name === pluginName);
956
+ const nextEntry = {
957
+ name: pluginName,
958
+ source: `./plugins/${pluginName}`,
959
+ description: plugin.description || ''
960
+ };
961
+ if (plugin.category) {
962
+ nextEntry.category = plugin.category;
963
+ }
964
+ if (existingIndex >= 0) {
965
+ marketplaceManifest.plugins[existingIndex] = {
966
+ ...marketplaceManifest.plugins[existingIndex],
967
+ ...nextEntry
968
+ };
969
+ } else {
970
+ marketplaceManifest.plugins.push(nextEntry);
971
+ }
972
+
973
+ const marketplacePluginDir = resolveInsideRoot(
974
+ marketplaceDir,
975
+ path.posix.join('plugins', normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin')),
976
+ 'Claude marketplace plugin path'
977
+ );
978
+ if (fs.existsSync(marketplacePluginDir)) {
979
+ fs.rmSync(marketplacePluginDir, { recursive: true, force: true });
980
+ }
981
+ this.copyDirRecursive(pluginDir, marketplacePluginDir, {
982
+ excludeNames: [REPO_SOURCE_META_FILE]
983
+ });
984
+ this._registerClaudeMarketplace(marketplace, marketplaceDir, marketplaceManifest);
985
+ }
986
+
987
+ _registerClaudeInstalledPlugin(name, marketplace, installPath, installData = {}) {
988
+ this._ensureDir(CLAUDE_PLUGINS_DIR);
989
+ let nativeData = { version: 2, plugins: {} };
990
+ if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
991
+ try {
992
+ nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
993
+ } catch {
994
+ nativeData = { version: 2, plugins: {} };
995
+ }
996
+ }
997
+ nativeData.version = nativeData.version || 2;
998
+ if (!nativeData.plugins || typeof nativeData.plugins !== 'object') {
999
+ nativeData.plugins = {};
1000
+ }
1001
+ const nativeKey = marketplace ? `${name}@${marketplace}` : name;
1002
+ const installTimestamp = installData.installedAt || new Date().toISOString();
1003
+ nativeData.plugins[nativeKey] = [{
1004
+ scope: installData.scope || 'user',
1005
+ installPath,
1006
+ version: installData.version || '1.0.0',
1007
+ installedAt: installTimestamp,
1008
+ lastUpdated: installData.lastUpdated || installTimestamp,
1009
+ ...(installData.source ? { source: installData.source } : {}),
1010
+ ...(installData.repoSourceMeta || {})
1011
+ }];
1012
+ fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
1013
+ this._setClaudePluginEnabled(name, marketplace, true);
1014
+ }
1015
+
775
1016
  _isCodexPluginEnabled(name, marketplace = '') {
776
1017
  const { config } = this._readCodexConfig();
777
1018
  const plugins = config.plugins || {};
@@ -888,7 +1129,7 @@ class PluginsService {
888
1129
  for (const [key, installations] of Object.entries(data.plugins)) {
889
1130
  if (installations && installations.length > 0) {
890
1131
  const install = installations[0]; // Get first installation
891
- const [name, marketplace] = key.split('@');
1132
+ const { name, marketplace } = splitPluginMarketplaceKey(key);
892
1133
 
893
1134
  // Read plugin.json from installPath for description
894
1135
  let description = '';
@@ -941,7 +1182,8 @@ class PluginsService {
941
1182
 
942
1183
  // Read enabled state from CTX registry (defaults to true if not set)
943
1184
  const legacyInfo = getPlugin(name);
944
- const enabledState = legacyInfo ? legacyInfo.enabled !== false : true;
1185
+ const enabledState = this._isClaudePluginEnabled(name, marketplace) &&
1186
+ (legacyInfo ? legacyInfo.enabled !== false : true);
945
1187
 
946
1188
  plugins.push({
947
1189
  name,
@@ -1052,7 +1294,7 @@ class PluginsService {
1052
1294
  */
1053
1295
  async installPlugin(source, repoInfo = null) {
1054
1296
  if (this._isCodex()) {
1055
- if (repoInfo && repoInfo.directory) {
1297
+ if (hasRepoInstallInfo(repoInfo)) {
1056
1298
  return this._installFromRepoDirectory(repoInfo);
1057
1299
  }
1058
1300
 
@@ -1080,7 +1322,7 @@ class PluginsService {
1080
1322
  }
1081
1323
 
1082
1324
  if (this._isOpenCode()) {
1083
- if (repoInfo && repoInfo.directory) {
1325
+ if (hasRepoInstallInfo(repoInfo)) {
1084
1326
  return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
1085
1327
  }
1086
1328
 
@@ -1089,6 +1331,11 @@ class PluginsService {
1089
1331
  return this._installFromRepoDirectory(parsedSource, { installRoot: this._getOpenCodePluginsDir() });
1090
1332
  }
1091
1333
 
1334
+ const parsedRepo = this._repoFromGitUrl(source, 'main');
1335
+ if (parsedRepo) {
1336
+ return this._installFromRepoDirectory({ ...parsedRepo, directory: '' }, { installRoot: this._getOpenCodePluginsDir() });
1337
+ }
1338
+
1092
1339
  // OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
1093
1340
  if (!/^https?:\/\//.test(source)) {
1094
1341
  const plugins = this._listOpenCodeConfiguredPlugins();
@@ -1108,7 +1355,7 @@ class PluginsService {
1108
1355
  };
1109
1356
  }
1110
1357
 
1111
- if (repoInfo && repoInfo.directory) {
1358
+ if (hasRepoInstallInfo(repoInfo)) {
1112
1359
  return await this._installFromRepoDirectory(repoInfo);
1113
1360
  }
1114
1361
 
@@ -1117,6 +1364,11 @@ class PluginsService {
1117
1364
  return await this._installFromRepoDirectory(parsedSource);
1118
1365
  }
1119
1366
 
1367
+ const parsedRepo = this._repoFromGitUrl(source, 'main');
1368
+ if (parsedRepo) {
1369
+ return await this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
1370
+ }
1371
+
1120
1372
  // Fallback to original git clone method
1121
1373
  return await installPluginCore(source);
1122
1374
  }
@@ -1153,6 +1405,8 @@ class PluginsService {
1153
1405
  // Create plugin directory
1154
1406
  const pluginDir = this._isCodex()
1155
1407
  ? installRoot
1408
+ : this.platform === 'claude'
1409
+ ? installRoot
1156
1410
  : resolveInsideRoot(
1157
1411
  installRoot,
1158
1412
  normalizePluginPathName(installedPluginName, 'plugin name'),
@@ -1226,6 +1480,13 @@ class PluginsService {
1226
1480
  this._ensureDir(path.dirname(fallbackManifestPath));
1227
1481
  fs.writeFileSync(fallbackManifestPath, JSON.stringify(manifest, null, 2));
1228
1482
  }
1483
+ if (this.platform === 'claude') {
1484
+ const claudeManifestPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
1485
+ if (!fs.existsSync(claudeManifestPath)) {
1486
+ this._ensureDir(path.dirname(claudeManifestPath));
1487
+ fs.writeFileSync(claudeManifestPath, JSON.stringify(manifest, null, 2));
1488
+ }
1489
+ }
1229
1490
 
1230
1491
  if (this._isCodex()) {
1231
1492
  const marketplace = this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace);
@@ -1247,9 +1508,10 @@ class PluginsService {
1247
1508
  });
1248
1509
  this._registerCodexMarketplace(marketplace, normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo));
1249
1510
  this._setCodexPluginEnabled(installedPluginName, marketplace, true);
1250
- } else if (!this._isOpenCode()) {
1511
+ } else if (this.platform === 'claude') {
1251
1512
  const installTimestamp = new Date().toISOString();
1252
1513
  const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
1514
+ const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
1253
1515
  const repoSourceMeta = {
1254
1516
  repoId: normalizedRepo.id,
1255
1517
  repoProvider: normalizedRepo.provider,
@@ -1261,6 +1523,7 @@ class PluginsService {
1261
1523
  repoProjectPath: normalizedRepo.projectPath || '',
1262
1524
  repoLocalPath: normalizedRepo.localPath || '',
1263
1525
  repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
1526
+ repoMarketplace: marketplace,
1264
1527
  source: sourceUrl
1265
1528
  };
1266
1529
 
@@ -1278,30 +1541,54 @@ class PluginsService {
1278
1541
  }
1279
1542
 
1280
1543
  this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
1544
+ this._upsertClaudeMarketplacePlugin(marketplace, pluginDir, {
1545
+ name: installedPluginName,
1546
+ description: manifest.description || '',
1547
+ category: manifest.category || ''
1548
+ });
1281
1549
 
1282
1550
  // Also register in Claude's native installed_plugins.json
1283
1551
  try {
1284
- this._ensureDir(CLAUDE_PLUGINS_DIR);
1285
- let nativeData = { plugins: {} };
1286
- if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
1287
- try {
1288
- nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1289
- if (!nativeData.plugins) nativeData.plugins = {};
1290
- } catch (e) { /* ignore parse error */ }
1291
- }
1292
- const nativeKey = `${installedPluginName}@ctx`;
1293
- nativeData.plugins[nativeKey] = [{
1552
+ this._registerClaudeInstalledPlugin(installedPluginName, marketplace, pluginDir, {
1294
1553
  version: manifest.version || '1.0.0',
1295
- installPath: pluginDir,
1296
1554
  installedAt: installTimestamp,
1297
1555
  scope: 'user',
1298
1556
  source: sourceUrl,
1299
- ...repoSourceMeta
1300
- }];
1301
- fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
1557
+ repoSourceMeta
1558
+ });
1302
1559
  } catch (e) {
1303
1560
  console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
1304
1561
  }
1562
+ } else if (!this._isOpenCode()) {
1563
+ const installTimestamp = new Date().toISOString();
1564
+ const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
1565
+ const repoSourceMeta = {
1566
+ repoId: normalizedRepo.id,
1567
+ repoProvider: normalizedRepo.provider,
1568
+ repoOwner: normalizedRepo.owner || '',
1569
+ repoName: normalizedRepo.name || '',
1570
+ repoBranch: normalizedRepo.branch,
1571
+ repoDirectory: directory,
1572
+ repoHost: normalizedRepo.host || '',
1573
+ repoProjectPath: normalizedRepo.projectPath || '',
1574
+ repoLocalPath: normalizedRepo.localPath || '',
1575
+ repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
1576
+ source: sourceUrl
1577
+ };
1578
+
1579
+ const { addPlugin } = require('../../plugins/registry');
1580
+ try {
1581
+ addPlugin(installedPluginName, {
1582
+ version: manifest.version || '1.0.0',
1583
+ enabled: true,
1584
+ installedAt: installTimestamp,
1585
+ source: sourceUrl
1586
+ });
1587
+ } catch (e) {
1588
+ console.warn('[PluginsService] Legacy registry addPlugin warning:', e.message);
1589
+ }
1590
+
1591
+ this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
1305
1592
  }
1306
1593
 
1307
1594
  return {
@@ -1332,6 +1619,12 @@ class PluginsService {
1332
1619
 
1333
1620
  _resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options = {}) {
1334
1621
  if (!this._isCodex()) {
1622
+ if (this.platform === 'claude') {
1623
+ const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
1624
+ const pluginName = manifest.name || normalizeRepoPath(repoInfo.directory || '').split('/').pop() || 'plugin';
1625
+ const version = manifest.version || repoInfo.version || 'local';
1626
+ return this._resolveClaudePluginCachePath(marketplace, pluginName, version);
1627
+ }
1335
1628
  return options.installRoot || INSTALLED_DIR;
1336
1629
  }
1337
1630
 
@@ -1501,7 +1794,7 @@ class PluginsService {
1501
1794
  const keysToDelete = [];
1502
1795
  const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
1503
1796
  for (const [key, installations] of Object.entries(data.plugins)) {
1504
- const [pluginName] = key.split('@');
1797
+ const { name: pluginName } = splitPluginMarketplaceKey(key);
1505
1798
  if (pluginName === safeName || key === safeName || pluginName === baseName) {
1506
1799
  keysToDelete.push(key);
1507
1800
  // Delete install directories
@@ -1525,6 +1818,8 @@ class PluginsService {
1525
1818
  }
1526
1819
  if (keysToDelete.length > 0) {
1527
1820
  for (const key of keysToDelete) {
1821
+ const parsed = splitPluginMarketplaceKey(key);
1822
+ this._removeClaudePluginEnabled(parsed.name || safeName, parsed.marketplace || '');
1528
1823
  delete data.plugins[key];
1529
1824
  }
1530
1825
  fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
@@ -1607,7 +1902,7 @@ class PluginsService {
1607
1902
  const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1608
1903
  if (data.plugins) {
1609
1904
  for (const key of Object.keys(data.plugins)) {
1610
- const [pluginName] = key.split('@');
1905
+ const { name: pluginName } = splitPluginMarketplaceKey(key);
1611
1906
  if (pluginName === name || key === name || pluginName === baseName) {
1612
1907
  pluginExists = true;
1613
1908
  break;
@@ -1626,6 +1921,7 @@ class PluginsService {
1626
1921
  }
1627
1922
 
1628
1923
  // Store enabled state in CTX registry (upsert)
1924
+ let matchedMarketplace = '';
1629
1925
  try {
1630
1926
  const { addPlugin } = require('../../plugins/registry');
1631
1927
  if (legacyPlugin) {
@@ -1636,6 +1932,21 @@ class PluginsService {
1636
1932
  } catch (e) {
1637
1933
  console.warn('[PluginsService] Failed to update plugin registry:', e.message);
1638
1934
  }
1935
+ if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
1936
+ try {
1937
+ const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1938
+ for (const key of Object.keys(data.plugins || {})) {
1939
+ const parsed = splitPluginMarketplaceKey(key);
1940
+ if (parsed.name === name || key === name || parsed.name === baseName) {
1941
+ matchedMarketplace = parsed.marketplace || '';
1942
+ break;
1943
+ }
1944
+ }
1945
+ } catch {
1946
+ // ignore
1947
+ }
1948
+ }
1949
+ this._setClaudePluginEnabled(baseName || name, matchedMarketplace, enabled);
1639
1950
 
1640
1951
  return {
1641
1952
  name,
@@ -2260,16 +2571,17 @@ class PluginsService {
2260
2571
  }
2261
2572
  }
2262
2573
 
2263
- copyDirRecursive(sourceDir, destDir) {
2574
+ copyDirRecursive(sourceDir, destDir, options = {}) {
2264
2575
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
2265
2576
  for (const entry of entries) {
2577
+ if (options.excludeNames?.includes(entry.name)) continue;
2266
2578
  const sourcePath = path.join(sourceDir, entry.name);
2267
2579
  const destPath = path.join(destDir, entry.name);
2268
2580
  if (entry.isDirectory()) {
2269
2581
  if (!fs.existsSync(destPath)) {
2270
2582
  fs.mkdirSync(destPath, { recursive: true });
2271
2583
  }
2272
- this.copyDirRecursive(sourcePath, destPath);
2584
+ this.copyDirRecursive(sourcePath, destPath, options);
2273
2585
  } else {
2274
2586
  fs.copyFileSync(sourcePath, destPath);
2275
2587
  }
@@ -2466,6 +2778,9 @@ class PluginsService {
2466
2778
 
2467
2779
  buildMarketPluginItem(repo, data = {}) {
2468
2780
  const itemRepo = data.repo || repo;
2781
+ const directory = Object.prototype.hasOwnProperty.call(data, 'directory')
2782
+ ? data.directory
2783
+ : data.name;
2469
2784
  return {
2470
2785
  name: data.name,
2471
2786
  displayName: data.displayName || '',
@@ -2483,13 +2798,17 @@ class PluginsService {
2483
2798
  repoProjectPath: itemRepo.projectPath || '',
2484
2799
  repoLocalPath: itemRepo.localPath || '',
2485
2800
  repoId: itemRepo.id,
2486
- directory: normalizeRepoPath(data.directory || data.name || ''),
2801
+ directory: normalizeRepoPath(directory || ''),
2487
2802
  installSource: data.installSource || '',
2488
2803
  marketplaceFormat: data.marketplaceFormat || '',
2489
- readmeUrl: this.buildRepoBrowserUrl(itemRepo, data.directory || data.name || ''),
2804
+ readmeUrl: this.buildRepoBrowserUrl(itemRepo, directory || ''),
2490
2805
  lspServers: data.lspServers || null,
2491
2806
  commands: data.commands || [],
2492
2807
  hooks: data.hooks || [],
2808
+ containsSkills: Boolean(data.containsSkills),
2809
+ skillPaths: Array.isArray(data.skillPaths) ? data.skillPaths : [],
2810
+ pluginKind: data.pluginKind || (data.containsSkills ? 'skill-bundle' : 'plugin'),
2811
+ strict: data.strict,
2493
2812
  isInstalled: false
2494
2813
  };
2495
2814
  }
@@ -2599,6 +2918,33 @@ class PluginsService {
2599
2918
  _repoFromGitUrl(url = '', branch = 'main') {
2600
2919
  const value = String(url || '').trim();
2601
2920
  if (!value) return null;
2921
+
2922
+ const sshMatch = value.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
2923
+ if (sshMatch) {
2924
+ const host = `https://${sshMatch[1]}`;
2925
+ const repoPath = sshMatch[2].replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
2926
+ const parts = repoPath.split('/').filter(Boolean);
2927
+ if (sshMatch[1].includes('github') && parts.length >= 2) {
2928
+ return this.normalizeRepoConfig({
2929
+ provider: 'github',
2930
+ host,
2931
+ owner: parts[0],
2932
+ name: parts[1],
2933
+ branch,
2934
+ repoUrl: value
2935
+ });
2936
+ }
2937
+ if (parts.length > 0) {
2938
+ return this.normalizeRepoConfig({
2939
+ provider: 'gitlab',
2940
+ host,
2941
+ projectPath: repoPath,
2942
+ branch,
2943
+ repoUrl: value
2944
+ });
2945
+ }
2946
+ }
2947
+
2602
2948
  let parsed;
2603
2949
  try {
2604
2950
  parsed = new URL(value);
@@ -2647,6 +2993,7 @@ class PluginsService {
2647
2993
  const results = [];
2648
2994
  for (const plugin of marketplace.plugins) {
2649
2995
  const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
2996
+ const skillPaths = normalizeMarketplaceSkillPaths(plugin.skills);
2650
2997
  const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
2651
2998
  const manifest = canReadManifest
2652
2999
  ? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
@@ -2668,7 +3015,11 @@ class PluginsService {
2668
3015
  marketplaceFormat: 'claude-marketplace',
2669
3016
  lspServers: plugin.lspServers || null,
2670
3017
  commands: manifest?.commands || [],
2671
- hooks: manifest?.hooks || []
3018
+ hooks: manifest?.hooks || [],
3019
+ containsSkills: skillPaths.length > 0,
3020
+ skillPaths,
3021
+ pluginKind: skillPaths.length > 0 ? 'skill-bundle' : 'plugin',
3022
+ strict: Object.prototype.hasOwnProperty.call(plugin, 'strict') ? plugin.strict : undefined
2672
3023
  }));
2673
3024
  }
2674
3025
  return results;
@@ -2783,16 +3134,24 @@ class PluginsService {
2783
3134
  }
2784
3135
 
2785
3136
  const nestedManifestFiles = files.filter(file => {
2786
- if (this._isCodex()) return file.path.endsWith('/.codex-plugin/plugin.json');
2787
- if (this.platform === 'claude') return file.path.endsWith('/.claude-plugin/plugin.json');
3137
+ if (this._isCodex()) {
3138
+ return file.path === '.codex-plugin/plugin.json' || file.path.endsWith('/.codex-plugin/plugin.json');
3139
+ }
3140
+ if (this.platform === 'claude') {
3141
+ return file.path === '.claude-plugin/plugin.json' || file.path.endsWith('/.claude-plugin/plugin.json');
3142
+ }
2788
3143
  return false;
2789
3144
  });
2790
3145
  const nestedDirs = new Set();
2791
3146
  for (const file of nestedManifestFiles) {
2792
3147
  const marker = this._isCodex() ? '/.codex-plugin/plugin.json' : '/.claude-plugin/plugin.json';
2793
- const dir = normalizeRepoPath(file.path.slice(0, -marker.length));
2794
- if (!dir || nestedDirs.has(dir)) continue;
2795
- nestedDirs.add(dir);
3148
+ const rootMarker = marker.slice(1);
3149
+ const dir = file.path === rootMarker
3150
+ ? ''
3151
+ : normalizeRepoPath(file.path.slice(0, -marker.length));
3152
+ const dirKey = dir || '.';
3153
+ if (nestedDirs.has(dirKey)) continue;
3154
+ nestedDirs.add(dirKey);
2796
3155
  const manifest = await readJson(file.path);
2797
3156
  marketPlugins.push(this.buildMarketPluginItem(repo, {
2798
3157
  name: manifest.name || dir.split('/').pop(),
@@ -2041,7 +2041,8 @@ ${content}
2041
2041
  async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '') {
2042
2042
  const safeDirectory = this.normalizeSkillDirectory(directory);
2043
2043
  // 先检查本地是否安装
2044
- const localPath = path.join(this.resolveInstallPath(safeDirectory).path, 'SKILL.md');
2044
+ const localSkillDir = this.resolveInstallPath(safeDirectory).path;
2045
+ const localPath = path.join(localSkillDir, 'SKILL.md');
2045
2046
 
2046
2047
  if (fs.existsSync(localPath)) {
2047
2048
  const content = fs.readFileSync(localPath, 'utf-8');
@@ -2058,6 +2059,9 @@ ${content}
2058
2059
  content: body,
2059
2060
  fullContent: content,
2060
2061
  installed: true,
2062
+ path: localSkillDir,
2063
+ fullPath: localPath,
2064
+ installPath: localSkillDir,
2061
2065
  source: this.isProtectedSkillDirectory(safeDirectory) ? 'system-installed' : 'local',
2062
2066
  protected: this.isProtectedSkillDirectory(safeDirectory)
2063
2067
  };
@@ -2086,7 +2090,8 @@ ${content}
2086
2090
  repoProjectPath: repo.projectPath || null,
2087
2091
  repoLocalPath: repo.localPath || null,
2088
2092
  repoId: repo.id,
2089
- repoUrl: repo.repoUrl || buildRepoUrl(repo)
2093
+ repoUrl: repo.repoUrl || buildRepoUrl(repo),
2094
+ path: fullDirectory
2090
2095
  };
2091
2096
  };
2092
2097
 
@@ -2106,7 +2111,11 @@ ${content}
2106
2111
  : path.join(repo.localPath, 'SKILL.md');
2107
2112
  if (!fs.existsSync(skillMdPath)) continue;
2108
2113
  const content = fs.readFileSync(skillMdPath, 'utf-8');
2109
- return parseRemoteSkillContent(content, repo, candidateDir);
2114
+ return {
2115
+ ...parseRemoteSkillContent(content, repo, candidateDir),
2116
+ path: path.dirname(skillMdPath),
2117
+ fullPath: skillMdPath
2118
+ };
2110
2119
  }
2111
2120
  return null;
2112
2121
  }