opencode-dynamic-skills 1.0.2 → 1.2.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/README.md +58 -19
- package/README.zh-CN.md +53 -19
- package/dist/api.d.ts +21 -61
- package/dist/commands/SlashCommand.d.ts +1 -2
- package/dist/config.d.ts +21 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +840 -598
- package/dist/lib/SkillFs.d.ts +1 -0
- package/dist/lib/formatLoadedSkill.d.ts +6 -0
- package/dist/services/Notifier.d.ts +13 -0
- package/dist/services/Notifier.test.d.ts +1 -0
- package/dist/tools/Skill.d.ts +3 -0
- package/dist/tools/Skill.test.d.ts +1 -0
- package/dist/tools/SkillRecommender.d.ts +25 -8
- package/dist/types.d.ts +17 -19
- package/package.json +1 -1
- package/dist/lib/createPromptRenderer.d.ts +0 -52
- package/dist/lib/createPromptRenderer.test.d.ts +0 -9
- package/dist/lib/getModelFormat.d.ts +0 -35
- package/dist/lib/renderers/JsonPromptRenderer.d.ts +0 -8
- package/dist/lib/renderers/JsonPromptRenderer.test.d.ts +0 -10
- package/dist/lib/renderers/MdPromptRenderer.d.ts +0 -18
- package/dist/lib/renderers/MdPromptRenderer.test.d.ts +0 -11
- package/dist/services/MessageModelIdAccountant.d.ts +0 -22
package/dist/index.js
CHANGED
|
@@ -15889,7 +15889,7 @@ function toolName(skillPath) {
|
|
|
15889
15889
|
|
|
15890
15890
|
// src/lib/SkillFs.ts
|
|
15891
15891
|
import { join } from "path";
|
|
15892
|
-
import { existsSync } from "fs";
|
|
15892
|
+
import { existsSync, statSync } from "fs";
|
|
15893
15893
|
|
|
15894
15894
|
// node_modules/mime/dist/types/other.js
|
|
15895
15895
|
var types = {
|
|
@@ -17075,13 +17075,24 @@ var Mime_default = Mime;
|
|
|
17075
17075
|
var src_default = new Mime_default(standard_default, other_default)._freeze();
|
|
17076
17076
|
|
|
17077
17077
|
// src/lib/SkillFs.ts
|
|
17078
|
+
function isRegularFile(filePath) {
|
|
17079
|
+
try {
|
|
17080
|
+
return statSync(filePath).isFile();
|
|
17081
|
+
} catch {
|
|
17082
|
+
return false;
|
|
17083
|
+
}
|
|
17084
|
+
}
|
|
17078
17085
|
var readSkillFile = async (path) => {
|
|
17079
17086
|
const file2 = Bun.file(path);
|
|
17080
17087
|
return file2.text();
|
|
17081
17088
|
};
|
|
17082
17089
|
var listSkillFiles = (skillPath, subdirectory) => {
|
|
17083
17090
|
const glob = new Bun.Glob(join(subdirectory, "**", "*"));
|
|
17084
|
-
return Array.from(glob.scanSync({ cwd: skillPath, absolute: true }));
|
|
17091
|
+
return Array.from(glob.scanSync({ cwd: skillPath, absolute: true })).filter(isRegularFile);
|
|
17092
|
+
};
|
|
17093
|
+
var listAllSkillFiles = (skillPath) => {
|
|
17094
|
+
const glob = new Bun.Glob("**/*");
|
|
17095
|
+
return Array.from(glob.scanSync({ cwd: skillPath, absolute: true })).filter(isRegularFile).filter((filePath) => !filePath.endsWith("/SKILL.md") && !filePath.endsWith("\\SKILL.md"));
|
|
17085
17096
|
};
|
|
17086
17097
|
var findSkillPaths = async (basePath) => {
|
|
17087
17098
|
const glob = new Bun.Glob("**/SKILL.md");
|
|
@@ -17682,6 +17693,7 @@ async function parseSkill(args) {
|
|
|
17682
17693
|
const scriptPaths = listSkillFiles(skillFullPath, "scripts");
|
|
17683
17694
|
const referencePaths = listSkillFiles(skillFullPath, "references");
|
|
17684
17695
|
const assetPaths = listSkillFiles(skillFullPath, "assets");
|
|
17696
|
+
const indexedFilePaths = listAllSkillFiles(skillFullPath);
|
|
17685
17697
|
return {
|
|
17686
17698
|
allowedTools: frontmatter.data["allowed-tools"],
|
|
17687
17699
|
compatibility: frontmatter.data.compatibility,
|
|
@@ -17693,6 +17705,7 @@ async function parseSkill(args) {
|
|
|
17693
17705
|
metadata: frontmatter.data.metadata,
|
|
17694
17706
|
name: frontmatter.data.name,
|
|
17695
17707
|
path: args.skillPath,
|
|
17708
|
+
files: createSkillResourceMap(skillFullPath, indexedFilePaths),
|
|
17696
17709
|
scripts: createSkillResourceMap(skillFullPath, scriptPaths),
|
|
17697
17710
|
references: createSkillResourceMap(skillFullPath, referencePaths),
|
|
17698
17711
|
assets: createSkillResourceMap(skillFullPath, assetPaths)
|
|
@@ -17735,6 +17748,9 @@ function createSkillFinder(provider) {
|
|
|
17735
17748
|
// src/tools/SkillRecommender.ts
|
|
17736
17749
|
var DEFAULT_RECOMMENDATION_LIMIT = 5;
|
|
17737
17750
|
var MIN_TERM_LENGTH = 3;
|
|
17751
|
+
function normalizeSkillSelector(selector) {
|
|
17752
|
+
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
17753
|
+
}
|
|
17738
17754
|
function getRecommendationReason(nameMatches, descMatches) {
|
|
17739
17755
|
const reasons = [];
|
|
17740
17756
|
if (nameMatches > 0) {
|
|
@@ -17748,46 +17764,190 @@ function getRecommendationReason(nameMatches, descMatches) {
|
|
|
17748
17764
|
}
|
|
17749
17765
|
return reasons.join("; ");
|
|
17750
17766
|
}
|
|
17751
|
-
function
|
|
17767
|
+
function formatResults(provider, task, recommendations) {
|
|
17768
|
+
return {
|
|
17769
|
+
mode: "recommend",
|
|
17770
|
+
query: task,
|
|
17771
|
+
skills: recommendations.map(({ name, description }) => ({
|
|
17772
|
+
name,
|
|
17773
|
+
description
|
|
17774
|
+
})),
|
|
17775
|
+
recommendations,
|
|
17776
|
+
guidance: recommendations.length > 0 ? "Use skill_use to load one of the recommended skills if you want to apply it." : "No strong skill recommendation was found. Try skill_find with a narrower query.",
|
|
17777
|
+
summary: {
|
|
17778
|
+
total: provider.controller.skills.length,
|
|
17779
|
+
matches: recommendations.length,
|
|
17780
|
+
feedback: recommendations.length > 0 ? `Recommended ${recommendations.length} skill(s) for: **${task}**` : `No strong recommendations found for: **${task}**`
|
|
17781
|
+
},
|
|
17782
|
+
debug: provider.debug
|
|
17783
|
+
};
|
|
17784
|
+
}
|
|
17785
|
+
function getHeuristicRecommendations(provider, task, limit) {
|
|
17786
|
+
const parsedQuery = parseQuery(task);
|
|
17787
|
+
const rankingTerms = parsedQuery.include.filter((term) => term.length >= MIN_TERM_LENGTH);
|
|
17788
|
+
const rankedMatches = provider.controller.skills.map((skill) => rankSkill(skill, rankingTerms)).filter((ranking) => ranking.totalScore > 0).sort((left, right) => {
|
|
17789
|
+
if (right.totalScore !== left.totalScore) {
|
|
17790
|
+
return right.totalScore - left.totalScore;
|
|
17791
|
+
}
|
|
17792
|
+
if (right.nameMatches !== left.nameMatches) {
|
|
17793
|
+
return right.nameMatches - left.nameMatches;
|
|
17794
|
+
}
|
|
17795
|
+
return left.skill.name.localeCompare(right.skill.name);
|
|
17796
|
+
});
|
|
17797
|
+
return rankedMatches.slice(0, limit).map((skill) => ({
|
|
17798
|
+
name: skill.skill.toolName,
|
|
17799
|
+
description: skill.skill.description,
|
|
17800
|
+
score: skill.totalScore,
|
|
17801
|
+
reason: getRecommendationReason(skill.nameMatches, skill.descMatches)
|
|
17802
|
+
}));
|
|
17803
|
+
}
|
|
17804
|
+
function buildModelRecommendationPrompt(args) {
|
|
17805
|
+
const catalog = args.skills.map((skill) => `- ${skill.toolName}: ${skill.description} (directory=${skill.name}, aliases=/${skill.name})`).join(`
|
|
17806
|
+
`);
|
|
17807
|
+
return [
|
|
17808
|
+
`User request: ${args.task}`,
|
|
17809
|
+
`Maximum recommendations: ${args.limit}`,
|
|
17810
|
+
"Available skills:",
|
|
17811
|
+
catalog,
|
|
17812
|
+
"",
|
|
17813
|
+
"Return strict JSON only. Do not include markdown fences unless required by the model."
|
|
17814
|
+
].join(`
|
|
17815
|
+
`);
|
|
17816
|
+
}
|
|
17817
|
+
function extractJsonPayload(text) {
|
|
17818
|
+
const fencedMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
17819
|
+
if (fencedMatch?.[1]) {
|
|
17820
|
+
return fencedMatch[1].trim();
|
|
17821
|
+
}
|
|
17822
|
+
const firstBrace = text.indexOf("{");
|
|
17823
|
+
const lastBrace = text.lastIndexOf("}");
|
|
17824
|
+
if (firstBrace >= 0 && lastBrace > firstBrace) {
|
|
17825
|
+
return text.slice(firstBrace, lastBrace + 1);
|
|
17826
|
+
}
|
|
17827
|
+
throw new Error("Model response did not contain a JSON object");
|
|
17828
|
+
}
|
|
17829
|
+
function parseModelRecommendations(text) {
|
|
17830
|
+
const payload = JSON.parse(extractJsonPayload(text));
|
|
17831
|
+
if (!Array.isArray(payload.recommendations)) {
|
|
17832
|
+
throw new Error("Model response is missing a recommendations array");
|
|
17833
|
+
}
|
|
17834
|
+
return payload.recommendations.filter((recommendation) => recommendation.name?.trim()).map((recommendation) => ({
|
|
17835
|
+
name: recommendation.name.trim(),
|
|
17836
|
+
reason: recommendation.reason?.trim()
|
|
17837
|
+
}));
|
|
17838
|
+
}
|
|
17839
|
+
function getAssistantText(messages) {
|
|
17840
|
+
const assistantMessage = [...messages].reverse().find((message) => message.info.role === "assistant");
|
|
17841
|
+
if (!assistantMessage) {
|
|
17842
|
+
throw new Error("Model session did not return an assistant message");
|
|
17843
|
+
}
|
|
17844
|
+
const text = assistantMessage.parts.filter((part) => part.type === "text").map((part) => part.text ?? "").join(`
|
|
17845
|
+
`).trim();
|
|
17846
|
+
if (!text) {
|
|
17847
|
+
throw new Error("Model session did not return assistant text");
|
|
17848
|
+
}
|
|
17849
|
+
return text;
|
|
17850
|
+
}
|
|
17851
|
+
function mapModelRecommendationsToSkills(skills, recommendations, limit) {
|
|
17852
|
+
const skillByName = new Map;
|
|
17853
|
+
for (const skill of skills) {
|
|
17854
|
+
skillByName.set(normalizeSkillSelector(skill.toolName), skill);
|
|
17855
|
+
skillByName.set(normalizeSkillSelector(skill.name), skill);
|
|
17856
|
+
}
|
|
17857
|
+
const results = [];
|
|
17858
|
+
const seen = new Set;
|
|
17859
|
+
for (const [index, recommendation] of recommendations.entries()) {
|
|
17860
|
+
const skill = skillByName.get(normalizeSkillSelector(recommendation.name));
|
|
17861
|
+
if (!skill || seen.has(skill.toolName)) {
|
|
17862
|
+
continue;
|
|
17863
|
+
}
|
|
17864
|
+
seen.add(skill.toolName);
|
|
17865
|
+
results.push({
|
|
17866
|
+
name: skill.toolName,
|
|
17867
|
+
description: skill.description,
|
|
17868
|
+
score: Math.max(limit - index, 1),
|
|
17869
|
+
reason: recommendation.reason || "Recommended by the configured skill_recommend model"
|
|
17870
|
+
});
|
|
17871
|
+
if (results.length >= limit) {
|
|
17872
|
+
break;
|
|
17873
|
+
}
|
|
17874
|
+
}
|
|
17875
|
+
return results;
|
|
17876
|
+
}
|
|
17877
|
+
function shouldUseModel(config2, client) {
|
|
17878
|
+
return config2.strategy === "model" && Boolean(client) && config2.model.includes("/");
|
|
17879
|
+
}
|
|
17880
|
+
function parseConfiguredModel(model) {
|
|
17881
|
+
const trimmedModel = model.trim();
|
|
17882
|
+
const separatorIndex = trimmedModel.indexOf("/");
|
|
17883
|
+
if (separatorIndex <= 0 || separatorIndex === trimmedModel.length - 1) {
|
|
17884
|
+
throw new Error(`Invalid skillRecommend.model: ${model}`);
|
|
17885
|
+
}
|
|
17886
|
+
return {
|
|
17887
|
+
providerID: trimmedModel.slice(0, separatorIndex).trim(),
|
|
17888
|
+
modelID: trimmedModel.slice(separatorIndex + 1).trim()
|
|
17889
|
+
};
|
|
17890
|
+
}
|
|
17891
|
+
function unwrapClientData(response, action) {
|
|
17892
|
+
if (response.error) {
|
|
17893
|
+
const message = response.error instanceof Error ? response.error.message : String(response.error);
|
|
17894
|
+
throw new Error(`${action} failed: ${message}`);
|
|
17895
|
+
}
|
|
17896
|
+
if (response.data === undefined) {
|
|
17897
|
+
throw new Error(`${action} failed: missing response data`);
|
|
17898
|
+
}
|
|
17899
|
+
return response.data;
|
|
17900
|
+
}
|
|
17901
|
+
function getModelSessionClient(client) {
|
|
17902
|
+
return client;
|
|
17903
|
+
}
|
|
17904
|
+
async function getModelRecommendations(provider, options2, task, limit) {
|
|
17905
|
+
if (!options2.client) {
|
|
17906
|
+
throw new Error("Model-based recommendation requires an OpenCode client");
|
|
17907
|
+
}
|
|
17908
|
+
const client = getModelSessionClient(options2.client);
|
|
17909
|
+
const selectedModel = parseConfiguredModel(options2.config.model);
|
|
17910
|
+
const session = unwrapClientData(await client.session.create(), "session.create");
|
|
17911
|
+
try {
|
|
17912
|
+
unwrapClientData(await client.session.chat(session.id, {
|
|
17913
|
+
providerID: selectedModel.providerID,
|
|
17914
|
+
modelID: selectedModel.modelID,
|
|
17915
|
+
parts: [
|
|
17916
|
+
{
|
|
17917
|
+
type: "text",
|
|
17918
|
+
text: buildModelRecommendationPrompt({
|
|
17919
|
+
task,
|
|
17920
|
+
limit,
|
|
17921
|
+
skills: provider.controller.skills
|
|
17922
|
+
})
|
|
17923
|
+
}
|
|
17924
|
+
],
|
|
17925
|
+
system: options2.config.systemPrompt,
|
|
17926
|
+
tools: {}
|
|
17927
|
+
}), "session.chat");
|
|
17928
|
+
const messages = unwrapClientData(await client.session.messages(session.id), "session.messages");
|
|
17929
|
+
const modelOutput = getAssistantText(messages);
|
|
17930
|
+
const parsedRecommendations = parseModelRecommendations(modelOutput);
|
|
17931
|
+
return mapModelRecommendationsToSkills(provider.controller.skills, parsedRecommendations, limit);
|
|
17932
|
+
} finally {
|
|
17933
|
+
await client.session.delete(session.id).catch(() => {
|
|
17934
|
+
return;
|
|
17935
|
+
});
|
|
17936
|
+
}
|
|
17937
|
+
}
|
|
17938
|
+
function createSkillRecommender(provider, options2) {
|
|
17752
17939
|
return async (args) => {
|
|
17753
17940
|
await provider.controller.ready.whenReady();
|
|
17754
17941
|
const limit = Math.max(1, Math.min(args.limit ?? DEFAULT_RECOMMENDATION_LIMIT, 10));
|
|
17755
|
-
|
|
17756
|
-
|
|
17757
|
-
|
|
17758
|
-
|
|
17759
|
-
|
|
17760
|
-
|
|
17761
|
-
if (right.nameMatches !== left.nameMatches) {
|
|
17762
|
-
return right.nameMatches - left.nameMatches;
|
|
17942
|
+
if (shouldUseModel(options2.config, options2.client)) {
|
|
17943
|
+
try {
|
|
17944
|
+
const modelRecommendations = await getModelRecommendations(provider, options2, args.task, limit);
|
|
17945
|
+
return formatResults(provider, args.task, modelRecommendations);
|
|
17946
|
+
} catch (error45) {
|
|
17947
|
+
provider.logger.warn("Model-based skill_recommend failed; falling back to heuristic ranking.", error45);
|
|
17763
17948
|
}
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
const topMatches = rankedMatches.slice(0, limit);
|
|
17767
|
-
const recommendations = topMatches.map((skill) => {
|
|
17768
|
-
return {
|
|
17769
|
-
name: skill.skill.toolName,
|
|
17770
|
-
description: skill.skill.description,
|
|
17771
|
-
score: skill.totalScore,
|
|
17772
|
-
reason: getRecommendationReason(skill.nameMatches, skill.descMatches)
|
|
17773
|
-
};
|
|
17774
|
-
});
|
|
17775
|
-
return {
|
|
17776
|
-
mode: "recommend",
|
|
17777
|
-
query: args.task,
|
|
17778
|
-
skills: recommendations.map(({ name, description }) => ({
|
|
17779
|
-
name,
|
|
17780
|
-
description
|
|
17781
|
-
})),
|
|
17782
|
-
recommendations,
|
|
17783
|
-
guidance: recommendations.length > 0 ? "Use skill_use to load one of the recommended skills if you want to apply it." : "No strong skill recommendation was found. Try skill_find with a narrower query.",
|
|
17784
|
-
summary: {
|
|
17785
|
-
total: provider.controller.skills.length,
|
|
17786
|
-
matches: recommendations.length,
|
|
17787
|
-
feedback: recommendations.length > 0 ? `Recommended ${recommendations.length} skill(s) for: **${args.task}**` : `No strong recommendations found for: **${args.task}**`
|
|
17788
|
-
},
|
|
17789
|
-
debug: provider.debug
|
|
17790
|
-
};
|
|
17949
|
+
}
|
|
17950
|
+
return formatResults(provider, args.task, getHeuristicRecommendations(provider, args.task, limit));
|
|
17791
17951
|
};
|
|
17792
17952
|
}
|
|
17793
17953
|
|
|
@@ -17809,7 +17969,7 @@ function normalizeSkillResourcePath(inputPath) {
|
|
|
17809
17969
|
}
|
|
17810
17970
|
function getSkillResources(skill) {
|
|
17811
17971
|
const resources = new Map;
|
|
17812
|
-
for (const resourceMap of [skill.references, skill.scripts, skill.assets]) {
|
|
17972
|
+
for (const resourceMap of [skill.files, skill.references, skill.scripts, skill.assets]) {
|
|
17813
17973
|
for (const [relativePath, resource] of resourceMap.entries()) {
|
|
17814
17974
|
resources.set(normalizeSkillResourcePath(relativePath), resource);
|
|
17815
17975
|
}
|
|
@@ -17899,7 +18059,136 @@ function createSkillResourceReader(provider) {
|
|
|
17899
18059
|
};
|
|
17900
18060
|
}
|
|
17901
18061
|
|
|
18062
|
+
// src/lib/SkillLinks.ts
|
|
18063
|
+
import path2 from "path";
|
|
18064
|
+
var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
18065
|
+
function normalizeLinkedSkillPath(target) {
|
|
18066
|
+
const trimmedTarget = target.trim();
|
|
18067
|
+
if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
|
|
18068
|
+
return null;
|
|
18069
|
+
}
|
|
18070
|
+
const [pathWithoutFragment] = trimmedTarget.split("#", 1);
|
|
18071
|
+
const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
|
|
18072
|
+
const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
|
|
18073
|
+
if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
|
|
18074
|
+
return null;
|
|
18075
|
+
}
|
|
18076
|
+
return normalizedPath;
|
|
18077
|
+
}
|
|
18078
|
+
function extractSkillLinks(content) {
|
|
18079
|
+
const links = new Map;
|
|
18080
|
+
for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
|
|
18081
|
+
const label = match[1]?.trim();
|
|
18082
|
+
const originalPath = match[2]?.trim();
|
|
18083
|
+
if (!label || !originalPath) {
|
|
18084
|
+
continue;
|
|
18085
|
+
}
|
|
18086
|
+
const resourcePath = normalizeLinkedSkillPath(originalPath);
|
|
18087
|
+
if (!resourcePath) {
|
|
18088
|
+
continue;
|
|
18089
|
+
}
|
|
18090
|
+
links.set(`${label}:${resourcePath}`, {
|
|
18091
|
+
label,
|
|
18092
|
+
originalPath,
|
|
18093
|
+
resourcePath
|
|
18094
|
+
});
|
|
18095
|
+
}
|
|
18096
|
+
for (const match of content.matchAll(/(^|[^a-zA-Z0-9])@([a-zA-Z0-9_.\-/]+(?:\/[a-zA-Z0-9_.\-/]+)*)/g)) {
|
|
18097
|
+
const originalPath = match[2]?.trim();
|
|
18098
|
+
if (!originalPath) {
|
|
18099
|
+
continue;
|
|
18100
|
+
}
|
|
18101
|
+
const resourcePath = normalizeLinkedSkillPath(originalPath);
|
|
18102
|
+
if (!resourcePath) {
|
|
18103
|
+
continue;
|
|
18104
|
+
}
|
|
18105
|
+
links.set(`@:${resourcePath}`, {
|
|
18106
|
+
label: `@${resourcePath}`,
|
|
18107
|
+
originalPath: `@${originalPath}`,
|
|
18108
|
+
resourcePath
|
|
18109
|
+
});
|
|
18110
|
+
}
|
|
18111
|
+
return Array.from(links.values());
|
|
18112
|
+
}
|
|
18113
|
+
|
|
18114
|
+
// src/lib/formatLoadedSkill.ts
|
|
18115
|
+
function formatLoadedSkill(args) {
|
|
18116
|
+
const linkedResources = extractSkillLinks(args.skill.content);
|
|
18117
|
+
const output = [
|
|
18118
|
+
`## Skill: ${args.skill.name}`,
|
|
18119
|
+
"",
|
|
18120
|
+
`**Skill identifier**: ${args.skill.toolName}`,
|
|
18121
|
+
`**Base directory**: ${args.skill.fullPath}`
|
|
18122
|
+
];
|
|
18123
|
+
if (args.invocationName) {
|
|
18124
|
+
output.push(`**Invocation**: /${args.invocationName}`);
|
|
18125
|
+
}
|
|
18126
|
+
if (args.userMessage?.trim()) {
|
|
18127
|
+
output.push(`**User request**: ${args.userMessage.trim()}`);
|
|
18128
|
+
}
|
|
18129
|
+
output.push("", "Relative file references in this skill resolve from the skill root directory.");
|
|
18130
|
+
output.push("Use skill_resource with the exact root-relative path when you need a linked file.");
|
|
18131
|
+
if (args.skill.files.size > 0) {
|
|
18132
|
+
output.push("", "### Available files", "");
|
|
18133
|
+
for (const relativePath of Array.from(args.skill.files.keys()).sort()) {
|
|
18134
|
+
output.push(`- ${relativePath}`);
|
|
18135
|
+
}
|
|
18136
|
+
}
|
|
18137
|
+
if (linkedResources.length > 0) {
|
|
18138
|
+
output.push("", "### Linked files", "");
|
|
18139
|
+
for (const link of linkedResources) {
|
|
18140
|
+
output.push(`- ${link.originalPath} -> ${link.resourcePath}`);
|
|
18141
|
+
}
|
|
18142
|
+
}
|
|
18143
|
+
output.push("", args.skill.content);
|
|
18144
|
+
return output.join(`
|
|
18145
|
+
`);
|
|
18146
|
+
}
|
|
18147
|
+
|
|
18148
|
+
// src/tools/Skill.ts
|
|
18149
|
+
function normalizeSkillSelector2(selector) {
|
|
18150
|
+
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
18151
|
+
}
|
|
18152
|
+
function findSkill(registry2, selector) {
|
|
18153
|
+
const directMatch = registry2.controller.get(selector);
|
|
18154
|
+
if (directMatch) {
|
|
18155
|
+
return directMatch;
|
|
18156
|
+
}
|
|
18157
|
+
const normalizedSelector = normalizeSkillSelector2(selector);
|
|
18158
|
+
for (const skill of registry2.controller.skills) {
|
|
18159
|
+
if (normalizeSkillSelector2(skill.name) === normalizedSelector || normalizeSkillSelector2(skill.toolName) === normalizedSelector) {
|
|
18160
|
+
return skill;
|
|
18161
|
+
}
|
|
18162
|
+
}
|
|
18163
|
+
return null;
|
|
18164
|
+
}
|
|
18165
|
+
function createSkillTool(registry2) {
|
|
18166
|
+
return tool({
|
|
18167
|
+
description: "Load a dynamic skill by exact name or slash alias without preloading the entire skill catalog. Use skill_find or skill_recommend first if you do not know the skill name.",
|
|
18168
|
+
args: {
|
|
18169
|
+
name: tool.schema.string().describe("The skill name. Use without the leading slash, for example bmad-help."),
|
|
18170
|
+
user_message: tool.schema.string().optional().describe("Optional user request or arguments to apply with the skill.")
|
|
18171
|
+
},
|
|
18172
|
+
async execute(args) {
|
|
18173
|
+
await registry2.controller.ready.whenReady();
|
|
18174
|
+
const skill = findSkill(registry2, args.name.replace(/^\//, ""));
|
|
18175
|
+
if (!skill) {
|
|
18176
|
+
const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
|
|
18177
|
+
throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
|
|
18178
|
+
}
|
|
18179
|
+
return formatLoadedSkill({
|
|
18180
|
+
skill,
|
|
18181
|
+
invocationName: args.name.replace(/^\//, ""),
|
|
18182
|
+
userMessage: args.user_message
|
|
18183
|
+
});
|
|
18184
|
+
}
|
|
18185
|
+
});
|
|
18186
|
+
}
|
|
18187
|
+
|
|
17902
18188
|
// src/tools/SkillUser.ts
|
|
18189
|
+
function normalizeSkillSelector3(selector) {
|
|
18190
|
+
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
18191
|
+
}
|
|
17903
18192
|
function createSkillLoader(provider) {
|
|
17904
18193
|
const registry2 = provider.controller;
|
|
17905
18194
|
async function loadSkills(skillNames) {
|
|
@@ -17907,7 +18196,8 @@ function createSkillLoader(provider) {
|
|
|
17907
18196
|
const loaded = [];
|
|
17908
18197
|
const notFound = [];
|
|
17909
18198
|
for (const name of skillNames) {
|
|
17910
|
-
const
|
|
18199
|
+
const normalizedName = normalizeSkillSelector3(name);
|
|
18200
|
+
const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector3(candidate.name) === normalizedName || normalizeSkillSelector3(candidate.toolName) === normalizedName);
|
|
17911
18201
|
if (skill) {
|
|
17912
18202
|
loaded.push(skill);
|
|
17913
18203
|
} else {
|
|
@@ -17923,7 +18213,7 @@ function createSkillLoader(provider) {
|
|
|
17923
18213
|
}
|
|
17924
18214
|
|
|
17925
18215
|
// src/api.ts
|
|
17926
|
-
var createApi = async (config2) => {
|
|
18216
|
+
var createApi = async (config2, client) => {
|
|
17927
18217
|
const logger = createLogger(config2);
|
|
17928
18218
|
const registry2 = await createSkillRegistry(config2, logger);
|
|
17929
18219
|
return {
|
|
@@ -17931,14 +18221,18 @@ var createApi = async (config2) => {
|
|
|
17931
18221
|
logger,
|
|
17932
18222
|
config: config2,
|
|
17933
18223
|
findSkills: createSkillFinder(registry2),
|
|
17934
|
-
recommendSkills: createSkillRecommender(registry2
|
|
18224
|
+
recommendSkills: createSkillRecommender(registry2, {
|
|
18225
|
+
client,
|
|
18226
|
+
config: config2.skillRecommend
|
|
18227
|
+
}),
|
|
17935
18228
|
readResource: createSkillResourceReader(registry2),
|
|
17936
|
-
loadSkill: createSkillLoader(registry2)
|
|
18229
|
+
loadSkill: createSkillLoader(registry2),
|
|
18230
|
+
skillTool: createSkillTool(registry2)
|
|
17937
18231
|
};
|
|
17938
18232
|
};
|
|
17939
18233
|
|
|
17940
18234
|
// node_modules/bunfig/dist/index.js
|
|
17941
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
18235
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
17942
18236
|
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
17943
18237
|
import { homedir as homedir2 } from "os";
|
|
17944
18238
|
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
@@ -18060,7 +18354,7 @@ class ConfigCache {
|
|
|
18060
18354
|
if (!existsSync2(configPath)) {
|
|
18061
18355
|
return true;
|
|
18062
18356
|
}
|
|
18063
|
-
const stats =
|
|
18357
|
+
const stats = statSync2(configPath);
|
|
18064
18358
|
return stats.mtime > cachedTimestamp;
|
|
18065
18359
|
} catch {
|
|
18066
18360
|
return true;
|
|
@@ -18079,7 +18373,7 @@ class ConfigCache {
|
|
|
18079
18373
|
}
|
|
18080
18374
|
setWithFileCheck(configName, value, configPath, customTtl) {
|
|
18081
18375
|
try {
|
|
18082
|
-
const stats = existsSync2(configPath) ?
|
|
18376
|
+
const stats = existsSync2(configPath) ? statSync2(configPath) : null;
|
|
18083
18377
|
const fileTimestamp = stats ? stats.mtime : new Date;
|
|
18084
18378
|
this.set(configName, { value, fileTimestamp }, configPath, customTtl);
|
|
18085
18379
|
} catch {
|
|
@@ -18399,10 +18693,10 @@ async function loadConfig({
|
|
|
18399
18693
|
var defaultConfigDir = resolve(process3.cwd(), "config");
|
|
18400
18694
|
var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
|
|
18401
18695
|
function getProjectRoot(filePath, options2 = {}) {
|
|
18402
|
-
let
|
|
18403
|
-
while (
|
|
18404
|
-
|
|
18405
|
-
const finalPath = resolve2(
|
|
18696
|
+
let path3 = process2.cwd();
|
|
18697
|
+
while (path3.includes("storage"))
|
|
18698
|
+
path3 = resolve2(path3, "..");
|
|
18699
|
+
const finalPath = resolve2(path3, filePath || "");
|
|
18406
18700
|
if (options2?.relative)
|
|
18407
18701
|
return relative3(process2.cwd(), finalPath);
|
|
18408
18702
|
return finalPath;
|
|
@@ -19849,10 +20143,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
|
|
|
19849
20143
|
return config3;
|
|
19850
20144
|
const envPrefix = name.toUpperCase().replace(/-/g, "_");
|
|
19851
20145
|
const result = { ...config3 };
|
|
19852
|
-
function processObject(obj,
|
|
20146
|
+
function processObject(obj, path3 = []) {
|
|
19853
20147
|
const result2 = { ...obj };
|
|
19854
20148
|
for (const [key, value] of Object.entries(obj)) {
|
|
19855
|
-
const envPath = [...
|
|
20149
|
+
const envPath = [...path3, key];
|
|
19856
20150
|
const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
19857
20151
|
const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
|
|
19858
20152
|
const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
|
|
@@ -20034,10 +20328,10 @@ async function loadConfig3({
|
|
|
20034
20328
|
var defaultConfigDir2 = resolve3(process6.cwd(), "config");
|
|
20035
20329
|
var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
|
|
20036
20330
|
function getProjectRoot2(filePath, options2 = {}) {
|
|
20037
|
-
let
|
|
20038
|
-
while (
|
|
20039
|
-
|
|
20040
|
-
const finalPath = resolve4(
|
|
20331
|
+
let path3 = process7.cwd();
|
|
20332
|
+
while (path3.includes("storage"))
|
|
20333
|
+
path3 = resolve4(path3, "..");
|
|
20334
|
+
const finalPath = resolve4(path3, filePath || "");
|
|
20041
20335
|
if (options2?.relative)
|
|
20042
20336
|
return relative2(process7.cwd(), finalPath);
|
|
20043
20337
|
return finalPath;
|
|
@@ -21474,10 +21768,10 @@ class EnvVarError extends BunfigError {
|
|
|
21474
21768
|
|
|
21475
21769
|
class FileSystemError extends BunfigError {
|
|
21476
21770
|
code = "FILE_SYSTEM_ERROR";
|
|
21477
|
-
constructor(operation,
|
|
21478
|
-
super(`File system ${operation} failed for "${
|
|
21771
|
+
constructor(operation, path3, cause) {
|
|
21772
|
+
super(`File system ${operation} failed for "${path3}": ${cause.message}`, {
|
|
21479
21773
|
operation,
|
|
21480
|
-
path:
|
|
21774
|
+
path: path3,
|
|
21481
21775
|
originalError: cause.name,
|
|
21482
21776
|
originalMessage: cause.message
|
|
21483
21777
|
});
|
|
@@ -21550,8 +21844,8 @@ var ErrorFactory = {
|
|
|
21550
21844
|
envVar(envKey, envValue, expectedType, configName) {
|
|
21551
21845
|
return new EnvVarError(envKey, envValue, expectedType, configName);
|
|
21552
21846
|
},
|
|
21553
|
-
fileSystem(operation,
|
|
21554
|
-
return new FileSystemError(operation,
|
|
21847
|
+
fileSystem(operation, path3, cause) {
|
|
21848
|
+
return new FileSystemError(operation, path3, cause);
|
|
21555
21849
|
},
|
|
21556
21850
|
typeGeneration(configDir, outputPath, cause) {
|
|
21557
21851
|
return new TypeGenerationError(configDir, outputPath, cause);
|
|
@@ -21687,9 +21981,9 @@ class EnvProcessor {
|
|
|
21687
21981
|
}
|
|
21688
21982
|
return key.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
21689
21983
|
}
|
|
21690
|
-
processObject(obj,
|
|
21984
|
+
processObject(obj, path3, envPrefix, options2) {
|
|
21691
21985
|
for (const [key, value] of Object.entries(obj)) {
|
|
21692
|
-
const envPath = [...
|
|
21986
|
+
const envPath = [...path3, key];
|
|
21693
21987
|
const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
|
|
21694
21988
|
const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
|
|
21695
21989
|
const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
|
|
@@ -21774,9 +22068,9 @@ class EnvProcessor {
|
|
|
21774
22068
|
return this.formatAsText(envVars, configName);
|
|
21775
22069
|
}
|
|
21776
22070
|
}
|
|
21777
|
-
extractEnvVarInfo(obj,
|
|
22071
|
+
extractEnvVarInfo(obj, path3, prefix, envVars) {
|
|
21778
22072
|
for (const [key, value] of Object.entries(obj)) {
|
|
21779
|
-
const envPath = [...
|
|
22073
|
+
const envPath = [...path3, key];
|
|
21780
22074
|
const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
|
|
21781
22075
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
21782
22076
|
this.extractEnvVarInfo(value, envPath, prefix, envVars);
|
|
@@ -22182,8 +22476,8 @@ class ConfigFileLoader {
|
|
|
22182
22476
|
}
|
|
22183
22477
|
async getFileModificationTime(filePath) {
|
|
22184
22478
|
try {
|
|
22185
|
-
const { statSync:
|
|
22186
|
-
const stats =
|
|
22479
|
+
const { statSync: statSync22 } = await import("fs");
|
|
22480
|
+
const stats = statSync22(filePath);
|
|
22187
22481
|
return stats.mtime;
|
|
22188
22482
|
} catch {
|
|
22189
22483
|
return null;
|
|
@@ -22191,15 +22485,15 @@ class ConfigFileLoader {
|
|
|
22191
22485
|
}
|
|
22192
22486
|
async preloadConfigurations(configPaths, options2 = {}) {
|
|
22193
22487
|
const preloaded = new Map;
|
|
22194
|
-
await Promise.allSettled(configPaths.map(async (
|
|
22488
|
+
await Promise.allSettled(configPaths.map(async (path3) => {
|
|
22195
22489
|
try {
|
|
22196
|
-
const result = await this.loadFromPath(
|
|
22490
|
+
const result = await this.loadFromPath(path3, {}, options2);
|
|
22197
22491
|
if (result) {
|
|
22198
|
-
preloaded.set(
|
|
22492
|
+
preloaded.set(path3, result.config);
|
|
22199
22493
|
}
|
|
22200
22494
|
} catch (error45) {
|
|
22201
22495
|
if (options2.verbose) {
|
|
22202
|
-
console.warn(`Failed to preload ${
|
|
22496
|
+
console.warn(`Failed to preload ${path3}:`, error45);
|
|
22203
22497
|
}
|
|
22204
22498
|
}
|
|
22205
22499
|
}));
|
|
@@ -22278,13 +22572,13 @@ class ConfigValidator {
|
|
|
22278
22572
|
warnings
|
|
22279
22573
|
};
|
|
22280
22574
|
}
|
|
22281
|
-
validateObjectAgainstSchema(value, schema,
|
|
22575
|
+
validateObjectAgainstSchema(value, schema, path3, errors3, warnings, options2) {
|
|
22282
22576
|
if (options2.validateTypes && schema.type) {
|
|
22283
22577
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22284
22578
|
const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
22285
22579
|
if (!expectedTypes.includes(actualType)) {
|
|
22286
22580
|
errors3.push({
|
|
22287
|
-
path:
|
|
22581
|
+
path: path3,
|
|
22288
22582
|
message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
|
|
22289
22583
|
expected: expectedTypes.join(" or "),
|
|
22290
22584
|
actual: actualType,
|
|
@@ -22296,7 +22590,7 @@ class ConfigValidator {
|
|
|
22296
22590
|
}
|
|
22297
22591
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
22298
22592
|
errors3.push({
|
|
22299
|
-
path:
|
|
22593
|
+
path: path3,
|
|
22300
22594
|
message: `Value must be one of: ${schema.enum.join(", ")}`,
|
|
22301
22595
|
expected: schema.enum.join(", "),
|
|
22302
22596
|
actual: value,
|
|
@@ -22308,7 +22602,7 @@ class ConfigValidator {
|
|
|
22308
22602
|
if (typeof value === "string") {
|
|
22309
22603
|
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
22310
22604
|
errors3.push({
|
|
22311
|
-
path:
|
|
22605
|
+
path: path3,
|
|
22312
22606
|
message: `String length must be at least ${schema.minLength}`,
|
|
22313
22607
|
expected: `>= ${schema.minLength}`,
|
|
22314
22608
|
actual: value.length,
|
|
@@ -22317,7 +22611,7 @@ class ConfigValidator {
|
|
|
22317
22611
|
}
|
|
22318
22612
|
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
22319
22613
|
errors3.push({
|
|
22320
|
-
path:
|
|
22614
|
+
path: path3,
|
|
22321
22615
|
message: `String length must not exceed ${schema.maxLength}`,
|
|
22322
22616
|
expected: `<= ${schema.maxLength}`,
|
|
22323
22617
|
actual: value.length,
|
|
@@ -22328,7 +22622,7 @@ class ConfigValidator {
|
|
|
22328
22622
|
const regex = new RegExp(schema.pattern);
|
|
22329
22623
|
if (!regex.test(value)) {
|
|
22330
22624
|
errors3.push({
|
|
22331
|
-
path:
|
|
22625
|
+
path: path3,
|
|
22332
22626
|
message: `String does not match pattern ${schema.pattern}`,
|
|
22333
22627
|
expected: schema.pattern,
|
|
22334
22628
|
actual: value,
|
|
@@ -22340,7 +22634,7 @@ class ConfigValidator {
|
|
|
22340
22634
|
if (typeof value === "number") {
|
|
22341
22635
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
22342
22636
|
errors3.push({
|
|
22343
|
-
path:
|
|
22637
|
+
path: path3,
|
|
22344
22638
|
message: `Value must be at least ${schema.minimum}`,
|
|
22345
22639
|
expected: `>= ${schema.minimum}`,
|
|
22346
22640
|
actual: value,
|
|
@@ -22349,7 +22643,7 @@ class ConfigValidator {
|
|
|
22349
22643
|
}
|
|
22350
22644
|
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
22351
22645
|
errors3.push({
|
|
22352
|
-
path:
|
|
22646
|
+
path: path3,
|
|
22353
22647
|
message: `Value must not exceed ${schema.maximum}`,
|
|
22354
22648
|
expected: `<= ${schema.maximum}`,
|
|
22355
22649
|
actual: value,
|
|
@@ -22359,7 +22653,7 @@ class ConfigValidator {
|
|
|
22359
22653
|
}
|
|
22360
22654
|
if (Array.isArray(value) && schema.items) {
|
|
22361
22655
|
for (let i = 0;i < value.length; i++) {
|
|
22362
|
-
const itemPath =
|
|
22656
|
+
const itemPath = path3 ? `${path3}[${i}]` : `[${i}]`;
|
|
22363
22657
|
this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
|
|
22364
22658
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22365
22659
|
return;
|
|
@@ -22371,7 +22665,7 @@ class ConfigValidator {
|
|
|
22371
22665
|
for (const requiredProp of schema.required) {
|
|
22372
22666
|
if (!(requiredProp in obj)) {
|
|
22373
22667
|
errors3.push({
|
|
22374
|
-
path:
|
|
22668
|
+
path: path3 ? `${path3}.${requiredProp}` : requiredProp,
|
|
22375
22669
|
message: `Missing required property '${requiredProp}'`,
|
|
22376
22670
|
expected: "required",
|
|
22377
22671
|
rule: "required"
|
|
@@ -22384,7 +22678,7 @@ class ConfigValidator {
|
|
|
22384
22678
|
if (schema.properties) {
|
|
22385
22679
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
22386
22680
|
if (propName in obj) {
|
|
22387
|
-
const propPath =
|
|
22681
|
+
const propPath = path3 ? `${path3}.${propName}` : propName;
|
|
22388
22682
|
this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
|
|
22389
22683
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22390
22684
|
return;
|
|
@@ -22396,7 +22690,7 @@ class ConfigValidator {
|
|
|
22396
22690
|
for (const propName of Object.keys(obj)) {
|
|
22397
22691
|
if (!allowedProps.has(propName)) {
|
|
22398
22692
|
warnings.push({
|
|
22399
|
-
path:
|
|
22693
|
+
path: path3 ? `${path3}.${propName}` : propName,
|
|
22400
22694
|
message: `Additional property '${propName}' is not allowed`,
|
|
22401
22695
|
rule: "additionalProperties"
|
|
22402
22696
|
});
|
|
@@ -22430,12 +22724,12 @@ class ConfigValidator {
|
|
|
22430
22724
|
warnings
|
|
22431
22725
|
};
|
|
22432
22726
|
}
|
|
22433
|
-
validateWithRule(value, rule,
|
|
22727
|
+
validateWithRule(value, rule, path3) {
|
|
22434
22728
|
const errors3 = [];
|
|
22435
22729
|
if (rule.required && (value === undefined || value === null)) {
|
|
22436
22730
|
errors3.push({
|
|
22437
|
-
path:
|
|
22438
|
-
message: rule.message || `Property '${
|
|
22731
|
+
path: path3,
|
|
22732
|
+
message: rule.message || `Property '${path3}' is required`,
|
|
22439
22733
|
expected: "required",
|
|
22440
22734
|
rule: "required"
|
|
22441
22735
|
});
|
|
@@ -22448,7 +22742,7 @@ class ConfigValidator {
|
|
|
22448
22742
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22449
22743
|
if (actualType !== rule.type) {
|
|
22450
22744
|
errors3.push({
|
|
22451
|
-
path:
|
|
22745
|
+
path: path3,
|
|
22452
22746
|
message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
|
|
22453
22747
|
expected: rule.type,
|
|
22454
22748
|
actual: actualType,
|
|
@@ -22460,7 +22754,7 @@ class ConfigValidator {
|
|
|
22460
22754
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22461
22755
|
if (length < rule.min) {
|
|
22462
22756
|
errors3.push({
|
|
22463
|
-
path:
|
|
22757
|
+
path: path3,
|
|
22464
22758
|
message: rule.message || `Value must be at least ${rule.min}`,
|
|
22465
22759
|
expected: `>= ${rule.min}`,
|
|
22466
22760
|
actual: length,
|
|
@@ -22472,7 +22766,7 @@ class ConfigValidator {
|
|
|
22472
22766
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22473
22767
|
if (length > rule.max) {
|
|
22474
22768
|
errors3.push({
|
|
22475
|
-
path:
|
|
22769
|
+
path: path3,
|
|
22476
22770
|
message: rule.message || `Value must not exceed ${rule.max}`,
|
|
22477
22771
|
expected: `<= ${rule.max}`,
|
|
22478
22772
|
actual: length,
|
|
@@ -22483,7 +22777,7 @@ class ConfigValidator {
|
|
|
22483
22777
|
if (rule.pattern && typeof value === "string") {
|
|
22484
22778
|
if (!rule.pattern.test(value)) {
|
|
22485
22779
|
errors3.push({
|
|
22486
|
-
path:
|
|
22780
|
+
path: path3,
|
|
22487
22781
|
message: rule.message || `Value does not match pattern ${rule.pattern}`,
|
|
22488
22782
|
expected: rule.pattern.toString(),
|
|
22489
22783
|
actual: value,
|
|
@@ -22493,7 +22787,7 @@ class ConfigValidator {
|
|
|
22493
22787
|
}
|
|
22494
22788
|
if (rule.enum && !rule.enum.includes(value)) {
|
|
22495
22789
|
errors3.push({
|
|
22496
|
-
path:
|
|
22790
|
+
path: path3,
|
|
22497
22791
|
message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
|
|
22498
22792
|
expected: rule.enum.join(", "),
|
|
22499
22793
|
actual: value,
|
|
@@ -22504,7 +22798,7 @@ class ConfigValidator {
|
|
|
22504
22798
|
const customError = rule.validator(value);
|
|
22505
22799
|
if (customError) {
|
|
22506
22800
|
errors3.push({
|
|
22507
|
-
path:
|
|
22801
|
+
path: path3,
|
|
22508
22802
|
message: rule.message || customError,
|
|
22509
22803
|
rule: "custom"
|
|
22510
22804
|
});
|
|
@@ -22512,10 +22806,10 @@ class ConfigValidator {
|
|
|
22512
22806
|
}
|
|
22513
22807
|
return errors3;
|
|
22514
22808
|
}
|
|
22515
|
-
getValueByPath(obj,
|
|
22516
|
-
if (!
|
|
22809
|
+
getValueByPath(obj, path3) {
|
|
22810
|
+
if (!path3)
|
|
22517
22811
|
return obj;
|
|
22518
|
-
const keys =
|
|
22812
|
+
const keys = path3.split(".");
|
|
22519
22813
|
let current = obj;
|
|
22520
22814
|
for (const key of keys) {
|
|
22521
22815
|
if (current && typeof current === "object" && key in current) {
|
|
@@ -22941,10 +23235,10 @@ async function loadConfig5(options2) {
|
|
|
22941
23235
|
function applyEnvVarsToConfig2(name, config4, verbose = false) {
|
|
22942
23236
|
const _envProcessor = new EnvProcessor;
|
|
22943
23237
|
const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
22944
|
-
function processConfigLevel(obj,
|
|
23238
|
+
function processConfigLevel(obj, path3 = []) {
|
|
22945
23239
|
const result = { ...obj };
|
|
22946
23240
|
for (const [key, value] of Object.entries(obj)) {
|
|
22947
|
-
const currentPath = [...
|
|
23241
|
+
const currentPath = [...path3, key];
|
|
22948
23242
|
const envKeys = [
|
|
22949
23243
|
`${envPrefix}_${currentPath.join("_").toUpperCase()}`,
|
|
22950
23244
|
`${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
|
|
@@ -22989,8 +23283,46 @@ var defaultConfigDir3 = resolve7(process12.cwd(), "config");
|
|
|
22989
23283
|
var defaultGeneratedDir3 = resolve7(process12.cwd(), "src/generated");
|
|
22990
23284
|
|
|
22991
23285
|
// src/config.ts
|
|
23286
|
+
import { access as access3, mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
|
|
22992
23287
|
import { homedir as homedir3 } from "os";
|
|
22993
23288
|
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join4, normalize, resolve as resolve8 } from "path";
|
|
23289
|
+
var DEFAULT_RESERVED_SLASH_COMMANDS = [
|
|
23290
|
+
"agent",
|
|
23291
|
+
"agents",
|
|
23292
|
+
"compact",
|
|
23293
|
+
"connect",
|
|
23294
|
+
"details",
|
|
23295
|
+
"editor",
|
|
23296
|
+
"exit",
|
|
23297
|
+
"export",
|
|
23298
|
+
"fork",
|
|
23299
|
+
"help",
|
|
23300
|
+
"init",
|
|
23301
|
+
"mcp",
|
|
23302
|
+
"model",
|
|
23303
|
+
"models",
|
|
23304
|
+
"new",
|
|
23305
|
+
"open",
|
|
23306
|
+
"redo",
|
|
23307
|
+
"sessions",
|
|
23308
|
+
"share",
|
|
23309
|
+
"skills",
|
|
23310
|
+
"terminal",
|
|
23311
|
+
"themes",
|
|
23312
|
+
"thinking",
|
|
23313
|
+
"undo",
|
|
23314
|
+
"unshare"
|
|
23315
|
+
];
|
|
23316
|
+
var DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT = [
|
|
23317
|
+
"You are selecting the most relevant dynamic skills for a user request.",
|
|
23318
|
+
"Return strict JSON only with this shape:",
|
|
23319
|
+
'{"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
|
|
23320
|
+
"Only recommend skills from the provided catalog.",
|
|
23321
|
+
"Prefer the smallest set of high-confidence matches."
|
|
23322
|
+
].join(" ");
|
|
23323
|
+
var MANAGED_PLUGIN_CONFIG_DIRECTORY = join4(homedir3(), ".config", "opencode");
|
|
23324
|
+
var MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
|
|
23325
|
+
var MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
|
|
22994
23326
|
function getOpenCodeConfigPaths() {
|
|
22995
23327
|
const home = homedir3();
|
|
22996
23328
|
const paths = [];
|
|
@@ -23010,14 +23342,14 @@ function getOpenCodeConfigPaths() {
|
|
|
23010
23342
|
paths.push(join4(home, ".opencode"));
|
|
23011
23343
|
return paths;
|
|
23012
23344
|
}
|
|
23013
|
-
function expandTildePath(
|
|
23014
|
-
if (
|
|
23345
|
+
function expandTildePath(path3) {
|
|
23346
|
+
if (path3 === "~") {
|
|
23015
23347
|
return homedir3();
|
|
23016
23348
|
}
|
|
23017
|
-
if (
|
|
23018
|
-
return join4(homedir3(),
|
|
23349
|
+
if (path3.startsWith("~/")) {
|
|
23350
|
+
return join4(homedir3(), path3.slice(2));
|
|
23019
23351
|
}
|
|
23020
|
-
return
|
|
23352
|
+
return path3;
|
|
23021
23353
|
}
|
|
23022
23354
|
var createPathKey = (absolutePath) => {
|
|
23023
23355
|
const normalizedPath = normalize(absolutePath);
|
|
@@ -23081,34 +23413,278 @@ var defaultSkillBasePaths = [
|
|
|
23081
23413
|
join4(homedir3(), ".claude", "skills"),
|
|
23082
23414
|
join4(homedir3(), ".agents", "skills")
|
|
23083
23415
|
];
|
|
23084
|
-
|
|
23085
|
-
|
|
23086
|
-
|
|
23087
|
-
|
|
23416
|
+
function createDefaultSkillRecommendConfig() {
|
|
23417
|
+
return {
|
|
23418
|
+
strategy: "heuristic",
|
|
23419
|
+
model: "",
|
|
23420
|
+
systemPrompt: DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
|
|
23421
|
+
};
|
|
23422
|
+
}
|
|
23423
|
+
function createDefaultNotificationConfig() {
|
|
23424
|
+
return {
|
|
23425
|
+
enabled: false,
|
|
23426
|
+
success: true,
|
|
23427
|
+
errors: true
|
|
23428
|
+
};
|
|
23429
|
+
}
|
|
23430
|
+
function createDefaultPluginConfig() {
|
|
23431
|
+
return {
|
|
23088
23432
|
debug: false,
|
|
23089
23433
|
basePaths: defaultSkillBasePaths,
|
|
23090
|
-
promptRenderer: "xml",
|
|
23091
|
-
modelRenderers: {},
|
|
23092
23434
|
slashCommandName: "skill",
|
|
23093
|
-
enableSkillAliases:
|
|
23435
|
+
enableSkillAliases: true,
|
|
23436
|
+
reservedSlashCommands: [...DEFAULT_RESERVED_SLASH_COMMANDS],
|
|
23437
|
+
notifications: createDefaultNotificationConfig(),
|
|
23438
|
+
skillRecommend: createDefaultSkillRecommendConfig()
|
|
23439
|
+
};
|
|
23440
|
+
}
|
|
23441
|
+
function createConfigOptions(defaultConfig3) {
|
|
23442
|
+
return {
|
|
23443
|
+
name: "opencode-dynamic-skills",
|
|
23444
|
+
cwd: "./",
|
|
23445
|
+
defaultConfig: defaultConfig3
|
|
23446
|
+
};
|
|
23447
|
+
}
|
|
23448
|
+
function stripJsonComments(input) {
|
|
23449
|
+
let output = "";
|
|
23450
|
+
let inString = false;
|
|
23451
|
+
let escaped = false;
|
|
23452
|
+
let inLineComment = false;
|
|
23453
|
+
let inBlockComment = false;
|
|
23454
|
+
for (let index = 0;index < input.length; index += 1) {
|
|
23455
|
+
const current = input[index] ?? "";
|
|
23456
|
+
const next = input[index + 1] ?? "";
|
|
23457
|
+
if (inLineComment) {
|
|
23458
|
+
if (current === `
|
|
23459
|
+
` || current === "\r") {
|
|
23460
|
+
inLineComment = false;
|
|
23461
|
+
output += current;
|
|
23462
|
+
}
|
|
23463
|
+
continue;
|
|
23464
|
+
}
|
|
23465
|
+
if (inBlockComment) {
|
|
23466
|
+
if (current === "*" && next === "/") {
|
|
23467
|
+
inBlockComment = false;
|
|
23468
|
+
index += 1;
|
|
23469
|
+
continue;
|
|
23470
|
+
}
|
|
23471
|
+
if (current === `
|
|
23472
|
+
` || current === "\r") {
|
|
23473
|
+
output += current;
|
|
23474
|
+
}
|
|
23475
|
+
continue;
|
|
23476
|
+
}
|
|
23477
|
+
if (inString) {
|
|
23478
|
+
output += current;
|
|
23479
|
+
if (escaped) {
|
|
23480
|
+
escaped = false;
|
|
23481
|
+
continue;
|
|
23482
|
+
}
|
|
23483
|
+
if (current === "\\") {
|
|
23484
|
+
escaped = true;
|
|
23485
|
+
continue;
|
|
23486
|
+
}
|
|
23487
|
+
if (current === '"') {
|
|
23488
|
+
inString = false;
|
|
23489
|
+
}
|
|
23490
|
+
continue;
|
|
23491
|
+
}
|
|
23492
|
+
if (current === "/" && next === "/") {
|
|
23493
|
+
inLineComment = true;
|
|
23494
|
+
index += 1;
|
|
23495
|
+
continue;
|
|
23496
|
+
}
|
|
23497
|
+
if (current === "/" && next === "*") {
|
|
23498
|
+
inBlockComment = true;
|
|
23499
|
+
index += 1;
|
|
23500
|
+
continue;
|
|
23501
|
+
}
|
|
23502
|
+
if (current === '"') {
|
|
23503
|
+
inString = true;
|
|
23504
|
+
}
|
|
23505
|
+
output += current;
|
|
23094
23506
|
}
|
|
23095
|
-
|
|
23507
|
+
return output;
|
|
23508
|
+
}
|
|
23509
|
+
function removeTrailingJsonCommas(input) {
|
|
23510
|
+
let output = "";
|
|
23511
|
+
let inString = false;
|
|
23512
|
+
let escaped = false;
|
|
23513
|
+
for (let index = 0;index < input.length; index += 1) {
|
|
23514
|
+
const current = input[index] ?? "";
|
|
23515
|
+
if (inString) {
|
|
23516
|
+
output += current;
|
|
23517
|
+
if (escaped) {
|
|
23518
|
+
escaped = false;
|
|
23519
|
+
continue;
|
|
23520
|
+
}
|
|
23521
|
+
if (current === "\\") {
|
|
23522
|
+
escaped = true;
|
|
23523
|
+
continue;
|
|
23524
|
+
}
|
|
23525
|
+
if (current === '"') {
|
|
23526
|
+
inString = false;
|
|
23527
|
+
}
|
|
23528
|
+
continue;
|
|
23529
|
+
}
|
|
23530
|
+
if (current === '"') {
|
|
23531
|
+
inString = true;
|
|
23532
|
+
output += current;
|
|
23533
|
+
continue;
|
|
23534
|
+
}
|
|
23535
|
+
if (current === ",") {
|
|
23536
|
+
let lookahead = index + 1;
|
|
23537
|
+
while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
|
|
23538
|
+
lookahead += 1;
|
|
23539
|
+
}
|
|
23540
|
+
const nextToken = input[lookahead];
|
|
23541
|
+
if (nextToken === "}" || nextToken === "]") {
|
|
23542
|
+
continue;
|
|
23543
|
+
}
|
|
23544
|
+
}
|
|
23545
|
+
output += current;
|
|
23546
|
+
}
|
|
23547
|
+
return output;
|
|
23548
|
+
}
|
|
23549
|
+
function parseJsonc(input) {
|
|
23550
|
+
return JSON.parse(removeTrailingJsonCommas(stripJsonComments(input)));
|
|
23551
|
+
}
|
|
23552
|
+
function trimString(value) {
|
|
23553
|
+
return value?.trim() ?? "";
|
|
23554
|
+
}
|
|
23555
|
+
function mergePluginConfig(baseConfig, override) {
|
|
23556
|
+
const skillRecommend = {
|
|
23557
|
+
...baseConfig.skillRecommend,
|
|
23558
|
+
...override.skillRecommend
|
|
23559
|
+
};
|
|
23560
|
+
const notifications = {
|
|
23561
|
+
...baseConfig.notifications,
|
|
23562
|
+
...override.notifications
|
|
23563
|
+
};
|
|
23564
|
+
const overrideReservedSlashCommands = override.reservedSlashCommands?.map((command) => trimString(command)).filter(Boolean) ?? [];
|
|
23565
|
+
return {
|
|
23566
|
+
...baseConfig,
|
|
23567
|
+
...override,
|
|
23568
|
+
skillRecommend: {
|
|
23569
|
+
strategy: skillRecommend.strategy === "model" ? "model" : "heuristic",
|
|
23570
|
+
model: trimString(skillRecommend.model),
|
|
23571
|
+
systemPrompt: trimString(skillRecommend.systemPrompt) || DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
|
|
23572
|
+
},
|
|
23573
|
+
reservedSlashCommands: overrideReservedSlashCommands.length > 0 ? overrideReservedSlashCommands : baseConfig.reservedSlashCommands,
|
|
23574
|
+
notifications: {
|
|
23575
|
+
enabled: notifications.enabled === true,
|
|
23576
|
+
success: notifications.success !== false,
|
|
23577
|
+
errors: notifications.errors !== false
|
|
23578
|
+
}
|
|
23579
|
+
};
|
|
23580
|
+
}
|
|
23581
|
+
function getManagedPluginConfigPaths(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23582
|
+
return [
|
|
23583
|
+
join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME),
|
|
23584
|
+
join4(configDirectory, MANAGED_PLUGIN_JSON_FILENAME)
|
|
23585
|
+
];
|
|
23586
|
+
}
|
|
23587
|
+
async function fileExists(filePath) {
|
|
23588
|
+
try {
|
|
23589
|
+
await access3(filePath);
|
|
23590
|
+
return true;
|
|
23591
|
+
} catch {
|
|
23592
|
+
return false;
|
|
23593
|
+
}
|
|
23594
|
+
}
|
|
23595
|
+
function renderManagedPluginConfigJsonc(config3 = createDefaultPluginConfig()) {
|
|
23596
|
+
const basePaths = config3.basePaths.map((basePath) => ` ${JSON.stringify(basePath)}`).join(`,
|
|
23597
|
+
`);
|
|
23598
|
+
return [
|
|
23599
|
+
"{",
|
|
23600
|
+
" // Enable verbose plugin logging output.",
|
|
23601
|
+
` "debug": ${JSON.stringify(config3.debug)},`,
|
|
23602
|
+
"",
|
|
23603
|
+
" // Global skill roots. Project-local .opencode/.claude/.agents paths are appended automatically.",
|
|
23604
|
+
' "basePaths": [',
|
|
23605
|
+
basePaths,
|
|
23606
|
+
" ],",
|
|
23607
|
+
"",
|
|
23608
|
+
" // Explicit slash entrypoint, for example: /skill git-release",
|
|
23609
|
+
` "slashCommandName": ${JSON.stringify(config3.slashCommandName)},`,
|
|
23610
|
+
"",
|
|
23611
|
+
" // When true, /<skill-name> alias invocations can be intercepted after submit.",
|
|
23612
|
+
` "enableSkillAliases": ${JSON.stringify(config3.enableSkillAliases)},`,
|
|
23613
|
+
"",
|
|
23614
|
+
" // Builtin or protected slash commands that should never be intercepted as /<skill-name> aliases.",
|
|
23615
|
+
' "reservedSlashCommands": [',
|
|
23616
|
+
config3.reservedSlashCommands.map((command) => ` ${JSON.stringify(command)}`).join(`,
|
|
23617
|
+
`),
|
|
23618
|
+
" ],",
|
|
23619
|
+
"",
|
|
23620
|
+
" // Best-effort OS notifications triggered by this plugin.",
|
|
23621
|
+
' "notifications": {',
|
|
23622
|
+
` "enabled": ${JSON.stringify(config3.notifications.enabled)},`,
|
|
23623
|
+
` "success": ${JSON.stringify(config3.notifications.success)},`,
|
|
23624
|
+
` "errors": ${JSON.stringify(config3.notifications.errors)}`,
|
|
23625
|
+
" },",
|
|
23626
|
+
"",
|
|
23627
|
+
' // skill_recommend strategy. Set strategy to "model" and fill model with provider/model to use an internal LLM call.',
|
|
23628
|
+
' "skillRecommend": {',
|
|
23629
|
+
` "strategy": ${JSON.stringify(config3.skillRecommend.strategy)},`,
|
|
23630
|
+
" // Model format: provider/model, for example openai/gpt-5.2",
|
|
23631
|
+
` "model": ${JSON.stringify(config3.skillRecommend.model)},`,
|
|
23632
|
+
' // The model must return strict JSON: {"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
|
|
23633
|
+
` "systemPrompt": ${JSON.stringify(config3.skillRecommend.systemPrompt)}`,
|
|
23634
|
+
" }",
|
|
23635
|
+
"}",
|
|
23636
|
+
""
|
|
23637
|
+
].join(`
|
|
23638
|
+
`);
|
|
23639
|
+
}
|
|
23640
|
+
async function ensureManagedPluginConfigFile(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23641
|
+
for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
|
|
23642
|
+
if (await fileExists(candidatePath)) {
|
|
23643
|
+
return candidatePath;
|
|
23644
|
+
}
|
|
23645
|
+
}
|
|
23646
|
+
await mkdir3(configDirectory, { recursive: true });
|
|
23647
|
+
const configPath = join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME);
|
|
23648
|
+
await writeFile3(configPath, renderManagedPluginConfigJsonc(), "utf8");
|
|
23649
|
+
return configPath;
|
|
23650
|
+
}
|
|
23651
|
+
async function loadManagedPluginConfig(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23652
|
+
for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
|
|
23653
|
+
if (!await fileExists(candidatePath)) {
|
|
23654
|
+
continue;
|
|
23655
|
+
}
|
|
23656
|
+
const content = await readFile(candidatePath, "utf8");
|
|
23657
|
+
if (candidatePath.endsWith(".jsonc")) {
|
|
23658
|
+
return parseJsonc(content);
|
|
23659
|
+
}
|
|
23660
|
+
return JSON.parse(content);
|
|
23661
|
+
}
|
|
23662
|
+
return {};
|
|
23663
|
+
}
|
|
23096
23664
|
async function getPluginConfig(ctx) {
|
|
23097
|
-
const
|
|
23665
|
+
const defaultConfig3 = createDefaultPluginConfig();
|
|
23666
|
+
await ensureManagedPluginConfigFile();
|
|
23667
|
+
const managedConfig = await loadManagedPluginConfig();
|
|
23668
|
+
const resolvedConfig = await loadConfig5(createConfigOptions(defaultConfig3));
|
|
23669
|
+
const mergedConfig = mergePluginConfig(mergePluginConfig(defaultConfig3, managedConfig), resolvedConfig);
|
|
23098
23670
|
const configuredBasePaths = [
|
|
23099
|
-
...
|
|
23671
|
+
...mergedConfig.basePaths,
|
|
23100
23672
|
...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
|
|
23101
23673
|
];
|
|
23102
|
-
|
|
23103
|
-
return
|
|
23674
|
+
mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
|
|
23675
|
+
return mergedConfig;
|
|
23104
23676
|
}
|
|
23105
23677
|
|
|
23106
23678
|
// src/commands/SlashCommand.ts
|
|
23107
23679
|
var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
|
|
23108
23680
|
var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
|
|
23109
|
-
function
|
|
23681
|
+
function normalizeSkillSelector4(selector) {
|
|
23110
23682
|
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
23111
23683
|
}
|
|
23684
|
+
function isReservedSlashCommand(invocationName, reservedSlashCommands) {
|
|
23685
|
+
const normalizedInvocationName = invocationName.trim().toLowerCase();
|
|
23686
|
+
return reservedSlashCommands.some((reservedCommand) => reservedCommand.trim().toLowerCase() === normalizedInvocationName);
|
|
23687
|
+
}
|
|
23112
23688
|
function parseSlashCommand(text, slashCommandName) {
|
|
23113
23689
|
const trimmedText = text.trim();
|
|
23114
23690
|
if (!trimmedText.startsWith("/")) {
|
|
@@ -23143,30 +23719,26 @@ function findSkillBySelector(registry2, selector) {
|
|
|
23143
23719
|
if (directMatch) {
|
|
23144
23720
|
return directMatch;
|
|
23145
23721
|
}
|
|
23146
|
-
const normalizedSelector =
|
|
23722
|
+
const normalizedSelector = normalizeSkillSelector4(selector);
|
|
23147
23723
|
for (const skill of registry2.controller.skills) {
|
|
23148
|
-
if (
|
|
23724
|
+
if (normalizeSkillSelector4(skill.toolName) === normalizedSelector) {
|
|
23149
23725
|
return skill;
|
|
23150
23726
|
}
|
|
23151
|
-
if (
|
|
23727
|
+
if (normalizeSkillSelector4(skill.name) === normalizedSelector) {
|
|
23152
23728
|
return skill;
|
|
23153
23729
|
}
|
|
23154
23730
|
}
|
|
23155
23731
|
return null;
|
|
23156
23732
|
}
|
|
23157
23733
|
function renderSlashSkillPrompt(args) {
|
|
23158
|
-
const userPrompt = args.userPrompt || "Apply this skill to the current request.";
|
|
23159
23734
|
return [
|
|
23160
23735
|
SLASH_COMMAND_SENTINEL,
|
|
23161
|
-
|
|
23162
|
-
|
|
23163
|
-
|
|
23164
|
-
|
|
23165
|
-
|
|
23166
|
-
|
|
23167
|
-
`User request: ${userPrompt}`,
|
|
23168
|
-
"",
|
|
23169
|
-
args.renderedSkill
|
|
23736
|
+
formatLoadedSkill({
|
|
23737
|
+
invocationName: args.invocationName,
|
|
23738
|
+
skill: args.skill,
|
|
23739
|
+
userMessage: args.userPrompt || "Apply this skill to the current request."
|
|
23740
|
+
}),
|
|
23741
|
+
""
|
|
23170
23742
|
].join(`
|
|
23171
23743
|
`);
|
|
23172
23744
|
}
|
|
@@ -23207,6 +23779,9 @@ async function rewriteSlashCommandText(args) {
|
|
|
23207
23779
|
if (isAliasInvocation && !args.enableSkillAliases) {
|
|
23208
23780
|
return null;
|
|
23209
23781
|
}
|
|
23782
|
+
if (isAliasInvocation && isReservedSlashCommand(parsedCommand.invocationName, args.reservedSlashCommands ?? [])) {
|
|
23783
|
+
return null;
|
|
23784
|
+
}
|
|
23210
23785
|
await args.registry.controller.ready.whenReady();
|
|
23211
23786
|
const skill = findSkillBySelector(args.registry, parsedCommand.skillSelector);
|
|
23212
23787
|
if (!skill) {
|
|
@@ -23214,69 +23789,11 @@ async function rewriteSlashCommandText(args) {
|
|
|
23214
23789
|
}
|
|
23215
23790
|
return renderSlashSkillPrompt({
|
|
23216
23791
|
invocationName: parsedCommand.invocationName,
|
|
23217
|
-
renderedSkill: args.renderSkill(skill),
|
|
23218
23792
|
skill,
|
|
23219
23793
|
userPrompt: parsedCommand.userPrompt
|
|
23220
23794
|
});
|
|
23221
23795
|
}
|
|
23222
23796
|
|
|
23223
|
-
// src/lib/SkillLinks.ts
|
|
23224
|
-
import path2 from "path";
|
|
23225
|
-
var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
23226
|
-
function normalizeLinkedSkillPath(target) {
|
|
23227
|
-
const trimmedTarget = target.trim();
|
|
23228
|
-
if (trimmedTarget.length === 0 || trimmedTarget.startsWith("#") || trimmedTarget.startsWith("/") || SCHEME_PATTERN.test(trimmedTarget)) {
|
|
23229
|
-
return null;
|
|
23230
|
-
}
|
|
23231
|
-
const [pathWithoutFragment] = trimmedTarget.split("#", 1);
|
|
23232
|
-
const [pathWithoutQuery] = pathWithoutFragment.split("?", 1);
|
|
23233
|
-
const normalizedPath = path2.posix.normalize(pathWithoutQuery.replace(/\\/g, "/")).replace(/^\.\//, "");
|
|
23234
|
-
if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
|
|
23235
|
-
return null;
|
|
23236
|
-
}
|
|
23237
|
-
return normalizedPath;
|
|
23238
|
-
}
|
|
23239
|
-
function extractSkillLinks(content) {
|
|
23240
|
-
const links = new Map;
|
|
23241
|
-
for (const match of content.matchAll(/\[([^\]]+)\]\(([^)]+)\)/g)) {
|
|
23242
|
-
const label = match[1]?.trim();
|
|
23243
|
-
const originalPath = match[2]?.trim();
|
|
23244
|
-
if (!label || !originalPath) {
|
|
23245
|
-
continue;
|
|
23246
|
-
}
|
|
23247
|
-
const resourcePath = normalizeLinkedSkillPath(originalPath);
|
|
23248
|
-
if (!resourcePath) {
|
|
23249
|
-
continue;
|
|
23250
|
-
}
|
|
23251
|
-
links.set(`${label}:${resourcePath}`, {
|
|
23252
|
-
label,
|
|
23253
|
-
originalPath,
|
|
23254
|
-
resourcePath
|
|
23255
|
-
});
|
|
23256
|
-
}
|
|
23257
|
-
return Array.from(links.values());
|
|
23258
|
-
}
|
|
23259
|
-
|
|
23260
|
-
// src/lib/renderers/JsonPromptRenderer.ts
|
|
23261
|
-
var createJsonPromptRenderer = () => {
|
|
23262
|
-
const renderer = {
|
|
23263
|
-
format: "json",
|
|
23264
|
-
render(args) {
|
|
23265
|
-
if (args.type === "Skill") {
|
|
23266
|
-
return JSON.stringify({
|
|
23267
|
-
Skill: {
|
|
23268
|
-
...args.data,
|
|
23269
|
-
linkedResources: extractSkillLinks(args.data.content),
|
|
23270
|
-
skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path."
|
|
23271
|
-
}
|
|
23272
|
-
}, null, 2);
|
|
23273
|
-
}
|
|
23274
|
-
return JSON.stringify({ [args.type]: args.data }, null, 2);
|
|
23275
|
-
}
|
|
23276
|
-
};
|
|
23277
|
-
return renderer;
|
|
23278
|
-
};
|
|
23279
|
-
|
|
23280
23797
|
// src/lib/xml.ts
|
|
23281
23798
|
function escapeXml(str2) {
|
|
23282
23799
|
return String(str2).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -23330,6 +23847,7 @@ var createXmlPromptRenderer = () => {
|
|
|
23330
23847
|
...skill,
|
|
23331
23848
|
linkedResources: extractSkillLinks(skill.content),
|
|
23332
23849
|
skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path.",
|
|
23850
|
+
files: resourceMapToArray(skill.files),
|
|
23333
23851
|
references: resourceMapToArray(skill.references),
|
|
23334
23852
|
scripts: resourceMapToArray(skill.scripts),
|
|
23335
23853
|
assets: resourceMapToArray(skill.assets)
|
|
@@ -23360,362 +23878,120 @@ var createXmlPromptRenderer = () => {
|
|
|
23360
23878
|
return renderer;
|
|
23361
23879
|
};
|
|
23362
23880
|
|
|
23363
|
-
//
|
|
23364
|
-
|
|
23365
|
-
|
|
23366
|
-
|
|
23367
|
-
|
|
23368
|
-
|
|
23369
|
-
|
|
23370
|
-
|
|
23371
|
-
}
|
|
23372
|
-
return keys;
|
|
23373
|
-
}
|
|
23374
|
-
function _objectSpread(target) {
|
|
23375
|
-
for (var i = 1;i < arguments.length; i++) {
|
|
23376
|
-
var source = arguments[i] != null ? arguments[i] : {};
|
|
23377
|
-
i % 2 ? ownKeys(Object(source), true).forEach(function(key) {
|
|
23378
|
-
_defineProperty(target, key, source[key]);
|
|
23379
|
-
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function(key) {
|
|
23380
|
-
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
|
23381
|
-
});
|
|
23382
|
-
}
|
|
23383
|
-
return target;
|
|
23881
|
+
// src/services/Notifier.ts
|
|
23882
|
+
import { execFile } from "child_process";
|
|
23883
|
+
import { promisify } from "util";
|
|
23884
|
+
var execFileAsync = promisify(execFile);
|
|
23885
|
+
var NOTIFICATION_TITLE = "OpenCode Dynamic Skills";
|
|
23886
|
+
function createDefaultRunner() {
|
|
23887
|
+
return async (command, args) => {
|
|
23888
|
+
await execFileAsync(command, args);
|
|
23889
|
+
};
|
|
23384
23890
|
}
|
|
23385
|
-
function
|
|
23386
|
-
|
|
23387
|
-
if (
|
|
23388
|
-
|
|
23389
|
-
} else {
|
|
23390
|
-
obj[key] = value;
|
|
23891
|
+
function truncateNotificationText(value, maxLength = 180) {
|
|
23892
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
23893
|
+
if (normalized.length <= maxLength) {
|
|
23894
|
+
return normalized;
|
|
23391
23895
|
}
|
|
23392
|
-
return
|
|
23896
|
+
return `${normalized.slice(0, maxLength - 1)}\u2026`;
|
|
23393
23897
|
}
|
|
23394
|
-
function
|
|
23395
|
-
|
|
23396
|
-
return typeof key === "symbol" ? key : String(key);
|
|
23898
|
+
function escapeAppleScript(value) {
|
|
23899
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
23397
23900
|
}
|
|
23398
|
-
function
|
|
23399
|
-
|
|
23400
|
-
return input;
|
|
23401
|
-
var prim = input[Symbol.toPrimitive];
|
|
23402
|
-
if (prim !== undefined) {
|
|
23403
|
-
var res = prim.call(input, hint || "default");
|
|
23404
|
-
if (typeof res !== "object")
|
|
23405
|
-
return res;
|
|
23406
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
23407
|
-
}
|
|
23408
|
-
return (hint === "string" ? String : Number)(input);
|
|
23409
|
-
}
|
|
23410
|
-
var dedent = createDedent({});
|
|
23411
|
-
var dedent_default = dedent;
|
|
23412
|
-
function createDedent(options3) {
|
|
23413
|
-
dedent2.withOptions = (newOptions) => createDedent(_objectSpread(_objectSpread({}, options3), newOptions));
|
|
23414
|
-
return dedent2;
|
|
23415
|
-
function dedent2(strings, ...values) {
|
|
23416
|
-
const raw = typeof strings === "string" ? [strings] : strings.raw;
|
|
23417
|
-
const {
|
|
23418
|
-
alignValues = false,
|
|
23419
|
-
escapeSpecialCharacters = Array.isArray(strings),
|
|
23420
|
-
trimWhitespace = true
|
|
23421
|
-
} = options3;
|
|
23422
|
-
let result = "";
|
|
23423
|
-
for (let i = 0;i < raw.length; i++) {
|
|
23424
|
-
let next = raw[i];
|
|
23425
|
-
if (escapeSpecialCharacters) {
|
|
23426
|
-
next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
|
|
23427
|
-
}
|
|
23428
|
-
result += next;
|
|
23429
|
-
if (i < values.length) {
|
|
23430
|
-
const value = alignValues ? alignValue(values[i], result) : values[i];
|
|
23431
|
-
result += value;
|
|
23432
|
-
}
|
|
23433
|
-
}
|
|
23434
|
-
const lines = result.split(`
|
|
23435
|
-
`);
|
|
23436
|
-
let mindent = null;
|
|
23437
|
-
for (const l of lines) {
|
|
23438
|
-
const m = l.match(/^(\s+)\S+/);
|
|
23439
|
-
if (m) {
|
|
23440
|
-
const indent = m[1].length;
|
|
23441
|
-
if (!mindent) {
|
|
23442
|
-
mindent = indent;
|
|
23443
|
-
} else {
|
|
23444
|
-
mindent = Math.min(mindent, indent);
|
|
23445
|
-
}
|
|
23446
|
-
}
|
|
23447
|
-
}
|
|
23448
|
-
if (mindent !== null) {
|
|
23449
|
-
const m = mindent;
|
|
23450
|
-
result = lines.map((l) => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join(`
|
|
23451
|
-
`);
|
|
23452
|
-
}
|
|
23453
|
-
if (trimWhitespace) {
|
|
23454
|
-
result = result.trim();
|
|
23455
|
-
}
|
|
23456
|
-
if (escapeSpecialCharacters) {
|
|
23457
|
-
result = result.replace(/\\n/g, `
|
|
23458
|
-
`).replace(/\\t/g, "\t").replace(/\\r/g, "\r").replace(/\\v/g, "\v").replace(/\\b/g, "\b").replace(/\\f/g, "\f").replace(/\\0/g, "\x00").replace(/\\x([\da-fA-F]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16))).replace(/\\u\{([\da-fA-F]{1,6})\}/g, (_, h) => String.fromCodePoint(parseInt(h, 16))).replace(/\\u([\da-fA-F]{4})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
|
|
23459
|
-
}
|
|
23460
|
-
if (typeof Bun !== "undefined") {
|
|
23461
|
-
result = result.replace(/\\u(?:\{([\da-fA-F]{1,6})\}|([\da-fA-F]{4}))/g, (_, braced, unbraced) => {
|
|
23462
|
-
var _ref;
|
|
23463
|
-
const hex3 = (_ref = braced !== null && braced !== undefined ? braced : unbraced) !== null && _ref !== undefined ? _ref : "";
|
|
23464
|
-
return String.fromCodePoint(parseInt(hex3, 16));
|
|
23465
|
-
});
|
|
23466
|
-
}
|
|
23467
|
-
return result;
|
|
23468
|
-
}
|
|
23901
|
+
function escapeXml2(value) {
|
|
23902
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
23469
23903
|
}
|
|
23470
|
-
function
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
|
|
23474
|
-
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23904
|
+
function buildWindowsToastScript(title, message) {
|
|
23905
|
+
const xml = `<toast><visual><binding template='ToastGeneric'><text>${escapeXml2(title)}</text><text>${escapeXml2(message)}</text></binding></visual></toast>`;
|
|
23906
|
+
return [
|
|
23907
|
+
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
|
|
23908
|
+
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
|
|
23909
|
+
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
|
|
23910
|
+
`$xml.LoadXml(@'${xml}'@)`,
|
|
23911
|
+
"$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
|
|
23912
|
+
`$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${NOTIFICATION_TITLE}')`,
|
|
23913
|
+
"$notifier.Show($toast)"
|
|
23914
|
+
].join("; ");
|
|
23915
|
+
}
|
|
23916
|
+
function getNotificationCommand(platform, title, message) {
|
|
23917
|
+
switch (platform) {
|
|
23918
|
+
case "darwin":
|
|
23919
|
+
return {
|
|
23920
|
+
command: "osascript",
|
|
23921
|
+
args: [
|
|
23922
|
+
"-e",
|
|
23923
|
+
`display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`
|
|
23924
|
+
]
|
|
23925
|
+
};
|
|
23926
|
+
case "linux":
|
|
23927
|
+
return {
|
|
23928
|
+
command: "notify-send",
|
|
23929
|
+
args: [title, message]
|
|
23930
|
+
};
|
|
23931
|
+
case "win32":
|
|
23932
|
+
return {
|
|
23933
|
+
command: "powershell",
|
|
23934
|
+
args: ["-NoProfile", "-Command", buildWindowsToastScript(title, message)]
|
|
23935
|
+
};
|
|
23936
|
+
default:
|
|
23937
|
+
return null;
|
|
23482
23938
|
}
|
|
23483
|
-
return value;
|
|
23484
23939
|
}
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
23488
|
-
|
|
23489
|
-
|
|
23490
|
-
|
|
23491
|
-
for (const [key, value] of entries) {
|
|
23492
|
-
if (value === null || value === undefined) {
|
|
23493
|
-
continue;
|
|
23494
|
-
}
|
|
23495
|
-
const heading = "#".repeat(headingLevel);
|
|
23496
|
-
output += `${heading} ${key}`;
|
|
23497
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
23498
|
-
output += renderObject(value, Math.min(headingLevel + 1, 6), indentLevel);
|
|
23499
|
-
} else if (Array.isArray(value)) {
|
|
23500
|
-
output += renderArray(value, indentLevel);
|
|
23501
|
-
} else {
|
|
23502
|
-
const indent = " ".repeat(indentLevel);
|
|
23503
|
-
const escapedValue = htmlEscape(String(value));
|
|
23504
|
-
output += `${indent}- **${key}**: *${escapedValue}*`;
|
|
23505
|
-
}
|
|
23506
|
-
output += `
|
|
23507
|
-
`;
|
|
23508
|
-
}
|
|
23509
|
-
return output;
|
|
23510
|
-
};
|
|
23511
|
-
const renderArray = (arr, indentLevel) => {
|
|
23512
|
-
const indent = " ".repeat(indentLevel);
|
|
23513
|
-
let output = "";
|
|
23514
|
-
for (const item of arr) {
|
|
23515
|
-
if (item === null || item === undefined) {
|
|
23516
|
-
continue;
|
|
23517
|
-
}
|
|
23518
|
-
if (typeof item === "object" && !Array.isArray(item)) {
|
|
23519
|
-
const nestedObj = item;
|
|
23520
|
-
for (const [key, value] of Object.entries(nestedObj)) {
|
|
23521
|
-
if (value === null || value === undefined) {
|
|
23522
|
-
continue;
|
|
23523
|
-
}
|
|
23524
|
-
if (typeof value === "object") {
|
|
23525
|
-
if (Array.isArray(value)) {
|
|
23526
|
-
output += `${indent}- **${key}**:
|
|
23527
|
-
`;
|
|
23528
|
-
output += renderArray(value, indentLevel + 1);
|
|
23529
|
-
} else {
|
|
23530
|
-
output += `${indent}- **${key}**
|
|
23531
|
-
`;
|
|
23532
|
-
output += renderObject(value, 4, indentLevel + 1);
|
|
23533
|
-
}
|
|
23534
|
-
} else {
|
|
23535
|
-
const escapedValue = htmlEscape(String(value));
|
|
23536
|
-
output += `${indent}- **${key}**: *${escapedValue}*
|
|
23537
|
-
`;
|
|
23538
|
-
}
|
|
23539
|
-
}
|
|
23540
|
-
} else if (Array.isArray(item)) {
|
|
23541
|
-
output += renderArray(item, indentLevel + 1);
|
|
23542
|
-
} else {
|
|
23543
|
-
const escapedValue = htmlEscape(String(item));
|
|
23544
|
-
output += `${indent}- *${escapedValue}*
|
|
23545
|
-
`;
|
|
23546
|
-
}
|
|
23547
|
-
}
|
|
23548
|
-
return output;
|
|
23549
|
-
};
|
|
23550
|
-
const htmlEscape = (value) => {
|
|
23551
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
23552
|
-
};
|
|
23553
|
-
const renderSkill = (skill) => {
|
|
23554
|
-
const linkedResources = extractSkillLinks(skill.content);
|
|
23555
|
-
return dedent_default`
|
|
23556
|
-
# ${skill.name}
|
|
23557
|
-
|
|
23558
|
-
> Skill root:
|
|
23559
|
-
> ${skill.fullPath}
|
|
23560
|
-
|
|
23561
|
-
Relative file references in this skill resolve from the skill root directory.
|
|
23562
|
-
Always use skill_resource with the exact root-relative path for linked files.
|
|
23563
|
-
|
|
23564
|
-
${linkedResources.length > 0 ? dedent_default`
|
|
23565
|
-
## Linked files detected in skill content
|
|
23566
|
-
|
|
23567
|
-
${linkedResources.map((link) => `- [${link.label}](${link.originalPath}) \u2192 use skill_resource with ${link.resourcePath}`).join(`
|
|
23568
|
-
`)}
|
|
23569
|
-
` : ""}
|
|
23570
|
-
|
|
23571
|
-
${skill.content}
|
|
23572
|
-
|
|
23573
|
-
## Metadata
|
|
23574
|
-
|
|
23575
|
-
${skill.metadata ? renderObject(skill.metadata, 3) : ""}
|
|
23576
|
-
|
|
23577
|
-
## References
|
|
23578
|
-
|
|
23579
|
-
${skill.references ? renderArray(resourceMapToArray(skill.references), 1) : ""}
|
|
23580
|
-
|
|
23581
|
-
## Scripts
|
|
23582
|
-
|
|
23583
|
-
${skill.scripts ? renderArray(resourceMapToArray(skill.scripts), 1) : ""}
|
|
23584
|
-
|
|
23585
|
-
## Assets
|
|
23586
|
-
|
|
23587
|
-
${skill.assets ? renderArray(resourceMapToArray(skill.assets), 1) : ""}
|
|
23588
|
-
`;
|
|
23589
|
-
};
|
|
23590
|
-
const renderResource = (resource) => {
|
|
23591
|
-
return renderObject(resource, 3);
|
|
23592
|
-
};
|
|
23593
|
-
const renderSearchResult = (result) => {
|
|
23594
|
-
return renderObject(result, 3);
|
|
23595
|
-
};
|
|
23596
|
-
const renderer = {
|
|
23597
|
-
format: "md",
|
|
23598
|
-
render(args) {
|
|
23599
|
-
if (args.type === "Skill") {
|
|
23600
|
-
return renderSkill(args.data);
|
|
23601
|
-
}
|
|
23602
|
-
if (args.type === "SkillResource") {
|
|
23603
|
-
return renderResource(args.data);
|
|
23604
|
-
}
|
|
23605
|
-
if (args.type === "SkillSearchResults") {
|
|
23606
|
-
return renderSearchResult(args.data);
|
|
23607
|
-
}
|
|
23608
|
-
return renderObject({}, 3);
|
|
23940
|
+
function createNotifier(args) {
|
|
23941
|
+
const runner = args.shell ?? createDefaultRunner();
|
|
23942
|
+
const platform = args.platform ?? process.platform;
|
|
23943
|
+
async function notify(title, message) {
|
|
23944
|
+
if (!args.config.enabled) {
|
|
23945
|
+
return;
|
|
23609
23946
|
}
|
|
23610
|
-
|
|
23611
|
-
|
|
23612
|
-
|
|
23613
|
-
|
|
23614
|
-
// src/lib/createPromptRenderer.ts
|
|
23615
|
-
function createPromptRenderer() {
|
|
23616
|
-
const renderers = {
|
|
23617
|
-
json: createJsonPromptRenderer(),
|
|
23618
|
-
xml: createXmlPromptRenderer(),
|
|
23619
|
-
md: createMdPromptRenderer()
|
|
23620
|
-
};
|
|
23621
|
-
const getFormatter = (format) => {
|
|
23622
|
-
switch (format) {
|
|
23623
|
-
case "json":
|
|
23624
|
-
return renderers.json.render;
|
|
23625
|
-
case "xml":
|
|
23626
|
-
return renderers.xml.render;
|
|
23627
|
-
case "md":
|
|
23628
|
-
return renderers.md.render;
|
|
23629
|
-
default:
|
|
23630
|
-
throw new Error(`Unsupported format: ${format}`);
|
|
23947
|
+
const notificationCommand = getNotificationCommand(platform, truncateNotificationText(title, 80), truncateNotificationText(message));
|
|
23948
|
+
if (!notificationCommand) {
|
|
23949
|
+
args.logger.debug("Notifications are not supported on this platform.", platform);
|
|
23950
|
+
return;
|
|
23631
23951
|
}
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
}
|
|
23637
|
-
|
|
23638
|
-
// src/lib/getModelFormat.ts
|
|
23639
|
-
function getModelFormat(args) {
|
|
23640
|
-
const { modelId, providerId, config: config3 } = args;
|
|
23641
|
-
const modelRenderers = config3.modelRenderers ?? {};
|
|
23642
|
-
if (providerId && modelId) {
|
|
23643
|
-
const combinedKey = `${providerId}-${modelId}`;
|
|
23644
|
-
if (combinedKey in modelRenderers) {
|
|
23645
|
-
return modelRenderers[combinedKey];
|
|
23952
|
+
try {
|
|
23953
|
+
await runner(notificationCommand.command, notificationCommand.args);
|
|
23954
|
+
} catch (error45) {
|
|
23955
|
+
args.logger.warn("Failed to send notification.", error45);
|
|
23646
23956
|
}
|
|
23647
23957
|
}
|
|
23648
|
-
|
|
23649
|
-
|
|
23650
|
-
|
|
23651
|
-
|
|
23652
|
-
}
|
|
23653
|
-
|
|
23654
|
-
|
|
23655
|
-
|
|
23656
|
-
|
|
23657
|
-
|
|
23658
|
-
|
|
23659
|
-
|
|
23660
|
-
|
|
23661
|
-
|
|
23662
|
-
|
|
23663
|
-
|
|
23664
|
-
|
|
23665
|
-
|
|
23666
|
-
};
|
|
23667
|
-
const untrackMessage = (args) => {
|
|
23668
|
-
const sessionMap = modelUsage.get(args.sessionID);
|
|
23669
|
-
if (sessionMap && sessionMap[args.messageID]) {
|
|
23670
|
-
delete sessionMap[args.messageID];
|
|
23671
|
-
if (Object.keys(sessionMap).length === 0) {
|
|
23672
|
-
modelUsage.delete(args.sessionID);
|
|
23958
|
+
return {
|
|
23959
|
+
async skillLoaded(skillNames) {
|
|
23960
|
+
if (!args.config.success || skillNames.length === 0) {
|
|
23961
|
+
return;
|
|
23962
|
+
}
|
|
23963
|
+
const count = skillNames.length;
|
|
23964
|
+
const message = count === 1 ? `Injected skill: ${skillNames[0]}` : `Injected ${count} skills: ${skillNames.join(", ")}`;
|
|
23965
|
+
await notify(NOTIFICATION_TITLE, message);
|
|
23966
|
+
},
|
|
23967
|
+
async resourceLoaded(skillName, relativePath) {
|
|
23968
|
+
if (!args.config.success) {
|
|
23969
|
+
return;
|
|
23970
|
+
}
|
|
23971
|
+
await notify(NOTIFICATION_TITLE, `Injected ${relativePath} from ${skillName}`);
|
|
23972
|
+
},
|
|
23973
|
+
async error(title, message) {
|
|
23974
|
+
if (!args.config.errors) {
|
|
23975
|
+
return;
|
|
23673
23976
|
}
|
|
23977
|
+
await notify(title, message);
|
|
23674
23978
|
}
|
|
23675
23979
|
};
|
|
23676
|
-
const untrackSession = (sessionID) => {
|
|
23677
|
-
modelUsage.delete(sessionID);
|
|
23678
|
-
};
|
|
23679
|
-
const getModelInfo = (args) => {
|
|
23680
|
-
const sessionMap = modelUsage.get(args.sessionID);
|
|
23681
|
-
return sessionMap ? sessionMap[args.messageID] : undefined;
|
|
23682
|
-
};
|
|
23683
|
-
const reset3 = () => {
|
|
23684
|
-
modelUsage.clear();
|
|
23685
|
-
};
|
|
23686
|
-
return {
|
|
23687
|
-
reset: reset3,
|
|
23688
|
-
track,
|
|
23689
|
-
untrackMessage,
|
|
23690
|
-
untrackSession,
|
|
23691
|
-
getModelInfo
|
|
23692
|
-
};
|
|
23693
23980
|
}
|
|
23694
23981
|
|
|
23695
23982
|
// src/index.ts
|
|
23696
23983
|
var SkillsPlugin = async (ctx) => {
|
|
23697
23984
|
const config3 = await getPluginConfig(ctx);
|
|
23698
|
-
const api2 = await createApi(config3);
|
|
23985
|
+
const api2 = await createApi(config3, ctx.client);
|
|
23699
23986
|
const sendPrompt = createInstructionInjector(ctx);
|
|
23700
|
-
const
|
|
23701
|
-
const
|
|
23987
|
+
const renderer = createXmlPromptRenderer().render;
|
|
23988
|
+
const notifier = createNotifier({
|
|
23989
|
+
config: config3.notifications,
|
|
23990
|
+
logger: api2.logger
|
|
23991
|
+
});
|
|
23702
23992
|
api2.registry.initialise();
|
|
23703
23993
|
return {
|
|
23704
|
-
"chat.message": async (
|
|
23705
|
-
if (input.messageID && input.model?.providerID && input.model?.modelID) {
|
|
23706
|
-
modelIdAccountant.track({
|
|
23707
|
-
messageID: input.messageID,
|
|
23708
|
-
providerID: input.model.providerID,
|
|
23709
|
-
modelID: input.model.modelID,
|
|
23710
|
-
sessionID: input.sessionID
|
|
23711
|
-
});
|
|
23712
|
-
}
|
|
23713
|
-
const format = getModelFormat({
|
|
23714
|
-
modelId: input.model?.modelID,
|
|
23715
|
-
providerId: input.model?.providerID,
|
|
23716
|
-
config: config3
|
|
23717
|
-
});
|
|
23718
|
-
const renderSkill = promptRenderer.getFormatter(format);
|
|
23994
|
+
"chat.message": async (_input, output) => {
|
|
23719
23995
|
for (const part of output.parts) {
|
|
23720
23996
|
if (part.type !== "text") {
|
|
23721
23997
|
continue;
|
|
@@ -23728,57 +24004,53 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23728
24004
|
const rewrittenText = await rewriteSlashCommandText({
|
|
23729
24005
|
text: part.text,
|
|
23730
24006
|
registry: api2.registry,
|
|
23731
|
-
renderSkill: (skill) => renderSkill({ data: skill, type: "Skill" }),
|
|
23732
24007
|
slashCommandName: config3.slashCommandName,
|
|
23733
|
-
enableSkillAliases: config3.enableSkillAliases
|
|
24008
|
+
enableSkillAliases: config3.enableSkillAliases,
|
|
24009
|
+
reservedSlashCommands: config3.reservedSlashCommands
|
|
23734
24010
|
});
|
|
23735
24011
|
if (!rewrittenText) {
|
|
23736
24012
|
continue;
|
|
23737
24013
|
}
|
|
23738
24014
|
part.text = rewrittenText;
|
|
24015
|
+
const parsedSkillName = part.text.match(/\*\*Skill identifier\*\*: ([^\n]+)/)?.[1];
|
|
24016
|
+
if (parsedSkillName) {
|
|
24017
|
+
await notifier.skillLoaded([parsedSkillName]);
|
|
24018
|
+
}
|
|
23739
24019
|
break;
|
|
23740
24020
|
}
|
|
23741
24021
|
},
|
|
23742
24022
|
async event(args) {
|
|
23743
24023
|
switch (args.event.type) {
|
|
23744
|
-
case "
|
|
23745
|
-
|
|
23746
|
-
break;
|
|
23747
|
-
case "session.deleted":
|
|
23748
|
-
modelIdAccountant.untrackSession(args.event.properties.info.id);
|
|
24024
|
+
case "session.error":
|
|
24025
|
+
await notifier.error("OpenCode session error", "A session error occurred in the current session.");
|
|
23749
24026
|
break;
|
|
23750
24027
|
}
|
|
23751
24028
|
},
|
|
23752
24029
|
tool: {
|
|
24030
|
+
skill: api2.skillTool,
|
|
23753
24031
|
skill_use: tool({
|
|
23754
24032
|
description: "Load one or more skills into the chat. Provide an array of skill names to load them as user messages.",
|
|
23755
24033
|
args: {
|
|
23756
24034
|
skill_names: tool.schema.array(tool.schema.string()).describe("An array of skill names to load.")
|
|
23757
24035
|
},
|
|
23758
24036
|
execute: async (args, toolCtx) => {
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
23769
|
-
|
|
23770
|
-
|
|
23771
|
-
const results = await api2.loadSkill(args.skill_names);
|
|
23772
|
-
for await (const skill of results.loaded) {
|
|
23773
|
-
await sendPrompt(renderer({ data: skill, type: "Skill" }), {
|
|
23774
|
-
sessionId: toolCtx.sessionID,
|
|
23775
|
-
agent: toolCtx.agent
|
|
24037
|
+
try {
|
|
24038
|
+
const results = await api2.loadSkill(args.skill_names);
|
|
24039
|
+
for await (const skill of results.loaded) {
|
|
24040
|
+
await sendPrompt(renderer({ data: skill, type: "Skill" }), {
|
|
24041
|
+
sessionId: toolCtx.sessionID,
|
|
24042
|
+
agent: toolCtx.agent
|
|
24043
|
+
});
|
|
24044
|
+
}
|
|
24045
|
+
await notifier.skillLoaded(results.loaded.map((skill) => skill.toolName));
|
|
24046
|
+
return JSON.stringify({
|
|
24047
|
+
loaded: results.loaded.map((skill) => skill.toolName),
|
|
24048
|
+
not_found: results.notFound
|
|
23776
24049
|
});
|
|
24050
|
+
} catch (error45) {
|
|
24051
|
+
await notifier.error("skill_use failed", error45 instanceof Error ? error45.message : String(error45));
|
|
24052
|
+
throw error45;
|
|
23777
24053
|
}
|
|
23778
|
-
return JSON.stringify({
|
|
23779
|
-
loaded: results.loaded.map((skill) => skill.toolName),
|
|
23780
|
-
not_found: results.notFound
|
|
23781
|
-
});
|
|
23782
24054
|
}
|
|
23783
24055
|
}),
|
|
23784
24056
|
skill_find: tool({
|
|
@@ -23786,19 +24058,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23786
24058
|
args: {
|
|
23787
24059
|
query: tool.schema.union([tool.schema.string(), tool.schema.array(tool.schema.string())]).describe("The search query string or array of strings.")
|
|
23788
24060
|
},
|
|
23789
|
-
execute: async (args
|
|
23790
|
-
const messageID = toolCtx.messageID;
|
|
23791
|
-
const sessionID = toolCtx.sessionID;
|
|
23792
|
-
const modelInfo = modelIdAccountant.getModelInfo({
|
|
23793
|
-
messageID,
|
|
23794
|
-
sessionID
|
|
23795
|
-
});
|
|
23796
|
-
const format = getModelFormat({
|
|
23797
|
-
config: config3,
|
|
23798
|
-
modelId: modelInfo?.modelID,
|
|
23799
|
-
providerId: modelInfo?.providerID
|
|
23800
|
-
});
|
|
23801
|
-
const renderer = promptRenderer.getFormatter(format);
|
|
24061
|
+
execute: async (args) => {
|
|
23802
24062
|
const results = await api2.findSkills(args);
|
|
23803
24063
|
const output = renderer({
|
|
23804
24064
|
data: results,
|
|
@@ -23813,19 +24073,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23813
24073
|
task: tool.schema.string().describe("The task or request to recommend skills for."),
|
|
23814
24074
|
limit: tool.schema.number().int().min(1).max(10).optional().describe("Maximum number of recommendations to return.")
|
|
23815
24075
|
},
|
|
23816
|
-
execute: async (args
|
|
23817
|
-
const messageID = toolCtx.messageID;
|
|
23818
|
-
const sessionID = toolCtx.sessionID;
|
|
23819
|
-
const modelInfo = modelIdAccountant.getModelInfo({
|
|
23820
|
-
messageID,
|
|
23821
|
-
sessionID
|
|
23822
|
-
});
|
|
23823
|
-
const format = getModelFormat({
|
|
23824
|
-
config: config3,
|
|
23825
|
-
modelId: modelInfo?.modelID,
|
|
23826
|
-
providerId: modelInfo?.providerID
|
|
23827
|
-
});
|
|
23828
|
-
const renderer = promptRenderer.getFormatter(format);
|
|
24076
|
+
execute: async (args) => {
|
|
23829
24077
|
const results = await api2.recommendSkills(args);
|
|
23830
24078
|
return renderer({
|
|
23831
24079
|
data: results,
|
|
@@ -23840,31 +24088,25 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23840
24088
|
relative_path: tool.schema.string().describe("The relative path to the resource file within the skill directory.")
|
|
23841
24089
|
},
|
|
23842
24090
|
execute: async (args, toolCtx) => {
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
23851
|
-
|
|
23852
|
-
|
|
23853
|
-
|
|
23854
|
-
|
|
23855
|
-
|
|
23856
|
-
|
|
23857
|
-
|
|
24091
|
+
try {
|
|
24092
|
+
const result = await api2.readResource(args);
|
|
24093
|
+
if (!result.injection) {
|
|
24094
|
+
throw new Error("Failed to read resource");
|
|
24095
|
+
}
|
|
24096
|
+
await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
|
|
24097
|
+
sessionId: toolCtx.sessionID,
|
|
24098
|
+
agent: toolCtx.agent
|
|
24099
|
+
});
|
|
24100
|
+
await notifier.resourceLoaded(args.skill_name, result.injection.resource_path);
|
|
24101
|
+
return JSON.stringify({
|
|
24102
|
+
result: "Resource injected successfully",
|
|
24103
|
+
resource_path: result.injection.resource_path,
|
|
24104
|
+
resource_mimetype: result.injection.resource_mimetype
|
|
24105
|
+
});
|
|
24106
|
+
} catch (error45) {
|
|
24107
|
+
await notifier.error("skill_resource failed", error45 instanceof Error ? error45.message : String(error45));
|
|
24108
|
+
throw error45;
|
|
23858
24109
|
}
|
|
23859
|
-
await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
|
|
23860
|
-
sessionId: toolCtx.sessionID,
|
|
23861
|
-
agent: toolCtx.agent
|
|
23862
|
-
});
|
|
23863
|
-
return JSON.stringify({
|
|
23864
|
-
result: "Resource injected successfully",
|
|
23865
|
-
resource_path: result.injection.resource_path,
|
|
23866
|
-
resource_mimetype: result.injection.resource_mimetype
|
|
23867
|
-
});
|
|
23868
24110
|
}
|
|
23869
24111
|
})
|
|
23870
24112
|
}
|