@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,127 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, rename, rm, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { skillList } from "#skill-hub/router";
5
+ import { SkillHubError } from "#skill-hub/shared";
6
+ import { printJson, printLines } from "../output.js";
7
+ const PAGE_SIZE = 100;
8
+ export async function runManifest(options) {
9
+ const apply = options.apply === true && options.dryRun !== true;
10
+ const outputPath = options.outputPath?.trim() ? path.resolve(options.outputPath) : undefined;
11
+ if (apply && !options.yes) {
12
+ throw new SkillHubError({
13
+ code: "E_CONFIG",
14
+ message: "写入 manifest 需要同时指定 --apply --yes",
15
+ });
16
+ }
17
+ if (apply && !outputPath) {
18
+ throw new SkillHubError({
19
+ code: "E_CONFIG",
20
+ message: "写入 manifest 需要 --out <path>",
21
+ });
22
+ }
23
+ const skills = [];
24
+ let offset = 0;
25
+ while (true) {
26
+ const page = await skillList({
27
+ offset,
28
+ limit: PAGE_SIZE,
29
+ catalogPath: options.catalogPath,
30
+ configPath: options.configPath,
31
+ });
32
+ skills.push(...page.skills);
33
+ if (page.nextOffset === null)
34
+ break;
35
+ offset = page.nextOffset;
36
+ }
37
+ const content = renderAvailableSkillsManifest(skills);
38
+ if (apply)
39
+ await writeManifestAtomically(outputPath, content);
40
+ if (options.json) {
41
+ printJson({
42
+ skillCount: skills.length,
43
+ outputPath: outputPath ?? null,
44
+ dryRun: !apply,
45
+ ...(outputPath ? {} : { content }),
46
+ });
47
+ }
48
+ else if (!outputPath) {
49
+ process.stdout.write(content);
50
+ }
51
+ else {
52
+ printLines([
53
+ `manifest skills=${skills.length} ${apply ? "written" : "dry-run"}`,
54
+ `path: ${outputPath}`,
55
+ ]);
56
+ }
57
+ return 0;
58
+ }
59
+ export function renderAvailableSkillsManifest(skills) {
60
+ const entries = skills
61
+ .slice()
62
+ .sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
63
+ .map((skill) => [
64
+ " <skill>",
65
+ ` <name>${escapeXml(skill.name)}</name>`,
66
+ ` <description>${escapeXml(skill.description)}</description>`,
67
+ " </skill>",
68
+ ].join("\n"));
69
+ return [
70
+ '<skills_system priority="1">',
71
+ "",
72
+ "## Available Skills",
73
+ "",
74
+ "<usage>",
75
+ "For an unknown task, use Skill Hub `skill_search`; for a known name, use `skill_read` directly.",
76
+ "Use `skill_files` and `skill_read` to load attachments. Read the selected skill before following it.",
77
+ "</usage>",
78
+ "",
79
+ "<available_skills>",
80
+ ...entries,
81
+ "</available_skills>",
82
+ "",
83
+ "</skills_system>",
84
+ "",
85
+ ].join("\n");
86
+ }
87
+ function escapeXml(value) {
88
+ const validXmlText = [...value]
89
+ .filter((character) => {
90
+ const codePoint = character.codePointAt(0) ?? 0;
91
+ return (codePoint === 0x09 ||
92
+ codePoint === 0x0a ||
93
+ codePoint === 0x0d ||
94
+ (codePoint >= 0x20 && codePoint <= 0xd7ff) ||
95
+ (codePoint >= 0xe000 && codePoint <= 0xfffd) ||
96
+ (codePoint >= 0x10000 && codePoint <= 0x10ffff));
97
+ })
98
+ .join("");
99
+ return validXmlText.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
100
+ }
101
+ async function writeManifestAtomically(filePath, content) {
102
+ try {
103
+ const target = await lstat(filePath);
104
+ if (!target.isFile()) {
105
+ throw new SkillHubError({
106
+ code: "E_PATH",
107
+ message: "manifest 输出目标必须是普通文件,不能是目录或符号链接",
108
+ details: { path: filePath },
109
+ });
110
+ }
111
+ }
112
+ catch (err) {
113
+ if (!err || typeof err !== "object" || !("code" in err) || err.code !== "ENOENT") {
114
+ throw err;
115
+ }
116
+ }
117
+ const directory = path.dirname(filePath);
118
+ await mkdir(directory, { recursive: true });
119
+ const temporary = path.join(directory, `.skill-hub-manifest-${randomUUID()}.tmp`);
120
+ try {
121
+ await writeFile(temporary, content, { encoding: "utf8", flag: "wx" });
122
+ await rename(temporary, filePath);
123
+ }
124
+ finally {
125
+ await rm(temporary, { force: true });
126
+ }
127
+ }
@@ -0,0 +1,37 @@
1
+ import { restoreFromBackup } from "#skill-hub/sync";
2
+ import { SkillHubError } from "#skill-hub/shared";
3
+ import { printJson, printLines } from "../output.js";
4
+ /**
5
+ * 恢复 backup。破坏性操作,要求 --yes。
6
+ * 默认 dry-run;真正写入还需 --apply(与 sync 对称,避免误触)。
7
+ */
8
+ export async function runRestore(options) {
9
+ if (!options.from) {
10
+ throw new SkillHubError({
11
+ code: "E_CONFIG",
12
+ message: "restore 需要 --from <backupDir|manifest.json>",
13
+ });
14
+ }
15
+ const wantWrite = options.apply && !options.dryRun;
16
+ if (wantWrite && !options.yes) {
17
+ throw new SkillHubError({
18
+ code: "E_BACKUP",
19
+ message: "restore 会覆盖 skills 目录,请同时传入 --yes --apply(建议先 --dry-run)",
20
+ });
21
+ }
22
+ const result = await restoreFromBackup(options.from, {
23
+ dryRun: !wantWrite,
24
+ agents: options.agents,
25
+ });
26
+ if (options.json)
27
+ printJson(result);
28
+ else {
29
+ printLines([
30
+ `restore ${result.dryRun ? "(dry-run)" : "APPLIED"}`,
31
+ ` from: ${result.manifest.backupDir}`,
32
+ ` restored: ${result.restored.join(", ") || "(none)"}`,
33
+ ...result.skipped.map((s) => ` skip ${s.id}: ${s.reason}`),
34
+ ]);
35
+ }
36
+ return 0;
37
+ }
@@ -0,0 +1,92 @@
1
+ import { routeWithProfile, skillSearch } from "#skill-hub/router";
2
+ import { SkillHubError } from "#skill-hub/shared";
3
+ import { printJson, printLines } from "../output.js";
4
+ export async function runRoute(options) {
5
+ if (!options.query.trim()) {
6
+ throw new SkillHubError({
7
+ code: "E_CONFIG",
8
+ message: "route 需要查询字符串",
9
+ });
10
+ }
11
+ const result = await skillSearch({
12
+ query: options.query,
13
+ topK: options.topK,
14
+ profile: options.profile,
15
+ engine: options.engine,
16
+ catalogPath: options.catalogPath,
17
+ configPath: options.configPath,
18
+ indexDir: options.indexDir,
19
+ profilesDir: options.profilesDir,
20
+ scope: options.scopes,
21
+ });
22
+ const payload = {
23
+ query: result.query,
24
+ top_k: result.top_k,
25
+ engine: result.engineId,
26
+ degraded: result.degraded,
27
+ degrade_reason: result.degradeReason ?? null,
28
+ profile: result.profile,
29
+ profile_matched: result.profileMatched,
30
+ profile_fallback: result.profileFallback,
31
+ scope: result.scope,
32
+ scope_matched: result.scopeMatched,
33
+ candidate_count: result.candidateCount,
34
+ index_fresh: result.indexFresh,
35
+ index_rebuild_recommended: result.indexRebuildRecommended,
36
+ index_source: result.indexSource,
37
+ index_reason: result.indexReason,
38
+ decision: result.decision
39
+ ? {
40
+ kind: result.decision.kind,
41
+ model: result.decision.model,
42
+ selected: result.decision.selected,
43
+ confidence: Number(result.decision.confidence.toFixed(4)),
44
+ selected_probability: Number(result.decision.selectedProbability.toFixed(4)),
45
+ none_probability: Number(result.decision.noneProbability.toFixed(4)),
46
+ threshold: Number(result.decision.threshold.toFixed(4)),
47
+ escalation_recommended: result.decision.escalationRecommended,
48
+ probabilities: Object.fromEntries(Object.entries(result.decision.probabilities).map(([name, value]) => [
49
+ name,
50
+ Number(value.toFixed(4)),
51
+ ])),
52
+ }
53
+ : null,
54
+ results: result.results.map((r) => ({
55
+ name: r.name,
56
+ score: Number(r.score.toFixed(4)),
57
+ description: r.description,
58
+ path: r.path,
59
+ provenance: r.provenance ?? null,
60
+ reasons: r.reasons,
61
+ })),
62
+ };
63
+ if (options.json)
64
+ printJson(payload);
65
+ else {
66
+ const head = [
67
+ `route top-${options.topK} engine=${payload.engine}${payload.degraded ? " (degraded)" : ""} q="${options.query}"`,
68
+ ` index=${payload.index_source} fresh=${payload.index_fresh === null ? "unknown" : payload.index_fresh}${payload.index_rebuild_recommended ? " → 请运行 index --rebuild" : ""}`,
69
+ ];
70
+ if (payload.profile) {
71
+ head.push(` profile=${payload.profile} matched=${payload.profile_matched ?? 0} fallback=${payload.profile_fallback} candidates=${payload.candidate_count}`);
72
+ }
73
+ if (payload.scope?.length) {
74
+ head.push(` scope=${payload.scope.join(",")} matched=${payload.scope_matched ?? 0} candidates=${payload.candidate_count}`);
75
+ }
76
+ if (payload.decision) {
77
+ head.push(` decision selected=${payload.decision.selected ?? "none"} confidence=${payload.decision.confidence.toFixed(3)}${payload.decision.escalation_recommended ? " → 建议升级判断" : ""}`);
78
+ }
79
+ if (payload.results.length === 0) {
80
+ printLines([...head, " (无结果)"]);
81
+ }
82
+ else {
83
+ printLines([
84
+ ...head,
85
+ ...payload.results.map((r, i) => ` ${String(i + 1).padStart(2)}. ${r.name.padEnd(28)} ${r.score.toFixed(3)} ${r.description.slice(0, 56)}`),
86
+ ]);
87
+ }
88
+ }
89
+ void options.includeBody;
90
+ void routeWithProfile;
91
+ return 0;
92
+ }
@@ -0,0 +1,89 @@
1
+ import { skillFiles, skillList, skillRead } from "#skill-hub/router";
2
+ import { SkillHubError } from "#skill-hub/shared";
3
+ import { printJson, printLines } from "../output.js";
4
+ export async function runBrowse(options) {
5
+ const result = await skillList({
6
+ query: options.query,
7
+ offset: options.offset,
8
+ limit: options.limit,
9
+ catalogPath: options.catalogPath,
10
+ configPath: options.configPath,
11
+ });
12
+ if (options.json) {
13
+ printJson(result);
14
+ return 0;
15
+ }
16
+ printLines([
17
+ `browse total=${result.total} offset=${result.offset}`,
18
+ ...(result.skills.length
19
+ ? result.skills.map((skill) => ` ${skill.name} ${skill.description}`)
20
+ : [" (无结果)"]),
21
+ ]);
22
+ printNextOffset("browse", options.query, result.nextOffset);
23
+ return 0;
24
+ }
25
+ export async function runFiles(options) {
26
+ requireName(options.name, "files");
27
+ const result = await skillFiles({
28
+ name: options.name,
29
+ path: options.path,
30
+ offset: options.offset,
31
+ limit: options.limit,
32
+ catalogPath: options.catalogPath,
33
+ configPath: options.configPath,
34
+ });
35
+ if (options.json) {
36
+ printJson(result);
37
+ return 0;
38
+ }
39
+ printLines([
40
+ `files ${result.name} path=${result.path || "."} total=${result.total} offset=${result.offset}`,
41
+ ...(result.files.length
42
+ ? result.files.map((file) => {
43
+ const size = file.size === undefined ? "" : ` size=${file.size}`;
44
+ const reason = file.reason ? ` ${file.reason}` : "";
45
+ return ` ${file.type.padEnd(9)} ${file.path}${size}${reason}`;
46
+ })
47
+ : [" (空目录)"]),
48
+ ]);
49
+ printNextOffset("files", options.name, result.nextOffset, options.path);
50
+ return 0;
51
+ }
52
+ export async function runRead(options) {
53
+ requireName(options.name, "read");
54
+ const result = await skillRead({
55
+ name: options.name,
56
+ file: options.file,
57
+ offset: options.offset,
58
+ maxChars: options.maxChars,
59
+ catalogPath: options.catalogPath,
60
+ configPath: options.configPath,
61
+ });
62
+ if (options.json) {
63
+ printJson(result);
64
+ return 0;
65
+ }
66
+ // 正文可直接管道给 Agent;分页说明始终写 stderr,避免混入正文。
67
+ process.stdout.write(result.body);
68
+ if (result.nextOffset !== null) {
69
+ const file = result.file === "SKILL.md" ? "" : ` --file ${quote(result.file)}`;
70
+ console.error(`\n续读: skill-hub read ${quote(result.name)}${file} --offset ${result.nextOffset}`);
71
+ }
72
+ return 0;
73
+ }
74
+ function requireName(name, command) {
75
+ if (!name.trim()) {
76
+ throw new SkillHubError({ code: "E_CONFIG", message: `${command} 需要 skill name` });
77
+ }
78
+ }
79
+ function printNextOffset(command, nameOrQuery, nextOffset, path) {
80
+ if (nextOffset === null)
81
+ return;
82
+ const target = nameOrQuery?.trim() ? ` ${quote(nameOrQuery)}` : "";
83
+ const pathFlag = path?.trim() ? ` --path ${quote(path)}` : "";
84
+ console.error(`续读: skill-hub ${command}${target}${pathFlag} --offset ${nextOffset}`);
85
+ }
86
+ function quote(value) {
87
+ // POSIX 单引号可防止 $(), 反引号和空白在复制续读命令时被 shell 解释。
88
+ return `'${value.replaceAll("'", "'\\''")}'`;
89
+ }
@@ -0,0 +1,195 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { appendHistoryBatch, catalogToSkillMetas, loadHubConfig, readCatalog, resolveHubConfigPaths, scanSkills, } from "#skill-hub/core";
3
+ import { applySync, assertBackupReady, filterGlobalSyncSkills, planSync } from "#skill-hub/sync";
4
+ import { SkillHubError, getDefaultHubHome, hubLayout, resolveAbsolutePath, } from "#skill-hub/shared";
5
+ import path from "node:path";
6
+ import { printJson, printLines } from "../output.js";
7
+ export async function runSync(options) {
8
+ const wantApply = options.apply && !options.dryRun;
9
+ if (options.createOnly && options.replaceReal) {
10
+ throw new SkillHubError({
11
+ code: "E_SYNC",
12
+ message: "--create-only 与 --replace-real 互斥,请只选其一。",
13
+ });
14
+ }
15
+ // 生产写入双重闸门:--apply --yes --allow-write
16
+ if (wantApply) {
17
+ if (!options.yes || !options.allowWrite) {
18
+ throw new SkillHubError({
19
+ code: "E_SYNC",
20
+ message: "sync --apply 需要同时提供 --yes --allow-write(将修改各 Agent skills 目录的 symlink)。可先 --dry-run。",
21
+ });
22
+ }
23
+ }
24
+ const dryRun = !wantApply;
25
+ const config = resolveHubConfigPaths(options.configPath ? await loadHubConfig(options.configPath) : await loadHubConfig());
26
+ const requireBackup = options.requireBackup ||
27
+ (wantApply && config.sync.require_backup) ||
28
+ (wantApply && options.replaceReal);
29
+ if (wantApply && requireBackup) {
30
+ const backupDir = options.backupDir;
31
+ if (!backupDir) {
32
+ throw new SkillHubError({
33
+ code: "E_BACKUP",
34
+ message: "sync --apply 要求备份:请先 skill-hub backup,并用 --backup-dir <path> 指向含 manifest.json 的备份目录(replace-real 强制需要)",
35
+ });
36
+ }
37
+ await assertBackupReady(backupDir, {
38
+ ...config,
39
+ agents: config.agents.filter((a) => !options.agents?.length || options.agents.includes(a.id)),
40
+ });
41
+ }
42
+ const layout = hubLayout(getDefaultHubHome());
43
+ const catalogPath = options.catalogPath
44
+ ? resolveAbsolutePath(options.catalogPath)
45
+ : layout.catalog;
46
+ let skills = [];
47
+ try {
48
+ skills = catalogToSkillMetas(await readCatalog(catalogPath));
49
+ }
50
+ catch (err) {
51
+ const exists = await lstat(catalogPath).catch((e) => {
52
+ if (e.code === "ENOENT")
53
+ return null;
54
+ throw e;
55
+ });
56
+ if (exists)
57
+ throw err;
58
+ const issues = [];
59
+ skills = await scanSkills(config.canonical_dir, { onIssue: (issue) => issues.push(issue) });
60
+ if (issues.length)
61
+ throw new SkillHubError({
62
+ code: "E_SYNC",
63
+ message: `canonical 扫描不完整: ${JSON.stringify(issues)}`,
64
+ });
65
+ }
66
+ // project/workspace source 是盘点/项目路由输入,不默认投影到全局 Agent。
67
+ // 旧 catalog 无 provenance 时按 user 兼容处理。
68
+ const filteredByScope = filterGlobalSyncSkills(skills);
69
+ const excludedByScope = filteredByScope.excluded;
70
+ skills = filteredByScope.skills;
71
+ const plan = await planSync(config, skills, {
72
+ dryRun,
73
+ agents: options.agents,
74
+ createOnly: options.createOnly,
75
+ replaceReal: options.replaceReal,
76
+ });
77
+ const modeNote = [
78
+ options.agents?.length ? `agents=${options.agents.join(",")}` : "agents=all-enabled",
79
+ options.createOnly ? "create-only" : options.replaceReal ? "replace-real" : "full-actions",
80
+ ].join(" ");
81
+ const replaceStashDir = options.backupDir
82
+ ? path.join(resolveAbsolutePath(options.backupDir), "replaced-real")
83
+ : undefined;
84
+ if (!wantApply) {
85
+ const createItems = plan.items.filter((i) => i.action === "create_symlink" || i.action === "create_copy");
86
+ const replaceItems = plan.items.filter((i) => i.action === "replace_real" || i.action === "replace_real_copy");
87
+ const updateItems = plan.items.filter((i) => i.action === "update_symlink" || i.action === "update_copy");
88
+ const skippedItems = plan.items.filter((i) => i.action === "skipped");
89
+ const result = {
90
+ dryRun: true,
91
+ require_backup: requireBackup,
92
+ createOnly: options.createOnly,
93
+ replaceReal: options.replaceReal,
94
+ agents: options.agents ?? null,
95
+ replace_stash_dir: replaceStashDir ?? null,
96
+ summary: plan.summary,
97
+ mode: plan.mode,
98
+ itemCount: plan.items.length,
99
+ createCount: createItems.length,
100
+ replaceCount: replaceItems.length,
101
+ updateCount: updateItems.length,
102
+ skippedCount: skippedItems.length,
103
+ excludedByScope,
104
+ items: plan.items,
105
+ note: `dry-run (${modeNote}):未修改任何路径。apply 需 --apply --yes --allow-write --backup-dir${options.replaceReal ? " --replace-real" : ""}${options.createOnly ? " --create-only" : ""}`,
106
+ };
107
+ if (options.json)
108
+ printJson(result);
109
+ else {
110
+ printLines([
111
+ `sync plan (dry-run=true mode=${plan.mode} ${modeNote})`,
112
+ ` create=${plan.summary.create} update=${plan.summary.update} replace=${plan.summary.replace} conflict=${plan.summary.conflict} noop=${plan.summary.noop} skipped=${skippedItems.length}`,
113
+ ...replaceItems
114
+ .slice(0, 30)
115
+ .map((i) => ` [replace] ${i.agentId}:${i.skillName} → stash then → ${i.source}`),
116
+ replaceItems.length > 30 ? ` ... +${replaceItems.length - 30} more replaces` : "",
117
+ ...createItems
118
+ .slice(0, 20)
119
+ .map((i) => ` [create] ${i.agentId}:${i.skillName} → ${i.target}`),
120
+ createItems.length > 20 ? ` ... +${createItems.length - 20} more creates` : "",
121
+ options.replaceReal
122
+ ? ` note: replace-real 将把真实目录/文件迁入 ${replaceStashDir ?? "<backup-dir>/replaced-real"}`
123
+ : "",
124
+ options.createOnly ? ` note: create-only 已将非 create 项标为 skipped` : "",
125
+ excludedByScope.length > 0
126
+ ? ` note: 已排除 ${excludedByScope.length} 个 project/workspace Skill(默认不投影到全局 Agent)`
127
+ : "",
128
+ result.note,
129
+ ].filter(Boolean));
130
+ }
131
+ if (options.createOnly)
132
+ return 0;
133
+ return plan.summary.conflict > 0 ? 2 : 0;
134
+ }
135
+ const applied = await applySync(plan, {
136
+ dryRun: false,
137
+ createOnly: options.createOnly,
138
+ replaceStashDir: options.replaceReal ? replaceStashDir : undefined,
139
+ });
140
+ const result = {
141
+ dryRun: false,
142
+ require_backup: requireBackup,
143
+ backup_dir: options.backupDir ?? null,
144
+ replace_stash_dir: options.replaceReal ? (replaceStashDir ?? null) : null,
145
+ createOnly: options.createOnly,
146
+ replaceReal: options.replaceReal,
147
+ mode: plan.mode,
148
+ agents: options.agents ?? null,
149
+ plan_summary: plan.summary,
150
+ excludedByScope,
151
+ apply: {
152
+ applied: applied.applied,
153
+ skipped: applied.skipped,
154
+ failed: applied.failed,
155
+ },
156
+ items: applied.items,
157
+ };
158
+ if (applied.applied > 0) {
159
+ await appendHistoryBatch(layout.history, applied.items
160
+ .filter((item) => item.result === "applied")
161
+ .map((item) => ({
162
+ at: new Date().toISOString(),
163
+ type: "sync",
164
+ name: item.skillName,
165
+ action: item.action,
166
+ agentId: item.agentId,
167
+ target: item.target,
168
+ path: item.source,
169
+ detail: { mode: plan.mode },
170
+ })));
171
+ }
172
+ if (options.json)
173
+ printJson(result);
174
+ else {
175
+ printLines([
176
+ `sync APPLY (mode=${plan.mode} ${modeNote})`,
177
+ ` plan: create=${plan.summary.create} update=${plan.summary.update} replace=${plan.summary.replace} conflict=${plan.summary.conflict} noop=${plan.summary.noop}`,
178
+ ` result: applied=${applied.applied} skipped=${applied.skipped} failed=${applied.failed}`,
179
+ options.replaceReal && replaceStashDir ? ` stash: ${replaceStashDir}` : "",
180
+ ...applied.items
181
+ .filter((i) => i.result === "applied")
182
+ .slice(0, 40)
183
+ .map((i) => ` + ${i.action} ${i.agentId}:${i.skillName}`),
184
+ applied.applied > 40 ? ` ... +${applied.applied - 40} more` : "",
185
+ ...applied.items
186
+ .filter((i) => i.result === "failed")
187
+ .map((i) => ` FAIL ${i.agentId}:${i.skillName} ${i.error ?? ""}`),
188
+ ].filter(Boolean));
189
+ }
190
+ if (applied.failed > 0)
191
+ return 1;
192
+ if (!options.createOnly && plan.summary.conflict > 0)
193
+ return 2;
194
+ return 0;
195
+ }
@@ -0,0 +1,45 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { inspectTakeover, loadHubConfig, resolveHubConfigPaths, } from "#skill-hub/core";
5
+ import { printJson, printLines } from "../output.js";
6
+ export async function runTakeover(options) {
7
+ const config = resolveHubConfigPaths(await loadHubConfig(options.configPath));
8
+ const roots = [
9
+ { id: "hub", path: config.canonical_dir, ownership: "canonical" },
10
+ ...config.agents
11
+ .filter((a) => a.enabled)
12
+ .map((a) => ({ id: a.id, path: a.skills_dir, ownership: "agent" })),
13
+ ...(config.sources ?? [])
14
+ .filter((s) => s.enabled)
15
+ .map((s) => ({ id: s.id, path: s.path, ownership: "source" })),
16
+ ];
17
+ if (options.includePlugins) {
18
+ for (const relative of [
19
+ ".codex/plugins/cache",
20
+ ".claude/plugins/cache",
21
+ ".codex/skills/.system",
22
+ ]) {
23
+ roots.push({ id: relative, path: path.join(homedir(), relative), ownership: "external" });
24
+ }
25
+ }
26
+ const report = await inspectTakeover(roots, config.canonical_dir);
27
+ if (options.out) {
28
+ const output = path.resolve(options.out);
29
+ await mkdir(path.dirname(output), { recursive: true });
30
+ await writeFile(output, `${JSON.stringify(report, null, 2)}\n`);
31
+ }
32
+ if (options.json)
33
+ printJson(report);
34
+ else
35
+ printLines([
36
+ "takeover(只读接管预览)",
37
+ JSON.stringify(report.summary, null, 2),
38
+ report.note,
39
+ ...report.skills
40
+ .filter((s) => s.action === "conflict" || s.action === "blocked")
41
+ .map((s) => ` ${s.action}: ${s.name}`),
42
+ ...(options.out ? [`报告: ${path.resolve(options.out)}`] : []),
43
+ ]);
44
+ return report.summary.conflicts || report.summary.issues ? 2 : 0;
45
+ }
@@ -0,0 +1,55 @@
1
+ import path from "node:path";
2
+ import { readUsageReport } from "#skill-hub/core";
3
+ import { skillContextCost } from "#skill-hub/router";
4
+ import { SkillHubError, getDefaultHubHome } from "#skill-hub/shared";
5
+ import { printJson, printLines } from "../output.js";
6
+ function integer(value) {
7
+ if (value === undefined)
8
+ return undefined;
9
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)))
10
+ throw new SkillHubError({ code: "E_CONFIG", message: "数值参数须为非负安全整数" });
11
+ return Number(value);
12
+ }
13
+ export async function runUsage(options) {
14
+ const file = options.file ??
15
+ process.env.SKILL_HUB_USAGE_LOG ??
16
+ path.join(getDefaultHubHome(), "usage.jsonl");
17
+ const report = {
18
+ ...(await readUsageReport({
19
+ file,
20
+ days: integer(options.days),
21
+ name: options.name,
22
+ origin: options.origin,
23
+ })),
24
+ recordingEnabled: Boolean(process.env.SKILL_HUB_USAGE_LOG),
25
+ };
26
+ if (options.json)
27
+ printJson(report);
28
+ else
29
+ printLines([
30
+ `Hub使用观测 complete=${report.complete} exists=${report.exists} recordingEnabled=${report.recordingEnabled}`,
31
+ `日志: ${report.file};最早可见记录: ${report.recordedSince ?? "未知"}`,
32
+ `search=${report.searchCalls} read=${report.readCalls} errors=${report.errors}`,
33
+ ...report.skills.map((s) => `${s.name} [${s.origin}] 候选出现=${s.appearances} 正文首段=${s.initialBodyReads} 续读=${s.continuationBodyReads} 附件=${s.attachmentReads} 错误=${s.errors} 返回文本粗估tokens=${s.estimatedReturnedTokens} 最近读取=${s.lastReadAt ?? "未知"}`),
34
+ ...report.issues.map((i) => `日志第${i.line}行 ${i.code}`),
35
+ "仅代表Hub服务调用;未记录历史未知,首段次数不等于唯一任务数。Token为启发式估算,模型真实Token和费用未知。",
36
+ ]);
37
+ return report.complete ? 0 : 3;
38
+ }
39
+ export async function runContextCost(options) {
40
+ const report = await skillContextCost({
41
+ ...options,
42
+ maxChars: integer(options.maxChars),
43
+ contextWindow: integer(options.contextWindow),
44
+ });
45
+ if (options.json)
46
+ printJson(report);
47
+ else
48
+ printLines([
49
+ `静态上下文粗估 (${report.method}) complete=${report.complete}`,
50
+ `元数据合计≈${report.metadataTotalEstimatedTokens} tokens;首段maxChars=${report.maxChars};contextWindow=${report.contextWindow ?? "未指定"}`,
51
+ ...report.items.map((s) => `${s.name} 元数据≈${s.metadata.estimatedTokens} 正文全文≈${s.fullBody?.estimatedTokens ?? "未知"} 首段≈${s.initialPage?.estimatedTokens ?? "未知"} tokens${s.initialPageContextPercent === null ? "" : ` 首段占窗口≈${s.initialPageContextPercent}%`}${s.errorCode ? ` ${s.errorCode}` : ""}`),
52
+ "粗略比较值,不是tokenizer/模型账单。附件、工具定义、封装、历史与缓存不计;不代表全库会被注入。",
53
+ ]);
54
+ return report.complete ? 0 : 3;
55
+ }