@yottameta/yotta-skills-plugin 0.28.1 → 0.28.3

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.
@@ -1017,6 +1017,277 @@ function unlinkSkills(options) {
1017
1017
  return { hubDir, targetDir, dryRun, results };
1018
1018
  }
1019
1019
 
1020
+ /**
1021
+ * 删除 Hub 技能:全宿主清理指向 Hub 的链接(含死链)→ Hub 目录移入回收站 →
1022
+ * 定向清 Hub 台账与链接台账 → 写审计。
1023
+ *
1024
+ * 五阶段 fail-closed:
1025
+ * 1. preflight(只读分类,dry-run 到此为止)
1026
+ * 2. unlink(只删指向 Hub 的链接;报错即中止,不进入 3)
1027
+ * 3. trash(目录移入回收站保留 7 天;跨卷走复制 + 校验 + 删源)
1028
+ * 4. ledger(定向删 Hub 台账条目 + 过滤链接台账;不调 syncHubState,防记回 missing)
1029
+ * 5. 汇总(含版本闸门报告)
1030
+ *
1031
+ * options:
1032
+ * - hubDir / slug(必填)
1033
+ * - targets: [{ dir, label, agentId }](调用方解析范围;核心不做范围判断)
1034
+ * - dryRun / trashRoot / trashRetentionDays / now
1035
+ * - renameEntry / unlink(测试注入点)
1036
+ */
1037
+ function removeSkills(options) {
1038
+ const opts = options || {};
1039
+ const hubDir = path.resolve(opts.hubDir);
1040
+ const slug = String(opts.slug || '').trim().toLowerCase();
1041
+ const dryRun = Boolean(opts.dryRun);
1042
+ const unlinkFn = typeof opts.unlink === 'function' ? opts.unlink : unlinkSkills;
1043
+ const targets = (Array.isArray(opts.targets) ? opts.targets : [])
1044
+ .filter((item) => item && item.dir)
1045
+ .map((item) => ({
1046
+ dir: path.resolve(item.dir),
1047
+ label: item.label || '指定目录',
1048
+ agentId: item.agentId || null,
1049
+ }));
1050
+ const trashRoot = resolveTrashRoot(hubDir, opts);
1051
+ const skillDir = path.join(hubDir, slug);
1052
+ const compact = (plan) => ({
1053
+ dir: plan.dir,
1054
+ label: plan.label,
1055
+ status: plan.status,
1056
+ action: plan.action,
1057
+ note: plan.note,
1058
+ ...(plan.version ? { version: plan.version } : {}),
1059
+ ...(plan.versionGate ? { versionGate: true } : {}),
1060
+ ...(plan.drift ? { drift: true } : {}),
1061
+ ...(plan.broken ? { broken: true } : {}),
1062
+ });
1063
+
1064
+ let hubPathKind = 'missing';
1065
+ let hubEntry = null;
1066
+ try {
1067
+ const stat = fs.lstatSync(skillDir);
1068
+ hubPathKind = stat.isSymbolicLink() ? 'link' : stat.isDirectory() ? 'directory' : 'file';
1069
+ } catch (_) {
1070
+ hubPathKind = 'missing';
1071
+ }
1072
+ if (hubPathKind === 'file') {
1073
+ return {
1074
+ hubDir,
1075
+ slug,
1076
+ dryRun,
1077
+ verdict: 'failed',
1078
+ ok: false,
1079
+ exitCode: 1,
1080
+ hubPresent: false,
1081
+ hubPathKind,
1082
+ hubVersion: null,
1083
+ targets: [],
1084
+ unlinkable: 0,
1085
+ kept: [],
1086
+ failed: [{ dir: skillDir, note: 'Hub 内同名路径是普通文件;fail-closed,拒绝删除' }],
1087
+ trashedTo: null,
1088
+ };
1089
+ }
1090
+ if (hubPathKind !== 'missing') {
1091
+ hubEntry = scanHubSkills(hubDir).find((item) => samePath(item.dir, skillDir)) || null;
1092
+ }
1093
+ const hubVersion = hubEntry && hubEntry.version ? hubEntry.version : '';
1094
+
1095
+ // ── 1. preflight(只读) ────────────────────────────────────────────────
1096
+ const plans = targets.map((target) => {
1097
+ const targetPath = path.join(target.dir, slug);
1098
+ const current = classifyTarget(targetPath, hubDir);
1099
+ if (current.kind === 'missing') {
1100
+ return {
1101
+ dir: target.dir,
1102
+ label: target.label,
1103
+ target: targetPath,
1104
+ action: 'none',
1105
+ status: 'missing',
1106
+ note: '目标不存在',
1107
+ };
1108
+ }
1109
+ if (current.kind === 'link') {
1110
+ if (!current.inHub) {
1111
+ return {
1112
+ dir: target.dir,
1113
+ label: target.label,
1114
+ target: targetPath,
1115
+ action: 'keep',
1116
+ status: 'refused',
1117
+ note: '链接指向 Hub 之外;fail-closed 保留',
1118
+ };
1119
+ }
1120
+ const drift = !samePath(current.target, skillDir);
1121
+ const broken = current.targetExists === false;
1122
+ return {
1123
+ dir: target.dir,
1124
+ label: target.label,
1125
+ target: targetPath,
1126
+ action: 'unlink',
1127
+ status: 'link',
1128
+ drift,
1129
+ broken,
1130
+ note: broken
1131
+ ? '死链(Hub 目标缺失),将清理'
1132
+ : drift
1133
+ ? '链接指向 Hub 内其它技能,将一并清理'
1134
+ : '指向本技能的链接,将清理',
1135
+ };
1136
+ }
1137
+ if (current.kind === 'directory') {
1138
+ const meta = readSkillMeta(targetPath);
1139
+ const version = meta && meta.version ? meta.version : '';
1140
+ const higher = Boolean(version && hubVersion && compareVersions(version, hubVersion) > 0);
1141
+ return {
1142
+ dir: target.dir,
1143
+ label: target.label,
1144
+ target: targetPath,
1145
+ action: 'keep',
1146
+ status: 'kept-directory',
1147
+ version: version || null,
1148
+ versionGate: higher,
1149
+ note: higher
1150
+ ? '宿主真副本 v' + version + ' 高于 Hub v' + hubVersion + ';保留(先确认该副本)'
1151
+ : '宿主真副本,保留(只删链接)',
1152
+ };
1153
+ }
1154
+ return {
1155
+ dir: target.dir,
1156
+ label: target.label,
1157
+ target: targetPath,
1158
+ action: 'keep',
1159
+ status: 'kept-file',
1160
+ note: '普通文件,保留',
1161
+ };
1162
+ });
1163
+
1164
+ const stateSnapshot = readHubState(hubDir);
1165
+ const hasStateEntry = Boolean(stateSnapshot.skills && stateSnapshot.skills[slug]);
1166
+ const hubPresent = hubPathKind === 'directory' || hubPathKind === 'link';
1167
+ const unlinkable = plans.filter((plan) => plan.action === 'unlink').length;
1168
+ const kept = plans.filter((plan) => plan.action === 'keep');
1169
+ const base = {
1170
+ hubDir,
1171
+ slug,
1172
+ dryRun,
1173
+ hubPresent,
1174
+ hubPathKind,
1175
+ hubVersion: hubVersion || null,
1176
+ targets: plans.map(compact),
1177
+ unlinkable,
1178
+ kept: kept.map(compact),
1179
+ };
1180
+ if (dryRun) {
1181
+ return { ...base, verdict: 'dry-run', ok: true, exitCode: 0, trashedTo: null };
1182
+ }
1183
+
1184
+ // ── 2. unlink(含死链;报错即中止,不删 Hub) ───────────────────────────
1185
+ const unlinked = [];
1186
+ const failed = [];
1187
+ for (const plan of plans) {
1188
+ if (plan.action !== 'unlink') continue;
1189
+ const result = unlinkFn({ hubDir, targetDir: plan.dir, slugs: [slug], dryRun: false });
1190
+ const item = (result.results || [])[0] || { status: 'error', note: '未知结果' };
1191
+ plan.status = item.status;
1192
+ plan.note = item.note || plan.note;
1193
+ if (item.status === 'error') failed.push({ dir: plan.dir, note: item.note });
1194
+ else unlinked.push({ dir: plan.dir, status: item.status, note: item.note });
1195
+ }
1196
+ if (failed.length > 0) {
1197
+ if (fs.existsSync(hubDir)) {
1198
+ try {
1199
+ appendAudit(hubDir, {
1200
+ event: 'remove',
1201
+ slug,
1202
+ verdict: 'failed',
1203
+ reason: 'unlink-error',
1204
+ targets: plans.map(compact),
1205
+ removedLinks: unlinked.length,
1206
+ });
1207
+ } catch (_) { /* audit best-effort */ }
1208
+ }
1209
+ return { ...base, verdict: 'failed', ok: false, exitCode: 1, unlinked, failed, trashedTo: null };
1210
+ }
1211
+
1212
+ // ── 3. trash(Hub 目录入回收站;失败即中止) ────────────────────────────
1213
+ let trashedTo = null;
1214
+ let method = null;
1215
+ if (hubPresent) {
1216
+ trashedTo = path.join(trashRoot, trashRunStamp(opts.now), 'hub-remove', slug);
1217
+ try {
1218
+ fs.mkdirSync(path.dirname(trashedTo), { recursive: true });
1219
+ const transfer = moveEntryAcrossDevices(skillDir, trashedTo, { renameEntry: opts.renameEntry });
1220
+ method = transfer.method;
1221
+ } catch (error) {
1222
+ if (fs.existsSync(hubDir)) {
1223
+ try {
1224
+ appendAudit(hubDir, {
1225
+ event: 'remove',
1226
+ slug,
1227
+ verdict: 'failed',
1228
+ reason: 'trash-error',
1229
+ targets: plans.map(compact),
1230
+ removedLinks: unlinked.length,
1231
+ trashedTo,
1232
+ });
1233
+ } catch (_) { /* audit best-effort */ }
1234
+ }
1235
+ return {
1236
+ ...base,
1237
+ verdict: 'failed',
1238
+ ok: false,
1239
+ exitCode: 1,
1240
+ unlinked,
1241
+ failed: [{ dir: skillDir, note: error.message }],
1242
+ trashedTo,
1243
+ };
1244
+ }
1245
+ }
1246
+
1247
+ // ── 4. ledger(定向清台账;不调 syncHubState) ──────────────────────────
1248
+ const state = readHubState(hubDir);
1249
+ if (state.skills && state.skills[slug]) {
1250
+ delete state.skills[slug];
1251
+ writeHubState(hubDir, state);
1252
+ }
1253
+ const linkState = readLinkState(hubDir);
1254
+ const remainingLinks = linkState.links.filter((item) => item.slug !== slug);
1255
+ if (remainingLinks.length !== linkState.links.length) {
1256
+ writeLinkState(hubDir, { links: remainingLinks });
1257
+ }
1258
+ let prunedTrash = 0;
1259
+ try { prunedTrash = pruneTrash(trashRoot, { days: opts.trashRetentionDays }); } catch (_) { prunedTrash = 0; }
1260
+
1261
+ // ── 5. 汇总 + 审计 ─────────────────────────────────────────────────────
1262
+ const verdict = hubPresent ? 'removed' : (unlinked.length > 0 || hasStateEntry ? 'cleanup-only' : 'not-found');
1263
+ const exitCode = verdict === 'not-found' ? 4 : 0;
1264
+ if (fs.existsSync(hubDir)) {
1265
+ try {
1266
+ appendAudit(hubDir, {
1267
+ event: 'remove',
1268
+ slug,
1269
+ verdict,
1270
+ targets: plans.map(compact),
1271
+ removedLinks: unlinked.length,
1272
+ kept: kept.length,
1273
+ trashedTo,
1274
+ method,
1275
+ });
1276
+ } catch (_) { /* audit best-effort */ }
1277
+ }
1278
+ return {
1279
+ ...base,
1280
+ verdict,
1281
+ ok: exitCode === 0,
1282
+ exitCode,
1283
+ unlinked,
1284
+ kept: kept.map(compact),
1285
+ trashedTo,
1286
+ method,
1287
+ prunedTrash,
1288
+ };
1289
+ }
1290
+
1020
1291
  function linkStatus(hubDir) {
1021
1292
  const state = readLinkState(hubDir);
1022
1293
  return state.links.map((item) => {
@@ -1233,6 +1504,7 @@ module.exports = {
1233
1504
  classifyTarget,
1234
1505
  linkSkills,
1235
1506
  unlinkSkills,
1507
+ removeSkills,
1236
1508
  linkStatus,
1237
1509
  status,
1238
1510
  doctor,
@@ -86,11 +86,35 @@ npx -y @yottameta/yotta-skills hub link --all --include-discovered
86
86
  # 只删除链接,不动 Hub 真源;清理范围外链接用 --dir <目录> 精确指定
87
87
  npx -y @yottameta/yotta-skills hub unlink --all
88
88
 
89
+ # 指定智能体分发 / 解除(只影响该宿主;Hub 真源保留)
90
+ npx -y @yottameta/yotta-skills hub link yotta-memory --agent codex
91
+ npx -y @yottameta/yotta-skills hub unlink yotta-memory --agent codex
92
+
93
+ # 彻底删除 Hub 技能:全宿主清链接(含死链)+ 目录入回收站 + 清台账 + 审计
94
+ npx -y @yottameta/yotta-skills hub remove my-skill --dry-run
95
+
89
96
  # 查看来源、版本、链接宿主与异常
90
97
  npx -y @yottameta/yotta-skills hub status
91
98
  npx -y @yottameta/yotta-skills hub doctor
92
99
  ```
93
100
 
101
+ ## 删除技能(hub remove,0.28.3 起)
102
+
103
+ `hub remove <slug>` 五阶段 fail-closed:
104
+
105
+ 1. **preflight(只读)**:逐目录分类健康链接 / 死链 / 漂移链接 / 指向 Hub 外链接 /
106
+ 真副本 / 不存在;`--dry-run` 到此为止(零写入)。
107
+ 2. **unlink**:只删指向 Hub 的链接(含死链);报错即中止,不进入下一步。
108
+ 3. **trash**:Hub 目录移入 `<hub>/../trash/<stamp>/hub-remove/<slug>`,保留 7 天;
109
+ 跨卷自动回退「复制 → 校验 → 删源」。
110
+ 4. **ledger**:定向删 `.yotta-hub.json` 条目 + 过滤 `.yotta-links.json`
111
+ (不调 `syncHubState`,防止把删除记回 `missing`)。
112
+ 5. **审计 + 汇总**:写 `remove` 审计;无痕迹时退出码 4。
113
+
114
+ 边界:真副本 / 外部链接一律保留并报告(真副本版本高于 Hub 时逐条提示);
115
+ `--agent` / `--dir` 收窄仅用于「Hub 目标已缺失」的死链清理;恢复路径 =
116
+ 从回收站取回 + `hub install` 重装 + `hub link` 重建。
117
+
94
118
  ## 宿主发现口径
95
119
 
96
120
  1. 已收录宿主映射(81 条;兼容 Vercel Labs `skills` CLI / SkillCat 的公开宿主表,
@@ -121,9 +145,11 @@ npx -y @yottameta/yotta-skills view --port 8790
121
145
 
122
146
  面板只在 `127.0.0.1`(默认 8789)监听,零远程资源、零遥测,只读写 Hub
123
147
  目录与台账 / 证据文件,不读元忆、不改宿主全局配置。六个视图:概览、宿主矩阵、
124
- 收编向导、链接与体检、记录与回滚、路由与编排,另附高级 CLI 页。
148
+ 收编向导、链接与体检、记录与回滚、路由与编排,另附**全量 CLI 页**(全部命令 /
149
+ 子命令 + 人话说明 + 可复制示例 + 完整选项,与 `--help` 单一真源)。
125
150
 
126
- 动作边界:收编 / 链接 / 解除 / 回滚可执行(预览 → 确认 → 执行 → 证据);
151
+ 动作边界:收编 / 链接 / 解除 / 回滚 / 删除技能可执行(预览 → 确认 → 执行 → 证据;
152
+ 删除技能需手输 slug 二次确认);概览 Hub 内容行可过滤后直接删除技能;
127
153
  install / update / refresh 只在高级 CLI 页给出可复制命令,不在网页执行。
128
154
  写操作需要页面会话令牌,解除与回滚需要破坏性确认串;收编始终逐项运行元信
129
155
  扫描,high / critical 阻断。
@@ -132,6 +158,8 @@ install / update / refresh 只在高级 CLI 页给出可复制命令,不在网
132
158
 
133
159
  - `unlink` 只删除 lstat 确认为链接、且 readlink 目标位于 Hub 内的路径。
134
160
  - 真目录 / 非 Hub 链接一律拒绝删除;`--force` 对真目录先备份再替换。
161
+ - `remove` 只删指向 Hub 的链接与 Hub 内目录;链接清理报错即中止、不删 Hub;
162
+ Hub 目录入回收站保留 7 天,可恢复。
135
163
  - 收敛只动元技能在宿主目录中的旧副本:移入回收站(7 天)而非直删;
136
164
  不碰插件与宿主 MCP 配置;外部技能源文件绝不修改。
137
165
  - Hub 仅本机使用,不对外分发技能内容。
@@ -1,14 +1,14 @@
1
1
  # 全家技能清单(27 个)
2
2
 
3
3
  > 机器权威源:`skills.json`(随包分发);本文件为人工可读副本,新技能发布后须同步更新
4
- > `skills.json`(登记表为内部台账,不随包分发)。清单更新日期:2026-10-02。
4
+ > `skills.json`(登记表为内部台账,不随包分发)。清单更新日期:2026-10-03。
5
5
 
6
6
  | slug | 中文名 | npm 包 | 清单版本 | 家族 | 说明 |
7
7
  |---|---|---|---|---|
8
8
  | `yotta-anti-shallow` | 元谨 | `@yottameta/yotta-anti-shallow` | 1.4.2 | quality | 防 AI 敷衍规则引擎(深入分析/根因追溯时激活) |
9
9
  | `yotta-code-quality` | 元质 | `@yottameta/yotta-code-quality` | 0.4.3 | quality | 结对式代码质量评审(十二类腐化风险 + 0-100 健康分) |
10
10
  | `yotta-workflow` | 元序 | `@yottameta/yotta-workflow` | 0.4.3 | workflow | 跨会话/跨项目通用工作流协议(状态就近存) |
11
- | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.21.1 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
11
+ | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.1 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
12
12
  | `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.2 | memory | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
13
13
  | `yotta-security-audit` | 元安 | `@yottameta/yotta-security-audit` | 0.3.1 | security | 安全扫描引擎:13 类检测器 + 系统安全基线 + 学生数据隐私(教育版) |
14
14
  | `yotta-vetter` | 元审 | `@yottameta/yotta-vetter` | 0.2.7 | security | 安全审查协议:四阶段 review + SAFE TO INSTALL 判定 |
@@ -197,10 +197,10 @@
197
197
  ]
198
198
  },
199
199
  "yotta-memory": {
200
- "version": "0.21.1",
201
- "treeHash": "sha256:452c1674084e916b3f6e0831f3662eaf2852af0e9a4fea3b0ff6a42c6bec9e71",
200
+ "version": "0.22.1",
201
+ "treeHash": "sha256:0fb2fa87f85c43a4e6393791944516e3daa5fecbf5bff77648f57eed71aa1ee2",
202
202
  "files": 18,
203
- "bytes": 1270192,
203
+ "bytes": 1299467,
204
204
  "mode": "strict",
205
205
  "exceptions": [
206
206
  {
@@ -27,7 +27,7 @@ import subprocess
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
- VERSION = "0.28.1"
30
+ VERSION = "0.28.3"
31
31
  TOOL_NAME = "yotta-skills"
32
32
  CN_NAME = "元阁"
33
33
  MCP_PROTOCOL_MODERN = "2026-07-28"
@@ -31,7 +31,7 @@
31
31
  "slug": "yotta-memory",
32
32
  "name": "元忆",
33
33
  "pkg": "@yottameta/yotta-memory",
34
- "version": "0.21.1",
34
+ "version": "0.22.1",
35
35
  "domain": "memory",
36
36
  "runtimePayload": ["bin"],
37
37
  "desc": "有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT)"