@lazyingart/agintiflow 0.20.47 → 0.20.49
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/docs/skillmesh.md +158 -0
- package/package.json +4 -2
- package/references/skill-mesh-sharing-design.md +1123 -0
- package/scripts/smoke-skillmesh.js +128 -0
- package/src/cli.js +14 -2
- package/src/i18n.js +1 -0
- package/src/interactive-cli.js +40 -0
- package/src/skill-library.js +66 -3
- package/src/skillmesh.js +1284 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
buildSkillPackFromMarkdown,
|
|
7
|
+
buildSkillMeshServiceUnit,
|
|
8
|
+
enableSkillMeshSkill,
|
|
9
|
+
installSkillPack,
|
|
10
|
+
listInstalledSkillMeshSkills,
|
|
11
|
+
loadSkillMeshConfig,
|
|
12
|
+
setSkillMeshMode,
|
|
13
|
+
startSkillMeshRelay,
|
|
14
|
+
submitSkillPack,
|
|
15
|
+
syncSkillMesh,
|
|
16
|
+
validateSkillPack,
|
|
17
|
+
} from "../src/skillmesh.js";
|
|
18
|
+
import { listSkills, selectSkillsForGoal } from "../src/skill-library.js";
|
|
19
|
+
|
|
20
|
+
function assert(condition, message) {
|
|
21
|
+
if (!condition) throw new Error(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "aginti-skillmesh-"));
|
|
25
|
+
process.env.AGINTIFLOW_HOME = path.join(tempRoot, "home");
|
|
26
|
+
|
|
27
|
+
const skillMarkdown = [
|
|
28
|
+
"---",
|
|
29
|
+
"id: mesh-android-screenshot",
|
|
30
|
+
"label: Mesh Android Screenshot",
|
|
31
|
+
"description: Capture Android emulator screenshots safely and save them in the current workspace.",
|
|
32
|
+
"triggers:",
|
|
33
|
+
" - android screenshot",
|
|
34
|
+
" - emulator capture",
|
|
35
|
+
"tools:",
|
|
36
|
+
" - shell",
|
|
37
|
+
"---",
|
|
38
|
+
"",
|
|
39
|
+
"When asked to capture an Android emulator screenshot, inspect available devices, run `adb exec-out screencap -p`,",
|
|
40
|
+
"save to a descriptive workspace path, then verify the PNG exists before reporting success.",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n");
|
|
43
|
+
|
|
44
|
+
const pack = await buildSkillPackFromMarkdown(skillMarkdown, { valueScore: 95 });
|
|
45
|
+
const validation = validateSkillPack(pack);
|
|
46
|
+
assert(validation.packHash === pack.packHash, "pack hash should validate");
|
|
47
|
+
|
|
48
|
+
const installedDisabled = await installSkillPack(pack, { enabled: false });
|
|
49
|
+
assert(installedDisabled.installedSkills.includes("mesh-android-screenshot"), "pack did not install skill");
|
|
50
|
+
assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "disabled mesh skill should not load");
|
|
51
|
+
|
|
52
|
+
await enableSkillMeshSkill("mesh-android-screenshot", true);
|
|
53
|
+
assert(listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "enabled mesh skill should load");
|
|
54
|
+
assert(
|
|
55
|
+
selectSkillsForGoal("please take an android emulator screenshot", { limit: 6 }).some((skill) => skill.id === "mesh-android-screenshot"),
|
|
56
|
+
"enabled mesh skill should be selectable"
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const installed = await listInstalledSkillMeshSkills();
|
|
60
|
+
assert(installed.some((skill) => skill.id === "mesh-android-screenshot"), "installed mesh skill should list");
|
|
61
|
+
|
|
62
|
+
await setSkillMeshMode("off");
|
|
63
|
+
assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "off mode should suppress mesh skills");
|
|
64
|
+
await setSkillMeshMode("share");
|
|
65
|
+
const config = await loadSkillMeshConfig();
|
|
66
|
+
assert(config.mode === "share", "share mode should persist");
|
|
67
|
+
const serviceUnit = buildSkillMeshServiceUnit({
|
|
68
|
+
dataDir: path.join(tempRoot, "relay-service"),
|
|
69
|
+
runScript: path.join(tempRoot, "relay-service", "aginti-skill-relay.run.sh"),
|
|
70
|
+
scope: "system",
|
|
71
|
+
user: "aginti-test",
|
|
72
|
+
});
|
|
73
|
+
assert(serviceUnit.includes("Restart=on-failure"), "systemd unit should restart relay");
|
|
74
|
+
assert(serviceUnit.includes("WantedBy=multi-user.target"), "systemd unit should start at boot in system scope");
|
|
75
|
+
assert(serviceUnit.includes("ExecStart="), "systemd unit should include ExecStart");
|
|
76
|
+
|
|
77
|
+
const secretMarkdown = skillMarkdown.replace("mesh-android-screenshot", "mesh-secret-test") + "\nOPENAI_API_KEY=sk-thisShouldBeRejected1234567890\n";
|
|
78
|
+
let rejected = false;
|
|
79
|
+
try {
|
|
80
|
+
await buildSkillPackFromMarkdown(secretMarkdown);
|
|
81
|
+
} catch {
|
|
82
|
+
rejected = true;
|
|
83
|
+
}
|
|
84
|
+
assert(rejected, "secret-like skill pack should be rejected");
|
|
85
|
+
|
|
86
|
+
const relay = await startSkillMeshRelay({ host: "127.0.0.1", port: 0, dataDir: path.join(tempRoot, "relay") });
|
|
87
|
+
try {
|
|
88
|
+
const submit = await submitSkillPack(pack, relay.url);
|
|
89
|
+
assert(submit.ok && submit.packHash === pack.packHash, "relay submit failed");
|
|
90
|
+
const health = await fetch(`${relay.url}/health`).then((response) => response.json());
|
|
91
|
+
assert(health.ok && health.acceptsRawSessions === false, "relay health should advertise safe contract");
|
|
92
|
+
const feed = await fetch(`${relay.url}/feed.json`).then((response) => response.json());
|
|
93
|
+
assert(feed.packs.some((item) => item.packHash === pack.packHash), "relay feed should include submitted pack");
|
|
94
|
+
const nodes = await fetch(`${relay.url}/nodes.json`).then((response) => response.json());
|
|
95
|
+
assert(Array.isArray(nodes.nodes), "relay should expose a node list");
|
|
96
|
+
|
|
97
|
+
process.env.AGINTIFLOW_HOME = path.join(tempRoot, "client-home");
|
|
98
|
+
const sync = await syncSkillMesh({ nodeUrl: relay.url, install: true });
|
|
99
|
+
assert(sync.ok && sync.downloaded.length === 1, "client sync should download one pack");
|
|
100
|
+
assert(!listSkills({ includeBody: false }).some((skill) => skill.id === "mesh-android-screenshot"), "synced community skill should install disabled");
|
|
101
|
+
} finally {
|
|
102
|
+
await relay.close();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
process.env.AGINTIFLOW_HOME = path.join(tempRoot, "unreachable-home");
|
|
106
|
+
const unreachable = await syncSkillMesh({ nodeUrl: "http://127.0.0.1:9" });
|
|
107
|
+
assert(unreachable.ok && unreachable.skipped && unreachable.unreachable, "unreachable sync should fail soft");
|
|
108
|
+
|
|
109
|
+
console.log(
|
|
110
|
+
JSON.stringify(
|
|
111
|
+
{
|
|
112
|
+
ok: true,
|
|
113
|
+
checks: [
|
|
114
|
+
"signed-pack",
|
|
115
|
+
"secret-rejection",
|
|
116
|
+
"install-disabled-by-default",
|
|
117
|
+
"enable-mesh-skill",
|
|
118
|
+
"relay-submit-feed",
|
|
119
|
+
"relay-node-list",
|
|
120
|
+
"metadata-sync",
|
|
121
|
+
"soft-unreachable-node",
|
|
122
|
+
"systemd-service-unit",
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
null,
|
|
126
|
+
2
|
|
127
|
+
)
|
|
128
|
+
);
|
package/src/cli.js
CHANGED
|
@@ -33,6 +33,7 @@ import { listSkills, selectSkillsForGoal } from "./skill-library.js";
|
|
|
33
33
|
import { languageLabel, resolveLanguage } from "./i18n.js";
|
|
34
34
|
import { maybeAutoUpdate } from "./auto-update.js";
|
|
35
35
|
import { readHousekeepingSummary } from "./housekeeping.js";
|
|
36
|
+
import { handleSkillMeshCommand } from "./skillmesh.js";
|
|
36
37
|
import fs from "node:fs/promises";
|
|
37
38
|
import path from "node:path";
|
|
38
39
|
import { fileURLToPath } from "node:url";
|
|
@@ -382,7 +383,7 @@ export function parseArgs(argv) {
|
|
|
382
383
|
|
|
383
384
|
function printUsage() {
|
|
384
385
|
console.log(
|
|
385
|
-
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
386
|
+
'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti skillmesh [status|off|record|share|sync|serve|service] OR aginti housekeeping [--json] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
|
|
386
387
|
);
|
|
387
388
|
console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
|
|
388
389
|
}
|
|
@@ -496,7 +497,8 @@ function printSkills(query = "") {
|
|
|
496
497
|
for (const skill of skills) {
|
|
497
498
|
const triggers = skill.triggers?.length ? ` triggers=${skill.triggers.join(",")}` : "";
|
|
498
499
|
const tools = skill.tools?.length ? ` tools=${skill.tools.join(",")}` : "";
|
|
499
|
-
|
|
500
|
+
const source = skill.source && skill.source !== "built-in" ? ` source=${skill.source}` : "";
|
|
501
|
+
console.log(`${skill.id}: ${skill.label} - ${skill.description}${triggers}${tools}${source}`);
|
|
500
502
|
}
|
|
501
503
|
}
|
|
502
504
|
|
|
@@ -1091,6 +1093,16 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
1091
1093
|
return;
|
|
1092
1094
|
}
|
|
1093
1095
|
|
|
1096
|
+
if (argv[0] === "skillmesh" || argv[0] === "skillsync" || argv[0] === "skill-sync" || argv[0] === "skill-share") {
|
|
1097
|
+
try {
|
|
1098
|
+
await handleSkillMeshCommand(argv.slice(1));
|
|
1099
|
+
} catch (error) {
|
|
1100
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1101
|
+
process.exit(1);
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1094
1106
|
if (argv[0] === "keys/status") {
|
|
1095
1107
|
await handleKeyCommand(["status"]);
|
|
1096
1108
|
return;
|
package/src/i18n.js
CHANGED
|
@@ -134,6 +134,7 @@ const TRANSLATIONS = {
|
|
|
134
134
|
helpResume: "Continue a saved session.",
|
|
135
135
|
helpSessions: "List recent sessions in this project.",
|
|
136
136
|
helpSkills: "List Markdown skills selected for a topic.",
|
|
137
|
+
helpSkillMesh: "Manage strict reviewed skill sharing.",
|
|
137
138
|
helpProfile: "Set task profile, e.g. code, website, latex, maintenance.",
|
|
138
139
|
helpWebSearch: "Enable or disable the web_search tool.",
|
|
139
140
|
helpScouts: "Enable parallel DeepSeek scouts and set scout count.",
|
package/src/interactive-cli.js
CHANGED
|
@@ -26,6 +26,13 @@ import {
|
|
|
26
26
|
modelsForProviderGroup,
|
|
27
27
|
} from "./model-routing.js";
|
|
28
28
|
import { languageLabel, resolveLanguage, t } from "./i18n.js";
|
|
29
|
+
import {
|
|
30
|
+
formatSkillMeshStatus,
|
|
31
|
+
handleSkillMeshCommand,
|
|
32
|
+
loadSkillMeshConfig,
|
|
33
|
+
setSkillMeshMode,
|
|
34
|
+
skillMeshModeChoices,
|
|
35
|
+
} from "./skillmesh.js";
|
|
29
36
|
|
|
30
37
|
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
31
38
|
const ansi = {
|
|
@@ -73,6 +80,8 @@ const SLASH_COMMANDS = [
|
|
|
73
80
|
"/rename",
|
|
74
81
|
"/skills",
|
|
75
82
|
"/skill",
|
|
83
|
+
"/skillmesh",
|
|
84
|
+
"/skillsync",
|
|
76
85
|
"/profile",
|
|
77
86
|
"/web-search",
|
|
78
87
|
"/scouts",
|
|
@@ -649,6 +658,7 @@ function printHelp() {
|
|
|
649
658
|
` ${command("/rename [title|auto]", "Rename the current session.", "helpRename")}`,
|
|
650
659
|
` ${command("/sessions", "List recent sessions in this project.", "helpSessions")}`,
|
|
651
660
|
` ${command("/skills [query]", "List Markdown skills selected for a topic.", "helpSkills")}`,
|
|
661
|
+
` ${command("/skillmesh [status|off|record|share|sync]", "Manage strict reviewed skill sharing.", "helpSkillMesh")}`,
|
|
652
662
|
` ${command("/profile <name>", "Set task profile, e.g. code, website, latex, maintenance.", "helpProfile")}`,
|
|
653
663
|
` ${command("/web-search on|off", "Enable or disable the web_search tool.", "helpWebSearch")}`,
|
|
654
664
|
` ${command("/scouts on|off|<1-10>", "Enable parallel DeepSeek scouts and set scout count.", "helpScouts")}`,
|
|
@@ -2707,6 +2717,36 @@ async function handleCommand(line, state, packageDir) {
|
|
|
2707
2717
|
);
|
|
2708
2718
|
return true;
|
|
2709
2719
|
}
|
|
2720
|
+
if (command === "skillmesh" || command === "skillsync") {
|
|
2721
|
+
if (!value && input.isTTY && output.isTTY && typeof input.setRawMode === "function") {
|
|
2722
|
+
const config = await loadSkillMeshConfig();
|
|
2723
|
+
const options = skillMeshModeChoices();
|
|
2724
|
+
const selected = await selectModelChoice({
|
|
2725
|
+
title: "Skill Mesh",
|
|
2726
|
+
subtitle: "Strict skill sharing. Up/Down selects, Enter confirms, Esc cancels.",
|
|
2727
|
+
options,
|
|
2728
|
+
initialIndex: Math.max(
|
|
2729
|
+
options.findIndex((item) => item.value === config.mode),
|
|
2730
|
+
0
|
|
2731
|
+
),
|
|
2732
|
+
escapeValue: null,
|
|
2733
|
+
});
|
|
2734
|
+
if (!selected) {
|
|
2735
|
+
printSystemLine(`skillmesh=${config.mode}`);
|
|
2736
|
+
return true;
|
|
2737
|
+
}
|
|
2738
|
+
const next = await setSkillMeshMode(selected.value);
|
|
2739
|
+
printSystemLine(`skillmesh=${next.mode}`);
|
|
2740
|
+
printAgentMessage(formatSkillMeshStatus(next));
|
|
2741
|
+
return true;
|
|
2742
|
+
}
|
|
2743
|
+
try {
|
|
2744
|
+
await handleSkillMeshCommand(value ? value.split(/\s+/).filter(Boolean) : ["status"]);
|
|
2745
|
+
} catch (error) {
|
|
2746
|
+
printAgentMessage(error instanceof Error ? error.message : String(error));
|
|
2747
|
+
}
|
|
2748
|
+
return true;
|
|
2749
|
+
}
|
|
2710
2750
|
if (command === "profile") {
|
|
2711
2751
|
state.taskProfile = normalizeTaskProfile(value || "auto");
|
|
2712
2752
|
state.maxSteps = Math.max(state.maxSteps, defaultMaxStepsForProfile(state.taskProfile));
|
package/src/skill-library.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { agintiflowHome } from "./session-index.js";
|
|
4
5
|
|
|
5
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
7
|
const DEFAULT_SKILLS_DIR = path.resolve(__dirname, "..", "skills");
|
|
@@ -17,7 +18,7 @@ function parseScalar(value = "") {
|
|
|
17
18
|
return trimmed;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function parseFrontmatter(text, filePath) {
|
|
21
|
+
export function parseFrontmatter(text, filePath) {
|
|
21
22
|
const match = String(text || "").match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
22
23
|
if (!match) throw new Error(`${filePath}: missing YAML frontmatter`);
|
|
23
24
|
const meta = {};
|
|
@@ -55,7 +56,7 @@ function normalizeList(value) {
|
|
|
55
56
|
.filter(Boolean);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
function loadSkillFile(filePath) {
|
|
59
|
+
export function loadSkillFile(filePath) {
|
|
59
60
|
const text = fs.readFileSync(filePath, "utf8");
|
|
60
61
|
const { meta, body } = parseFrontmatter(text, filePath);
|
|
61
62
|
for (const field of ["id", "label", "description"]) {
|
|
@@ -74,7 +75,7 @@ function loadSkillFile(filePath) {
|
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
function loadSkillsFromDir({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR, source = "built-in" } = {}) {
|
|
78
79
|
let dirEntries = [];
|
|
79
80
|
try {
|
|
80
81
|
dirEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
@@ -88,11 +89,73 @@ export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR
|
|
|
88
89
|
try {
|
|
89
90
|
const skill = loadSkillFile(skillPath);
|
|
90
91
|
if (!includeBody) delete skill.body;
|
|
92
|
+
skill.source = source;
|
|
91
93
|
skills.push(skill);
|
|
92
94
|
} catch {
|
|
93
95
|
// Invalid local skill files are skipped so one bad skill does not break the agent.
|
|
94
96
|
}
|
|
95
97
|
}
|
|
98
|
+
return skills;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function skillMeshSkillsDir() {
|
|
102
|
+
return path.join(agintiflowHome(), "skillmesh", "skills");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function skillMeshEnabled() {
|
|
106
|
+
try {
|
|
107
|
+
const configPath = path.join(agintiflowHome(), "skillmesh", "config.json");
|
|
108
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
109
|
+
return String(config.mode || "share") !== "off";
|
|
110
|
+
} catch {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadEnabledSkillMeshSkills({ includeBody = false } = {}) {
|
|
116
|
+
if (!skillMeshEnabled()) return [];
|
|
117
|
+
const skillsDir = skillMeshSkillsDir();
|
|
118
|
+
let dirEntries = [];
|
|
119
|
+
try {
|
|
120
|
+
dirEntries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
121
|
+
} catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const skills = [];
|
|
125
|
+
for (const entry of dirEntries) {
|
|
126
|
+
if (!entry.isDirectory()) continue;
|
|
127
|
+
const skillDir = path.join(skillsDir, entry.name);
|
|
128
|
+
const metadataPath = path.join(skillDir, "skillmesh.json");
|
|
129
|
+
try {
|
|
130
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
131
|
+
if (!metadata.enabled) continue;
|
|
132
|
+
const skill = loadSkillFile(path.join(skillDir, "SKILL.md"));
|
|
133
|
+
if (!includeBody) delete skill.body;
|
|
134
|
+
skill.source = "skillmesh";
|
|
135
|
+
skill.skillmesh = {
|
|
136
|
+
trustLevel: metadata.trustLevel || "community-reviewed",
|
|
137
|
+
packHash: metadata.packHash || "",
|
|
138
|
+
sourceFeed: metadata.sourceFeed || "",
|
|
139
|
+
};
|
|
140
|
+
skills.push(skill);
|
|
141
|
+
} catch {
|
|
142
|
+
// Invalid or unreviewed imported skills stay inert.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return skills;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function listSkills({ includeBody = false, skillsDir = DEFAULT_SKILLS_DIR, includeSkillMesh = true } = {}) {
|
|
149
|
+
const skills = loadSkillsFromDir({ includeBody, skillsDir, source: skillsDir === DEFAULT_SKILLS_DIR ? "built-in" : "local" });
|
|
150
|
+
if (includeSkillMesh && skillsDir === DEFAULT_SKILLS_DIR) {
|
|
151
|
+
const existing = new Set(skills.map((skill) => skill.id));
|
|
152
|
+
for (const skill of loadEnabledSkillMeshSkills({ includeBody })) {
|
|
153
|
+
if (!existing.has(skill.id)) {
|
|
154
|
+
existing.add(skill.id);
|
|
155
|
+
skills.push(skill);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
96
159
|
return skills.sort((a, b) => a.id.localeCompare(b.id));
|
|
97
160
|
}
|
|
98
161
|
|