@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.
- package/LICENSE +21 -0
- package/NOTICE.md +7 -0
- package/README.md +32 -2
- package/apps/cli/dist/args.js +111 -0
- package/apps/cli/dist/commands/audit.js +38 -0
- package/apps/cli/dist/commands/backup.js +34 -0
- package/apps/cli/dist/commands/doctor.js +86 -0
- package/apps/cli/dist/commands/index-cmd.js +122 -0
- package/apps/cli/dist/commands/ingest.js +198 -0
- package/apps/cli/dist/commands/init.js +110 -0
- package/apps/cli/dist/commands/inspect.js +68 -0
- package/apps/cli/dist/commands/inventory.js +259 -0
- package/apps/cli/dist/commands/manage.js +149 -0
- package/apps/cli/dist/commands/manifest.js +127 -0
- package/apps/cli/dist/commands/restore.js +37 -0
- package/apps/cli/dist/commands/route.js +92 -0
- package/apps/cli/dist/commands/runtime.js +89 -0
- package/apps/cli/dist/commands/sync.js +195 -0
- package/apps/cli/dist/commands/takeover.js +45 -0
- package/apps/cli/dist/commands/usage.js +55 -0
- package/apps/cli/dist/commands/verify.js +48 -0
- package/apps/cli/dist/index.js +468 -0
- package/apps/cli/dist/output.js +7 -0
- package/apps/mcp-server/dist/http.js +265 -0
- package/apps/mcp-server/dist/index.js +42 -0
- package/apps/mcp-server/dist/server.js +541 -0
- package/apps/web/dist/index.js +355 -0
- package/apps/web/public/app.js +927 -0
- package/apps/web/public/index.html +109 -0
- package/apps/web/public/styles.css +1796 -0
- package/configs/agents/default.yaml +44 -0
- package/configs/models.yaml +55 -0
- package/configs/profiles/coding.yaml +29 -0
- package/configs/profiles/video.yaml +18 -0
- package/configs/taxonomy.yaml +24 -0
- package/package.json +42 -4
- package/packages/core/dist/agent-sources.js +38 -0
- package/packages/core/dist/audit.js +275 -0
- package/packages/core/dist/catalog.js +173 -0
- package/packages/core/dist/config.js +295 -0
- package/packages/core/dist/conflict.js +95 -0
- package/packages/core/dist/context-cost.js +19 -0
- package/packages/core/dist/frontmatter.js +118 -0
- package/packages/core/dist/hash.js +7 -0
- package/packages/core/dist/history.js +60 -0
- package/packages/core/dist/index.js +23 -0
- package/packages/core/dist/managed.js +1774 -0
- package/packages/core/dist/materialize.js +149 -0
- package/packages/core/dist/parse.js +74 -0
- package/packages/core/dist/scan.js +112 -0
- package/packages/core/dist/takeover.js +96 -0
- package/packages/core/dist/tree.js +61 -0
- package/packages/core/dist/types.js +4 -0
- package/packages/core/dist/usage.js +267 -0
- package/packages/router/dist/bm25.js +144 -0
- package/packages/router/dist/context-cost.js +82 -0
- package/packages/router/dist/external.js +107 -0
- package/packages/router/dist/factory.js +144 -0
- package/packages/router/dist/hub-service.js +589 -0
- package/packages/router/dist/index-store.js +153 -0
- package/packages/router/dist/index.js +22 -0
- package/packages/router/dist/llm/config.js +311 -0
- package/packages/router/dist/llm/http.js +102 -0
- package/packages/router/dist/llm/index.js +6 -0
- package/packages/router/dist/llm/ollama.js +68 -0
- package/packages/router/dist/llm/openai-compatible.js +66 -0
- package/packages/router/dist/llm/registry.js +103 -0
- package/packages/router/dist/llm/types.js +31 -0
- package/packages/router/dist/profile.js +124 -0
- package/packages/router/dist/runtime.js +270 -0
- package/packages/router/dist/scope.js +13 -0
- package/packages/router/dist/taxonomy.js +172 -0
- package/packages/router/dist/tokenize.js +38 -0
- package/packages/router/dist/types.js +1 -0
- package/packages/router/dist/typesafe.js +331 -0
- package/packages/router/dist/usage-overview.js +143 -0
- package/packages/router/dist/usage.js +76 -0
- package/packages/shared/dist/constants.js +14 -0
- package/packages/shared/dist/errors.js +90 -0
- package/packages/shared/dist/index.js +14 -0
- package/packages/shared/dist/logger.js +95 -0
- package/packages/shared/dist/paths.js +118 -0
- package/packages/shared/dist/types.js +12 -0
- package/packages/sync/dist/apply.js +227 -0
- package/packages/sync/dist/backup.js +193 -0
- package/packages/sync/dist/fs-utils.js +188 -0
- package/packages/sync/dist/index.js +18 -0
- package/packages/sync/dist/integrity.js +238 -0
- package/packages/sync/dist/plan.js +211 -0
- package/packages/sync/dist/restore.js +69 -0
- package/packages/sync/dist/scope.js +22 -0
- package/packages/sync/dist/types.js +1 -0
- package/packages/sync/dist/verify.js +189 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Skill Hub contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Third-party notices and scope
|
|
2
|
+
|
|
3
|
+
The MIT license covers Skill Hub's own source and documentation. Dependency packages retain their licenses; package names and locked versions are listed in `package.json`, workspace manifests and `pnpm-lock.yaml`.
|
|
4
|
+
|
|
5
|
+
The Agent Skills format is an open ecosystem specification: [agentskills.io](https://agentskills.io/specification). Skill Hub is an independent project, not an official product of Anthropic, OpenAI, Vercel, TypeSafe, WorkBuddy, Qoder or OpenCode.
|
|
6
|
+
|
|
7
|
+
Research documents link to public project documentation; links do not imply endorsement or a dependency on those projects. Skill Hub does not redistribute a user's production Skills. Test fixture instructions are synthetic and intended only for validation. Installed Skills, assets, scripts, plugins and model services keep their original licenses and terms.
|
package/README.md
CHANGED
|
@@ -1,3 +1,33 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Skill Hub
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
One Skill store for multiple agents: task-based search, progressive text/attachment reads, managed updates, and optional local usage insights.
|
|
4
|
+
|
|
5
|
+
This is the npm distribution candidate for [983033995/skill-hub](https://github.com/983033995/skill-hub). A generated tarball is not proof of registry publication. Requires Node.js >=20; Git is needed for GitHub-backed Skill installation.
|
|
6
|
+
|
|
7
|
+
## Install a local release candidate
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install --prefix ./hub-trial /absolute/path/to/zhangheteng-skill-hub-0.2.0.tgz
|
|
11
|
+
./hub-trial/node_modules/.bin/skill-hub version
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
After a version has actually been published to npm, install the verified package/version using your npm client. For multiple binaries, use `npm exec --package=<verified-package>@<version> -- <binary>` and choose a command below. No package install hook creates or migrates your Skill store.
|
|
15
|
+
|
|
16
|
+
| Command | Purpose |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `skill-hub` | CLI; use `help` to discover commands |
|
|
19
|
+
| `skill-hub-mcp` | Read-only stdio MCP; full by default, optional runtime |
|
|
20
|
+
| `skill-hub-mcp-http` | Bearer-authenticated Streamable HTTP MCP; three runtime tools |
|
|
21
|
+
| `skill-hub-web` | Local, unauthenticated read-only Web viewer |
|
|
22
|
+
|
|
23
|
+
## New isolated store
|
|
24
|
+
|
|
25
|
+
Set `SKILL_HUB_HOME` to a new absolute directory, then run `skill-hub init`. Use the existing CLI's preview/confirmation workflow to install or update Skills. Never run `init --force` over an existing store without understanding the replacement behavior. `skill-hub help` describes the installed CLI.
|
|
26
|
+
|
|
27
|
+
Configure stdio MCP hosts to execute `skill-hub-mcp` from the installed bin path. Set `SKILL_HUB_MCP_MODE=runtime` for the optional three-tool mode; this does not disable the host's native Skill discovery.
|
|
28
|
+
|
|
29
|
+
For HTTP, inject `SKILL_HUB_HTTP_TOKEN` (at least 32 random Bearer-token characters) into the process environment, select the server-side `SKILL_HUB_HOME`, and run `skill-hub-mcp-http`. Default listener: `127.0.0.1:8788/mcp`. Every request requires `Authorization: Bearer <token>`. Exposing a non-loopback listener requires explicit `SKILL_HUB_HTTP_ALLOWED_HOSTS`. Use HTTPS ingress and persistent storage on a hosting platform. Static Bearer authentication is not OAuth or multi-tenant isolation.
|
|
30
|
+
|
|
31
|
+
The npm registry distributes the program; it does not run your HTTP service. Skill text remains untrusted task guidance and never authorizes script execution. Optional Jev transmits task queries/metadata only through the configured/requested provider route; default BM25 works without a model key. Usage observation is disabled unless explicitly configured.
|
|
32
|
+
|
|
33
|
+
See the source repository's [hosting guide](https://github.com/983033995/skill-hub/blob/main/docs/ops/REMOTE_MCP.md), [API](https://github.com/983033995/skill-hub/blob/main/docs/architecture/API.md), [security policy](https://github.com/983033995/skill-hub/blob/main/SECURITY.md), and [license](https://github.com/983033995/skill-hub/blob/main/LICENSE). Local candidates may contain changes not yet merged into main.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 argv 解析:
|
|
3
|
+
* skill-hub [--json] [--verbose] [--config path] <command> [args] [--flag value]
|
|
4
|
+
*/
|
|
5
|
+
export function parseArgs(argv) {
|
|
6
|
+
const booleanFlags = new Set([
|
|
7
|
+
"dry-run",
|
|
8
|
+
"apply",
|
|
9
|
+
"yes",
|
|
10
|
+
"allow-write",
|
|
11
|
+
"force",
|
|
12
|
+
"rebuild",
|
|
13
|
+
"include-body",
|
|
14
|
+
"include-plugins",
|
|
15
|
+
"create-only",
|
|
16
|
+
"replace-real",
|
|
17
|
+
"no-materialize",
|
|
18
|
+
"no-canonical",
|
|
19
|
+
"require-backup",
|
|
20
|
+
]);
|
|
21
|
+
const flags = {};
|
|
22
|
+
const positionals = [];
|
|
23
|
+
let json = false;
|
|
24
|
+
let verbose = false;
|
|
25
|
+
let config;
|
|
26
|
+
let command = "";
|
|
27
|
+
let i = 0;
|
|
28
|
+
const takeValue = (cur, next) => {
|
|
29
|
+
if (next && !next.startsWith("-"))
|
|
30
|
+
return next;
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
while (i < argv.length) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === "--json") {
|
|
36
|
+
json = true;
|
|
37
|
+
i += 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (a === "--verbose" || a === "-v") {
|
|
41
|
+
verbose = true;
|
|
42
|
+
i += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (a === "--config") {
|
|
46
|
+
const v = argv[i + 1];
|
|
47
|
+
if (!v || v.startsWith("-")) {
|
|
48
|
+
throw new Error("--config 需要路径参数");
|
|
49
|
+
}
|
|
50
|
+
config = v;
|
|
51
|
+
i += 2;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (a === "--version" || a === "-V") {
|
|
55
|
+
command = "version";
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (a === "--help" || a === "-h") {
|
|
60
|
+
command = "help";
|
|
61
|
+
i += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (a.startsWith("--")) {
|
|
65
|
+
const eq = a.indexOf("=");
|
|
66
|
+
if (eq > 2) {
|
|
67
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
68
|
+
i += 1;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const key = a.slice(2);
|
|
72
|
+
if (booleanFlags.has(key)) {
|
|
73
|
+
flags[key] = true;
|
|
74
|
+
i += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const val = takeValue(a, argv[i + 1]);
|
|
78
|
+
if (val === true) {
|
|
79
|
+
flags[key] = true;
|
|
80
|
+
i += 1;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
flags[key] = val;
|
|
84
|
+
i += 2;
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!command) {
|
|
89
|
+
command = a;
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
positionals.push(a);
|
|
94
|
+
i += 1;
|
|
95
|
+
}
|
|
96
|
+
if (!command)
|
|
97
|
+
command = "help";
|
|
98
|
+
return { json, verbose, config, command, positionals, flags };
|
|
99
|
+
}
|
|
100
|
+
export function flagBool(flags, key) {
|
|
101
|
+
const v = flags[key];
|
|
102
|
+
if (v === true || v === "true" || v === "1")
|
|
103
|
+
return true;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
export function flagString(flags, key) {
|
|
107
|
+
const v = flags[key];
|
|
108
|
+
if (typeof v === "string")
|
|
109
|
+
return v;
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { skillAudit } from "#skill-hub/router";
|
|
2
|
+
import { SkillHubError } from "#skill-hub/shared";
|
|
3
|
+
import { printJson, printLines } from "../output.js";
|
|
4
|
+
const SEVERITIES = ["info", "warning", "high", "critical"];
|
|
5
|
+
export async function runAudit(options) {
|
|
6
|
+
const failOn = parseFailOn(options.failOn);
|
|
7
|
+
const report = await skillAudit({
|
|
8
|
+
name: options.name,
|
|
9
|
+
catalogPath: options.catalogPath,
|
|
10
|
+
configPath: options.configPath,
|
|
11
|
+
});
|
|
12
|
+
const failedByPolicy = report.skills.some((skill) => !skill.complete || skill.findings.some((finding) => SEVERITIES.indexOf(finding.severity) >= SEVERITIES.indexOf(failOn)));
|
|
13
|
+
const payload = { ...report, fail_on: failOn, failed_by_policy: failedByPolicy };
|
|
14
|
+
if (options.json) {
|
|
15
|
+
printJson(payload);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
printLines([
|
|
19
|
+
`audit ${options.name ?? "(all skills)"} passed=${report.summary.passed} failed=${report.summary.failed}`,
|
|
20
|
+
` findings critical=${report.summary.critical} high=${report.summary.high} warning=${report.summary.warning} info=${report.summary.info}`,
|
|
21
|
+
...report.skills.flatMap((skill) => [
|
|
22
|
+
` ${skill.passed ? "PASS" : "FAIL"} ${skill.name} files=${skill.summary.filesScanned}`,
|
|
23
|
+
...skill.findings.map((finding) => ` [${finding.severity}] ${finding.file}:${finding.line} ${finding.ruleId} — ${finding.message}`),
|
|
24
|
+
]),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
return failedByPolicy ? 2 : 0;
|
|
28
|
+
}
|
|
29
|
+
function parseFailOn(raw) {
|
|
30
|
+
const value = raw?.trim() || "high";
|
|
31
|
+
if (!SEVERITIES.includes(value)) {
|
|
32
|
+
throw new SkillHubError({
|
|
33
|
+
code: "E_CONFIG",
|
|
34
|
+
message: `--fail-on 无效: ${value}(应为 info|warning|high|critical)`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { loadHubConfig, resolveHubConfigPaths } from "#skill-hub/core";
|
|
2
|
+
import { createBackup } from "#skill-hub/sync";
|
|
3
|
+
import { getDefaultHubHome, hubLayout } from "#skill-hub/shared";
|
|
4
|
+
import { printJson, printLines } from "../output.js";
|
|
5
|
+
export async function runBackup(options) {
|
|
6
|
+
const config = resolveHubConfigPaths(options.configPath ? await loadHubConfig(options.configPath) : await loadHubConfig());
|
|
7
|
+
if (options.out) {
|
|
8
|
+
config.backup_dir = options.out;
|
|
9
|
+
}
|
|
10
|
+
const layout = hubLayout(getDefaultHubHome());
|
|
11
|
+
const manifest = await createBackup(config, {
|
|
12
|
+
notes: options.notes ?? "skill-hub backup",
|
|
13
|
+
includeCanonical: options.includeCanonical,
|
|
14
|
+
dryRun: options.dryRun,
|
|
15
|
+
hubMeta: { catalogPath: layout.catalog, configPath: layout.config },
|
|
16
|
+
});
|
|
17
|
+
if (options.json) {
|
|
18
|
+
printJson(manifest);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
printLines([
|
|
22
|
+
`backup ${options.dryRun ? "(dry-run)" : "written"}`,
|
|
23
|
+
` dir: ${manifest.backupDir}`,
|
|
24
|
+
...manifest.agents.map((a) => ` agent ${a.id.padEnd(12)} ${a.skipped ? "SKIP" : "OK"} entries=${a.entryCount} ${a.path}`),
|
|
25
|
+
manifest.canonical
|
|
26
|
+
? ` canonical ${manifest.canonical.exists ? "OK" : "MISS"} ${manifest.canonical.path}`
|
|
27
|
+
: "",
|
|
28
|
+
options.dryRun
|
|
29
|
+
? " note: dry-run 未复制文件"
|
|
30
|
+
: ` manifest: ${manifest.backupDir}/manifest.json`,
|
|
31
|
+
].filter(Boolean));
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { access, readdir } from "node:fs/promises";
|
|
2
|
+
import { constants as fsConstants } from "node:fs";
|
|
3
|
+
import { defaultHubConfig, loadHubConfig, resolveHubConfigPaths, scanSkills, } from "#skill-hub/core";
|
|
4
|
+
import { SKILL_HUB_VERSION, expandUserPath, getDefaultHubHome, hubLayout, } from "#skill-hub/shared";
|
|
5
|
+
import { printJson, printLines } from "../output.js";
|
|
6
|
+
export async function runDoctor(options) {
|
|
7
|
+
const hubHome = getDefaultHubHome();
|
|
8
|
+
const layout = hubLayout(hubHome);
|
|
9
|
+
let config;
|
|
10
|
+
try {
|
|
11
|
+
config = resolveHubConfigPaths(options.configPath
|
|
12
|
+
? await loadHubConfig(options.configPath)
|
|
13
|
+
: await loadHubConfig().catch(() => defaultHubConfig()));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
config = resolveHubConfigPaths(defaultHubConfig());
|
|
17
|
+
}
|
|
18
|
+
const agents = [];
|
|
19
|
+
for (const a of config.agents) {
|
|
20
|
+
const p = expandUserPath(a.skills_dir);
|
|
21
|
+
let exists = false;
|
|
22
|
+
let count = 0;
|
|
23
|
+
try {
|
|
24
|
+
await access(p, fsConstants.R_OK);
|
|
25
|
+
exists = true;
|
|
26
|
+
// 只统计一级子目录中有 SKILL.md 的数量(轻量);完整 scan 留给 inventory
|
|
27
|
+
try {
|
|
28
|
+
const skills = await scanSkills(p, { agentId: a.id, maxDepth: 3 });
|
|
29
|
+
count = skills.length;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
const entries = await readdir(p).catch(() => []);
|
|
33
|
+
count = entries.length;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
exists = false;
|
|
38
|
+
}
|
|
39
|
+
agents.push({
|
|
40
|
+
id: a.id,
|
|
41
|
+
path: p,
|
|
42
|
+
enabled: a.enabled,
|
|
43
|
+
exists,
|
|
44
|
+
count,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const issues = [];
|
|
48
|
+
let canonicalExists = false;
|
|
49
|
+
try {
|
|
50
|
+
await access(layout.home, fsConstants.R_OK);
|
|
51
|
+
canonicalExists = true;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
issues.push(`hub home 不存在(可运行 skill-hub init): ${layout.home}`);
|
|
55
|
+
}
|
|
56
|
+
for (const a of agents) {
|
|
57
|
+
if (a.enabled && !a.exists) {
|
|
58
|
+
issues.push(`agent ${a.id} skills 目录不存在: ${a.path}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const result = {
|
|
62
|
+
ok: issues.length === 0,
|
|
63
|
+
version: SKILL_HUB_VERSION,
|
|
64
|
+
node: process.version,
|
|
65
|
+
canonical: { path: layout.home, exists: canonicalExists },
|
|
66
|
+
catalog: layout.catalog,
|
|
67
|
+
agents,
|
|
68
|
+
issues,
|
|
69
|
+
};
|
|
70
|
+
if (options.json) {
|
|
71
|
+
printJson(result);
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
printLines([
|
|
75
|
+
`skill-hub doctor v${SKILL_HUB_VERSION}`,
|
|
76
|
+
`node: ${process.version}`,
|
|
77
|
+
`hub: ${layout.home} (${canonicalExists ? "exists" : "missing"})`,
|
|
78
|
+
"",
|
|
79
|
+
"agents:",
|
|
80
|
+
...agents.map((a) => ` - ${a.id.padEnd(12)} ${a.exists ? "OK" : "MISS"} count≈${a.count} ${a.path}`),
|
|
81
|
+
"",
|
|
82
|
+
issues.length === 0 ? "issues: (none)" : `issues:\n${issues.map((i) => ` - ${i}`).join("\n")}`,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
return result.ok ? 0 : 3;
|
|
86
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdir, writeFile, lstat, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { appendHistory, loadHubConfig, resolveHubConfigPaths, catalogToSkillMetas, catalogHash, createEmptyCatalog, readCatalog, scanSkills, skillMetaToCatalogEntry, writeCatalog, } from "#skill-hub/core";
|
|
4
|
+
import { createBm25Router } from "#skill-hub/router";
|
|
5
|
+
import { SkillHubError, getDefaultHubHome, hubLayout, resolveAbsolutePath, isPathInside, } from "#skill-hub/shared";
|
|
6
|
+
import { printJson, printLines } from "../output.js";
|
|
7
|
+
export async function runIndex(options) {
|
|
8
|
+
const layout = hubLayout(getDefaultHubHome());
|
|
9
|
+
const config = resolveHubConfigPaths(await loadHubConfig(options.configPath));
|
|
10
|
+
const catalogPath = options.catalogPath
|
|
11
|
+
? resolveAbsolutePath(options.catalogPath)
|
|
12
|
+
: layout.catalog;
|
|
13
|
+
const indexDir = options.indexDir ? resolveAbsolutePath(options.indexDir) : config.index_dir;
|
|
14
|
+
let catalog;
|
|
15
|
+
try {
|
|
16
|
+
catalog = await readCatalog(catalogPath);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
const exists = await lstat(catalogPath).catch((e) => {
|
|
20
|
+
if (e.code === "ENOENT")
|
|
21
|
+
return null;
|
|
22
|
+
throw e;
|
|
23
|
+
});
|
|
24
|
+
if (exists)
|
|
25
|
+
throw err;
|
|
26
|
+
if (!options.rebuild) {
|
|
27
|
+
throw new SkillHubError({
|
|
28
|
+
code: "E_INDEX",
|
|
29
|
+
message: `catalog 不存在,请先 ingest 或 index --rebuild 从 canonical 扫描: ${catalogPath}`,
|
|
30
|
+
details: { path: catalogPath },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catalog = createEmptyCatalog();
|
|
34
|
+
}
|
|
35
|
+
let skills = catalogToSkillMetas(catalog);
|
|
36
|
+
if (options.rebuild || skills.length === 0) {
|
|
37
|
+
{
|
|
38
|
+
const issues = [];
|
|
39
|
+
skills = await scanSkills(config.canonical_dir, {
|
|
40
|
+
onIssue: (issue) => issues.push(issue),
|
|
41
|
+
source: config.canonical_dir,
|
|
42
|
+
provenance: {
|
|
43
|
+
scope: "user",
|
|
44
|
+
sourceType: "local",
|
|
45
|
+
sourceRef: config.canonical_dir,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (issues.length)
|
|
49
|
+
throw new SkillHubError({
|
|
50
|
+
code: "E_INDEX",
|
|
51
|
+
message: `canonical 扫描不完整: ${JSON.stringify(issues)}`,
|
|
52
|
+
});
|
|
53
|
+
const previousEntries = catalog.skills;
|
|
54
|
+
const physicalCanonical = await realpath(config.canonical_dir);
|
|
55
|
+
catalog = {
|
|
56
|
+
version: 1,
|
|
57
|
+
updatedAt: new Date().toISOString(),
|
|
58
|
+
skills: Object.fromEntries([
|
|
59
|
+
...Object.entries(previousEntries).filter(([, entry]) => !isPathInside(entry.path, config.canonical_dir) &&
|
|
60
|
+
!isPathInside(entry.path, physicalCanonical)),
|
|
61
|
+
...skills.map((s) => {
|
|
62
|
+
const entry = skillMetaToCatalogEntry(s);
|
|
63
|
+
const previous = previousEntries[s.name];
|
|
64
|
+
if (previous?.provenance)
|
|
65
|
+
entry.provenance = { ...previous.provenance };
|
|
66
|
+
if (previous?.source)
|
|
67
|
+
entry.source = previous.source;
|
|
68
|
+
return [s.name, entry];
|
|
69
|
+
}),
|
|
70
|
+
]),
|
|
71
|
+
};
|
|
72
|
+
await writeCatalog(catalogPath, catalog);
|
|
73
|
+
skills = catalogToSkillMetas(catalog);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const router = createBm25Router();
|
|
77
|
+
await router.build(skills);
|
|
78
|
+
await mkdir(indexDir, { recursive: true });
|
|
79
|
+
const currentCatalogHash = catalogHash(catalog);
|
|
80
|
+
const artifactPath = path.join(indexDir, "bm25.json");
|
|
81
|
+
await writeFile(artifactPath, `${JSON.stringify(router.toArtifact(), null, 2)}\n`);
|
|
82
|
+
const meta = {
|
|
83
|
+
version: 1,
|
|
84
|
+
engine: "bm25",
|
|
85
|
+
builtAt: new Date().toISOString(),
|
|
86
|
+
skillCount: skills.length,
|
|
87
|
+
catalogHash: currentCatalogHash,
|
|
88
|
+
artifact: path.basename(artifactPath),
|
|
89
|
+
artifactFormat: "skill-hub.bm25.v2",
|
|
90
|
+
};
|
|
91
|
+
await writeFile(path.join(indexDir, "index-meta.json"), `${JSON.stringify(meta, null, 2)}\n`);
|
|
92
|
+
await appendHistory(layout.history, {
|
|
93
|
+
at: meta.builtAt,
|
|
94
|
+
type: "index",
|
|
95
|
+
action: options.rebuild ? "rebuild" : "build",
|
|
96
|
+
detail: {
|
|
97
|
+
engine: meta.engine,
|
|
98
|
+
skillCount: meta.skillCount,
|
|
99
|
+
catalogHash: meta.catalogHash,
|
|
100
|
+
artifact: artifactPath,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
const result = {
|
|
104
|
+
ok: true,
|
|
105
|
+
...meta,
|
|
106
|
+
catalogPath,
|
|
107
|
+
indexDir,
|
|
108
|
+
};
|
|
109
|
+
if (options.quiet)
|
|
110
|
+
return 0;
|
|
111
|
+
if (options.json)
|
|
112
|
+
printJson(result);
|
|
113
|
+
else {
|
|
114
|
+
printLines([
|
|
115
|
+
`index built engine=bm25 skills=${meta.skillCount}`,
|
|
116
|
+
` catalog: ${catalogPath}`,
|
|
117
|
+
` meta: ${path.join(indexDir, "index-meta.json")}`,
|
|
118
|
+
` artifact: ${artifactPath}`,
|
|
119
|
+
]);
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { appendHistoryBatch, createEmptyCatalog, detectConflicts, hashSkillTree, materializeToCanonical, readCatalog, scanSkills, sourceMappingToProvenance, upsertSkills, writeCatalog, } from "#skill-hub/core";
|
|
3
|
+
import { getDefaultHubHome, hubLayout, resolveAbsolutePath, isPathInside, } from "#skill-hub/shared";
|
|
4
|
+
import { printJson, printLines } from "../output.js";
|
|
5
|
+
export async function runIngest(options) {
|
|
6
|
+
if (options.sources.length === 0) {
|
|
7
|
+
throw new Error("ingest 需要 --sources <dir>[,<dir>...]");
|
|
8
|
+
}
|
|
9
|
+
const layout = hubLayout(getDefaultHubHome());
|
|
10
|
+
const catalogPath = options.catalogPath
|
|
11
|
+
? resolveAbsolutePath(options.catalogPath)
|
|
12
|
+
: layout.catalog;
|
|
13
|
+
let catalog;
|
|
14
|
+
try {
|
|
15
|
+
catalog = await readCatalog(catalogPath);
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const exists = await lstat(catalogPath).catch((e) => {
|
|
19
|
+
if (e.code === "ENOENT")
|
|
20
|
+
return null;
|
|
21
|
+
throw e;
|
|
22
|
+
});
|
|
23
|
+
if (exists)
|
|
24
|
+
throw err;
|
|
25
|
+
catalog = createEmptyCatalog();
|
|
26
|
+
}
|
|
27
|
+
const previousCatalog = catalog;
|
|
28
|
+
const canonicalDir = resolveAbsolutePath(options.canonicalDir ?? layout.skills);
|
|
29
|
+
const scanned = [];
|
|
30
|
+
const resolvedSources = [];
|
|
31
|
+
for (const requested of options.sources) {
|
|
32
|
+
const mapping = (options.sourceMappings ?? []).find((s) => s.id === requested || resolveAbsolutePath(s.path) === resolveAbsolutePath(requested));
|
|
33
|
+
const abs = resolveAbsolutePath(mapping?.path ?? requested);
|
|
34
|
+
const provenance = mapping
|
|
35
|
+
? sourceMappingToProvenance(mapping, abs)
|
|
36
|
+
: { scope: "user", sourceType: "local", sourceRef: abs };
|
|
37
|
+
const scanIssues = [];
|
|
38
|
+
const skills = await scanSkills(abs, {
|
|
39
|
+
onIssue: (issue) => scanIssues.push(issue),
|
|
40
|
+
source: abs,
|
|
41
|
+
provenance,
|
|
42
|
+
maxDepth: 5,
|
|
43
|
+
});
|
|
44
|
+
if (scanIssues.length)
|
|
45
|
+
throw new Error(`来源扫描不完整: ${JSON.stringify(scanIssues)}`);
|
|
46
|
+
scanned.push(...skills);
|
|
47
|
+
resolvedSources.push({ requested, path: abs, sourceId: mapping?.id });
|
|
48
|
+
}
|
|
49
|
+
// 附件参与冲突检测;同名冲突未明确选源时不导入任何一方。
|
|
50
|
+
const trees = new Map();
|
|
51
|
+
for (const skill of scanned)
|
|
52
|
+
trees.set(skill.path, await hashSkillTree(skill.path));
|
|
53
|
+
const conflicts = detectConflicts(scanned.map((skill) => ({ ...skill, hash: trees.get(skill.path) })));
|
|
54
|
+
const conflictNames = new Set(conflicts.map((c) => c.name));
|
|
55
|
+
const groups = new Map();
|
|
56
|
+
for (const skill of scanned)
|
|
57
|
+
groups.set(skill.name, [...(groups.get(skill.name) ?? []), skill]);
|
|
58
|
+
const selected = [];
|
|
59
|
+
const unresolved = [];
|
|
60
|
+
for (const [name, variants] of groups) {
|
|
61
|
+
if (!conflictNames.has(name)) {
|
|
62
|
+
selected.push(variants[0]);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const preferred = options.prefer
|
|
66
|
+
? variants.filter((skill) => isPathInside(skill.path, options.prefer))
|
|
67
|
+
: [];
|
|
68
|
+
if (preferred.length && new Set(preferred.map((skill) => trees.get(skill.path))).size === 1) {
|
|
69
|
+
selected.push(preferred[0]);
|
|
70
|
+
}
|
|
71
|
+
else
|
|
72
|
+
unresolved.push(name);
|
|
73
|
+
}
|
|
74
|
+
// report 模式整个批次不写;skip 模式仅导入无冲突项。
|
|
75
|
+
if (unresolved.length && options.conflictMode === "report" && !options.dryRun) {
|
|
76
|
+
if (options.json)
|
|
77
|
+
printJson({
|
|
78
|
+
dryRun: false,
|
|
79
|
+
applied: false,
|
|
80
|
+
conflicts,
|
|
81
|
+
unresolved,
|
|
82
|
+
note: "未写入:请 --prefer 选择来源或 --conflict skip",
|
|
83
|
+
});
|
|
84
|
+
else
|
|
85
|
+
printLines([`导入已停止:${unresolved.length} 个未解决冲突`, ...unresolved]);
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
let materializeSummary = null;
|
|
89
|
+
let catalogSkills = selected;
|
|
90
|
+
let skippedUpsert = [];
|
|
91
|
+
let updated = [];
|
|
92
|
+
if (options.materialize) {
|
|
93
|
+
const mat = await materializeToCanonical(selected, canonicalDir, {
|
|
94
|
+
force: options.forceMaterialize === true,
|
|
95
|
+
dryRun: options.dryRun,
|
|
96
|
+
backupDir: options.backupDir,
|
|
97
|
+
});
|
|
98
|
+
materializeSummary = { copied: mat.copied, skipped: mat.skipped, failed: mat.failed };
|
|
99
|
+
catalogSkills = mat.skills;
|
|
100
|
+
const upserted = upsertSkills(catalog, catalogSkills, { skipOnHashConflict: false });
|
|
101
|
+
catalog = upserted.catalog;
|
|
102
|
+
updated = upserted.updated;
|
|
103
|
+
skippedUpsert = [
|
|
104
|
+
...unresolved,
|
|
105
|
+
...mat.skipped.filter((s) => s.reason.includes("冲突")).map((s) => s.name),
|
|
106
|
+
];
|
|
107
|
+
if (!options.dryRun && updated.length)
|
|
108
|
+
await writeCatalog(catalogPath, catalog);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
const upserted = upsertSkills(catalog, selected, {
|
|
112
|
+
skipOnHashConflict: !options.prefer || options.conflictMode === "skip",
|
|
113
|
+
});
|
|
114
|
+
catalog = upserted.catalog;
|
|
115
|
+
updated = upserted.updated;
|
|
116
|
+
skippedUpsert = [...unresolved, ...upserted.skipped];
|
|
117
|
+
if (!options.dryRun && updated.length)
|
|
118
|
+
await writeCatalog(catalogPath, catalog);
|
|
119
|
+
}
|
|
120
|
+
const preview = catalog;
|
|
121
|
+
if (!options.dryRun && updated.length > 0) {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
await appendHistoryBatch(layout.history, updated.flatMap((name) => {
|
|
124
|
+
const next = catalog.skills[name];
|
|
125
|
+
if (!next)
|
|
126
|
+
return [];
|
|
127
|
+
const previous = previousCatalog.skills[name];
|
|
128
|
+
const type = next.provenance?.overlayOf ? "overlay" : "ingest";
|
|
129
|
+
return [
|
|
130
|
+
{
|
|
131
|
+
at: now,
|
|
132
|
+
type,
|
|
133
|
+
name,
|
|
134
|
+
action: previous ? "update" : "create",
|
|
135
|
+
previousHash: previous?.hash,
|
|
136
|
+
hash: next.hash,
|
|
137
|
+
revision: next.provenance?.revision,
|
|
138
|
+
source: next.source ?? next.provenance?.sourceRef,
|
|
139
|
+
provenance: next.provenance,
|
|
140
|
+
path: next.path,
|
|
141
|
+
detail: { catalogPath, materialize: options.materialize },
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
const result = {
|
|
147
|
+
dryRun: options.dryRun,
|
|
148
|
+
materializeEnabled: options.materialize && !options.dryRun,
|
|
149
|
+
sources: resolvedSources,
|
|
150
|
+
prefer: options.prefer ? resolveAbsolutePath(options.prefer) : null,
|
|
151
|
+
scanned: scanned.length,
|
|
152
|
+
selected: selected.length,
|
|
153
|
+
updated: updated.length,
|
|
154
|
+
skipped: skippedUpsert,
|
|
155
|
+
conflicts,
|
|
156
|
+
materialize: materializeSummary,
|
|
157
|
+
catalogPath,
|
|
158
|
+
catalogSkillCount: Object.keys(preview.skills).length,
|
|
159
|
+
canonicalDir,
|
|
160
|
+
note: options.dryRun
|
|
161
|
+
? "dry-run:未写入 catalog / canonical"
|
|
162
|
+
: options.materialize
|
|
163
|
+
? "已物化到 canonical 并写入 catalog(未修改各 Agent skills 目录)"
|
|
164
|
+
: "已写入 catalog(未复制 skill 文件;路径仍指向源目录)",
|
|
165
|
+
};
|
|
166
|
+
if (options.json)
|
|
167
|
+
printJson(result);
|
|
168
|
+
else {
|
|
169
|
+
printLines([
|
|
170
|
+
`ingest ${options.dryRun ? "(dry-run)" : ""}`.trim(),
|
|
171
|
+
` scanned: ${result.scanned}`,
|
|
172
|
+
` selected: ${result.selected}`,
|
|
173
|
+
` updated: ${result.updated}`,
|
|
174
|
+
` skipped: ${result.skipped.length}`,
|
|
175
|
+
` conflicts: ${result.conflicts.length}`,
|
|
176
|
+
` catalog: ${catalogPath} (${result.catalogSkillCount} skills)`,
|
|
177
|
+
materializeSummary
|
|
178
|
+
? ` materialize: copied=${materializeSummary.copied.length} skipped=${materializeSummary.skipped.length} failed=${materializeSummary.failed.length}`
|
|
179
|
+
: ` materialize: ${options.dryRun ? "n/a (dry-run)" : options.materialize ? "requested" : "off"}`,
|
|
180
|
+
` note: ${result.note}`,
|
|
181
|
+
]);
|
|
182
|
+
if (result.conflicts.length > 0 && result.conflicts.length <= 30) {
|
|
183
|
+
printLines([
|
|
184
|
+
" conflict names:",
|
|
185
|
+
...result.conflicts.map((c) => ` - ${c.name} (${c.variants.length} variants)`),
|
|
186
|
+
]);
|
|
187
|
+
}
|
|
188
|
+
if (materializeSummary?.failed.length) {
|
|
189
|
+
printLines([
|
|
190
|
+
" materialize failed:",
|
|
191
|
+
...materializeSummary.failed.slice(0, 20).map((f) => ` - ${f.name}: ${f.error}`),
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (materializeSummary && materializeSummary.failed.length > 0)
|
|
196
|
+
return 3;
|
|
197
|
+
return skippedUpsert.length > 0 ? 2 : 0;
|
|
198
|
+
}
|