coding-tool-x 3.8.9 → 3.8.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 (124) hide show
  1. package/README.md +56 -388
  2. package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
  3. package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
  4. package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
  5. package/dist/web/assets/{ChatHistoryDrawer-BTmQ1aQr.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
  6. package/dist/web/assets/{CollapseItem-CwqJk3bE.js → CollapseItem-CVWojShK.js} +1 -1
  7. package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
  8. package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
  9. package/dist/web/assets/{ConfigTemplates-gyERg9iC.js → ConfigTemplates-BpaWG-iC.js} +1 -1
  10. package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
  11. package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
  12. package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
  13. package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
  14. package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
  15. package/dist/web/assets/Home-BIxWvm29.js +1 -0
  16. package/dist/web/assets/{MarkdownEditor-9TTwTS-U.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
  17. package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
  18. package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
  19. package/dist/web/assets/{PluginManager-BuRcWuBj.js → PluginManager-BGQ0xF2F.js} +1 -1
  20. package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-STwHEqiF.js} +1 -1
  21. package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
  22. package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
  23. package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
  24. package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
  25. package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
  26. package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
  27. package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
  28. package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-PcAa5EbF.js} +1 -1
  29. package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-DPdU4p6C.js} +1 -1
  30. package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
  31. package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
  32. package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
  33. package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-6VvRWe3r.js} +1 -1
  34. package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
  35. package/dist/web/assets/client-BgcNg4Bt.js +1 -0
  36. package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
  37. package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
  38. package/dist/web/assets/index-BlUXw84U.js +2 -0
  39. package/dist/web/assets/index-Bn8BUnwP.css +1 -0
  40. package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
  41. package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
  42. package/dist/web/assets/platforms-mqqe_82d.js +1 -0
  43. package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
  44. package/dist/web/assets/sessions-53-Fr68H.js +1 -0
  45. package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
  46. package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
  47. package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
  48. package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
  49. package/dist/web/index.html +8 -8
  50. package/package.json +4 -2
  51. package/src/commands/export-config.js +17 -211
  52. package/src/config/model-metadata.json +180 -16
  53. package/src/platforms/channel-auth-service.js +1 -1
  54. package/src/platforms/commands-service.js +28 -0
  55. package/src/platforms/driver-registry.js +1 -0
  56. package/src/platforms/drivers/claude/channels-implementation.js +79 -90
  57. package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
  58. package/src/platforms/drivers/codex/channels-implementation.js +34 -49
  59. package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
  60. package/src/platforms/drivers/dsh/api-operations.js +1 -1
  61. package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
  62. package/src/platforms/drivers/dsh/channels.js +0 -18
  63. package/src/platforms/drivers/dsh/common.js +7 -0
  64. package/src/platforms/drivers/dsh/native-config.js +7 -2
  65. package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
  66. package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
  67. package/src/platforms/drivers/generic-native-config.js +20 -0
  68. package/src/platforms/drivers/native-config-snapshot.js +30 -4
  69. package/src/platforms/drivers/omp/channels-implementation.js +51 -17
  70. package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
  71. package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
  72. package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
  73. package/src/platforms/manifest-schema.js +35 -2
  74. package/src/platforms/manifests/claude.json +0 -8
  75. package/src/platforms/manifests/codex.json +0 -8
  76. package/src/platforms/manifests/dsh.json +6 -9
  77. package/src/platforms/manifests/gemini.json +0 -8
  78. package/src/platforms/manifests/omp.json +0 -8
  79. package/src/platforms/manifests/opencode.json +0 -8
  80. package/src/platforms/native-oauth-adapters.js +33 -33
  81. package/src/server/index.js +2 -0
  82. package/src/server/services/bounded-content.js +60 -0
  83. package/src/server/services/channel-balance.js +10 -4
  84. package/src/server/services/config-export-service.js +2 -2
  85. package/src/server/services/config-templates-service.js +5 -1
  86. package/src/server/services/plugins-service.js +162 -85
  87. package/src/server/services/session-history-maintenance-worker.js +71 -0
  88. package/src/server/services/session-history-maintenance.js +166 -0
  89. package/src/server/services/skill-service.js +23 -3
  90. package/src/server/services/speed-test.js +0 -72
  91. package/src/shared/channel-driver.js +16 -0
  92. package/src/shared/driver-factories/api.js +1 -0
  93. package/src/utils/native-config-patcher.js +474 -0
  94. package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
  95. package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
  96. package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
  97. package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
  98. package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
  99. package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
  100. package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
  101. package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
  102. package/dist/web/assets/Home-DOSUTfrE.js +0 -1
  103. package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
  104. package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
  105. package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
  106. package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
  107. package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
  108. package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
  109. package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
  110. package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
  111. package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
  112. package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
  113. package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
  114. package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
  115. package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
  116. package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
  117. package/dist/web/assets/client-UDwXBP9i.js +0 -1
  118. package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
  119. package/dist/web/assets/index-BKlAWTH-.js +0 -2
  120. package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
  121. package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
  122. package/dist/web/assets/project-config-B2baPzYL.js +0 -1
  123. package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
  124. package/dist/web/assets/use-message-DqCwefG6.js +0 -1
@@ -26,6 +26,7 @@ const {
26
26
  const { createApiRequestLogger } = require('./services/request-logger');
27
27
  const { inspectWebBuildState, ensureWebDistReady } = require('./services/web-build');
28
28
  const { ensureHttpsCredentials } = require('./services/https-cert');
29
+ const { scheduleSessionHistoryMaintenance } = require('./services/session-history-maintenance');
29
30
  const notificationHooks = require('../platforms/notification-hooks');
30
31
  const {
31
32
  prepareNativeCliLogObserver,
@@ -323,6 +324,7 @@ async function startServer(port, host = '127.0.0.1', options = {}) {
323
324
  server.once('listening', onListening);
324
325
  server.once('error', onError);
325
326
  });
327
+ scheduleSessionHistoryMaintenance(PATHS.sessionHistoryIndex);
326
328
 
327
329
  console.log(`\n[START] Coding-Tool Web UI running at:`);
328
330
  const protocol = useHttps ? 'https' : 'http';
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+
5
+ function normalizeLimit(maxBytes) {
6
+ const limit = Number(maxBytes);
7
+ if (!Number.isSafeInteger(limit) || limit <= 0) {
8
+ throw new Error('A positive byte limit is required');
9
+ }
10
+ return limit;
11
+ }
12
+
13
+ function truncateUtf8(value, maxBytes) {
14
+ const limit = normalizeLimit(maxBytes);
15
+ const text = String(value ?? '');
16
+ const bytes = Buffer.byteLength(text, 'utf8');
17
+ if (bytes <= limit) {
18
+ return { text, bytes, truncated: false };
19
+ }
20
+ return {
21
+ text: Buffer.from(text, 'utf8').subarray(0, limit).toString('utf8'),
22
+ bytes,
23
+ truncated: true
24
+ };
25
+ }
26
+
27
+ function readTextFileLimited(filePath, maxBytes, fsImpl = fs) {
28
+ const limit = normalizeLimit(maxBytes);
29
+ const totalBytes = fsImpl.statSync(filePath).size;
30
+ if (totalBytes <= limit) {
31
+ return {
32
+ text: fsImpl.readFileSync(filePath, 'utf8'),
33
+ bytes: totalBytes,
34
+ truncated: false
35
+ };
36
+ }
37
+
38
+ const fd = fsImpl.openSync(filePath, 'r');
39
+ try {
40
+ const buffer = Buffer.allocUnsafe(limit);
41
+ let offset = 0;
42
+ while (offset < limit) {
43
+ const bytesRead = fsImpl.readSync(fd, buffer, offset, limit - offset, offset);
44
+ if (!bytesRead) break;
45
+ offset += bytesRead;
46
+ }
47
+ return {
48
+ text: buffer.subarray(0, offset).toString('utf8'),
49
+ bytes: totalBytes,
50
+ truncated: true
51
+ };
52
+ } finally {
53
+ fsImpl.closeSync(fd);
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ readTextFileLimited,
59
+ truncateUtf8
60
+ };
@@ -14,6 +14,7 @@ const {
14
14
  } = require('./snapshot-cache');
15
15
 
16
16
  const CACHE_TTL_MS = 5 * 60 * 1000;
17
+ const OAUTH_QUOTA_CACHE_TTL_MS = 60 * 1000;
17
18
  const REQUEST_TIMEOUT_MS = 8000;
18
19
  const MAX_RESPONSE_BYTES = 1024 * 1024;
19
20
  const QUOTA_UNIT = 500000;
@@ -124,6 +125,10 @@ function isOAuthChannel(channel = {}) {
124
125
  return channel?.authMode === 'oauth';
125
126
  }
126
127
 
128
+ function getBalanceCacheTtlMs(channel = {}) {
129
+ return isOAuthChannel(channel) ? OAUTH_QUOTA_CACHE_TTL_MS : CACHE_TTL_MS;
130
+ }
131
+
127
132
  function resolveOAuthCacheIdentity(channel = {}) {
128
133
  const ref = channel.authRef && typeof channel.authRef === 'object' ? channel.authRef : {};
129
134
  return [
@@ -1581,6 +1586,7 @@ function makeFallbackBalanceSnapshot(currentTime = Date.now()) {
1581
1586
  async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
1582
1587
  const force = options.force === true;
1583
1588
  const currentTime = options.now || Date.now();
1589
+ const cacheTtlMs = getBalanceCacheTtlMs(channel);
1584
1590
  const key = buildCacheKey(source, channel);
1585
1591
  const cached = balanceCache.get(key);
1586
1592
 
@@ -1598,7 +1604,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
1598
1604
  if (snapshot.visible) {
1599
1605
  balanceCache.set(key, {
1600
1606
  snapshot,
1601
- expiresAt: currentTime + CACHE_TTL_MS
1607
+ expiresAt: currentTime + cacheTtlMs
1602
1608
  });
1603
1609
  return snapshot;
1604
1610
  }
@@ -1613,7 +1619,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
1613
1619
  };
1614
1620
  balanceCache.set(key, {
1615
1621
  snapshot: staleSnapshot,
1616
- expiresAt: currentTime + CACHE_TTL_MS
1622
+ expiresAt: currentTime + cacheTtlMs
1617
1623
  });
1618
1624
  return staleSnapshot;
1619
1625
  }
@@ -1621,7 +1627,7 @@ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
1621
1627
  const hiddenSnapshot = makeHiddenSnapshot(source, { updatedAt: nowIso(currentTime) });
1622
1628
  balanceCache.set(key, {
1623
1629
  snapshot: hiddenSnapshot,
1624
- expiresAt: currentTime + CACHE_TTL_MS
1630
+ expiresAt: currentTime + cacheTtlMs
1625
1631
  });
1626
1632
  return hiddenSnapshot;
1627
1633
  }
@@ -1799,7 +1805,7 @@ async function getChannelBalances(source, options = {}) {
1799
1805
  const currentTime = options.now || Date.now();
1800
1806
  const entries = await mapWithConcurrency(channels, 4, async (channel) => {
1801
1807
  const snapshot = await getSnapshot(buildBalanceSnapshotKey(normalizedSource, channel), {
1802
- ttlMs: CACHE_TTL_MS,
1808
+ ttlMs: getBalanceCacheTtlMs(channel),
1803
1809
  fallbackValue: makeFallbackBalanceSnapshot(currentTime),
1804
1810
  refresh: () => refreshChannelBalanceSnapshot(normalizedSource, channel, { ...options, force: true, now: Date.now() })
1805
1811
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 配置导出/导入服务
3
- * 支持配置模板、频道配置的导出与导入
3
+ * 支持配置模板、频道配置及按平台 Manifest 声明的原生配置快照导出与导入
4
4
  */
5
5
 
6
6
  const fs = require('fs');
@@ -517,7 +517,7 @@ function buildExportReadme(exportData) {
517
517
  - 插件 (Plugins)
518
518
  - MCP 服务器配置
519
519
  - OAuth 凭证管理池
520
- - 各平台原生配置(Claude / Codex / Gemini / OpenCode / OMP)
520
+ - 各平台原生配置(由平台 Manifest 的 nativeConfigSnapshot 声明决定)
521
521
  - UI 配置(主题、面板显示、排序等)
522
522
  - Prompts 预设
523
523
  - 安全配置
@@ -429,7 +429,11 @@ function getAvailableConfigs(options = {}) {
429
429
  const { commands } = service.listCommands();
430
430
  for (const command of commands || []) {
431
431
  if (command.scope !== 'user') continue;
432
- const detail = typeof service.getCommand === 'function' ? (service.getCommand(command.name, 'user', null, command.namespace) || command) : command;
432
+ const detail = typeof service.getCommandByPath === 'function' && command.path
433
+ ? (service.getCommandByPath(command.path, 'user') || command)
434
+ : (typeof service.getCommand === 'function'
435
+ ? (service.getCommand(command.name, 'user', null, command.namespace) || command)
436
+ : command);
433
437
  const normalizedCommand = { name: command.name, namespace: command.namespace, description: command.description, allowedTools: command.allowedTools, argumentHint: command.argumentHint, body: detail.body };
434
438
  commandsByPlatform[platform] = commandsByPlatform[platform] || [];
435
439
  commandsByPlatform[platform].push(normalizedCommand);
@@ -36,6 +36,7 @@ const {
36
36
  normalizeSafeRelativePath,
37
37
  resolveInsideRoot
38
38
  } = require('../../shared/config-artifact-paths');
39
+ const { readTextFileLimited, truncateUtf8 } = require('./bounded-content');
39
40
 
40
41
  const CLAUDE_PLUGINS_DIR = NATIVE_PATHS.claude.plugins
41
42
  || path.join(NATIVE_PATHS.claude.dir || path.dirname(NATIVE_PATHS.claude.settings), 'plugins');
@@ -49,6 +50,13 @@ const OPENCODE_CONFIG_DIR = NATIVE_PATHS.opencode.config;
49
50
  const REPO_SOURCE_META_FILE = '.cc-tool-plugin-source.json';
50
51
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
51
52
  const PLUGIN_LIST_TTL_MS = 1000;
53
+ const MAX_PLUGIN_DETAIL_BYTES = 128 * 1024;
54
+ const MAX_PLUGIN_MANIFEST_BYTES = 256 * 1024;
55
+ const MAX_PLUGIN_STATE_BYTES = 4 * 1024 * 1024;
56
+ const MAX_PLUGIN_LIST_CACHE_ENTRIES = 32;
57
+ const MAX_MARKET_CACHE_ENTRIES = 32;
58
+ const MAX_MARKET_README_CACHE_ENTRIES = 32;
59
+ const MAX_MARKET_CACHE_FILE_BYTES = 8 * 1024 * 1024;
52
60
  const DEFAULT_GITHUB_HOST = 'https://github.com';
53
61
  const DEFAULT_GITLAB_HOST = 'https://gitlab.com';
54
62
  const DEFAULT_REPOS_BY_PLATFORM = {
@@ -522,6 +530,7 @@ class PluginsService {
522
530
  this._marketReadmeCache = new Map();
523
531
  this._marketReadmeCacheLoaded = false;
524
532
  this._lastFetchedRepoTrees = new Map();
533
+ this._claudeInstalledPluginIndex = null;
525
534
  this.ompNativeAdapter = this.platform === 'omp'
526
535
  ? new OmpNativePluginAdapter({ pathContext: this.pathContext })
527
536
  : null;
@@ -542,6 +551,47 @@ class PluginsService {
542
551
  }
543
552
  }
544
553
 
554
+ _rememberPluginList(key, value, cachedAt = Date.now()) {
555
+ this._pluginListCache.delete(key);
556
+ this._pluginListCache.set(key, {
557
+ value: this._clone(value),
558
+ cachedAt,
559
+ generation: this._pluginListGeneration
560
+ });
561
+ while (this._pluginListCache.size > MAX_PLUGIN_LIST_CACHE_ENTRIES) {
562
+ this._pluginListCache.delete(this._pluginListCache.keys().next().value);
563
+ }
564
+ }
565
+
566
+ _rememberMarketPlugins(key, plugins, fetchedAt, reposFingerprint) {
567
+ this._marketCacheByKey.delete(key);
568
+ this._marketCacheByKey.set(key, { plugins, fetchedAt, reposFingerprint });
569
+ while (this._marketCacheByKey.size > MAX_MARKET_CACHE_ENTRIES) {
570
+ this._marketCacheByKey.delete(this._marketCacheByKey.keys().next().value);
571
+ }
572
+ }
573
+
574
+ _setMarketReadme(key, value) {
575
+ if (typeof value !== 'string') return '';
576
+ const bounded = truncateUtf8(value, MAX_PLUGIN_DETAIL_BYTES);
577
+ this._marketReadmeCache.delete(key);
578
+ this._marketReadmeCache.set(key, bounded.text);
579
+ while (this._marketReadmeCache.size > MAX_MARKET_README_CACHE_ENTRIES) {
580
+ this._marketReadmeCache.delete(this._marketReadmeCache.keys().next().value);
581
+ }
582
+ return bounded.text;
583
+ }
584
+
585
+ _loadMarketReadmes(readmes = {}) {
586
+ this._marketReadmeCache.clear();
587
+ for (const [key, value] of Object.entries(readmes || {})) {
588
+ this._setMarketReadme(key, value);
589
+ if (this._marketReadmeCache.size >= MAX_MARKET_README_CACHE_ENTRIES) break;
590
+ }
591
+ this._marketReadmeCacheLoaded = true;
592
+ return this._marketReadmeCache;
593
+ }
594
+
545
595
  _invalidatePluginList(options = null) {
546
596
  this._pluginListGeneration++;
547
597
  if (!options) {
@@ -751,7 +801,9 @@ class PluginsService {
751
801
  loadMarketCacheEnvelope() {
752
802
  try {
753
803
  if (!fs.existsSync(this.marketCachePath)) return null;
754
- const data = JSON.parse(fs.readFileSync(this.marketCachePath, 'utf-8'));
804
+ const boundedCache = readTextFileLimited(this.marketCachePath, MAX_MARKET_CACHE_FILE_BYTES);
805
+ if (boundedCache.truncated) return null;
806
+ const data = JSON.parse(boundedCache.text);
755
807
  if (Array.isArray(data)) {
756
808
  return { plugins: data, fetchedAt: 0, reposFingerprint: null, readmes: {} };
757
809
  }
@@ -793,20 +845,25 @@ class PluginsService {
793
845
  _loadMarketReadmeCache() {
794
846
  if (this._marketReadmeCacheLoaded) return this._marketReadmeCache;
795
847
  const envelope = this.loadMarketCacheEnvelope();
796
- this._marketReadmeCache = new Map(Object.entries(envelope?.readmes || {}));
797
- this._marketReadmeCacheLoaded = true;
798
- return this._marketReadmeCache;
848
+ return this._loadMarketReadmes(envelope?.readmes || {});
799
849
  }
800
850
 
801
851
  saveMarketCacheToFile(plugins, fetchedAt = Date.now(), reposFingerprint = null, readmes = null) {
802
852
  try {
803
853
  this._ensureDir(path.dirname(this.marketCachePath));
854
+ const readmeSource = readmes || Object.fromEntries(this._loadMarketReadmeCache());
855
+ const boundedReadmes = {};
856
+ for (const [key, value] of Object.entries(readmeSource || {})) {
857
+ if (Object.keys(boundedReadmes).length >= MAX_MARKET_README_CACHE_ENTRIES) break;
858
+ if (typeof value !== 'string') continue;
859
+ boundedReadmes[key] = truncateUtf8(value, MAX_PLUGIN_DETAIL_BYTES).text;
860
+ }
804
861
  const payload = {
805
862
  fetchedAt,
806
863
  time: fetchedAt,
807
864
  reposFingerprint,
808
865
  plugins,
809
- readmes: readmes || Object.fromEntries(this._loadMarketReadmeCache())
866
+ readmes: boundedReadmes
810
867
  };
811
868
  const tempPath = `${this.marketCachePath}.tmp-${process.pid}-${Date.now()}`;
812
869
  try {
@@ -881,6 +938,25 @@ class PluginsService {
881
938
  return (Array.isArray(plugins) ? plugins : []).map(plugin => this.sanitizePluginListItem(plugin));
882
939
  }
883
940
 
941
+ sanitizePluginDetail(plugin = {}) {
942
+ const sanitized = { ...plugin };
943
+ for (const key of Object.keys(sanitized)) {
944
+ const normalizedKey = key.toLowerCase();
945
+ if (
946
+ normalizedKey === 'content'
947
+ || normalizedKey === 'fullcontent'
948
+ || normalizedKey === 'manifest'
949
+ || normalizedKey === 'rawmanifest'
950
+ || normalizedKey === 'files'
951
+ || normalizedKey === 'filecontents'
952
+ || (normalizedKey.includes('readme') && normalizedKey !== 'readmeurl')
953
+ ) {
954
+ delete sanitized[key];
955
+ }
956
+ }
957
+ return sanitized;
958
+ }
959
+
884
960
  _ensureDir(dirPath) {
885
961
  if (!fs.existsSync(dirPath)) {
886
962
  fs.mkdirSync(dirPath, { recursive: true });
@@ -931,7 +1007,7 @@ class PluginsService {
931
1007
  { allowHiddenSegments: true }
932
1008
  );
933
1009
  if (!fs.existsSync(cachedPath)) return null;
934
- return fs.readFileSync(cachedPath, 'utf8');
1010
+ return readTextFileLimited(cachedPath, MAX_PLUGIN_DETAIL_BYTES).text;
935
1011
  } catch {
936
1012
  return null;
937
1013
  }
@@ -1137,7 +1213,7 @@ class PluginsService {
1137
1213
  _readFirstMarkdownParagraph(filePath) {
1138
1214
  if (!fs.existsSync(filePath)) return '';
1139
1215
  try {
1140
- const content = fs.readFileSync(filePath, 'utf8');
1216
+ const content = readTextFileLimited(filePath, MAX_PLUGIN_DETAIL_BYTES).text;
1141
1217
  const lines = content
1142
1218
  .split(/\r?\n/)
1143
1219
  .map(line => line.trim())
@@ -1207,7 +1283,9 @@ class PluginsService {
1207
1283
  const manifestPath = path.join(rootDir, candidate);
1208
1284
  if (!fs.existsSync(manifestPath)) continue;
1209
1285
  try {
1210
- return JSON.parse(stripJsonComments(fs.readFileSync(manifestPath, 'utf8')));
1286
+ const bounded = readTextFileLimited(manifestPath, MAX_PLUGIN_MANIFEST_BYTES);
1287
+ if (bounded.truncated) continue;
1288
+ return JSON.parse(stripJsonComments(bounded.text));
1211
1289
  } catch {
1212
1290
  // try next candidate
1213
1291
  }
@@ -1254,6 +1332,32 @@ class PluginsService {
1254
1332
  }
1255
1333
  }
1256
1334
 
1335
+ _readClaudeInstalledPluginIndex() {
1336
+ if (!fs.existsSync(this.claudeInstalledFile)) return [];
1337
+ try {
1338
+ const stat = fs.statSync(this.claudeInstalledFile);
1339
+ const signature = `${stat.size}:${stat.mtimeMs}`;
1340
+ if (this._claudeInstalledPluginIndex?.signature === signature) {
1341
+ return this._claudeInstalledPluginIndex.entries;
1342
+ }
1343
+ const boundedState = readTextFileLimited(this.claudeInstalledFile, MAX_PLUGIN_STATE_BYTES);
1344
+ if (boundedState.truncated) return [];
1345
+ const data = JSON.parse(boundedState.text);
1346
+ const entries = Object.keys(data.plugins || {}).map(key => {
1347
+ const parsed = splitPluginMarketplaceKey(key);
1348
+ return { key, name: parsed.name, marketplace: parsed.marketplace || '' };
1349
+ });
1350
+ this._claudeInstalledPluginIndex = { signature, entries };
1351
+ return entries;
1352
+ } catch {
1353
+ return [];
1354
+ }
1355
+ }
1356
+
1357
+ _clearClaudeInstalledPluginIndex() {
1358
+ this._claudeInstalledPluginIndex = null;
1359
+ }
1360
+
1257
1361
  _writeClaudeSettings(settings) {
1258
1362
  const filePath = this.pathContext?.customized
1259
1363
  ? (this.pathContext.native?.settings || NATIVE_PATHS.claude.settings)
@@ -1455,6 +1559,7 @@ class PluginsService {
1455
1559
  ...(installData.repoSourceMeta || {})
1456
1560
  }];
1457
1561
  fs.writeFileSync(this.claudeInstalledFile, JSON.stringify(nativeData, null, 2), 'utf8');
1562
+ this._clearClaudeInstalledPluginIndex();
1458
1563
  this._invalidatePluginList();
1459
1564
  this._setClaudePluginEnabled(name, marketplace, true);
1460
1565
  }
@@ -1533,11 +1638,7 @@ class PluginsService {
1533
1638
  }
1534
1639
 
1535
1640
  const value = this._listPluginsUncached(options);
1536
- this._pluginListCache.set(key, {
1537
- value: this._clone(value),
1538
- cachedAt: now,
1539
- generation: this._pluginListGeneration
1540
- });
1641
+ this._rememberPluginList(key, value, now);
1541
1642
  return this._clone(value);
1542
1643
  }
1543
1644
 
@@ -1675,15 +1776,12 @@ class PluginsService {
1675
1776
  let repoId = install.repoId || '';
1676
1777
 
1677
1778
  if (install.installPath && fs.existsSync(install.installPath)) {
1678
- const manifestPath = path.join(install.installPath, 'plugin.json');
1679
- if (fs.existsSync(manifestPath)) {
1680
- try {
1681
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1682
- description = manifest.description || '';
1683
- } catch (err) {
1684
- // Ignore parse errors
1685
- }
1686
- }
1779
+ const manifest = this._readLocalManifest(install.installPath, [
1780
+ 'plugin.json',
1781
+ '.claude-plugin/plugin.json',
1782
+ 'package.json'
1783
+ ]);
1784
+ description = manifest?.description || '';
1687
1785
 
1688
1786
  const repoSourceMeta = this.readRepoSourceMeta(install.installPath) || {};
1689
1787
  repoUrl = repoUrl || repoSourceMeta.repoUrl || '';
@@ -1762,9 +1860,10 @@ class PluginsService {
1762
1860
  */
1763
1861
  getPlugin(name, options = {}) {
1764
1862
  if (this._isDsh()) {
1765
- return this.listPlugins(options).plugins.find(plugin => (
1863
+ const plugin = this.listPlugins(options).plugins.find(plugin => (
1766
1864
  plugin.key === name || plugin.pluginId === name || plugin.name === name
1767
- )) || null;
1865
+ ));
1866
+ return plugin ? this.sanitizePluginDetail(plugin) : null;
1768
1867
  }
1769
1868
  if (this._isCodex()) {
1770
1869
  const plugin = this.listPlugins().plugins.find(p => p.name === name || `${p.name}@${p.marketplace}` === name);
@@ -1785,7 +1884,7 @@ class PluginsService {
1785
1884
  if (this._isOpenCode()) {
1786
1885
  const plugin = this.listPlugins().plugins.find(p => p.name === name || p.directory === name);
1787
1886
  if (!plugin) return null;
1788
- return plugin;
1887
+ return this.sanitizePluginDetail(plugin);
1789
1888
  }
1790
1889
 
1791
1890
  if (this._isOmp()) {
@@ -1793,7 +1892,7 @@ class PluginsService {
1793
1892
  p.pluginId === name || p.id === name || p.name === name
1794
1893
  );
1795
1894
  if (!plugin) return null;
1796
- return plugin;
1895
+ return this.sanitizePluginDetail(plugin);
1797
1896
  }
1798
1897
 
1799
1898
  const plugin = getPlugin(name);
@@ -1806,16 +1905,12 @@ class PluginsService {
1806
1905
 
1807
1906
  let manifest = null;
1808
1907
  if (fs.existsSync(manifestPath)) {
1809
- try {
1810
- manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
1811
- } catch (err) {
1812
- // Ignore parse errors
1813
- }
1908
+ manifest = this._readLocalManifest(pluginDir, ['plugin.json']);
1814
1909
  }
1815
1910
 
1816
1911
  return {
1817
1912
  name,
1818
- ...plugin,
1913
+ ...this.sanitizePluginDetail(plugin),
1819
1914
  description: manifest?.description || '',
1820
1915
  author: manifest?.author || '',
1821
1916
  commands: manifest?.commands || [],
@@ -2374,6 +2469,7 @@ class PluginsService {
2374
2469
  delete data.plugins[key];
2375
2470
  }
2376
2471
  fs.writeFileSync(this.claudeInstalledFile, JSON.stringify(data, null, 2), 'utf8');
2472
+ this._clearClaudeInstalledPluginIndex();
2377
2473
  this._invalidatePluginList();
2378
2474
  removed = true;
2379
2475
  }
@@ -2457,22 +2553,12 @@ class PluginsService {
2457
2553
 
2458
2554
  // Claude: store enabled state in CTX registry
2459
2555
  // First check if plugin exists in native installed_plugins.json
2460
- let pluginExists = false;
2461
2556
  const baseName = name.split('/').pop();
2462
- if (fs.existsSync(this.claudeInstalledFile)) {
2463
- try {
2464
- const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
2465
- if (data.plugins) {
2466
- for (const key of Object.keys(data.plugins)) {
2467
- const { name: pluginName } = splitPluginMarketplaceKey(key);
2468
- if (pluginName === name || key === name || pluginName === baseName) {
2469
- pluginExists = true;
2470
- break;
2471
- }
2472
- }
2473
- }
2474
- } catch (e) { /* ignore */ }
2475
- }
2557
+ const installedEntries = this._readClaudeInstalledPluginIndex();
2558
+ const installedMatch = installedEntries.find(entry => (
2559
+ entry.name === name || entry.key === name || entry.name === baseName
2560
+ ));
2561
+ let pluginExists = Boolean(installedMatch);
2476
2562
 
2477
2563
  // Also check legacy registry
2478
2564
  const legacyPlugin = getPlugin(name);
@@ -2494,20 +2580,7 @@ class PluginsService {
2494
2580
  } catch (e) {
2495
2581
  console.warn('[PluginsService] Failed to update plugin registry:', e.message);
2496
2582
  }
2497
- if (fs.existsSync(this.claudeInstalledFile)) {
2498
- try {
2499
- const data = JSON.parse(fs.readFileSync(this.claudeInstalledFile, 'utf8'));
2500
- for (const key of Object.keys(data.plugins || {})) {
2501
- const parsed = splitPluginMarketplaceKey(key);
2502
- if (parsed.name === name || key === name || parsed.name === baseName) {
2503
- matchedMarketplace = parsed.marketplace || '';
2504
- break;
2505
- }
2506
- }
2507
- } catch {
2508
- // ignore
2509
- }
2510
- }
2583
+ matchedMarketplace = installedMatch?.marketplace || '';
2511
2584
  this._setClaudePluginEnabled(baseName || name, matchedMarketplace, enabled);
2512
2585
 
2513
2586
  return {
@@ -3309,7 +3382,7 @@ class PluginsService {
3309
3382
  : readmeCandidates.map(candidate => path.join(plugin.installPath, candidate));
3310
3383
  for (const candidatePath of localCandidates) {
3311
3384
  if (fs.existsSync(candidatePath)) {
3312
- return fs.readFileSync(candidatePath, 'utf8');
3385
+ return readTextFileLimited(candidatePath, MAX_PLUGIN_DETAIL_BYTES).text;
3313
3386
  }
3314
3387
  }
3315
3388
  }
@@ -3361,7 +3434,7 @@ class PluginsService {
3361
3434
  for (const candidate of readmeCandidates) {
3362
3435
  const cachedReadme = this._readCachedRepoFile(repo, candidate, { scope, cwd });
3363
3436
  if (cachedReadme !== null) {
3364
- if (cacheRemote) readmeCache.set(this._pluginReadmeCacheKey(plugin), cachedReadme);
3437
+ if (cacheRemote) this._setMarketReadme(this._pluginReadmeCacheKey(plugin), cachedReadme);
3365
3438
  return cachedReadme;
3366
3439
  }
3367
3440
  }
@@ -3371,10 +3444,11 @@ class PluginsService {
3371
3444
  for (const candidate of readmeCandidates) {
3372
3445
  try {
3373
3446
  const readme = await this.fetchRepoFileContent(repo, candidate);
3447
+ const boundedReadme = truncateUtf8(readme, MAX_PLUGIN_DETAIL_BYTES).text;
3374
3448
  if (cacheRemote && typeof readme === 'string') {
3375
- readmeCache.set(this._pluginReadmeCacheKey(plugin), readme);
3449
+ this._setMarketReadme(this._pluginReadmeCacheKey(plugin), boundedReadme);
3376
3450
  }
3377
- return readme;
3451
+ return boundedReadme;
3378
3452
  } catch {
3379
3453
  // try next candidate
3380
3454
  }
@@ -3706,16 +3780,16 @@ class PluginsService {
3706
3780
  const disk = this.loadMarketCacheEnvelope();
3707
3781
  if (!forceRefresh && Array.isArray(disk?.plugins)) {
3708
3782
  const diskPlugins = this.prepareMarketPlugins(disk.plugins);
3709
- this._marketCacheByKey.set(key, {
3710
- plugins: diskPlugins,
3711
- fetchedAt: disk.fetchedAt,
3712
- reposFingerprint: disk.reposFingerprint || reposFingerprint
3713
- });
3783
+ this._rememberMarketPlugins(
3784
+ key,
3785
+ diskPlugins,
3786
+ disk.fetchedAt,
3787
+ disk.reposFingerprint || reposFingerprint
3788
+ );
3714
3789
  this._marketCache = diskPlugins;
3715
3790
  this._marketFetchedAt = disk.fetchedAt;
3716
3791
  this._marketRepoFingerprint = disk.reposFingerprint || reposFingerprint;
3717
- this._marketReadmeCache = new Map(Object.entries(disk.readmes || {}));
3718
- this._marketReadmeCacheLoaded = true;
3792
+ this._loadMarketReadmes(disk.readmes || {});
3719
3793
  return this._clone(diskPlugins);
3720
3794
  }
3721
3795
 
@@ -3729,11 +3803,7 @@ class PluginsService {
3729
3803
  this._marketCache = Array.isArray(plugins) ? plugins : [];
3730
3804
  this._marketFetchedAt = this._marketLastUsedStale ? 0 : Date.now();
3731
3805
  this._marketRepoFingerprint = reposFingerprint;
3732
- this._marketCacheByKey.set(key, {
3733
- plugins: this._marketCache,
3734
- fetchedAt: this._marketFetchedAt,
3735
- reposFingerprint
3736
- });
3806
+ this._rememberMarketPlugins(key, this._marketCache, this._marketFetchedAt, reposFingerprint);
3737
3807
  if (!this._marketLastUsedStale) {
3738
3808
  this.saveMarketCacheToFile(this._marketCache, this._marketFetchedAt, reposFingerprint);
3739
3809
  }
@@ -3809,15 +3879,15 @@ class PluginsService {
3809
3879
  const previousPlugins = this.prepareMarketPlugins(previousEnvelope.plugins);
3810
3880
  this._marketCache = previousPlugins;
3811
3881
  this._marketCacheByKey.clear();
3812
- this._marketCacheByKey.set(this._marketKey(refreshOptions), {
3813
- plugins: previousPlugins,
3814
- fetchedAt: previousEnvelope.fetchedAt || 0,
3815
- reposFingerprint: previousEnvelope.reposFingerprint || null
3816
- });
3882
+ this._rememberMarketPlugins(
3883
+ this._marketKey(refreshOptions),
3884
+ previousPlugins,
3885
+ previousEnvelope.fetchedAt || 0,
3886
+ previousEnvelope.reposFingerprint || null
3887
+ );
3817
3888
  this._marketFetchedAt = previousEnvelope.fetchedAt || 0;
3818
3889
  this._marketRepoFingerprint = previousEnvelope.reposFingerprint || null;
3819
- this._marketReadmeCache = new Map(Object.entries(previousEnvelope.readmes || {}));
3820
- this._marketReadmeCacheLoaded = true;
3890
+ this._loadMarketReadmes(previousEnvelope.readmes || {});
3821
3891
  this.saveMarketCacheToFile(
3822
3892
  previousEnvelope.plugins,
3823
3893
  previousEnvelope.fetchedAt,
@@ -4033,4 +4103,11 @@ class PluginsService {
4033
4103
  }
4034
4104
  }
4035
4105
 
4036
- module.exports = { PluginsService };
4106
+ module.exports = {
4107
+ PluginsService,
4108
+ MAX_PLUGIN_DETAIL_BYTES,
4109
+ MAX_PLUGIN_MANIFEST_BYTES,
4110
+ MAX_PLUGIN_LIST_CACHE_ENTRIES,
4111
+ MAX_MARKET_CACHE_ENTRIES,
4112
+ MAX_MARKET_README_CACHE_ENTRIES
4113
+ };