@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.
- 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 +64 -4
- package/skills/yotta-skills/assets/view.html +188 -14
- package/skills/yotta-skills/bin/yotta-skills.js +712 -86
- package/skills/yotta-skills/lib/agent-dirs.js +17 -3
- package/skills/yotta-skills/lib/agent-discovery.js +203 -13
- package/skills/yotta-skills/lib/cli-help.js +40 -3
- package/skills/yotta-skills/lib/host-purge.js +167 -0
- package/skills/yotta-skills/lib/hosts-registry.js +339 -0
- package/skills/yotta-skills/lib/hub-view-server.js +177 -1
- package/skills/yotta-skills/lib/hub.js +16 -0
- package/skills/yotta-skills/lib/install-pipeline.js +58 -2
- package/skills/yotta-skills/lib/self-install.js +224 -0
- package/skills/yotta-skills/lib/skills-scan.js +25 -0
- package/skills/yotta-skills/references/hub.md +66 -0
- package/skills/yotta-skills/references/install-flow.md +5 -2
- package/skills/yotta-skills/references/skill-list.md +1 -1
- 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 +1 -1
|
@@ -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
|
};
|
|
@@ -15,6 +15,7 @@ const fs = require('fs');
|
|
|
15
15
|
const os = require('os');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const agentDirs = require('./agent-dirs');
|
|
18
|
+
const hostsRegistry = require('./hosts-registry');
|
|
18
19
|
|
|
19
20
|
const SKILL_FILE = 'SKILL.md';
|
|
20
21
|
const SKIP_DIRS = new Set([
|
|
@@ -81,6 +82,7 @@ function isNoisePath(dir, baseDir) {
|
|
|
81
82
|
if (part === 'temp' || part === 'tmp') return true;
|
|
82
83
|
if (/\.bak$/.test(part)) return true;
|
|
83
84
|
if (part === 'plugin-sources' || part === 'plugins' || part === 'node_modules') return true;
|
|
85
|
+
if (part === '.yottacode') return true;
|
|
84
86
|
if (part === 'candidate' || part.startsWith('candidate-') || part.includes('staging')) return true;
|
|
85
87
|
if (part === 'memories' || part === 'connectors' || part === 'workspace') return true;
|
|
86
88
|
}
|
|
@@ -103,6 +105,15 @@ function addRoot(list, seen, dir, meta) {
|
|
|
103
105
|
if (meta && meta.label && existing.label === '自动发现目录') existing.label = meta.label;
|
|
104
106
|
if (meta && meta.verified) existing.verified = true;
|
|
105
107
|
if (meta && meta.bridgeOnly) existing.bridgeOnly = true;
|
|
108
|
+
// 用户注册优先:覆盖分类与手动标记(0.29.0 F2/F3)。
|
|
109
|
+
if (meta && meta.registry) {
|
|
110
|
+
existing.registry = true;
|
|
111
|
+
existing.verified = true;
|
|
112
|
+
existing.detection = 'user-registry';
|
|
113
|
+
if (meta.label) existing.label = meta.label;
|
|
114
|
+
if (meta.agentId) existing.agentId = meta.agentId;
|
|
115
|
+
if (meta.manualState) existing.manualState = meta.manualState;
|
|
116
|
+
}
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
108
119
|
const record = {
|
|
@@ -115,6 +126,8 @@ function addRoot(list, seen, dir, meta) {
|
|
|
115
126
|
bridgeOnly: Boolean(meta && meta.bridgeOnly),
|
|
116
127
|
detection: (meta && meta.detection) || 'mapping',
|
|
117
128
|
exists: isDirectory(resolved),
|
|
129
|
+
registry: Boolean(meta && meta.registry),
|
|
130
|
+
manualState: (meta && meta.manualState) || null,
|
|
118
131
|
};
|
|
119
132
|
record.skillCount = record.exists ? countSkills(resolved) : 0;
|
|
120
133
|
record.status = record.exists ? 'skills-dir' : 'missing';
|
|
@@ -180,7 +193,7 @@ function commonRoots(options) {
|
|
|
180
193
|
add(env.OPENCLAW_STATE_DIR, 2);
|
|
181
194
|
add(env.CLAUDE_CONFIG_DIR, 2);
|
|
182
195
|
add(env.CODEX_HOME, 2);
|
|
183
|
-
|
|
196
|
+
// YOTTACODE_HOME 不扫描(YottaCode 自带三层技能管理,0.29.1 U2)。
|
|
184
197
|
|
|
185
198
|
// A portable agent install often lives beside the current workspace. The
|
|
186
199
|
// scanner only follows agent-looking branches, so this stays bounded.
|
|
@@ -223,23 +236,87 @@ function installedMarkers(options) {
|
|
|
223
236
|
env['ProgramFiles(x86)'],
|
|
224
237
|
];
|
|
225
238
|
for (const root of roots) collectMarkerNames(root, 2, names, 800);
|
|
226
|
-
|
|
239
|
+
// 标记过滤:已收录宿主的 label 双向匹配 + 启发式关键词;排除卸载器 / 运行时噪音。
|
|
240
|
+
return [...names].filter((name) =>
|
|
241
|
+
(knownLabelMatches(name) || AGENT_HINT.test(name)) && !INSTALLED_NOISE.test(name));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeLabel(value) {
|
|
245
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
227
246
|
}
|
|
228
247
|
|
|
248
|
+
/** 标记名与已收录宿主 label 的双向词边界包含("Trae" ↔ "Trae Code CLI")。 */
|
|
229
249
|
function knownLabelMatches(name) {
|
|
230
|
-
const normalized =
|
|
250
|
+
const normalized = normalizeLabel(name);
|
|
251
|
+
if (!normalized) return false;
|
|
231
252
|
for (const info of Object.values(agentDirs.AGENT_DIRS)) {
|
|
232
|
-
|
|
253
|
+
const label = normalizeLabel(info.label);
|
|
254
|
+
if (!label) continue;
|
|
255
|
+
if (normalized === label) return true;
|
|
256
|
+
if (normalized.startsWith(label + ' ') || normalized.endsWith(' ' + label) || normalized.includes(' ' + label + ' ')) return true;
|
|
257
|
+
if (label.startsWith(normalized + ' ') || label.endsWith(' ' + normalized) || label.includes(' ' + normalized + ' ')) return true;
|
|
233
258
|
}
|
|
234
259
|
return false;
|
|
235
260
|
}
|
|
236
261
|
|
|
262
|
+
/**
|
|
263
|
+
* 应用标记与宿主匹配:label / agentId 与标记名按词边界双向包含。
|
|
264
|
+
* 例:"Claude" 匹配 "Claude Code";"Trae" 匹配 "Trae Code CLI";
|
|
265
|
+
* 避免 "Pi" 误配 "Pilot" 这类裸子串。
|
|
266
|
+
*/
|
|
267
|
+
function markerMatchesHost(markerName, host) {
|
|
268
|
+
const marker = normalizeLabel(markerName);
|
|
269
|
+
if (!marker) return false;
|
|
270
|
+
const candidates = [host && host.label, host && host.agentId].filter(Boolean).map(normalizeLabel);
|
|
271
|
+
for (const candidate of candidates) {
|
|
272
|
+
if (!candidate) continue;
|
|
273
|
+
if (marker === candidate) return true;
|
|
274
|
+
if (marker.startsWith(candidate + ' ') || marker.endsWith(' ' + candidate) || marker.includes(' ' + candidate + ' ')) return true;
|
|
275
|
+
if (candidate.startsWith(marker + ' ') || candidate.endsWith(' ' + marker) || candidate.includes(' ' + marker + ' ')) return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** F3 状态判定:目录 × 实体证据(应用标记 / 用户注册 / 手动标记)。 */
|
|
281
|
+
function judgeHostState(host) {
|
|
282
|
+
if (host.manualState) return host.manualState; // 手动覆盖(available / orphan / ignored)
|
|
283
|
+
const entity = Boolean(host.markerEvidence || (host.evidence && host.evidence.registry));
|
|
284
|
+
if (host.exists && entity) return 'available';
|
|
285
|
+
if (host.exists) return 'orphan';
|
|
286
|
+
if (entity) return 'marker-only';
|
|
287
|
+
return 'missing';
|
|
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
|
+
|
|
237
312
|
/**
|
|
238
313
|
* Discover hosts and their skill directories.
|
|
239
314
|
*
|
|
240
315
|
* Returns:
|
|
241
316
|
* hosts - all candidate skill roots (existing and missing known mappings)
|
|
242
|
-
* installed -
|
|
317
|
+
* installed - entity evidence (app markers + user registry), including hosts
|
|
318
|
+
* without a skills dir; plain directory existence is NOT an
|
|
319
|
+
* installed marker (0.29.0 F3)
|
|
243
320
|
* errors - non-fatal discovery errors
|
|
244
321
|
*/
|
|
245
322
|
function discoverHosts(options) {
|
|
@@ -247,17 +324,74 @@ function discoverHosts(options) {
|
|
|
247
324
|
const seen = new Map();
|
|
248
325
|
const errors = [];
|
|
249
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
|
+
|
|
250
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;
|
|
251
367
|
addRoot(roots, seen, root.dir, { ...root, detection: 'mapping' });
|
|
252
368
|
}
|
|
253
369
|
for (const root of agentDirs.envRoots(options)) {
|
|
370
|
+
if (skipDir(root.dir, root.agentId)) continue;
|
|
254
371
|
addRoot(roots, seen, root.dir, { ...root, detection: root.env ? 'env' : 'workspace' });
|
|
255
372
|
}
|
|
373
|
+
// 合并顺序:内置映射 → 环境变量 → 用户注册表(F2)→ 自动发现;
|
|
374
|
+
// 同 realpath 用户注册优先(分类 = 已核实,detection = user-registry)。
|
|
375
|
+
for (const item of registry.hosts || []) {
|
|
376
|
+
if (skipDir(item.dir, item.agentId)) continue;
|
|
377
|
+
addRoot(roots, seen, item.dir, {
|
|
378
|
+
detection: 'user-registry',
|
|
379
|
+
label: item.label || '自定义宿主',
|
|
380
|
+
agentId: item.agentId || null,
|
|
381
|
+
verified: true,
|
|
382
|
+
known: false,
|
|
383
|
+
registry: true,
|
|
384
|
+
manualState: item.manualState || null,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
256
387
|
for (const root of commonRoots(options)) {
|
|
257
388
|
for (const found of scanForSkillDirs(root.dir, {
|
|
258
389
|
maxDepth: root.maxDepth,
|
|
259
390
|
baseDir: options.homeDir || os.homedir(),
|
|
260
391
|
})) {
|
|
392
|
+
if (skipDir(found.dir, null)) continue;
|
|
393
|
+
// 被目录覆盖替换掉的旧映射目录不再进入默认发现 / 链接。
|
|
394
|
+
if (previousDirKeys.has(normalizePath(found.dir).toLowerCase())) continue;
|
|
261
395
|
addRoot(roots, seen, found.dir, {
|
|
262
396
|
detection: 'discovered',
|
|
263
397
|
label: '自动发现:' + path.basename(found.parent),
|
|
@@ -265,6 +399,22 @@ function discoverHosts(options) {
|
|
|
265
399
|
}
|
|
266
400
|
}
|
|
267
401
|
|
|
402
|
+
// F3:目录证据与实体证据分开记录,判定生命周期状态。
|
|
403
|
+
const markers = installedMarkers(options);
|
|
404
|
+
for (const host of roots) {
|
|
405
|
+
const markerEvidence = markers.some((name) => markerMatchesHost(name, host));
|
|
406
|
+
host.markerEvidence = markerEvidence;
|
|
407
|
+
host.dirEvidence = host.exists;
|
|
408
|
+
host.evidence = {
|
|
409
|
+
dir: host.exists,
|
|
410
|
+
marker: markerEvidence,
|
|
411
|
+
registry: Boolean(host.registry),
|
|
412
|
+
manual: Boolean(host.manualState),
|
|
413
|
+
};
|
|
414
|
+
host.stateSource = host.manualState ? 'manual' : 'auto';
|
|
415
|
+
host.state = judgeHostState(host);
|
|
416
|
+
}
|
|
417
|
+
|
|
268
418
|
const installed = [];
|
|
269
419
|
const installedSeen = new Set();
|
|
270
420
|
const addInstalled = (name, source) => {
|
|
@@ -275,20 +425,44 @@ function discoverHosts(options) {
|
|
|
275
425
|
installedSeen.add(key);
|
|
276
426
|
installed.push({ label, source, hasSkillsDir: false });
|
|
277
427
|
};
|
|
278
|
-
|
|
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
|
+
};
|
|
440
|
+
for (const name of markers) {
|
|
441
|
+
if (isMarkerSuppressed(name)) continue;
|
|
279
442
|
if (knownLabelMatches(name) || AGENT_HINT.test(name)) addInstalled(name, '应用标记');
|
|
280
443
|
}
|
|
444
|
+
for (const host of roots) {
|
|
445
|
+
if (host.detection === 'user-registry') addInstalled(host.label, '用户注册');
|
|
446
|
+
}
|
|
447
|
+
// 0.29.1 U2:配对确定性优选(先收集全部候选,再按优先级选定),消除 last-write-wins。
|
|
448
|
+
const pairCandidates = new Map();
|
|
281
449
|
for (const host of roots) {
|
|
282
450
|
if (!host.exists) continue;
|
|
283
|
-
|
|
284
|
-
const item
|
|
285
|
-
|
|
286
|
-
item.
|
|
287
|
-
item.
|
|
288
|
-
item.skillCount = host.skillCount;
|
|
289
|
-
item.agentId = host.agentId;
|
|
451
|
+
if (!host.markerEvidence && host.detection !== 'user-registry') continue;
|
|
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);
|
|
290
456
|
}
|
|
291
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
|
+
}
|
|
292
466
|
|
|
293
467
|
return {
|
|
294
468
|
generatedAt: new Date().toISOString(),
|
|
@@ -298,9 +472,25 @@ function discoverHosts(options) {
|
|
|
298
472
|
};
|
|
299
473
|
}
|
|
300
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
|
+
|
|
301
488
|
module.exports = {
|
|
302
489
|
AGENT_HINT,
|
|
303
490
|
discoverHosts,
|
|
304
491
|
scanForSkillDirs,
|
|
305
492
|
countSkills,
|
|
493
|
+
markerMatchesHost,
|
|
494
|
+
judgeHostState,
|
|
495
|
+
unpairedMarkers,
|
|
306
496
|
};
|
|
@@ -44,6 +44,11 @@ const CLI_GLOBAL_OPTIONS = [
|
|
|
44
44
|
{ flag: '--port', arg: '<n>', what: 'view 面板端口(默认 8789)', when: '端口冲突或想固定端口时', caution: '仅 127.0.0.1 监听' },
|
|
45
45
|
{ flag: '--all', arg: '', what: 'hub link / unlink 作用于全部已核实宿主', when: '全量分发或解除时', caution: '' },
|
|
46
46
|
{ flag: '--include-discovered', arg: '', what: 'hub link / unlink / remove 时纳入自动发现目录', when: '确认要扩围到未核实目录时', caution: '默认只链已核实宿主;桥接目录永不链接' },
|
|
47
|
+
{ flag: '--state', arg: '<state>', what: 'hub hosts 状态过滤(available / orphan / missing / marker-only / ignored);hub hosts mark 时为手动状态', when: '只看某类宿主、或手动标记宿主状态时', caution: '' },
|
|
48
|
+
{ flag: '--label', arg: '<名称>', what: 'hub hosts add 的显示名称(默认取目录名)', when: '注册自定义宿主时', caution: '最长 48 字符,禁控制字符' },
|
|
49
|
+
{ flag: '--id', arg: '<agentId>', what: 'hub hosts add / remove 的智能体标识', when: '按智能体注册或移除自定义宿主时', caution: '' },
|
|
50
|
+
{ flag: '--purge', arg: '', what: 'hub hosts remove 清理残留目录(默认预览;--yes 执行)', when: '清理实体未确认的残留技能目录时', caution: '只删指向 Hub 的链接;目录移入回收站保留 7 天' },
|
|
51
|
+
{ flag: '--clear', arg: '', what: 'hub hosts set 恢复内置映射(删除目录覆盖)', when: '想撤销目录覆盖时', caution: '' },
|
|
47
52
|
{ flag: '--scan', arg: '', what: 'hub adopt 只读预演(默认行为)', when: '先看收编候选与冲突时', caution: '' },
|
|
48
53
|
{ flag: '--apply', arg: '', what: 'hub adopt 执行收编', when: '确认候选后执行时', caution: '执行时逐项运行元信扫描;high / critical 默认阻断' },
|
|
49
54
|
{ flag: '--in-place', arg: '', what: 'hub adopt 原地登记,不复制', when: '想让 Hub 直接指向原目录时', caution: '原目录删除即断链;默认复制保真更安全' },
|
|
@@ -74,7 +79,7 @@ const CLI_HELP_MODEL = [
|
|
|
74
79
|
{
|
|
75
80
|
name: 'update',
|
|
76
81
|
usage: 'update [--agent <name> | --dir <path>] [--installed-only] [--check [--scheduled] | --auto] [--only <a,b> | --domain <name>]',
|
|
77
|
-
what: '增量更新已装技能(补齐缺失 /
|
|
82
|
+
what: '增量更新已装技能(补齐缺失 / 升级;只升不降:本地更高保留,--force 才显式降级)',
|
|
78
83
|
when: '日常维护、只读检查更新、后台周检、自动跟进时',
|
|
79
84
|
options: ['--agent', '--dir', '--installed-only', '--check', '--scheduled', '--auto', '--only', '--domain', '--dry-run', '--skip-scan'],
|
|
80
85
|
},
|
|
@@ -85,6 +90,20 @@ const CLI_HELP_MODEL = [
|
|
|
85
90
|
when: '执行前先确认影响面时',
|
|
86
91
|
options: ['--dry-run'],
|
|
87
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: 'install-self',
|
|
95
|
+
usage: 'install-self [--dir <path>] [--dry-run] [--force] [--json]',
|
|
96
|
+
what: '元阁独立安装:把管理引擎装到独立位置(默认 ~/.yottaskills/yotta-skills),与 Hub / 宿主分离',
|
|
97
|
+
when: '想把元阁装成独立 CLI(不依赖任何智能体)时',
|
|
98
|
+
options: ['--dir', '--dry-run', '--force', '--json'],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'where',
|
|
102
|
+
usage: 'where [--json]',
|
|
103
|
+
what: '查看元阁位置:运行中 CLI / 独立安装目录(标记当前运行项)/ Hub 真源',
|
|
104
|
+
when: '确认元阁装在哪、是否独立安装时',
|
|
105
|
+
options: ['--json', '--hub'],
|
|
106
|
+
},
|
|
88
107
|
],
|
|
89
108
|
},
|
|
90
109
|
{
|
|
@@ -156,9 +175,16 @@ const CLI_HELP_MODEL = [
|
|
|
156
175
|
when: '多智能体共享技能、统一管理技能目录时',
|
|
157
176
|
options: ['--hub', '--all', '--include-discovered', '--dry-run', '--json'],
|
|
158
177
|
subcommands: [
|
|
159
|
-
{ name: 'hosts', usage: 'hub hosts [--json]', what: '
|
|
178
|
+
{ name: 'hosts', usage: 'hub hosts [--json] [--state <state>]', what: '发现宿主与技能目录(可用 / 残留 / 未创建 / 仅标记 / 桥接)', when: '先看这台机器有哪些宿主目录时', options: ['--json', '--state'] },
|
|
179
|
+
{ name: 'hosts add', usage: 'hub hosts add <dir> [--label <名称>] [--id <agentId>] [--json]', what: '注册自定义宿主目录(纳入发现 / 分发 / 收编;不创建目录)', when: '宿主未被自动识别或识别错误时', options: ['--label', '--id', '--json'] },
|
|
180
|
+
{ name: 'hosts remove', usage: 'hub hosts remove <dir | --id <agentId>> [--purge [--yes]] [--json]', what: '移除注册(绝不删目录);--purge = 清理残留目录(默认预览,--yes 执行)', when: '注销自定义宿主、清理残留目录时', options: ['--id', '--purge', '--yes', '--json'] },
|
|
181
|
+
{ name: 'hosts list', usage: 'hub hosts list [--json]', what: '查看自定义宿主注册表(hosts.json)', when: '核对已注册的自定义目录时', options: ['--json'] },
|
|
182
|
+
{ name: 'hosts mark', usage: 'hub hosts mark <dir> --state <available|orphan|ignored> [--json]', what: '手动标记宿主状态(可用 / 残留 / 忽略)', when: '实体证据不足需要人工确认时', options: ['--state', '--json'] },
|
|
183
|
+
{ name: 'hosts exclude', usage: 'hub hosts exclude <agentId|dir> [--json]', what: '加入「不接管」名单(发现 / 显示 / 链接三层跳过)', when: '某宿主 / 目录不归元阁管理时', options: ['--json'] },
|
|
184
|
+
{ name: 'hosts include', usage: 'hub hosts include <agentId|dir> [--json]', what: '恢复接管(移出「不接管」名单)', when: '重新纳入发现 / 链接时', options: ['--json'] },
|
|
185
|
+
{ 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'] },
|
|
160
186
|
{ name: 'install', usage: 'hub install [skill...]', what: '把元技能装进 Hub 真源(清单 27 + 特殊家族 5)', when: '初始化 Hub 或补装技能时', options: ['--hub'] },
|
|
161
|
-
{ name: 'update', usage: 'hub update', what: '更新 Hub
|
|
187
|
+
{ name: 'update', usage: 'hub update', what: '更新 Hub 内的元技能(非元技能不参与;只升不降)', when: '日常维护 Hub 真源时', options: ['--hub'] },
|
|
162
188
|
{ 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'] },
|
|
163
189
|
{ name: 'refresh', usage: 'hub refresh <slug> --from <path>', what: '手动同步非元阁技能(无统一更新源)', when: '外部技能更新后刷新 Hub 副本时', options: ['--from'] },
|
|
164
190
|
{ 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'] },
|
|
@@ -240,6 +266,8 @@ const CLI_HELP_QUICK = [
|
|
|
240
266
|
{ cmd: 'update', example: 'yotta-skills update --check' },
|
|
241
267
|
{ cmd: 'update', example: 'yotta-skills update --auto' },
|
|
242
268
|
{ cmd: '--dry-run', example: 'yotta-skills install --dry-run --agent codex' },
|
|
269
|
+
{ cmd: 'install-self', example: 'yotta-skills install-self' },
|
|
270
|
+
{ cmd: 'where', example: 'yotta-skills where' },
|
|
243
271
|
],
|
|
244
272
|
},
|
|
245
273
|
{
|
|
@@ -265,6 +293,15 @@ const CLI_HELP_QUICK = [
|
|
|
265
293
|
items: [
|
|
266
294
|
{ cmd: 'hub', example: 'yotta-skills hub' },
|
|
267
295
|
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts' },
|
|
296
|
+
{ cmd: 'hub hosts', example: 'yotta-skills hub hosts --state orphan' },
|
|
297
|
+
{ cmd: 'hub hosts add', example: 'yotta-skills hub hosts add <技能目录>' },
|
|
298
|
+
{ cmd: 'hub hosts list', example: 'yotta-skills hub hosts list' },
|
|
299
|
+
{ cmd: 'hub hosts mark', example: 'yotta-skills hub hosts mark <技能目录> --state orphan' },
|
|
300
|
+
{ cmd: 'hub hosts exclude', example: 'yotta-skills hub hosts exclude <agentId|目录>' },
|
|
301
|
+
{ cmd: 'hub hosts include', example: 'yotta-skills hub hosts include <agentId|目录>' },
|
|
302
|
+
{ cmd: 'hub hosts set', example: 'yotta-skills hub hosts set <agentId> --dir <目录>' },
|
|
303
|
+
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录>', note: '只移除注册,不删目录' },
|
|
304
|
+
{ cmd: 'hub hosts remove', example: 'yotta-skills hub hosts remove <技能目录> --purge', danger: true, note: '清理残留目录:先预览,--yes 才执行;目录入回收站保留 7 天' },
|
|
268
305
|
{ cmd: 'hub install', example: 'yotta-skills hub install' },
|
|
269
306
|
{ cmd: 'hub update', example: 'yotta-skills hub update' },
|
|
270
307
|
{ cmd: 'hub adopt', example: 'yotta-skills hub adopt --scan' },
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* 残留目录清理核心(0.29.0 F3):CLI 与面板共用。
|
|
4
|
+
*
|
|
5
|
+
* 只处理 state=orphan 的宿主目录 —— 全量 unlink(只删指向 Hub 的链接,含死链)
|
|
6
|
+
* → 目录移入回收站(保留 7 天,跨卷走复制 + 校验 + 删源)→ 清注册 / 链接台账
|
|
7
|
+
* → 写审计。非 Hub 链接 / 非链接内容保留并报告。
|
|
8
|
+
*
|
|
9
|
+
* fail-closed:Hub 真源 / 子目录、锁与数据桥接目录、非残留状态(可用 / 仅标记 /
|
|
10
|
+
* 未创建 / 已忽略)一律拒绝;需要人工确认时先 hub hosts mark --state orphan。
|
|
11
|
+
*/
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const hubLib = require('./hub');
|
|
16
|
+
const agentDirsLib = require('./agent-dirs');
|
|
17
|
+
const agentDiscoveryLib = require('./agent-discovery');
|
|
18
|
+
const hostsRegistryLib = require('./hosts-registry');
|
|
19
|
+
|
|
20
|
+
function purgeStamp(now) {
|
|
21
|
+
const d = now instanceof Date ? now : new Date();
|
|
22
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
23
|
+
return d.getFullYear() + pad(d.getMonth() + 1) + pad(d.getDate()) + '-' +
|
|
24
|
+
pad(d.getHours()) + pad(d.getMinutes()) + pad(d.getSeconds()) + '-' + process.pid;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function safeOrphanName(value) {
|
|
28
|
+
const cleaned = String(value || '')
|
|
29
|
+
.replace(/[\\/:*?"<>|\s]+/g, '-')
|
|
30
|
+
.replace(/^-+|-+$/g, '');
|
|
31
|
+
return cleaned || 'host';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pathKey(dir) {
|
|
35
|
+
let real = path.resolve(dir);
|
|
36
|
+
try {
|
|
37
|
+
real = fs.realpathSync.native(real);
|
|
38
|
+
} catch (_) {
|
|
39
|
+
try { real = fs.realpathSync(real); } catch (_) { /* keep resolved */ }
|
|
40
|
+
}
|
|
41
|
+
return process.platform === 'win32' ? real.toLowerCase() : real;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isInside(root, target) {
|
|
45
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
46
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 只读预检 + 清理计划(不写任何文件)。 */
|
|
50
|
+
function planHostPurge(options) {
|
|
51
|
+
const opts = options || {};
|
|
52
|
+
const hubDir = path.resolve(opts.hubDir);
|
|
53
|
+
const target = path.resolve(opts.dir);
|
|
54
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
55
|
+
const env = opts.env || process.env;
|
|
56
|
+
|
|
57
|
+
if (!fs.existsSync(target)) {
|
|
58
|
+
return { ok: false, code: 400, error: '目录不存在: ' + target };
|
|
59
|
+
}
|
|
60
|
+
if (isInside(hubDir, target)) {
|
|
61
|
+
return { ok: false, code: 400, error: '该目录不允许残留清理(Hub 真源 / 子目录): ' + target };
|
|
62
|
+
}
|
|
63
|
+
if (agentDirsLib.isBridgeOnlyDir(target, { homeDir, env })) {
|
|
64
|
+
return { ok: false, code: 400, error: '锁 / 数据桥接目录不参与残留清理: ' + target };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const discovery = opts.discovery || agentDiscoveryLib.discoverHosts({ homeDir, env });
|
|
68
|
+
const host = discovery.hosts.find((item) => pathKey(item.dir) === pathKey(target));
|
|
69
|
+
const state = host ? host.state : null;
|
|
70
|
+
if (state !== 'orphan') {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
code: 400,
|
|
74
|
+
state,
|
|
75
|
+
error: '目标不是「残留(实体未确认)」状态' + (state ? '(当前: ' + state + ')' : '(不在发现列表)'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const linkSlugs = [];
|
|
80
|
+
let keptLinks = 0;
|
|
81
|
+
let keptEntries = 0;
|
|
82
|
+
let entries = [];
|
|
83
|
+
try { entries = fs.readdirSync(target, { withFileTypes: true }); } catch (_) { entries = []; }
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const full = path.join(target, entry.name);
|
|
86
|
+
const cls = hubLib.classifyTarget(full, hubDir);
|
|
87
|
+
if (cls.kind === 'link') {
|
|
88
|
+
if (cls.inHub) linkSlugs.push(entry.name);
|
|
89
|
+
else keptLinks++;
|
|
90
|
+
} else if (cls.kind !== 'missing') {
|
|
91
|
+
keptEntries++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const trashRoot = hubLib.resolveTrashRoot(hubDir, opts);
|
|
95
|
+
const label = host && host.label ? host.label : path.basename(target);
|
|
96
|
+
const trashPath = path.join(trashRoot, purgeStamp(opts.now), 'orphan-' + safeOrphanName(label));
|
|
97
|
+
return {
|
|
98
|
+
ok: true,
|
|
99
|
+
hubDir,
|
|
100
|
+
target,
|
|
101
|
+
state,
|
|
102
|
+
label,
|
|
103
|
+
linkSlugs,
|
|
104
|
+
keptLinks,
|
|
105
|
+
keptEntries,
|
|
106
|
+
trashRoot,
|
|
107
|
+
trashPath,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 执行清理:unlink → trash → 清注册 / 台账 → 审计。 */
|
|
112
|
+
function applyHostPurge(options) {
|
|
113
|
+
const opts = options || {};
|
|
114
|
+
const plan = planHostPurge(opts);
|
|
115
|
+
if (!plan.ok) return plan;
|
|
116
|
+
|
|
117
|
+
const unlinkResult = hubLib.unlinkSkills({
|
|
118
|
+
hubDir: plan.hubDir,
|
|
119
|
+
targetDir: plan.target,
|
|
120
|
+
slugs: plan.linkSlugs,
|
|
121
|
+
dryRun: false,
|
|
122
|
+
});
|
|
123
|
+
const unlinkedCount = (unlinkResult.results || [])
|
|
124
|
+
.filter((item) => item.status === 'unlinked').length;
|
|
125
|
+
|
|
126
|
+
fs.mkdirSync(path.dirname(plan.trashPath), { recursive: true });
|
|
127
|
+
hubLib.moveEntryAcrossDevices(plan.target, plan.trashPath);
|
|
128
|
+
try {
|
|
129
|
+
hubLib.pruneTrash(plan.trashRoot, { days: hubLib.TRASH_RETENTION_DAYS });
|
|
130
|
+
} catch (_) { /* best-effort */ }
|
|
131
|
+
|
|
132
|
+
const registryOpts = { homeDir: opts.homeDir, env: opts.env, hub: opts.hub };
|
|
133
|
+
const registry = hostsRegistryLib.readHostsRegistry(registryOpts);
|
|
134
|
+
if (registry.hosts.some((item) => pathKey(item.dir) === pathKey(plan.target))) {
|
|
135
|
+
hostsRegistryLib.removeHost(registryOpts, { dir: plan.target });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
hubLib.appendAudit(plan.hubDir, {
|
|
140
|
+
event: 'hosts.purge',
|
|
141
|
+
dir: plan.target,
|
|
142
|
+
label: plan.label,
|
|
143
|
+
state: plan.state,
|
|
144
|
+
trashedTo: plan.trashPath,
|
|
145
|
+
unlinked: unlinkedCount,
|
|
146
|
+
keptLinks: plan.keptLinks,
|
|
147
|
+
keptEntries: plan.keptEntries,
|
|
148
|
+
});
|
|
149
|
+
} catch (_) { /* 审计失败不阻断 */ }
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
dir: plan.target,
|
|
154
|
+
state: plan.state,
|
|
155
|
+
unlinkCount: unlinkedCount,
|
|
156
|
+
keptLinks: plan.keptLinks,
|
|
157
|
+
keptEntries: plan.keptEntries,
|
|
158
|
+
trashedTo: plan.trashPath,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
planHostPurge,
|
|
164
|
+
applyHostPurge,
|
|
165
|
+
purgeStamp,
|
|
166
|
+
safeOrphanName,
|
|
167
|
+
};
|