skillhelper 0.4.2

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 (89) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/bin/lib/atomic-write.mjs +72 -0
  4. package/bin/lib/paths.mjs +65 -0
  5. package/bin/lib/port.mjs +23 -0
  6. package/bin/skillhelper.mjs +720 -0
  7. package/build/build-site.mjs +35 -0
  8. package/build/build-web.mjs +29 -0
  9. package/build/pack-smoke.mjs +195 -0
  10. package/build/prepare-publish.mjs +3 -0
  11. package/build/verify.mjs +157 -0
  12. package/config/sources.example.yaml +126 -0
  13. package/docs/Frontend-Spec.md +1207 -0
  14. package/docs/scan_skills_rules.md +432 -0
  15. package/package.json +95 -0
  16. package/packages/scanner/package.json +15 -0
  17. package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
  18. package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
  19. package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
  20. package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
  21. package/packages/scanner/src/adapters/file-docs.mjs +158 -0
  22. package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
  23. package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
  24. package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
  25. package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
  26. package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
  27. package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
  28. package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
  29. package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
  30. package/packages/scanner/src/config/editor-tiers.mjs +191 -0
  31. package/packages/scanner/src/core/atomic-write.mjs +11 -0
  32. package/packages/scanner/src/core/descriptor.mjs +143 -0
  33. package/packages/scanner/src/core/path-hash.mjs +89 -0
  34. package/packages/scanner/src/core/registry.mjs +128 -0
  35. package/packages/scanner/src/hash/path-hash.mjs +8 -0
  36. package/packages/scanner/src/icon/brand-map.mjs +268 -0
  37. package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
  38. package/packages/scanner/src/index.mjs +450 -0
  39. package/packages/scanner/src/types.d.ts +144 -0
  40. package/packages/scanner/src/utils.mjs +194 -0
  41. package/packages/server/package.json +15 -0
  42. package/packages/server/src/index.mjs +1071 -0
  43. package/packages/server/src/labels.mjs +64 -0
  44. package/packages/server/src/langdetect.mjs +34 -0
  45. package/packages/server/src/llm-client.mjs +42 -0
  46. package/packages/server/src/translate-cache.mjs +132 -0
  47. package/packages/server/src/translator.mjs +53 -0
  48. package/packages/web/README.md +36 -0
  49. package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
  50. package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
  51. package/packages/web/dist/brand-icon.png +0 -0
  52. package/packages/web/dist/brand-logo.png +0 -0
  53. package/packages/web/dist/favicon-16x16.png +0 -0
  54. package/packages/web/dist/favicon-192x192.png +0 -0
  55. package/packages/web/dist/favicon-32x32.png +0 -0
  56. package/packages/web/dist/favicon-512x512.png +0 -0
  57. package/packages/web/dist/favicon-source.svg +5 -0
  58. package/packages/web/dist/favicon.ico +0 -0
  59. package/packages/web/dist/favicon.svg +5 -0
  60. package/packages/web/dist/icons/hermes-128.png +0 -0
  61. package/packages/web/dist/icons/hermes-192.png +0 -0
  62. package/packages/web/dist/icons/hermes-256.png +0 -0
  63. package/packages/web/dist/icons/hermes-32.png +0 -0
  64. package/packages/web/dist/icons/hermes-512.png +0 -0
  65. package/packages/web/dist/icons/hermes-64.png +0 -0
  66. package/packages/web/dist/icons/hermes.png +0 -0
  67. package/packages/web/dist/index.html +37 -0
  68. package/packages/web/dist/robots.txt +3 -0
  69. package/packages/web/dist/site.webmanifest +24 -0
  70. package/packages/web/public/brand-icon.png +0 -0
  71. package/packages/web/public/brand-logo.png +0 -0
  72. package/packages/web/public/favicon-16x16.png +0 -0
  73. package/packages/web/public/favicon-192x192.png +0 -0
  74. package/packages/web/public/favicon-32x32.png +0 -0
  75. package/packages/web/public/favicon-512x512.png +0 -0
  76. package/packages/web/public/favicon-source.svg +5 -0
  77. package/packages/web/public/favicon.ico +0 -0
  78. package/packages/web/public/favicon.svg +5 -0
  79. package/packages/web/public/icons/hermes-128.png +0 -0
  80. package/packages/web/public/icons/hermes-192.png +0 -0
  81. package/packages/web/public/icons/hermes-256.png +0 -0
  82. package/packages/web/public/icons/hermes-32.png +0 -0
  83. package/packages/web/public/icons/hermes-512.png +0 -0
  84. package/packages/web/public/icons/hermes-64.png +0 -0
  85. package/packages/web/public/icons/hermes.png +0 -0
  86. package/packages/web/public/robots.txt +3 -0
  87. package/packages/web/public/site.webmanifest +24 -0
  88. package/scripts/install-and-sync.sh +209 -0
  89. package/scripts/sync-skills.sh +605 -0
@@ -0,0 +1,260 @@
1
+ // Claude Code plugin adapter — catalog local plugin manifests without executing code.
2
+ // Plugin manifest lives at <plugin-dir>/.claude-plugin/plugin.json (Anthropic convention).
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import fg from 'fast-glob';
7
+ import {
8
+ classifyRoot,
9
+ expandRoots,
10
+ makePreview,
11
+ readFileSafe,
12
+ sha1Id,
13
+ } from '../utils.mjs';
14
+
15
+ const MANIFEST_GLOB = '**/.claude-plugin/plugin.json';
16
+ const IGNORE = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/.venv/**'];
17
+
18
+ export async function scanClaudePlugins(opts = {}) {
19
+ const {
20
+ source = 'claude-plugin',
21
+ editor = 'Claude Code',
22
+ roots = [],
23
+ limits = { maxFiles: 5000, maxFileBytes: 1024 * 1024 },
24
+ } = opts;
25
+
26
+ const expandedRoots = await expandRoots(roots);
27
+ if (!expandedRoots.length) {
28
+ return { items: [], stats: { source, available: false, files: 0, roots: [] } };
29
+ }
30
+
31
+ const manifests = [];
32
+ for (const root of expandedRoots) {
33
+ const found = await fg(MANIFEST_GLOB, {
34
+ cwd: root,
35
+ absolute: true,
36
+ onlyFiles: true,
37
+ dot: true,
38
+ followSymbolicLinks: false,
39
+ ignore: IGNORE,
40
+ });
41
+ for (const manifestPath of found) {
42
+ manifests.push({ manifestPath, root });
43
+ if (manifests.length >= limits.maxFiles) break;
44
+ }
45
+ if (manifests.length >= limits.maxFiles) break;
46
+ }
47
+
48
+ const parsed = manifests.map((entry) => parsePluginManifest({ ...entry, source, editor, limits }));
49
+ const items = dedupePluginVersions(parsed);
50
+ return {
51
+ items,
52
+ stats: { source, available: true, files: items.length, roots: expandedRoots },
53
+ };
54
+ }
55
+
56
+ function parsePluginManifest({ manifestPath, root, source, editor, limits }) {
57
+ const pluginDir = path.dirname(path.dirname(manifestPath));
58
+ const rel = path.relative(root, pluginDir);
59
+ const id = sha1Id(pluginDir);
60
+ const read = readFileSafe(manifestPath, limits.maxFileBytes);
61
+
62
+ if (read.error || read.truncated) {
63
+ return baseItem({
64
+ id,
65
+ source,
66
+ editor,
67
+ pluginDir,
68
+ rel,
69
+ manifestPath,
70
+ mtime: read.mtime,
71
+ name: path.basename(pluginDir),
72
+ description: read.error || `manifest > ${limits.maxFileBytes} bytes, skipped`,
73
+ parseError: read.error || 'manifest too large',
74
+ });
75
+ }
76
+
77
+ let manifest;
78
+ try {
79
+ manifest = JSON.parse(read.text);
80
+ } catch (error) {
81
+ return baseItem({
82
+ id,
83
+ source,
84
+ editor,
85
+ pluginDir,
86
+ rel,
87
+ manifestPath,
88
+ mtime: read.mtime,
89
+ name: path.basename(pluginDir),
90
+ description: 'Claude 插件清单解析失败',
91
+ parseError: `invalid plugin.json: ${error.message}`,
92
+ });
93
+ }
94
+
95
+ const authorInfo = asObject(manifest.author);
96
+ const name = stringValue(manifest.name) || path.basename(pluginDir);
97
+ const title = stringValue(manifest.displayName) || stringValue(manifest.title) || name;
98
+ const description = stringValue(manifest.description) || '未提供插件说明';
99
+ const longDescription = stringValue(manifest.description);
100
+ const category = stringValue(manifest.category);
101
+ const author = stringValue(authorInfo.name)
102
+ || stringValue(typeof manifest.author === 'string' ? manifest.author : undefined);
103
+ const homepage = firstSafeUrl(manifest.homepage, manifest.repository);
104
+ const keywords = stringArray(manifest.keywords);
105
+ const tags = uniqueStrings([...keywords, category].filter(Boolean));
106
+ const components = uniqueComponents(collectComponents(manifest));
107
+
108
+ const raw = renderPluginRaw({
109
+ title,
110
+ description: longDescription,
111
+ version: stringValue(manifest.version),
112
+ author,
113
+ category,
114
+ components,
115
+ });
116
+
117
+ const item = baseItem({
118
+ id,
119
+ source,
120
+ editor,
121
+ pluginDir,
122
+ rel,
123
+ manifestPath,
124
+ mtime: read.mtime,
125
+ name,
126
+ title,
127
+ description,
128
+ category,
129
+ tags,
130
+ raw,
131
+ links: homepage ? [{ label: '官网', url: homepage }] : undefined,
132
+ });
133
+ item.brand = 'claude';
134
+ item.editorBrand = 'claude';
135
+ return item;
136
+ }
137
+
138
+ function baseItem({
139
+ id,
140
+ source,
141
+ editor,
142
+ pluginDir,
143
+ rel,
144
+ manifestPath,
145
+ mtime,
146
+ name,
147
+ title,
148
+ description,
149
+ category,
150
+ tags,
151
+ raw = '',
152
+ links,
153
+ parseError,
154
+ }) {
155
+ const item = {
156
+ id,
157
+ kind: 'plugin',
158
+ source,
159
+ editor,
160
+ name,
161
+ title: title && title !== name ? title : undefined,
162
+ description,
163
+ category,
164
+ tags: tags?.length ? tags : undefined,
165
+ paths: { abs: pluginDir, rel, rootKind: classifyRoot(pluginDir) },
166
+ preview: makePreview(raw, 600),
167
+ raw,
168
+ links,
169
+ plugin: { manifestPath, capabilities: [] },
170
+ updatedAt: new Date(mtime || 0).toISOString(),
171
+ };
172
+ if (parseError) item.parseError = parseError;
173
+ return item;
174
+ }
175
+
176
+ function collectComponents(manifest) {
177
+ const present = [];
178
+ const hasDir = (value) => stringArray(value).length > 0;
179
+ if (hasDir(manifest.agents)) present.push('agents');
180
+ if (hasDir(manifest.skills)) present.push('skills');
181
+ if (hasDir(manifest.commands)) present.push('commands');
182
+ if (hasDir(manifest.hooks)) present.push('hooks');
183
+ if (manifest.mcpServers) present.push('mcp');
184
+ if (manifest.lspServers) present.push('lsp');
185
+ return present;
186
+ }
187
+
188
+ function uniqueComponents(items) {
189
+ return [...new Set(items)];
190
+ }
191
+
192
+ function dedupePluginVersions(items) {
193
+ const byPlugin = new Map();
194
+ for (const item of items) {
195
+ const key = `${item.name.toLowerCase()}\u0000${pluginOrigin(item.paths.abs)}`;
196
+ const current = byPlugin.get(key);
197
+ if (!current || comparePluginItems(item, current) > 0) byPlugin.set(key, item);
198
+ }
199
+ return [...byPlugin.values()].sort((a, b) => a.name.localeCompare(b.name));
200
+ }
201
+
202
+ function pluginOrigin(pluginDir) {
203
+ const cacheIndex = pluginDir.indexOf('/cache/');
204
+ if (cacheIndex === -1) return path.dirname(pluginDir);
205
+ return pluginDir.slice(cacheIndex + '/cache/'.length).split('/').slice(0, -2).join('/');
206
+ }
207
+
208
+ function comparePluginItems(a, b) {
209
+ const versionCompare = compareVersions(a.plugin?.version, b.plugin?.version);
210
+ if (versionCompare !== 0) return versionCompare;
211
+ return Date.parse(a.updatedAt) - Date.parse(b.updatedAt);
212
+ }
213
+
214
+ function compareVersions(a = '', b = '') {
215
+ const aParts = a.split(/[^0-9]+/).filter(Boolean).map(Number);
216
+ const bParts = b.split(/[^0-9]+/).filter(Boolean).map(Number);
217
+ const max = Math.max(aParts.length, bParts.length);
218
+ for (let index = 0; index < max; index += 1) {
219
+ const diff = (aParts[index] || 0) - (bParts[index] || 0);
220
+ if (diff) return diff;
221
+ }
222
+ return a.localeCompare(b);
223
+ }
224
+
225
+ function renderPluginRaw({ title, description, version, author, category, components }) {
226
+ const lines = [`# ${title}`, '', description || '未提供插件说明'];
227
+ if (version) lines.push('', `版本:${version}`);
228
+ if (author) lines.push(`作者:${author}`);
229
+ if (category) lines.push(`分类:${category}`);
230
+ if (components?.length) {
231
+ lines.push('', '## 插件组件', '');
232
+ for (const name of components) lines.push(`- ${name}`);
233
+ }
234
+ return lines.join('\n');
235
+ }
236
+
237
+ function asObject(value) {
238
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
239
+ }
240
+
241
+ function stringValue(value) {
242
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
243
+ }
244
+
245
+ function stringArray(value) {
246
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim());
247
+ return stringValue(value) ? [value.trim()] : [];
248
+ }
249
+
250
+ function firstSafeUrl(...values) {
251
+ for (const value of values) {
252
+ const url = stringValue(typeof value === 'object' && value ? value.url : value);
253
+ if (url && /^https?:\/\//i.test(url)) return url;
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ function uniqueStrings(values) {
259
+ return [...new Set(values)];
260
+ }
@@ -0,0 +1,330 @@
1
+ // Codex plugin adapter — catalog local plugin manifests without executing code.
2
+
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import fg from 'fast-glob';
6
+ import {
7
+ classifyRoot,
8
+ expandRoots,
9
+ makePreview,
10
+ readFileSafe,
11
+ sha1Id,
12
+ } from '../utils.mjs';
13
+
14
+ const MANIFEST_GLOB = '**/.codex-plugin/plugin.json';
15
+ const IGNORE = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/.venv/**'];
16
+
17
+ export async function scanCodexPlugins(opts = {}) {
18
+ const {
19
+ source = 'codex-plugin',
20
+ editor = 'Codex',
21
+ roots = [],
22
+ limits = { maxFiles: 5000, maxFileBytes: 1024 * 1024 },
23
+ } = opts;
24
+
25
+ const expandedRoots = await expandRoots(roots);
26
+ if (!expandedRoots.length) {
27
+ return { items: [], stats: { source, available: false, files: 0, roots: [] } };
28
+ }
29
+
30
+ const manifests = [];
31
+ for (const root of expandedRoots) {
32
+ const found = await fg(MANIFEST_GLOB, {
33
+ cwd: root,
34
+ absolute: true,
35
+ onlyFiles: true,
36
+ dot: true,
37
+ followSymbolicLinks: false,
38
+ ignore: IGNORE,
39
+ });
40
+ for (const manifestPath of found) {
41
+ manifests.push({ manifestPath, root });
42
+ if (manifests.length >= limits.maxFiles) break;
43
+ }
44
+ if (manifests.length >= limits.maxFiles) break;
45
+ }
46
+
47
+ const parsed = manifests.map((entry) => parsePluginManifest({ ...entry, source, editor, limits }));
48
+ const items = dedupePluginVersions(parsed);
49
+ return {
50
+ items,
51
+ stats: { source, available: true, files: items.length, roots: expandedRoots },
52
+ };
53
+ }
54
+
55
+ function parsePluginManifest({ manifestPath, root, source, editor, limits }) {
56
+ const pluginDir = path.dirname(path.dirname(manifestPath));
57
+ const rel = path.relative(root, pluginDir);
58
+ const id = sha1Id(pluginDir);
59
+ const read = readFileSafe(manifestPath, limits.maxFileBytes);
60
+
61
+ if (read.error || read.truncated) {
62
+ return baseItem({
63
+ id,
64
+ source,
65
+ editor,
66
+ pluginDir,
67
+ rel,
68
+ manifestPath,
69
+ mtime: read.mtime,
70
+ name: path.basename(pluginDir),
71
+ description: read.error || `manifest > ${limits.maxFileBytes} bytes, skipped`,
72
+ parseError: read.error || 'manifest too large',
73
+ });
74
+ }
75
+
76
+ let manifest;
77
+ try {
78
+ manifest = JSON.parse(read.text);
79
+ } catch (error) {
80
+ return baseItem({
81
+ id,
82
+ source,
83
+ editor,
84
+ pluginDir,
85
+ rel,
86
+ manifestPath,
87
+ mtime: read.mtime,
88
+ name: path.basename(pluginDir),
89
+ description: 'Codex 插件清单解析失败',
90
+ parseError: `invalid plugin.json: ${error.message}`,
91
+ });
92
+ }
93
+
94
+ const interfaceInfo = asObject(manifest.interface);
95
+ const name = stringValue(manifest.name) || path.basename(pluginDir);
96
+ const title = stringValue(interfaceInfo.displayName) || name;
97
+ const description = stringValue(interfaceInfo.shortDescription)
98
+ || stringValue(manifest.description)
99
+ || stringValue(interfaceInfo.longDescription)
100
+ || '未提供插件说明';
101
+ const longDescription = stringValue(interfaceInfo.longDescription) || stringValue(manifest.description);
102
+ const category = stringValue(interfaceInfo.category);
103
+ const author = stringValue(asObject(manifest.author).name) || stringValue(interfaceInfo.developerName);
104
+ const homepage = firstSafeUrl(manifest.homepage, manifest.repository, interfaceInfo.websiteURL);
105
+ const capabilities = collectCapabilities({ manifest, interfaceInfo, pluginDir });
106
+ const defaultPrompts = stringArray(interfaceInfo.defaultPrompt).slice(0, 3);
107
+ const logoPath = resolveAssetPath(pluginDir, interfaceInfo.logo || interfaceInfo.composerIcon);
108
+ const tags = uniqueStrings([...stringArray(manifest.keywords), category].filter(Boolean));
109
+
110
+ const raw = renderPluginRaw({
111
+ title,
112
+ description: longDescription || description,
113
+ version: stringValue(manifest.version),
114
+ author,
115
+ category,
116
+ capabilities,
117
+ defaultPrompts,
118
+ });
119
+ const item = baseItem({
120
+ id,
121
+ source,
122
+ editor,
123
+ pluginDir,
124
+ rel,
125
+ manifestPath,
126
+ mtime: read.mtime,
127
+ name,
128
+ title,
129
+ description,
130
+ category,
131
+ tags,
132
+ raw,
133
+ preview: makePreview(longDescription || description, 600),
134
+ links: homepage ? [{ label: '官网', url: homepage }] : undefined,
135
+ plugin: {
136
+ manifestPath,
137
+ version: stringValue(manifest.version),
138
+ author,
139
+ homepage,
140
+ category,
141
+ capabilities,
142
+ defaultPrompts: defaultPrompts.length ? defaultPrompts : undefined,
143
+ logoPath,
144
+ },
145
+ });
146
+ item.brand = 'codex';
147
+ return item;
148
+ }
149
+
150
+ function baseItem({
151
+ id,
152
+ source,
153
+ editor,
154
+ pluginDir,
155
+ rel,
156
+ manifestPath,
157
+ mtime,
158
+ name,
159
+ title,
160
+ description,
161
+ category,
162
+ tags,
163
+ raw = '',
164
+ preview = '',
165
+ links,
166
+ plugin,
167
+ parseError,
168
+ }) {
169
+ const item = {
170
+ id,
171
+ kind: 'plugin',
172
+ source,
173
+ editor,
174
+ name,
175
+ title: title && title !== name ? title : undefined,
176
+ description,
177
+ category,
178
+ tags: tags?.length ? tags : undefined,
179
+ paths: { abs: pluginDir, rel, rootKind: classifyRoot(pluginDir) },
180
+ preview,
181
+ raw,
182
+ links,
183
+ plugin: plugin || { manifestPath, capabilities: [] },
184
+ updatedAt: new Date(mtime || 0).toISOString(),
185
+ };
186
+ if (parseError) item.parseError = parseError;
187
+ return item;
188
+ }
189
+
190
+ function collectCapabilities({ manifest, interfaceInfo, pluginDir }) {
191
+ const capabilities = [];
192
+ const skillPath = resolveAssetPath(pluginDir, manifest.skills);
193
+ if (skillPath && fs.existsSync(skillPath)) {
194
+ capabilities.push({ kind: 'skill', label: 'skills', count: countSkillFiles(skillPath) });
195
+ }
196
+
197
+ const mcpValue = manifest.mcpServers;
198
+ if (mcpValue) {
199
+ capabilities.push({ kind: 'mcp', label: 'mcpServers', count: objectSize(mcpValue) || undefined });
200
+ }
201
+
202
+ const appValue = manifest.apps;
203
+ if (appValue) {
204
+ capabilities.push({ kind: 'app', label: 'apps', count: objectSize(appValue) || undefined });
205
+ }
206
+
207
+ for (const capability of stringArray(interfaceInfo.capabilities)) {
208
+ const kind = normalizeInterfaceCapability(capability);
209
+ if (kind) capabilities.push({ kind, label: capability });
210
+ }
211
+ return uniqueCapabilities(capabilities);
212
+ }
213
+
214
+ function countSkillFiles(skillPath) {
215
+ try {
216
+ const stat = fs.statSync(skillPath);
217
+ if (!stat.isDirectory()) return 1;
218
+ return fg.sync(['**/SKILL.md', '**/skill.md'], {
219
+ cwd: skillPath,
220
+ onlyFiles: true,
221
+ dot: true,
222
+ followSymbolicLinks: false,
223
+ ignore: IGNORE,
224
+ }).length;
225
+ } catch {
226
+ return undefined;
227
+ }
228
+ }
229
+
230
+ function normalizeInterfaceCapability(value) {
231
+ const normalized = value.trim().toLowerCase();
232
+ if (normalized === 'interactive') return 'interactive';
233
+ if (normalized === 'write') return 'write';
234
+ return undefined;
235
+ }
236
+
237
+ function uniqueCapabilities(capabilities) {
238
+ const byKind = new Map();
239
+ for (const capability of capabilities) {
240
+ const current = byKind.get(capability.kind);
241
+ if (!current || (capability.count || 0) > (current.count || 0)) byKind.set(capability.kind, capability);
242
+ }
243
+ return [...byKind.values()];
244
+ }
245
+
246
+ function dedupePluginVersions(items) {
247
+ const byPlugin = new Map();
248
+ for (const item of items) {
249
+ const key = `${item.name.toLowerCase()}\u0000${pluginOrigin(item.paths.abs)}`;
250
+ const current = byPlugin.get(key);
251
+ if (!current || comparePluginItems(item, current) > 0) byPlugin.set(key, item);
252
+ }
253
+ return [...byPlugin.values()].sort((a, b) => a.name.localeCompare(b.name));
254
+ }
255
+
256
+ function pluginOrigin(pluginDir) {
257
+ const cacheIndex = pluginDir.indexOf('/cache/');
258
+ if (cacheIndex === -1) return path.dirname(pluginDir);
259
+ return pluginDir.slice(cacheIndex + '/cache/'.length).split('/').slice(0, -2).join('/');
260
+ }
261
+
262
+ function comparePluginItems(a, b) {
263
+ const versionCompare = compareVersions(a.plugin?.version, b.plugin?.version);
264
+ if (versionCompare !== 0) return versionCompare;
265
+ return Date.parse(a.updatedAt) - Date.parse(b.updatedAt);
266
+ }
267
+
268
+ function compareVersions(a = '', b = '') {
269
+ const aParts = a.split(/[^0-9]+/).filter(Boolean).map(Number);
270
+ const bParts = b.split(/[^0-9]+/).filter(Boolean).map(Number);
271
+ const max = Math.max(aParts.length, bParts.length);
272
+ for (let index = 0; index < max; index += 1) {
273
+ const diff = (aParts[index] || 0) - (bParts[index] || 0);
274
+ if (diff) return diff;
275
+ }
276
+ return a.localeCompare(b);
277
+ }
278
+
279
+ function renderPluginRaw({ title, description, version, author, category, capabilities, defaultPrompts }) {
280
+ const lines = [`# ${title}`, '', description];
281
+ if (version) lines.push('', `版本:${version}`);
282
+ if (author) lines.push(`开发者:${author}`);
283
+ if (category) lines.push(`分类:${category}`);
284
+ if (capabilities.length) {
285
+ lines.push('', '## 插件能力', '');
286
+ for (const capability of capabilities) {
287
+ lines.push(`- ${capability.label}${capability.count ? ` (${capability.count})` : ''}`);
288
+ }
289
+ }
290
+ if (defaultPrompts.length) {
291
+ lines.push('', '## 示例提示词', '');
292
+ for (const prompt of defaultPrompts) lines.push(`- ${prompt}`);
293
+ }
294
+ return lines.join('\n');
295
+ }
296
+
297
+ function asObject(value) {
298
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
299
+ }
300
+
301
+ function stringValue(value) {
302
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
303
+ }
304
+
305
+ function stringArray(value) {
306
+ if (Array.isArray(value)) return value.filter((entry) => typeof entry === 'string' && entry.trim()).map((entry) => entry.trim());
307
+ return stringValue(value) ? [value.trim()] : [];
308
+ }
309
+
310
+ function objectSize(value) {
311
+ return value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).length : 0;
312
+ }
313
+
314
+ function resolveAssetPath(pluginDir, value) {
315
+ if (!stringValue(value)) return undefined;
316
+ const candidate = path.resolve(pluginDir, value);
317
+ return candidate.startsWith(pluginDir + path.sep) || candidate === pluginDir ? candidate : undefined;
318
+ }
319
+
320
+ function firstSafeUrl(...values) {
321
+ for (const value of values) {
322
+ const url = stringValue(value);
323
+ if (url && /^https?:\/\//i.test(url)) return url;
324
+ }
325
+ return undefined;
326
+ }
327
+
328
+ function uniqueStrings(values) {
329
+ return [...new Set(values)];
330
+ }