@yottameta/yotta-skills 0.24.2 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +20 -0
- package/README.zh-CN.md +20 -0
- package/SKILL.md +43 -2
- package/bin/yotta-skills.js +415 -29
- package/lib/agent-dirs.js +201 -0
- package/lib/agent-discovery.js +302 -0
- package/lib/hub-adopt.js +268 -0
- package/lib/hub.js +583 -0
- package/lib/skills-cli-lock.js +52 -0
- package/lib/skills-scan.js +28 -38
- package/package.json +1 -1
- package/references/hub.md +66 -0
- package/scripts/yotta-skills-mcp.py +1 -1
- package/skill-manifest.json +1 -1
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agent skill directory mappings.
|
|
5
|
+
*
|
|
6
|
+
* This module is intentionally data-only. It is used by both the installer
|
|
7
|
+
* CLI and the local discovery/inventory code so a host mapping is defined
|
|
8
|
+
* once. `resolveUserDir` applies the documented environment overrides.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
|
|
14
|
+
const AGENT_DIRS = {
|
|
15
|
+
'aider-desk': { label: 'AiderDesk', dirs: ['.aider-desk/skills'] },
|
|
16
|
+
amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
|
|
17
|
+
antigravity: { label: 'Antigravity', dirs: ['.gemini/antigravity/skills', '.agents/skills'] },
|
|
18
|
+
'antigravity-cli': { label: 'Antigravity CLI', dirs: ['.gemini/antigravity-cli/skills', '.agents/skills'] },
|
|
19
|
+
'autohand-code': { label: 'Autohand Code CLI', dirs: ['.autohand/skills'] },
|
|
20
|
+
augment: { label: 'Augment', dirs: ['.augment/skills'] },
|
|
21
|
+
bob: { label: 'IBM Bob', dirs: ['.bob/skills'] },
|
|
22
|
+
claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
|
|
23
|
+
cline: { label: 'Cline', dirs: ['.agents/skills', '.cline/skills'] },
|
|
24
|
+
codearts: { label: 'CodeArts Agent', dirs: ['.codeartsdoer/skills'] },
|
|
25
|
+
codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
|
|
26
|
+
codemaker: { label: 'Codemaker', dirs: ['.codemaker/skills'] },
|
|
27
|
+
codestudio: { label: 'Code Studio', dirs: ['.codestudio/skills'] },
|
|
28
|
+
codex: { label: 'Codex', dirs: ['.codex/skills', '.agents/skills'] },
|
|
29
|
+
'command-code': { label: 'Command Code', dirs: ['.commandcode/skills'] },
|
|
30
|
+
comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
|
|
31
|
+
continue: { label: 'Continue', dirs: ['.continue/skills'] },
|
|
32
|
+
cortex: { label: 'Cortex Code', dirs: ['.snowflake/cortex/skills'] },
|
|
33
|
+
crush: { label: 'Crush', dirs: ['.config/crush/skills'] },
|
|
34
|
+
cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
|
|
35
|
+
deepagents: { label: 'Deep Agents', dirs: ['.deepagents/agent/skills', '.agents/skills'] },
|
|
36
|
+
devin: { label: 'Devin for Terminal', dirs: ['.config/devin/skills'] },
|
|
37
|
+
droid: { label: 'Droid', dirs: ['.factory/skills', '.agents/skills'] },
|
|
38
|
+
firebender: { label: 'Firebender', dirs: ['.firebender/skills', '.agents/skills'] },
|
|
39
|
+
forgecode: { label: 'ForgeCode', dirs: ['.forge/skills'] },
|
|
40
|
+
fx: { label: 'fx', dirs: ['.fx/skills'] },
|
|
41
|
+
gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
|
|
42
|
+
copilot: { label: 'GitHub Copilot', dirs: ['.copilot/skills', '.agents/skills'] },
|
|
43
|
+
goose: { label: 'Goose', dirs: ['.config/goose/skills', '.goose/skills'] },
|
|
44
|
+
grok: { label: 'Grok Build', dirs: ['.grok/skills'] },
|
|
45
|
+
hermes: { label: 'Hermes Agent', dirs: ['.hermes/skills'] },
|
|
46
|
+
'inference-sh': { label: 'inference.sh', dirs: ['.inferencesh/skills'] },
|
|
47
|
+
jazz: { label: 'Jazz', dirs: ['.jazz/skills'] },
|
|
48
|
+
junie: { label: 'Junie', dirs: ['.junie/skills'] },
|
|
49
|
+
iflow: { label: 'iFlow CLI', dirs: ['.iflow/skills'] },
|
|
50
|
+
kilo: { label: 'Kilo Code', dirs: ['.kilo/skills', '.agents/skills'] },
|
|
51
|
+
kimchi: { label: 'Kimchi', dirs: ['.config/kimchi/harness/skills'] },
|
|
52
|
+
kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills', '.agents/skills'] },
|
|
53
|
+
kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
|
|
54
|
+
kode: { label: 'Kode', dirs: ['.kode/skills'] },
|
|
55
|
+
lingma: { label: 'Lingma', dirs: ['.lingma/skills'] },
|
|
56
|
+
mcpjam: { label: 'MCPJam', dirs: ['.mcpjam/skills'] },
|
|
57
|
+
minimax: { label: 'MiniMax Code', dirs: ['.minimax/skills'] },
|
|
58
|
+
'mistral-vibe': { label: 'Mistral Vibe', dirs: ['.vibe/skills'] },
|
|
59
|
+
moxby: { label: 'Moxby', dirs: ['.moxby/skills'] },
|
|
60
|
+
mux: { label: 'Mux', dirs: ['.mux/skills'] },
|
|
61
|
+
opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills', '.agents/skills'] },
|
|
62
|
+
openhands: { label: 'OpenHands', dirs: ['.openhands/skills'] },
|
|
63
|
+
ona: { label: 'Ona', dirs: ['.ona/skills'] },
|
|
64
|
+
openclaw: { label: 'OpenClaw / QClaw', dirs: ['.openclaw/skills'] },
|
|
65
|
+
pi: { label: 'Pi', dirs: ['.agents/skills'] },
|
|
66
|
+
posit: { label: 'Posit Assistant', dirs: ['.posit/assistant/skills'] },
|
|
67
|
+
qoder: { label: 'Qoder', dirs: ['.qoder/skills'] },
|
|
68
|
+
'qoder-cn': { label: 'Qoder CN', dirs: ['.qoder-cn/skills'] },
|
|
69
|
+
qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
|
|
70
|
+
reasonix: { label: 'Reasonix', dirs: ['.reasonix/skills'] },
|
|
71
|
+
rovodev: { label: 'Rovo Dev', dirs: ['.rovodev/skills'] },
|
|
72
|
+
roo: { label: 'Roo Code', dirs: ['.roo/skills'] },
|
|
73
|
+
'sarvam-code': { label: 'Sarvam Code', dirs: ['.sarvam/skills', '.agents/skills'] },
|
|
74
|
+
tabnine: { label: 'Tabnine CLI', dirs: ['.tabnine/agent/skills'] },
|
|
75
|
+
terramind: { label: 'Terramind', dirs: ['.terramind/skills'] },
|
|
76
|
+
tinycloud: { label: 'Tinycloud', dirs: ['.tinycloud/skills'] },
|
|
77
|
+
trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills', '.trae/skills'] },
|
|
78
|
+
'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills', '.trae/skills'] },
|
|
79
|
+
warp: { label: 'Warp', dirs: ['.agents/skills', '.warp/skills'] },
|
|
80
|
+
windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills', '.windsurf/skills'] },
|
|
81
|
+
workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
|
|
82
|
+
zcode: { label: 'ZCode', dirs: ['.zcode/skills'] },
|
|
83
|
+
zencoder: { label: 'Zencoder', dirs: ['.zencoder/skills'] },
|
|
84
|
+
neovate: { label: 'Neovate', dirs: ['.neovate/skills'] },
|
|
85
|
+
pochi: { label: 'Pochi', dirs: ['.pochi/skills'] },
|
|
86
|
+
adal: { label: 'AdaL', dirs: ['.adal/skills'] },
|
|
87
|
+
dsh: { label: 'DSH / DeepSeek Harness', dirs: ['.dsh/skills'] },
|
|
88
|
+
yottacode: { label: 'YottaCode', dirs: ['.yottacode/skills'] },
|
|
89
|
+
box: { label: 'Box Agent', dirs: ['.box-agent/skills'] },
|
|
90
|
+
lmstudio: { label: 'LM Studio', dirs: ['.lmstudio/skills'] },
|
|
91
|
+
ccswitch: { label: 'CC Switch', dirs: ['.cc-switch/skills'] },
|
|
92
|
+
agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
|
|
93
|
+
universal: { label: 'Universal .agents', dirs: ['.agents/skills'] },
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function envPath(env, key) {
|
|
97
|
+
const value = env && env[key];
|
|
98
|
+
return value ? String(value) : null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function homeDir(options) {
|
|
102
|
+
return (options && options.homeDir) || os.homedir();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function envObject(options) {
|
|
106
|
+
return (options && options.env) || process.env;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolve one relative mapping. Environment overrides are intentionally
|
|
111
|
+
* explicit rather than inferred: if an agent supports a custom home, the
|
|
112
|
+
* caller must set that agent's documented variable.
|
|
113
|
+
*/
|
|
114
|
+
function resolveUserDir(rel, options) {
|
|
115
|
+
const home = homeDir(options);
|
|
116
|
+
const env = envObject(options);
|
|
117
|
+
|
|
118
|
+
if (rel === '.codex/skills') {
|
|
119
|
+
return path.join(envPath(env, 'CODEX_HOME') || path.join(home, '.codex'), 'skills');
|
|
120
|
+
}
|
|
121
|
+
if (rel.startsWith('.config/')) {
|
|
122
|
+
const base = envPath(env, 'XDG_CONFIG_HOME') || path.join(home, '.config');
|
|
123
|
+
return path.join(base, ...rel.slice('.config/'.length).split('/'));
|
|
124
|
+
}
|
|
125
|
+
if (rel === '.openclaw/skills') {
|
|
126
|
+
return path.join(envPath(env, 'OPENCLAW_STATE_DIR') || path.join(home, '.openclaw'), 'skills');
|
|
127
|
+
}
|
|
128
|
+
if (rel === '.claude/skills') {
|
|
129
|
+
return path.join(envPath(env, 'CLAUDE_CONFIG_DIR') || path.join(home, '.claude'), 'skills');
|
|
130
|
+
}
|
|
131
|
+
if (rel === '.hermes/skills') {
|
|
132
|
+
return path.join(envPath(env, 'HERMES_HOME') || path.join(home, '.hermes'), 'skills');
|
|
133
|
+
}
|
|
134
|
+
if (rel === '.vibe/skills') {
|
|
135
|
+
return path.join(envPath(env, 'VIBE_HOME') || path.join(home, '.vibe'), 'skills');
|
|
136
|
+
}
|
|
137
|
+
if (rel === '.grok/skills') {
|
|
138
|
+
return path.join(envPath(env, 'GROK_HOME') || path.join(home, '.grok'), 'skills');
|
|
139
|
+
}
|
|
140
|
+
if (rel === '.autohand/skills') {
|
|
141
|
+
return path.join(envPath(env, 'AUTOHAND_HOME') || path.join(home, '.autohand'), 'skills');
|
|
142
|
+
}
|
|
143
|
+
if (rel === '.sarvam/skills') {
|
|
144
|
+
return path.join(envPath(env, 'SARVAM_HOME') || path.join(home, '.sarvam'), 'skills');
|
|
145
|
+
}
|
|
146
|
+
return path.join(home, rel);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** All known agent skill directories, including missing ones. */
|
|
150
|
+
function knownRoots(options) {
|
|
151
|
+
const roots = [];
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const add = (dir, agentId, label) => {
|
|
154
|
+
if (!dir) return;
|
|
155
|
+
const resolved = path.resolve(dir);
|
|
156
|
+
if (seen.has(resolved)) return;
|
|
157
|
+
seen.add(resolved);
|
|
158
|
+
roots.push({ dir: resolved, agentId, label, known: true });
|
|
159
|
+
};
|
|
160
|
+
for (const [agentId, info] of Object.entries(AGENT_DIRS)) {
|
|
161
|
+
for (const rel of info.dirs) {
|
|
162
|
+
add(resolveUserDir(rel, options), agentId, info.label);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return roots;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Additional roots that are commonly used by desktop / portable agents. */
|
|
169
|
+
function envRoots(options) {
|
|
170
|
+
const env = envObject(options);
|
|
171
|
+
const roots = [];
|
|
172
|
+
const add = (dir, agentId, label) => {
|
|
173
|
+
if (!dir) return;
|
|
174
|
+
roots.push({ dir: path.resolve(dir), agentId: agentId || null, label: label || '环境变量指定目录', known: false, env: true });
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
add(envPath(env, 'CODEX_HOME') && path.join(env.CODEX_HOME, 'skills'), 'codex', 'Codex(CODEX_HOME)');
|
|
178
|
+
add(envPath(env, 'XDG_CONFIG_HOME') && path.join(env.XDG_CONFIG_HOME, 'opencode', 'skills'), 'opencode', 'OpenCode(XDG_CONFIG_HOME)');
|
|
179
|
+
add(envPath(env, 'XDG_STATE_HOME') && path.join(env.XDG_STATE_HOME, 'skills'), null, 'XDG_STATE_HOME/skills');
|
|
180
|
+
add(envPath(env, 'XDG_DATA_HOME') && path.join(env.XDG_DATA_HOME, 'skills'), null, 'XDG_DATA_HOME/skills');
|
|
181
|
+
add(envPath(env, 'DSH_HOME') && path.join(env.DSH_HOME, 'skills'), 'dsh', 'DSH(DSH_HOME)');
|
|
182
|
+
add(envPath(env, 'DSH_AGENTS_HOME') && path.join(env.DSH_AGENTS_HOME, 'skills'), 'dsh', 'DSH Agents(DSH_AGENTS_HOME)');
|
|
183
|
+
add(envPath(env, 'OPENCLAW_STATE_DIR') && path.join(env.OPENCLAW_STATE_DIR, 'skills'), 'openclaw', 'OpenClaw(OPENCLAW_STATE_DIR)');
|
|
184
|
+
add(envPath(env, 'CLAUDE_CONFIG_DIR') && path.join(env.CLAUDE_CONFIG_DIR, 'skills'), 'claude', 'Claude Code(CLAUDE_CONFIG_DIR)');
|
|
185
|
+
add(envPath(env, 'YOTTACODE_HOME') && path.join(env.YOTTACODE_HOME, 'skills'), 'yottacode', 'YottaCode(YOTTACODE_HOME)');
|
|
186
|
+
|
|
187
|
+
const extra = String(env.YOTTA_SKILLS_DISCOVERY_ROOTS || '')
|
|
188
|
+
.split(path.delimiter)
|
|
189
|
+
.map((item) => item.trim())
|
|
190
|
+
.filter(Boolean);
|
|
191
|
+
for (const dir of extra) add(dir, null, 'YOTTA_SKILLS_DISCOVERY_ROOTS');
|
|
192
|
+
|
|
193
|
+
return roots;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
AGENT_DIRS,
|
|
198
|
+
resolveUserDir,
|
|
199
|
+
knownRoots,
|
|
200
|
+
envRoots,
|
|
201
|
+
};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local agent / skill-directory discovery.
|
|
5
|
+
*
|
|
6
|
+
* The discovery path is intentionally filesystem-first: it does not read the
|
|
7
|
+
* memory registry or any YottaMeta state. It combines:
|
|
8
|
+
* 1. known host mappings + documented environment overrides;
|
|
9
|
+
* 2. bounded scans of common config / app-data roots;
|
|
10
|
+
* 3. a lightweight installed-app marker scan for hosts that have no
|
|
11
|
+
* conventional skills directory.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const agentDirs = require('./agent-dirs');
|
|
18
|
+
|
|
19
|
+
const SKILL_FILE = 'SKILL.md';
|
|
20
|
+
const SKIP_DIRS = new Set([
|
|
21
|
+
'.git', 'node_modules', '__pycache__', '.pytest_cache', '.mypy_cache',
|
|
22
|
+
'.venv', 'venv', 'site-packages', 'dist', 'build', 'out', 'coverage',
|
|
23
|
+
'logs', 'log', 'tmp', 'temp', 'cache', '.cache', '.npm', '.cargo',
|
|
24
|
+
'.rustup', '.nuget', '.gradle', '.m2', '.ssh',
|
|
25
|
+
]);
|
|
26
|
+
const AGENT_HINT = /(agent|claude|cursor|codex|opencode|openclaw|qclaw|trae|qwen|kimi|comate|codebuddy|workbuddy|dsh|deepseek|mimo|yotta|bionic|cherry|teleagent|dumate|cc.?switch|lmstudio|box|hermes|mavis|pi\b)/i;
|
|
27
|
+
const INSTALLED_NOISE = /(uninstall|updater|update|virtualbox|oracle|redistributable|runtime|driver)/i;
|
|
28
|
+
|
|
29
|
+
function envObject(options) {
|
|
30
|
+
return (options && options.env) || process.env;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function homeDir(options) {
|
|
34
|
+
return (options && options.homeDir) || os.homedir();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isDirectory(dir) {
|
|
38
|
+
try {
|
|
39
|
+
return fs.statSync(dir).isDirectory();
|
|
40
|
+
} catch (_) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeReaddir(dir) {
|
|
46
|
+
try {
|
|
47
|
+
return fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
} catch (_) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countSkills(dir) {
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const entry of safeReaddir(dir)) {
|
|
56
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
57
|
+
const child = path.join(dir, entry.name);
|
|
58
|
+
if (!isDirectory(child)) continue;
|
|
59
|
+
try {
|
|
60
|
+
if (fs.statSync(path.join(child, SKILL_FILE)).isFile()) count++;
|
|
61
|
+
} catch (_) { /* not a standard skill directory */ }
|
|
62
|
+
}
|
|
63
|
+
return count;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizePath(dir) {
|
|
67
|
+
const resolved = path.resolve(dir);
|
|
68
|
+
try {
|
|
69
|
+
return fs.realpathSync.native(resolved);
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isNoisePath(dir, baseDir) {
|
|
76
|
+
const resolved = path.resolve(dir);
|
|
77
|
+
const base = baseDir ? path.resolve(baseDir) : null;
|
|
78
|
+
const relative = base && isInsidePath(base, resolved) ? path.relative(base, resolved) : resolved;
|
|
79
|
+
const parts = relative.split(/[\\/]+/).filter(Boolean).map((part) => part.toLowerCase());
|
|
80
|
+
for (const part of parts) {
|
|
81
|
+
if (part === 'temp' || part === 'tmp') return true;
|
|
82
|
+
if (/\.bak$/.test(part)) return true;
|
|
83
|
+
if (part === 'plugin-sources' || part === 'plugins' || part === 'node_modules') return true;
|
|
84
|
+
if (part === 'candidate' || part.startsWith('candidate-') || part.includes('staging')) return true;
|
|
85
|
+
if (part === 'memories' || part === 'connectors' || part === 'workspace') return true;
|
|
86
|
+
}
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isInsidePath(root, target) {
|
|
91
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
92
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function addRoot(list, seen, dir, meta) {
|
|
96
|
+
if (!dir) return;
|
|
97
|
+
const resolved = path.resolve(dir);
|
|
98
|
+
const key = normalizePath(resolved).toLowerCase();
|
|
99
|
+
if (seen.has(key)) {
|
|
100
|
+
const existing = seen.get(key);
|
|
101
|
+
if (meta && meta.known && existing) existing.known = true;
|
|
102
|
+
if (meta && meta.agentId && !existing.agentId) existing.agentId = meta.agentId;
|
|
103
|
+
if (meta && meta.label && existing.label === '自动发现目录') existing.label = meta.label;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const record = {
|
|
107
|
+
dir: resolved,
|
|
108
|
+
agentId: (meta && meta.agentId) || null,
|
|
109
|
+
label: (meta && meta.label) || '自动发现目录',
|
|
110
|
+
known: Boolean(meta && meta.known),
|
|
111
|
+
env: Boolean(meta && meta.env),
|
|
112
|
+
detection: (meta && meta.detection) || 'mapping',
|
|
113
|
+
exists: isDirectory(resolved),
|
|
114
|
+
};
|
|
115
|
+
record.skillCount = record.exists ? countSkills(resolved) : 0;
|
|
116
|
+
record.status = record.exists ? 'skills-dir' : 'missing';
|
|
117
|
+
seen.set(key, record);
|
|
118
|
+
list.push(record);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Bounded recursive scan for directories named "skills".
|
|
123
|
+
* Only branches that look agent-related are followed after the first level.
|
|
124
|
+
*/
|
|
125
|
+
function scanForSkillDirs(root, options) {
|
|
126
|
+
const maxDepth = (options && options.maxDepth) || 3;
|
|
127
|
+
const maxVisits = (options && options.maxVisits) || 6000;
|
|
128
|
+
const found = [];
|
|
129
|
+
const queue = [{ dir: path.resolve(root), depth: 0 }];
|
|
130
|
+
let visits = 0;
|
|
131
|
+
|
|
132
|
+
while (queue.length > 0 && visits < maxVisits) {
|
|
133
|
+
const current = queue.shift();
|
|
134
|
+
if (!isDirectory(current.dir)) continue;
|
|
135
|
+
visits++;
|
|
136
|
+
const entries = safeReaddir(current.dir);
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
139
|
+
const full = path.join(current.dir, entry.name);
|
|
140
|
+
if (/^skills$/i.test(entry.name)) {
|
|
141
|
+
if (isDirectory(full) && !isNoisePath(full, options && options.baseDir)) found.push({ dir: full, parent: current.dir });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (current.depth >= maxDepth) continue;
|
|
145
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
146
|
+
if (!isDirectory(full)) continue;
|
|
147
|
+
const branchLooksRelevant = current.depth === 0
|
|
148
|
+
|| AGENT_HINT.test(entry.name)
|
|
149
|
+
|| AGENT_HINT.test(path.basename(current.dir));
|
|
150
|
+
if (branchLooksRelevant) queue.push({ dir: full, depth: current.depth + 1 });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return found;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function commonRoots(options) {
|
|
157
|
+
const env = envObject(options);
|
|
158
|
+
const home = homeDir(options);
|
|
159
|
+
const roots = [];
|
|
160
|
+
const add = (dir, depth) => {
|
|
161
|
+
if (!dir) return;
|
|
162
|
+
roots.push({ dir: path.resolve(dir), maxDepth: depth });
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
add(home, 2);
|
|
166
|
+
add(env.APPDATA, 3);
|
|
167
|
+
add(env.LOCALAPPDATA, 3);
|
|
168
|
+
add(env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'Programs'), 3);
|
|
169
|
+
add(env.ProgramFiles, 2);
|
|
170
|
+
add(env['ProgramFiles(x86)'], 2);
|
|
171
|
+
add(env.XDG_CONFIG_HOME, 3);
|
|
172
|
+
add(env.XDG_STATE_HOME, 3);
|
|
173
|
+
add(env.XDG_DATA_HOME, 3);
|
|
174
|
+
add(env.DSH_HOME, 3);
|
|
175
|
+
add(env.DSH_AGENTS_HOME, 3);
|
|
176
|
+
add(env.OPENCLAW_STATE_DIR, 2);
|
|
177
|
+
add(env.CLAUDE_CONFIG_DIR, 2);
|
|
178
|
+
add(env.CODEX_HOME, 2);
|
|
179
|
+
add(env.YOTTACODE_HOME, 2);
|
|
180
|
+
|
|
181
|
+
// A portable agent install often lives beside the current workspace. The
|
|
182
|
+
// scanner only follows agent-looking branches, so this stays bounded.
|
|
183
|
+
if (!options || (options.includeCwd !== false && String(env.YOTTA_SKILLS_DISCOVERY_NO_CWD || '') !== '1')) {
|
|
184
|
+
let current = path.resolve(process.cwd());
|
|
185
|
+
for (let depth = 0; depth < 3; depth++) {
|
|
186
|
+
add(current, 2);
|
|
187
|
+
const parent = path.dirname(current);
|
|
188
|
+
if (parent === current) break;
|
|
189
|
+
current = parent;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return roots;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function collectMarkerNames(root, depth, output, maxEntries) {
|
|
196
|
+
if (!root || depth < 0 || output.size >= maxEntries) return;
|
|
197
|
+
for (const entry of safeReaddir(root)) {
|
|
198
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
199
|
+
if (/\.lnk$/i.test(entry.name)) {
|
|
200
|
+
output.add(entry.name.replace(/\.lnk$/i, '').trim());
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (entry.isDirectory() && depth > 0) {
|
|
204
|
+
collectMarkerNames(path.join(root, entry.name), depth - 1, output, maxEntries);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function installedMarkers(options) {
|
|
210
|
+
const env = envObject(options);
|
|
211
|
+
const names = new Set();
|
|
212
|
+
const roots = [
|
|
213
|
+
env.APPDATA && path.join(env.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs'),
|
|
214
|
+
env.ProgramData && path.join(env.ProgramData, 'Microsoft', 'Windows', 'Start Menu', 'Programs'),
|
|
215
|
+
env.APPDATA,
|
|
216
|
+
env.LOCALAPPDATA,
|
|
217
|
+
env.LOCALAPPDATA && path.join(env.LOCALAPPDATA, 'Programs'),
|
|
218
|
+
env.ProgramFiles,
|
|
219
|
+
env['ProgramFiles(x86)'],
|
|
220
|
+
];
|
|
221
|
+
for (const root of roots) collectMarkerNames(root, 2, names, 800);
|
|
222
|
+
return [...names].filter((name) => AGENT_HINT.test(name) && !INSTALLED_NOISE.test(name));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function knownLabelMatches(name) {
|
|
226
|
+
const normalized = String(name || '').toLowerCase();
|
|
227
|
+
for (const info of Object.values(agentDirs.AGENT_DIRS)) {
|
|
228
|
+
if (normalized.includes(String(info.label).toLowerCase())) return true;
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Discover hosts and their skill directories.
|
|
235
|
+
*
|
|
236
|
+
* Returns:
|
|
237
|
+
* hosts - all candidate skill roots (existing and missing known mappings)
|
|
238
|
+
* installed - installed app markers, including hosts without a skills dir
|
|
239
|
+
* errors - non-fatal discovery errors
|
|
240
|
+
*/
|
|
241
|
+
function discoverHosts(options) {
|
|
242
|
+
const roots = [];
|
|
243
|
+
const seen = new Map();
|
|
244
|
+
const errors = [];
|
|
245
|
+
|
|
246
|
+
for (const root of agentDirs.knownRoots(options)) {
|
|
247
|
+
addRoot(roots, seen, root.dir, { ...root, detection: 'mapping' });
|
|
248
|
+
}
|
|
249
|
+
for (const root of agentDirs.envRoots(options)) {
|
|
250
|
+
addRoot(roots, seen, root.dir, { ...root, detection: root.env ? 'env' : 'workspace' });
|
|
251
|
+
}
|
|
252
|
+
for (const root of commonRoots(options)) {
|
|
253
|
+
for (const found of scanForSkillDirs(root.dir, {
|
|
254
|
+
maxDepth: root.maxDepth,
|
|
255
|
+
baseDir: options.homeDir || os.homedir(),
|
|
256
|
+
})) {
|
|
257
|
+
addRoot(roots, seen, found.dir, {
|
|
258
|
+
detection: 'discovered',
|
|
259
|
+
label: '自动发现:' + path.basename(found.parent),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const installed = [];
|
|
265
|
+
const installedSeen = new Set();
|
|
266
|
+
const addInstalled = (name, source) => {
|
|
267
|
+
const label = String(name || '').trim();
|
|
268
|
+
if (!label) return;
|
|
269
|
+
const key = label.toLowerCase();
|
|
270
|
+
if (installedSeen.has(key)) return;
|
|
271
|
+
installedSeen.add(key);
|
|
272
|
+
installed.push({ label, source, hasSkillsDir: false });
|
|
273
|
+
};
|
|
274
|
+
for (const name of installedMarkers(options)) {
|
|
275
|
+
if (knownLabelMatches(name) || AGENT_HINT.test(name)) addInstalled(name, '应用标记');
|
|
276
|
+
}
|
|
277
|
+
for (const host of roots) {
|
|
278
|
+
if (!host.exists) continue;
|
|
279
|
+
addInstalled(host.label, host.known ? '已知宿主' : '自动发现');
|
|
280
|
+
const item = installed.find((entry) => entry.label.toLowerCase() === String(host.label).toLowerCase());
|
|
281
|
+
if (item) {
|
|
282
|
+
item.hasSkillsDir = true;
|
|
283
|
+
item.skillDir = host.dir;
|
|
284
|
+
item.skillCount = host.skillCount;
|
|
285
|
+
item.agentId = host.agentId;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
generatedAt: new Date().toISOString(),
|
|
291
|
+
hosts: roots,
|
|
292
|
+
installed,
|
|
293
|
+
errors,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
module.exports = {
|
|
298
|
+
AGENT_HINT,
|
|
299
|
+
discoverHosts,
|
|
300
|
+
scanForSkillDirs,
|
|
301
|
+
countSkills,
|
|
302
|
+
};
|