coding-tool-x 3.6.8 → 3.6.9

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.
@@ -5,14 +5,14 @@
5
5
  <link rel="icon" href="/favicon.ico">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
7
  <title>coding-tool-x · AI 编程工作台</title>
8
- <script type="module" crossorigin src="/assets/index-CpnFNWqC.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-y-eEayxR.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/markdown-BNJfEgCA.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vue-vendor-B5RqjEjQ.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/naive-ui-kcWDeJwl.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/icons-QFXzEgqM.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/vendors-DQA4Hw9v.js">
14
14
  <link rel="stylesheet" crossorigin href="/assets/markdown-BfC0goYb.css">
15
- <link rel="stylesheet" crossorigin href="/assets/index-DJ9VFidd.css">
15
+ <link rel="stylesheet" crossorigin href="/assets/index-Dsh4Q5Cg.css">
16
16
  </head>
17
17
  <body>
18
18
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coding-tool-x",
3
- "version": "3.6.8",
3
+ "version": "3.6.9",
4
4
  "description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -42,7 +42,8 @@ function extractRepoPayload(source = {}) {
42
42
  projectPath: repo.projectPath || source.projectPath || '',
43
43
  localPath: repo.localPath || source.localPath || '',
44
44
  repoUrl: repo.repoUrl || repo.url || source.repoUrl || source.url || '',
45
- token: repo.token || source.token || ''
45
+ token: repo.token || source.token || '',
46
+ marketplace: repo.marketplace || source.marketplace || ''
46
47
  };
47
48
  }
48
49
 
@@ -140,12 +141,13 @@ router.post('/install', async (req, res) => {
140
141
  try {
141
142
  const { platform, service } = getPluginsService(req);
142
143
  const { directory, repo, gitUrl, source } = req.body;
144
+ const hasDirectoryField = Object.prototype.hasOwnProperty.call(req.body, 'directory');
143
145
 
144
146
  // Support both new format (directory + repo) and legacy format (gitUrl)
145
147
  let installUrl;
146
148
  if (source) {
147
149
  installUrl = source;
148
- } else if (directory && repo) {
150
+ } else if (repo && hasDirectoryField) {
149
151
  installUrl = '';
150
152
  } else if (gitUrl) {
151
153
  installUrl = gitUrl;
@@ -158,10 +160,10 @@ router.post('/install', async (req, res) => {
158
160
 
159
161
  const result = await service.installPlugin(
160
162
  installUrl,
161
- directory && repo
163
+ repo && hasDirectoryField
162
164
  ? {
163
165
  ...extractRepoPayload({ repo }),
164
- directory
166
+ directory: directory || ''
165
167
  }
166
168
  : null
167
169
  );
@@ -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,21 @@ function splitPluginMarketplaceKey(key = '') {
171
177
  };
172
178
  }
173
179
 
180
+ function hasRepoInstallInfo(repoInfo = null) {
181
+ return Boolean(
182
+ repoInfo &&
183
+ typeof repoInfo === 'object' &&
184
+ (
185
+ Object.prototype.hasOwnProperty.call(repoInfo, 'directory') ||
186
+ repoInfo.localPath ||
187
+ repoInfo.owner ||
188
+ repoInfo.projectPath ||
189
+ repoInfo.repoUrl ||
190
+ repoInfo.url
191
+ )
192
+ );
193
+ }
194
+
174
195
  function isWindowsAbsolutePath(input = '') {
175
196
  return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
176
197
  }
@@ -772,6 +793,219 @@ class PluginsService {
772
793
  fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
773
794
  }
774
795
 
796
+ _readClaudeSettings() {
797
+ const filePath = NATIVE_PATHS.claude.settings;
798
+ if (!fs.existsSync(filePath)) return { filePath, settings: {} };
799
+ try {
800
+ const raw = fs.readFileSync(filePath, 'utf8');
801
+ return { filePath, settings: raw.trim() ? JSON.parse(raw) : {} };
802
+ } catch (err) {
803
+ console.error('[PluginsService] Failed to read Claude settings:', err.message);
804
+ return { filePath, settings: {} };
805
+ }
806
+ }
807
+
808
+ _writeClaudeSettings(settings) {
809
+ const filePath = NATIVE_PATHS.claude.settings;
810
+ this._ensureDir(path.dirname(filePath));
811
+ fs.writeFileSync(filePath, JSON.stringify(settings || {}, null, 2), 'utf8');
812
+ }
813
+
814
+ _setClaudePluginEnabled(name, marketplace, enabled) {
815
+ const { settings } = this._readClaudeSettings();
816
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
817
+ const pluginKey = marketplace ? `${name}@${marketplace}` : name;
818
+ nextSettings.enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
819
+ nextSettings.enabledPlugins[pluginKey] = !!enabled;
820
+ this._writeClaudeSettings(nextSettings);
821
+ }
822
+
823
+ _removeClaudePluginEnabled(name, marketplace = '') {
824
+ const { settings } = this._readClaudeSettings();
825
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
826
+ const enabledPlugins = { ...(nextSettings.enabledPlugins || {}) };
827
+ for (const key of Object.keys(enabledPlugins)) {
828
+ const parsed = splitPluginMarketplaceKey(key);
829
+ if (parsed.name === name && (!marketplace || parsed.marketplace === marketplace)) {
830
+ delete enabledPlugins[key];
831
+ }
832
+ }
833
+ nextSettings.enabledPlugins = enabledPlugins;
834
+ this._writeClaudeSettings(nextSettings);
835
+ }
836
+
837
+ _isClaudePluginEnabled(name, marketplace = '') {
838
+ const { settings } = this._readClaudeSettings();
839
+ const enabledPlugins = settings.enabledPlugins || {};
840
+ const pluginKey = marketplace ? `${name}@${marketplace}` : name;
841
+ if (Object.prototype.hasOwnProperty.call(enabledPlugins, pluginKey)) {
842
+ return enabledPlugins[pluginKey] !== false;
843
+ }
844
+ for (const [key, value] of Object.entries(enabledPlugins)) {
845
+ const parsed = splitPluginMarketplaceKey(key);
846
+ if (parsed.name === name) {
847
+ return value !== false;
848
+ }
849
+ }
850
+ return true;
851
+ }
852
+
853
+ _resolveClaudeMarketplaceName(normalizedRepo, explicit = '') {
854
+ if (explicit) return normalizeClaudeMarketplaceSegment(explicit);
855
+ if (normalizedRepo.marketplace) return normalizeClaudeMarketplaceSegment(normalizedRepo.marketplace);
856
+ if (normalizedRepo.provider === 'local') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.localPath, 'local');
857
+ if (normalizedRepo.provider === 'gitlab') return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.projectPath, 'gitlab');
858
+ return normalizeClaudeMarketplaceSegment(normalizedRepo.name || normalizedRepo.owner || 'ctx');
859
+ }
860
+
861
+ _resolveClaudePluginCachePath(marketplace, pluginName, version) {
862
+ return resolveInsideRoot(
863
+ CLAUDE_PLUGINS_CACHE_DIR,
864
+ path.posix.join(
865
+ normalizeClaudeMarketplaceSegment(marketplace),
866
+ normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin'),
867
+ normalizePluginCacheSegment(String(version || 'local'), 'Claude plugin version', 'local')
868
+ ),
869
+ 'Claude plugin cache path'
870
+ );
871
+ }
872
+
873
+ _resolveClaudeMarketplacePath(marketplace) {
874
+ return resolveInsideRoot(
875
+ CLAUDE_MARKETPLACES_DIR,
876
+ normalizeClaudeMarketplaceSegment(marketplace),
877
+ 'Claude marketplace path'
878
+ );
879
+ }
880
+
881
+ _registerClaudeMarketplace(marketplace, sourceDir, marketplaceInfo = {}) {
882
+ const safeMarketplace = normalizeClaudeMarketplaceSegment(marketplace);
883
+ const marketplaceDir = this._resolveClaudeMarketplacePath(safeMarketplace);
884
+ this._ensureDir(path.join(marketplaceDir, '.claude-plugin'));
885
+
886
+ const plugins = Array.isArray(marketplaceInfo.plugins) ? marketplaceInfo.plugins : [];
887
+ const manifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
888
+ const marketplaceManifest = {
889
+ name: safeMarketplace,
890
+ owner: { name: 'CTX' },
891
+ plugins,
892
+ ...(marketplaceInfo.description ? { description: marketplaceInfo.description } : {})
893
+ };
894
+ fs.writeFileSync(manifestPath, JSON.stringify(marketplaceManifest, null, 2), 'utf8');
895
+
896
+ this._ensureDir(CLAUDE_PLUGINS_DIR);
897
+ let known = {};
898
+ if (fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
899
+ try {
900
+ known = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
901
+ } catch {
902
+ known = {};
903
+ }
904
+ }
905
+ known[safeMarketplace] = {
906
+ source: {
907
+ source: 'directory',
908
+ path: marketplaceDir
909
+ },
910
+ installLocation: marketplaceDir,
911
+ lastUpdated: new Date().toISOString()
912
+ };
913
+ fs.writeFileSync(CLAUDE_MARKETPLACES_FILE, JSON.stringify(known, null, 2), 'utf8');
914
+
915
+ const { settings } = this._readClaudeSettings();
916
+ const nextSettings = (settings && typeof settings === 'object') ? { ...settings } : {};
917
+ nextSettings.extraKnownMarketplaces = { ...(nextSettings.extraKnownMarketplaces || {}) };
918
+ nextSettings.extraKnownMarketplaces[safeMarketplace] = {
919
+ source: {
920
+ source: 'directory',
921
+ path: marketplaceDir
922
+ }
923
+ };
924
+ this._writeClaudeSettings(nextSettings);
925
+ return marketplaceDir;
926
+ }
927
+
928
+ _upsertClaudeMarketplacePlugin(marketplace, pluginDir, plugin) {
929
+ const marketplaceDir = this._resolveClaudeMarketplacePath(marketplace);
930
+ const marketplaceManifestPath = path.join(marketplaceDir, '.claude-plugin', 'marketplace.json');
931
+ let marketplaceManifest = {
932
+ name: normalizeClaudeMarketplaceSegment(marketplace),
933
+ owner: { name: 'CTX' },
934
+ plugins: []
935
+ };
936
+ if (fs.existsSync(marketplaceManifestPath)) {
937
+ try {
938
+ marketplaceManifest = JSON.parse(fs.readFileSync(marketplaceManifestPath, 'utf8'));
939
+ } catch {
940
+ // recreate malformed marketplace manifest below
941
+ }
942
+ }
943
+ if (!Array.isArray(marketplaceManifest.plugins)) {
944
+ marketplaceManifest.plugins = [];
945
+ }
946
+
947
+ const pluginName = String(plugin.name || '').trim();
948
+ const existingIndex = marketplaceManifest.plugins.findIndex(item => item?.name === pluginName);
949
+ const nextEntry = {
950
+ name: pluginName,
951
+ source: `./plugins/${pluginName}`,
952
+ description: plugin.description || ''
953
+ };
954
+ if (plugin.category) {
955
+ nextEntry.category = plugin.category;
956
+ }
957
+ if (existingIndex >= 0) {
958
+ marketplaceManifest.plugins[existingIndex] = {
959
+ ...marketplaceManifest.plugins[existingIndex],
960
+ ...nextEntry
961
+ };
962
+ } else {
963
+ marketplaceManifest.plugins.push(nextEntry);
964
+ }
965
+
966
+ const marketplacePluginDir = resolveInsideRoot(
967
+ marketplaceDir,
968
+ path.posix.join('plugins', normalizePluginCacheSegment(pluginName, 'Claude plugin name', 'plugin')),
969
+ 'Claude marketplace plugin path'
970
+ );
971
+ if (fs.existsSync(marketplacePluginDir)) {
972
+ fs.rmSync(marketplacePluginDir, { recursive: true, force: true });
973
+ }
974
+ this.copyDirRecursive(pluginDir, marketplacePluginDir, {
975
+ excludeNames: [REPO_SOURCE_META_FILE]
976
+ });
977
+ this._registerClaudeMarketplace(marketplace, marketplaceDir, marketplaceManifest);
978
+ }
979
+
980
+ _registerClaudeInstalledPlugin(name, marketplace, installPath, installData = {}) {
981
+ this._ensureDir(CLAUDE_PLUGINS_DIR);
982
+ let nativeData = { version: 2, plugins: {} };
983
+ if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
984
+ try {
985
+ nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
986
+ } catch {
987
+ nativeData = { version: 2, plugins: {} };
988
+ }
989
+ }
990
+ nativeData.version = nativeData.version || 2;
991
+ if (!nativeData.plugins || typeof nativeData.plugins !== 'object') {
992
+ nativeData.plugins = {};
993
+ }
994
+ const nativeKey = marketplace ? `${name}@${marketplace}` : name;
995
+ const installTimestamp = installData.installedAt || new Date().toISOString();
996
+ nativeData.plugins[nativeKey] = [{
997
+ scope: installData.scope || 'user',
998
+ installPath,
999
+ version: installData.version || '1.0.0',
1000
+ installedAt: installTimestamp,
1001
+ lastUpdated: installData.lastUpdated || installTimestamp,
1002
+ ...(installData.source ? { source: installData.source } : {}),
1003
+ ...(installData.repoSourceMeta || {})
1004
+ }];
1005
+ fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
1006
+ this._setClaudePluginEnabled(name, marketplace, true);
1007
+ }
1008
+
775
1009
  _isCodexPluginEnabled(name, marketplace = '') {
776
1010
  const { config } = this._readCodexConfig();
777
1011
  const plugins = config.plugins || {};
@@ -888,7 +1122,7 @@ class PluginsService {
888
1122
  for (const [key, installations] of Object.entries(data.plugins)) {
889
1123
  if (installations && installations.length > 0) {
890
1124
  const install = installations[0]; // Get first installation
891
- const [name, marketplace] = key.split('@');
1125
+ const { name, marketplace } = splitPluginMarketplaceKey(key);
892
1126
 
893
1127
  // Read plugin.json from installPath for description
894
1128
  let description = '';
@@ -941,7 +1175,8 @@ class PluginsService {
941
1175
 
942
1176
  // Read enabled state from CTX registry (defaults to true if not set)
943
1177
  const legacyInfo = getPlugin(name);
944
- const enabledState = legacyInfo ? legacyInfo.enabled !== false : true;
1178
+ const enabledState = this._isClaudePluginEnabled(name, marketplace) &&
1179
+ (legacyInfo ? legacyInfo.enabled !== false : true);
945
1180
 
946
1181
  plugins.push({
947
1182
  name,
@@ -1052,7 +1287,7 @@ class PluginsService {
1052
1287
  */
1053
1288
  async installPlugin(source, repoInfo = null) {
1054
1289
  if (this._isCodex()) {
1055
- if (repoInfo && repoInfo.directory) {
1290
+ if (hasRepoInstallInfo(repoInfo)) {
1056
1291
  return this._installFromRepoDirectory(repoInfo);
1057
1292
  }
1058
1293
 
@@ -1080,7 +1315,7 @@ class PluginsService {
1080
1315
  }
1081
1316
 
1082
1317
  if (this._isOpenCode()) {
1083
- if (repoInfo && repoInfo.directory) {
1318
+ if (hasRepoInstallInfo(repoInfo)) {
1084
1319
  return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
1085
1320
  }
1086
1321
 
@@ -1089,6 +1324,11 @@ class PluginsService {
1089
1324
  return this._installFromRepoDirectory(parsedSource, { installRoot: this._getOpenCodePluginsDir() });
1090
1325
  }
1091
1326
 
1327
+ const parsedRepo = this._repoFromGitUrl(source, 'main');
1328
+ if (parsedRepo) {
1329
+ return this._installFromRepoDirectory({ ...parsedRepo, directory: '' }, { installRoot: this._getOpenCodePluginsDir() });
1330
+ }
1331
+
1092
1332
  // OpenCode 原生支持 npm 包名,通过 opencode.json 的 plugin 数组管理
1093
1333
  if (!/^https?:\/\//.test(source)) {
1094
1334
  const plugins = this._listOpenCodeConfiguredPlugins();
@@ -1108,7 +1348,7 @@ class PluginsService {
1108
1348
  };
1109
1349
  }
1110
1350
 
1111
- if (repoInfo && repoInfo.directory) {
1351
+ if (hasRepoInstallInfo(repoInfo)) {
1112
1352
  return await this._installFromRepoDirectory(repoInfo);
1113
1353
  }
1114
1354
 
@@ -1117,6 +1357,11 @@ class PluginsService {
1117
1357
  return await this._installFromRepoDirectory(parsedSource);
1118
1358
  }
1119
1359
 
1360
+ const parsedRepo = this._repoFromGitUrl(source, 'main');
1361
+ if (parsedRepo) {
1362
+ return await this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
1363
+ }
1364
+
1120
1365
  // Fallback to original git clone method
1121
1366
  return await installPluginCore(source);
1122
1367
  }
@@ -1153,6 +1398,8 @@ class PluginsService {
1153
1398
  // Create plugin directory
1154
1399
  const pluginDir = this._isCodex()
1155
1400
  ? installRoot
1401
+ : this.platform === 'claude'
1402
+ ? installRoot
1156
1403
  : resolveInsideRoot(
1157
1404
  installRoot,
1158
1405
  normalizePluginPathName(installedPluginName, 'plugin name'),
@@ -1226,6 +1473,13 @@ class PluginsService {
1226
1473
  this._ensureDir(path.dirname(fallbackManifestPath));
1227
1474
  fs.writeFileSync(fallbackManifestPath, JSON.stringify(manifest, null, 2));
1228
1475
  }
1476
+ if (this.platform === 'claude') {
1477
+ const claudeManifestPath = path.join(pluginDir, '.claude-plugin', 'plugin.json');
1478
+ if (!fs.existsSync(claudeManifestPath)) {
1479
+ this._ensureDir(path.dirname(claudeManifestPath));
1480
+ fs.writeFileSync(claudeManifestPath, JSON.stringify(manifest, null, 2));
1481
+ }
1482
+ }
1229
1483
 
1230
1484
  if (this._isCodex()) {
1231
1485
  const marketplace = this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace);
@@ -1247,9 +1501,10 @@ class PluginsService {
1247
1501
  });
1248
1502
  this._registerCodexMarketplace(marketplace, normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo));
1249
1503
  this._setCodexPluginEnabled(installedPluginName, marketplace, true);
1250
- } else if (!this._isOpenCode()) {
1504
+ } else if (this.platform === 'claude') {
1251
1505
  const installTimestamp = new Date().toISOString();
1252
1506
  const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
1507
+ const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
1253
1508
  const repoSourceMeta = {
1254
1509
  repoId: normalizedRepo.id,
1255
1510
  repoProvider: normalizedRepo.provider,
@@ -1261,6 +1516,7 @@ class PluginsService {
1261
1516
  repoProjectPath: normalizedRepo.projectPath || '',
1262
1517
  repoLocalPath: normalizedRepo.localPath || '',
1263
1518
  repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
1519
+ repoMarketplace: marketplace,
1264
1520
  source: sourceUrl
1265
1521
  };
1266
1522
 
@@ -1278,30 +1534,54 @@ class PluginsService {
1278
1534
  }
1279
1535
 
1280
1536
  this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
1537
+ this._upsertClaudeMarketplacePlugin(marketplace, pluginDir, {
1538
+ name: installedPluginName,
1539
+ description: manifest.description || '',
1540
+ category: manifest.category || ''
1541
+ });
1281
1542
 
1282
1543
  // Also register in Claude's native installed_plugins.json
1283
1544
  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] = [{
1545
+ this._registerClaudeInstalledPlugin(installedPluginName, marketplace, pluginDir, {
1294
1546
  version: manifest.version || '1.0.0',
1295
- installPath: pluginDir,
1296
1547
  installedAt: installTimestamp,
1297
1548
  scope: 'user',
1298
1549
  source: sourceUrl,
1299
- ...repoSourceMeta
1300
- }];
1301
- fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
1550
+ repoSourceMeta
1551
+ });
1302
1552
  } catch (e) {
1303
1553
  console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
1304
1554
  }
1555
+ } else if (!this._isOpenCode()) {
1556
+ const installTimestamp = new Date().toISOString();
1557
+ const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
1558
+ const repoSourceMeta = {
1559
+ repoId: normalizedRepo.id,
1560
+ repoProvider: normalizedRepo.provider,
1561
+ repoOwner: normalizedRepo.owner || '',
1562
+ repoName: normalizedRepo.name || '',
1563
+ repoBranch: normalizedRepo.branch,
1564
+ repoDirectory: directory,
1565
+ repoHost: normalizedRepo.host || '',
1566
+ repoProjectPath: normalizedRepo.projectPath || '',
1567
+ repoLocalPath: normalizedRepo.localPath || '',
1568
+ repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
1569
+ source: sourceUrl
1570
+ };
1571
+
1572
+ const { addPlugin } = require('../../plugins/registry');
1573
+ try {
1574
+ addPlugin(installedPluginName, {
1575
+ version: manifest.version || '1.0.0',
1576
+ enabled: true,
1577
+ installedAt: installTimestamp,
1578
+ source: sourceUrl
1579
+ });
1580
+ } catch (e) {
1581
+ console.warn('[PluginsService] Legacy registry addPlugin warning:', e.message);
1582
+ }
1583
+
1584
+ this.writeRepoSourceMeta(pluginDir, repoSourceMeta);
1305
1585
  }
1306
1586
 
1307
1587
  return {
@@ -1332,6 +1612,12 @@ class PluginsService {
1332
1612
 
1333
1613
  _resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options = {}) {
1334
1614
  if (!this._isCodex()) {
1615
+ if (this.platform === 'claude') {
1616
+ const marketplace = this._resolveClaudeMarketplaceName(normalizedRepo, repoInfo.marketplace);
1617
+ const pluginName = manifest.name || normalizeRepoPath(repoInfo.directory || '').split('/').pop() || 'plugin';
1618
+ const version = manifest.version || repoInfo.version || 'local';
1619
+ return this._resolveClaudePluginCachePath(marketplace, pluginName, version);
1620
+ }
1335
1621
  return options.installRoot || INSTALLED_DIR;
1336
1622
  }
1337
1623
 
@@ -1501,7 +1787,7 @@ class PluginsService {
1501
1787
  const keysToDelete = [];
1502
1788
  const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
1503
1789
  for (const [key, installations] of Object.entries(data.plugins)) {
1504
- const [pluginName] = key.split('@');
1790
+ const { name: pluginName } = splitPluginMarketplaceKey(key);
1505
1791
  if (pluginName === safeName || key === safeName || pluginName === baseName) {
1506
1792
  keysToDelete.push(key);
1507
1793
  // Delete install directories
@@ -1525,6 +1811,8 @@ class PluginsService {
1525
1811
  }
1526
1812
  if (keysToDelete.length > 0) {
1527
1813
  for (const key of keysToDelete) {
1814
+ const parsed = splitPluginMarketplaceKey(key);
1815
+ this._removeClaudePluginEnabled(parsed.name || safeName, parsed.marketplace || '');
1528
1816
  delete data.plugins[key];
1529
1817
  }
1530
1818
  fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(data, null, 2), 'utf8');
@@ -1607,7 +1895,7 @@ class PluginsService {
1607
1895
  const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1608
1896
  if (data.plugins) {
1609
1897
  for (const key of Object.keys(data.plugins)) {
1610
- const [pluginName] = key.split('@');
1898
+ const { name: pluginName } = splitPluginMarketplaceKey(key);
1611
1899
  if (pluginName === name || key === name || pluginName === baseName) {
1612
1900
  pluginExists = true;
1613
1901
  break;
@@ -1626,6 +1914,7 @@ class PluginsService {
1626
1914
  }
1627
1915
 
1628
1916
  // Store enabled state in CTX registry (upsert)
1917
+ let matchedMarketplace = '';
1629
1918
  try {
1630
1919
  const { addPlugin } = require('../../plugins/registry');
1631
1920
  if (legacyPlugin) {
@@ -1636,6 +1925,21 @@ class PluginsService {
1636
1925
  } catch (e) {
1637
1926
  console.warn('[PluginsService] Failed to update plugin registry:', e.message);
1638
1927
  }
1928
+ if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
1929
+ try {
1930
+ const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
1931
+ for (const key of Object.keys(data.plugins || {})) {
1932
+ const parsed = splitPluginMarketplaceKey(key);
1933
+ if (parsed.name === name || key === name || parsed.name === baseName) {
1934
+ matchedMarketplace = parsed.marketplace || '';
1935
+ break;
1936
+ }
1937
+ }
1938
+ } catch {
1939
+ // ignore
1940
+ }
1941
+ }
1942
+ this._setClaudePluginEnabled(baseName || name, matchedMarketplace, enabled);
1639
1943
 
1640
1944
  return {
1641
1945
  name,
@@ -2260,16 +2564,17 @@ class PluginsService {
2260
2564
  }
2261
2565
  }
2262
2566
 
2263
- copyDirRecursive(sourceDir, destDir) {
2567
+ copyDirRecursive(sourceDir, destDir, options = {}) {
2264
2568
  const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
2265
2569
  for (const entry of entries) {
2570
+ if (options.excludeNames?.includes(entry.name)) continue;
2266
2571
  const sourcePath = path.join(sourceDir, entry.name);
2267
2572
  const destPath = path.join(destDir, entry.name);
2268
2573
  if (entry.isDirectory()) {
2269
2574
  if (!fs.existsSync(destPath)) {
2270
2575
  fs.mkdirSync(destPath, { recursive: true });
2271
2576
  }
2272
- this.copyDirRecursive(sourcePath, destPath);
2577
+ this.copyDirRecursive(sourcePath, destPath, options);
2273
2578
  } else {
2274
2579
  fs.copyFileSync(sourcePath, destPath);
2275
2580
  }
@@ -2466,6 +2771,9 @@ class PluginsService {
2466
2771
 
2467
2772
  buildMarketPluginItem(repo, data = {}) {
2468
2773
  const itemRepo = data.repo || repo;
2774
+ const directory = Object.prototype.hasOwnProperty.call(data, 'directory')
2775
+ ? data.directory
2776
+ : data.name;
2469
2777
  return {
2470
2778
  name: data.name,
2471
2779
  displayName: data.displayName || '',
@@ -2483,10 +2791,10 @@ class PluginsService {
2483
2791
  repoProjectPath: itemRepo.projectPath || '',
2484
2792
  repoLocalPath: itemRepo.localPath || '',
2485
2793
  repoId: itemRepo.id,
2486
- directory: normalizeRepoPath(data.directory || data.name || ''),
2794
+ directory: normalizeRepoPath(directory || ''),
2487
2795
  installSource: data.installSource || '',
2488
2796
  marketplaceFormat: data.marketplaceFormat || '',
2489
- readmeUrl: this.buildRepoBrowserUrl(itemRepo, data.directory || data.name || ''),
2797
+ readmeUrl: this.buildRepoBrowserUrl(itemRepo, directory || ''),
2490
2798
  lspServers: data.lspServers || null,
2491
2799
  commands: data.commands || [],
2492
2800
  hooks: data.hooks || [],
@@ -2599,6 +2907,33 @@ class PluginsService {
2599
2907
  _repoFromGitUrl(url = '', branch = 'main') {
2600
2908
  const value = String(url || '').trim();
2601
2909
  if (!value) return null;
2910
+
2911
+ const sshMatch = value.match(/^git@([^:]+):(.+?)(?:\.git)?$/i);
2912
+ if (sshMatch) {
2913
+ const host = `https://${sshMatch[1]}`;
2914
+ const repoPath = sshMatch[2].replace(/\.git$/i, '').replace(/^\/+|\/+$/g, '');
2915
+ const parts = repoPath.split('/').filter(Boolean);
2916
+ if (sshMatch[1].includes('github') && parts.length >= 2) {
2917
+ return this.normalizeRepoConfig({
2918
+ provider: 'github',
2919
+ host,
2920
+ owner: parts[0],
2921
+ name: parts[1],
2922
+ branch,
2923
+ repoUrl: value
2924
+ });
2925
+ }
2926
+ if (parts.length > 0) {
2927
+ return this.normalizeRepoConfig({
2928
+ provider: 'gitlab',
2929
+ host,
2930
+ projectPath: repoPath,
2931
+ branch,
2932
+ repoUrl: value
2933
+ });
2934
+ }
2935
+ }
2936
+
2602
2937
  let parsed;
2603
2938
  try {
2604
2939
  parsed = new URL(value);
@@ -2783,16 +3118,24 @@ class PluginsService {
2783
3118
  }
2784
3119
 
2785
3120
  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');
3121
+ if (this._isCodex()) {
3122
+ return file.path === '.codex-plugin/plugin.json' || file.path.endsWith('/.codex-plugin/plugin.json');
3123
+ }
3124
+ if (this.platform === 'claude') {
3125
+ return file.path === '.claude-plugin/plugin.json' || file.path.endsWith('/.claude-plugin/plugin.json');
3126
+ }
2788
3127
  return false;
2789
3128
  });
2790
3129
  const nestedDirs = new Set();
2791
3130
  for (const file of nestedManifestFiles) {
2792
3131
  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);
3132
+ const rootMarker = marker.slice(1);
3133
+ const dir = file.path === rootMarker
3134
+ ? ''
3135
+ : normalizeRepoPath(file.path.slice(0, -marker.length));
3136
+ const dirKey = dir || '.';
3137
+ if (nestedDirs.has(dirKey)) continue;
3138
+ nestedDirs.add(dirKey);
2796
3139
  const manifest = await readJson(file.path);
2797
3140
  marketPlugins.push(this.buildMarketPluginItem(repo, {
2798
3141
  name: manifest.name || dir.split('/').pop(),