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.
- package/README.md +56 -388
- package/dist/web/assets/AgentsDrawer-Bam7kBjG.js +1 -0
- package/dist/web/assets/Analytics-D5dUWXEa.js +1 -0
- package/dist/web/assets/AssetPathField-CLqalYv-.js +1 -0
- package/dist/web/assets/{ChatHistoryDrawer-BTmQ1aQr.js → ChatHistoryDrawer-C1At65O3.js} +1 -1
- package/dist/web/assets/{CollapseItem-CwqJk3bE.js → CollapseItem-CVWojShK.js} +1 -1
- package/dist/web/assets/CommandsDrawer-BEoypGBf.js +1 -0
- package/dist/web/assets/ConfigExportDrawer-DBvFDDWK.js +1 -0
- package/dist/web/assets/{ConfigTemplates-gyERg9iC.js → ConfigTemplates-BpaWG-iC.js} +1 -1
- package/dist/web/assets/{ConfigTemplatesDrawer-B17RqFi4.js → ConfigTemplatesDrawer-C2lvyKMf.js} +1 -1
- package/dist/web/assets/ConfigTemplatesPanel-CorOY7kY.js +1 -0
- package/dist/web/assets/{Divider-DsdbkHrG.js → Divider-BLLhMTSt.js} +1 -1
- package/dist/web/assets/FavoritesDrawer-uW88N6te.js +1 -0
- package/dist/web/assets/GatewayConvertDrawer-i5Wt9Dd9.js +1 -0
- package/dist/web/assets/Home-BIxWvm29.js +1 -0
- package/dist/web/assets/{MarkdownEditor-9TTwTS-U.js → MarkdownEditor-Ul0bSGj-.js} +1 -1
- package/dist/web/assets/McpDrawer-Cc2xq77I.js +1 -0
- package/dist/web/assets/McpFormDrawer-DLHX_nrI.js +1 -0
- package/dist/web/assets/{PluginManager-BuRcWuBj.js → PluginManager-BGQ0xF2F.js} +1 -1
- package/dist/web/assets/{PluginsDrawer-_zZENaQn.js → PluginsDrawer-STwHEqiF.js} +1 -1
- package/dist/web/assets/PluginsPanel-1zuNP7da.js +1 -0
- package/dist/web/assets/ProjectList-_pe2nk9c.js +1 -0
- package/dist/web/assets/PromptsDrawer-CJN1xZk7.js +1 -0
- package/dist/web/assets/RecentSessionsDrawer-DX1HI8Nm.js +1 -0
- package/dist/web/assets/SessionCard-Op6PHvi0.js +1 -0
- package/dist/web/assets/SessionList-Da_TAEYr.js +1 -0
- package/dist/web/assets/SettingsDrawer-DKLd4Sjy.js +1 -0
- package/dist/web/assets/{SkillManager-S698Hns8.js → SkillManager-PcAa5EbF.js} +1 -1
- package/dist/web/assets/{SkillsDrawer-Ms4lPCow.js → SkillsDrawer-DPdU4p6C.js} +1 -1
- package/dist/web/assets/SkillsPanel-B9qzRqjd.js +1 -0
- package/dist/web/assets/Upload-e1M9Yp5z.js +1 -0
- package/dist/web/assets/{WorkspaceDrawer-L7HJRc6u.js → WorkspaceDrawer-oS6mB1VZ.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-DsoNg2z8.js → WorkspaceManager-6VvRWe3r.js} +1 -1
- package/dist/web/assets/WorkspacePanel-CYq0Z8iY.js +1 -0
- package/dist/web/assets/client-BgcNg4Bt.js +1 -0
- package/dist/web/assets/{headers-B5JWGn3v.js → headers-DbowkmZJ.js} +1 -1
- package/dist/web/assets/{icons-DmFe4b5j.js → icons-B-AzZoeE.js} +1 -1
- package/dist/web/assets/index-BlUXw84U.js +2 -0
- package/dist/web/assets/index-Bn8BUnwP.css +1 -0
- package/dist/web/assets/{light-0eavxNvt.js → light-C2X7hegj.js} +1 -1
- package/dist/web/assets/platforms-DrzAtnjY.js +1 -0
- package/dist/web/assets/platforms-mqqe_82d.js +1 -0
- package/dist/web/assets/project-config-BVt5s-pH.js +1 -0
- package/dist/web/assets/sessions-53-Fr68H.js +1 -0
- package/dist/web/assets/{sessions-D4yQtfKb.js → sessions-CaWUaysM.js} +1 -1
- package/dist/web/assets/use-message-DBsCm7FJ.js +1 -0
- package/dist/web/assets/{useResponsiveDrawer-V_onl9NV.js → useResponsiveDrawer-ChHZrTfl.js} +1 -1
- package/dist/web/assets/{workspaces-mjyPVGS8.js → workspaces-DQuZd4qw.js} +1 -1
- package/dist/web/index.html +8 -8
- package/package.json +4 -2
- package/src/commands/export-config.js +17 -211
- package/src/config/model-metadata.json +180 -16
- package/src/platforms/channel-auth-service.js +1 -1
- package/src/platforms/commands-service.js +28 -0
- package/src/platforms/driver-registry.js +1 -0
- package/src/platforms/drivers/claude/channels-implementation.js +79 -90
- package/src/platforms/drivers/claude/native-config-implementation.js +3 -4
- package/src/platforms/drivers/codex/channels-implementation.js +34 -49
- package/src/platforms/drivers/codex/native-config-implementation.js +3 -51
- package/src/platforms/drivers/dsh/api-operations.js +1 -1
- package/src/platforms/drivers/dsh/channels-implementation.js +28 -8
- package/src/platforms/drivers/dsh/channels.js +0 -18
- package/src/platforms/drivers/dsh/common.js +7 -0
- package/src/platforms/drivers/dsh/native-config.js +7 -2
- package/src/platforms/drivers/gemini/channels-implementation.js +47 -55
- package/src/platforms/drivers/gemini/native-config-implementation.js +8 -20
- package/src/platforms/drivers/generic-native-config.js +20 -0
- package/src/platforms/drivers/native-config-snapshot.js +30 -4
- package/src/platforms/drivers/omp/channels-implementation.js +51 -17
- package/src/platforms/drivers/omp/native-config-implementation.js +10 -17
- package/src/platforms/drivers/opencode/channels-implementation.js +6 -0
- package/src/platforms/drivers/opencode/native-config-implementation.js +3 -67
- package/src/platforms/manifest-schema.js +35 -2
- package/src/platforms/manifests/claude.json +0 -8
- package/src/platforms/manifests/codex.json +0 -8
- package/src/platforms/manifests/dsh.json +6 -9
- package/src/platforms/manifests/gemini.json +0 -8
- package/src/platforms/manifests/omp.json +0 -8
- package/src/platforms/manifests/opencode.json +0 -8
- package/src/platforms/native-oauth-adapters.js +33 -33
- package/src/server/index.js +2 -0
- package/src/server/services/bounded-content.js +60 -0
- package/src/server/services/channel-balance.js +10 -4
- package/src/server/services/config-export-service.js +2 -2
- package/src/server/services/config-templates-service.js +5 -1
- package/src/server/services/plugins-service.js +162 -85
- package/src/server/services/session-history-maintenance-worker.js +71 -0
- package/src/server/services/session-history-maintenance.js +166 -0
- package/src/server/services/skill-service.js +23 -3
- package/src/server/services/speed-test.js +0 -72
- package/src/shared/channel-driver.js +16 -0
- package/src/shared/driver-factories/api.js +1 -0
- package/src/utils/native-config-patcher.js +474 -0
- package/dist/web/assets/AgentsDrawer-Cj5CJ4d9.js +0 -1
- package/dist/web/assets/Analytics-1doS-8uC.js +0 -1
- package/dist/web/assets/AssetPathField-C9TOPl7o.js +0 -1
- package/dist/web/assets/CommandsDrawer-CaZWDI_G.js +0 -1
- package/dist/web/assets/ConfigExportDrawer-C3vSZEsS.js +0 -1
- package/dist/web/assets/ConfigTemplatesPanel-DKIC_FqG.js +0 -1
- package/dist/web/assets/FavoritesDrawer-DaziQmBp.js +0 -1
- package/dist/web/assets/GatewayConvertDrawer-OMgWUUSc.js +0 -1
- package/dist/web/assets/Home-DOSUTfrE.js +0 -1
- package/dist/web/assets/McpDrawer-I8rl8lGH.js +0 -1
- package/dist/web/assets/McpFormDrawer-DEP3C7ut.js +0 -1
- package/dist/web/assets/PluginsPanel-Bs5qZ-TK.js +0 -1
- package/dist/web/assets/ProjectList-BSxuL2td.js +0 -1
- package/dist/web/assets/PromptsDrawer-CoZ-ZtsY.js +0 -1
- package/dist/web/assets/RecentSessionsDrawer-B_17aJBG.js +0 -1
- package/dist/web/assets/SessionCard-BL_x0d3t.js +0 -1
- package/dist/web/assets/SessionList-DhRYZ6vl.js +0 -1
- package/dist/web/assets/SettingsDrawer-fYb0DU7X.js +0 -1
- package/dist/web/assets/SkillsPanel-rqnqAOGL.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Bbn38SfV.js +0 -1
- package/dist/web/assets/SpeedTestDrawer-Dc0WToJ_.css +0 -1
- package/dist/web/assets/Upload-CdyVHxCI.js +0 -1
- package/dist/web/assets/WorkspacePanel-Be8PVZah.js +0 -1
- package/dist/web/assets/client-UDwXBP9i.js +0 -1
- package/dist/web/assets/index-3Lp6z8TQ.css +0 -1
- package/dist/web/assets/index-BKlAWTH-.js +0 -2
- package/dist/web/assets/platforms-BmzzwnSw.js +0 -1
- package/dist/web/assets/platforms-DWXgmy2w.js +0 -1
- package/dist/web/assets/project-config-B2baPzYL.js +0 -1
- package/dist/web/assets/sessions-CC0j-KH_.js +0 -1
- package/dist/web/assets/use-message-DqCwefG6.js +0 -1
package/src/server/index.js
CHANGED
|
@@ -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 +
|
|
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 +
|
|
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 +
|
|
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:
|
|
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
|
-
-
|
|
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.
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1863
|
+
const plugin = this.listPlugins(options).plugins.find(plugin => (
|
|
1766
1864
|
plugin.key === name || plugin.pluginId === name || plugin.name === name
|
|
1767
|
-
))
|
|
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
|
-
|
|
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
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
3449
|
+
this._setMarketReadme(this._pluginReadmeCacheKey(plugin), boundedReadme);
|
|
3376
3450
|
}
|
|
3377
|
-
return
|
|
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.
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
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.
|
|
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 = {
|
|
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
|
+
};
|