u-foo 2.3.18 → 2.3.20
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/README.md +8 -2
- package/README.zh-CN.md +8 -2
- package/package.json +1 -1
- package/src/chat/agentViewController.js +98 -11
- package/src/chat/commandExecutor.js +3 -3
- package/src/chat/commands.js +3 -3
- package/src/chat/daemonMessageRouter.js +18 -2
- package/src/chat/index.js +23 -0
- package/src/code/agent.js +118 -4
- package/src/code/cli.js +47 -0
- package/src/code/prompts/index.js +9 -0
- package/src/code/skills/index.js +74 -0
- package/src/code/skills/injection.js +120 -0
- package/src/code/skills/loader.js +218 -0
- package/src/code/skills/render.js +46 -0
- package/src/daemon/index.js +196 -12
- package/src/shared/eventContract.js +1 -0
package/src/code/cli.js
CHANGED
|
@@ -4,6 +4,11 @@ const {
|
|
|
4
4
|
listResults,
|
|
5
5
|
} = require("./runtime");
|
|
6
6
|
const { runUcodeCoreAgent } = require("./agent");
|
|
7
|
+
const {
|
|
8
|
+
formatSkillsList,
|
|
9
|
+
listUcodeSkills,
|
|
10
|
+
showSkill,
|
|
11
|
+
} = require("./skills");
|
|
7
12
|
|
|
8
13
|
function parseArgs(argv = []) {
|
|
9
14
|
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
@@ -16,11 +21,21 @@ function parseArgs(argv = []) {
|
|
|
16
21
|
max: 1,
|
|
17
22
|
num: 20,
|
|
18
23
|
taskId: "",
|
|
24
|
+
skillsAction: "",
|
|
25
|
+
skillsName: "",
|
|
19
26
|
};
|
|
20
27
|
|
|
21
28
|
for (let i = 1; i < args.length; i += 1) {
|
|
22
29
|
const item = String(args[i] || "").trim();
|
|
23
30
|
if (!item) continue;
|
|
31
|
+
if (out.command === "skills" && !item.startsWith("-")) {
|
|
32
|
+
if (!out.skillsAction) {
|
|
33
|
+
out.skillsAction = item.toLowerCase();
|
|
34
|
+
} else if (!out.skillsName) {
|
|
35
|
+
out.skillsName = item;
|
|
36
|
+
}
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
24
39
|
if (item === "--json") {
|
|
25
40
|
out.json = true;
|
|
26
41
|
continue;
|
|
@@ -69,6 +84,8 @@ function usage() {
|
|
|
69
84
|
" submit --tool <read|write|edit|bash> --args-json <json> [--workspace <path>] [--task-id <id>]",
|
|
70
85
|
" run-once [--max <n>] [--workspace <path>]",
|
|
71
86
|
" list [--num <n>]",
|
|
87
|
+
" skills list [--workspace <path>]",
|
|
88
|
+
" skills show <name> [--workspace <path>]",
|
|
72
89
|
"",
|
|
73
90
|
"Flags:",
|
|
74
91
|
" --json Output JSON",
|
|
@@ -147,6 +164,36 @@ async function runUcodeCoreCli({
|
|
|
147
164
|
return { exitCode: 0, output: `${lines.join("\n")}${lines.length ? "\n" : ""}` };
|
|
148
165
|
}
|
|
149
166
|
|
|
167
|
+
if (cmd === "skills") {
|
|
168
|
+
const action = options.skillsAction || "list";
|
|
169
|
+
const workspaceRoot = options.workspace || projectRoot;
|
|
170
|
+
if (action === "list" || action === "ls") {
|
|
171
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
172
|
+
if (options.json) {
|
|
173
|
+
return { exitCode: 0, output: `${JSON.stringify({ ok: true, ...outcome })}\n` };
|
|
174
|
+
}
|
|
175
|
+
return { exitCode: 0, output: `${formatSkillsList(outcome)}\n` };
|
|
176
|
+
}
|
|
177
|
+
if (action === "show") {
|
|
178
|
+
if (!options.skillsName) {
|
|
179
|
+
return { exitCode: 1, output: "skills show requires <name>\n" };
|
|
180
|
+
}
|
|
181
|
+
const result = showSkill({
|
|
182
|
+
name: options.skillsName,
|
|
183
|
+
workspaceRoot,
|
|
184
|
+
asJson: options.json,
|
|
185
|
+
});
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
return { exitCode: 1, output: `${options.json ? JSON.stringify({ ok: false, error: result.error }) : result.error}\n` };
|
|
188
|
+
}
|
|
189
|
+
if (options.json) {
|
|
190
|
+
return { exitCode: 0, output: `${JSON.stringify(result)}\n` };
|
|
191
|
+
}
|
|
192
|
+
return { exitCode: 0, output: `${result.output}\n` };
|
|
193
|
+
}
|
|
194
|
+
return { exitCode: 1, output: "unknown skills command: use list or show\n" };
|
|
195
|
+
}
|
|
196
|
+
|
|
150
197
|
return { exitCode: 1, output: `unknown command: ${cmd}\n` };
|
|
151
198
|
}
|
|
152
199
|
|
|
@@ -8,8 +8,13 @@ const { getSafetySection } = require("./safety");
|
|
|
8
8
|
const { getOutputEfficiencySection } = require("./efficiency");
|
|
9
9
|
const { getUfooIntegrationSection } = require("./ufoo");
|
|
10
10
|
const { getEnvironmentSection } = require("./environment");
|
|
11
|
+
const {
|
|
12
|
+
listUcodeSkills,
|
|
13
|
+
renderSkillsSection,
|
|
14
|
+
} = require("../skills");
|
|
11
15
|
const {
|
|
12
16
|
systemPromptSection,
|
|
17
|
+
uncachedSection,
|
|
13
18
|
resolveSections,
|
|
14
19
|
clearSectionCache,
|
|
15
20
|
} = require("./sections");
|
|
@@ -68,6 +73,10 @@ function getSystemPrompt({
|
|
|
68
73
|
systemPromptSection("environment", () =>
|
|
69
74
|
getEnvironmentSection({ workspaceRoot, model, provider }),
|
|
70
75
|
),
|
|
76
|
+
uncachedSection("skills", () => {
|
|
77
|
+
const outcome = listUcodeSkills({ workspaceRoot: workspaceRoot || process.cwd() });
|
|
78
|
+
return renderSkillsSection(outcome.skills);
|
|
79
|
+
}),
|
|
71
80
|
];
|
|
72
81
|
const dynamicSections = resolveSections(dynamicSectionDefs);
|
|
73
82
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const {
|
|
2
|
+
listUcodeSkills,
|
|
3
|
+
findSkillsByName,
|
|
4
|
+
} = require("./loader");
|
|
5
|
+
const {
|
|
6
|
+
renderSkillsSection,
|
|
7
|
+
formatSkillsList,
|
|
8
|
+
} = require("./render");
|
|
9
|
+
const {
|
|
10
|
+
buildSkillInjections,
|
|
11
|
+
} = require("./injection");
|
|
12
|
+
|
|
13
|
+
function showSkill({ name = "", workspaceRoot = process.cwd(), asJson = false } = {}) {
|
|
14
|
+
const outcome = listUcodeSkills({ workspaceRoot });
|
|
15
|
+
const matches = findSkillsByName(outcome.skills, name);
|
|
16
|
+
if (matches.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `skill not found: ${name}`,
|
|
20
|
+
outcome,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (matches.length > 1) {
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
error: `skill is ambiguous: ${name}`,
|
|
27
|
+
outcome,
|
|
28
|
+
matches,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const skill = matches[0];
|
|
32
|
+
let content = "";
|
|
33
|
+
try {
|
|
34
|
+
content = require("fs").readFileSync(skill.path, "utf8");
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
error: err && err.message ? err.message : "failed to read skill",
|
|
39
|
+
outcome,
|
|
40
|
+
skill,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (asJson) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
skill,
|
|
47
|
+
content,
|
|
48
|
+
errors: outcome.errors,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
output: [
|
|
54
|
+
`# ${skill.name}`,
|
|
55
|
+
"",
|
|
56
|
+
`Description: ${skill.description}`,
|
|
57
|
+
`Path: ${skill.path}`,
|
|
58
|
+
"",
|
|
59
|
+
content.trim(),
|
|
60
|
+
].join("\n"),
|
|
61
|
+
skill,
|
|
62
|
+
content,
|
|
63
|
+
errors: outcome.errors,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
listUcodeSkills,
|
|
69
|
+
findSkillsByName,
|
|
70
|
+
renderSkillsSection,
|
|
71
|
+
formatSkillsList,
|
|
72
|
+
buildSkillInjections,
|
|
73
|
+
showSkill,
|
|
74
|
+
};
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const {
|
|
4
|
+
canonicalPath,
|
|
5
|
+
findSkillsByName,
|
|
6
|
+
listUcodeSkills,
|
|
7
|
+
} = require("./loader");
|
|
8
|
+
|
|
9
|
+
function markdownSkillLinks(prompt = "") {
|
|
10
|
+
const text = String(prompt || "");
|
|
11
|
+
const links = [];
|
|
12
|
+
const re = /\[([^\]]+)]\(([^)]+)\)/g;
|
|
13
|
+
let match;
|
|
14
|
+
while ((match = re.exec(text))) {
|
|
15
|
+
const label = String(match[1] || "").trim();
|
|
16
|
+
const target = String(match[2] || "").trim();
|
|
17
|
+
if (!target) continue;
|
|
18
|
+
if (target.startsWith("skill://") || /(?:^|[/\\])SKILL\.md(?:[#?].*)?$/i.test(target)) {
|
|
19
|
+
links.push({ label, target });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return links;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mentionedSkillNames(prompt = "") {
|
|
26
|
+
const text = String(prompt || "");
|
|
27
|
+
const names = new Set();
|
|
28
|
+
const re = /(^|[^\w-])\$([A-Za-z0-9][A-Za-z0-9_-]{0,63})\b/g;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = re.exec(text))) {
|
|
31
|
+
names.add(String(match[2] || "").trim());
|
|
32
|
+
}
|
|
33
|
+
return Array.from(names).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveSkillLinkTarget(target = "", workspaceRoot = process.cwd()) {
|
|
37
|
+
const raw = String(target || "").trim().replace(/[#?].*$/, "");
|
|
38
|
+
if (!raw) return "";
|
|
39
|
+
let value = raw;
|
|
40
|
+
if (value.startsWith("skill://")) {
|
|
41
|
+
value = value.slice("skill://".length);
|
|
42
|
+
try {
|
|
43
|
+
value = decodeURIComponent(value);
|
|
44
|
+
} catch {
|
|
45
|
+
// keep raw value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!value) return "";
|
|
49
|
+
if (path.isAbsolute(value)) return canonicalPath(value);
|
|
50
|
+
return canonicalPath(path.resolve(workspaceRoot || process.cwd(), value));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findSkillByPath(skills = [], targetPath = "") {
|
|
54
|
+
const target = canonicalPath(targetPath);
|
|
55
|
+
return (Array.isArray(skills) ? skills : []).find((skill) => canonicalPath(skill.path) === target) || null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readSkillBlock(skill) {
|
|
59
|
+
const content = fs.readFileSync(skill.path, "utf8");
|
|
60
|
+
return `<skill>\n<name>${skill.name}</name>\n<path>${String(skill.path).replace(/\\/g, "/")}</path>\n${content}\n</skill>`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSkillInjections({
|
|
64
|
+
prompt = "",
|
|
65
|
+
workspaceRoot = process.cwd(),
|
|
66
|
+
skillsOutcome = null,
|
|
67
|
+
loadSkills = listUcodeSkills,
|
|
68
|
+
} = {}) {
|
|
69
|
+
const outcome = skillsOutcome || loadSkills({ workspaceRoot });
|
|
70
|
+
const skills = Array.isArray(outcome.skills) ? outcome.skills : [];
|
|
71
|
+
const warnings = Array.isArray(outcome.errors)
|
|
72
|
+
? outcome.errors.map((err) => `failed to load skill ${err.path}: ${err.message}`)
|
|
73
|
+
: [];
|
|
74
|
+
const selected = new Map();
|
|
75
|
+
|
|
76
|
+
for (const name of mentionedSkillNames(prompt)) {
|
|
77
|
+
const matches = findSkillsByName(skills, name);
|
|
78
|
+
if (matches.length === 1) {
|
|
79
|
+
selected.set(canonicalPath(matches[0].path), matches[0]);
|
|
80
|
+
} else if (matches.length > 1) {
|
|
81
|
+
warnings.push(`skill $${name} is ambiguous; link to a specific SKILL.md path`);
|
|
82
|
+
} else {
|
|
83
|
+
warnings.push(`skill $${name} was not found`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const link of markdownSkillLinks(prompt)) {
|
|
88
|
+
const targetPath = resolveSkillLinkTarget(link.target, workspaceRoot);
|
|
89
|
+
const skill = findSkillByPath(skills, targetPath);
|
|
90
|
+
if (skill) {
|
|
91
|
+
selected.set(canonicalPath(skill.path), skill);
|
|
92
|
+
} else {
|
|
93
|
+
warnings.push(`skill link ${link.target} did not resolve to an enabled skill`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const blocks = [];
|
|
98
|
+
for (const skill of selected.values()) {
|
|
99
|
+
try {
|
|
100
|
+
blocks.push(readSkillBlock(skill));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
warnings.push(`failed to read skill ${skill.path}: ${err && err.message ? err.message : "read failed"}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
blocks,
|
|
108
|
+
warnings,
|
|
109
|
+
skills,
|
|
110
|
+
errors: Array.isArray(outcome.errors) ? outcome.errors : [],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = {
|
|
115
|
+
mentionedSkillNames,
|
|
116
|
+
markdownSkillLinks,
|
|
117
|
+
resolveSkillLinkTarget,
|
|
118
|
+
findSkillByPath,
|
|
119
|
+
buildSkillInjections,
|
|
120
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const matter = require("gray-matter");
|
|
5
|
+
|
|
6
|
+
const SKILL_FILE = "SKILL.md";
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 6;
|
|
8
|
+
|
|
9
|
+
function repoRootFromHere() {
|
|
10
|
+
return path.resolve(__dirname, "..", "..", "..");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function canonicalPath(filePath = "") {
|
|
14
|
+
const resolved = path.resolve(String(filePath || ""));
|
|
15
|
+
try {
|
|
16
|
+
return fs.realpathSync.native ? fs.realpathSync.native(resolved) : fs.realpathSync(resolved);
|
|
17
|
+
} catch {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isDirectory(filePath = "") {
|
|
23
|
+
try {
|
|
24
|
+
return fs.statSync(filePath).isDirectory();
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isFile(filePath = "") {
|
|
31
|
+
try {
|
|
32
|
+
return fs.statSync(filePath).isFile();
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function homeDir(env = process.env) {
|
|
39
|
+
return String((env && env.HOME) || os.homedir() || "").trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function defaultSkillRoots({
|
|
43
|
+
workspaceRoot = process.cwd(),
|
|
44
|
+
env = process.env,
|
|
45
|
+
repoRoot = repoRootFromHere(),
|
|
46
|
+
} = {}) {
|
|
47
|
+
const workspace = path.resolve(String(workspaceRoot || process.cwd()));
|
|
48
|
+
const home = homeDir(env);
|
|
49
|
+
const codexHome = String((env && env.CODEX_HOME) || "").trim()
|
|
50
|
+
|| (home ? path.join(home, ".codex") : "");
|
|
51
|
+
const roots = [
|
|
52
|
+
{ path: path.join(workspace, ".agents", "skills"), scope: "repo", source: "workspace-agents" },
|
|
53
|
+
{ path: path.join(workspace, ".codex", "skills"), scope: "repo", source: "workspace-codex" },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
if (home) {
|
|
57
|
+
roots.push({ path: path.join(home, ".agents", "skills"), scope: "user", source: "user-agents" });
|
|
58
|
+
}
|
|
59
|
+
if (codexHome) {
|
|
60
|
+
roots.push({ path: path.join(codexHome, "skills"), scope: "user", source: "user-codex" });
|
|
61
|
+
roots.push({ path: path.join(codexHome, "skills", ".system"), scope: "system", source: "codex-system" });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const root = path.resolve(String(repoRoot || repoRootFromHere()));
|
|
65
|
+
roots.push({ path: path.join(root, "SKILLS"), scope: "builtin", source: "ufoo" });
|
|
66
|
+
const modulesDir = path.join(root, "modules");
|
|
67
|
+
try {
|
|
68
|
+
for (const entry of fs.readdirSync(modulesDir, { withFileTypes: true })) {
|
|
69
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
70
|
+
roots.push({
|
|
71
|
+
path: path.join(modulesDir, entry.name, "SKILLS"),
|
|
72
|
+
scope: "builtin",
|
|
73
|
+
source: `ufoo-module:${entry.name}`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Modules are optional in tests and local installs.
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const seen = new Set();
|
|
81
|
+
return roots
|
|
82
|
+
.map((rootInfo) => ({ ...rootInfo, path: path.resolve(rootInfo.path) }))
|
|
83
|
+
.filter((rootInfo) => {
|
|
84
|
+
if (seen.has(rootInfo.path)) return false;
|
|
85
|
+
seen.add(rootInfo.path);
|
|
86
|
+
return true;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseSkillFile(filePath, rootInfo = {}) {
|
|
91
|
+
const skillPath = canonicalPath(filePath);
|
|
92
|
+
const dir = path.dirname(skillPath);
|
|
93
|
+
let parsed;
|
|
94
|
+
try {
|
|
95
|
+
parsed = matter(fs.readFileSync(skillPath, "utf8"));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
throw new Error(err && err.message ? err.message : "failed to read skill");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = parsed && parsed.data && typeof parsed.data === "object" ? parsed.data : {};
|
|
101
|
+
const name = String(data.name || "").trim();
|
|
102
|
+
const description = String(data.description || "").trim();
|
|
103
|
+
if (!name) throw new Error("missing required frontmatter field: name");
|
|
104
|
+
if (!description) throw new Error("missing required frontmatter field: description");
|
|
105
|
+
|
|
106
|
+
const metadata = data.metadata && typeof data.metadata === "object" ? data.metadata : {};
|
|
107
|
+
const shortDescription = String(
|
|
108
|
+
metadata["short-description"]
|
|
109
|
+
|| metadata.shortDescription
|
|
110
|
+
|| data["short-description"]
|
|
111
|
+
|| data.shortDescription
|
|
112
|
+
|| ""
|
|
113
|
+
).trim();
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
description,
|
|
118
|
+
shortDescription,
|
|
119
|
+
path: skillPath,
|
|
120
|
+
dir,
|
|
121
|
+
scope: rootInfo.scope || "repo",
|
|
122
|
+
source: rootInfo.source || "",
|
|
123
|
+
enabled: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function discoverSkillsUnderRoot(rootInfo = {}, options = {}) {
|
|
128
|
+
const root = path.resolve(String(rootInfo.path || ""));
|
|
129
|
+
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : DEFAULT_MAX_DEPTH;
|
|
130
|
+
const skills = [];
|
|
131
|
+
const errors = [];
|
|
132
|
+
if (!root || !isDirectory(root)) return { skills, errors };
|
|
133
|
+
|
|
134
|
+
const walk = (dir, depth) => {
|
|
135
|
+
if (depth > maxDepth) return;
|
|
136
|
+
const skillFile = path.join(dir, SKILL_FILE);
|
|
137
|
+
if (isFile(skillFile)) {
|
|
138
|
+
try {
|
|
139
|
+
skills.push(parseSkillFile(skillFile, rootInfo));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
errors.push({
|
|
142
|
+
path: canonicalPath(skillFile),
|
|
143
|
+
message: err && err.message ? err.message : "invalid skill",
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
let entries = [];
|
|
150
|
+
try {
|
|
151
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
152
|
+
} catch (err) {
|
|
153
|
+
errors.push({
|
|
154
|
+
path: canonicalPath(dir),
|
|
155
|
+
message: err && err.message ? err.message : "failed to read directory",
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!entry.isDirectory()) continue;
|
|
162
|
+
if (entry.name.startsWith(".")) continue;
|
|
163
|
+
walk(path.join(dir, entry.name), depth + 1);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
walk(root, 0);
|
|
168
|
+
return { skills, errors };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function listUcodeSkills(options = {}) {
|
|
172
|
+
const roots = Array.isArray(options.roots)
|
|
173
|
+
? options.roots
|
|
174
|
+
: defaultSkillRoots(options);
|
|
175
|
+
const seenPaths = new Set();
|
|
176
|
+
const skills = [];
|
|
177
|
+
const errors = [];
|
|
178
|
+
|
|
179
|
+
for (const rootInfo of roots) {
|
|
180
|
+
const discovered = discoverSkillsUnderRoot(rootInfo, options);
|
|
181
|
+
for (const err of discovered.errors) errors.push(err);
|
|
182
|
+
for (const skill of discovered.skills) {
|
|
183
|
+
const key = canonicalPath(skill.path);
|
|
184
|
+
if (seenPaths.has(key)) continue;
|
|
185
|
+
seenPaths.add(key);
|
|
186
|
+
skills.push(skill);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const scopeRank = { repo: 0, user: 1, builtin: 2, system: 3, admin: 4 };
|
|
191
|
+
skills.sort((a, b) => {
|
|
192
|
+
const rankA = scopeRank[a.scope] == null ? 9 : scopeRank[a.scope];
|
|
193
|
+
const rankB = scopeRank[b.scope] == null ? 9 : scopeRank[b.scope];
|
|
194
|
+
if (rankA !== rankB) return rankA - rankB;
|
|
195
|
+
if (a.name !== b.name) return a.name.localeCompare(b.name);
|
|
196
|
+
return a.path.localeCompare(b.path);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return { skills, errors };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function findSkillsByName(skills = [], name = "") {
|
|
203
|
+
const target = String(name || "").trim().toLowerCase();
|
|
204
|
+
if (!target) return [];
|
|
205
|
+
return (Array.isArray(skills) ? skills : [])
|
|
206
|
+
.filter((skill) => skill && skill.enabled !== false && String(skill.name || "").trim().toLowerCase() === target);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
SKILL_FILE,
|
|
211
|
+
DEFAULT_MAX_DEPTH,
|
|
212
|
+
canonicalPath,
|
|
213
|
+
defaultSkillRoots,
|
|
214
|
+
parseSkillFile,
|
|
215
|
+
discoverSkillsUnderRoot,
|
|
216
|
+
listUcodeSkills,
|
|
217
|
+
findSkillsByName,
|
|
218
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function renderSkillsSection(skills = []) {
|
|
2
|
+
const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
|
|
3
|
+
if (list.length === 0) return "";
|
|
4
|
+
|
|
5
|
+
const lines = [];
|
|
6
|
+
lines.push("## Skills");
|
|
7
|
+
lines.push("ufoo/ucode skills are built-in or local preset workflow capabilities discovered from SKILL.md files. The list below is for discovery and selection; it is not a private capability list for one agent, and the full skill body is loaded only when a user explicitly requests a skill.");
|
|
8
|
+
lines.push("### Available skills");
|
|
9
|
+
|
|
10
|
+
for (const skill of list) {
|
|
11
|
+
const pathText = String(skill.path || "").replace(/\\/g, "/");
|
|
12
|
+
const desc = String(skill.description || skill.shortDescription || "").trim();
|
|
13
|
+
lines.push(`- ${skill.name}: ${desc} (file: ${pathText})`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
lines.push("### How to use skills");
|
|
17
|
+
lines.push("- If the user names a skill with `$SkillName` or links directly to a `SKILL.md`, use that skill for this turn.");
|
|
18
|
+
lines.push("- Do not assume a skill applies just because it exists; match the user request to the listed skill descriptions.");
|
|
19
|
+
lines.push("- When a skill is selected, read only the specific skill body and nearby referenced files needed for the task.");
|
|
20
|
+
lines.push("- If a skill is ambiguous, missing, or unreadable, say so briefly and continue with the best fallback.");
|
|
21
|
+
|
|
22
|
+
return lines.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatSkillsList({ skills = [], errors = [] } = {}) {
|
|
26
|
+
const lines = [];
|
|
27
|
+
const list = (Array.isArray(skills) ? skills : []).filter((skill) => skill && skill.enabled !== false);
|
|
28
|
+
lines.push(`Available ufoo/ucode skills and preset workflows: ${list.length}`);
|
|
29
|
+
for (const skill of list) {
|
|
30
|
+
lines.push(`- ${skill.name}: ${skill.description} (${skill.scope}, ${skill.path})`);
|
|
31
|
+
}
|
|
32
|
+
const errs = Array.isArray(errors) ? errors : [];
|
|
33
|
+
if (errs.length > 0) {
|
|
34
|
+
lines.push("");
|
|
35
|
+
lines.push(`Skill load warnings: ${errs.length}`);
|
|
36
|
+
for (const err of errs) {
|
|
37
|
+
lines.push(`- ${err.path}: ${err.message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return lines.join("\n");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = {
|
|
44
|
+
renderSkillsSection,
|
|
45
|
+
formatSkillsList,
|
|
46
|
+
};
|