@yottameta/yotta-skills-plugin 0.28.3 → 0.29.1

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.
@@ -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
+ };
@@ -113,6 +113,30 @@ function compareSemver(left, right) {
113
113
  return comparePrerelease(a.prerelease, b.prerelease);
114
114
  }
115
115
 
116
+ /**
117
+ * 版本关系判定(元阁 update「只升不降」统一口径,0.29.1 U1)。
118
+ *
119
+ * installed:本地已装版本(null = 未装);target:目标版本(精确 semver 或 'latest')。
120
+ * 返回:
121
+ * - install:本地未装(含 target = latest,解析后照常安装)
122
+ * - upgrade:本地低于目标(唯一应安装的关系)
123
+ * - equal:本地与目标相等
124
+ * - local-ahead:本地高于目标(保留,不降级)
125
+ * - unknown:任一侧无法解析(fail-safe 保留)
126
+ * - resolve-needed:目标为 latest(需先解析出精确版本再复判)
127
+ *
128
+ * 注意:compareSemver 对不可解析输入返回 0,必须先 parseSemver 校验,
129
+ * 不能直接拿 compare === 0 当相等。
130
+ */
131
+ function versionRelation(installed, target) {
132
+ if (!installed) return 'install';
133
+ if (target === 'latest') return 'resolve-needed';
134
+ if (!parseSemver(installed) || !parseSemver(target)) return 'unknown';
135
+ const diff = compareSemver(installed, target);
136
+ if (diff === 0) return 'equal';
137
+ return diff > 0 ? 'local-ahead' : 'upgrade';
138
+ }
139
+
116
140
  /** 从多个安装副本中选择代表版本:优先合法版本中的最高 semver。 */
117
141
  function selectRepresentativeVariant(variants) {
118
142
  let selected = null;
@@ -436,6 +460,7 @@ module.exports = {
436
460
  parseFrontmatter,
437
461
  parseSemver,
438
462
  compareSemver,
463
+ versionRelation,
439
464
  strictFrontmatterWarnings,
440
465
  scanSkillDir,
441
466
  defaultRoots,
@@ -55,12 +55,78 @@ 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
+
90
+ ## 不接管与目录覆盖(0.29.1 起)
91
+
92
+ **不接管**(excluded):把某个智能体或目录从发现、显示、链接三层全部跳过
93
+ (`--include-discovered` 也不纳入);已建立的链接不受影响,可用 `hub unlink` 清理:
94
+
95
+ ```bash
96
+ npx -y @yottameta/yotta-skills hub hosts exclude box # 按 agentId
97
+ npx -y @yottameta/yotta-skills hub hosts exclude "D:\my-agent\skills" # 按目录
98
+ npx -y @yottameta/yotta-skills hub hosts include box # 恢复接管
99
+ ```
100
+
101
+ **目录覆盖**(override):用户级覆盖某个已收录 agentId 的内置映射目录
102
+ (不改宿主配置;被覆盖的旧目录不再进入默认发现 / 链接):
103
+
104
+ ```bash
105
+ npx -y @yottameta/yotta-skills hub hosts set box --dir "D:\box-agent\skills"
106
+ npx -y @yottameta/yotta-skills hub hosts set box --clear # 恢复内置映射
107
+ ```
108
+
109
+ 面板「宿主矩阵」提供等价动作:行内「不接管 / 编辑目录 / 标记可用 / 忽略」,
110
+ 底部「不接管名单」区可「恢复接管」;写操作均写 Hub 审计。
111
+
112
+ **YottaCode 不纳入接管**:YottaCode 自带三层技能管理(skill-inventory / skill /
113
+ user-skills),元阁对其 `.yottacode` 路径与 `YOTTACODE_HOME` 子树在映射、扫描、
114
+ 注册、标记、链接全部 fail-closed 排除,避免重复管理。
115
+
116
+ **未核实映射 ≠ 自动发现**:内置映射里尚未逐机核实的条目显示「映射·未核实」;
117
+ 「自动发现」只用于文件系统扫描结果。`installed` 配对按「用户注册 / 已核实映射 > env >
118
+ 未核实映射 > 自动发现」确定性优选(同级按技能数 + 路径稳定排序)。
119
+
58
120
  ## 命令
59
121
 
60
122
  ```bash
61
123
  # 发现本机已装智能体与技能目录(文件系统优先;不读元忆 / 注册表)
62
124
  npx -y @yottameta/yotta-skills hub hosts
63
125
 
126
+ # 状态过滤 / 自定义注册 / 残留清理(详见「宿主状态细分与自定义注册」节)
127
+ npx -y @yottameta/yotta-skills hub hosts --state orphan
128
+ npx -y @yottameta/yotta-skills hub hosts add ~/my-agent/skills --label "我的宿主"
129
+
64
130
  # 安装 / 更新 Hub 真源
65
131
  # 范围 = 全家清单 27 + 特殊家族 5;特殊家族跟随各自 npm latest
66
132
  # 非元技能不参与更新(来源无统一安装源,用户自行处理)
@@ -77,14 +77,17 @@
77
77
 
78
78
  - 读取目标 `<dest>/<slug>/SKILL.md` 的 frontmatter `version`;
79
79
  - 与清单 `version` 一致 → 跳过(幂等:第二次安装 27 个全部跳过);
80
- - `--force` 强制重装;`update` 对「缺失 / 版本不一致」的技能重装。
80
+ - **只升不降(0.29.1 起)**:本地版本高于目标(清单 pin / npm latest)→ 保留并标注「本地领先」,
81
+ `update` / `hub update` 跳过、`update --check` 不计更新(退出码 0)、`update --auto` 不动作;
82
+ `latest` / `--range` 目标在解析出精确版本后、任何写入之前复判;
83
+ - `--force` 强制重装 / 显式降级;`rollback` 恢复快照;`update` 对「缺失 / 需要升级」的技能安装。
81
84
 
82
85
  ## 范围控制(接多少管多少)
83
86
 
84
87
  | 用法 | 范围 |
85
88
  |---|---|
86
89
  | `install`(无旗标) | 全量清单全部安装 |
87
- | `update`(无旗标) | 补齐缺失 + 升级已装(现状语义不变) |
90
+ | `update`(无旗标) | 补齐缺失 + 升级已装(只升不降:本地更高保留不降级) |
88
91
  | `update --installed-only` | 只维护目标目录已安装的家族技能(不补装缺失;无匹配时退出码 0) |
89
92
  | `--only <a,b>` / `--domain <name>` | 只处理指定技能 / 家族;与 `--installed-only` 取交集 |
90
93
  | `update --check` / `--auto` | 只检查 / 维护已装技能,不补装缺失 |
@@ -8,7 +8,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.1 | memory | 有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT) |
11
+ | `yotta-memory` | 元忆 | `@yottameta/yotta-memory` | 0.22.2 | 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.22.1",
201
- "treeHash": "sha256:0fb2fa87f85c43a4e6393791944516e3daa5fecbf5bff77648f57eed71aa1ee2",
200
+ "version": "0.22.2",
201
+ "treeHash": "sha256:7b2cd7a2b5d4eef365dabd6f1d83200458d28ddea1140c362bbea628e832b5f6",
202
202
  "files": 18,
203
- "bytes": 1299467,
203
+ "bytes": 1302762,
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.3"
30
+ VERSION = "0.29.1"
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.22.1",
34
+ "version": "0.22.2",
35
35
  "domain": "memory",
36
36
  "runtimePayload": ["bin"],
37
37
  "desc": "有权限边界的文件式智能体记忆(FACT/PREF/BOUND/COMMIT)"