@zhangheteng/skill-hub 0.0.0-stage → 0.2.0

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 (93) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE.md +7 -0
  3. package/README.md +32 -2
  4. package/apps/cli/dist/args.js +111 -0
  5. package/apps/cli/dist/commands/audit.js +38 -0
  6. package/apps/cli/dist/commands/backup.js +34 -0
  7. package/apps/cli/dist/commands/doctor.js +86 -0
  8. package/apps/cli/dist/commands/index-cmd.js +122 -0
  9. package/apps/cli/dist/commands/ingest.js +198 -0
  10. package/apps/cli/dist/commands/init.js +110 -0
  11. package/apps/cli/dist/commands/inspect.js +68 -0
  12. package/apps/cli/dist/commands/inventory.js +259 -0
  13. package/apps/cli/dist/commands/manage.js +149 -0
  14. package/apps/cli/dist/commands/manifest.js +127 -0
  15. package/apps/cli/dist/commands/restore.js +37 -0
  16. package/apps/cli/dist/commands/route.js +92 -0
  17. package/apps/cli/dist/commands/runtime.js +89 -0
  18. package/apps/cli/dist/commands/sync.js +195 -0
  19. package/apps/cli/dist/commands/takeover.js +45 -0
  20. package/apps/cli/dist/commands/usage.js +55 -0
  21. package/apps/cli/dist/commands/verify.js +48 -0
  22. package/apps/cli/dist/index.js +468 -0
  23. package/apps/cli/dist/output.js +7 -0
  24. package/apps/mcp-server/dist/http.js +265 -0
  25. package/apps/mcp-server/dist/index.js +42 -0
  26. package/apps/mcp-server/dist/server.js +541 -0
  27. package/apps/web/dist/index.js +355 -0
  28. package/apps/web/public/app.js +927 -0
  29. package/apps/web/public/index.html +109 -0
  30. package/apps/web/public/styles.css +1796 -0
  31. package/configs/agents/default.yaml +44 -0
  32. package/configs/models.yaml +55 -0
  33. package/configs/profiles/coding.yaml +29 -0
  34. package/configs/profiles/video.yaml +18 -0
  35. package/configs/taxonomy.yaml +24 -0
  36. package/package.json +42 -4
  37. package/packages/core/dist/agent-sources.js +38 -0
  38. package/packages/core/dist/audit.js +275 -0
  39. package/packages/core/dist/catalog.js +173 -0
  40. package/packages/core/dist/config.js +295 -0
  41. package/packages/core/dist/conflict.js +95 -0
  42. package/packages/core/dist/context-cost.js +19 -0
  43. package/packages/core/dist/frontmatter.js +118 -0
  44. package/packages/core/dist/hash.js +7 -0
  45. package/packages/core/dist/history.js +60 -0
  46. package/packages/core/dist/index.js +23 -0
  47. package/packages/core/dist/managed.js +1774 -0
  48. package/packages/core/dist/materialize.js +149 -0
  49. package/packages/core/dist/parse.js +74 -0
  50. package/packages/core/dist/scan.js +112 -0
  51. package/packages/core/dist/takeover.js +96 -0
  52. package/packages/core/dist/tree.js +61 -0
  53. package/packages/core/dist/types.js +4 -0
  54. package/packages/core/dist/usage.js +267 -0
  55. package/packages/router/dist/bm25.js +144 -0
  56. package/packages/router/dist/context-cost.js +82 -0
  57. package/packages/router/dist/external.js +107 -0
  58. package/packages/router/dist/factory.js +144 -0
  59. package/packages/router/dist/hub-service.js +589 -0
  60. package/packages/router/dist/index-store.js +153 -0
  61. package/packages/router/dist/index.js +22 -0
  62. package/packages/router/dist/llm/config.js +311 -0
  63. package/packages/router/dist/llm/http.js +102 -0
  64. package/packages/router/dist/llm/index.js +6 -0
  65. package/packages/router/dist/llm/ollama.js +68 -0
  66. package/packages/router/dist/llm/openai-compatible.js +66 -0
  67. package/packages/router/dist/llm/registry.js +103 -0
  68. package/packages/router/dist/llm/types.js +31 -0
  69. package/packages/router/dist/profile.js +124 -0
  70. package/packages/router/dist/runtime.js +270 -0
  71. package/packages/router/dist/scope.js +13 -0
  72. package/packages/router/dist/taxonomy.js +172 -0
  73. package/packages/router/dist/tokenize.js +38 -0
  74. package/packages/router/dist/types.js +1 -0
  75. package/packages/router/dist/typesafe.js +331 -0
  76. package/packages/router/dist/usage-overview.js +143 -0
  77. package/packages/router/dist/usage.js +76 -0
  78. package/packages/shared/dist/constants.js +14 -0
  79. package/packages/shared/dist/errors.js +90 -0
  80. package/packages/shared/dist/index.js +14 -0
  81. package/packages/shared/dist/logger.js +95 -0
  82. package/packages/shared/dist/paths.js +118 -0
  83. package/packages/shared/dist/types.js +12 -0
  84. package/packages/sync/dist/apply.js +227 -0
  85. package/packages/sync/dist/backup.js +193 -0
  86. package/packages/sync/dist/fs-utils.js +188 -0
  87. package/packages/sync/dist/index.js +18 -0
  88. package/packages/sync/dist/integrity.js +238 -0
  89. package/packages/sync/dist/plan.js +211 -0
  90. package/packages/sync/dist/restore.js +69 -0
  91. package/packages/sync/dist/scope.js +22 -0
  92. package/packages/sync/dist/types.js +1 -0
  93. package/packages/sync/dist/verify.js +189 -0
@@ -0,0 +1,355 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * skill-hub Web 只读视图(Phase 7)。
4
+ *
5
+ * - HTTP API 对齐 MCP 只读契约:list/search/fetch/files/read/explain/history/stats/inventory
6
+ * - 不提供任何写操作(无 sync/restore/install/update)
7
+ * - 默认 bind 127.0.0.1,仅本机访问
8
+ * - 同时托管 public/ 下的前端 SPA
9
+ */
10
+ import { createServer } from "node:http";
11
+ import { readFile } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import { getLlmStatus, skillAudit, skillExplain, skillFetch, skillFiles, skillHistory, skillInventorySummary, skillList, skillRead, skillSearch, skillStats, skillTagSummary, skillUsageOverview, } from "#skill-hub/router";
15
+ import { SKILL_HUB_VERSION, SkillHubError } from "#skill-hub/shared";
16
+ import { withUsageScope } from "#skill-hub/router";
17
+ const DEFAULT_PORT = 4173;
18
+ const DEFAULT_HOST = "127.0.0.1";
19
+ const PUBLIC_DIR = fileURLToPath(new URL("../public", import.meta.url));
20
+ const MIME_TYPES = {
21
+ ".html": "text/html; charset=utf-8",
22
+ ".js": "text/javascript; charset=utf-8",
23
+ ".css": "text/css; charset=utf-8",
24
+ ".json": "application/json; charset=utf-8",
25
+ ".svg": "image/svg+xml",
26
+ ".png": "image/png",
27
+ ".ico": "image/x-icon",
28
+ ".woff2": "font/woff2",
29
+ };
30
+ function sendJson(res, status, data) {
31
+ const body = JSON.stringify(data);
32
+ res.writeHead(status, {
33
+ "content-type": "application/json; charset=utf-8",
34
+ "cache-control": "no-store",
35
+ });
36
+ res.end(body);
37
+ }
38
+ function statusForError(err) {
39
+ if (err instanceof SkillHubError) {
40
+ switch (err.code) {
41
+ case "E_NOT_FOUND":
42
+ return 404;
43
+ case "E_CONFIG":
44
+ case "E_PARSE":
45
+ return 400;
46
+ case "E_CONFLICT":
47
+ return 409;
48
+ default:
49
+ return 500;
50
+ }
51
+ }
52
+ return 500;
53
+ }
54
+ function errorPayload(err) {
55
+ if (err instanceof SkillHubError) {
56
+ return { ok: false, code: err.code, message: err.message, details: err.details ?? null };
57
+ }
58
+ return {
59
+ ok: false,
60
+ code: "E_INTERNAL",
61
+ message: err instanceof Error ? err.message : String(err),
62
+ };
63
+ }
64
+ function parseScopes(raw) {
65
+ if (raw === null || raw.trim() === "")
66
+ return undefined;
67
+ const allowed = new Set(["user", "workspace", "project"]);
68
+ const values = raw
69
+ .split(",")
70
+ .map((s) => s.trim())
71
+ .filter(Boolean);
72
+ for (const scope of values) {
73
+ if (!allowed.has(scope)) {
74
+ throw new SkillHubError({
75
+ code: "E_CONFIG",
76
+ message: `scope 无效: ${scope}(应为 user|workspace|project)`,
77
+ });
78
+ }
79
+ }
80
+ return values;
81
+ }
82
+ function intParam(url, key, bounds) {
83
+ const raw = url.searchParams.get(key);
84
+ if (raw === null)
85
+ return undefined;
86
+ if (!/^-?\d+$/.test(raw)) {
87
+ throw new SkillHubError({ code: "E_CONFIG", message: `${key} 必须是整数` });
88
+ }
89
+ const value = Number(raw);
90
+ if (!Number.isSafeInteger(value) || value < bounds.min || value > bounds.max) {
91
+ throw new SkillHubError({
92
+ code: "E_CONFIG",
93
+ message: `${key} 越界(${bounds.min}-${bounds.max}): ${raw}`,
94
+ });
95
+ }
96
+ return value;
97
+ }
98
+ function stringParam(url, key) {
99
+ const raw = url.searchParams.get(key);
100
+ return raw === null || raw === "" ? undefined : raw;
101
+ }
102
+ async function handleApi(req, res, url) {
103
+ if (req.method !== "GET") {
104
+ sendJson(res, 405, { ok: false, code: "E_CONFIG", message: "Web 视图只读,仅支持 GET" });
105
+ return;
106
+ }
107
+ const segments = url.pathname.split("/").filter(Boolean); // e.g. ["api","skills",":name","files"]
108
+ const resource = segments[1];
109
+ if (url.pathname === "/api/health") {
110
+ sendJson(res, 200, { ok: true, name: "skill-hub-web", version: SKILL_HUB_VERSION });
111
+ return;
112
+ }
113
+ if (url.pathname === "/api/usage-overview") {
114
+ for (const key of url.searchParams.keys()) {
115
+ if (!["days", "origin", "q", "tag", "status", "sort", "offset", "limit", "max_chars"].includes(key)) {
116
+ throw new SkillHubError({ code: "E_CONFIG", message: `usage-overview参数无效: ${key}` });
117
+ }
118
+ }
119
+ sendJson(res, 200, await skillUsageOverview({
120
+ days: intParam(url, "days", { min: 1, max: 3650 }),
121
+ origin: stringParam(url, "origin"),
122
+ query: stringParam(url, "q"),
123
+ tag: stringParam(url, "tag"),
124
+ status: stringParam(url, "status"),
125
+ sort: stringParam(url, "sort"),
126
+ offset: intParam(url, "offset", { min: 0, max: 10000000 }),
127
+ limit: intParam(url, "limit", { min: 1, max: 100 }),
128
+ maxChars: intParam(url, "max_chars", { min: 1, max: 100000 }),
129
+ }));
130
+ return;
131
+ }
132
+ if (resource === "stats") {
133
+ sendJson(res, 200, await skillStats());
134
+ return;
135
+ }
136
+ if (resource === "tags") {
137
+ sendJson(res, 200, await skillTagSummary());
138
+ return;
139
+ }
140
+ // 可选增强层状态:未配置/配置错误/服务不可达都以 200 + state 字段返回,绝不抛错
141
+ if (url.pathname === "/api/llm/status") {
142
+ sendJson(res, 200, await getLlmStatus({ probe: url.searchParams.get("probe") === "1" }));
143
+ return;
144
+ }
145
+ if (resource === "inventory") {
146
+ sendJson(res, 200, await skillInventorySummary({
147
+ agent: stringParam(url, "agent"),
148
+ scope: parseScopes(url.searchParams.get("scope")),
149
+ }));
150
+ return;
151
+ }
152
+ if (resource === "search") {
153
+ const query = stringParam(url, "q") ?? stringParam(url, "query");
154
+ if (!query || !query.trim()) {
155
+ throw new SkillHubError({ code: "E_CONFIG", message: "search 需要 q 参数" });
156
+ }
157
+ const result = await skillSearch({
158
+ query,
159
+ topK: intParam(url, "top_k", { min: 1, max: 100 }),
160
+ profile: stringParam(url, "profile"),
161
+ engine: stringParam(url, "engine"),
162
+ scope: parseScopes(url.searchParams.get("scope")),
163
+ tag: stringParam(url, "tag"),
164
+ });
165
+ sendJson(res, 200, {
166
+ query: result.query,
167
+ top_k: result.top_k,
168
+ engine: result.engineId,
169
+ degraded: result.degraded,
170
+ degrade_reason: result.degradeReason ?? null,
171
+ profile: result.profile,
172
+ profile_matched: result.profileMatched,
173
+ scope: result.scope,
174
+ scope_matched: result.scopeMatched,
175
+ candidate_count: result.candidateCount,
176
+ index_fresh: result.indexFresh,
177
+ index_rebuild_recommended: result.indexRebuildRecommended,
178
+ index_source: result.indexSource,
179
+ index_reason: result.indexReason,
180
+ decision: result.decision
181
+ ? {
182
+ kind: result.decision.kind,
183
+ model: result.decision.model,
184
+ selected: result.decision.selected,
185
+ confidence: Number(result.decision.confidence.toFixed(4)),
186
+ selected_probability: Number(result.decision.selectedProbability.toFixed(4)),
187
+ none_probability: Number(result.decision.noneProbability.toFixed(4)),
188
+ threshold: Number(result.decision.threshold.toFixed(4)),
189
+ escalation_recommended: result.decision.escalationRecommended,
190
+ probabilities: Object.fromEntries(Object.entries(result.decision.probabilities).map(([skill, probability]) => [
191
+ skill,
192
+ Number(probability.toFixed(4)),
193
+ ])),
194
+ }
195
+ : null,
196
+ results: result.results.map((r) => ({
197
+ name: r.name,
198
+ score: Number(r.score.toFixed(4)),
199
+ description: r.description,
200
+ path: r.path,
201
+ provenance: r.provenance ?? null,
202
+ reasons: r.reasons,
203
+ tags: r.tags,
204
+ })),
205
+ });
206
+ return;
207
+ }
208
+ if (url.pathname === "/api/audit") {
209
+ sendJson(res, 200, await skillAudit({ name: stringParam(url, "name") }));
210
+ return;
211
+ }
212
+ if (resource === "skills") {
213
+ const name = segments[2] ? decodeURIComponent(segments[2]) : undefined;
214
+ const sub = segments[3];
215
+ if (!name) {
216
+ sendJson(res, 200, await skillList({
217
+ query: stringParam(url, "query"),
218
+ tag: stringParam(url, "tag"),
219
+ offset: intParam(url, "offset", { min: 0, max: 10485760 }),
220
+ limit: intParam(url, "limit", { min: 1, max: 100 }),
221
+ }));
222
+ return;
223
+ }
224
+ switch (sub) {
225
+ case undefined:
226
+ sendJson(res, 200, await skillFetch({
227
+ name,
228
+ offset: intParam(url, "offset", { min: 0, max: 10485760 }),
229
+ maxChars: intParam(url, "max_chars", { min: 1, max: 100000 }),
230
+ }));
231
+ return;
232
+ case "files":
233
+ sendJson(res, 200, await skillFiles({
234
+ name,
235
+ path: stringParam(url, "path"),
236
+ offset: intParam(url, "offset", { min: 0, max: 10485760 }),
237
+ limit: intParam(url, "limit", { min: 1, max: 100 }),
238
+ }));
239
+ return;
240
+ case "read":
241
+ sendJson(res, 200, await skillRead({
242
+ name,
243
+ file: stringParam(url, "file"),
244
+ offset: intParam(url, "offset", { min: 0, max: 10485760 }),
245
+ maxChars: intParam(url, "max_chars", { min: 1, max: 100000 }),
246
+ }));
247
+ return;
248
+ case "explain":
249
+ sendJson(res, 200, await skillExplain({ name }));
250
+ return;
251
+ case "history":
252
+ sendJson(res, 200, await skillHistory({
253
+ name,
254
+ limit: intParam(url, "limit", { min: 1, max: 1000 }),
255
+ }));
256
+ return;
257
+ case "audit":
258
+ sendJson(res, 200, await skillAudit({ name }));
259
+ return;
260
+ default:
261
+ sendJson(res, 404, {
262
+ ok: false,
263
+ code: "E_NOT_FOUND",
264
+ message: `未知资源: ${url.pathname}`,
265
+ });
266
+ return;
267
+ }
268
+ }
269
+ sendJson(res, 404, { ok: false, code: "E_NOT_FOUND", message: `未知资源: ${url.pathname}` });
270
+ }
271
+ async function serveStatic(res, pathname) {
272
+ const rel = pathname === "/" ? "index.html" : pathname.replace(/^\/+/, "");
273
+ const abs = path.normalize(path.join(PUBLIC_DIR, rel));
274
+ if (!abs.startsWith(PUBLIC_DIR + path.sep) && abs !== PUBLIC_DIR) {
275
+ sendJson(res, 403, { ok: false, code: "E_PATH", message: "禁止越界访问" });
276
+ return;
277
+ }
278
+ try {
279
+ const content = await readFile(abs);
280
+ const ext = path.extname(abs).toLowerCase();
281
+ res.writeHead(200, {
282
+ "content-type": MIME_TYPES[ext] ?? "application/octet-stream",
283
+ "cache-control": "no-store",
284
+ });
285
+ res.end(content);
286
+ }
287
+ catch {
288
+ // SPA hash 路由不需要服务端回退;其余一律 404
289
+ sendJson(res, 404, { ok: false, code: "E_NOT_FOUND", message: `文件不存在: ${rel}` });
290
+ }
291
+ }
292
+ export function createWebServer() {
293
+ return createServer((req, res) => {
294
+ void withUsageScope("web", async () => {
295
+ const url = new URL(req.url ?? "/", `http://${DEFAULT_HOST}`);
296
+ try {
297
+ if (url.pathname.startsWith("/api/")) {
298
+ await handleApi(req, res, url);
299
+ }
300
+ else {
301
+ await serveStatic(res, decodeURIComponent(url.pathname));
302
+ }
303
+ }
304
+ catch (err) {
305
+ sendJson(res, statusForError(err), errorPayload(err));
306
+ }
307
+ });
308
+ });
309
+ }
310
+ export async function main(options = {}) {
311
+ const port = options.port ?? Number(process.env.SKILL_HUB_WEB_PORT ?? DEFAULT_PORT);
312
+ const host = options.host ?? process.env.SKILL_HUB_WEB_HOST ?? DEFAULT_HOST;
313
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
314
+ throw new SkillHubError({ code: "E_CONFIG", message: `端口无效: ${port}` });
315
+ }
316
+ const server = createWebServer();
317
+ await new Promise((resolve, reject) => {
318
+ server.once("error", reject);
319
+ server.listen(port, host, () => resolve());
320
+ });
321
+ console.error(`[skill-hub-web] v${SKILL_HUB_VERSION} 只读视图已启动: http://${host}:${port}`);
322
+ return server;
323
+ }
324
+ function parseCliArgs(argv) {
325
+ const options = {};
326
+ for (let i = 0; i < argv.length; i += 1) {
327
+ const arg = argv[i];
328
+ const next = argv[i + 1];
329
+ if (arg === "--port" && next) {
330
+ options.port = Number(next);
331
+ i += 1;
332
+ }
333
+ else if (arg === "--host" && next) {
334
+ options.host = next;
335
+ i += 1;
336
+ }
337
+ }
338
+ return options;
339
+ }
340
+ function shouldAutoStart() {
341
+ if (process.env.SKILL_HUB_WEB_NO_START === "1")
342
+ return false;
343
+ const entry = process.argv[1];
344
+ if (!entry)
345
+ return false;
346
+ if (entry === "-" || entry === "--")
347
+ return false;
348
+ return true;
349
+ }
350
+ if (shouldAutoStart()) {
351
+ main(parseCliArgs(process.argv.slice(2))).catch((err) => {
352
+ console.error(err instanceof Error ? (err.stack ?? err.message) : err);
353
+ process.exit(1);
354
+ });
355
+ }