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.
Files changed (69) hide show
  1. package/README.md +17 -0
  2. package/bin/ctx.js +6 -1
  3. package/dist/web/assets/{Analytics-CRNCHeui.js → Analytics-BzoNzfbi.js} +2 -2
  4. package/dist/web/assets/Analytics-vQS5IWvs.css +1 -0
  5. package/dist/web/assets/{ConfigTemplates-C0erJdo2.js → ConfigTemplates-O4ikBt1o.js} +1 -1
  6. package/dist/web/assets/{Home-CL5z6Q4d.js → Home-BQjsnblU.js} +1 -1
  7. package/dist/web/assets/Home-qzk118Of.css +1 -0
  8. package/dist/web/assets/{PluginManager-hDx0XMO_.js → PluginManager-DS_DJnVc.js} +1 -1
  9. package/dist/web/assets/ProjectList-CqYDtsHx.js +1 -0
  10. package/dist/web/assets/ProjectList-GCC2QOmq.css +1 -0
  11. package/dist/web/assets/SessionList-CfPtcq6Y.css +1 -0
  12. package/dist/web/assets/SessionList-DMlLtMCz.js +1 -0
  13. package/dist/web/assets/{SkillManager-D6Vwpajh.js → SkillManager-DpNE02r0.js} +1 -1
  14. package/dist/web/assets/{WorkspaceManager-C3TjeOPy.js → WorkspaceManager-DMY7_SHh.js} +1 -1
  15. package/dist/web/assets/icons-CEq2hYB-.js +1 -0
  16. package/dist/web/assets/index-Clf0l3wc.js +2 -0
  17. package/dist/web/assets/index-Dih_bOsv.css +1 -0
  18. package/dist/web/assets/{naive-ui-BaTCPPL5.js → naive-ui-Cg4_ZeoT.js} +1 -1
  19. package/dist/web/assets/{vendors-Fza9uSYn.js → vendors-Bsp-dq2d.js} +1 -1
  20. package/dist/web/assets/vue-vendor-BxIT0uQq.js +45 -0
  21. package/dist/web/index.html +7 -7
  22. package/docs/Caddyfile.example +19 -0
  23. package/docs/reverse-proxy-https.md +57 -0
  24. package/package.json +2 -1
  25. package/src/commands/daemon.js +33 -5
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/ui.js +12 -3
  28. package/src/config/default.js +2 -6
  29. package/src/config/loader.js +2 -2
  30. package/src/config/paths.js +166 -33
  31. package/src/index.js +124 -34
  32. package/src/server/api/agents.js +52 -2
  33. package/src/server/api/commands.js +38 -2
  34. package/src/server/api/plugins.js +104 -1
  35. package/src/server/api/sessions.js +5 -5
  36. package/src/server/index.js +25 -5
  37. package/src/server/services/agents-service.js +269 -62
  38. package/src/server/services/commands-service.js +281 -81
  39. package/src/server/services/config-export-service.js +7 -7
  40. package/src/server/services/config-registry-service.js +4 -5
  41. package/src/server/services/config-sync-manager.js +61 -41
  42. package/src/server/services/config-sync-service.js +3 -3
  43. package/src/server/services/gemini-channels.js +5 -5
  44. package/src/server/services/gemini-config.js +3 -4
  45. package/src/server/services/gemini-sessions.js +23 -20
  46. package/src/server/services/gemini-settings-manager.js +2 -3
  47. package/src/server/services/https-cert.js +171 -0
  48. package/src/server/services/mcp-service.js +9 -14
  49. package/src/server/services/native-oauth-adapters.js +3 -3
  50. package/src/server/services/network-access.js +47 -2
  51. package/src/server/services/notification-hooks.js +11 -5
  52. package/src/server/services/opencode-sessions.js +4 -4
  53. package/src/server/services/opencode-settings-manager.js +3 -3
  54. package/src/server/services/plugins-service.js +499 -23
  55. package/src/server/services/prompts-service.js +5 -9
  56. package/src/server/services/sessions.js +2 -2
  57. package/src/server/services/skill-service.js +155 -18
  58. package/src/server/services/web-ui-runtime.js +54 -0
  59. package/src/server/websocket-server.js +11 -4
  60. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  61. package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
  62. package/dist/web/assets/ProjectList-BNsz96av.js +0 -1
  63. package/dist/web/assets/ProjectList-DL4JK6ci.css +0 -1
  64. package/dist/web/assets/SessionList-B8dXVXfi.css +0 -1
  65. package/dist/web/assets/SessionList-CG1UhFo3.js +0 -1
  66. package/dist/web/assets/icons-CQuif85v.js +0 -1
  67. package/dist/web/assets/index-GuER-BmS.js +0 -2
  68. package/dist/web/assets/index-VGAxnLqi.css +0 -1
  69. 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 = path.join(path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
22
- const CLAUDE_INSTALLED_FILE = path.join(CLAUDE_PLUGINS_DIR, 'installed_plugins.json');
23
- const CLAUDE_MARKETPLACES_FILE = path.join(CLAUDE_PLUGINS_DIR, 'known_marketplaces.json');
24
- const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
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.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
334
- this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
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
- _summarizeContentFromFiles(files = [], directory = '') {
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 normalizeRepoPath(filePath);
436
+ return normalizedFilePath;
419
437
  }
420
438
  const prefix = `${normalizedDirectory}/`;
421
- return filePath.startsWith(prefix) ? filePath.slice(prefix.length) : filePath;
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
- const jsonc = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
751
- const json = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
752
- const config = path.join(OPENCODE_CONFIG_DIR, 'config.json');
753
- if (fs.existsSync(jsonc)) return jsonc;
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
- const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
1643
- this._ensureDir(configDir);
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
- tree.push({ path: relativePath, type: 'blob', name: entry.name });
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 os = require('os');
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 = path.join(HOME_DIR, '.claude', 'CLAUDE.md');
20
- const CODEX_PROMPT_PATH = path.join(HOME_DIR, '.codex', 'AGENTS.md');
21
- const GEMINI_PROMPT_PATH = path.join(HOME_DIR, '.gemini', 'GEMINI.md');
22
- const OPENCODE_PROMPT_PATH = path.join(NATIVE_PATHS.opencode.config, 'AGENTS.md');
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 = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
- const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
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();