coding-tool-x 3.5.6 → 3.5.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/README.md +17 -0
- package/bin/ctx.js +6 -1
- package/dist/web/assets/{Analytics-CRNCHeui.js → Analytics-BzoNzfbi.js} +2 -2
- package/dist/web/assets/Analytics-vQS5IWvs.css +1 -0
- package/dist/web/assets/{ConfigTemplates-C0erJdo2.js → ConfigTemplates-O4ikBt1o.js} +1 -1
- package/dist/web/assets/{Home-CL5z6Q4d.js → Home-BQjsnblU.js} +1 -1
- package/dist/web/assets/Home-qzk118Of.css +1 -0
- package/dist/web/assets/{PluginManager-hDx0XMO_.js → PluginManager-DS_DJnVc.js} +1 -1
- package/dist/web/assets/ProjectList-CqYDtsHx.js +1 -0
- package/dist/web/assets/ProjectList-GCC2QOmq.css +1 -0
- package/dist/web/assets/SessionList-CfPtcq6Y.css +1 -0
- package/dist/web/assets/SessionList-DMlLtMCz.js +1 -0
- package/dist/web/assets/{SkillManager-D6Vwpajh.js → SkillManager-DpNE02r0.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C3TjeOPy.js → WorkspaceManager-DMY7_SHh.js} +1 -1
- package/dist/web/assets/icons-CEq2hYB-.js +1 -0
- package/dist/web/assets/index-Clf0l3wc.js +2 -0
- package/dist/web/assets/index-Dih_bOsv.css +1 -0
- package/dist/web/assets/{naive-ui-BaTCPPL5.js → naive-ui-Cg4_ZeoT.js} +1 -1
- package/dist/web/assets/{vendors-Fza9uSYn.js → vendors-Bsp-dq2d.js} +1 -1
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +45 -0
- package/dist/web/index.html +7 -7
- package/docs/Caddyfile.example +19 -0
- package/docs/reverse-proxy-https.md +57 -0
- package/package.json +2 -1
- package/src/commands/daemon.js +33 -5
- package/src/commands/export-config.js +6 -6
- package/src/commands/ui.js +12 -3
- package/src/config/default.js +2 -6
- package/src/config/loader.js +2 -2
- package/src/config/paths.js +166 -33
- package/src/index.js +124 -34
- package/src/server/api/agents.js +52 -2
- package/src/server/api/commands.js +38 -2
- package/src/server/api/plugins.js +104 -1
- package/src/server/api/sessions.js +5 -5
- package/src/server/index.js +25 -5
- package/src/server/services/agents-service.js +269 -62
- package/src/server/services/commands-service.js +281 -81
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +4 -5
- package/src/server/services/config-sync-manager.js +61 -41
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +5 -5
- package/src/server/services/gemini-config.js +3 -4
- package/src/server/services/gemini-sessions.js +23 -20
- package/src/server/services/gemini-settings-manager.js +2 -3
- package/src/server/services/https-cert.js +171 -0
- package/src/server/services/mcp-service.js +9 -14
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/network-access.js +47 -2
- package/src/server/services/notification-hooks.js +11 -5
- package/src/server/services/opencode-sessions.js +4 -4
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +499 -23
- package/src/server/services/prompts-service.js +5 -9
- package/src/server/services/sessions.js +2 -2
- package/src/server/services/skill-service.js +155 -18
- package/src/server/services/web-ui-runtime.js +54 -0
- package/src/server/websocket-server.js +11 -4
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
- package/dist/web/assets/ProjectList-BNsz96av.js +0 -1
- package/dist/web/assets/ProjectList-DL4JK6ci.css +0 -1
- package/dist/web/assets/SessionList-B8dXVXfi.css +0 -1
- package/dist/web/assets/SessionList-CG1UhFo3.js +0 -1
- package/dist/web/assets/icons-CQuif85v.js +0 -1
- package/dist/web/assets/index-GuER-BmS.js +0 -2
- package/dist/web/assets/index-VGAxnLqi.css +0 -1
- package/dist/web/assets/vue-vendor-aWwwFAao.js +0 -45
|
@@ -18,10 +18,15 @@ const { INSTALLED_DIR, CONFIG_DIR } = require('../../plugins/constants');
|
|
|
18
18
|
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
19
19
|
const { maskToken } = require('./oauth-utils');
|
|
20
20
|
|
|
21
|
-
const CLAUDE_PLUGINS_DIR =
|
|
22
|
-
const CLAUDE_INSTALLED_FILE =
|
|
23
|
-
const CLAUDE_MARKETPLACES_FILE =
|
|
24
|
-
const
|
|
21
|
+
const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins;
|
|
22
|
+
const CLAUDE_INSTALLED_FILE = NATIVE_PATHS.claude.installedPlugins;
|
|
23
|
+
const CLAUDE_MARKETPLACES_FILE = NATIVE_PATHS.claude.pluginMarketplaces;
|
|
24
|
+
const OPENCODE_PLUGINS_DIR = NATIVE_PATHS.opencode.plugins;
|
|
25
|
+
const OPENCODE_LEGACY_PLUGINS_DIR = NATIVE_PATHS.opencode.pluginsLegacy;
|
|
26
|
+
const OPENCODE_CONFIG_JSONC = NATIVE_PATHS.opencode.configJsonc;
|
|
27
|
+
const OPENCODE_CONFIG_JSON = NATIVE_PATHS.opencode.configJson;
|
|
28
|
+
const OPENCODE_CONFIG_LEGACY = NATIVE_PATHS.opencode.configLegacy;
|
|
29
|
+
const OPENCODE_PLUGINS_CONFIG_DIR = NATIVE_PATHS.opencode.pluginsConfig;
|
|
25
30
|
const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
|
|
26
31
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
27
32
|
const DEFAULT_GITHUB_HOST = 'https://github.com';
|
|
@@ -30,6 +35,15 @@ const DEFAULT_REPOS_BY_PLATFORM = {
|
|
|
30
35
|
claude: [],
|
|
31
36
|
opencode: []
|
|
32
37
|
};
|
|
38
|
+
const MAX_PLUGIN_FILE_PREVIEW_BYTES = 256 * 1024;
|
|
39
|
+
const TEXT_PREVIEW_EXTENSIONS = new Set([
|
|
40
|
+
'.md', '.txt', '.json', '.jsonc', '.js', '.cjs', '.mjs', '.ts', '.jsx', '.tsx', '.vue',
|
|
41
|
+
'.css', '.scss', '.sass', '.less', '.html', '.xml', '.svg', '.py', '.sh', '.bash', '.zsh',
|
|
42
|
+
'.yaml', '.yml', '.toml', '.ini', '.conf', '.cfg', '.env', '.sample', '.gitignore', '.npmignore'
|
|
43
|
+
]);
|
|
44
|
+
const TEXT_PREVIEW_BASENAMES = new Set([
|
|
45
|
+
'README', 'README.md', 'readme.md', 'LICENSE', 'Dockerfile', 'Makefile', 'SKILL.md'
|
|
46
|
+
]);
|
|
33
47
|
|
|
34
48
|
function cloneRepos(repos = []) {
|
|
35
49
|
return repos.map(repo => ({ ...repo }));
|
|
@@ -330,12 +344,16 @@ class PluginsService {
|
|
|
330
344
|
this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
|
|
331
345
|
this.configDir = PATHS.config || path.join((PATHS.base || process.env.HOME || os.homedir()), 'config');
|
|
332
346
|
this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
|
|
333
|
-
this.
|
|
334
|
-
|
|
347
|
+
this.storageDir = this.platform === 'opencode'
|
|
348
|
+
? PATHS.localPlugins.opencode
|
|
349
|
+
: PATHS.localPlugins.claude;
|
|
350
|
+
this.opencodePluginsDir = OPENCODE_PLUGINS_DIR;
|
|
351
|
+
this.opencodeLegacyPluginsDir = OPENCODE_LEGACY_PLUGINS_DIR;
|
|
335
352
|
this.marketCachePath = this.platform === 'opencode'
|
|
336
353
|
? PATHS.pluginMarketCache.opencode
|
|
337
354
|
: PATHS.pluginMarketCache.claude;
|
|
338
355
|
this._marketCache = null;
|
|
356
|
+
this._ensureDir(this.storageDir);
|
|
339
357
|
}
|
|
340
358
|
|
|
341
359
|
_createEmptyContentSummary() {
|
|
@@ -391,8 +409,7 @@ class PluginsService {
|
|
|
391
409
|
};
|
|
392
410
|
}
|
|
393
411
|
|
|
394
|
-
|
|
395
|
-
const emptySummary = this._createEmptyContentSummary();
|
|
412
|
+
_getScopedFileContext(files = [], directory = '') {
|
|
396
413
|
const normalizedDirectory = normalizeRepoPath(directory);
|
|
397
414
|
const directoryLooksLikeFile = /\.[a-z0-9]+$/i.test(path.posix.basename(normalizedDirectory || ''));
|
|
398
415
|
const allFiles = (Array.isArray(files) ? files : [])
|
|
@@ -414,12 +431,26 @@ class PluginsService {
|
|
|
414
431
|
}
|
|
415
432
|
|
|
416
433
|
const toScopedPath = (filePath = '') => {
|
|
434
|
+
const normalizedFilePath = normalizeRepoPath(filePath);
|
|
417
435
|
if (!normalizedDirectory || directoryLooksLikeFile) {
|
|
418
|
-
return
|
|
436
|
+
return normalizedFilePath;
|
|
419
437
|
}
|
|
420
438
|
const prefix = `${normalizedDirectory}/`;
|
|
421
|
-
return
|
|
439
|
+
return normalizedFilePath.startsWith(prefix) ? normalizedFilePath.slice(prefix.length) : normalizedFilePath;
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
normalizedDirectory,
|
|
444
|
+
directoryLooksLikeFile,
|
|
445
|
+
allFiles,
|
|
446
|
+
scopedFiles,
|
|
447
|
+
toScopedPath
|
|
422
448
|
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
_summarizeContentFromFiles(files = [], directory = '') {
|
|
452
|
+
const emptySummary = this._createEmptyContentSummary();
|
|
453
|
+
const { scopedFiles, toScopedPath } = this._getScopedFileContext(files, directory);
|
|
423
454
|
|
|
424
455
|
const skillEntries = [];
|
|
425
456
|
const agentEntries = [];
|
|
@@ -495,7 +526,7 @@ class PluginsService {
|
|
|
495
526
|
const stat = fs.statSync(resolvedPath);
|
|
496
527
|
if (stat.isFile()) {
|
|
497
528
|
const fileName = path.basename(resolvedPath);
|
|
498
|
-
return [{ path: fileName, type: 'blob', name: fileName }];
|
|
529
|
+
return [{ path: fileName, type: 'blob', name: fileName, size: stat.size }];
|
|
499
530
|
}
|
|
500
531
|
|
|
501
532
|
const tree = [];
|
|
@@ -506,6 +537,197 @@ class PluginsService {
|
|
|
506
537
|
}
|
|
507
538
|
}
|
|
508
539
|
|
|
540
|
+
_isTextPreviewFile(filePath = '') {
|
|
541
|
+
const normalizedPath = normalizeRepoPath(filePath);
|
|
542
|
+
const baseName = path.posix.basename(normalizedPath);
|
|
543
|
+
const extension = path.posix.extname(baseName).toLowerCase();
|
|
544
|
+
if (TEXT_PREVIEW_EXTENSIONS.has(extension)) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
if (TEXT_PREVIEW_BASENAMES.has(baseName)) {
|
|
548
|
+
return true;
|
|
549
|
+
}
|
|
550
|
+
return /^\.env(\..+)?$/i.test(baseName);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
_bufferLooksBinary(buffer) {
|
|
554
|
+
if (!Buffer.isBuffer(buffer)) return false;
|
|
555
|
+
const sampleLength = Math.min(buffer.length, 1024);
|
|
556
|
+
for (let index = 0; index < sampleLength; index += 1) {
|
|
557
|
+
if (buffer[index] === 0) {
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async _resolvePluginFileSource(pluginInput = {}) {
|
|
565
|
+
const detail = await this.getPluginDetail(pluginInput);
|
|
566
|
+
if (!detail) {
|
|
567
|
+
throw new Error('插件不存在');
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (detail.installPath && fs.existsSync(detail.installPath)) {
|
|
571
|
+
const files = this._scanInstallPath(detail.installPath);
|
|
572
|
+
return {
|
|
573
|
+
detail,
|
|
574
|
+
installPath: detail.installPath,
|
|
575
|
+
managedPath: detail.managedPath || '',
|
|
576
|
+
repo: null,
|
|
577
|
+
fileContext: this._getScopedFileContext(files)
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (detail.managedPath && fs.existsSync(detail.managedPath)) {
|
|
582
|
+
const files = this._scanInstallPath(detail.managedPath);
|
|
583
|
+
return {
|
|
584
|
+
detail,
|
|
585
|
+
installPath: '',
|
|
586
|
+
managedPath: detail.managedPath,
|
|
587
|
+
repo: null,
|
|
588
|
+
fileContext: this._getScopedFileContext(files)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const repo = this.resolvePluginRepo(detail);
|
|
593
|
+
if (!repo) {
|
|
594
|
+
return {
|
|
595
|
+
detail,
|
|
596
|
+
installPath: '',
|
|
597
|
+
managedPath: detail.managedPath || '',
|
|
598
|
+
repo: null,
|
|
599
|
+
fileContext: this._getScopedFileContext([], detail.directory || '')
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const repoTree = await this.fetchRepoTree(repo);
|
|
604
|
+
return {
|
|
605
|
+
detail,
|
|
606
|
+
installPath: '',
|
|
607
|
+
repo,
|
|
608
|
+
fileContext: this._getScopedFileContext(repoTree, detail.directory || '')
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async getPluginFiles(pluginInput = {}) {
|
|
613
|
+
const source = await this._resolvePluginFileSource(pluginInput);
|
|
614
|
+
return {
|
|
615
|
+
files: source.fileContext.scopedFiles.map(file => {
|
|
616
|
+
const scopedPath = source.fileContext.toScopedPath(file.path);
|
|
617
|
+
return {
|
|
618
|
+
name: path.posix.basename(scopedPath),
|
|
619
|
+
path: scopedPath,
|
|
620
|
+
size: Number.isFinite(file.size) ? file.size : null,
|
|
621
|
+
isText: this._isTextPreviewFile(scopedPath)
|
|
622
|
+
};
|
|
623
|
+
})
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async getPluginFileContent(pluginInput = {}, filePath = '') {
|
|
628
|
+
const normalizedPath = normalizeRepoPath(filePath);
|
|
629
|
+
if (!normalizedPath) {
|
|
630
|
+
throw new Error('请指定文件路径');
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const source = await this._resolvePluginFileSource(pluginInput);
|
|
634
|
+
const targetFile = source.fileContext.scopedFiles.find(file =>
|
|
635
|
+
source.fileContext.toScopedPath(file.path) === normalizedPath
|
|
636
|
+
);
|
|
637
|
+
|
|
638
|
+
if (!targetFile) {
|
|
639
|
+
throw new Error(`文件 "${normalizedPath}" 不存在`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const scopedPath = source.fileContext.toScopedPath(targetFile.path);
|
|
643
|
+
const declaredSize = Number.isFinite(targetFile.size) ? targetFile.size : null;
|
|
644
|
+
|
|
645
|
+
if (!this._isTextPreviewFile(scopedPath)) {
|
|
646
|
+
return {
|
|
647
|
+
path: scopedPath,
|
|
648
|
+
size: declaredSize,
|
|
649
|
+
content: '',
|
|
650
|
+
isBinary: true,
|
|
651
|
+
isBase64: false,
|
|
652
|
+
tooLarge: false
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (declaredSize && declaredSize > MAX_PLUGIN_FILE_PREVIEW_BYTES) {
|
|
657
|
+
return {
|
|
658
|
+
path: scopedPath,
|
|
659
|
+
size: declaredSize,
|
|
660
|
+
content: '',
|
|
661
|
+
isBinary: false,
|
|
662
|
+
isBase64: false,
|
|
663
|
+
tooLarge: true
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
let content = '';
|
|
668
|
+
let size = declaredSize;
|
|
669
|
+
|
|
670
|
+
if (source.installPath || source.managedPath) {
|
|
671
|
+
const fileRoot = source.installPath || source.managedPath;
|
|
672
|
+
const resolvedRoot = path.resolve(fileRoot);
|
|
673
|
+
const resolvedPath = path.resolve(fileRoot, targetFile.path);
|
|
674
|
+
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
675
|
+
throw new Error('文件路径无效');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const stats = fs.statSync(resolvedPath);
|
|
679
|
+
if (stats.size > MAX_PLUGIN_FILE_PREVIEW_BYTES) {
|
|
680
|
+
return {
|
|
681
|
+
path: scopedPath,
|
|
682
|
+
size: stats.size,
|
|
683
|
+
content: '',
|
|
684
|
+
isBinary: false,
|
|
685
|
+
isBase64: false,
|
|
686
|
+
tooLarge: true
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const buffer = fs.readFileSync(resolvedPath);
|
|
691
|
+
if (this._bufferLooksBinary(buffer)) {
|
|
692
|
+
return {
|
|
693
|
+
path: scopedPath,
|
|
694
|
+
size: stats.size,
|
|
695
|
+
content: '',
|
|
696
|
+
isBinary: true,
|
|
697
|
+
isBase64: false,
|
|
698
|
+
tooLarge: false
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
content = buffer.toString('utf8');
|
|
703
|
+
size = stats.size;
|
|
704
|
+
} else if (source.repo) {
|
|
705
|
+
content = await this.fetchRepoFileContent(source.repo, targetFile.path, targetFile);
|
|
706
|
+
size = Buffer.byteLength(content, 'utf8');
|
|
707
|
+
if (size > MAX_PLUGIN_FILE_PREVIEW_BYTES) {
|
|
708
|
+
return {
|
|
709
|
+
path: scopedPath,
|
|
710
|
+
size,
|
|
711
|
+
content: '',
|
|
712
|
+
isBinary: false,
|
|
713
|
+
isBase64: false,
|
|
714
|
+
tooLarge: true
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
} else {
|
|
718
|
+
throw new Error('当前插件没有可读取的文件源');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return {
|
|
722
|
+
path: scopedPath,
|
|
723
|
+
size,
|
|
724
|
+
content,
|
|
725
|
+
isBinary: false,
|
|
726
|
+
isBase64: false,
|
|
727
|
+
tooLarge: false
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
509
731
|
_readPluginManifestFromInstallPath(installPath = '') {
|
|
510
732
|
const resolvedPath = String(installPath || '').trim();
|
|
511
733
|
if (!resolvedPath || !fs.existsSync(resolvedPath)) {
|
|
@@ -747,13 +969,10 @@ class PluginsService {
|
|
|
747
969
|
}
|
|
748
970
|
|
|
749
971
|
_getOpenCodeConfigPath() {
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (fs.existsSync(json)) return json;
|
|
755
|
-
if (fs.existsSync(config)) return config;
|
|
756
|
-
return json;
|
|
972
|
+
if (fs.existsSync(OPENCODE_CONFIG_JSONC)) return OPENCODE_CONFIG_JSONC;
|
|
973
|
+
if (fs.existsSync(OPENCODE_CONFIG_JSON)) return OPENCODE_CONFIG_JSON;
|
|
974
|
+
if (fs.existsSync(OPENCODE_CONFIG_LEGACY)) return OPENCODE_CONFIG_LEGACY;
|
|
975
|
+
return OPENCODE_CONFIG_JSON;
|
|
757
976
|
}
|
|
758
977
|
|
|
759
978
|
_readOpenCodeConfig() {
|
|
@@ -852,12 +1071,137 @@ class PluginsService {
|
|
|
852
1071
|
return plugins;
|
|
853
1072
|
}
|
|
854
1073
|
|
|
1074
|
+
_normalizeManagedPluginStorageName(plugin = {}) {
|
|
1075
|
+
const rawName = String(plugin.name || path.basename(plugin.installPath || plugin.directory || '') || '').trim();
|
|
1076
|
+
if (!rawName) return '';
|
|
1077
|
+
return rawName.replace(/[\\/]+/g, '__');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
_getManagedPluginPath(plugin = {}) {
|
|
1081
|
+
const storageName = this._normalizeManagedPluginStorageName(plugin);
|
|
1082
|
+
return storageName ? path.join(this.storageDir, storageName) : '';
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
_ensureManagedPluginCopy(plugin = {}, options = {}) {
|
|
1086
|
+
const overwrite = options.overwrite === true;
|
|
1087
|
+
const sourcePath = String(plugin.installPath || '').trim();
|
|
1088
|
+
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
|
1089
|
+
return false;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const managedPath = this._getManagedPluginPath(plugin);
|
|
1093
|
+
if (!managedPath) {
|
|
1094
|
+
return false;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (fs.existsSync(managedPath)) {
|
|
1098
|
+
if (!overwrite) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
fs.rmSync(managedPath, { recursive: true, force: true });
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
this._ensureDir(path.dirname(managedPath));
|
|
1105
|
+
const stat = fs.statSync(sourcePath);
|
|
1106
|
+
if (stat.isDirectory()) {
|
|
1107
|
+
fs.mkdirSync(managedPath, { recursive: true });
|
|
1108
|
+
this.copyDirRecursive(sourcePath, managedPath);
|
|
1109
|
+
const repoSourceMeta = {
|
|
1110
|
+
repoId: plugin.repoId || '',
|
|
1111
|
+
repoProvider: plugin.repoProvider || '',
|
|
1112
|
+
repoOwner: plugin.repoOwner || '',
|
|
1113
|
+
repoName: plugin.repoName || '',
|
|
1114
|
+
repoBranch: plugin.repoBranch || '',
|
|
1115
|
+
repoDirectory: plugin.repoDirectory || plugin.directory || '',
|
|
1116
|
+
repoHost: plugin.repoHost || '',
|
|
1117
|
+
repoProjectPath: plugin.repoProjectPath || '',
|
|
1118
|
+
repoLocalPath: plugin.repoLocalPath || '',
|
|
1119
|
+
repoUrl: plugin.repoUrl || '',
|
|
1120
|
+
source: plugin.source || ''
|
|
1121
|
+
};
|
|
1122
|
+
this.writeRepoSourceMeta(managedPath, repoSourceMeta);
|
|
1123
|
+
} else {
|
|
1124
|
+
fs.copyFileSync(sourcePath, managedPath);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return true;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
_listManagedClaudePlugins() {
|
|
1131
|
+
if (!fs.existsSync(this.storageDir)) return [];
|
|
1132
|
+
|
|
1133
|
+
const entries = fs.readdirSync(this.storageDir, { withFileTypes: true });
|
|
1134
|
+
const plugins = [];
|
|
1135
|
+
|
|
1136
|
+
for (const entry of entries) {
|
|
1137
|
+
if (!entry.isDirectory() || entry.name.startsWith('.')) {
|
|
1138
|
+
continue;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const managedPath = path.join(this.storageDir, entry.name);
|
|
1142
|
+
const manifest = this._readPluginManifestFromInstallPath(managedPath) || {};
|
|
1143
|
+
const repoSourceMeta = this.readRepoSourceMeta(managedPath) || {};
|
|
1144
|
+
const pluginName = manifest.name || entry.name;
|
|
1145
|
+
|
|
1146
|
+
plugins.push({
|
|
1147
|
+
name: pluginName,
|
|
1148
|
+
version: manifest.version || '1.0.0',
|
|
1149
|
+
description: manifest.description || '',
|
|
1150
|
+
author: manifest.author || '',
|
|
1151
|
+
directory: repoSourceMeta.repoDirectory || pluginName,
|
|
1152
|
+
repoDirectory: repoSourceMeta.repoDirectory || '',
|
|
1153
|
+
source: 'local-managed',
|
|
1154
|
+
installPath: '',
|
|
1155
|
+
managedPath,
|
|
1156
|
+
installed: false,
|
|
1157
|
+
enabled: true,
|
|
1158
|
+
isLocal: true,
|
|
1159
|
+
repoUrl: repoSourceMeta.repoUrl || '',
|
|
1160
|
+
repoProvider: repoSourceMeta.repoProvider || '',
|
|
1161
|
+
repoOwner: repoSourceMeta.repoOwner || '',
|
|
1162
|
+
repoName: repoSourceMeta.repoName || '',
|
|
1163
|
+
repoBranch: repoSourceMeta.repoBranch || 'main',
|
|
1164
|
+
repoHost: repoSourceMeta.repoHost || '',
|
|
1165
|
+
repoProjectPath: repoSourceMeta.repoProjectPath || '',
|
|
1166
|
+
repoLocalPath: repoSourceMeta.repoLocalPath || '',
|
|
1167
|
+
repoId: repoSourceMeta.repoId || ''
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
return plugins;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
_mergeManagedClaudePlugins(plugins = [], options = {}) {
|
|
1175
|
+
const syncManagedLocalPlugins = options.syncManagedLocalPlugins === true;
|
|
1176
|
+
|
|
1177
|
+
for (const installedPlugin of plugins) {
|
|
1178
|
+
if (!installedPlugin.installPath || !fs.existsSync(installedPlugin.installPath)) {
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
this._ensureManagedPluginCopy(installedPlugin, {
|
|
1183
|
+
overwrite: syncManagedLocalPlugins
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const managedPlugins = this._listManagedClaudePlugins();
|
|
1188
|
+
for (const managedPlugin of managedPlugins) {
|
|
1189
|
+
const existing = plugins.find(plugin => plugin.name === managedPlugin.name || plugin.directory === managedPlugin.directory);
|
|
1190
|
+
if (existing) {
|
|
1191
|
+
existing.isLocal = true;
|
|
1192
|
+
existing.managedPath = managedPlugin.managedPath;
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
plugins.push(managedPlugin);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
855
1199
|
/**
|
|
856
1200
|
* List all installed plugins with their status
|
|
857
1201
|
* Reads from Claude Code's native installed_plugins.json
|
|
858
1202
|
* @returns {Object} { plugins: Array }
|
|
859
1203
|
*/
|
|
860
|
-
listPlugins() {
|
|
1204
|
+
listPlugins(options = {}) {
|
|
861
1205
|
if (this._isOpenCode()) {
|
|
862
1206
|
const plugins = [];
|
|
863
1207
|
const seen = new Set();
|
|
@@ -888,6 +1232,7 @@ class PluginsService {
|
|
|
888
1232
|
}
|
|
889
1233
|
|
|
890
1234
|
const plugins = [];
|
|
1235
|
+
const syncManagedLocalPlugins = options.syncManagedLocalPlugins === true;
|
|
891
1236
|
|
|
892
1237
|
// Read Claude Code's installed_plugins.json
|
|
893
1238
|
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
@@ -957,6 +1302,7 @@ class PluginsService {
|
|
|
957
1302
|
marketplace,
|
|
958
1303
|
version: install.version || '1.0.0',
|
|
959
1304
|
installPath: install.installPath,
|
|
1305
|
+
installed: true,
|
|
960
1306
|
installedAt: install.installedAt,
|
|
961
1307
|
scope: install.scope,
|
|
962
1308
|
enabled: enabledState,
|
|
@@ -994,6 +1340,8 @@ class PluginsService {
|
|
|
994
1340
|
// Ignore legacy registry errors
|
|
995
1341
|
}
|
|
996
1342
|
|
|
1343
|
+
this._mergeManagedClaudePlugins(plugins, { syncManagedLocalPlugins });
|
|
1344
|
+
|
|
997
1345
|
return { plugins };
|
|
998
1346
|
}
|
|
999
1347
|
|
|
@@ -1154,8 +1502,15 @@ class PluginsService {
|
|
|
1154
1502
|
? path.join(INSTALLED_DIR, mergedPlugin.name)
|
|
1155
1503
|
: '';
|
|
1156
1504
|
const installPath = mergedPlugin.installPath || (fallbackInstallPath && fs.existsSync(fallbackInstallPath) ? fallbackInstallPath : '');
|
|
1505
|
+
const managedPath = mergedPlugin.managedPath && fs.existsSync(mergedPlugin.managedPath)
|
|
1506
|
+
? mergedPlugin.managedPath
|
|
1507
|
+
: this._getManagedPluginPath(mergedPlugin);
|
|
1508
|
+
const resolvedManagedPath = managedPath && fs.existsSync(managedPath) ? managedPath : '';
|
|
1157
1509
|
|
|
1158
1510
|
let manifest = this._readPluginManifestFromInstallPath(installPath);
|
|
1511
|
+
if (!manifest && resolvedManagedPath) {
|
|
1512
|
+
manifest = this._readPluginManifestFromInstallPath(resolvedManagedPath);
|
|
1513
|
+
}
|
|
1159
1514
|
const repo = this.resolvePluginRepo(mergedPlugin);
|
|
1160
1515
|
if (!manifest) {
|
|
1161
1516
|
manifest = await this._readPluginManifestFromRepo(mergedPlugin, repo);
|
|
@@ -1164,6 +1519,8 @@ class PluginsService {
|
|
|
1164
1519
|
let fileSummary = this._createEmptyContentSummary();
|
|
1165
1520
|
if (installPath && fs.existsSync(installPath)) {
|
|
1166
1521
|
fileSummary = this._summarizeContentFromFiles(this._scanInstallPath(installPath));
|
|
1522
|
+
} else if (resolvedManagedPath) {
|
|
1523
|
+
fileSummary = this._summarizeContentFromFiles(this._scanInstallPath(resolvedManagedPath));
|
|
1167
1524
|
} else if (repo) {
|
|
1168
1525
|
try {
|
|
1169
1526
|
const repoTree = await this.fetchRepoTree(repo);
|
|
@@ -1190,6 +1547,7 @@ class PluginsService {
|
|
|
1190
1547
|
return {
|
|
1191
1548
|
...mergedPlugin,
|
|
1192
1549
|
installPath,
|
|
1550
|
+
managedPath: resolvedManagedPath,
|
|
1193
1551
|
installed,
|
|
1194
1552
|
description: mergedPlugin.description || manifest?.description || '',
|
|
1195
1553
|
author: mergedPlugin.author || manifest?.author || '',
|
|
@@ -1252,6 +1610,124 @@ class PluginsService {
|
|
|
1252
1610
|
return await installPluginCore(source);
|
|
1253
1611
|
}
|
|
1254
1612
|
|
|
1613
|
+
installLocalPlugin(name) {
|
|
1614
|
+
const plugin = this.listPlugins().plugins.find(item => item.name === name || item.directory === name);
|
|
1615
|
+
const managedPath = plugin?.managedPath || '';
|
|
1616
|
+
|
|
1617
|
+
if (!managedPath || !fs.existsSync(managedPath)) {
|
|
1618
|
+
throw new Error(`本地插件 "${name}" 不存在`);
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
if (this._isOpenCode()) {
|
|
1622
|
+
const pluginsDir = this._getOpenCodePluginsDir();
|
|
1623
|
+
this._ensureDir(pluginsDir);
|
|
1624
|
+
const targetPath = path.join(pluginsDir, path.basename(managedPath));
|
|
1625
|
+
|
|
1626
|
+
if (fs.existsSync(targetPath)) {
|
|
1627
|
+
return { success: true, plugin: { name: plugin.name }, message: 'Already installed' };
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
const stat = fs.statSync(managedPath);
|
|
1631
|
+
if (stat.isDirectory()) {
|
|
1632
|
+
fs.mkdirSync(targetPath, { recursive: true });
|
|
1633
|
+
this.copyDirRecursive(managedPath, targetPath);
|
|
1634
|
+
} else {
|
|
1635
|
+
fs.copyFileSync(managedPath, targetPath);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
return {
|
|
1639
|
+
success: true,
|
|
1640
|
+
plugin: { name: plugin.name, version: plugin.version || '1.0.0', description: plugin.description || '' },
|
|
1641
|
+
message: 'Installed successfully'
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
const manifest = this._readPluginManifestFromInstallPath(managedPath) || {};
|
|
1646
|
+
const pluginName = manifest.name || plugin?.name || name;
|
|
1647
|
+
const pluginDir = path.join(INSTALLED_DIR, pluginName);
|
|
1648
|
+
|
|
1649
|
+
if (fs.existsSync(pluginDir)) {
|
|
1650
|
+
return { success: true, plugin: { name: pluginName }, message: 'Already installed' };
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
1654
|
+
this.copyDirRecursive(managedPath, pluginDir);
|
|
1655
|
+
|
|
1656
|
+
const installTimestamp = new Date().toISOString();
|
|
1657
|
+
const repoSourceMeta = this.readRepoSourceMeta(managedPath) || {};
|
|
1658
|
+
const sourceUrl = plugin?.source || repoSourceMeta.source || plugin?.repoUrl || '';
|
|
1659
|
+
|
|
1660
|
+
try {
|
|
1661
|
+
const { addPlugin } = require('../../plugins/registry');
|
|
1662
|
+
addPlugin(pluginName, {
|
|
1663
|
+
version: manifest.version || plugin?.version || '1.0.0',
|
|
1664
|
+
enabled: true,
|
|
1665
|
+
installedAt: installTimestamp,
|
|
1666
|
+
source: sourceUrl
|
|
1667
|
+
});
|
|
1668
|
+
} catch (e) {
|
|
1669
|
+
console.warn('[PluginsService] Legacy registry addPlugin warning:', e.message);
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
this.writeRepoSourceMeta(pluginDir, {
|
|
1673
|
+
repoId: plugin?.repoId || repoSourceMeta.repoId || '',
|
|
1674
|
+
repoProvider: plugin?.repoProvider || repoSourceMeta.repoProvider || '',
|
|
1675
|
+
repoOwner: plugin?.repoOwner || repoSourceMeta.repoOwner || '',
|
|
1676
|
+
repoName: plugin?.repoName || repoSourceMeta.repoName || '',
|
|
1677
|
+
repoBranch: plugin?.repoBranch || repoSourceMeta.repoBranch || 'main',
|
|
1678
|
+
repoDirectory: plugin?.repoDirectory || repoSourceMeta.repoDirectory || plugin?.directory || '',
|
|
1679
|
+
repoHost: plugin?.repoHost || repoSourceMeta.repoHost || '',
|
|
1680
|
+
repoProjectPath: plugin?.repoProjectPath || repoSourceMeta.repoProjectPath || '',
|
|
1681
|
+
repoLocalPath: plugin?.repoLocalPath || repoSourceMeta.repoLocalPath || '',
|
|
1682
|
+
repoUrl: plugin?.repoUrl || repoSourceMeta.repoUrl || '',
|
|
1683
|
+
source: sourceUrl
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
try {
|
|
1687
|
+
this._ensureDir(CLAUDE_PLUGINS_DIR);
|
|
1688
|
+
let nativeData = { plugins: {} };
|
|
1689
|
+
if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
|
|
1690
|
+
try {
|
|
1691
|
+
nativeData = JSON.parse(fs.readFileSync(CLAUDE_INSTALLED_FILE, 'utf8'));
|
|
1692
|
+
if (!nativeData.plugins) nativeData.plugins = {};
|
|
1693
|
+
} catch {
|
|
1694
|
+
nativeData = { plugins: {} };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
nativeData.plugins[`${pluginName}@ctx`] = [{
|
|
1699
|
+
version: manifest.version || plugin?.version || '1.0.0',
|
|
1700
|
+
installPath: pluginDir,
|
|
1701
|
+
installedAt: installTimestamp,
|
|
1702
|
+
scope: 'user',
|
|
1703
|
+
source: sourceUrl,
|
|
1704
|
+
repoId: plugin?.repoId || repoSourceMeta.repoId || '',
|
|
1705
|
+
repoProvider: plugin?.repoProvider || repoSourceMeta.repoProvider || '',
|
|
1706
|
+
repoOwner: plugin?.repoOwner || repoSourceMeta.repoOwner || '',
|
|
1707
|
+
repoName: plugin?.repoName || repoSourceMeta.repoName || '',
|
|
1708
|
+
repoBranch: plugin?.repoBranch || repoSourceMeta.repoBranch || 'main',
|
|
1709
|
+
repoDirectory: plugin?.repoDirectory || repoSourceMeta.repoDirectory || plugin?.directory || '',
|
|
1710
|
+
repoHost: plugin?.repoHost || repoSourceMeta.repoHost || '',
|
|
1711
|
+
repoProjectPath: plugin?.repoProjectPath || repoSourceMeta.repoProjectPath || '',
|
|
1712
|
+
repoLocalPath: plugin?.repoLocalPath || repoSourceMeta.repoLocalPath || '',
|
|
1713
|
+
repoUrl: plugin?.repoUrl || repoSourceMeta.repoUrl || ''
|
|
1714
|
+
}];
|
|
1715
|
+
fs.writeFileSync(CLAUDE_INSTALLED_FILE, JSON.stringify(nativeData, null, 2), 'utf8');
|
|
1716
|
+
} catch (e) {
|
|
1717
|
+
console.error('[PluginsService] Failed to update native installed_plugins.json:', e.message);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
return {
|
|
1721
|
+
success: true,
|
|
1722
|
+
plugin: {
|
|
1723
|
+
name: pluginName,
|
|
1724
|
+
version: manifest.version || plugin?.version || '1.0.0',
|
|
1725
|
+
description: manifest.description || plugin?.description || ''
|
|
1726
|
+
},
|
|
1727
|
+
message: 'Installed successfully'
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1255
1731
|
/**
|
|
1256
1732
|
* Install plugin from repo directory
|
|
1257
1733
|
* @private
|
|
@@ -1639,9 +2115,8 @@ class PluginsService {
|
|
|
1639
2115
|
*/
|
|
1640
2116
|
updatePluginConfig(name, config) {
|
|
1641
2117
|
if (this._isOpenCode()) {
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
const configFile = path.join(configDir, `${name}.json`);
|
|
2118
|
+
this._ensureDir(OPENCODE_PLUGINS_CONFIG_DIR);
|
|
2119
|
+
const configFile = path.join(OPENCODE_PLUGINS_CONFIG_DIR, `${name}.json`);
|
|
1645
2120
|
fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
|
|
1646
2121
|
return {
|
|
1647
2122
|
success: true,
|
|
@@ -2131,7 +2606,8 @@ class PluginsService {
|
|
|
2131
2606
|
tree.push({ path: relativePath, type: 'tree', name: entry.name });
|
|
2132
2607
|
this.scanLocalRepoTree(fullPath, repoRoot, tree);
|
|
2133
2608
|
} else {
|
|
2134
|
-
|
|
2609
|
+
const stats = fs.statSync(fullPath);
|
|
2610
|
+
tree.push({ path: relativePath, type: 'blob', name: entry.name, size: stats.size });
|
|
2135
2611
|
}
|
|
2136
2612
|
}
|
|
2137
2613
|
}
|
|
@@ -6,20 +6,16 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
-
const
|
|
10
|
-
const { NATIVE_PATHS, PATHS } = require('../../config/paths');
|
|
11
|
-
const { resolvePreferredHomeDir } = require('../../utils/home-dir');
|
|
12
|
-
|
|
13
|
-
const HOME_DIR = resolvePreferredHomeDir(process.platform, process.env, os.homedir());
|
|
9
|
+
const { PATHS, getNativePlatformPromptPath } = require('../../config/paths');
|
|
14
10
|
|
|
15
11
|
// Prompts 配置文件路径
|
|
16
12
|
const PROMPTS_FILE = PATHS.prompts;
|
|
17
13
|
|
|
18
14
|
// 各平台提示词文件路径
|
|
19
|
-
const CLAUDE_PROMPT_PATH =
|
|
20
|
-
const CODEX_PROMPT_PATH =
|
|
21
|
-
const GEMINI_PROMPT_PATH =
|
|
22
|
-
const OPENCODE_PROMPT_PATH =
|
|
15
|
+
const CLAUDE_PROMPT_PATH = getNativePlatformPromptPath('claude');
|
|
16
|
+
const CODEX_PROMPT_PATH = getNativePlatformPromptPath('codex');
|
|
17
|
+
const GEMINI_PROMPT_PATH = getNativePlatformPromptPath('gemini');
|
|
18
|
+
const OPENCODE_PROMPT_PATH = getNativePlatformPromptPath('opencode');
|
|
23
19
|
|
|
24
20
|
function normalizeApps(apps = {}, defaults = { claude: true, codex: true, gemini: true, opencode: false }) {
|
|
25
21
|
return {
|
|
@@ -14,8 +14,8 @@ const { globalCache, CacheKeys } = require('./enhanced-cache');
|
|
|
14
14
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
15
15
|
|
|
16
16
|
const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
|
|
17
|
-
const CODEX_PROJECTS_DIR =
|
|
18
|
-
const GEMINI_PROJECTS_DIR =
|
|
17
|
+
const CODEX_PROJECTS_DIR = NATIVE_PATHS.codex.projects;
|
|
18
|
+
const GEMINI_PROJECTS_DIR = NATIVE_PATHS.gemini.projects;
|
|
19
19
|
const PROJECT_PATH_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
20
20
|
const MAX_PROJECT_PATH_CACHE_ENTRIES = 500;
|
|
21
21
|
let projectPathResolutionCache = new Map();
|