coding-tool-x 3.5.9 → 3.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +4 -25
  3. package/bin/ctx.js +1 -6
  4. package/dist/web/assets/{Analytics-C5W3axXs.js → Analytics-DPb1zErn.js} +2 -2
  5. package/dist/web/assets/Analytics-RNn1BUbG.css +1 -0
  6. package/dist/web/assets/{ConfigTemplates-DzyVFDx9.js → ConfigTemplates-Bki2k4Lo.js} +1 -1
  7. package/dist/web/assets/{Home-C9TQNB6f.js → Home-BKYHKSax.js} +1 -1
  8. package/dist/web/assets/Home-BQxQ1LhR.css +1 -0
  9. package/dist/web/assets/{PluginManager-9B_brLWT.js → PluginManager-jgeQOE24.js} +1 -1
  10. package/dist/web/assets/ProjectList-CF67RzP_.js +1 -0
  11. package/dist/web/assets/ProjectList-DL4JK6ci.css +1 -0
  12. package/dist/web/assets/SessionList-DhHsY5q9.js +1 -0
  13. package/dist/web/assets/SessionList-lz23U6ZJ.css +1 -0
  14. package/dist/web/assets/{SkillManager-vST8DRRg.js → SkillManager-CtEz5gGp.js} +1 -1
  15. package/dist/web/assets/{WorkspaceManager-ov1KgRXR.js → WorkspaceManager-D3B0Y5XE.js} +1 -1
  16. package/dist/web/assets/icons-lKCTySl7.js +1 -0
  17. package/dist/web/assets/index-BS7aplYp.css +1 -0
  18. package/dist/web/assets/index-BnyNm74G.js +2 -0
  19. package/dist/web/assets/{markdown-DyTJGI4N.js → markdown-BNJfEgCA.js} +1 -1
  20. package/dist/web/assets/{naive-ui-Cg4_ZeoT.js → naive-ui-DM897mIP.js} +1 -1
  21. package/dist/web/assets/vendors-DQA4Hw9v.js +7 -0
  22. package/dist/web/assets/vue-vendor-B5RqjEjQ.js +45 -0
  23. package/dist/web/index.html +8 -8
  24. package/package.json +1 -2
  25. package/src/commands/daemon.js +5 -33
  26. package/src/commands/export-config.js +6 -6
  27. package/src/commands/stats.js +4 -41
  28. package/src/commands/ui.js +3 -12
  29. package/src/config/channel-preset-websites.js +114 -0
  30. package/src/config/default.js +9 -5
  31. package/src/config/loader.js +2 -2
  32. package/src/config/model-metadata.json +16 -20
  33. package/src/config/paths.js +33 -166
  34. package/src/index.js +34 -125
  35. package/src/server/api/agents.js +2 -52
  36. package/src/server/api/codex-proxy.js +4 -2
  37. package/src/server/api/codex-sessions.js +89 -12
  38. package/src/server/api/commands.js +2 -38
  39. package/src/server/api/dashboard.js +1 -25
  40. package/src/server/api/gemini-proxy.js +4 -2
  41. package/src/server/api/gemini-sessions.js +86 -9
  42. package/src/server/api/hooks.js +1 -17
  43. package/src/server/api/opencode-proxy.js +7 -2
  44. package/src/server/api/opencode-sessions.js +83 -10
  45. package/src/server/api/plugins.js +34 -128
  46. package/src/server/api/proxy.js +5 -3
  47. package/src/server/api/sessions.js +181 -27
  48. package/src/server/api/workspaces.js +0 -14
  49. package/src/server/index.js +9 -31
  50. package/src/server/services/agents-service.js +62 -269
  51. package/src/server/services/base/base-channel-service.js +3 -1
  52. package/src/server/services/channels.js +5 -0
  53. package/src/server/services/codex-channels.js +2 -0
  54. package/src/server/services/codex-sessions.js +78 -108
  55. package/src/server/services/commands-service.js +81 -281
  56. package/src/server/services/config-export-service.js +7 -7
  57. package/src/server/services/config-registry-service.js +9 -8
  58. package/src/server/services/config-sync-manager.js +41 -61
  59. package/src/server/services/config-sync-service.js +3 -3
  60. package/src/server/services/gemini-channels.js +17 -6
  61. package/src/server/services/gemini-config.js +4 -3
  62. package/src/server/services/gemini-sessions.js +51 -26
  63. package/src/server/services/gemini-settings-manager.js +3 -2
  64. package/src/server/services/mcp-service.js +14 -9
  65. package/src/server/services/native-oauth-adapters.js +3 -3
  66. package/src/server/services/network-access.js +2 -61
  67. package/src/server/services/notification-hooks.js +19 -184
  68. package/src/server/services/opencode-channels.js +9 -0
  69. package/src/server/services/opencode-sessions.js +6 -16
  70. package/src/server/services/opencode-settings-manager.js +3 -3
  71. package/src/server/services/plugins-service.js +62 -996
  72. package/src/server/services/prompts-service.js +9 -5
  73. package/src/server/services/sessions.js +161 -183
  74. package/src/server/services/skill-service.js +18 -155
  75. package/src/server/websocket-server.js +4 -35
  76. package/dist/web/assets/Analytics-vQS5IWvs.css +0 -1
  77. package/dist/web/assets/Home-qzk118Of.css +0 -1
  78. package/dist/web/assets/ProjectList-Bjt6mrsV.js +0 -1
  79. package/dist/web/assets/ProjectList-GCC2QOmq.css +0 -1
  80. package/dist/web/assets/SessionList-BsHPgmUR.css +0 -1
  81. package/dist/web/assets/SessionList-DcBH13uA.js +0 -1
  82. package/dist/web/assets/icons-CEq2hYB-.js +0 -1
  83. package/dist/web/assets/index-Dih_bOsv.css +0 -1
  84. package/dist/web/assets/index-Duc7QP4e.js +0 -2
  85. package/dist/web/assets/vendors-Bsp-dq2d.js +0 -7
  86. package/dist/web/assets/vue-vendor-BxIT0uQq.js +0 -45
  87. package/docs/Caddyfile.example +0 -19
  88. package/docs/reverse-proxy-https.md +0 -57
  89. package/src/server/services/https-cert.js +0 -171
  90. package/src/server/services/session-launch-command.js +0 -58
  91. package/src/server/services/web-ui-runtime.js +0 -54
@@ -18,15 +18,10 @@ 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 = 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;
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;
30
25
  const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
31
26
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
32
27
  const DEFAULT_GITHUB_HOST = 'https://github.com';
@@ -35,15 +30,6 @@ const DEFAULT_REPOS_BY_PLATFORM = {
35
30
  claude: [],
36
31
  opencode: []
37
32
  };
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
- ]);
47
33
 
48
34
  function cloneRepos(repos = []) {
49
35
  return repos.map(repo => ({ ...repo }));
@@ -222,538 +208,17 @@ function stripJsonComments(input = '') {
222
208
  return result;
223
209
  }
224
210
 
225
- function isPlainObject(value) {
226
- return !!value && typeof value === 'object' && !Array.isArray(value);
227
- }
228
-
229
- function getNestedValue(source, segments = []) {
230
- return segments.reduce((acc, segment) => (acc && acc[segment] !== undefined ? acc[segment] : undefined), source);
231
- }
232
-
233
- function normalizeContentText(value = '') {
234
- return String(value || '').trim();
235
- }
236
-
237
- function normalizeContentEntry(raw, fallback = {}) {
238
- if (typeof raw === 'string') {
239
- const name = normalizeContentText(raw);
240
- if (!name) return null;
241
- return {
242
- name,
243
- path: normalizeRepoPath(fallback.path || ''),
244
- description: normalizeContentText(fallback.description || '')
245
- };
246
- }
247
-
248
- if (!isPlainObject(raw)) {
249
- return null;
250
- }
251
-
252
- const candidateName = [
253
- raw.name,
254
- raw.displayName,
255
- raw.id,
256
- raw.title,
257
- raw.fileName,
258
- raw.directory,
259
- raw.path,
260
- fallback.name
261
- ].map(normalizeContentText).find(Boolean) || '';
262
- const itemPath = normalizeRepoPath(raw.path || raw.file || raw.fileName || raw.directory || fallback.path || '');
263
- const description = normalizeContentText(raw.description || raw.prompt || raw.usage || fallback.description || '');
264
- const derivedName = candidateName || (itemPath ? path.posix.basename(itemPath, path.posix.extname(itemPath)) : '');
265
-
266
- if (!derivedName && !itemPath) {
267
- return null;
268
- }
269
-
270
- return {
271
- name: derivedName,
272
- path: itemPath,
273
- description
274
- };
275
- }
276
-
277
- function normalizeContentCollection(value, fallback = {}) {
278
- if (Array.isArray(value)) {
279
- return value.map(item => normalizeContentEntry(item, fallback)).filter(Boolean);
280
- }
281
-
282
- if (isPlainObject(value)) {
283
- return Object.entries(value)
284
- .map(([key, item]) => {
285
- if (isPlainObject(item)) {
286
- return normalizeContentEntry({
287
- ...item,
288
- name: item.name || key
289
- }, fallback);
290
- }
291
-
292
- if (typeof item === 'string') {
293
- return normalizeContentEntry({
294
- name: key,
295
- description: item
296
- }, fallback);
297
- }
298
-
299
- return normalizeContentEntry({ name: key }, fallback);
300
- })
301
- .filter(Boolean);
302
- }
303
-
304
- return [];
305
- }
306
-
307
- function mergeContentCollections(...collections) {
308
- const merged = [];
309
- const seen = new Set();
310
-
311
- for (const collection of collections) {
312
- for (const rawEntry of collection || []) {
313
- const entry = normalizeContentEntry(rawEntry);
314
- if (!entry) continue;
315
-
316
- const normalizedPath = normalizeRepoPath(entry.path || '');
317
- const normalizedName = normalizeContentText(entry.name || '');
318
- const key = normalizedPath
319
- ? `path:${normalizedPath.toLowerCase()}`
320
- : (normalizedName ? `name:${normalizedName.toLowerCase()}` : '');
321
-
322
- if (!key || seen.has(key)) {
323
- continue;
324
- }
325
-
326
- seen.add(key);
327
- merged.push({
328
- name: normalizedName || path.posix.basename(normalizedPath, path.posix.extname(normalizedPath)),
329
- path: normalizedPath,
330
- description: normalizeContentText(entry.description || '')
331
- });
332
- }
333
- }
334
-
335
- return merged.sort((a, b) => {
336
- const left = a.name || a.path;
337
- const right = b.name || b.path;
338
- return left.localeCompare(right);
339
- });
340
- }
341
-
342
211
  class PluginsService {
343
212
  constructor(platform = 'claude') {
344
213
  this.platform = ['claude', 'opencode'].includes(platform) ? platform : 'claude';
345
214
  this.configDir = PATHS.config || path.join((PATHS.base || process.env.HOME || os.homedir()), 'config');
346
215
  this.ccToolConfigDir = path.dirname(PATHS.pluginRepos.claude);
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;
216
+ this.opencodePluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugins');
217
+ this.opencodeLegacyPluginsDir = path.join(OPENCODE_CONFIG_DIR, 'plugin');
352
218
  this.marketCachePath = this.platform === 'opencode'
353
219
  ? PATHS.pluginMarketCache.opencode
354
220
  : PATHS.pluginMarketCache.claude;
355
221
  this._marketCache = null;
356
- this._ensureDir(this.storageDir);
357
- }
358
-
359
- _createEmptyContentSummary() {
360
- return {
361
- skills: [],
362
- agents: [],
363
- commands: [],
364
- hooks: [],
365
- files: [],
366
- totalFiles: 0,
367
- additionalFileCount: 0
368
- };
369
- }
370
-
371
- _getManifestCollection(manifest = null, paths = []) {
372
- if (!isPlainObject(manifest)) {
373
- return [];
374
- }
375
-
376
- return mergeContentCollections(
377
- ...paths.map(segments => normalizeContentCollection(getNestedValue(manifest, segments)))
378
- );
379
- }
380
-
381
- _summarizeManifestContent(manifest = null) {
382
- const emptySummary = this._createEmptyContentSummary();
383
- if (!isPlainObject(manifest)) {
384
- return emptySummary;
385
- }
386
-
387
- return {
388
- ...emptySummary,
389
- skills: this._getManifestCollection(manifest, [
390
- ['skills'],
391
- ['ctx', 'skills'],
392
- ['contributes', 'skills']
393
- ]),
394
- agents: this._getManifestCollection(manifest, [
395
- ['agents'],
396
- ['ctx', 'agents'],
397
- ['contributes', 'agents']
398
- ]),
399
- commands: this._getManifestCollection(manifest, [
400
- ['commands'],
401
- ['ctx', 'commands'],
402
- ['contributes', 'commands']
403
- ]),
404
- hooks: this._getManifestCollection(manifest, [
405
- ['hooks'],
406
- ['ctx', 'hooks'],
407
- ['contributes', 'hooks']
408
- ])
409
- };
410
- }
411
-
412
- _getScopedFileContext(files = [], directory = '') {
413
- const normalizedDirectory = normalizeRepoPath(directory);
414
- const directoryLooksLikeFile = /\.[a-z0-9]+$/i.test(path.posix.basename(normalizedDirectory || ''));
415
- const allFiles = (Array.isArray(files) ? files : [])
416
- .filter(item => item && item.type === 'blob' && item.path)
417
- .map(item => ({
418
- ...item,
419
- path: normalizeRepoPath(item.path)
420
- }))
421
- .sort((a, b) => a.path.localeCompare(b.path));
422
-
423
- let scopedFiles = allFiles;
424
- if (normalizedDirectory) {
425
- if (directoryLooksLikeFile) {
426
- scopedFiles = allFiles.filter(item => item.path === normalizedDirectory);
427
- } else {
428
- const directoryPrefix = `${normalizedDirectory}/`;
429
- scopedFiles = allFiles.filter(item => item.path.startsWith(directoryPrefix));
430
- }
431
- }
432
-
433
- const toScopedPath = (filePath = '') => {
434
- const normalizedFilePath = normalizeRepoPath(filePath);
435
- if (!normalizedDirectory || directoryLooksLikeFile) {
436
- return normalizedFilePath;
437
- }
438
- const prefix = `${normalizedDirectory}/`;
439
- return normalizedFilePath.startsWith(prefix) ? normalizedFilePath.slice(prefix.length) : normalizedFilePath;
440
- };
441
-
442
- return {
443
- normalizedDirectory,
444
- directoryLooksLikeFile,
445
- allFiles,
446
- scopedFiles,
447
- toScopedPath
448
- };
449
- }
450
-
451
- _summarizeContentFromFiles(files = [], directory = '') {
452
- const emptySummary = this._createEmptyContentSummary();
453
- const { scopedFiles, toScopedPath } = this._getScopedFileContext(files, directory);
454
-
455
- const skillEntries = [];
456
- const agentEntries = [];
457
- const commandEntries = [];
458
-
459
- for (const file of scopedFiles) {
460
- const scopedPath = toScopedPath(file.path);
461
- const skillMatch = scopedPath.match(/^skills\/([^/]+)\/SKILL\.md$/i);
462
- if (skillMatch) {
463
- skillEntries.push({ name: skillMatch[1], path: scopedPath });
464
- }
465
-
466
- const agentMatch = scopedPath.match(/^agents\/(.+)\.md$/i);
467
- if (agentMatch) {
468
- agentEntries.push({
469
- name: agentMatch[1].split('/').pop(),
470
- path: scopedPath
471
- });
472
- }
473
-
474
- const commandMatch = scopedPath.match(/^(?:commands|prompts)\/(.+)\.md$/i);
475
- if (commandMatch) {
476
- commandEntries.push({
477
- name: commandMatch[1].split('/').pop(),
478
- path: scopedPath
479
- });
480
- }
481
- }
482
-
483
- const filePreview = scopedFiles.slice(0, 8).map(file => {
484
- const scopedPath = toScopedPath(file.path);
485
- return {
486
- name: path.posix.basename(scopedPath),
487
- path: scopedPath
488
- };
489
- });
490
-
491
- return {
492
- ...emptySummary,
493
- skills: mergeContentCollections(skillEntries),
494
- agents: mergeContentCollections(agentEntries),
495
- commands: mergeContentCollections(commandEntries),
496
- files: filePreview,
497
- totalFiles: scopedFiles.length,
498
- additionalFileCount: Math.max(0, scopedFiles.length - filePreview.length)
499
- };
500
- }
501
-
502
- _mergeContentSummary(...summaries) {
503
- const emptySummary = this._createEmptyContentSummary();
504
- const validSummaries = summaries.filter(Boolean);
505
- const fileSummary = validSummaries.find(summary => Array.isArray(summary.files) && summary.files.length > 0) || emptySummary;
506
-
507
- return {
508
- ...emptySummary,
509
- skills: mergeContentCollections(...validSummaries.map(summary => summary.skills)),
510
- agents: mergeContentCollections(...validSummaries.map(summary => summary.agents)),
511
- commands: mergeContentCollections(...validSummaries.map(summary => summary.commands)),
512
- hooks: mergeContentCollections(...validSummaries.map(summary => summary.hooks)),
513
- files: fileSummary.files || [],
514
- totalFiles: fileSummary.totalFiles || 0,
515
- additionalFileCount: fileSummary.additionalFileCount || 0
516
- };
517
- }
518
-
519
- _scanInstallPath(installPath = '') {
520
- const resolvedPath = String(installPath || '').trim();
521
- if (!resolvedPath || !fs.existsSync(resolvedPath)) {
522
- return [];
523
- }
524
-
525
- try {
526
- const stat = fs.statSync(resolvedPath);
527
- if (stat.isFile()) {
528
- const fileName = path.basename(resolvedPath);
529
- return [{ path: fileName, type: 'blob', name: fileName, size: stat.size }];
530
- }
531
-
532
- const tree = [];
533
- this.scanLocalRepoTree(resolvedPath, resolvedPath, tree);
534
- return tree;
535
- } catch {
536
- return [];
537
- }
538
- }
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
-
731
- _readPluginManifestFromInstallPath(installPath = '') {
732
- const resolvedPath = String(installPath || '').trim();
733
- if (!resolvedPath || !fs.existsSync(resolvedPath)) {
734
- return null;
735
- }
736
-
737
- try {
738
- const stat = fs.statSync(resolvedPath);
739
- if (stat.isFile()) {
740
- if (!resolvedPath.endsWith('.json')) {
741
- return null;
742
- }
743
- return JSON.parse(stripJsonComments(fs.readFileSync(resolvedPath, 'utf8')));
744
- }
745
-
746
- const candidates = ['plugin.json', 'package.json'];
747
- for (const candidate of candidates) {
748
- const manifestPath = path.join(resolvedPath, candidate);
749
- if (!fs.existsSync(manifestPath)) continue;
750
- return JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')));
751
- }
752
- } catch {
753
- return null;
754
- }
755
-
756
- return null;
757
222
  }
758
223
 
759
224
  normalizeRepoConfig(repo = {}) {
@@ -969,10 +434,13 @@ class PluginsService {
969
434
  }
970
435
 
971
436
  _getOpenCodeConfigPath() {
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;
437
+ const jsonc = path.join(OPENCODE_CONFIG_DIR, 'opencode.jsonc');
438
+ const json = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
439
+ const config = path.join(OPENCODE_CONFIG_DIR, 'config.json');
440
+ if (fs.existsSync(jsonc)) return jsonc;
441
+ if (fs.existsSync(json)) return json;
442
+ if (fs.existsSync(config)) return config;
443
+ return json;
976
444
  }
977
445
 
978
446
  _readOpenCodeConfig() {
@@ -1071,137 +539,12 @@ class PluginsService {
1071
539
  return plugins;
1072
540
  }
1073
541
 
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
-
1199
542
  /**
1200
543
  * List all installed plugins with their status
1201
544
  * Reads from Claude Code's native installed_plugins.json
1202
545
  * @returns {Object} { plugins: Array }
1203
546
  */
1204
- listPlugins(options = {}) {
547
+ listPlugins() {
1205
548
  if (this._isOpenCode()) {
1206
549
  const plugins = [];
1207
550
  const seen = new Set();
@@ -1232,7 +575,6 @@ class PluginsService {
1232
575
  }
1233
576
 
1234
577
  const plugins = [];
1235
- const syncManagedLocalPlugins = options.syncManagedLocalPlugins === true;
1236
578
 
1237
579
  // Read Claude Code's installed_plugins.json
1238
580
  if (fs.existsSync(CLAUDE_INSTALLED_FILE)) {
@@ -1302,7 +644,6 @@ class PluginsService {
1302
644
  marketplace,
1303
645
  version: install.version || '1.0.0',
1304
646
  installPath: install.installPath,
1305
- installed: true,
1306
647
  installedAt: install.installedAt,
1307
648
  scope: install.scope,
1308
649
  enabled: enabledState,
@@ -1340,8 +681,6 @@ class PluginsService {
1340
681
  // Ignore legacy registry errors
1341
682
  }
1342
683
 
1343
- this._mergeManagedClaudePlugins(plugins, { syncManagedLocalPlugins });
1344
-
1345
684
  return { plugins };
1346
685
  }
1347
686
 
@@ -1380,183 +719,7 @@ class PluginsService {
1380
719
  description: manifest?.description || '',
1381
720
  author: manifest?.author || '',
1382
721
  commands: manifest?.commands || [],
1383
- hooks: manifest?.hooks || manifest?.ctx?.hooks || [],
1384
- manifest
1385
- };
1386
- }
1387
-
1388
- resolvePluginRepo(plugin = {}) {
1389
- let repo = null;
1390
-
1391
- if (plugin.repoProvider || plugin.repoLocalPath || plugin.repoProjectPath || plugin.repoOwner) {
1392
- try {
1393
- repo = this.normalizeRepoConfig({
1394
- id: plugin.repoId,
1395
- provider: plugin.repoProvider,
1396
- host: plugin.repoHost,
1397
- owner: plugin.repoOwner,
1398
- name: plugin.repoName,
1399
- branch: plugin.repoBranch || 'main',
1400
- projectPath: plugin.repoProjectPath,
1401
- localPath: plugin.repoLocalPath,
1402
- repoUrl: plugin.repoUrl
1403
- });
1404
- } catch {
1405
- repo = null;
1406
- }
1407
- } else if (plugin.source) {
1408
- repo = this.parseRepoTreeSource(plugin.source);
1409
- }
1410
-
1411
- if (!repo && plugin.repoUrl) {
1412
- const parsedTreeSource = this.parseRepoTreeSource(plugin.repoUrl);
1413
- if (parsedTreeSource) {
1414
- repo = parsedTreeSource;
1415
- } else if (plugin.repoUrl.includes('github.com')) {
1416
- const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
1417
- if (match) {
1418
- try {
1419
- repo = this.normalizeRepoConfig({
1420
- provider: 'github',
1421
- owner: match[1],
1422
- name: match[2],
1423
- branch: plugin.repoBranch || 'main'
1424
- });
1425
- } catch {
1426
- repo = null;
1427
- }
1428
- }
1429
- }
1430
- }
1431
-
1432
- return repo;
1433
- }
1434
-
1435
- async _readPluginManifestFromRepo(plugin = {}, repo = null) {
1436
- const resolvedRepo = repo || this.resolvePluginRepo(plugin);
1437
- if (!resolvedRepo) {
1438
- return null;
1439
- }
1440
-
1441
- const normalizedDirectory = normalizeRepoPath(plugin.directory || '');
1442
- const candidates = [];
1443
-
1444
- if (normalizedDirectory) {
1445
- if (/\.[a-z0-9]+$/i.test(path.posix.basename(normalizedDirectory))) {
1446
- candidates.push(normalizedDirectory);
1447
- } else {
1448
- candidates.push(`${normalizedDirectory}/plugin.json`, `${normalizedDirectory}/package.json`);
1449
- }
1450
- }
1451
-
1452
- candidates.push('plugin.json', 'package.json');
1453
-
1454
- for (const candidate of Array.from(new Set(candidates))) {
1455
- try {
1456
- return await this.fetchRepoJson(resolvedRepo, candidate);
1457
- } catch {
1458
- // try next candidate
1459
- }
1460
- }
1461
-
1462
- return null;
1463
- }
1464
-
1465
- async getPluginDetail(pluginInput = {}) {
1466
- const requestedPlugin = typeof pluginInput === 'string'
1467
- ? { name: pluginInput }
1468
- : { ...(pluginInput || {}) };
1469
- const requestedName = String(requestedPlugin.name || '').trim();
1470
-
1471
- if (!requestedName && !requestedPlugin.directory) {
1472
- return null;
1473
- }
1474
-
1475
- const installedPlugin = this.listPlugins().plugins.find(plugin => {
1476
- if (requestedName && (plugin.name === requestedName || plugin.directory === requestedName)) {
1477
- return true;
1478
- }
1479
- if (requestedPlugin.installPath && plugin.installPath === requestedPlugin.installPath) {
1480
- return true;
1481
- }
1482
- return Boolean(
1483
- requestedPlugin.repoId &&
1484
- plugin.repoId === requestedPlugin.repoId &&
1485
- plugin.directory === requestedPlugin.directory
1486
- );
1487
- }) || null;
1488
- const legacyPlugin = (!this._isOpenCode() && requestedName) ? getPlugin(requestedName) : null;
1489
-
1490
- const mergedPlugin = {
1491
- ...requestedPlugin,
1492
- ...(installedPlugin || {}),
1493
- ...(legacyPlugin || {})
1494
- };
1495
- mergedPlugin.name = requestedName || mergedPlugin.name || mergedPlugin.directory || '';
1496
-
1497
- if (!mergedPlugin.name) {
1498
- return null;
1499
- }
1500
-
1501
- const fallbackInstallPath = (!mergedPlugin.installPath && !this._isOpenCode())
1502
- ? path.join(INSTALLED_DIR, mergedPlugin.name)
1503
- : '';
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 : '';
1509
-
1510
- let manifest = this._readPluginManifestFromInstallPath(installPath);
1511
- if (!manifest && resolvedManagedPath) {
1512
- manifest = this._readPluginManifestFromInstallPath(resolvedManagedPath);
1513
- }
1514
- const repo = this.resolvePluginRepo(mergedPlugin);
1515
- if (!manifest) {
1516
- manifest = await this._readPluginManifestFromRepo(mergedPlugin, repo);
1517
- }
1518
-
1519
- let fileSummary = this._createEmptyContentSummary();
1520
- if (installPath && fs.existsSync(installPath)) {
1521
- fileSummary = this._summarizeContentFromFiles(this._scanInstallPath(installPath));
1522
- } else if (resolvedManagedPath) {
1523
- fileSummary = this._summarizeContentFromFiles(this._scanInstallPath(resolvedManagedPath));
1524
- } else if (repo) {
1525
- try {
1526
- const repoTree = await this.fetchRepoTree(repo);
1527
- fileSummary = this._summarizeContentFromFiles(repoTree, mergedPlugin.directory || '');
1528
- } catch {
1529
- fileSummary = this._createEmptyContentSummary();
1530
- }
1531
- }
1532
-
1533
- const manifestSummary = this._summarizeManifestContent(manifest);
1534
- const contentSummary = this._mergeContentSummary(manifestSummary, fileSummary);
1535
- const commands = mergeContentCollections(
1536
- contentSummary.commands,
1537
- normalizeContentCollection(mergedPlugin.commands)
1538
- );
1539
- const hooks = mergeContentCollections(
1540
- contentSummary.hooks,
1541
- normalizeContentCollection(mergedPlugin.hooks)
1542
- );
1543
- const installed = mergedPlugin.installed !== undefined
1544
- ? !!mergedPlugin.installed
1545
- : Boolean(mergedPlugin.isInstalled || installPath || installedPlugin);
1546
-
1547
- return {
1548
- ...mergedPlugin,
1549
- installPath,
1550
- managedPath: resolvedManagedPath,
1551
- installed,
1552
- description: mergedPlugin.description || manifest?.description || '',
1553
- author: mergedPlugin.author || manifest?.author || '',
1554
- version: mergedPlugin.version || manifest?.version || (this._isOpenCode() ? 'latest' : '1.0.0'),
1555
- commands,
1556
- hooks,
1557
- skills: contentSummary.skills,
1558
- agents: contentSummary.agents,
1559
- contentSummary,
722
+ hooks: manifest?.hooks || [],
1560
723
  manifest
1561
724
  };
1562
725
  }
@@ -1610,124 +773,6 @@ class PluginsService {
1610
773
  return await installPluginCore(source);
1611
774
  }
1612
775
 
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
-
1731
776
  /**
1732
777
  * Install plugin from repo directory
1733
778
  * @private
@@ -2115,8 +1160,9 @@ class PluginsService {
2115
1160
  */
2116
1161
  updatePluginConfig(name, config) {
2117
1162
  if (this._isOpenCode()) {
2118
- this._ensureDir(OPENCODE_PLUGINS_CONFIG_DIR);
2119
- const configFile = path.join(OPENCODE_PLUGINS_CONFIG_DIR, `${name}.json`);
1163
+ const configDir = path.join(OPENCODE_CONFIG_DIR, 'plugins-config');
1164
+ this._ensureDir(configDir);
1165
+ const configFile = path.join(configDir, `${name}.json`);
2120
1166
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf8');
2121
1167
  return {
2122
1168
  success: true,
@@ -2606,8 +1652,7 @@ class PluginsService {
2606
1652
  tree.push({ path: relativePath, type: 'tree', name: entry.name });
2607
1653
  this.scanLocalRepoTree(fullPath, repoRoot, tree);
2608
1654
  } else {
2609
- const stats = fs.statSync(fullPath);
2610
- tree.push({ path: relativePath, type: 'blob', name: entry.name, size: stats.size });
1655
+ tree.push({ path: relativePath, type: 'blob', name: entry.name });
2611
1656
  }
2612
1657
  }
2613
1658
  }
@@ -2847,7 +1892,44 @@ class PluginsService {
2847
1892
  }
2848
1893
  }
2849
1894
 
2850
- const repo = this.resolvePluginRepo(plugin);
1895
+ let repo = null;
1896
+ if (plugin.repoProvider || plugin.repoLocalPath || plugin.repoProjectPath || plugin.repoOwner) {
1897
+ try {
1898
+ repo = this.normalizeRepoConfig({
1899
+ id: plugin.repoId,
1900
+ provider: plugin.repoProvider,
1901
+ host: plugin.repoHost,
1902
+ owner: plugin.repoOwner,
1903
+ name: plugin.repoName,
1904
+ branch: plugin.repoBranch || 'main',
1905
+ projectPath: plugin.repoProjectPath,
1906
+ localPath: plugin.repoLocalPath,
1907
+ repoUrl: plugin.repoUrl
1908
+ });
1909
+ } catch {
1910
+ repo = null;
1911
+ }
1912
+ } else if (plugin.source) {
1913
+ repo = this.parseRepoTreeSource(plugin.source);
1914
+ }
1915
+
1916
+ if (!repo && plugin.repoUrl) {
1917
+ const parsedTreeSource = this.parseRepoTreeSource(plugin.repoUrl);
1918
+ if (parsedTreeSource) {
1919
+ repo = parsedTreeSource;
1920
+ } else if (plugin.repoUrl.includes('github.com')) {
1921
+ const match = plugin.repoUrl.match(/github\.com\/([^\/]+)\/([^\/\.]+)/);
1922
+ if (match) {
1923
+ repo = this.normalizeRepoConfig({
1924
+ provider: 'github',
1925
+ owner: match[1],
1926
+ name: match[2],
1927
+ branch: plugin.repoBranch || 'main'
1928
+ });
1929
+ }
1930
+ }
1931
+ }
1932
+
2851
1933
  if (!repo) return '';
2852
1934
 
2853
1935
  for (const candidate of readmeCandidates) {
@@ -2898,7 +1980,6 @@ class PluginsService {
2898
1980
  lspServers: data.lspServers || null,
2899
1981
  commands: data.commands || [],
2900
1982
  hooks: data.hooks || [],
2901
- contentSummary: data.contentSummary || this._createEmptyContentSummary(),
2902
1983
  isInstalled: false
2903
1984
  };
2904
1985
  }
@@ -2938,11 +2019,7 @@ class PluginsService {
2938
2019
  directory: file.path,
2939
2020
  installSource: githubRepo ? '' : installSource,
2940
2021
  marketplaceFormat: 'opencode-plugin-json',
2941
- repoUrl,
2942
- contentSummary: this._mergeContentSummary(
2943
- this._summarizeManifestContent(manifest),
2944
- this._summarizeContentFromFiles(tree, file.path)
2945
- )
2022
+ repoUrl
2946
2023
  });
2947
2024
  })
2948
2025
  );
@@ -3009,11 +2086,7 @@ class PluginsService {
3009
2086
  version: plugin.version || '1.0.0',
3010
2087
  category: plugin.category || 'general',
3011
2088
  directory: plugin.source?.replace(/^\.\//, '') || plugin.name,
3012
- lspServers: plugin.lspServers || null,
3013
- contentSummary: this._summarizeContentFromFiles(
3014
- files,
3015
- plugin.source?.replace(/^\.\//, '') || plugin.name
3016
- )
2089
+ lspServers: plugin.lspServers || null
3017
2090
  }));
3018
2091
  }
3019
2092
  continue; // Skip legacy format check
@@ -3039,8 +2112,6 @@ class PluginsService {
3039
2112
  for (const dir of pluginDirs) {
3040
2113
  try {
3041
2114
  const manifest = await readJson(`${dir}/plugin.json`);
3042
- const manifestSummary = this._summarizeManifestContent(manifest);
3043
- const fileSummary = this._summarizeContentFromFiles(files, dir);
3044
2115
 
3045
2116
  marketPlugins.push(this.buildMarketPluginItem(repo, {
3046
2117
  name: manifest.name || dir,
@@ -3048,9 +2119,8 @@ class PluginsService {
3048
2119
  author: manifest.author || repo.owner,
3049
2120
  version: manifest.version || '1.0.0',
3050
2121
  directory: dir,
3051
- commands: manifestSummary.commands,
3052
- hooks: manifestSummary.hooks,
3053
- contentSummary: this._mergeContentSummary(manifestSummary, fileSummary)
2122
+ commands: manifest.commands || [],
2123
+ hooks: manifest.hooks || []
3054
2124
  }));
3055
2125
  } catch (e) {
3056
2126
  // OpenCode 仓库常见 package.json 格式
@@ -3062,11 +2132,7 @@ class PluginsService {
3062
2132
  description: pkg.description || '',
3063
2133
  author: pkg.author || repo.owner,
3064
2134
  version: pkg.version || '1.0.0',
3065
- directory: dir,
3066
- contentSummary: this._mergeContentSummary(
3067
- this._summarizeManifestContent(pkg),
3068
- this._summarizeContentFromFiles(files, dir)
3069
- )
2135
+ directory: dir
3070
2136
  }));
3071
2137
  } catch (pkgErr) {
3072
2138
  // neither plugin.json nor package.json