@yottameta/yotta-skills-plugin 0.28.2 → 0.29.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.
@@ -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) => {
@@ -1190,6 +1461,22 @@ function doctor(options) {
1190
1461
  singleSourceChecks(opts, add);
1191
1462
  } catch (_) { /* 只读检查失败不影响 doctor 主流程 */ }
1192
1463
 
1464
+ // F3(0.29.0):宿主状态细分只读检查 —— 残留 / 仅标记给 info 提示,不 fail。
1465
+ if (opts.discovery) {
1466
+ for (const host of opts.discovery.hosts || []) {
1467
+ if (!host || !host.dir) continue;
1468
+ if (host.state === 'orphan') {
1469
+ add('host_state:' + pathKey(host.dir), true, 'info',
1470
+ '宿主目录疑似残留(实体未确认): ' + host.label + ' — ' + host.dir,
1471
+ '确认宿主已卸载后可清理:yotta-skills hub hosts remove "' + host.dir + '" --purge(默认预览,--yes 执行)');
1472
+ } else if (host.state === 'marker-only') {
1473
+ add('host_state:' + pathKey(host.dir), true, 'info',
1474
+ '已装实体但技能目录未创建: ' + host.label,
1475
+ '需要分发时运行 yotta-skills hub link --agent ' + (host.agentId || '<id>'));
1476
+ }
1477
+ }
1478
+ }
1479
+
1193
1480
  const errors = checks.filter((check) => !check.ok && check.severity === 'error');
1194
1481
  const warnings = checks.filter((check) => !check.ok && check.severity === 'warning');
1195
1482
  return {
@@ -1233,6 +1520,7 @@ module.exports = {
1233
1520
  classifyTarget,
1234
1521
  linkSkills,
1235
1522
  unlinkSkills,
1523
+ removeSkills,
1236
1524
  linkStatus,
1237
1525
  status,
1238
1526
  doctor,
@@ -86,6 +86,25 @@ function createInstaller(deps) {
86
86
  if (!opts.force && existingVersion === skill.version) {
87
87
  return { skill, status: 'skip', version: existingVersion, note: '已是最新', exitCode: 0 };
88
88
  }
89
+ if (opts.dryRun) {
90
+ // 0.29.0 D1:只读预览 —— 不发网络请求、不写任何文件(含台账)。
91
+ const latestUnresolved = skill.version === 'latest';
92
+ const planned = existingVersion ? 'update' : 'install';
93
+ return {
94
+ skill,
95
+ status: 'planned',
96
+ planned,
97
+ version: latestUnresolved ? 'latest' : skill.version,
98
+ installedVersion: existingVersion,
99
+ latestUnresolved,
100
+ note: latestUnresolved
101
+ ? 'latest(预览不解析)'
102
+ : planned === 'update'
103
+ ? '将更新 v' + existingVersion + ' -> v' + skill.version
104
+ : '将安装 v' + skill.version,
105
+ exitCode: 0,
106
+ };
107
+ }
89
108
 
90
109
  let tmp = null;
91
110
  let staged = null;
@@ -0,0 +1,224 @@
1
+ 'use strict';
2
+ /**
3
+ * 元阁独立安装(install-self)与位置查看(where)—— 0.29.0 F1。
4
+ *
5
+ * 独立安装 = 把管理引擎完整运行件(bin / lib / assets / package.json / skills.json)
6
+ * 装到用户级独立位置(默认 ~/.yottaskills/yotta-skills;YOTTA_SKILLS_HOME 可覆盖根),
7
+ * 写 <root>/self.json 登记。与 Hub 真源、宿主技能池完全独立:不写宿主配置、
8
+ * 不写 mcp.json、不建全局 shim。
9
+ *
10
+ * fail-closed:目标与 Hub 真源重叠 / 锁与数据桥接目录 / 非空陌生目录(--force 才覆盖
11
+ * 同名运行件,无关文件保留)。
12
+ */
13
+ const fs = require('fs');
14
+ const os = require('os');
15
+ const path = require('path');
16
+ const hubLib = require('./hub');
17
+ const agentDirsLib = require('./agent-dirs');
18
+
19
+ const SELF_REGISTRY_VERSION = 1;
20
+ const RUNTIME_PAYLOAD = ['bin', 'lib', 'assets', 'package.json', 'skills.json'];
21
+ const PACKAGE_NAME = '@yottameta/yotta-skills';
22
+
23
+ function nowIso() {
24
+ return new Date().toISOString();
25
+ }
26
+
27
+ function resolveSelfRoot(opts) {
28
+ const options = opts || {};
29
+ const env = options.env || process.env;
30
+ const explicit = options.selfRoot || env.YOTTA_SKILLS_HOME;
31
+ if (explicit) return path.resolve(explicit);
32
+ return path.join(options.homeDir || os.homedir(), '.yottaskills');
33
+ }
34
+
35
+ function selfRegistryPath(opts) {
36
+ return path.join(resolveSelfRoot(opts), 'self.json');
37
+ }
38
+
39
+ function defaultSelfDir(opts) {
40
+ return path.join(resolveSelfRoot(opts), 'yotta-skills');
41
+ }
42
+
43
+ function readSelfRegistry(opts) {
44
+ try {
45
+ const value = JSON.parse(fs.readFileSync(selfRegistryPath(opts), 'utf8'));
46
+ if (value && typeof value === 'object' && Array.isArray(value.installs)) {
47
+ return {
48
+ version: SELF_REGISTRY_VERSION,
49
+ installs: value.installs.filter((item) => item && typeof item.dir === 'string' && item.dir),
50
+ };
51
+ }
52
+ } catch (_) { /* missing or corrupt registry falls back to empty */ }
53
+ return { version: SELF_REGISTRY_VERSION, installs: [] };
54
+ }
55
+
56
+ function writeSelfRegistry(opts, registry) {
57
+ const file = selfRegistryPath(opts);
58
+ fs.mkdirSync(path.dirname(file), { recursive: true });
59
+ const tmp = file + '.tmp-' + process.pid;
60
+ fs.writeFileSync(tmp, JSON.stringify({
61
+ version: SELF_REGISTRY_VERSION,
62
+ installs: registry.installs,
63
+ }, null, 2) + '\n', 'utf8');
64
+ fs.renameSync(tmp, file);
65
+ return file;
66
+ }
67
+
68
+ function safeRealpath(target) {
69
+ try {
70
+ return fs.realpathSync.native(target);
71
+ } catch (_) {
72
+ try {
73
+ return fs.realpathSync(target);
74
+ } catch (_) {
75
+ return path.resolve(target);
76
+ }
77
+ }
78
+ }
79
+
80
+ function samePath(left, right) {
81
+ const a = safeRealpath(left);
82
+ const b = safeRealpath(right);
83
+ if (process.platform === 'win32') return a.toLowerCase() === b.toLowerCase();
84
+ return a === b;
85
+ }
86
+
87
+ function isInside(root, target) {
88
+ const relative = path.relative(path.resolve(root), path.resolve(target));
89
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
90
+ }
91
+
92
+ function packageRoot() {
93
+ return path.join(__dirname, '..');
94
+ }
95
+
96
+ function readPackageVersion(dir) {
97
+ try {
98
+ return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).version || null;
99
+ } catch (_) {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function hasContent(dir) {
105
+ try {
106
+ return fs.readdirSync(dir).length > 0;
107
+ } catch (_) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ /** 目标是否已是元阁运行件目录(允许正常覆盖升级)。 */
113
+ function hasOwnMarker(dir) {
114
+ try {
115
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
116
+ if (pkg && pkg.name === PACKAGE_NAME) return true;
117
+ } catch (_) { /* no readable package.json */ }
118
+ return fs.existsSync(path.join(dir, 'bin', 'yotta-skills.js'));
119
+ }
120
+
121
+ function guardTarget(dir, opts) {
122
+ const hubDir = hubLib.resolveHubDir(opts);
123
+ if (samePath(dir, hubDir) || isInside(hubDir, dir) || isInside(dir, hubDir)) {
124
+ return '目标与 Hub 真源重叠(' + hubDir + '):独立安装与 Hub 必须分离';
125
+ }
126
+ if (agentDirsLib.isBridgeOnlyDir(dir, opts)) {
127
+ return '目标是锁 / 数据桥接目录(XDG_STATE_HOME/skills 或 XDG_DATA_HOME/skills),不允许独立安装';
128
+ }
129
+ return null;
130
+ }
131
+
132
+ function copyEntry(from, to) {
133
+ fs.rmSync(to, { recursive: true, force: true });
134
+ if (fs.statSync(from).isDirectory()) {
135
+ fs.mkdirSync(to, { recursive: true });
136
+ fs.cpSync(from, to, { recursive: true });
137
+ } else {
138
+ fs.mkdirSync(path.dirname(to), { recursive: true });
139
+ fs.copyFileSync(from, to);
140
+ }
141
+ }
142
+
143
+ function installSelf(opts) {
144
+ const options = opts || {};
145
+ const src = packageRoot();
146
+ const dir = path.resolve(options.dir || defaultSelfDir(options));
147
+ const dryRun = Boolean(options.dryRun);
148
+ const force = Boolean(options.force);
149
+ const version = readPackageVersion(src);
150
+ const registryFile = selfRegistryPath(options);
151
+
152
+ const guard = guardTarget(dir, options);
153
+ if (guard) return { ok: false, error: guard, exitCode: 2 };
154
+
155
+ const exists = fs.existsSync(dir);
156
+ const stranger = exists && hasContent(dir) && !hasOwnMarker(dir);
157
+ if (stranger && !force) {
158
+ return {
159
+ ok: false,
160
+ error: '目标是非空陌生目录:' + dir + '(默认拒绝覆盖)',
161
+ hint: '确认目录用途后加 --force 覆盖同名运行件(无关文件保留);或改用 --dir 指定空目录。',
162
+ exitCode: 2,
163
+ };
164
+ }
165
+
166
+ const payload = {
167
+ action: 'install-self',
168
+ dryRun,
169
+ dir,
170
+ version,
171
+ files: RUNTIME_PAYLOAD.slice(),
172
+ registry: registryFile,
173
+ overwrite: exists,
174
+ forced: Boolean(stranger && force),
175
+ exitCode: 0,
176
+ };
177
+ if (dryRun) return { ok: true, ...payload };
178
+
179
+ for (const name of RUNTIME_PAYLOAD) {
180
+ copyEntry(path.join(src, name), path.join(dir, name));
181
+ }
182
+
183
+ const registry = readSelfRegistry(options);
184
+ const entry = { dir, version, installedAt: nowIso(), source: src };
185
+ const index = registry.installs.findIndex((item) => samePath(item.dir, dir));
186
+ if (index >= 0) registry.installs[index] = entry;
187
+ else registry.installs.push(entry);
188
+ writeSelfRegistry(options, registry);
189
+
190
+ return { ok: true, ...payload, installs: registry.installs };
191
+ }
192
+
193
+ function whereInfo(opts) {
194
+ const options = opts || {};
195
+ const running = safeRealpath(process.argv[1] || '');
196
+ const registry = readSelfRegistry(options);
197
+ return {
198
+ action: 'where',
199
+ running,
200
+ version: readPackageVersion(packageRoot()),
201
+ defaultDir: defaultSelfDir(options),
202
+ hubDir: hubLib.resolveHubDir(options),
203
+ registry: selfRegistryPath(options),
204
+ installs: registry.installs.map((item) => ({
205
+ dir: item.dir,
206
+ version: item.version || null,
207
+ installedAt: item.installedAt || null,
208
+ source: item.source || null,
209
+ current: running ? isInside(item.dir, running) : false,
210
+ })),
211
+ };
212
+ }
213
+
214
+ module.exports = {
215
+ SELF_REGISTRY_VERSION,
216
+ RUNTIME_PAYLOAD,
217
+ resolveSelfRoot,
218
+ selfRegistryPath,
219
+ defaultSelfDir,
220
+ readSelfRegistry,
221
+ writeSelfRegistry,
222
+ installSelf,
223
+ whereInfo,
224
+ };
@@ -55,12 +55,48 @@ Hub 真源对清单声明了 `runtimePayload` 的技能(元忆 / 元阁:`bin
55
55
  `hub doctor` 的 `link_scope:*` 检查只读报告「位于默认范围之外」的已建链接,
56
56
  并给出 `hub unlink --dir <目录>` 的清理提示;`hub hosts` 输出三类的本机计数。
57
57
 
58
+ ## 宿主状态细分与自定义注册(0.29.0 起)
59
+
60
+ 每条宿主目录带生命周期状态(`hub hosts` / 面板宿主矩阵):
61
+
62
+ - **可用**:目录存在 + 实体证据(应用标记 / 用户注册 / 手动标记)—— 默认链接 / 分发;
63
+ - **残留**:目录存在 + 无实体证据(卸载残留或 CLI-only 安装)—— 默认保留使用(仍可链接),
64
+ 可显式清理;文案统一「实体未确认」,不判定「已卸载」;
65
+ - **未创建**:目录不存在且无实体证据 —— 安装宿主后自动出现;
66
+ - **仅标记**:已装实体、技能目录未创建 —— `hub link --agent <id>` 会创建目录并链接。
67
+
68
+ `hub doctor` 的 `host_state:*` 检查对残留 / 仅标记给 info 提示,不判 fail。
69
+ 自定义宿主目录可用 `hub hosts add` 注册进发现 / 分发 / 收编范围(只注册,
70
+ 不创建目录、不改宿主配置;移除注册绝不删目录):
71
+
72
+ ```bash
73
+ # 注册 / 查看 / 移除自定义宿主(realpath 去重;Hub / 桥接目录拒绝注册)
74
+ npx -y @yottameta/yotta-skills hub hosts add ~/my-agent/skills --label "我的宿主"
75
+ npx -y @yottameta/yotta-skills hub hosts list
76
+ npx -y @yottameta/yotta-skills hub hosts remove ~/my-agent/skills
77
+
78
+ # 手动标记状态(实体证据不足时):available / orphan / ignored
79
+ npx -y @yottameta/yotta-skills hub hosts mark ~/my-agent/skills --state orphan
80
+
81
+ # 清理残留目录:默认预览;--yes 执行 —— 只删指向 Hub 的链接(含死链),
82
+ # 非 Hub 链接 / 非技能内容随目录保留在回收站(7 天,输出恢复路径)
83
+ npx -y @yottameta/yotta-skills hub hosts remove ~/my-agent/skills --purge
84
+ npx -y @yottameta/yotta-skills hub hosts remove ~/my-agent/skills --purge --yes
85
+ ```
86
+
87
+ 注册表 = `<root>/hosts.json`(root = `YOTTA_SKILLS_HOME` 或 `~/.yottaskills`,schema v1,
88
+ 可 diff);面板「宿主矩阵 → 添加自定义目录」等价可用,写操作需页面令牌与确认串。
89
+
58
90
  ## 命令
59
91
 
60
92
  ```bash
61
93
  # 发现本机已装智能体与技能目录(文件系统优先;不读元忆 / 注册表)
62
94
  npx -y @yottameta/yotta-skills hub hosts
63
95
 
96
+ # 状态过滤 / 自定义注册 / 残留清理(详见「宿主状态细分与自定义注册」节)
97
+ npx -y @yottameta/yotta-skills hub hosts --state orphan
98
+ npx -y @yottameta/yotta-skills hub hosts add ~/my-agent/skills --label "我的宿主"
99
+
64
100
  # 安装 / 更新 Hub 真源
65
101
  # 范围 = 全家清单 27 + 特殊家族 5;特殊家族跟随各自 npm latest
66
102
  # 非元技能不参与更新(来源无统一安装源,用户自行处理)
@@ -86,11 +122,35 @@ npx -y @yottameta/yotta-skills hub link --all --include-discovered
86
122
  # 只删除链接,不动 Hub 真源;清理范围外链接用 --dir <目录> 精确指定
87
123
  npx -y @yottameta/yotta-skills hub unlink --all
88
124
 
125
+ # 指定智能体分发 / 解除(只影响该宿主;Hub 真源保留)
126
+ npx -y @yottameta/yotta-skills hub link yotta-memory --agent codex
127
+ npx -y @yottameta/yotta-skills hub unlink yotta-memory --agent codex
128
+
129
+ # 彻底删除 Hub 技能:全宿主清链接(含死链)+ 目录入回收站 + 清台账 + 审计
130
+ npx -y @yottameta/yotta-skills hub remove my-skill --dry-run
131
+
89
132
  # 查看来源、版本、链接宿主与异常
90
133
  npx -y @yottameta/yotta-skills hub status
91
134
  npx -y @yottameta/yotta-skills hub doctor
92
135
  ```
93
136
 
137
+ ## 删除技能(hub remove,0.28.3 起)
138
+
139
+ `hub remove <slug>` 五阶段 fail-closed:
140
+
141
+ 1. **preflight(只读)**:逐目录分类健康链接 / 死链 / 漂移链接 / 指向 Hub 外链接 /
142
+ 真副本 / 不存在;`--dry-run` 到此为止(零写入)。
143
+ 2. **unlink**:只删指向 Hub 的链接(含死链);报错即中止,不进入下一步。
144
+ 3. **trash**:Hub 目录移入 `<hub>/../trash/<stamp>/hub-remove/<slug>`,保留 7 天;
145
+ 跨卷自动回退「复制 → 校验 → 删源」。
146
+ 4. **ledger**:定向删 `.yotta-hub.json` 条目 + 过滤 `.yotta-links.json`
147
+ (不调 `syncHubState`,防止把删除记回 `missing`)。
148
+ 5. **审计 + 汇总**:写 `remove` 审计;无痕迹时退出码 4。
149
+
150
+ 边界:真副本 / 外部链接一律保留并报告(真副本版本高于 Hub 时逐条提示);
151
+ `--agent` / `--dir` 收窄仅用于「Hub 目标已缺失」的死链清理;恢复路径 =
152
+ 从回收站取回 + `hub install` 重装 + `hub link` 重建。
153
+
94
154
  ## 宿主发现口径
95
155
 
96
156
  1. 已收录宿主映射(81 条;兼容 Vercel Labs `skills` CLI / SkillCat 的公开宿主表,
@@ -121,9 +181,11 @@ npx -y @yottameta/yotta-skills view --port 8790
121
181
 
122
182
  面板只在 `127.0.0.1`(默认 8789)监听,零远程资源、零遥测,只读写 Hub
123
183
  目录与台账 / 证据文件,不读元忆、不改宿主全局配置。六个视图:概览、宿主矩阵、
124
- 收编向导、链接与体检、记录与回滚、路由与编排,另附高级 CLI 页。
184
+ 收编向导、链接与体检、记录与回滚、路由与编排,另附**全量 CLI 页**(全部命令 /
185
+ 子命令 + 人话说明 + 可复制示例 + 完整选项,与 `--help` 单一真源)。
125
186
 
126
- 动作边界:收编 / 链接 / 解除 / 回滚可执行(预览 → 确认 → 执行 → 证据);
187
+ 动作边界:收编 / 链接 / 解除 / 回滚 / 删除技能可执行(预览 → 确认 → 执行 → 证据;
188
+ 删除技能需手输 slug 二次确认);概览 Hub 内容行可过滤后直接删除技能;
127
189
  install / update / refresh 只在高级 CLI 页给出可复制命令,不在网页执行。
128
190
  写操作需要页面会话令牌,解除与回滚需要破坏性确认串;收编始终逐项运行元信
129
191
  扫描,high / critical 阻断。
@@ -132,6 +194,8 @@ install / update / refresh 只在高级 CLI 页给出可复制命令,不在网
132
194
 
133
195
  - `unlink` 只删除 lstat 确认为链接、且 readlink 目标位于 Hub 内的路径。
134
196
  - 真目录 / 非 Hub 链接一律拒绝删除;`--force` 对真目录先备份再替换。
197
+ - `remove` 只删指向 Hub 的链接与 Hub 内目录;链接清理报错即中止、不删 Hub;
198
+ Hub 目录入回收站保留 7 天,可恢复。
135
199
  - 收敛只动元技能在宿主目录中的旧副本:移入回收站(7 天)而非直删;
136
200
  不碰插件与宿主 MCP 配置;外部技能源文件绝不修改。
137
201
  - 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.22.0 | 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 判定 |