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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/bin/lib/atomic-write.mjs +72 -0
- package/bin/lib/paths.mjs +65 -0
- package/bin/lib/port.mjs +23 -0
- package/bin/skillhelper.mjs +720 -0
- package/build/build-site.mjs +35 -0
- package/build/build-web.mjs +29 -0
- package/build/pack-smoke.mjs +195 -0
- package/build/prepare-publish.mjs +3 -0
- package/build/verify.mjs +157 -0
- package/config/sources.example.yaml +126 -0
- package/docs/Frontend-Spec.md +1207 -0
- package/docs/scan_skills_rules.md +432 -0
- package/package.json +95 -0
- package/packages/scanner/package.json +15 -0
- package/packages/scanner/src/adapters/claude-agents.mjs +134 -0
- package/packages/scanner/src/adapters/claude-plugin.mjs +260 -0
- package/packages/scanner/src/adapters/codex-plugin.mjs +330 -0
- package/packages/scanner/src/adapters/directory-skill.mjs +239 -0
- package/packages/scanner/src/adapters/file-docs.mjs +158 -0
- package/packages/scanner/src/adapters/hermes-plugin.mjs +149 -0
- package/packages/scanner/src/adapters/markdown-skill.mjs +229 -0
- package/packages/scanner/src/adapters/mcp-config.mjs +190 -0
- package/packages/scanner/src/adapters/scan-tier.mjs +99 -0
- package/packages/scanner/src/adapters/skill-adapter.mjs +345 -0
- package/packages/scanner/src/adapters/tier1-editor-skills.mjs +278 -0
- package/packages/scanner/src/adapters/tier2-user-skills.mjs +218 -0
- package/packages/scanner/src/adapters/tier3-other-skills.mjs +225 -0
- package/packages/scanner/src/config/editor-tiers.mjs +191 -0
- package/packages/scanner/src/core/atomic-write.mjs +11 -0
- package/packages/scanner/src/core/descriptor.mjs +143 -0
- package/packages/scanner/src/core/path-hash.mjs +89 -0
- package/packages/scanner/src/core/registry.mjs +128 -0
- package/packages/scanner/src/hash/path-hash.mjs +8 -0
- package/packages/scanner/src/icon/brand-map.mjs +268 -0
- package/packages/scanner/src/icon/icon-extractor.mjs +466 -0
- package/packages/scanner/src/index.mjs +450 -0
- package/packages/scanner/src/types.d.ts +144 -0
- package/packages/scanner/src/utils.mjs +194 -0
- package/packages/server/package.json +15 -0
- package/packages/server/src/index.mjs +1071 -0
- package/packages/server/src/labels.mjs +64 -0
- package/packages/server/src/langdetect.mjs +34 -0
- package/packages/server/src/llm-client.mjs +42 -0
- package/packages/server/src/translate-cache.mjs +132 -0
- package/packages/server/src/translator.mjs +53 -0
- package/packages/web/README.md +36 -0
- package/packages/web/dist/assets/index-CGt04vt0.js +304 -0
- package/packages/web/dist/assets/index-CLSDoa3H.css +1 -0
- package/packages/web/dist/brand-icon.png +0 -0
- package/packages/web/dist/brand-logo.png +0 -0
- package/packages/web/dist/favicon-16x16.png +0 -0
- package/packages/web/dist/favicon-192x192.png +0 -0
- package/packages/web/dist/favicon-32x32.png +0 -0
- package/packages/web/dist/favicon-512x512.png +0 -0
- package/packages/web/dist/favicon-source.svg +5 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/favicon.svg +5 -0
- package/packages/web/dist/icons/hermes-128.png +0 -0
- package/packages/web/dist/icons/hermes-192.png +0 -0
- package/packages/web/dist/icons/hermes-256.png +0 -0
- package/packages/web/dist/icons/hermes-32.png +0 -0
- package/packages/web/dist/icons/hermes-512.png +0 -0
- package/packages/web/dist/icons/hermes-64.png +0 -0
- package/packages/web/dist/icons/hermes.png +0 -0
- package/packages/web/dist/index.html +37 -0
- package/packages/web/dist/robots.txt +3 -0
- package/packages/web/dist/site.webmanifest +24 -0
- package/packages/web/public/brand-icon.png +0 -0
- package/packages/web/public/brand-logo.png +0 -0
- package/packages/web/public/favicon-16x16.png +0 -0
- package/packages/web/public/favicon-192x192.png +0 -0
- package/packages/web/public/favicon-32x32.png +0 -0
- package/packages/web/public/favicon-512x512.png +0 -0
- package/packages/web/public/favicon-source.svg +5 -0
- package/packages/web/public/favicon.ico +0 -0
- package/packages/web/public/favicon.svg +5 -0
- package/packages/web/public/icons/hermes-128.png +0 -0
- package/packages/web/public/icons/hermes-192.png +0 -0
- package/packages/web/public/icons/hermes-256.png +0 -0
- package/packages/web/public/icons/hermes-32.png +0 -0
- package/packages/web/public/icons/hermes-512.png +0 -0
- package/packages/web/public/icons/hermes-64.png +0 -0
- package/packages/web/public/icons/hermes.png +0 -0
- package/packages/web/public/robots.txt +3 -0
- package/packages/web/public/site.webmanifest +24 -0
- package/scripts/install-and-sync.sh +209 -0
- package/scripts/sync-skills.sh +605 -0
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
// @skillhelper/scanner — multi-source skill aggregator.
|
|
2
|
+
//
|
|
3
|
+
// P1 wires hermes + claude-code adapters via the shared markdown-skill
|
|
4
|
+
// scanner. Other adapters land in P4. The orchestrator loads sources.yaml
|
|
5
|
+
// from ~/.config/skillhelper/, dispatches to enabled adapters, and
|
|
6
|
+
// returns a flat IR list.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import crypto from 'node:crypto';
|
|
11
|
+
import YAML from 'yaml';
|
|
12
|
+
import { configFile } from '../../../bin/lib/paths.mjs';
|
|
13
|
+
import { scanMarkdownSkills } from './adapters/markdown-skill.mjs';
|
|
14
|
+
import { scanDirectorySkills } from './adapters/directory-skill.mjs';
|
|
15
|
+
import { scanFileDocs } from './adapters/file-docs.mjs';
|
|
16
|
+
import { scanMcpConfigs } from './adapters/mcp-config.mjs';
|
|
17
|
+
import { scanHermesPlugins } from './adapters/hermes-plugin.mjs';
|
|
18
|
+
import { scanCodexPlugins } from './adapters/codex-plugin.mjs';
|
|
19
|
+
import { scanClaudeAgents } from './adapters/claude-agents.mjs';
|
|
20
|
+
import { scanClaudePlugins } from './adapters/claude-plugin.mjs';
|
|
21
|
+
import { expandRoots, expandTilde } from './utils.mjs';
|
|
22
|
+
import { scanTierSkills } from './adapters/scan-tier.mjs';
|
|
23
|
+
|
|
24
|
+
const ADAPTERS = {
|
|
25
|
+
// Tier 1: Tool adapters (return items with tier='tool', brand set)
|
|
26
|
+
hermes: async (cfg, limits) => {
|
|
27
|
+
const result = await scanMarkdownSkills({
|
|
28
|
+
source: 'hermes',
|
|
29
|
+
roots: cfg.roots || [],
|
|
30
|
+
fileGlob: '**/SKILL.md',
|
|
31
|
+
limits,
|
|
32
|
+
});
|
|
33
|
+
result.items.forEach(it => {
|
|
34
|
+
it.tier = 'tool';
|
|
35
|
+
it.brand = 'hermes';
|
|
36
|
+
});
|
|
37
|
+
return result;
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
'claude-code': async (cfg, limits) => {
|
|
41
|
+
const result = await scanMarkdownSkills({
|
|
42
|
+
source: 'claude-code',
|
|
43
|
+
roots: cfg.roots || [],
|
|
44
|
+
fileGlob: '**/SKILL.md',
|
|
45
|
+
limits,
|
|
46
|
+
});
|
|
47
|
+
result.items.forEach(it => {
|
|
48
|
+
it.tier = 'tool';
|
|
49
|
+
it.brand = 'claude';
|
|
50
|
+
});
|
|
51
|
+
return result;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
codex: async (cfg, limits) => {
|
|
55
|
+
const result = await scanFileDocs({
|
|
56
|
+
source: 'codex',
|
|
57
|
+
editor: 'Codex',
|
|
58
|
+
kind: 'instruction',
|
|
59
|
+
files: cfg.files || [],
|
|
60
|
+
roots: cfg.roots || [],
|
|
61
|
+
globs: normalizeGlobs(cfg, ['AGENTS.md']),
|
|
62
|
+
limits,
|
|
63
|
+
});
|
|
64
|
+
result.items.forEach(it => {
|
|
65
|
+
it.tier = 'tool';
|
|
66
|
+
it.brand = 'codex';
|
|
67
|
+
});
|
|
68
|
+
return result;
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
cursor: async (cfg, limits) => {
|
|
72
|
+
const result = await scanFileDocs({
|
|
73
|
+
source: 'cursor',
|
|
74
|
+
editor: 'Cursor',
|
|
75
|
+
kind: 'instruction',
|
|
76
|
+
files: cfg.files || [],
|
|
77
|
+
roots: cfg.roots || [],
|
|
78
|
+
globs: normalizeGlobs(cfg, ['.cursorrules', '.cursor/rules/**/*.{md,mdc}']),
|
|
79
|
+
limits,
|
|
80
|
+
});
|
|
81
|
+
result.items.forEach(it => {
|
|
82
|
+
it.tier = 'tool';
|
|
83
|
+
it.brand = 'cursor';
|
|
84
|
+
});
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
'hermes-plugin': async (cfg, limits) => {
|
|
89
|
+
const result = await scanHermesPlugins({
|
|
90
|
+
source: 'hermes-plugin',
|
|
91
|
+
editor: 'Hermes Agent',
|
|
92
|
+
roots: cfg.roots || [],
|
|
93
|
+
limits,
|
|
94
|
+
});
|
|
95
|
+
result.items.forEach(it => {
|
|
96
|
+
it.tier = 'tool';
|
|
97
|
+
it.brand = 'hermes';
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
'codex-plugin': async (cfg, limits) => {
|
|
103
|
+
const result = await scanCodexPlugins({
|
|
104
|
+
source: 'codex-plugin',
|
|
105
|
+
editor: 'Codex',
|
|
106
|
+
roots: cfg.roots || [],
|
|
107
|
+
limits,
|
|
108
|
+
});
|
|
109
|
+
result.items.forEach(it => {
|
|
110
|
+
it.tier = 'tool';
|
|
111
|
+
it.brand = 'codex';
|
|
112
|
+
});
|
|
113
|
+
return result;
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
'claude-agents': async (cfg, limits) => {
|
|
117
|
+
const result = await scanClaudeAgents({
|
|
118
|
+
source: 'claude-agents',
|
|
119
|
+
editor: 'Claude Code',
|
|
120
|
+
roots: cfg.roots || [],
|
|
121
|
+
limits,
|
|
122
|
+
});
|
|
123
|
+
result.items.forEach(it => {
|
|
124
|
+
it.tier = 'tool';
|
|
125
|
+
});
|
|
126
|
+
return result;
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
'claude-plugin': async (cfg, limits) => {
|
|
130
|
+
const result = await scanClaudePlugins({
|
|
131
|
+
source: 'claude-plugin',
|
|
132
|
+
editor: 'Claude Code',
|
|
133
|
+
roots: cfg.roots || [],
|
|
134
|
+
limits,
|
|
135
|
+
});
|
|
136
|
+
result.items.forEach(it => {
|
|
137
|
+
it.tier = 'tool';
|
|
138
|
+
});
|
|
139
|
+
return result;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// Tier 2: Directory-based skills (already returns tier='directory', dirName)
|
|
143
|
+
'directory-skill': async (cfg, limits) => scanDirectorySkills({
|
|
144
|
+
paths: cfg.paths || [],
|
|
145
|
+
globs: normalizeGlobs(cfg, ['**/SKILL.md']),
|
|
146
|
+
limits,
|
|
147
|
+
}),
|
|
148
|
+
|
|
149
|
+
// Tier 3: Other sources (assign tier='other')
|
|
150
|
+
'project-runbook': async (cfg, limits) => {
|
|
151
|
+
const result = await scanFileDocs({
|
|
152
|
+
source: 'project-runbook',
|
|
153
|
+
editor: 'Project Docs',
|
|
154
|
+
kind: 'runbook',
|
|
155
|
+
files: cfg.files || [],
|
|
156
|
+
roots: cfg.roots || [],
|
|
157
|
+
globs: normalizeGlobs(cfg, ['docs/RUNBOOK-*.md', 'AGENTS.md', 'CLAUDE.md', '.cursorrules', '.cursor/rules/**/*.{md,mdc}']),
|
|
158
|
+
limits,
|
|
159
|
+
});
|
|
160
|
+
result.items.forEach(it => {
|
|
161
|
+
it.tier = 'other';
|
|
162
|
+
});
|
|
163
|
+
return result;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Removed: 'mcp-config', 'mcp', 'skills', 'skill' — no longer scanned
|
|
167
|
+
// Future: obsidian
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const DEFAULT_CODEX_PLUGIN_SOURCE = Object.freeze({
|
|
171
|
+
enabled: true,
|
|
172
|
+
roots: ['~/.codex/plugins/cache'],
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const DEFAULT_CLAUDE_AGENTS_SOURCE = Object.freeze({
|
|
176
|
+
enabled: true,
|
|
177
|
+
roots: ['~/.claude/agents'],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const DEFAULT_CLAUDE_PLUGIN_SOURCE = Object.freeze({
|
|
181
|
+
enabled: true,
|
|
182
|
+
roots: ['~/.claude/plugins/cache'],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
function codexPluginConfig(cfg) {
|
|
186
|
+
return cfg.sources?.['codex-plugin'] ?? DEFAULT_CODEX_PLUGIN_SOURCE;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function claudeAgentsConfig(cfg) {
|
|
190
|
+
return cfg.sources?.['claude-agents'] ?? DEFAULT_CLAUDE_AGENTS_SOURCE;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function claudePluginConfig(cfg) {
|
|
194
|
+
return cfg.sources?.['claude-plugin'] ?? DEFAULT_CLAUDE_PLUGIN_SOURCE;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizeGlobs(cfg, defaults) {
|
|
198
|
+
if (Array.isArray(cfg.globs)) return cfg.globs.filter(Boolean);
|
|
199
|
+
if (cfg.glob) return [cfg.glob].flat().filter(Boolean);
|
|
200
|
+
return defaults;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Run all enabled adapters and return a flat IR list.
|
|
205
|
+
* v4.0: 使用三层优先级扫描器(Tier 1 编辑器 + Tier 2 用户 + Tier 3 其他)
|
|
206
|
+
* @returns {Promise<import('./types').SkillItem[]>}
|
|
207
|
+
*/
|
|
208
|
+
export async function scan() {
|
|
209
|
+
const cfg = loadConfig();
|
|
210
|
+
if (!cfg) return [];
|
|
211
|
+
|
|
212
|
+
const limits = {
|
|
213
|
+
maxFiles: cfg.limits?.maxFiles ?? 5000,
|
|
214
|
+
maxFileBytes: cfg.limits?.maxFileBytes ?? 1024 * 1024,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// ✅ v4.0: 调用三层优先级扫描器
|
|
218
|
+
try {
|
|
219
|
+
if (process.env.SKILLHELPER_DEBUG) {
|
|
220
|
+
console.error('[scan] Calling scanTierSkills (Tier 1 → 2 → 3)...');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tierResult = await scanTierSkills({
|
|
224
|
+
scanTier1: cfg.scanTier1 !== false,
|
|
225
|
+
scanTier2: cfg.scanTier2 !== false,
|
|
226
|
+
scanTier3: cfg.scanTier3 === true,
|
|
227
|
+
projectRoot: process.cwd(),
|
|
228
|
+
limits,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (process.env.SKILLHELPER_DEBUG) {
|
|
232
|
+
console.error('[scan] tierResult stats:', JSON.stringify(tierResult.stats, null, 2));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 三层扫描器可能产生同名重复(gstack 为 .cursor/.factory/.kiro 等多编辑器
|
|
236
|
+
// 生成的副本,source/kind/name 全相同),用语义去重合并为一条,保留主文件。
|
|
237
|
+
const items = [...(tierResult.items || [])];
|
|
238
|
+
const pluginConfig = codexPluginConfig(cfg);
|
|
239
|
+
if (pluginConfig.enabled) {
|
|
240
|
+
try {
|
|
241
|
+
const pluginResult = await ADAPTERS['codex-plugin'](pluginConfig, limits);
|
|
242
|
+
items.push(...pluginResult.items.map(withLegacyMetadata));
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// 插件扫描失败不能让既有 Tier 1-3 技能整体退回旧扫描路径。
|
|
245
|
+
console.warn('[scan] Codex plugin scan failed:', error.message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const agentsConfig = claudeAgentsConfig(cfg);
|
|
250
|
+
if (agentsConfig.enabled) {
|
|
251
|
+
try {
|
|
252
|
+
const agentsResult = await ADAPTERS['claude-agents'](agentsConfig, limits);
|
|
253
|
+
items.push(...agentsResult.items.map(withLegacyMetadata));
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.warn('[scan] Claude agents scan failed:', error.message);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const claudePluginConf = claudePluginConfig(cfg);
|
|
260
|
+
if (claudePluginConf.enabled) {
|
|
261
|
+
try {
|
|
262
|
+
const claudePluginResult = await ADAPTERS['claude-plugin'](claudePluginConf, limits);
|
|
263
|
+
items.push(...claudePluginResult.items.map(withLegacyMetadata));
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.warn('[scan] Claude plugin scan failed:', error.message);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return dedupeSemantic(items);
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.warn('[scan] Three-tier scanner failed:', e.message);
|
|
271
|
+
if (process.env.SKILLHELPER_DEBUG) {
|
|
272
|
+
console.error('[scan] Error stack:', e.stack);
|
|
273
|
+
}
|
|
274
|
+
// 降级:如果三层扫描失败,使用旧的 adapter 模式
|
|
275
|
+
console.error('[scan] Falling back to legacy adapters...');
|
|
276
|
+
return scanLegacy(cfg, limits);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* 降级方案:旧的 adapter 模式(当三层扫描失败时使用)
|
|
282
|
+
* v4.0: 补上 pathHash 和 tier 字段,便于前端分层菜单显示
|
|
283
|
+
*
|
|
284
|
+
* export 供单元测试验证「sources.yaml 聚合 + 语义去重」逻辑:
|
|
285
|
+
* scan() 主路径走三层扫描器(固定路径,不读 sources.yaml 的 hermes/codex/cursor 配置),
|
|
286
|
+
* 只有 scanLegacy 通过 sources.yaml 聚合多 source。
|
|
287
|
+
*/
|
|
288
|
+
export async function scanLegacy(cfg, limits) {
|
|
289
|
+
const all = [];
|
|
290
|
+
const stats = [];
|
|
291
|
+
for (const [name, src] of Object.entries(cfg.sources || {})) {
|
|
292
|
+
if (!src?.enabled) continue;
|
|
293
|
+
const fn = ADAPTERS[name];
|
|
294
|
+
if (!fn) continue;
|
|
295
|
+
try {
|
|
296
|
+
const { items, stats: s } = await fn(src, limits);
|
|
297
|
+
all.push(...items);
|
|
298
|
+
stats.push(s);
|
|
299
|
+
} catch (e) {
|
|
300
|
+
stats.push({ source: name, available: false, error: e.message });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const out = dedupeSemantic(all);
|
|
305
|
+
|
|
306
|
+
if (process.env.SKILLHELPER_DEBUG) {
|
|
307
|
+
console.error('[scan] legacy stats:', JSON.stringify(stats, null, 2));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 补上 v4.0 必需字段:tier(必须)、pathHash(必须)、editorBrand(可选)
|
|
311
|
+
// 注意:本文件是 ESM,crypto/path 已在顶部 import,不能用 require。
|
|
312
|
+
return out.map(withLegacyMetadata);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function withLegacyMetadata(item) {
|
|
316
|
+
// 计算 pathHash(如果有 filePath)
|
|
317
|
+
let pathHash = '';
|
|
318
|
+
if (item.paths?.abs) {
|
|
319
|
+
const normalized = path.resolve(item.paths.abs);
|
|
320
|
+
pathHash = crypto.createHash('md5').update(normalized).digest('hex');
|
|
321
|
+
} else if (item.id) {
|
|
322
|
+
pathHash = item.id; // 降级:用 id 作为 pathHash
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 分层逻辑
|
|
326
|
+
let tier = 'tier-1'; // 默认 tier-1(编辑器工具)
|
|
327
|
+
if (item.tier === 'directory') {
|
|
328
|
+
tier = 'tier-2'; // 用户目录技能
|
|
329
|
+
} else if (item.tier === 'other') {
|
|
330
|
+
tier = 'tier-3'; // 其他
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// 提取 editorBrand
|
|
334
|
+
let editorBrand = item.brand || item.source;
|
|
335
|
+
if (editorBrand === 'claude-code') editorBrand = 'claude';
|
|
336
|
+
if (editorBrand === 'hermes-plugin') editorBrand = 'hermes';
|
|
337
|
+
if (editorBrand === 'claude-agents') editorBrand = 'claude';
|
|
338
|
+
if (editorBrand === 'claude-plugin') editorBrand = 'claude';
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
...item,
|
|
342
|
+
tier, // 'tier-1' | 'tier-2' | 'tier-3'
|
|
343
|
+
pathHash, // MD5(filePath) 用于去重
|
|
344
|
+
editorBrand, // 品牌名,用于 Tier1 分组和图标
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function getWatchTargets() {
|
|
349
|
+
const cfg = loadConfig();
|
|
350
|
+
const targets = new Set([configFile()]);
|
|
351
|
+
if (!cfg) return [...targets];
|
|
352
|
+
|
|
353
|
+
for (const [name, src] of Object.entries(cfg.sources || {})) {
|
|
354
|
+
if (!src?.enabled) continue;
|
|
355
|
+
for (const f of src.files || []) targets.add(expandTilde(f));
|
|
356
|
+
|
|
357
|
+
// directory-skill uses 'paths' instead of 'roots'
|
|
358
|
+
const roots = name === 'directory-skill'
|
|
359
|
+
? await expandRoots(src.paths || [])
|
|
360
|
+
: await expandRoots(src.roots || []);
|
|
361
|
+
|
|
362
|
+
const globs = normalizeGlobs(src, defaultWatchGlobs(name));
|
|
363
|
+
for (const root of roots) {
|
|
364
|
+
for (const g of globs) targets.add(`${root}/${g}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 旧版 sources.yaml 没有 codex-plugin 配置时也应立即识别本机插件。
|
|
369
|
+
// 用户可显式写 enabled: false 关闭这条默认扫描路径。
|
|
370
|
+
if (cfg.sources?.['codex-plugin'] === undefined) {
|
|
371
|
+
const roots = await expandRoots(DEFAULT_CODEX_PLUGIN_SOURCE.roots);
|
|
372
|
+
for (const root of roots) targets.add(`${root}/**/.codex-plugin/plugin.json`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return [...targets];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function defaultWatchGlobs(name) {
|
|
379
|
+
switch (name) {
|
|
380
|
+
case 'hermes':
|
|
381
|
+
case 'claude-code':
|
|
382
|
+
case 'directory-skill':
|
|
383
|
+
return ['**/SKILL.md'];
|
|
384
|
+
case 'hermes-plugin':
|
|
385
|
+
return ['**/{plugin.yaml,plugin.yml,plugin.json,manifest.json,package.json,README.md,readme.md}'];
|
|
386
|
+
case 'codex-plugin':
|
|
387
|
+
return ['**/.codex-plugin/plugin.json'];
|
|
388
|
+
case 'project-runbook':
|
|
389
|
+
return ['docs/RUNBOOK-*.md', 'AGENTS.md', 'CLAUDE.md', '.cursorrules', '.cursor/rules/**/*.{md,mdc}'];
|
|
390
|
+
case 'cursor':
|
|
391
|
+
return ['rules/**/*.{md,mdc}', '.cursorrules'];
|
|
392
|
+
case 'codex':
|
|
393
|
+
return ['AGENTS.md'];
|
|
394
|
+
default:
|
|
395
|
+
return ['**/*'];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function dedupeSemantic(items) {
|
|
400
|
+
// First remove exact same id defensively.
|
|
401
|
+
const byId = new Map();
|
|
402
|
+
for (const it of items) if (!byId.has(it.id)) byId.set(it.id, it);
|
|
403
|
+
|
|
404
|
+
// Then collapse repeated exports of the same semantic skill.
|
|
405
|
+
// gstack publishes the same skill into multiple hidden tool namespaces:
|
|
406
|
+
// .agents/.cursor/.factory/.gbrain/.hermes/.kiro/.opencode/.openclaw/...
|
|
407
|
+
// Those are useful as provenance, but noisy as separate entries in this hub.
|
|
408
|
+
const bySemantic = new Map();
|
|
409
|
+
for (const it of byId.values()) {
|
|
410
|
+
const key = `${it.source}:${it.kind}:${it.name}`;
|
|
411
|
+
const current = bySemantic.get(key);
|
|
412
|
+
if (!current || rankItem(it) > rankItem(current)) bySemantic.set(key, it);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return [...bySemantic.values()].sort((a, b) => {
|
|
416
|
+
const sa = `${a.source}\u0000${a.category || ''}\u0000${a.name}`;
|
|
417
|
+
const sb = `${b.source}\u0000${b.category || ''}\u0000${b.name}`;
|
|
418
|
+
return sa.localeCompare(sb);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function rankItem(it) {
|
|
423
|
+
const p = it.paths?.abs || '';
|
|
424
|
+
const parts = p.split('/').filter(Boolean);
|
|
425
|
+
const parent = parts.at(-2) || '';
|
|
426
|
+
const hiddenSegments = parts.filter(x => x.startsWith('.')).length;
|
|
427
|
+
let score = 0;
|
|
428
|
+
if (hiddenSegments === 0) score += 1000;
|
|
429
|
+
if (parent === it.name) score += 400;
|
|
430
|
+
if (it.description) score += 80;
|
|
431
|
+
if (it.triggers?.length) score += 60;
|
|
432
|
+
if (it.raw) score += Math.min(50, Math.floor(it.raw.length / 1000));
|
|
433
|
+
score -= hiddenSegments * 50;
|
|
434
|
+
score -= Math.min(200, p.length / 10);
|
|
435
|
+
return score;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function loadConfig() {
|
|
439
|
+
const f = configFile();
|
|
440
|
+
if (!fs.existsSync(f)) return null;
|
|
441
|
+
try {
|
|
442
|
+
const text = fs.readFileSync(f, 'utf8');
|
|
443
|
+
return YAML.parse(text) || {};
|
|
444
|
+
} catch (e) {
|
|
445
|
+
console.error(`[scan] sources.yaml parse error: ${e.message}`);
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export { configFile };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Unified IR (Intermediate Representation) — every adapter normalizes its
|
|
2
|
+
// source into this shape. The web UI only knows about SkillItem, never the
|
|
3
|
+
// raw source layout.
|
|
4
|
+
//
|
|
5
|
+
// Type definitions live here for IDE / future TS migration. Runtime is plain
|
|
6
|
+
// JS, no compile step needed.
|
|
7
|
+
|
|
8
|
+
export interface ParamSpec {
|
|
9
|
+
name: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
description?: string;
|
|
13
|
+
default?: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type PluginCapabilityKind = 'skill' | 'mcp' | 'app' | 'interactive' | 'write';
|
|
17
|
+
|
|
18
|
+
export interface PluginCapability {
|
|
19
|
+
kind: PluginCapabilityKind;
|
|
20
|
+
label: string;
|
|
21
|
+
count?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PluginMetadata {
|
|
25
|
+
manifestPath: string;
|
|
26
|
+
version?: string;
|
|
27
|
+
author?: string;
|
|
28
|
+
homepage?: string;
|
|
29
|
+
category?: string;
|
|
30
|
+
capabilities: PluginCapability[];
|
|
31
|
+
defaultPrompts?: string[];
|
|
32
|
+
logoPath?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SkillItem {
|
|
36
|
+
/** sha1(absPath) — stable across runs, doubles as web-side key */
|
|
37
|
+
id: string;
|
|
38
|
+
|
|
39
|
+
/** broad type — drives icon + filtering */
|
|
40
|
+
kind: 'skill' | 'plugin' | 'mcp' | 'runbook' | 'instruction' | 'config' | 'doc' | 'agent-rule';
|
|
41
|
+
|
|
42
|
+
/** which adapter produced this item */
|
|
43
|
+
source:
|
|
44
|
+
| 'hermes'
|
|
45
|
+
| 'claude-code'
|
|
46
|
+
| 'codex'
|
|
47
|
+
| 'cursor'
|
|
48
|
+
| 'obsidian'
|
|
49
|
+
| 'project'
|
|
50
|
+
| 'project-runbook'
|
|
51
|
+
| 'hermes-plugin'
|
|
52
|
+
| 'codex-plugin'
|
|
53
|
+
| 'claude-agents'
|
|
54
|
+
| 'claude-plugin'
|
|
55
|
+
| 'directory'
|
|
56
|
+
| 'mcp-config';
|
|
57
|
+
|
|
58
|
+
/** machine name (frontmatter.name or file basename) */
|
|
59
|
+
name: string;
|
|
60
|
+
|
|
61
|
+
/** human-readable title — falls back to name */
|
|
62
|
+
title?: string;
|
|
63
|
+
|
|
64
|
+
/** one-line description — falls back to first paragraph */
|
|
65
|
+
description?: string;
|
|
66
|
+
|
|
67
|
+
/** category path, e.g. "devops", "mlops/inference" */
|
|
68
|
+
category?: string;
|
|
69
|
+
|
|
70
|
+
/** owning editor/tool surface, e.g. "Hermes Agent", "Claude Code", "Cursor" */
|
|
71
|
+
editor?: string;
|
|
72
|
+
|
|
73
|
+
/** brand inferred from description / path, e.g. "OpenAI", "Cloudflare" */
|
|
74
|
+
brand?: string;
|
|
75
|
+
|
|
76
|
+
/** product name extracted from skill, e.g. "new-api", "frp" */
|
|
77
|
+
product?: string;
|
|
78
|
+
|
|
79
|
+
/** trigger phrases (when_to_use / triggers / aliases) */
|
|
80
|
+
triggers?: string[];
|
|
81
|
+
|
|
82
|
+
/** MCP tool param schema — empty for skills */
|
|
83
|
+
params?: ParamSpec[];
|
|
84
|
+
|
|
85
|
+
/** free-form tags */
|
|
86
|
+
tags?: string[];
|
|
87
|
+
|
|
88
|
+
paths: {
|
|
89
|
+
/** absolute path on disk — copy-button payload */
|
|
90
|
+
abs: string;
|
|
91
|
+
/** path relative to project root if applicable */
|
|
92
|
+
rel?: string;
|
|
93
|
+
/** classification of the root the file lives under */
|
|
94
|
+
rootKind: 'home' | 'project' | 'icloud';
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** truncated markdown body for list-view preview */
|
|
98
|
+
preview: string;
|
|
99
|
+
|
|
100
|
+
/** complete file content — lazy-loaded by detail view via /api/skills/:id */
|
|
101
|
+
raw: string;
|
|
102
|
+
|
|
103
|
+
/** related links (docs URL, source repo) discovered in frontmatter */
|
|
104
|
+
links?: { label: string; url: string }[];
|
|
105
|
+
|
|
106
|
+
/** ISO timestamp — last modified time of the underlying file */
|
|
107
|
+
updatedAt: string;
|
|
108
|
+
|
|
109
|
+
/** present when frontmatter / format parse failed; UI shows red badge */
|
|
110
|
+
parseError?: string;
|
|
111
|
+
|
|
112
|
+
/** structured, safe metadata for local plugin manifests */
|
|
113
|
+
plugin?: PluginMetadata;
|
|
114
|
+
|
|
115
|
+
// NEW FIELDS for Skills Tab Redesign v0.3.3
|
|
116
|
+
/** Tier 1/2/3 categorization — controls display logic and icon in UI */
|
|
117
|
+
tier?: 'tool' | 'directory' | 'other';
|
|
118
|
+
|
|
119
|
+
/** Parent directory name (Tier 2 only) — used for display label */
|
|
120
|
+
dirName?: string;
|
|
121
|
+
|
|
122
|
+
// NEW FIELDS for v4.0 Priority Scan & Path Dedup
|
|
123
|
+
/** MD5(normalizedAbsPath) — used for deduplication and menu layering */
|
|
124
|
+
pathHash?: string;
|
|
125
|
+
|
|
126
|
+
/** v4.0 tier classification: tier-1 (editor tools), tier-2 (user), tier-3 (other) */
|
|
127
|
+
tierId?: 'tier-1' | 'tier-2' | 'tier-3';
|
|
128
|
+
|
|
129
|
+
/** v4.0 editor brand within Tier 1 (cursor, claude, hermes, etc.) */
|
|
130
|
+
editorBrand?: string;
|
|
131
|
+
|
|
132
|
+
/** 检测置信度(v0.4 对标 cockpit registry 化):L1 只发现目录 / L2 命中技能文件 / L3 解析有效 / L4 完整元数据 */
|
|
133
|
+
confidence?: 'L1' | 'L2' | 'L3' | 'L4';
|
|
134
|
+
|
|
135
|
+
/** i18n translation metadata (if present) */
|
|
136
|
+
i18n?: {
|
|
137
|
+
zh?: {
|
|
138
|
+
title?: string;
|
|
139
|
+
description?: string;
|
|
140
|
+
};
|
|
141
|
+
translatedAt?: string;
|
|
142
|
+
translationModel?: string;
|
|
143
|
+
};
|
|
144
|
+
}
|