coding-tool-x 3.6.7 → 3.6.8
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-CKycBI0G.js → Analytics-C57ZRDq5.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-B3qqqL05.js} +1 -1
- package/dist/web/assets/Home-CG5XrTNc.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-BhX0LUzr.js} +1 -1
- package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-BQ01OZn9.js} +1 -1
- package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-DPv2IOXG.js} +1 -1
- package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-D5CfQm2O.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-Ce8ggteK.js} +1 -1
- package/dist/web/assets/index-CpnFNWqC.js +2 -0
- package/dist/web/assets/index-DJ9VFidd.css +1 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +40 -65
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +771 -63
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +187 -63
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-fWgvlZQx.js +0 -2
|
@@ -10,6 +10,8 @@ const http = require('http');
|
|
|
10
10
|
const https = require('https');
|
|
11
11
|
const path = require('path');
|
|
12
12
|
const { execFileSync } = require('child_process');
|
|
13
|
+
const toml = require('toml');
|
|
14
|
+
const tomlStringify = require('@iarna/toml').stringify;
|
|
13
15
|
const AdmZip = require('adm-zip');
|
|
14
16
|
const { listPlugins, getPlugin, updatePlugin: updatePluginRegistry } = require('../../plugins/registry');
|
|
15
17
|
const { installPlugin: installPluginCore, uninstallPlugin: uninstallPluginCore } = require('../../plugins/plugin-installer');
|
|
@@ -17,10 +19,18 @@ const { initializePlugins, shutdownPlugins } = require('../../plugins/plugin-man
|
|
|
17
19
|
const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
18
20
|
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
19
21
|
const { maskToken } = require('./oauth-utils');
|
|
22
|
+
const {
|
|
23
|
+
assertInsideAllowedRoots,
|
|
24
|
+
normalizeSafeFileStem,
|
|
25
|
+
normalizeSafeRelativePath,
|
|
26
|
+
resolveInsideRoot
|
|
27
|
+
} = require('./config-artifact-paths');
|
|
20
28
|
|
|
21
29
|
const CLAUDE_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
|
|
22
30
|
const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
|
|
23
31
|
const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
|
|
32
|
+
const CODEX_PLUGINS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'plugins');
|
|
33
|
+
const CODEX_PLUGINS_CACHE_DIR = path.join(CODEX_PLUGINS_DIR, 'cache');
|
|
24
34
|
const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
|
|
25
35
|
const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
|
|
26
36
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
@@ -28,8 +38,62 @@ const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
|
28
38
|
const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
|
|
29
39
|
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
30
40
|
claude: [],
|
|
41
|
+
codex: [],
|
|
42
|
+
gemini: [],
|
|
31
43
|
opencode: []
|
|
32
44
|
};
|
|
45
|
+
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
46
|
+
const PLATFORM_CAPABILITIES = {
|
|
47
|
+
claude: {
|
|
48
|
+
platform: 'claude',
|
|
49
|
+
supportsPlugins: true,
|
|
50
|
+
repositories: true,
|
|
51
|
+
market: true,
|
|
52
|
+
install: true,
|
|
53
|
+
uninstall: true,
|
|
54
|
+
toggle: true,
|
|
55
|
+
config: true,
|
|
56
|
+
import: true,
|
|
57
|
+
syncRepos: true
|
|
58
|
+
},
|
|
59
|
+
codex: {
|
|
60
|
+
platform: 'codex',
|
|
61
|
+
supportsPlugins: true,
|
|
62
|
+
repositories: true,
|
|
63
|
+
market: true,
|
|
64
|
+
install: true,
|
|
65
|
+
uninstall: true,
|
|
66
|
+
toggle: true,
|
|
67
|
+
config: false,
|
|
68
|
+
import: false,
|
|
69
|
+
syncRepos: false
|
|
70
|
+
},
|
|
71
|
+
gemini: {
|
|
72
|
+
platform: 'gemini',
|
|
73
|
+
supportsPlugins: false,
|
|
74
|
+
repositories: false,
|
|
75
|
+
market: false,
|
|
76
|
+
install: false,
|
|
77
|
+
uninstall: false,
|
|
78
|
+
toggle: false,
|
|
79
|
+
config: false,
|
|
80
|
+
import: false,
|
|
81
|
+
syncRepos: false,
|
|
82
|
+
disabledReason: 'Gemini plugin management is not implemented yet'
|
|
83
|
+
},
|
|
84
|
+
opencode: {
|
|
85
|
+
platform: 'opencode',
|
|
86
|
+
supportsPlugins: true,
|
|
87
|
+
repositories: true,
|
|
88
|
+
market: true,
|
|
89
|
+
install: true,
|
|
90
|
+
uninstall: true,
|
|
91
|
+
toggle: true,
|
|
92
|
+
config: true,
|
|
93
|
+
import: false,
|
|
94
|
+
syncRepos: false
|
|
95
|
+
}
|
|
96
|
+
};
|
|
33
97
|
|
|
34
98
|
function cloneRepos(repos = []) {
|
|
35
99
|
return repos.map(repo => ({ ...repo }));
|
|
@@ -50,10 +114,63 @@ function normalizeRepoDirectory(directory = '') {
|
|
|
50
114
|
return normalizeRepoPath(directory);
|
|
51
115
|
}
|
|
52
116
|
|
|
117
|
+
function normalizePluginRepoDirectory(directory = '', label = 'plugin directory') {
|
|
118
|
+
return normalizeSafeRelativePath(directory || '', label, {
|
|
119
|
+
allowEmpty: true,
|
|
120
|
+
allowHiddenSegments: true
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizePluginPathName(name = '', label = 'plugin name') {
|
|
125
|
+
return normalizeSafeRelativePath(name, label, {
|
|
126
|
+
allowHiddenSegments: false
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizePluginCacheSegment(value = '', label = 'plugin segment', fallback = 'local') {
|
|
131
|
+
const rawValue = String(value || '').trim() || fallback;
|
|
132
|
+
return normalizeSafeFileStem(rawValue, label, {
|
|
133
|
+
allowDots: true,
|
|
134
|
+
pattern: /^[a-zA-Z0-9][a-zA-Z0-9._@+-]*$/
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function resolvePluginConfigFile(configDir, name) {
|
|
139
|
+
const safeName = normalizePluginPathName(name, 'plugin config name');
|
|
140
|
+
return resolveInsideRoot(configDir, `${safeName}.json`, 'Plugin config path');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function joinRepoPath(...parts) {
|
|
144
|
+
return normalizeRepoPath(parts.filter(Boolean).join('/'));
|
|
145
|
+
}
|
|
146
|
+
|
|
53
147
|
function stripGitSuffix(value = '') {
|
|
54
148
|
return String(value || '').replace(/\.git$/i, '');
|
|
55
149
|
}
|
|
56
150
|
|
|
151
|
+
function slugifyCodexKey(value = '', fallback = 'local') {
|
|
152
|
+
const slug = String(value || '')
|
|
153
|
+
.trim()
|
|
154
|
+
.replace(/\\/g, '/')
|
|
155
|
+
.split('/')
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.pop()
|
|
158
|
+
?.replace(/\.git$/i, '')
|
|
159
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
160
|
+
.replace(/^-+|-+$/g, '');
|
|
161
|
+
return slug || fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function splitPluginMarketplaceKey(key = '') {
|
|
165
|
+
const value = String(key || '');
|
|
166
|
+
const atIndex = value.lastIndexOf('@');
|
|
167
|
+
if (atIndex <= 0) return { name: value, marketplace: '' };
|
|
168
|
+
return {
|
|
169
|
+
name: value.slice(0, atIndex),
|
|
170
|
+
marketplace: value.slice(atIndex + 1)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
57
174
|
function isWindowsAbsolutePath(input = '') {
|
|
58
175
|
return /^[a-zA-Z]:[\\/]/.test(String(input || ''));
|
|
59
176
|
}
|
|
@@ -210,17 +327,24 @@ function stripJsonComments(input = '') {
|
|
|
210
327
|
|
|
211
328
|
class PluginsService {
|
|
212
329
|
constructor(platform = 'claude') {
|
|
213
|
-
this.platform =
|
|
330
|
+
this.platform = SUPPORTED_PLATFORMS.includes(platform) ? platform : 'claude';
|
|
214
331
|
this.configDir = PATHS.config || path.join((PATHS.base || process.env.HOME || os.homedir()), 'config');
|
|
215
332
|
this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
|
|
216
333
|
this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
|
|
217
334
|
this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
|
|
218
|
-
this.
|
|
219
|
-
|
|
220
|
-
: PATHS.pluginMarketCache.claude;
|
|
335
|
+
this.codexPluginsCacheDir = CODEX_PLUGINS_CACHE_DIR;
|
|
336
|
+
this.marketCachePath = PATHS.pluginMarketCache[this.platform] || PATHS.pluginMarketCache.claude;
|
|
221
337
|
this._marketCache = null;
|
|
222
338
|
}
|
|
223
339
|
|
|
340
|
+
getCapabilities() {
|
|
341
|
+
return { ...(PLATFORM_CAPABILITIES[this.platform] || PLATFORM_CAPABILITIES.claude) };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
_pluginsSupported() {
|
|
345
|
+
return this.getCapabilities().supportsPlugins !== false;
|
|
346
|
+
}
|
|
347
|
+
|
|
224
348
|
normalizeRepoConfig(repo = {}) {
|
|
225
349
|
const provider = SUPPORTED_REPO_PROVIDERS.includes(repo.provider)
|
|
226
350
|
? repo.provider
|
|
@@ -426,6 +550,14 @@ class PluginsService {
|
|
|
426
550
|
return this.platform === 'opencode';
|
|
427
551
|
}
|
|
428
552
|
|
|
553
|
+
_isCodex() {
|
|
554
|
+
return this.platform === 'codex';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
_isGemini() {
|
|
558
|
+
return this.platform === 'gemini';
|
|
559
|
+
}
|
|
560
|
+
|
|
429
561
|
_getOpenCodePluginsDir() {
|
|
430
562
|
if (fs.existsSync(this.opencodeLegacyPluginsDir) && !fs.existsSync(this.opencodePluginsDir)) {
|
|
431
563
|
return this.opencodeLegacyPluginsDir;
|
|
@@ -539,12 +671,184 @@ class PluginsService {
|
|
|
539
671
|
return plugins;
|
|
540
672
|
}
|
|
541
673
|
|
|
674
|
+
_readFirstMarkdownParagraph(filePath) {
|
|
675
|
+
if (!fs.existsSync(filePath)) return '';
|
|
676
|
+
try {
|
|
677
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
678
|
+
const lines = content
|
|
679
|
+
.split(/\r?\n/)
|
|
680
|
+
.map(line => line.trim())
|
|
681
|
+
.filter(line => line && !line.startsWith('#') && !line.startsWith('<!--'));
|
|
682
|
+
return lines[0] || '';
|
|
683
|
+
} catch {
|
|
684
|
+
return '';
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
_listCodexCachedPlugins() {
|
|
689
|
+
const cacheDir = this.codexPluginsCacheDir;
|
|
690
|
+
if (!fs.existsSync(cacheDir)) return [];
|
|
691
|
+
|
|
692
|
+
const plugins = [];
|
|
693
|
+
try {
|
|
694
|
+
const marketplaces = fs.readdirSync(cacheDir, { withFileTypes: true });
|
|
695
|
+
for (const marketplaceEntry of marketplaces) {
|
|
696
|
+
if (!marketplaceEntry.isDirectory() || marketplaceEntry.name.startsWith('.')) continue;
|
|
697
|
+
const marketplaceDir = path.join(cacheDir, marketplaceEntry.name);
|
|
698
|
+
const pluginEntries = fs.readdirSync(marketplaceDir, { withFileTypes: true });
|
|
699
|
+
|
|
700
|
+
for (const pluginEntry of pluginEntries) {
|
|
701
|
+
if (!pluginEntry.isDirectory() || pluginEntry.name.startsWith('.')) continue;
|
|
702
|
+
const pluginRoot = path.join(marketplaceDir, pluginEntry.name);
|
|
703
|
+
const versionEntries = fs.readdirSync(pluginRoot, { withFileTypes: true })
|
|
704
|
+
.filter(entry => entry.isDirectory() && !entry.name.startsWith('.'));
|
|
705
|
+
const selectedVersion = versionEntries
|
|
706
|
+
.map(entry => ({
|
|
707
|
+
name: entry.name,
|
|
708
|
+
fullPath: path.join(pluginRoot, entry.name),
|
|
709
|
+
mtimeMs: fs.statSync(path.join(pluginRoot, entry.name)).mtimeMs
|
|
710
|
+
}))
|
|
711
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)[0];
|
|
712
|
+
if (!selectedVersion) continue;
|
|
713
|
+
|
|
714
|
+
const description = this._readFirstMarkdownParagraph(path.join(selectedVersion.fullPath, 'README.md'));
|
|
715
|
+
const manifest = this._readLocalManifest(selectedVersion.fullPath, [
|
|
716
|
+
'.codex-plugin/plugin.json',
|
|
717
|
+
'plugin.json',
|
|
718
|
+
'package.json'
|
|
719
|
+
]) || {};
|
|
720
|
+
plugins.push({
|
|
721
|
+
name: manifest.name || pluginEntry.name,
|
|
722
|
+
marketplace: marketplaceEntry.name,
|
|
723
|
+
version: manifest.version || selectedVersion.name,
|
|
724
|
+
installPath: selectedVersion.fullPath,
|
|
725
|
+
directory: path.relative(CODEX_PLUGINS_DIR, selectedVersion.fullPath),
|
|
726
|
+
source: 'codex-cache',
|
|
727
|
+
installed: true,
|
|
728
|
+
enabled: this._isCodexPluginEnabled(manifest.name || pluginEntry.name, marketplaceEntry.name),
|
|
729
|
+
pluginType: 'cache',
|
|
730
|
+
description: manifest.description || description
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
} catch (err) {
|
|
735
|
+
console.warn('[PluginsService] Failed to scan Codex plugin cache:', err.message);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
plugins.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
739
|
+
return plugins;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
_readLocalManifest(rootDir, candidates = []) {
|
|
743
|
+
for (const candidate of candidates) {
|
|
744
|
+
const manifestPath = path.join(rootDir, candidate);
|
|
745
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
746
|
+
try {
|
|
747
|
+
return JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')));
|
|
748
|
+
} catch {
|
|
749
|
+
// try next candidate
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
_readCodexConfig() {
|
|
756
|
+
const filePath = NATIVE_PATHS.codex.config;
|
|
757
|
+
if (!fs.existsSync(filePath)) return { filePath, config: {} };
|
|
758
|
+
try {
|
|
759
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
760
|
+
if (!raw.trim()) return { filePath, config: {} };
|
|
761
|
+
return { filePath, config: toml.parse(raw) };
|
|
762
|
+
} catch (err) {
|
|
763
|
+
console.error('[PluginsService] Failed to read Codex config:', err.message);
|
|
764
|
+
return { filePath, config: {} };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
_writeCodexConfig(config) {
|
|
769
|
+
const filePath = NATIVE_PATHS.codex.config;
|
|
770
|
+
this._ensureDir(path.dirname(filePath));
|
|
771
|
+
const safeConfig = JSON.parse(JSON.stringify(config || {}));
|
|
772
|
+
fs.writeFileSync(filePath, tomlStringify(safeConfig), 'utf8');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_isCodexPluginEnabled(name, marketplace = '') {
|
|
776
|
+
const { config } = this._readCodexConfig();
|
|
777
|
+
const plugins = config.plugins || {};
|
|
778
|
+
if (marketplace) {
|
|
779
|
+
const exact = plugins[`${name}@${marketplace}`];
|
|
780
|
+
if (exact && typeof exact === 'object' && exact.enabled === false) return false;
|
|
781
|
+
}
|
|
782
|
+
for (const [key, value] of Object.entries(plugins)) {
|
|
783
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
784
|
+
if (parsed.name === name && value && typeof value === 'object' && value.enabled === false) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
_setCodexPluginEnabled(name, marketplace, enabled) {
|
|
792
|
+
const { config } = this._readCodexConfig();
|
|
793
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
794
|
+
nextConfig.plugins = { ...(nextConfig.plugins || {}) };
|
|
795
|
+
const pluginKey = marketplace ? `${name}@${marketplace}` : name;
|
|
796
|
+
nextConfig.plugins[pluginKey] = {
|
|
797
|
+
...(nextConfig.plugins[pluginKey] || {}),
|
|
798
|
+
enabled: !!enabled
|
|
799
|
+
};
|
|
800
|
+
this._writeCodexConfig(nextConfig);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
_registerCodexMarketplace(marketplace, source) {
|
|
804
|
+
const { config } = this._readCodexConfig();
|
|
805
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
806
|
+
nextConfig.marketplaces = { ...(nextConfig.marketplaces || {}) };
|
|
807
|
+
nextConfig.marketplaces[marketplace] = {
|
|
808
|
+
...(nextConfig.marketplaces[marketplace] || {}),
|
|
809
|
+
source_type: isLikelyLocalPath(source) ? 'local' : 'git',
|
|
810
|
+
source,
|
|
811
|
+
last_updated: new Date().toISOString()
|
|
812
|
+
};
|
|
813
|
+
this._writeCodexConfig(nextConfig);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_unregisterCodexPlugin(name, marketplace = '') {
|
|
817
|
+
const { config } = this._readCodexConfig();
|
|
818
|
+
const nextConfig = (config && typeof config === 'object') ? { ...config } : {};
|
|
819
|
+
const plugins = { ...(nextConfig.plugins || {}) };
|
|
820
|
+
for (const key of Object.keys(plugins)) {
|
|
821
|
+
const parsed = splitPluginMarketplaceKey(key);
|
|
822
|
+
if (parsed.name === name && (!marketplace || parsed.marketplace === marketplace)) {
|
|
823
|
+
delete plugins[key];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
nextConfig.plugins = plugins;
|
|
827
|
+
this._writeCodexConfig(nextConfig);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
_resolveCodexMarketplaceName(repo, explicit = '') {
|
|
831
|
+
if (explicit) return slugifyCodexKey(explicit);
|
|
832
|
+
if (repo.marketplace) return slugifyCodexKey(repo.marketplace);
|
|
833
|
+
if (repo.provider === 'local') return slugifyCodexKey(repo.localPath, 'local');
|
|
834
|
+
if (repo.provider === 'gitlab') return slugifyCodexKey(repo.projectPath, 'gitlab');
|
|
835
|
+
return slugifyCodexKey(repo.name || repo.owner || 'github');
|
|
836
|
+
}
|
|
837
|
+
|
|
542
838
|
/**
|
|
543
839
|
* List all installed plugins with their status
|
|
544
840
|
* Reads from Claude Code's native installed_plugins.json
|
|
545
841
|
* @returns {Object} { plugins: Array }
|
|
546
842
|
*/
|
|
547
843
|
listPlugins() {
|
|
844
|
+
if (!this._pluginsSupported()) {
|
|
845
|
+
return { plugins: [] };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (this._isCodex()) {
|
|
849
|
+
return { plugins: this._listCodexCachedPlugins() };
|
|
850
|
+
}
|
|
851
|
+
|
|
548
852
|
if (this._isOpenCode()) {
|
|
549
853
|
const plugins = [];
|
|
550
854
|
const seen = new Set();
|
|
@@ -690,6 +994,22 @@ class PluginsService {
|
|
|
690
994
|
* @returns {Object|null} Plugin details or null
|
|
691
995
|
*/
|
|
692
996
|
getPlugin(name) {
|
|
997
|
+
if (this._isCodex()) {
|
|
998
|
+
const plugin = this.listPlugins().plugins.find(p => p.name === name || `${p.name}@${p.marketplace}` === name);
|
|
999
|
+
if (!plugin) return null;
|
|
1000
|
+
const manifest = plugin.installPath
|
|
1001
|
+
? this._readLocalManifest(plugin.installPath, ['.codex-plugin/plugin.json', 'plugin.json', 'package.json'])
|
|
1002
|
+
: null;
|
|
1003
|
+
return {
|
|
1004
|
+
name: plugin.name,
|
|
1005
|
+
...plugin,
|
|
1006
|
+
author: manifest?.author || '',
|
|
1007
|
+
commands: manifest?.commands || [],
|
|
1008
|
+
hooks: manifest?.hooks || [],
|
|
1009
|
+
manifest
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
|
|
693
1013
|
if (this._isOpenCode()) {
|
|
694
1014
|
const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
|
|
695
1015
|
if (!plugin) return null;
|
|
@@ -731,6 +1051,34 @@ class PluginsService {
|
|
|
731
1051
|
* @returns {Promise<Object>} Installation result
|
|
732
1052
|
*/
|
|
733
1053
|
async installPlugin(source, repoInfo = null) {
|
|
1054
|
+
if (this._isCodex()) {
|
|
1055
|
+
if (repoInfo && repoInfo.directory) {
|
|
1056
|
+
return this._installFromRepoDirectory(repoInfo);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
const parsedSource = this.parseRepoTreeSource(source);
|
|
1060
|
+
if (parsedSource) {
|
|
1061
|
+
return this._installFromRepoDirectory(parsedSource);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
const parsedRepo = this._repoFromGitUrl(source, 'main');
|
|
1065
|
+
if (parsedRepo) {
|
|
1066
|
+
return this._installFromRepoDirectory({ ...parsedRepo, directory: '' });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
success: false,
|
|
1071
|
+
error: 'Codex plugin install expects repository metadata, a GitHub/GitLab tree URL, or a Git repository URL'
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
if (!this._pluginsSupported()) {
|
|
1076
|
+
return {
|
|
1077
|
+
success: false,
|
|
1078
|
+
error: `${this.platform} plugin management is not supported`
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
734
1082
|
if (this._isOpenCode()) {
|
|
735
1083
|
if (repoInfo && repoInfo.directory) {
|
|
736
1084
|
return this._installFromRepoDirectory(repoInfo, { installRoot: this._getOpenCodePluginsDir() });
|
|
@@ -779,30 +1127,46 @@ class PluginsService {
|
|
|
779
1127
|
*/
|
|
780
1128
|
async _installFromRepoDirectory(repoInfo, options = {}) {
|
|
781
1129
|
const normalizedRepo = this.normalizeRepoConfig(repoInfo);
|
|
782
|
-
const directory =
|
|
783
|
-
const pluginName = directory.split('/').pop();
|
|
784
|
-
const
|
|
1130
|
+
const directory = normalizePluginRepoDirectory(repoInfo.directory || normalizedRepo.directory || '');
|
|
1131
|
+
const pluginName = directory.split('/').filter(Boolean).pop() || slugifyCodexKey(normalizedRepo.name, 'plugin');
|
|
1132
|
+
const manifestCandidates = this._getManifestCandidates(directory);
|
|
785
1133
|
|
|
786
1134
|
try {
|
|
787
1135
|
let manifest;
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
} catch {
|
|
1136
|
+
let manifestPath = '';
|
|
1137
|
+
for (const candidate of manifestCandidates) {
|
|
791
1138
|
try {
|
|
792
|
-
|
|
1139
|
+
manifestPath = joinRepoPath(directory, candidate);
|
|
1140
|
+
manifest = await this.fetchRepoJson(normalizedRepo, manifestPath);
|
|
1141
|
+
break;
|
|
793
1142
|
} catch {
|
|
794
|
-
|
|
1143
|
+
manifestPath = '';
|
|
795
1144
|
}
|
|
796
1145
|
}
|
|
1146
|
+
if (!manifest) {
|
|
1147
|
+
manifest = { name: pluginName, version: '1.0.0' };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const installedPluginName = String(manifest.name || pluginName || 'plugin').trim() || 'plugin';
|
|
1151
|
+
const installRoot = this._resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options);
|
|
797
1152
|
|
|
798
1153
|
// Create plugin directory
|
|
799
|
-
const pluginDir =
|
|
1154
|
+
const pluginDir = this._isCodex()
|
|
1155
|
+
? installRoot
|
|
1156
|
+
: resolveInsideRoot(
|
|
1157
|
+
installRoot,
|
|
1158
|
+
normalizePluginPathName(installedPluginName, 'plugin name'),
|
|
1159
|
+
'Plugin install path',
|
|
1160
|
+
{ allowHiddenSegments: false }
|
|
1161
|
+
);
|
|
800
1162
|
if (!fs.existsSync(pluginDir)) {
|
|
801
1163
|
fs.mkdirSync(pluginDir, { recursive: true });
|
|
802
1164
|
}
|
|
803
1165
|
|
|
804
1166
|
if (normalizedRepo.provider === 'local') {
|
|
805
|
-
const sourceDir =
|
|
1167
|
+
const sourceDir = directory
|
|
1168
|
+
? resolveInsideRoot(normalizedRepo.localPath, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
1169
|
+
: normalizedRepo.localPath;
|
|
806
1170
|
if (!fs.existsSync(sourceDir)) {
|
|
807
1171
|
throw new Error(`Plugin directory not found: ${directory}`);
|
|
808
1172
|
}
|
|
@@ -843,7 +1207,10 @@ class PluginsService {
|
|
|
843
1207
|
throw new Error('Empty archive');
|
|
844
1208
|
}
|
|
845
1209
|
|
|
846
|
-
const
|
|
1210
|
+
const extractedRoot = path.join(tempDir, extractedDir);
|
|
1211
|
+
const sourceDir = directory
|
|
1212
|
+
? resolveInsideRoot(extractedRoot, directory, 'Plugin directory', { allowHiddenSegments: true })
|
|
1213
|
+
: extractedRoot;
|
|
847
1214
|
if (!fs.existsSync(sourceDir)) {
|
|
848
1215
|
throw new Error(`Plugin directory not found: ${directory}`);
|
|
849
1216
|
}
|
|
@@ -854,13 +1221,33 @@ class PluginsService {
|
|
|
854
1221
|
}
|
|
855
1222
|
|
|
856
1223
|
// Write plugin.json if not exists
|
|
857
|
-
const
|
|
858
|
-
if (!fs.existsSync(
|
|
859
|
-
|
|
1224
|
+
const fallbackManifestPath = path.join(pluginDir, this._isCodex() ? '.codex-plugin/plugin.json' : 'plugin.json');
|
|
1225
|
+
if (!fs.existsSync(fallbackManifestPath)) {
|
|
1226
|
+
this._ensureDir(path.dirname(fallbackManifestPath));
|
|
1227
|
+
fs.writeFileSync(fallbackManifestPath, JSON.stringify(manifest, null, 2));
|
|
860
1228
|
}
|
|
861
1229
|
|
|
862
|
-
if (
|
|
863
|
-
const
|
|
1230
|
+
if (this._isCodex()) {
|
|
1231
|
+
const marketplace = this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace);
|
|
1232
|
+
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
1233
|
+
this.writeRepoSourceMeta(pluginDir, {
|
|
1234
|
+
repoId: normalizedRepo.id,
|
|
1235
|
+
repoProvider: normalizedRepo.provider,
|
|
1236
|
+
repoOwner: normalizedRepo.owner || '',
|
|
1237
|
+
repoName: normalizedRepo.name || '',
|
|
1238
|
+
repoBranch: normalizedRepo.branch,
|
|
1239
|
+
repoDirectory: directory,
|
|
1240
|
+
repoHost: normalizedRepo.host || '',
|
|
1241
|
+
repoProjectPath: normalizedRepo.projectPath || '',
|
|
1242
|
+
repoLocalPath: normalizedRepo.localPath || '',
|
|
1243
|
+
repoUrl: normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo),
|
|
1244
|
+
marketplace,
|
|
1245
|
+
manifestPath,
|
|
1246
|
+
source: sourceUrl
|
|
1247
|
+
});
|
|
1248
|
+
this._registerCodexMarketplace(marketplace, normalizedRepo.repoUrl || buildRepoUrl(normalizedRepo));
|
|
1249
|
+
this._setCodexPluginEnabled(installedPluginName, marketplace, true);
|
|
1250
|
+
} else if (!this._isOpenCode()) {
|
|
864
1251
|
const installTimestamp = new Date().toISOString();
|
|
865
1252
|
const sourceUrl = this.buildRepoBrowserUrl(normalizedRepo, directory) || buildRepoUrl(normalizedRepo);
|
|
866
1253
|
const repoSourceMeta = {
|
|
@@ -933,6 +1320,43 @@ class PluginsService {
|
|
|
933
1320
|
}
|
|
934
1321
|
}
|
|
935
1322
|
|
|
1323
|
+
_getManifestCandidates() {
|
|
1324
|
+
if (this._isCodex()) {
|
|
1325
|
+
return ['.codex-plugin/plugin.json', 'plugin.json', 'package.json'];
|
|
1326
|
+
}
|
|
1327
|
+
if (this._isOpenCode()) {
|
|
1328
|
+
return ['package.json', 'plugin.json'];
|
|
1329
|
+
}
|
|
1330
|
+
return ['.claude-plugin/plugin.json', 'plugin.json', 'package.json'];
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
_resolvePluginInstallRoot(normalizedRepo, repoInfo, manifest, options = {}) {
|
|
1334
|
+
if (!this._isCodex()) {
|
|
1335
|
+
return options.installRoot || INSTALLED_DIR;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
const marketplace = normalizePluginCacheSegment(
|
|
1339
|
+
this._resolveCodexMarketplaceName(normalizedRepo, repoInfo.marketplace),
|
|
1340
|
+
'Codex marketplace',
|
|
1341
|
+
'local'
|
|
1342
|
+
);
|
|
1343
|
+
const pluginName = normalizePluginCacheSegment(
|
|
1344
|
+
manifest.name || normalizeRepoPath(repoInfo.directory || '').split('/').pop() || 'plugin',
|
|
1345
|
+
'Codex plugin name',
|
|
1346
|
+
'plugin'
|
|
1347
|
+
);
|
|
1348
|
+
const version = normalizePluginCacheSegment(
|
|
1349
|
+
String(manifest.version || repoInfo.version || 'local').trim() || 'local',
|
|
1350
|
+
'Codex plugin version',
|
|
1351
|
+
'local'
|
|
1352
|
+
);
|
|
1353
|
+
return resolveInsideRoot(
|
|
1354
|
+
this.codexPluginsCacheDir,
|
|
1355
|
+
path.posix.join(marketplace, pluginName, version),
|
|
1356
|
+
'Codex plugin cache path'
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
936
1360
|
/**
|
|
937
1361
|
* Parse GitHub/GitLab tree URL or local path
|
|
938
1362
|
* @private
|
|
@@ -986,13 +1410,56 @@ class PluginsService {
|
|
|
986
1410
|
* @returns {Object} Uninstallation result
|
|
987
1411
|
*/
|
|
988
1412
|
uninstallPlugin(name) {
|
|
1413
|
+
if (this._isCodex()) {
|
|
1414
|
+
const plugins = this.listPlugins().plugins;
|
|
1415
|
+
const target = plugins.find(plugin => plugin.name === name || `${plugin.name}@${plugin.marketplace}` === name);
|
|
1416
|
+
if (!target) {
|
|
1417
|
+
return {
|
|
1418
|
+
success: false,
|
|
1419
|
+
error: `Plugin "${name}" not found`
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
const pluginRoot = target.installPath
|
|
1424
|
+
? assertInsideAllowedRoots(
|
|
1425
|
+
path.dirname(assertInsideAllowedRoots(target.installPath, [this.codexPluginsCacheDir], 'Codex plugin install path')),
|
|
1426
|
+
[this.codexPluginsCacheDir],
|
|
1427
|
+
'Codex plugin root'
|
|
1428
|
+
)
|
|
1429
|
+
: resolveInsideRoot(
|
|
1430
|
+
this.codexPluginsCacheDir,
|
|
1431
|
+
path.posix.join(
|
|
1432
|
+
normalizePluginCacheSegment(target.marketplace || '', 'Codex marketplace', 'local'),
|
|
1433
|
+
normalizePluginCacheSegment(target.name || name, 'Codex plugin name', 'plugin')
|
|
1434
|
+
),
|
|
1435
|
+
'Codex plugin root'
|
|
1436
|
+
);
|
|
1437
|
+
if (pluginRoot && fs.existsSync(pluginRoot)) {
|
|
1438
|
+
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
|
1439
|
+
}
|
|
1440
|
+
this._unregisterCodexPlugin(target.name, target.marketplace || '');
|
|
1441
|
+
|
|
1442
|
+
return {
|
|
1443
|
+
success: true,
|
|
1444
|
+
message: 'Plugin uninstalled successfully'
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (!this._pluginsSupported()) {
|
|
1449
|
+
return {
|
|
1450
|
+
success: false,
|
|
1451
|
+
error: `${this.platform} plugin management is not supported`
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
|
|
989
1455
|
if (this._isOpenCode()) {
|
|
1456
|
+
const safeName = normalizePluginPathName(name, 'plugin name');
|
|
990
1457
|
const pluginsDir = this._getOpenCodePluginsDir();
|
|
991
1458
|
let removed = false;
|
|
992
1459
|
|
|
993
1460
|
// Remove from opencode config.plugin (npm plugins)
|
|
994
1461
|
const configured = this._listOpenCodeConfiguredPlugins();
|
|
995
|
-
const next = configured.filter(p => p !==
|
|
1462
|
+
const next = configured.filter(p => p !== safeName);
|
|
996
1463
|
if (next.length !== configured.length) {
|
|
997
1464
|
this._setOpenCodeConfiguredPlugins(next);
|
|
998
1465
|
removed = true;
|
|
@@ -1000,7 +1467,7 @@ class PluginsService {
|
|
|
1000
1467
|
|
|
1001
1468
|
// Remove local plugin directory/file
|
|
1002
1469
|
if (fs.existsSync(pluginsDir)) {
|
|
1003
|
-
const directPath =
|
|
1470
|
+
const directPath = resolveInsideRoot(pluginsDir, safeName, 'OpenCode plugin path');
|
|
1004
1471
|
if (fs.existsSync(directPath)) {
|
|
1005
1472
|
fs.rmSync(directPath, { recursive: true, force: true });
|
|
1006
1473
|
removed = true;
|
|
@@ -1008,8 +1475,9 @@ class PluginsService {
|
|
|
1008
1475
|
const entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
|
|
1009
1476
|
for (const entry of entries) {
|
|
1010
1477
|
const baseName = entry.name.replace(path.extname(entry.name), '');
|
|
1011
|
-
if (entry.name ===
|
|
1012
|
-
|
|
1478
|
+
if (entry.name === safeName || baseName === safeName) {
|
|
1479
|
+
const entryPath = resolveInsideRoot(pluginsDir, entry.name, 'OpenCode plugin path');
|
|
1480
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
1013
1481
|
removed = true;
|
|
1014
1482
|
break;
|
|
1015
1483
|
}
|
|
@@ -1025,22 +1493,28 @@ class PluginsService {
|
|
|
1025
1493
|
|
|
1026
1494
|
// Claude: Remove from native installed_plugins.json and delete install directories
|
|
1027
1495
|
let removed = false;
|
|
1496
|
+
const safeName = normalizePluginPathName(name, 'plugin name');
|
|
1028
1497
|
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
1029
1498
|
try {
|
|
1030
1499
|
const data = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1031
1500
|
if (data.plugins) {
|
|
1032
1501
|
const keysToDelete = [];
|
|
1033
|
-
const baseName =
|
|
1502
|
+
const baseName = safeName.split('/').pop(); // handle "plugins/pr-review-toolkit" → "pr-review-toolkit"
|
|
1034
1503
|
for (const [key, installations] of Object.entries(data.plugins)) {
|
|
1035
1504
|
const [pluginName] = key.split('@');
|
|
1036
|
-
if (pluginName ===
|
|
1505
|
+
if (pluginName === safeName || key === safeName || pluginName === baseName) {
|
|
1037
1506
|
keysToDelete.push(key);
|
|
1038
1507
|
// Delete install directories
|
|
1039
1508
|
if (Array.isArray(installations)) {
|
|
1040
1509
|
for (const install of installations) {
|
|
1041
1510
|
if (install.installPath && fs.existsSync(install.installPath)) {
|
|
1042
1511
|
try {
|
|
1043
|
-
|
|
1512
|
+
const installPath = assertInsideAllowedRoots(
|
|
1513
|
+
install.installPath,
|
|
1514
|
+
[INSTALLED_DIR, CLAUDE_PLUGINS_DIR],
|
|
1515
|
+
'Claude plugin install path'
|
|
1516
|
+
);
|
|
1517
|
+
fs.rmSync(installPath, { recursive: true, force: true });
|
|
1044
1518
|
} catch (e) {
|
|
1045
1519
|
console.error('[PluginsService] Failed to delete install dir:', e.message);
|
|
1046
1520
|
}
|
|
@@ -1090,6 +1564,24 @@ class PluginsService {
|
|
|
1090
1564
|
* @returns {Object} Updated plugin info
|
|
1091
1565
|
*/
|
|
1092
1566
|
togglePlugin(name, enabled) {
|
|
1567
|
+
if (this._isCodex()) {
|
|
1568
|
+
const plugins = this.listPlugins().plugins;
|
|
1569
|
+
const target = plugins.find(plugin => plugin.name === name || `${plugin.name}@${plugin.marketplace}` === name);
|
|
1570
|
+
if (!target) {
|
|
1571
|
+
throw new Error(`Plugin "${name}" not found`);
|
|
1572
|
+
}
|
|
1573
|
+
this._setCodexPluginEnabled(target.name, target.marketplace || '', enabled);
|
|
1574
|
+
return {
|
|
1575
|
+
name: target.name,
|
|
1576
|
+
enabled,
|
|
1577
|
+
success: true
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
if (!this._pluginsSupported()) {
|
|
1582
|
+
throw new Error(`${this.platform} plugin management is not supported`);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1093
1585
|
if (this._isOpenCode()) {
|
|
1094
1586
|
const configured = this._listOpenCodeConfiguredPlugins();
|
|
1095
1587
|
const exists = configured.includes(name);
|
|
@@ -1159,10 +1651,18 @@ class PluginsService {
|
|
|
1159
1651
|
* @returns {Object} Result
|
|
1160
1652
|
*/
|
|
1161
1653
|
updatePluginConfig(name, config) {
|
|
1654
|
+
if (this._isCodex()) {
|
|
1655
|
+
throw new Error('Codex cached plugins do not support config updates');
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (!this._pluginsSupported()) {
|
|
1659
|
+
throw new Error(`${this.platform} plugin management is not supported`);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1162
1662
|
if (this._isOpenCode()) {
|
|
1163
1663
|
const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
|
|
1164
|
-
|
|
1165
|
-
|
|
1664
|
+
const configFile = resolvePluginConfigFile(configDir, name);
|
|
1665
|
+
this._ensureDir(path.dirname(configFile));
|
|
1166
1666
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
1167
1667
|
return {
|
|
1168
1668
|
success: true,
|
|
@@ -1175,11 +1675,11 @@ class PluginsService {
|
|
|
1175
1675
|
throw new Error(`Plugin "${name}" not found`);
|
|
1176
1676
|
}
|
|
1177
1677
|
|
|
1178
|
-
const configFile =
|
|
1678
|
+
const configFile = resolvePluginConfigFile(CONFIG_DIR, name);
|
|
1179
1679
|
|
|
1180
1680
|
// Ensure config directory exists
|
|
1181
|
-
if (!fs.existsSync(
|
|
1182
|
-
fs.mkdirSync(
|
|
1681
|
+
if (!fs.existsSync(path.dirname(configFile))) {
|
|
1682
|
+
fs.mkdirSync(path.dirname(configFile), { recursive: true });
|
|
1183
1683
|
}
|
|
1184
1684
|
|
|
1185
1685
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
@@ -1195,7 +1695,7 @@ class PluginsService {
|
|
|
1195
1695
|
* @returns {string} Config file path
|
|
1196
1696
|
*/
|
|
1197
1697
|
getReposConfigPath() {
|
|
1198
|
-
const filePath =
|
|
1698
|
+
const filePath = PATHS.pluginRepos[this.platform] || PATHS.pluginRepos.claude;
|
|
1199
1699
|
this._ensureDir(path.dirname(filePath));
|
|
1200
1700
|
return filePath;
|
|
1201
1701
|
}
|
|
@@ -1242,6 +1742,10 @@ class PluginsService {
|
|
|
1242
1742
|
* @returns {Array} Repos list
|
|
1243
1743
|
*/
|
|
1244
1744
|
getRepos() {
|
|
1745
|
+
if (!this.getCapabilities().repositories) {
|
|
1746
|
+
return [];
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1245
1749
|
const repos = [];
|
|
1246
1750
|
const seenRepos = new Set();
|
|
1247
1751
|
const pushRepo = (repo) => {
|
|
@@ -1271,7 +1775,7 @@ class PluginsService {
|
|
|
1271
1775
|
}
|
|
1272
1776
|
|
|
1273
1777
|
// 2. Load Claude Code's native marketplace config (Claude only)
|
|
1274
|
-
if (
|
|
1778
|
+
if (this.platform === 'claude' && fs.existsSync(CLAUDE_MARKETPLACES_FILE)) {
|
|
1275
1779
|
try {
|
|
1276
1780
|
const marketplaces = JSON.parse(fs.readFileSync(CLAUDE_MARKETPLACES_FILE, 'utf8'));
|
|
1277
1781
|
const entries = [];
|
|
@@ -1644,11 +2148,10 @@ class PluginsService {
|
|
|
1644
2148
|
scanLocalRepoTree(currentDir, repoRoot, tree) {
|
|
1645
2149
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1646
2150
|
for (const entry of entries) {
|
|
1647
|
-
if (entry.name.startsWith('.')) continue;
|
|
1648
2151
|
const fullPath = path.join(currentDir, entry.name);
|
|
1649
2152
|
const relativePath = normalizeRepoPath(path.relative(repoRoot, fullPath));
|
|
1650
2153
|
if (entry.isDirectory()) {
|
|
1651
|
-
if (entry.name === 'node_modules') continue;
|
|
2154
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
1652
2155
|
tree.push({ path: relativePath, type: 'tree', name: entry.name });
|
|
1653
2156
|
this.scanLocalRepoTree(fullPath, repoRoot, tree);
|
|
1654
2157
|
} else {
|
|
@@ -1704,7 +2207,12 @@ class PluginsService {
|
|
|
1704
2207
|
|
|
1705
2208
|
async fetchRepoFileContent(repo, filePath, file = null) {
|
|
1706
2209
|
if (repo.provider === 'local') {
|
|
1707
|
-
|
|
2210
|
+
const safeFilePath = normalizePluginRepoDirectory(filePath, 'plugin repository file path');
|
|
2211
|
+
if (!safeFilePath) {
|
|
2212
|
+
throw new Error('Invalid plugin repository file path');
|
|
2213
|
+
}
|
|
2214
|
+
const localFilePath = resolveInsideRoot(repo.localPath, safeFilePath, 'Plugin repository file path', { allowHiddenSegments: true });
|
|
2215
|
+
return fs.readFileSync(localFilePath, 'utf-8');
|
|
1708
2216
|
}
|
|
1709
2217
|
if (repo.provider === 'gitlab') {
|
|
1710
2218
|
return this.fetchGitLabFileContent(repo, filePath);
|
|
@@ -1820,7 +2328,7 @@ class PluginsService {
|
|
|
1820
2328
|
* @returns {Promise<Object>} Sync results
|
|
1821
2329
|
*/
|
|
1822
2330
|
async syncRepos() {
|
|
1823
|
-
if (this.
|
|
2331
|
+
if (!this.getCapabilities().syncRepos) {
|
|
1824
2332
|
return { success: true, results: [] };
|
|
1825
2333
|
}
|
|
1826
2334
|
|
|
@@ -1957,26 +2465,28 @@ class PluginsService {
|
|
|
1957
2465
|
}
|
|
1958
2466
|
|
|
1959
2467
|
buildMarketPluginItem(repo, data = {}) {
|
|
2468
|
+
const itemRepo = data.repo || repo;
|
|
1960
2469
|
return {
|
|
1961
2470
|
name: data.name,
|
|
1962
2471
|
displayName: data.displayName || '',
|
|
1963
2472
|
description: data.description || '',
|
|
1964
|
-
author: data.author ||
|
|
2473
|
+
author: data.author || itemRepo.owner || itemRepo.projectPath || 'unknown',
|
|
1965
2474
|
version: data.version || '1.0.0',
|
|
1966
2475
|
category: data.category || 'general',
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2476
|
+
marketplace: data.marketplace || repo.marketplace || '',
|
|
2477
|
+
repoUrl: data.repoUrl || itemRepo.repoUrl || buildRepoUrl(itemRepo),
|
|
2478
|
+
repoProvider: itemRepo.provider,
|
|
2479
|
+
repoOwner: itemRepo.owner || '',
|
|
2480
|
+
repoName: itemRepo.name || '',
|
|
2481
|
+
repoBranch: itemRepo.branch || 'main',
|
|
2482
|
+
repoHost: itemRepo.host || '',
|
|
2483
|
+
repoProjectPath: itemRepo.projectPath || '',
|
|
2484
|
+
repoLocalPath: itemRepo.localPath || '',
|
|
2485
|
+
repoId: itemRepo.id,
|
|
1976
2486
|
directory: normalizeRepoPath(data.directory || data.name || ''),
|
|
1977
2487
|
installSource: data.installSource || '',
|
|
1978
2488
|
marketplaceFormat: data.marketplaceFormat || '',
|
|
1979
|
-
readmeUrl: this.buildRepoBrowserUrl(
|
|
2489
|
+
readmeUrl: this.buildRepoBrowserUrl(itemRepo, data.directory || data.name || ''),
|
|
1980
2490
|
lspServers: data.lspServers || null,
|
|
1981
2491
|
commands: data.commands || [],
|
|
1982
2492
|
hooks: data.hooks || [],
|
|
@@ -2017,7 +2527,7 @@ class PluginsService {
|
|
|
2017
2527
|
version: manifest.version || manifest.opencode?.minimumVersion || '1.0.0',
|
|
2018
2528
|
category: firstCategory ? String(firstCategory).toLowerCase() : 'general',
|
|
2019
2529
|
directory: file.path,
|
|
2020
|
-
installSource
|
|
2530
|
+
installSource,
|
|
2021
2531
|
marketplaceFormat: 'opencode-plugin-json',
|
|
2022
2532
|
repoUrl
|
|
2023
2533
|
});
|
|
@@ -2029,11 +2539,182 @@ class PluginsService {
|
|
|
2029
2539
|
.map(item => item.value);
|
|
2030
2540
|
}
|
|
2031
2541
|
|
|
2542
|
+
_repoFromMarketplaceSource(parentRepo, source) {
|
|
2543
|
+
if (!source) return { repo: parentRepo, directory: '', installSource: '' };
|
|
2544
|
+
|
|
2545
|
+
if (typeof source === 'string') {
|
|
2546
|
+
const raw = source.trim();
|
|
2547
|
+
if (!raw) return { repo: parentRepo, directory: '', installSource: '' };
|
|
2548
|
+
if (isLikelyLocalPath(raw) || raw.startsWith('.')) {
|
|
2549
|
+
return {
|
|
2550
|
+
repo: parentRepo,
|
|
2551
|
+
directory: normalizeRepoPath(raw.replace(/^\.\//, '')),
|
|
2552
|
+
installSource: ''
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
const parsedTree = this.parseRepoTreeSource(raw);
|
|
2556
|
+
if (parsedTree) {
|
|
2557
|
+
return { repo: this.normalizeRepoConfig(parsedTree), directory: parsedTree.directory, installSource: raw };
|
|
2558
|
+
}
|
|
2559
|
+
const parsedRepo = this._repoFromGitUrl(raw, parentRepo.branch || 'main');
|
|
2560
|
+
if (parsedRepo) {
|
|
2561
|
+
return { repo: parsedRepo, directory: '', installSource: raw };
|
|
2562
|
+
}
|
|
2563
|
+
return { repo: parentRepo, directory: normalizeRepoPath(raw.replace(/^\.\//, '')), installSource: raw };
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (typeof source !== 'object') {
|
|
2567
|
+
return { repo: parentRepo, directory: '', installSource: '' };
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
const sourceType = source.source || source.type || '';
|
|
2571
|
+
const rawPath = source.path || source.directory || '';
|
|
2572
|
+
if (sourceType === 'local' || (!source.url && rawPath)) {
|
|
2573
|
+
return {
|
|
2574
|
+
repo: parentRepo,
|
|
2575
|
+
directory: normalizeRepoPath(String(rawPath).replace(/^\.\//, '')),
|
|
2576
|
+
installSource: ''
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
if (source.url) {
|
|
2581
|
+
const branch = source.ref || source.branch || source.sha || parentRepo.branch || 'main';
|
|
2582
|
+
const parsedRepo = this._repoFromGitUrl(source.url, branch);
|
|
2583
|
+
if (parsedRepo) {
|
|
2584
|
+
return {
|
|
2585
|
+
repo: parsedRepo,
|
|
2586
|
+
directory: normalizeRepoPath(rawPath),
|
|
2587
|
+
installSource: source.url
|
|
2588
|
+
};
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
return {
|
|
2593
|
+
repo: parentRepo,
|
|
2594
|
+
directory: normalizeRepoPath(rawPath),
|
|
2595
|
+
installSource: source.url || ''
|
|
2596
|
+
};
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
_repoFromGitUrl(url = '', branch = 'main') {
|
|
2600
|
+
const value = String(url || '').trim();
|
|
2601
|
+
if (!value) return null;
|
|
2602
|
+
let parsed;
|
|
2603
|
+
try {
|
|
2604
|
+
parsed = new URL(value);
|
|
2605
|
+
} catch {
|
|
2606
|
+
return null;
|
|
2607
|
+
}
|
|
2608
|
+
|
|
2609
|
+
const parts = parsed.pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '').split('/').filter(Boolean);
|
|
2610
|
+
if (parsed.hostname.includes('github') && parts.length >= 2) {
|
|
2611
|
+
return this.normalizeRepoConfig({
|
|
2612
|
+
provider: 'github',
|
|
2613
|
+
host: `${parsed.protocol}//${parsed.host}`,
|
|
2614
|
+
owner: parts[0],
|
|
2615
|
+
name: parts[1],
|
|
2616
|
+
branch,
|
|
2617
|
+
repoUrl: value
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
|
|
2621
|
+
if (parsed.hostname.includes('gitlab') && parts.length >= 1) {
|
|
2622
|
+
return this.normalizeRepoConfig({
|
|
2623
|
+
provider: 'gitlab',
|
|
2624
|
+
host: `${parsed.protocol}//${parsed.host}`,
|
|
2625
|
+
projectPath: parts.join('/'),
|
|
2626
|
+
branch,
|
|
2627
|
+
repoUrl: value
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
return null;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
async _readManifestFromRepo(repo, directory, candidates) {
|
|
2635
|
+
for (const candidate of candidates) {
|
|
2636
|
+
try {
|
|
2637
|
+
return await this.fetchRepoJson(repo, joinRepoPath(directory, candidate));
|
|
2638
|
+
} catch {
|
|
2639
|
+
// try next candidate
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
return null;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
async _fetchClaudeMarketplacePlugins(repo, marketplace, readJson) {
|
|
2646
|
+
if (!marketplace || !Array.isArray(marketplace.plugins)) return [];
|
|
2647
|
+
const results = [];
|
|
2648
|
+
for (const plugin of marketplace.plugins) {
|
|
2649
|
+
const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
|
|
2650
|
+
const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
|
|
2651
|
+
const manifest = canReadManifest
|
|
2652
|
+
? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
|
|
2653
|
+
'.claude-plugin/plugin.json',
|
|
2654
|
+
'plugin.json',
|
|
2655
|
+
'package.json'
|
|
2656
|
+
])
|
|
2657
|
+
: null;
|
|
2658
|
+
results.push(this.buildMarketPluginItem(repo, {
|
|
2659
|
+
repo: sourceInfo.repo,
|
|
2660
|
+
name: manifest?.name || plugin.name,
|
|
2661
|
+
description: manifest?.description || plugin.description || '',
|
|
2662
|
+
author: manifest?.author || plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
2663
|
+
version: manifest?.version || plugin.version || '1.0.0',
|
|
2664
|
+
category: plugin.category || 'general',
|
|
2665
|
+
directory: sourceInfo.directory || plugin.name,
|
|
2666
|
+
marketplace: marketplace.name || repo.marketplace || '',
|
|
2667
|
+
installSource: sourceInfo.directory ? '' : sourceInfo.installSource,
|
|
2668
|
+
marketplaceFormat: 'claude-marketplace',
|
|
2669
|
+
lspServers: plugin.lspServers || null,
|
|
2670
|
+
commands: manifest?.commands || [],
|
|
2671
|
+
hooks: manifest?.hooks || []
|
|
2672
|
+
}));
|
|
2673
|
+
}
|
|
2674
|
+
return results;
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2677
|
+
async _fetchCodexMarketplacePlugins(repo, marketplace) {
|
|
2678
|
+
if (!marketplace || !Array.isArray(marketplace.plugins)) return [];
|
|
2679
|
+
const marketplaceName = this._resolveCodexMarketplaceName(repo, marketplace.name || marketplace.interface?.displayName || repo.marketplace);
|
|
2680
|
+
const results = [];
|
|
2681
|
+
for (const plugin of marketplace.plugins) {
|
|
2682
|
+
const sourceInfo = this._repoFromMarketplaceSource(repo, plugin.source);
|
|
2683
|
+
const canReadManifest = sourceInfo.repo.provider === 'local' || sourceInfo.repo.id === repo.id;
|
|
2684
|
+
const manifest = canReadManifest
|
|
2685
|
+
? await this._readManifestFromRepo(sourceInfo.repo, sourceInfo.directory, [
|
|
2686
|
+
'.codex-plugin/plugin.json',
|
|
2687
|
+
'plugin.json',
|
|
2688
|
+
'package.json'
|
|
2689
|
+
])
|
|
2690
|
+
: null;
|
|
2691
|
+
results.push(this.buildMarketPluginItem(repo, {
|
|
2692
|
+
repo: sourceInfo.repo,
|
|
2693
|
+
name: manifest?.name || plugin.name,
|
|
2694
|
+
description: manifest?.description || plugin.description || '',
|
|
2695
|
+
author: manifest?.author || plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
2696
|
+
version: manifest?.version || plugin.version || '1.0.0',
|
|
2697
|
+
category: plugin.category || 'general',
|
|
2698
|
+
directory: sourceInfo.directory || plugin.name,
|
|
2699
|
+
marketplace: marketplaceName,
|
|
2700
|
+
installSource: sourceInfo.directory ? '' : sourceInfo.installSource,
|
|
2701
|
+
marketplaceFormat: 'codex-marketplace',
|
|
2702
|
+
commands: manifest?.commands || [],
|
|
2703
|
+
hooks: manifest?.hooks || []
|
|
2704
|
+
}));
|
|
2705
|
+
}
|
|
2706
|
+
return results;
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2032
2709
|
/**
|
|
2033
2710
|
* Get market plugins from configured repositories
|
|
2034
2711
|
* @returns {Promise<Array>} List of available market plugins
|
|
2035
2712
|
*/
|
|
2036
2713
|
async getMarketPlugins(forceRefresh = false) {
|
|
2714
|
+
if (!this.getCapabilities().market) {
|
|
2715
|
+
return [];
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2037
2718
|
if (forceRefresh) {
|
|
2038
2719
|
this.clearMarketCache({ removeFile: false });
|
|
2039
2720
|
}
|
|
@@ -2074,25 +2755,24 @@ class PluginsService {
|
|
|
2074
2755
|
return this.fetchRepoJson(repo, normalizedPath, file);
|
|
2075
2756
|
};
|
|
2076
2757
|
|
|
2077
|
-
//
|
|
2078
|
-
if (fileMap.has('.claude-plugin/marketplace.json')) {
|
|
2758
|
+
// Claude official marketplace format
|
|
2759
|
+
if (this.platform === 'claude' && fileMap.has('.claude-plugin/marketplace.json')) {
|
|
2079
2760
|
const marketplace = await readJson('.claude-plugin/marketplace.json');
|
|
2080
2761
|
if (marketplace && marketplace.plugins) {
|
|
2081
|
-
|
|
2082
|
-
marketPlugins.push(this.buildMarketPluginItem(repo, {
|
|
2083
|
-
name: plugin.name,
|
|
2084
|
-
description: plugin.description || '',
|
|
2085
|
-
author: plugin.author?.name || marketplace.owner?.name || repo.owner,
|
|
2086
|
-
version: plugin.version || '1.0.0',
|
|
2087
|
-
category: plugin.category || 'general',
|
|
2088
|
-
directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
|
|
2089
|
-
lspServers: plugin.lspServers || null
|
|
2090
|
-
}));
|
|
2091
|
-
}
|
|
2762
|
+
marketPlugins.push(...await this._fetchClaudeMarketplacePlugins(repo, marketplace, readJson));
|
|
2092
2763
|
continue; // Skip legacy format check
|
|
2093
2764
|
}
|
|
2094
2765
|
}
|
|
2095
2766
|
|
|
2767
|
+
// Codex marketplace format generated by plugin-creator
|
|
2768
|
+
if (this._isCodex() && fileMap.has('.agents/plugins/marketplace.json')) {
|
|
2769
|
+
const marketplace = await readJson('.agents/plugins/marketplace.json');
|
|
2770
|
+
if (marketplace && marketplace.plugins) {
|
|
2771
|
+
marketPlugins.push(...await this._fetchCodexMarketplacePlugins(repo, marketplace));
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
|
|
2096
2776
|
// OpenCode plugin marketplace format: plugins/*.plugin.json
|
|
2097
2777
|
if (this._isOpenCode()) {
|
|
2098
2778
|
const openCodeMarketplacePlugins = await this._fetchOpenCodeMarketplacePlugins(repo, repo.branch || 'main');
|
|
@@ -2102,6 +2782,34 @@ class PluginsService {
|
|
|
2102
2782
|
}
|
|
2103
2783
|
}
|
|
2104
2784
|
|
|
2785
|
+
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');
|
|
2788
|
+
return false;
|
|
2789
|
+
});
|
|
2790
|
+
const nestedDirs = new Set();
|
|
2791
|
+
for (const file of nestedManifestFiles) {
|
|
2792
|
+
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);
|
|
2796
|
+
const manifest = await readJson(file.path);
|
|
2797
|
+
marketPlugins.push(this.buildMarketPluginItem(repo, {
|
|
2798
|
+
name: manifest.name || dir.split('/').pop(),
|
|
2799
|
+
description: manifest.description || '',
|
|
2800
|
+
author: manifest.author || repo.owner,
|
|
2801
|
+
version: manifest.version || '1.0.0',
|
|
2802
|
+
directory: dir,
|
|
2803
|
+
marketplace: this._isCodex() ? this._resolveCodexMarketplaceName(repo) : '',
|
|
2804
|
+
marketplaceFormat: this._isCodex() ? 'codex-manifest' : 'claude-manifest',
|
|
2805
|
+
commands: manifest.commands || [],
|
|
2806
|
+
hooks: manifest.hooks || []
|
|
2807
|
+
}));
|
|
2808
|
+
}
|
|
2809
|
+
if (nestedDirs.size > 0) {
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2105
2813
|
// Legacy format: each directory is a plugin with plugin.json/package.json
|
|
2106
2814
|
const pluginDirs = Array.from(new Set(
|
|
2107
2815
|
files
|