@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,265 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, timingSafeEqual } from "node:crypto";
3
+ import { createServer } from "node:http";
4
+ import { realpathSync } from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
7
+ import { SkillHubError, SKILL_HUB_VERSION } from "#skill-hub/shared";
8
+ import { createSkillHubMcpServer } from "./server.js";
9
+ const MAX_BODY_BYTES = 1024 * 1024;
10
+ const MAX_CONCURRENT_REQUESTS = 16;
11
+ const REQUEST_TIMEOUT_MS = 30_000;
12
+ function configError(message) {
13
+ throw new SkillHubError({ code: "E_CONFIG", message });
14
+ }
15
+ function digest(value) {
16
+ return createHash("sha256").update(value).digest();
17
+ }
18
+ function reply(res, status, message) {
19
+ if (res.destroyed || res.writableEnded)
20
+ return;
21
+ if (res.headersSent) {
22
+ res.destroy();
23
+ return;
24
+ }
25
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
26
+ res.end(JSON.stringify({ error: message }));
27
+ }
28
+ async function readBody(req, res) {
29
+ const chunks = [];
30
+ let bytes = 0;
31
+ const body = await new Promise((resolve, reject) => {
32
+ const cleanup = () => {
33
+ req.off("data", onData);
34
+ req.off("end", onEnd);
35
+ req.off("error", onError);
36
+ res.off("close", onClose);
37
+ };
38
+ const onError = (error) => {
39
+ cleanup();
40
+ reject(error);
41
+ };
42
+ const onClose = () => onError(new Error("Request closed"));
43
+ const onEnd = () => {
44
+ cleanup();
45
+ resolve(Buffer.concat(chunks));
46
+ };
47
+ const onData = (chunk) => {
48
+ bytes += chunk.length;
49
+ if (bytes > MAX_BODY_BYTES) {
50
+ reply(res, 413, "Request body too large");
51
+ onError(new Error("Request body too large"));
52
+ req.resume();
53
+ return;
54
+ }
55
+ chunks.push(chunk);
56
+ };
57
+ req.on("data", onData);
58
+ req.once("end", onEnd);
59
+ req.once("error", onError);
60
+ res.once("close", onClose);
61
+ });
62
+ try {
63
+ return JSON.parse(body.toString("utf8"));
64
+ }
65
+ catch {
66
+ reply(res, 400, "Invalid JSON");
67
+ throw new Error("Invalid JSON");
68
+ }
69
+ }
70
+ /** A single-tenant, stateless Streamable HTTP endpoint. No stdio or user-directory writes on import. */
71
+ export function createSkillHubHttpServer(options) {
72
+ if (typeof options.token !== "string" ||
73
+ options.token.length < 32 ||
74
+ !/^[A-Za-z0-9._~+/-]+=*$/.test(options.token)) {
75
+ configError("SKILL_HUB_HTTP_TOKEN 需要至少32字符的Bearer token");
76
+ }
77
+ const allowedHosts = options.allowedHosts?.map((value) => value.toLowerCase());
78
+ if (allowedHosts &&
79
+ (!allowedHosts.length ||
80
+ allowedHosts.some((value) => {
81
+ try {
82
+ return (!/^(?:[a-z0-9][a-z0-9.-]*|\[[0-9a-f:]+\])(?::[0-9]+)?$/.test(value) ||
83
+ new URL(`http://${value}`).host !== value);
84
+ }
85
+ catch {
86
+ return true;
87
+ }
88
+ })))
89
+ configError("SKILL_HUB_HTTP_ALLOWED_HOSTS 必须是精确host[:port]列表");
90
+ const allowedOrigins = options.allowedOrigins ?? [];
91
+ if (allowedOrigins.some((value) => {
92
+ try {
93
+ const url = new URL(value);
94
+ return !["https:", "http:"].includes(url.protocol) || url.origin !== value;
95
+ }
96
+ catch {
97
+ return true;
98
+ }
99
+ }))
100
+ configError("SKILL_HUB_HTTP_ALLOWED_ORIGINS 必须是精确HTTP(S) Origin列表");
101
+ const expectedToken = digest(`Bearer ${options.token}`);
102
+ let active = 0;
103
+ const server = createServer((req, res) => {
104
+ res.setHeader("cache-control", "no-store");
105
+ res.setHeader("x-content-type-options", "nosniff");
106
+ // Never trust Forwarded/X-Forwarded-*; proxies must preserve a configured Host.
107
+ const address = server.address();
108
+ const port = address && typeof address === "object" ? address.port : 0;
109
+ const hosts = allowedHosts ?? [`127.0.0.1:${port}`, `localhost:${port}`, `[::1]:${port}`];
110
+ if (!req.headers.host ||
111
+ !hosts.includes(req.headers.host.toLowerCase()) ||
112
+ (req.headers.origin !== undefined && !allowedOrigins.includes(req.headers.origin))) {
113
+ reply(res, 403, "Host or Origin not allowed");
114
+ req.resume();
115
+ return;
116
+ }
117
+ if (!timingSafeEqual(expectedToken, digest(req.headers.authorization ?? ""))) {
118
+ res.setHeader("www-authenticate", 'Bearer realm="skill-hub"');
119
+ reply(res, 401, "Unauthorized");
120
+ req.resume();
121
+ return;
122
+ }
123
+ // Exact paths only: no absolute-form target, query, encoded alias or Web REST fallback.
124
+ if (req.url === "/healthz" && req.method === "GET") {
125
+ res.writeHead(200, { "content-type": "application/json" });
126
+ res.end(JSON.stringify({ ok: true, name: "skill-hub-mcp-http", version: SKILL_HUB_VERSION }));
127
+ return;
128
+ }
129
+ if (req.url !== "/mcp") {
130
+ reply(res, 404, "Not found");
131
+ req.resume();
132
+ return;
133
+ }
134
+ if (req.method !== "POST") {
135
+ res.setHeader("allow", "POST");
136
+ reply(res, 405, "Stateless endpoint supports POST only");
137
+ req.resume();
138
+ return;
139
+ }
140
+ if (active >= MAX_CONCURRENT_REQUESTS) {
141
+ res.setHeader("retry-after", "1");
142
+ reply(res, 429, "Too many concurrent requests");
143
+ req.resume();
144
+ return;
145
+ }
146
+ if (req.headers["content-encoding"] !== undefined ||
147
+ req.headers["content-type"]?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
148
+ reply(res, 415, "Expected uncompressed application/json");
149
+ req.resume();
150
+ return;
151
+ }
152
+ if (Number(req.headers["content-length"] ?? 0) > MAX_BODY_BYTES) {
153
+ reply(res, 413, "Request body too large");
154
+ req.resume();
155
+ return;
156
+ }
157
+ active++;
158
+ let mcp;
159
+ let closed = false;
160
+ const timeout = setTimeout(() => reply(res, 504, "Request timed out"), REQUEST_TIMEOUT_MS);
161
+ res.once("close", () => {
162
+ closed = true;
163
+ active--;
164
+ clearTimeout(timeout);
165
+ // Closing a transport aborts protocol requests and releases its SSE response streams.
166
+ if (mcp)
167
+ void mcp.close().catch(() => console.error("[skill-hub-http] cleanup failed"));
168
+ });
169
+ void (async () => {
170
+ const body = await readBody(req, res);
171
+ if (closed)
172
+ return;
173
+ mcp = await createSkillHubMcpServer({ mode: "runtime", exposure: "remote" });
174
+ if (closed) {
175
+ await mcp.close();
176
+ return;
177
+ }
178
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
179
+ await mcp.connect(transport);
180
+ await transport.handleRequest(req, res, body);
181
+ })().catch(() => {
182
+ // Do not expose filesystem paths, request bodies, auth headers or provider errors.
183
+ if (!res.writableEnded && !res.destroyed) {
184
+ console.error("[skill-hub-http] request failed");
185
+ reply(res, 500, "Internal server error");
186
+ }
187
+ });
188
+ });
189
+ server.headersTimeout = 10_000;
190
+ server.requestTimeout = REQUEST_TIMEOUT_MS;
191
+ server.keepAliveTimeout = 5_000;
192
+ return server;
193
+ }
194
+ function envList(name) {
195
+ const value = process.env[name];
196
+ return value === undefined ? undefined : value.split(",").map((item) => item.trim());
197
+ }
198
+ export async function startSkillHubHttpServer(options) {
199
+ const host = options.host ?? "127.0.0.1";
200
+ const port = options.port ?? 8788;
201
+ if (!Number.isSafeInteger(port) || port < 0 || port > 65535)
202
+ configError("HTTP端口无效");
203
+ if (!["127.0.0.1", "localhost", "::1"].includes(host) && !options.allowedHosts?.length) {
204
+ configError("非loopback监听必须显式配置SKILL_HUB_HTTP_ALLOWED_HOSTS");
205
+ }
206
+ const server = createSkillHubHttpServer(options);
207
+ await new Promise((resolve, reject) => {
208
+ server.once("error", reject);
209
+ server.listen(port, host, () => {
210
+ server.off("error", reject);
211
+ resolve();
212
+ });
213
+ });
214
+ return server;
215
+ }
216
+ /** Drain requests; force-close sockets after five seconds. No process exit in library use. */
217
+ export async function stopSkillHubHttpServer(server) {
218
+ if (!server.listening)
219
+ return;
220
+ const timeout = setTimeout(() => server.closeAllConnections(), 5_000);
221
+ try {
222
+ await new Promise((resolve, reject) => {
223
+ server.close((error) => (error ? reject(error) : resolve()));
224
+ });
225
+ }
226
+ finally {
227
+ clearTimeout(timeout);
228
+ }
229
+ }
230
+ async function main() {
231
+ const server = await startSkillHubHttpServer({
232
+ token: process.env.SKILL_HUB_HTTP_TOKEN ?? "",
233
+ host: process.env.SKILL_HUB_HTTP_HOST,
234
+ port: Number(process.env.SKILL_HUB_HTTP_PORT ?? process.env.PORT ?? 8788),
235
+ allowedHosts: envList("SKILL_HUB_HTTP_ALLOWED_HOSTS"),
236
+ allowedOrigins: envList("SKILL_HUB_HTTP_ALLOWED_ORIGINS"),
237
+ });
238
+ const address = server.address();
239
+ console.error(`[skill-hub-http] v${SKILL_HUB_VERSION} runtime listening on ${address && typeof address === "object" ? `${address.address}:${address.port}` : address}/mcp`);
240
+ const shutdown = () => {
241
+ process.off("SIGINT", shutdown);
242
+ process.off("SIGTERM", shutdown);
243
+ void stopSkillHubHttpServer(server).catch(() => {
244
+ process.exitCode = 1;
245
+ });
246
+ };
247
+ process.once("SIGINT", shutdown);
248
+ process.once("SIGTERM", shutdown);
249
+ }
250
+ function isHttpEntry() {
251
+ if (!process.argv[1])
252
+ return false;
253
+ try {
254
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
255
+ }
256
+ catch {
257
+ return false; // A synthetic/nonexistent argv entry is a library import, not this CLI.
258
+ }
259
+ }
260
+ if (isHttpEntry()) {
261
+ main().catch((error) => {
262
+ console.error(error instanceof SkillHubError ? error.message : "[skill-hub-http] startup failed");
263
+ process.exitCode = 1;
264
+ });
265
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { SKILL_HUB_VERSION } from "#skill-hub/shared";
4
+ import { createSkillHubMcpServer } from "./server.js";
5
+ export { createSkillHubMcpServer } from "./server.js";
6
+ export async function main() {
7
+ // 诊断:只写 stderr,避免污染 MCP stdio JSON-RPC
8
+ console.error(`[skill-hub-mcp] starting v${SKILL_HUB_VERSION} pid=${process.pid}`);
9
+ const server = await createSkillHubMcpServer();
10
+ const transport = new StdioServerTransport();
11
+ await server.connect(transport);
12
+ console.error("[skill-hub-mcp] stdio transport connected");
13
+ }
14
+ /** 库侧探测(非 MCP 进程)。 */
15
+ export function mcpServerPlaceholder() {
16
+ return `skill-hub mcp-server ${SKILL_HUB_VERSION}`;
17
+ }
18
+ /**
19
+ * 默认作为 stdio MCP 入口启动。
20
+ * - 宿主(WorkBuddy/Cursor)通过 `node …/index.js` 拉起时必须进入 main
21
+ * - 仅当被库 import 且设置 SKILL_HUB_MCP_NO_START=1 时跳过
22
+ * - 旧逻辑用 path.endsWith 判断,路径被 wrapper 改写时会静默不启动 → Connection closed
23
+ */
24
+ function shouldAutoStart() {
25
+ if (process.env.SKILL_HUB_MCP_NO_START === "1")
26
+ return false;
27
+ if (process.env.SKILL_HUB_MCP_FORCE_START === "1")
28
+ return true;
29
+ // 作为 CLI / MCP 入口:有 argv[1] 且不是 -e / 交互 REPL
30
+ const entry = process.argv[1];
31
+ if (!entry)
32
+ return false;
33
+ if (entry === "-" || entry === "--")
34
+ return false;
35
+ return true;
36
+ }
37
+ if (shouldAutoStart()) {
38
+ main().catch((err) => {
39
+ console.error(err instanceof Error ? (err.stack ?? err.message) : err);
40
+ process.exit(1);
41
+ });
42
+ }