@yottameta/yotta-skills-plugin 0.29.0 → 0.29.2
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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/yotta-skills/SKILL.md +28 -3
- package/skills/yotta-skills/assets/view.html +130 -5
- package/skills/yotta-skills/bin/yotta-skills.js +465 -37
- package/skills/yotta-skills/lib/agent-dirs.js +17 -3
- package/skills/yotta-skills/lib/agent-discovery.js +109 -11
- package/skills/yotta-skills/lib/cli-help.js +17 -3
- package/skills/yotta-skills/lib/hosts-registry.js +143 -1
- package/skills/yotta-skills/lib/hub-adopt.js +1 -1
- package/skills/yotta-skills/lib/hub-migrate.js +183 -0
- package/skills/yotta-skills/lib/hub-scan.js +37 -3
- package/skills/yotta-skills/lib/hub-view-server.js +131 -1
- package/skills/yotta-skills/lib/hub.js +8 -4
- package/skills/yotta-skills/lib/install-pipeline.js +39 -2
- package/skills/yotta-skills/lib/self-install.js +5 -1
- package/skills/yotta-skills/lib/skills-config.js +170 -0
- package/skills/yotta-skills/lib/skills-scan.js +25 -0
- package/skills/yotta-skills/references/hub.md +48 -1
- package/skills/yotta-skills/references/install-flow.md +5 -2
- package/skills/yotta-skills/references/skill-list.md +2 -2
- package/skills/yotta-skills/scan-policy.json +3 -3
- package/skills/yotta-skills/scripts/yotta-skills-mcp.py +1 -1
- package/skills/yotta-skills/skills.json +2 -2
|
@@ -87,7 +87,9 @@ const AGENT_DIRS = {
|
|
|
87
87
|
pochi: { label: 'Pochi', dirs: ['.pochi/skills'] },
|
|
88
88
|
adal: { label: 'AdaL', dirs: ['.adal/skills'] },
|
|
89
89
|
dsh: { label: 'DSH / DeepSeek Harness', dirs: ['.dsh/skills'] },
|
|
90
|
-
|
|
90
|
+
// YottaCode 自带三层技能管理(skill-inventory / skill / user-skills),
|
|
91
|
+
// 不纳入元阁接管:neverLink 表示发现 / 显示 / 链接全部跳过(0.29.1 U2)。
|
|
92
|
+
yottacode: { label: 'YottaCode', dirs: ['.yottacode/skills'], verified: false, neverLink: true },
|
|
91
93
|
box: { label: 'Box Agent', dirs: ['.box-agent/skills'], verified: false },
|
|
92
94
|
lmstudio: { label: 'LM Studio', dirs: ['.lmstudio/skills'] },
|
|
93
95
|
ccswitch: { label: 'CC Switch', dirs: ['.cc-switch/skills'] },
|
|
@@ -167,6 +169,14 @@ function resolveUserDir(rel, options) {
|
|
|
167
169
|
return path.join(home, rel);
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
const YOTTACODE_PATH_RE = /(^|[\\/])\.yottacode([\\/]|$)/i;
|
|
173
|
+
|
|
174
|
+
/** YottaCode 路径判定(不接管红线;发现 / 链接 / 注册 / 标记统一 fail-closed)。 */
|
|
175
|
+
function isYottaCodeDir(dir) {
|
|
176
|
+
if (!dir) return false;
|
|
177
|
+
return YOTTACODE_PATH_RE.test(path.resolve(dir));
|
|
178
|
+
}
|
|
179
|
+
|
|
170
180
|
/** All known agent skill directories, including missing ones. */
|
|
171
181
|
function knownRoots(options) {
|
|
172
182
|
const roots = [];
|
|
@@ -179,8 +189,11 @@ function knownRoots(options) {
|
|
|
179
189
|
roots.push({ dir: resolved, agentId, label, known: true, verified: verified !== false });
|
|
180
190
|
};
|
|
181
191
|
for (const [agentId, info] of Object.entries(AGENT_DIRS)) {
|
|
192
|
+
if (info.neverLink) continue;
|
|
182
193
|
for (const rel of info.dirs) {
|
|
183
|
-
|
|
194
|
+
const dir = resolveUserDir(rel, options);
|
|
195
|
+
if (isYottaCodeDir(dir)) continue;
|
|
196
|
+
add(dir, agentId, info.label, info.verified !== false);
|
|
184
197
|
}
|
|
185
198
|
}
|
|
186
199
|
return roots;
|
|
@@ -213,7 +226,7 @@ function envRoots(options) {
|
|
|
213
226
|
add(envPath(env, 'DSH_AGENTS_HOME') && path.join(env.DSH_AGENTS_HOME, 'skills'), 'dsh', 'DSH Agents(DSH_AGENTS_HOME)');
|
|
214
227
|
add(envPath(env, 'OPENCLAW_STATE_DIR') && path.join(env.OPENCLAW_STATE_DIR, 'skills'), 'openclaw', 'OpenClaw(OPENCLAW_STATE_DIR)');
|
|
215
228
|
add(envPath(env, 'CLAUDE_CONFIG_DIR') && path.join(env.CLAUDE_CONFIG_DIR, 'skills'), 'claude', 'Claude Code(CLAUDE_CONFIG_DIR)');
|
|
216
|
-
|
|
229
|
+
// YOTTACODE_HOME 不纳入元阁接管(YottaCode 自带三层技能管理,0.29.1 U2)。
|
|
217
230
|
|
|
218
231
|
const extra = String(env.YOTTA_SKILLS_DISCOVERY_ROOTS || '')
|
|
219
232
|
.split(path.delimiter)
|
|
@@ -255,4 +268,5 @@ module.exports = {
|
|
|
255
268
|
envRoots,
|
|
256
269
|
bridgeOnlyDirs,
|
|
257
270
|
isBridgeOnlyDir,
|
|
271
|
+
isYottaCodeDir,
|
|
258
272
|
};
|
|
@@ -82,6 +82,7 @@ function isNoisePath(dir, baseDir) {
|
|
|
82
82
|
if (part === 'temp' || part === 'tmp') return true;
|
|
83
83
|
if (/\.bak$/.test(part)) return true;
|
|
84
84
|
if (part === 'plugin-sources' || part === 'plugins' || part === 'node_modules') return true;
|
|
85
|
+
if (part === '.yottacode') return true;
|
|
85
86
|
if (part === 'candidate' || part.startsWith('candidate-') || part.includes('staging')) return true;
|
|
86
87
|
if (part === 'memories' || part === 'connectors' || part === 'workspace') return true;
|
|
87
88
|
}
|
|
@@ -192,7 +193,7 @@ function commonRoots(options) {
|
|
|
192
193
|
add(env.OPENCLAW_STATE_DIR, 2);
|
|
193
194
|
add(env.CLAUDE_CONFIG_DIR, 2);
|
|
194
195
|
add(env.CODEX_HOME, 2);
|
|
195
|
-
|
|
196
|
+
// YOTTACODE_HOME 不扫描(YottaCode 自带三层技能管理,0.29.1 U2)。
|
|
196
197
|
|
|
197
198
|
// A portable agent install often lives beside the current workspace. The
|
|
198
199
|
// scanner only follows agent-looking branches, so this stays bounded.
|
|
@@ -286,6 +287,28 @@ function judgeHostState(host) {
|
|
|
286
287
|
return 'missing';
|
|
287
288
|
}
|
|
288
289
|
|
|
290
|
+
/**
|
|
291
|
+
* 0.29.1 U2:installed 配对优先级 ——
|
|
292
|
+
* user-registry / 目录覆盖 / 已核实映射 > env > 未核实映射 > 自动发现。
|
|
293
|
+
*/
|
|
294
|
+
function hostPairingPriority(host) {
|
|
295
|
+
if (host.detection === 'user-registry' || host.registry) return 0;
|
|
296
|
+
if (host.detection === 'override') return 1;
|
|
297
|
+
if (host.verified && host.detection === 'mapping') return 1;
|
|
298
|
+
if (host.detection === 'env') return 2;
|
|
299
|
+
if (host.detection === 'mapping') return 3;
|
|
300
|
+
return 4;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** 同级按技能数降序 + 路径稳定排序(消除 last-write-wins)。 */
|
|
304
|
+
function compareHostCandidates(a, b) {
|
|
305
|
+
const priority = hostPairingPriority(a) - hostPairingPriority(b);
|
|
306
|
+
if (priority) return priority;
|
|
307
|
+
const count = (b.skillCount || 0) - (a.skillCount || 0);
|
|
308
|
+
if (count) return count;
|
|
309
|
+
return String(a.dir).localeCompare(String(b.dir));
|
|
310
|
+
}
|
|
311
|
+
|
|
289
312
|
/**
|
|
290
313
|
* Discover hosts and their skill directories.
|
|
291
314
|
*
|
|
@@ -301,19 +324,56 @@ function discoverHosts(options) {
|
|
|
301
324
|
const seen = new Map();
|
|
302
325
|
const errors = [];
|
|
303
326
|
|
|
327
|
+
// 0.29.1 U2:用户级「不接管」名单 + 目录覆盖(hosts.json;旧文件向后兼容)。
|
|
328
|
+
let registry = { hosts: [], excluded: [], overrides: [] };
|
|
329
|
+
try {
|
|
330
|
+
registry = hostsRegistry.readHostsRegistry(options);
|
|
331
|
+
} catch (_) { /* registry 读取失败不影响发现 */ }
|
|
332
|
+
const overridesByAgent = new Map(
|
|
333
|
+
(registry.overrides || []).map((item) => [String(item.agentId).toLowerCase(), item]),
|
|
334
|
+
);
|
|
335
|
+
const previousDirKeys = new Set();
|
|
336
|
+
for (const item of registry.overrides || []) {
|
|
337
|
+
for (const prev of item.previousDirs || []) previousDirKeys.add(normalizePath(prev).toLowerCase());
|
|
338
|
+
}
|
|
339
|
+
// YottaCode 不接管:.yottacode 路径 + YOTTACODE_HOME 整棵子树(含任意自定义目录名)。
|
|
340
|
+
const yottacodeHome = envObject(options).YOTTACODE_HOME
|
|
341
|
+
? path.resolve(envObject(options).YOTTACODE_HOME)
|
|
342
|
+
: null;
|
|
343
|
+
const isYottaCodePath = (dir) => {
|
|
344
|
+
if (!dir) return false;
|
|
345
|
+
if (agentDirs.isYottaCodeDir(dir)) return true;
|
|
346
|
+
if (!yottacodeHome) return false;
|
|
347
|
+
const resolved = path.resolve(dir);
|
|
348
|
+
const relative = path.relative(yottacodeHome, resolved);
|
|
349
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
350
|
+
};
|
|
351
|
+
const isExcluded = (meta) => Boolean(hostsRegistry.excludedEntryFor(registry, meta || {}));
|
|
352
|
+
const skipDir = (dir, agentId) => isYottaCodePath(dir) || isExcluded({ dir, agentId });
|
|
353
|
+
|
|
304
354
|
for (const root of agentDirs.knownRoots(options)) {
|
|
355
|
+
const override = root.agentId ? overridesByAgent.get(String(root.agentId).toLowerCase()) : null;
|
|
356
|
+
if (override) {
|
|
357
|
+
if (skipDir(override.dir, root.agentId)) continue;
|
|
358
|
+
addRoot(roots, seen, override.dir, {
|
|
359
|
+
...root,
|
|
360
|
+
label: override.label || root.label,
|
|
361
|
+
verified: true,
|
|
362
|
+
detection: 'override',
|
|
363
|
+
});
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (skipDir(root.dir, root.agentId)) continue;
|
|
305
367
|
addRoot(roots, seen, root.dir, { ...root, detection: 'mapping' });
|
|
306
368
|
}
|
|
307
369
|
for (const root of agentDirs.envRoots(options)) {
|
|
370
|
+
if (skipDir(root.dir, root.agentId)) continue;
|
|
308
371
|
addRoot(roots, seen, root.dir, { ...root, detection: root.env ? 'env' : 'workspace' });
|
|
309
372
|
}
|
|
310
373
|
// 合并顺序:内置映射 → 环境变量 → 用户注册表(F2)→ 自动发现;
|
|
311
374
|
// 同 realpath 用户注册优先(分类 = 已核实,detection = user-registry)。
|
|
312
|
-
let registry = { hosts: [] };
|
|
313
|
-
try {
|
|
314
|
-
registry = hostsRegistry.readHostsRegistry(options);
|
|
315
|
-
} catch (_) { /* registry 读取失败不影响发现 */ }
|
|
316
375
|
for (const item of registry.hosts || []) {
|
|
376
|
+
if (skipDir(item.dir, item.agentId)) continue;
|
|
317
377
|
addRoot(roots, seen, item.dir, {
|
|
318
378
|
detection: 'user-registry',
|
|
319
379
|
label: item.label || '自定义宿主',
|
|
@@ -329,6 +389,9 @@ function discoverHosts(options) {
|
|
|
329
389
|
maxDepth: root.maxDepth,
|
|
330
390
|
baseDir: options.homeDir || os.homedir(),
|
|
331
391
|
})) {
|
|
392
|
+
if (skipDir(found.dir, null)) continue;
|
|
393
|
+
// 被目录覆盖替换掉的旧映射目录不再进入默认发现 / 链接。
|
|
394
|
+
if (previousDirKeys.has(normalizePath(found.dir).toLowerCase())) continue;
|
|
332
395
|
addRoot(roots, seen, found.dir, {
|
|
333
396
|
detection: 'discovered',
|
|
334
397
|
label: '自动发现:' + path.basename(found.parent),
|
|
@@ -362,23 +425,44 @@ function discoverHosts(options) {
|
|
|
362
425
|
installedSeen.add(key);
|
|
363
426
|
installed.push({ label, source, hasSkillsDir: false });
|
|
364
427
|
};
|
|
428
|
+
// 0.29.1 U2:不接管(excluded)/ neverLink(YottaCode)宿主的应用标记同样跳过显示。
|
|
429
|
+
const isMarkerSuppressed = (name) => {
|
|
430
|
+
for (const [agentId, info] of Object.entries(agentDirs.AGENT_DIRS)) {
|
|
431
|
+
if (!info.neverLink) continue;
|
|
432
|
+
if (markerMatchesHost(name, { agentId, label: info.label })) return true;
|
|
433
|
+
}
|
|
434
|
+
return (registry.excluded || []).some((item) => {
|
|
435
|
+
if (item.kind !== 'agent') return false;
|
|
436
|
+
const info = agentDirs.AGENT_DIRS[String(item.value).toLowerCase()] || {};
|
|
437
|
+
return markerMatchesHost(name, { agentId: item.value, label: info.label || item.value });
|
|
438
|
+
});
|
|
439
|
+
};
|
|
365
440
|
for (const name of markers) {
|
|
441
|
+
if (isMarkerSuppressed(name)) continue;
|
|
366
442
|
if (knownLabelMatches(name) || AGENT_HINT.test(name)) addInstalled(name, '应用标记');
|
|
367
443
|
}
|
|
368
444
|
for (const host of roots) {
|
|
369
445
|
if (host.detection === 'user-registry') addInstalled(host.label, '用户注册');
|
|
370
446
|
}
|
|
447
|
+
// 0.29.1 U2:配对确定性优选(先收集全部候选,再按优先级选定),消除 last-write-wins。
|
|
448
|
+
const pairCandidates = new Map();
|
|
371
449
|
for (const host of roots) {
|
|
372
450
|
if (!host.exists) continue;
|
|
373
451
|
if (!host.markerEvidence && host.detection !== 'user-registry') continue;
|
|
374
|
-
const item
|
|
375
|
-
|
|
376
|
-
item.
|
|
377
|
-
item.
|
|
378
|
-
item.skillCount = host.skillCount;
|
|
379
|
-
item.agentId = host.agentId;
|
|
452
|
+
for (const item of installed) {
|
|
453
|
+
if (!markerMatchesHost(item.label, host)) continue;
|
|
454
|
+
if (!pairCandidates.has(item)) pairCandidates.set(item, []);
|
|
455
|
+
pairCandidates.get(item).push(host);
|
|
380
456
|
}
|
|
381
457
|
}
|
|
458
|
+
for (const [item, hosts] of pairCandidates) {
|
|
459
|
+
hosts.sort(compareHostCandidates);
|
|
460
|
+
const host = hosts[0];
|
|
461
|
+
item.hasSkillsDir = true;
|
|
462
|
+
item.skillDir = host.dir;
|
|
463
|
+
item.skillCount = host.skillCount;
|
|
464
|
+
item.agentId = host.agentId;
|
|
465
|
+
}
|
|
382
466
|
|
|
383
467
|
return {
|
|
384
468
|
generatedAt: new Date().toISOString(),
|
|
@@ -388,6 +472,19 @@ function discoverHosts(options) {
|
|
|
388
472
|
};
|
|
389
473
|
}
|
|
390
474
|
|
|
475
|
+
/**
|
|
476
|
+
* 0.29.1 U2:未配对且无技能目录的已安装标记(CLI / 面板共用单一真源)。
|
|
477
|
+
* 已配对(hasSkillsDir)或 label 与已显示宿主重复的标记不再出现在「无技能目录」区。
|
|
478
|
+
*/
|
|
479
|
+
function unpairedMarkers(discovery) {
|
|
480
|
+
const shown = new Set();
|
|
481
|
+
for (const host of (discovery && discovery.hosts) || []) {
|
|
482
|
+
if (host.exists || host.state === 'marker-only') shown.add(String(host.label).toLowerCase());
|
|
483
|
+
}
|
|
484
|
+
return ((discovery && discovery.installed) || []).filter((item) =>
|
|
485
|
+
!item.hasSkillsDir && !shown.has(String(item.label).toLowerCase()));
|
|
486
|
+
}
|
|
487
|
+
|
|
391
488
|
module.exports = {
|
|
392
489
|
AGENT_HINT,
|
|
393
490
|
discoverHosts,
|
|
@@ -395,4 +492,5 @@ module.exports = {
|
|
|
395
492
|
countSkills,
|
|
396
493
|
markerMatchesHost,
|
|
397
494
|
judgeHostState,
|
|
495
|
+
unpairedMarkers,
|
|
398
496
|
};
|
|
@@ -40,7 +40,8 @@ const CLI_GLOBAL_OPTIONS = [
|
|
|
40
40
|
{ flag: '--yes', arg: '', what: 'usage reset 的确认开关', when: '确认清空本地使用记录时', caution: '会清空记录文件' },
|
|
41
41
|
{ flag: '--explain', arg: '', what: 'decide-memory 文本报告追加信号明细', when: '想看裁决依据时', caution: '' },
|
|
42
42
|
{ flag: '--promote', arg: '', what: 'decide-memory 只写本地建议文件(不写元忆)', when: '想留下裁决建议文件时', caution: '不会写元忆、不会删除任何内容' },
|
|
43
|
-
{ flag: '--hub', arg: '<path>', what: 'Hub 目录(默认 ~/.yottaskills/hub)', when: '使用自定义 Hub 位置时', caution: 'YOTTA_SKILLS_HUB
|
|
43
|
+
{ flag: '--hub', arg: '<path>', what: 'Hub 目录(默认 ~/.yottaskills/hub;持久配置见 hub config)', when: '使用自定义 Hub 位置时', caution: '覆盖顺序:--hub > YOTTA_SKILLS_HUB > config.json > 默认' },
|
|
44
|
+
{ flag: '--move', arg: '', what: 'hub config set 迁移:复制校验旧 Hub → 重链宿主 → 旧 Hub 入回收站(7 天)', when: '换 Hub 位置并带上已有技能时', caution: 'fail-closed:校验失败不切配置;重链失败保留旧 Hub' },
|
|
44
45
|
{ flag: '--port', arg: '<n>', what: 'view 面板端口(默认 8789)', when: '端口冲突或想固定端口时', caution: '仅 127.0.0.1 监听' },
|
|
45
46
|
{ flag: '--all', arg: '', what: 'hub link / unlink 作用于全部已核实宿主', when: '全量分发或解除时', caution: '' },
|
|
46
47
|
{ flag: '--include-discovered', arg: '', what: 'hub link / unlink / remove 时纳入自动发现目录', when: '确认要扩围到未核实目录时', caution: '默认只链已核实宿主;桥接目录永不链接' },
|
|
@@ -48,6 +49,7 @@ const CLI_GLOBAL_OPTIONS = [
|
|
|
48
49
|
{ flag: '--label', arg: '<名称>', what: 'hub hosts add 的显示名称(默认取目录名)', when: '注册自定义宿主时', caution: '最长 48 字符,禁控制字符' },
|
|
49
50
|
{ flag: '--id', arg: '<agentId>', what: 'hub hosts add / remove 的智能体标识', when: '按智能体注册或移除自定义宿主时', caution: '' },
|
|
50
51
|
{ flag: '--purge', arg: '', what: 'hub hosts remove 清理残留目录(默认预览;--yes 执行)', when: '清理实体未确认的残留技能目录时', caution: '只删指向 Hub 的链接;目录移入回收站保留 7 天' },
|
|
52
|
+
{ flag: '--clear', arg: '', what: 'hub hosts set 恢复内置映射(删除目录覆盖)', when: '想撤销目录覆盖时', caution: '' },
|
|
51
53
|
{ flag: '--scan', arg: '', what: 'hub adopt 只读预演(默认行为)', when: '先看收编候选与冲突时', caution: '' },
|
|
52
54
|
{ flag: '--apply', arg: '', what: 'hub adopt 执行收编', when: '确认候选后执行时', caution: '执行时逐项运行元信扫描;high / critical 默认阻断' },
|
|
53
55
|
{ flag: '--in-place', arg: '', what: 'hub adopt 原地登记,不复制', when: '想让 Hub 直接指向原目录时', caution: '原目录删除即断链;默认复制保真更安全' },
|
|
@@ -78,7 +80,7 @@ const CLI_HELP_MODEL = [
|
|
|
78
80
|
{
|
|
79
81
|
name: 'update',
|
|
80
82
|
usage: 'update [--agent <name> | --dir <path>] [--installed-only] [--check [--scheduled] | --auto] [--only <a,b> | --domain <name>]',
|
|
81
|
-
what: '增量更新已装技能(补齐缺失 /
|
|
83
|
+
what: '增量更新已装技能(补齐缺失 / 升级;只升不降:本地更高保留,--force 才显式降级)',
|
|
82
84
|
when: '日常维护、只读检查更新、后台周检、自动跟进时',
|
|
83
85
|
options: ['--agent', '--dir', '--installed-only', '--check', '--scheduled', '--auto', '--only', '--domain', '--dry-run', '--skip-scan'],
|
|
84
86
|
},
|
|
@@ -179,8 +181,14 @@ const CLI_HELP_MODEL = [
|
|
|
179
181
|
{ name: 'hosts remove', usage: 'hub hosts remove <dir | --id <agentId>> [--purge [--yes]] [--json]', what: '移除注册(绝不删目录);--purge = 清理残留目录(默认预览,--yes 执行)', when: '注销自定义宿主、清理残留目录时', options: ['--id', '--purge', '--yes', '--json'] },
|
|
180
182
|
{ name: 'hosts list', usage: 'hub hosts list [--json]', what: '查看自定义宿主注册表(hosts.json)', when: '核对已注册的自定义目录时', options: ['--json'] },
|
|
181
183
|
{ name: 'hosts mark', usage: 'hub hosts mark <dir> --state <available|orphan|ignored> [--json]', what: '手动标记宿主状态(可用 / 残留 / 忽略)', when: '实体证据不足需要人工确认时', options: ['--state', '--json'] },
|
|
184
|
+
{ name: 'hosts exclude', usage: 'hub hosts exclude <agentId|dir> [--json]', what: '加入「不接管」名单(发现 / 显示 / 链接三层跳过)', when: '某宿主 / 目录不归元阁管理时', options: ['--json'] },
|
|
185
|
+
{ name: 'hosts include', usage: 'hub hosts include <agentId|dir> [--json]', what: '恢复接管(移出「不接管」名单)', when: '重新纳入发现 / 链接时', options: ['--json'] },
|
|
186
|
+
{ name: 'hosts set', usage: 'hub hosts set <agentId> --dir <dir> [--label <名称>] [--json] | hub hosts set <agentId> --clear', what: '目录覆盖:用户级覆盖内置映射;--clear 恢复默认', when: '宿主技能目录与内置映射不一致时', options: ['--dir', '--label', '--clear', '--json'] },
|
|
187
|
+
{ name: 'config get', usage: 'hub config get [--json]', what: '查看生效 Hub 位置与来源(--hub / YOTTA_SKILLS_HUB / config.json / 默认)', when: '确认当前 Hub 与配置覆盖时', options: ['--json'] },
|
|
188
|
+
{ name: 'config set', usage: 'hub config set --hub <path> [--move]', what: '持久化 Hub 位置;--move 迁移旧 Hub(复制校验 → 重链 → 回收站 7 天)', when: '换 Hub 存储位置时', options: ['--hub', '--move', '--json'] },
|
|
189
|
+
{ name: 'config clear', usage: 'hub config clear [--json]', what: '清除配置覆盖,恢复 flag > env > 默认', when: '撤销持久 Hub 位置时', options: ['--json'] },
|
|
182
190
|
{ name: 'install', usage: 'hub install [skill...]', what: '把元技能装进 Hub 真源(清单 27 + 特殊家族 5)', when: '初始化 Hub 或补装技能时', options: ['--hub'] },
|
|
183
|
-
{ name: 'update', usage: 'hub update', what: '更新 Hub
|
|
191
|
+
{ name: 'update', usage: 'hub update', what: '更新 Hub 内的元技能(非元技能不参与;只升不降)', when: '日常维护 Hub 真源时', options: ['--hub'] },
|
|
184
192
|
{ name: 'adopt', usage: 'hub adopt [--scan | --apply] [--include <a,b>] [--in-place]', what: '把各宿主已有技能收编进 Hub(默认复制保真)', when: '接管机器上已装的技能时', options: ['--scan', '--apply', '--include', '--in-place', '--force', '--skip-scan', '--allow-unverified'] },
|
|
185
193
|
{ name: 'refresh', usage: 'hub refresh <slug> --from <path>', what: '手动同步非元阁技能(无统一更新源)', when: '外部技能更新后刷新 Hub 副本时', options: ['--from'] },
|
|
186
194
|
{ name: 'link', usage: 'hub link [skill...] [--all | --agent <id> | --dir <path>] [--include-discovered] [--dry-run]', what: '把 Hub 技能链接到宿主目录(指定智能体 / 全量)', when: '分发技能到各智能体时', options: ['--all', '--agent', '--dir', '--include-discovered', '--force', '--dry-run'] },
|
|
@@ -293,6 +301,12 @@ const CLI_HELP_QUICK = [
|
|
|
293
301
|
{ cmd: 'hub hosts add', example: 'yotta-skills hub hosts add <技能目录>' },
|
|
294
302
|
{ cmd: 'hub hosts list', example: 'yotta-skills hub hosts list' },
|
|
295
303
|
{ cmd: 'hub hosts mark', example: 'yotta-skills hub hosts mark <技能目录> --state orphan' },
|
|
304
|
+
{ cmd: 'hub hosts exclude', example: 'yotta-skills hub hosts exclude <agentId|目录>' },
|
|
305
|
+
{ cmd: 'hub hosts include', example: 'yotta-skills hub hosts include <agentId|目录>' },
|
|
306
|
+
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>' },
|
|
307
|
+
{ cmd: 'hub config get', example: 'yotta-skills hub config get', note: '查看生效位置与来源' },
|
|
308
|
+
{ cmd: 'hub config set', example: 'yotta-skills hub config set --hub <Hub目录>', note: '持久化 Hub 位置;迁移旧 Hub 加 --move' },
|
|
309
|
+
{ cmd: 'hub config clear', example: 'yotta-skills hub config clear', note: '清除持久覆盖,恢复默认解析' },
|
|
296
310
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录>', note: '只移除注册,不删目录' },
|
|
297
311
|
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录> --purge', danger: true, note: '清理残留目录:先预览,--yes 才执行;目录入回收站保留 7 天' },
|
|
298
312
|
{ cmd: 'hub install', example: 'yotta-skills hub install' },
|
|
@@ -42,10 +42,19 @@ function readHostsRegistry(opts) {
|
|
|
42
42
|
return {
|
|
43
43
|
version: HOSTS_REGISTRY_VERSION,
|
|
44
44
|
hosts: value.hosts.filter((item) => item && typeof item.dir === 'string' && item.dir),
|
|
45
|
+
// 0.29.1 U2:不接管名单 + 目录覆盖(可选字段,向后兼容旧文件)。
|
|
46
|
+
excluded: Array.isArray(value.excluded)
|
|
47
|
+
? value.excluded.filter((item) => item && typeof item.value === 'string' && item.value &&
|
|
48
|
+
(item.kind === 'agent' || item.kind === 'dir'))
|
|
49
|
+
: [],
|
|
50
|
+
overrides: Array.isArray(value.overrides)
|
|
51
|
+
? value.overrides.filter((item) => item && typeof item.agentId === 'string' && item.agentId &&
|
|
52
|
+
typeof item.dir === 'string' && item.dir)
|
|
53
|
+
: [],
|
|
45
54
|
};
|
|
46
55
|
}
|
|
47
56
|
} catch (_) { /* missing or corrupt registry falls back to empty */ }
|
|
48
|
-
return { version: HOSTS_REGISTRY_VERSION, hosts: [] };
|
|
57
|
+
return { version: HOSTS_REGISTRY_VERSION, hosts: [], excluded: [], overrides: [] };
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
function writeHostsRegistry(opts, registry) {
|
|
@@ -55,6 +64,8 @@ function writeHostsRegistry(opts, registry) {
|
|
|
55
64
|
fs.writeFileSync(tmp, JSON.stringify({
|
|
56
65
|
version: HOSTS_REGISTRY_VERSION,
|
|
57
66
|
hosts: registry.hosts,
|
|
67
|
+
excluded: registry.excluded || [],
|
|
68
|
+
overrides: registry.overrides || [],
|
|
58
69
|
}, null, 2) + '\n', 'utf8');
|
|
59
70
|
fs.renameSync(tmp, file);
|
|
60
71
|
return file;
|
|
@@ -84,6 +95,14 @@ function isInside(root, target) {
|
|
|
84
95
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
85
96
|
}
|
|
86
97
|
|
|
98
|
+
/** YottaCode 目标判定:.yottacode 路径或 YOTTACODE_HOME 子树(0.29.1 U2)。 */
|
|
99
|
+
function isYottaCodeTarget(dir, opts) {
|
|
100
|
+
if (agentDirsLib.isYottaCodeDir(dir)) return true;
|
|
101
|
+
const env = (opts && opts.env) || process.env;
|
|
102
|
+
const ycHome = env.YOTTACODE_HOME ? path.resolve(env.YOTTACODE_HOME) : null;
|
|
103
|
+
return Boolean(ycHome && dir && isInside(ycHome, dir));
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/** fail-closed 校验:返回中文错误或 null。 */
|
|
88
107
|
function validateHostDir(dir, opts) {
|
|
89
108
|
const target = path.resolve(dir);
|
|
@@ -101,6 +120,9 @@ function validateHostDir(dir, opts) {
|
|
|
101
120
|
if (agentDirsLib.isBridgeOnlyDir(target, opts)) {
|
|
102
121
|
return '锁 / 数据桥接目录不允许注册:' + target;
|
|
103
122
|
}
|
|
123
|
+
if (isYottaCodeTarget(target, opts)) {
|
|
124
|
+
return 'YottaCode 自带三层技能管理(skill-inventory / skill / user-skills),不纳入元阁接管:' + target;
|
|
125
|
+
}
|
|
104
126
|
return null;
|
|
105
127
|
}
|
|
106
128
|
|
|
@@ -165,6 +187,9 @@ function markHost(opts, input) {
|
|
|
165
187
|
}
|
|
166
188
|
const dir = input && input.dir ? path.resolve(input.dir) : null;
|
|
167
189
|
if (!dir) return { ok: false, error: '缺少目录' };
|
|
190
|
+
if (isYottaCodeTarget(dir, options)) {
|
|
191
|
+
return { ok: false, error: 'YottaCode 自带三层技能管理,不纳入元阁接管,不能标记:' + dir };
|
|
192
|
+
}
|
|
168
193
|
const registry = readHostsRegistry(options);
|
|
169
194
|
let entry = registry.hosts.find((item) => samePath(item.dir, dir));
|
|
170
195
|
if (!entry) {
|
|
@@ -181,6 +206,118 @@ function markHost(opts, input) {
|
|
|
181
206
|
return { ok: true, entry, registry };
|
|
182
207
|
}
|
|
183
208
|
|
|
209
|
+
/** 0.29.1 U2:不接管名单匹配(agentId 或目录);返回命中条目或 null。 */
|
|
210
|
+
function excludedEntryFor(registry, input) {
|
|
211
|
+
const agentId = input && input.agentId ? String(input.agentId).toLowerCase() : null;
|
|
212
|
+
const dir = input && input.dir ? path.resolve(input.dir) : null;
|
|
213
|
+
for (const item of (registry && registry.excluded) || []) {
|
|
214
|
+
if (!item) continue;
|
|
215
|
+
if (item.kind === 'agent' && agentId && String(item.value).toLowerCase() === agentId) return item;
|
|
216
|
+
if (item.kind === 'dir' && dir && samePath(item.value, dir)) return item;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** 目标归类:已收录 agentId -> agent;否则按目录。 */
|
|
222
|
+
function classifyExcludeTarget(target) {
|
|
223
|
+
const value = String(target || '').trim();
|
|
224
|
+
const agentId = value.toLowerCase();
|
|
225
|
+
if (agentDirsLib.AGENT_DIRS[agentId]) return { kind: 'agent', value: agentId };
|
|
226
|
+
return { kind: 'dir', value: path.resolve(value) };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 加入「不接管」名单(0.29.1 U2):发现 / 显示 / 链接三层全部跳过,
|
|
231
|
+
* `--include-discovered` 也不纳入;幂等。
|
|
232
|
+
*/
|
|
233
|
+
function excludeHost(opts, input) {
|
|
234
|
+
const options = opts || {};
|
|
235
|
+
const target = input && input.target ? String(input.target).trim() : '';
|
|
236
|
+
if (!target) return { ok: false, error: '缺少目标:hub hosts exclude <agentId|目录>' };
|
|
237
|
+
const { kind, value } = classifyExcludeTarget(target);
|
|
238
|
+
if (kind === 'dir') {
|
|
239
|
+
if (isYottaCodeTarget(value, options)) {
|
|
240
|
+
return { ok: false, error: 'YottaCode 自带三层技能管理,不纳入元阁接管,无需加入不接管名单:' + value };
|
|
241
|
+
}
|
|
242
|
+
if (agentDirsLib.isBridgeOnlyDir(value, options)) {
|
|
243
|
+
return { ok: false, error: '锁 / 数据桥接目录本就不参与链接,无需加入不接管名单:' + value };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const registry = readHostsRegistry(options);
|
|
247
|
+
const existing = excludedEntryFor(registry, { agentId: kind === 'agent' ? value : null, dir: kind === 'dir' ? value : null });
|
|
248
|
+
if (existing) return { ok: true, entry: existing, registry, already: true };
|
|
249
|
+
const label = input && input.label
|
|
250
|
+
? String(input.label).trim()
|
|
251
|
+
: kind === 'agent'
|
|
252
|
+
? ((agentDirsLib.AGENT_DIRS[value] || {}).label || value)
|
|
253
|
+
: path.basename(value);
|
|
254
|
+
const entry = { kind, value, label, addedAt: nowIso() };
|
|
255
|
+
registry.excluded = (registry.excluded || []).concat([entry]);
|
|
256
|
+
writeHostsRegistry(options, registry);
|
|
257
|
+
return { ok: true, entry, registry };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** 移出「不接管」名单(恢复接管)。 */
|
|
261
|
+
function includeHost(opts, input) {
|
|
262
|
+
const options = opts || {};
|
|
263
|
+
const target = input && input.target ? String(input.target).trim() : '';
|
|
264
|
+
if (!target) return { ok: false, error: '缺少目标:hub hosts include <agentId|目录>' };
|
|
265
|
+
const { kind, value } = classifyExcludeTarget(target);
|
|
266
|
+
const registry = readHostsRegistry(options);
|
|
267
|
+
const index = (registry.excluded || []).findIndex((item) => item.kind === kind &&
|
|
268
|
+
(kind === 'agent' ? String(item.value).toLowerCase() === value : samePath(item.value, value)));
|
|
269
|
+
if (index < 0) return { ok: false, error: '未在不接管名单:' + target };
|
|
270
|
+
const removed = registry.excluded.splice(index, 1)[0];
|
|
271
|
+
writeHostsRegistry(options, registry);
|
|
272
|
+
return { ok: true, removed, registry };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 目录覆盖(0.29.1 U2):用户级覆盖已收录 agentId 的内置映射目录;
|
|
277
|
+
* 被覆盖的旧目录不再进入默认发现 / 链接。
|
|
278
|
+
*/
|
|
279
|
+
function setHostOverride(opts, input) {
|
|
280
|
+
const options = opts || {};
|
|
281
|
+
const agentId = input && input.agentId ? String(input.agentId).trim().toLowerCase() : '';
|
|
282
|
+
if (!agentId) return { ok: false, error: '缺少 agentId:hub hosts set <agentId> --dir <目录>(恢复默认用 --clear)' };
|
|
283
|
+
const info = agentDirsLib.AGENT_DIRS[agentId];
|
|
284
|
+
if (!info) return { ok: false, error: '未收录智能体: ' + agentId };
|
|
285
|
+
if (info.neverLink) return { ok: false, error: 'YottaCode 不纳入元阁接管,不能设置目录覆盖。' };
|
|
286
|
+
const dir = input && input.dir ? path.resolve(input.dir) : null;
|
|
287
|
+
if (!dir) return { ok: false, error: '缺少目录:hub hosts set <agentId> --dir <目录>(恢复默认用 --clear)' };
|
|
288
|
+
const guard = validateHostDir(dir, options);
|
|
289
|
+
if (guard) return { ok: false, error: guard };
|
|
290
|
+
const label = input && input.label ? String(input.label).trim() : info.label;
|
|
291
|
+
const labelError = validateLabel(label);
|
|
292
|
+
if (labelError) return { ok: false, error: labelError };
|
|
293
|
+
const registry = readHostsRegistry(options);
|
|
294
|
+
const entry = {
|
|
295
|
+
agentId,
|
|
296
|
+
dir,
|
|
297
|
+
label,
|
|
298
|
+
previousDirs: (info.dirs || []).map((rel) => agentDirsLib.resolveUserDir(rel, options)),
|
|
299
|
+
addedAt: nowIso(),
|
|
300
|
+
};
|
|
301
|
+
const index = (registry.overrides || []).findIndex((item) => String(item.agentId).toLowerCase() === agentId);
|
|
302
|
+
if (index >= 0) registry.overrides[index] = entry;
|
|
303
|
+
else registry.overrides = (registry.overrides || []).concat([entry]);
|
|
304
|
+
writeHostsRegistry(options, registry);
|
|
305
|
+
return { ok: true, entry, registry };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** 清除目录覆盖(恢复内置映射)。 */
|
|
309
|
+
function clearHostOverride(opts, input) {
|
|
310
|
+
const options = opts || {};
|
|
311
|
+
const agentId = input && input.agentId ? String(input.agentId).trim().toLowerCase() : '';
|
|
312
|
+
if (!agentId) return { ok: false, error: '缺少 agentId:hub hosts set <agentId> --clear' };
|
|
313
|
+
const registry = readHostsRegistry(options);
|
|
314
|
+
const index = (registry.overrides || []).findIndex((item) => String(item.agentId).toLowerCase() === agentId);
|
|
315
|
+
if (index < 0) return { ok: false, error: '未找到目录覆盖:' + agentId };
|
|
316
|
+
const removed = registry.overrides.splice(index, 1)[0];
|
|
317
|
+
writeHostsRegistry(options, registry);
|
|
318
|
+
return { ok: true, removed, registry };
|
|
319
|
+
}
|
|
320
|
+
|
|
184
321
|
module.exports = {
|
|
185
322
|
HOSTS_REGISTRY_VERSION,
|
|
186
323
|
MANUAL_STATES,
|
|
@@ -194,4 +331,9 @@ module.exports = {
|
|
|
194
331
|
addHost,
|
|
195
332
|
removeHost,
|
|
196
333
|
markHost,
|
|
334
|
+
excludedEntryFor,
|
|
335
|
+
excludeHost,
|
|
336
|
+
includeHost,
|
|
337
|
+
setHostOverride,
|
|
338
|
+
clearHostOverride,
|
|
197
339
|
};
|
|
@@ -269,7 +269,7 @@ function refreshFrom(options) {
|
|
|
269
269
|
updatedAt: new Date().toISOString(),
|
|
270
270
|
});
|
|
271
271
|
hubLib.appendAudit(hubDir, { event: 'refresh', slug, source, verdict: scan.verdict, backup });
|
|
272
|
-
return { ok: true, slug, source, backup, verdict: scan.verdict };
|
|
272
|
+
return { ok: true, slug, source, backup, verdict: scan.verdict, scanPolicy: scan.policy || null };
|
|
273
273
|
} catch (error) {
|
|
274
274
|
if (tmp) fs.rmSync(tmp, { recursive: true, force: true });
|
|
275
275
|
return { ok: false, slug, error: error.message };
|