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.
- package/dist/web/assets/{Analytics-C57ZRDq5.js → Analytics-vN7CJLtB.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-B3qqqL05.js → ConfigTemplates-D8w_9p6Z.js} +1 -1
- package/dist/web/assets/Home-BdyDRjGR.js +1 -0
- package/dist/web/assets/Home-BspgEJBb.css +1 -0
- package/dist/web/assets/{PluginManager-BhX0LUzr.js → PluginManager-vFp_miHj.js} +1 -1
- package/dist/web/assets/{ProjectList-BQ01OZn9.js → ProjectList-DDfNp6CO.js} +1 -1
- package/dist/web/assets/{SessionList-DPv2IOXG.js → SessionList-Cit8Ylm_.js} +1 -1
- package/dist/web/assets/{SkillManager-D5CfQm2O.js → SkillManager-Cp0EigHc.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce8ggteK.js → WorkspaceManager-DPTYHnp4.js} +1 -1
- package/dist/web/assets/index-CfBxeWy0.js +2 -0
- package/dist/web/assets/index-Dl2Bz258.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/server/api/plugins.js +6 -4
- package/src/server/services/channel-balance.js +147 -0
- package/src/server/services/plugins-service.js +391 -32
- package/src/server/services/skill-service.js +12 -3
- package/dist/web/assets/Home-CG5XrTNc.js +0 -1
- package/dist/web/assets/Home-CIua-b-o.css +0 -1
- package/dist/web/assets/index-CpnFNWqC.js +0 -2
- package/dist/web/assets/index-DJ9VFidd.css +0 -1
|
@@ -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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
2801
|
+
directory: normalizeRepoPath(directory || ''),
|
|
2487
2802
|
installSource: data.installSource || '',
|
|
2488
2803
|
marketplaceFormat: data.marketplaceFormat || '',
|
|
2489
|
-
readmeUrl: this.buildRepoBrowserUrl(itemRepo,
|
|
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())
|
|
2787
|
-
|
|
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
|
|
2794
|
-
|
|
2795
|
-
|
|
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
|
|
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
|
|
2114
|
+
return {
|
|
2115
|
+
...parseRemoteSkillContent(content, repo, candidateDir),
|
|
2116
|
+
path: path.dirname(skillMdPath),
|
|
2117
|
+
fullPath: skillMdPath
|
|
2118
|
+
};
|
|
2110
2119
|
}
|
|
2111
2120
|
return null;
|
|
2112
2121
|
}
|