@yottameta/yotta-skills-plugin 0.29.3 → 0.29.5

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.
@@ -61,6 +61,39 @@ function restoreBackup(target, backup, hadTarget) {
61
61
  return false;
62
62
  }
63
63
 
64
+ /** 暂存子目录超过 1 小时视为崩溃残留(并发安装的暂存目录总是新鲜的)。 */
65
+ const STAGING_STALE_MS = 60 * 60 * 1000;
66
+
67
+ /** 清理目标目录 `.yottaskills-staging` 中的陈旧暂存(best-effort,返回清理数)。 */
68
+ function pruneStaleStaging(stagingRoot, options) {
69
+ const opts = options || {};
70
+ const now = opts.now instanceof Date ? opts.now.getTime() : Date.now();
71
+ let entries;
72
+ try {
73
+ entries = fs.readdirSync(stagingRoot, { withFileTypes: true });
74
+ } catch (_) {
75
+ return 0;
76
+ }
77
+ let pruned = 0;
78
+ for (const entry of entries) {
79
+ if (!entry.isDirectory()) continue;
80
+ const full = path.join(stagingRoot, entry.name);
81
+ let stat;
82
+ try {
83
+ stat = fs.statSync(full);
84
+ } catch (_) {
85
+ continue;
86
+ }
87
+ if (now - stat.mtimeMs > STAGING_STALE_MS) {
88
+ try {
89
+ fs.rmSync(full, { recursive: true, force: true });
90
+ pruned += 1;
91
+ } catch (_) { /* 保留供下次清理 */ }
92
+ }
93
+ }
94
+ return pruned;
95
+ }
96
+
64
97
  /** 0.29.1 U1「只升不降」跳过原因文案(equal / local-ahead / unknown)。 */
65
98
  function versionSkipNote(relation, installed, target) {
66
99
  if (relation === 'equal') return '已是最新';
@@ -128,6 +161,7 @@ function createInstaller(deps) {
128
161
 
129
162
  let tmp = null;
130
163
  let staged = null;
164
+ let stagingRoot = null;
131
165
  let snapshot = null;
132
166
  let backup = null;
133
167
  const hadTarget = fs.existsSync(target);
@@ -274,14 +308,24 @@ function createInstaller(deps) {
274
308
  }
275
309
 
276
310
  fs.mkdirSync(dest, { recursive: true });
277
- const stagingRoot = path.join(dest, '.yottaskills-staging');
311
+ stagingRoot = path.join(dest, '.yottaskills-staging');
278
312
  fs.mkdirSync(stagingRoot, { recursive: true });
313
+ pruneStaleStaging(stagingRoot);
279
314
  staged = fs.mkdtempSync(path.join(stagingRoot, skill.slug + '-'));
280
315
  // Hub installs keep declared runtime payload (e.g. yotta-skills bin):
281
316
  // hosts linked to the Hub then inherit the runtime files (OpenCode contract).
282
- const runtimePayload = (opts.hubScope && Array.isArray(resolvedSkill.runtimePayload))
283
- ? resolvedSkill.runtimePayload
284
- : [];
317
+ // 0.29.5 S4:`--dir` 托管目录更新保持既有运行时载荷(目标已带 bin/ 等 → 继续携带),
318
+ // 防更新后引擎缺 bin;新装 / 普通技能目录仍只落本体(§6.3 口径不变)。
319
+ let runtimePayload = [];
320
+ if (Array.isArray(resolvedSkill.runtimePayload) && resolvedSkill.runtimePayload.length > 0) {
321
+ if (opts.hubScope) {
322
+ runtimePayload = resolvedSkill.runtimePayload;
323
+ } else if (hadTarget) {
324
+ const carriesPayload = resolvedSkill.runtimePayload
325
+ .some((name) => fs.existsSync(path.join(target, name)));
326
+ if (carriesPayload) runtimePayload = resolvedSkill.runtimePayload;
327
+ }
328
+ }
285
329
  const copyWithRuntime = (src, dst) => deps.copyDir(src, dst, { keep: runtimePayload });
286
330
  copyTree(extracted.pkgDir, staged, copyWithRuntime);
287
331
 
@@ -454,8 +498,12 @@ function createInstaller(deps) {
454
498
  } finally {
455
499
  if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
456
500
  if (staged) fs.rmSync(staged, { recursive: true, force: true });
501
+ if (stagingRoot) {
502
+ // 结束(含失败)强制清理:目录为空时删除;并发安装留下的新鲜暂存则保留。
503
+ try { fs.rmdirSync(stagingRoot); } catch (_) { /* 非空 / 并发占用,保留 */ }
504
+ }
457
505
  }
458
506
  };
459
507
  }
460
508
 
461
- module.exports = { createInstaller, renameWithRetry, isSafeTarEntry };
509
+ module.exports = { createInstaller, renameWithRetry, isSafeTarEntry, pruneStaleStaging, STAGING_STALE_MS };
@@ -72,6 +72,9 @@ function createPackRunner(deps) {
72
72
  timeout: 180000,
73
73
  maxBuffer: 64 * 1024 * 1024,
74
74
  shell: npm.shell,
75
+ // 0.29.5 S4:显式继承调用方环境(YOTTA_SKILLS_REGISTRY_FILE / YOTTA_SKILLS_MANIFEST /
76
+ // npm_config_* 等必须传到 npm / npx 子进程;此前依赖 spawnSync 默认继承,行为不变)。
77
+ env: process.env,
75
78
  });
76
79
 
77
80
  let result = attempt(baseArgs);
@@ -1,9 +1,15 @@
1
1
  'use strict';
2
2
  /**
3
- * 用户级配置(0.29.2 U4)—— <root>/config.json。
3
+ * 用户级配置(0.29.2 U4;0.29.5 扩展 lastMigration / lastSwitch)—— <root>/config.json。
4
4
  *
5
5
  * root = YOTTA_SKILLS_HOME 或 ~/.yottaskills(与 hosts.json / self.json 同根,
6
- * 独立于 Hub 目录本身)。schema v1:{ version, hub: "<path>" | null }。
6
+ * 独立于 Hub 目录本身)。schema v1(向后兼容增量):
7
+ * { version, hub: "<path>" | null, lastMigration?: {...}, lastSwitch?: {...} }
8
+ *
9
+ * - lastMigration:最近一次成功切换位置的真实迁移(含回滚;from→to + 校验技能数 +
10
+ * 重链目录数 + 回收站路径),供 `hub config get` / 面板「最近一次迁移 + 一键回滚」。
11
+ * - lastSwitch:仅切换指针(未迁移)时记录的原位置,供面板提示
12
+ * 「旧 Hub 还有 N 个技能未迁移」。
7
13
  *
8
14
  * Hub 解析优先级:--hub flag > YOTTA_SKILLS_HUB env > config.hub > 默认 <root>/hub。
9
15
  * set 为 fail-closed 校验:不能是文件、不能与配置根 / 独立安装目录重叠、
@@ -39,20 +45,72 @@ function readConfig(opts) {
39
45
  return {
40
46
  version: CONFIG_VERSION,
41
47
  hub: typeof value.hub === 'string' && value.hub.trim() ? value.hub : null,
48
+ lastMigration: sanitizeMigration(value.lastMigration),
49
+ lastSwitch: sanitizeSwitch(value.lastSwitch),
42
50
  };
43
51
  }
44
52
  } catch (_) { /* missing or corrupt config falls back to empty */ }
45
- return { version: CONFIG_VERSION, hub: null };
53
+ return { version: CONFIG_VERSION, hub: null, lastMigration: null, lastSwitch: null };
54
+ }
55
+
56
+ function cleanString(value) {
57
+ return typeof value === 'string' && value.trim() ? value : null;
58
+ }
59
+
60
+ function cleanCount(value) {
61
+ const num = Number(value);
62
+ return Number.isFinite(num) && num >= 0 ? num : 0;
63
+ }
64
+
65
+ /** 规范化 lastMigration(非法条目按缺失处理,绝不抛错)。 */
66
+ function sanitizeMigration(value) {
67
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
68
+ const from = cleanString(value.from);
69
+ const to = cleanString(value.to);
70
+ if (!from || !to) return null;
71
+ return {
72
+ kind: value.kind === 'rollback' ? 'rollback' : 'migrate',
73
+ from,
74
+ to,
75
+ at: cleanString(value.at),
76
+ verifiedSkills: cleanCount(value.verifiedSkills),
77
+ relinkDirs: cleanCount(value.relinkDirs),
78
+ incomplete: cleanCount(value.incomplete),
79
+ trashedTo: cleanString(value.trashedTo),
80
+ trashError: cleanString(value.trashError),
81
+ oldHubKept: cleanString(value.oldHubKept),
82
+ via: cleanString(value.via),
83
+ };
84
+ }
85
+
86
+ /** 规范化 lastSwitch(仅切换指针的记录)。 */
87
+ function sanitizeSwitch(value) {
88
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
89
+ const from = cleanString(value.from);
90
+ const to = cleanString(value.to);
91
+ if (!from || !to) return null;
92
+ return {
93
+ from,
94
+ to,
95
+ at: cleanString(value.at),
96
+ skills: cleanCount(value.skills),
97
+ via: cleanString(value.via),
98
+ };
46
99
  }
47
100
 
48
101
  function writeConfig(opts, config) {
49
102
  const file = configPath(opts);
50
103
  fs.mkdirSync(path.dirname(file), { recursive: true });
51
104
  const tmp = file + '.tmp-' + process.pid;
52
- fs.writeFileSync(tmp, JSON.stringify({
105
+ const payload = {
53
106
  version: CONFIG_VERSION,
54
107
  hub: config && config.hub ? config.hub : null,
55
- }, null, 2) + '\n', 'utf8');
108
+ };
109
+ const migration = sanitizeMigration(config && config.lastMigration);
110
+ const switched = sanitizeSwitch(config && config.lastSwitch);
111
+ if (migration) payload.lastMigration = migration;
112
+ if (switched) payload.lastSwitch = switched;
113
+ fs.writeFileSync(tmp, JSON.stringify(payload, null, 2) + '\n', 'utf8');
56
114
  fs.renameSync(tmp, file);
57
115
  return file;
58
116
  }
@@ -155,6 +213,39 @@ function clearHub(opts) {
155
213
  return { ok: true, removed, configFile: file };
156
214
  }
157
215
 
216
+ /**
217
+ * 记录最近一次迁移(真实移动;kind=migrate / rollback)。
218
+ * 迁移完成即清除 lastSwitch(原位置内容已处理,不再提示)。
219
+ */
220
+ function recordLastMigration(opts, migration) {
221
+ const options = opts || {};
222
+ const config = readConfig(options);
223
+ config.lastMigration = sanitizeMigration(migration);
224
+ config.lastSwitch = null;
225
+ const file = writeConfig(options, config);
226
+ return { ok: true, lastMigration: readConfig(options).lastMigration, configFile: file };
227
+ }
228
+
229
+ /** 记录「仅切换位置」(未迁移):原位置留给面板提示「未迁移」。 */
230
+ function recordLastSwitch(opts, info) {
231
+ const options = opts || {};
232
+ const config = readConfig(options);
233
+ config.lastSwitch = sanitizeSwitch(info);
234
+ const file = writeConfig(options, config);
235
+ return { ok: true, lastSwitch: readConfig(options).lastSwitch, configFile: file };
236
+ }
237
+
238
+ /** 清除「未迁移」提示(原位置已空 / 已迁移 / 用户处理)。 */
239
+ function clearLastSwitch(opts) {
240
+ const options = opts || {};
241
+ const config = readConfig(options);
242
+ if (!config.lastSwitch) return { ok: false, cleared: null };
243
+ const cleared = config.lastSwitch;
244
+ config.lastSwitch = null;
245
+ const file = writeConfig(options, config);
246
+ return { ok: true, cleared, configFile: file };
247
+ }
248
+
158
249
  module.exports = {
159
250
  CONFIG_VERSION,
160
251
  resolveSkillsRoot,
@@ -167,4 +258,7 @@ module.exports = {
167
258
  validateHubPath,
168
259
  setHub,
169
260
  clearHub,
261
+ recordLastMigration,
262
+ recordLastSwitch,
263
+ clearLastSwitch,
170
264
  };
@@ -197,6 +197,9 @@ function scanSkillDir(dir) {
197
197
  return results;
198
198
  }
199
199
  for (const e of entries) {
200
+ // 0.29.5 S4:点目录(.yottaskills-staging / .yottaskills-backup-* 等暂存残留)
201
+ // 绝不当作技能 slug。
202
+ if (e.name.startsWith('.')) continue;
200
203
  if (!e.isDirectory() && !e.isSymbolicLink()) continue;
201
204
  const skillDir = path.join(dir, e.name);
202
205
  try {
@@ -14,7 +14,7 @@ symlink),升级只改真源,已链接宿主即时生效。
14
14
  - 收敛回收站:`~/.yottaskills/trash/<时间戳>/<宿主>/<技能>/`(保留 7 天,可用 `YOTTA_SKILLS_TRASH` 覆盖)
15
15
  - 覆盖方式(优先级):`--hub <dir>` > `YOTTA_SKILLS_HUB` > `config.json`(`hub config set`)> 默认
16
16
 
17
- ## Hub 位置持久化(0.29.2 起)
17
+ ## Hub 位置持久化(0.29.2 起;0.29.5 迁移向导 / 回滚 / 残留清理)
18
18
 
19
19
  `hub config` 把 Hub 位置写进用户级配置(与 hosts.json / self.json 同根):
20
20
 
@@ -22,15 +22,33 @@ symlink),升级只改真源,已链接宿主即时生效。
22
22
  npx -y @yottameta/yotta-skills hub config get # 生效位置 + 来源 + 配置文件
23
23
  npx -y @yottameta/yotta-skills hub config set --hub "D:\my-hub" # 只换指针(旧 Hub 保留)
24
24
  npx -y @yottameta/yotta-skills hub config set --hub "D:\my-hub" --move # 迁移:复制校验 → 重链 → 旧 Hub 入回收站
25
+ npx -y @yottameta/yotta-skills hub config rollback # 回滚预览(上次迁移前的原位置)
26
+ npx -y @yottameta/yotta-skills hub config rollback --yes # 执行回滚(位置回退,用当前内容)
25
27
  npx -y @yottameta/yotta-skills hub config clear # 清除覆盖,回到 flag > env > 默认
26
28
  ```
27
29
 
28
30
  - 配置文件:`<YOTTA_SKILLS_HOME>/config.json`(默认 `~/.yottaskills/config.json`;schema v1,原子写)。
29
31
  - 校验(fail-closed):路径不能是文件、不能与配置根 / 独立安装目录重叠、不能是锁 / 数据桥接目录;路径不存在允许(首次使用时创建)。
30
32
  - `--move` 语义:旧 Hub 复制到新位置 → 逐技能 treeHash 校验 → 切换配置 → `hub link` 以新 Hub 为源 `--force` 重链 → 旧 Hub 入回收站(保留 7 天)。校验失败不切配置;重链未全部完成时旧 Hub 保留供重试(新旧内容一致,混合链接均可用)。
31
- - 面板「Hub 位置」区显示来源 / 配置覆盖 / 重启提示,并提供「设置新位置 / 清除覆盖」(写操作需页面令牌 + `hub-config` 确认串);已在运行的 `view` 需重启后生效。
33
+ - 目标目录只含元阁自己的空台账残留(`.yotta-hub.json` / `.yotta-hub-audit.jsonl`)时,`--move` 默认 fail-closed 并提示;确认后加 `--clean-residue`(残留移入回收站)再迁移。
34
+ - 迁移记录:`config.json` 的 `lastMigration` 记录时间 / from→to / 校验技能数 / 重链目录数 / 回收站与剩余天数;`hub config get` 展示「最近一次迁移」块与回滚命令。
35
+ - 回滚语义:`hub config rollback` 默认预览、`--yes` 执行 = 位置回退(把当前 Hub 内容复制校验回原位置,再重链、当前 Hub 入回收站 7 天);**不是**恢复旧快照。目标非空 fail-closed(不合并)。
36
+ - 仅切换指针(不带 `--move`)时,若旧 Hub 非空会记录「未迁移」并在面板提示「旧 Hub 还有 N 个技能未迁移」+ 一键迁移;执行迁移 / 回滚后该提示自动清除。
37
+ - 面板「Hub 位置」区显示来源 / 配置覆盖 / 重启提示,并提供「设置新位置(迁移向导:迁移 / 仅切换,默认迁移)/ 最近一次迁移 + 一键回滚 / 清除覆盖」(写操作需页面令牌 + `hub-config` 确认串);已在运行的 `view` 需重启后生效。
32
38
  - 误用防护:`--hub` 只允许与 `hub` / `view` / `where` 一起使用;其它命令(含裸 `--hub`)fail-closed 报错,不再静默忽略。
33
39
 
40
+ ## 备份 / 暂存残留清理(0.29.5 起)
41
+
42
+ `hub link --force` 替换真目录时,旧副本移入回收站(不再原地留 `<name>.yottaskills-backup-*`,宿主不会把备份当技能扫描)。历史残留可用:
43
+
44
+ ```bash
45
+ npx -y @yottameta/yotta-skills hub cleanup-backups # 预览(Hub + 已核实宿主)
46
+ npx -y @yottameta/yotta-skills hub cleanup-backups --yes # 移入回收站(7 天可恢复)
47
+ ```
48
+
49
+ - 识别标记:`.yottaskills-backup-*` / `.yottaskills-import-*` / `.yottaskills-rollback-*` / `.yottaskills-staging`。
50
+ - `hub doctor` 会只读报告 `backup_residue:*` 警告并给出清理命令;`--dir` / `--agent` 可收窄范围。
51
+
34
52
  ## 元技能唯一性收敛(`hub link` 执行时)
35
53
 
36
54
  元技能(清单 27 + 特殊家族 5:元开 / 元伴 / 元呈 / 元阁本体 / 元信MCP)在
@@ -160,7 +178,7 @@ npx -y @yottameta/yotta-skills hub adopt --apply
160
178
  npx -y @yottameta/yotta-skills hub refresh my-skill --from <技能目录>
161
179
 
162
180
  # 分发到全部已核实宿主(默认范围):元技能链接时收敛旧副本(回收站 7 天);
163
- # 外部技能同名真目录默认跳过(--force 才备份并替换)
181
+ # 外部技能同名真目录默认跳过(--force 才把旧副本移入回收站并替换)
164
182
  npx -y @yottameta/yotta-skills hub link --all
165
183
 
166
184
  # 显式把自动发现目录一并纳入(桥接目录永不链接)
@@ -240,7 +258,8 @@ install / update / refresh 只在高级 CLI 页给出可复制命令,不在网
240
258
  ## 安全边界
241
259
 
242
260
  - `unlink` 只删除 lstat 确认为链接、且 readlink 目标位于 Hub 内的路径。
243
- - 真目录 / 非 Hub 链接一律拒绝删除;`--force` 对真目录先备份再替换。
261
+ - 真目录 / 非 Hub 链接一律拒绝删除;`--force` 对真目录把旧副本移入回收站(7 天)再替换;
262
+ 替换失败会把旧副本移回原位(回收站路径随结果输出)。
244
263
  - `remove` 只删指向 Hub 的链接与 Hub 内目录;链接清理报错即中止、不删 Hub;
245
264
  Hub 目录入回收站保留 7 天,可恢复。
246
265
  - 收敛只动元技能在宿主目录中的旧副本:移入回收站(7 天)而非直删;
@@ -9,7 +9,7 @@
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
11
  | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.3 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
12
- | `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.2.2 | memory | 学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目 |
12
+ | `yotta-learn` | 元习 | `@yottameta/yotta-learn` | 0.3.0 | 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 判定 |
15
15
  | `yotta-recon` | 元析 | `@yottameta/yotta-recon` | 0.1.8 | security | 跨智能体网络侦察:零依赖端口/服务/版本指纹探测 |
@@ -27,7 +27,7 @@ import subprocess
27
27
  import sys
28
28
  from pathlib import Path
29
29
 
30
- VERSION = "0.29.3"
30
+ VERSION = "0.29.5"
31
31
  TOOL_NAME = "yotta-skills"
32
32
  CN_NAME = "元阁"
33
33
  MCP_PROTOCOL_MODERN = "2026-07-28"
@@ -40,9 +40,9 @@
40
40
  "slug": "yotta-learn",
41
41
  "name": "元习",
42
42
  "pkg": "@yottameta/yotta-learn",
43
- "version": "0.2.2",
43
+ "version": "0.3.0",
44
44
  "domain": "memory",
45
- "desc": "学习沉淀 CLI:错误/纠正/洞见沉淀为 .learnings/ 条目"
45
+ "desc": "学习闭环 + 知识库 CLI:经验沉淀为 .learnings/ 条目;知识分类索引 + 关键词查询"
46
46
  },
47
47
  {
48
48
  "slug": "yotta-security-audit",