opencode-dynamic-skills 1.1.0 → 1.3.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 +53 -18
- package/README.zh-CN.md +41 -18
- package/dist/api.d.ts +5 -4
- package/dist/commands/MirroredSkillCommand.d.ts +15 -0
- package/dist/commands/MirroredSkillCommand.test.d.ts +1 -0
- package/dist/commands/SlashCommand.d.ts +2 -1
- package/dist/config.d.ts +24 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1014 -560
- package/dist/lib/SkillFs.d.ts +1 -0
- package/dist/lib/formatLoadedSkill.d.ts +1 -1
- package/dist/lib/formatLoadedSkill.test.d.ts +1 -0
- package/dist/services/Notifier.d.ts +13 -0
- package/dist/services/Notifier.test.d.ts +1 -0
- package/dist/services/TuiCommandMirror.d.ts +15 -0
- package/dist/services/TuiCommandMirror.test.d.ts +1 -0
- package/dist/tools/SkillRecommender.d.ts +25 -8
- package/dist/types.d.ts +22 -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,6 +18059,9 @@ function createSkillResourceReader(provider) {
|
|
|
17899
18059
|
};
|
|
17900
18060
|
}
|
|
17901
18061
|
|
|
18062
|
+
// src/lib/formatLoadedSkill.ts
|
|
18063
|
+
import path3 from "path";
|
|
18064
|
+
|
|
17902
18065
|
// src/lib/SkillLinks.ts
|
|
17903
18066
|
import path2 from "path";
|
|
17904
18067
|
var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
@@ -17952,11 +18115,116 @@ function extractSkillLinks(content) {
|
|
|
17952
18115
|
}
|
|
17953
18116
|
|
|
17954
18117
|
// src/lib/formatLoadedSkill.ts
|
|
17955
|
-
|
|
17956
|
-
|
|
18118
|
+
var MAX_REFERENCE_DEPTH = 3;
|
|
18119
|
+
function normalizeSkillResourcePath2(inputPath) {
|
|
18120
|
+
const unixPath = inputPath.replace(/\\/g, "/").trim();
|
|
18121
|
+
if (unixPath.length === 0 || unixPath.startsWith("/")) {
|
|
18122
|
+
return null;
|
|
18123
|
+
}
|
|
18124
|
+
const normalizedPath = path3.posix.normalize(unixPath).replace(/^\.\//, "");
|
|
18125
|
+
if (normalizedPath.length === 0 || normalizedPath === "." || normalizedPath.startsWith("../") || normalizedPath.includes("/../")) {
|
|
18126
|
+
return null;
|
|
18127
|
+
}
|
|
18128
|
+
return normalizedPath;
|
|
18129
|
+
}
|
|
18130
|
+
function buildSkillResourceIndex(skill) {
|
|
18131
|
+
const resources = new Map;
|
|
18132
|
+
const basenameIndex = new Map;
|
|
18133
|
+
for (const resourceMap of [skill.files, skill.references, skill.scripts, skill.assets]) {
|
|
18134
|
+
for (const [relativePath, resource] of resourceMap.entries()) {
|
|
18135
|
+
const normalizedPath = normalizeSkillResourcePath2(relativePath);
|
|
18136
|
+
if (!normalizedPath) {
|
|
18137
|
+
continue;
|
|
18138
|
+
}
|
|
18139
|
+
const resolvedResource = {
|
|
18140
|
+
relativePath: normalizedPath,
|
|
18141
|
+
absolutePath: resource.absolutePath,
|
|
18142
|
+
mimeType: resource.mimeType
|
|
18143
|
+
};
|
|
18144
|
+
resources.set(normalizedPath, resolvedResource);
|
|
18145
|
+
const basename2 = path3.posix.basename(normalizedPath);
|
|
18146
|
+
const basenameMatches = basenameIndex.get(basename2) ?? [];
|
|
18147
|
+
basenameMatches.push(resolvedResource);
|
|
18148
|
+
basenameIndex.set(basename2, basenameMatches);
|
|
18149
|
+
}
|
|
18150
|
+
}
|
|
18151
|
+
return {
|
|
18152
|
+
resources,
|
|
18153
|
+
basenameIndex
|
|
18154
|
+
};
|
|
18155
|
+
}
|
|
18156
|
+
function resolveReferencedSkillResource(resourceIndex, resourcePath) {
|
|
18157
|
+
const normalizedPath = normalizeSkillResourcePath2(resourcePath);
|
|
18158
|
+
if (!normalizedPath) {
|
|
18159
|
+
return null;
|
|
18160
|
+
}
|
|
18161
|
+
const { resources, basenameIndex } = resourceIndex;
|
|
18162
|
+
const exactMatch = resources.get(normalizedPath);
|
|
18163
|
+
if (exactMatch) {
|
|
18164
|
+
return exactMatch;
|
|
18165
|
+
}
|
|
18166
|
+
const basenameMatches = basenameIndex.get(path3.posix.basename(normalizedPath)) ?? [];
|
|
18167
|
+
return basenameMatches.length === 1 ? basenameMatches[0] : null;
|
|
18168
|
+
}
|
|
18169
|
+
async function collectReferencedSkillFiles(args) {
|
|
18170
|
+
const depth = args.depth ?? 0;
|
|
18171
|
+
const visited = args.visited ?? new Set;
|
|
18172
|
+
if (depth >= MAX_REFERENCE_DEPTH) {
|
|
18173
|
+
return [];
|
|
18174
|
+
}
|
|
18175
|
+
const referencedFiles = [];
|
|
18176
|
+
for (const link of extractSkillLinks(args.content)) {
|
|
18177
|
+
const resolvedResource = resolveReferencedSkillResource(args.resourceIndex, link.resourcePath);
|
|
18178
|
+
if (!resolvedResource || visited.has(resolvedResource.relativePath)) {
|
|
18179
|
+
continue;
|
|
18180
|
+
}
|
|
18181
|
+
visited.add(resolvedResource.relativePath);
|
|
18182
|
+
const fileContent = await readSkillFile(resolvedResource.absolutePath);
|
|
18183
|
+
referencedFiles.push({
|
|
18184
|
+
...resolvedResource,
|
|
18185
|
+
content: fileContent
|
|
18186
|
+
});
|
|
18187
|
+
const nestedReferences = await collectReferencedSkillFiles({
|
|
18188
|
+
skill: args.skill,
|
|
18189
|
+
content: fileContent,
|
|
18190
|
+
resourceIndex: args.resourceIndex,
|
|
18191
|
+
depth: depth + 1,
|
|
18192
|
+
visited
|
|
18193
|
+
});
|
|
18194
|
+
referencedFiles.push(...nestedReferences);
|
|
18195
|
+
}
|
|
18196
|
+
return referencedFiles;
|
|
18197
|
+
}
|
|
18198
|
+
function formatExpandedReferences(references) {
|
|
18199
|
+
if (references.length === 0) {
|
|
18200
|
+
return [];
|
|
18201
|
+
}
|
|
18202
|
+
const output = ["", "### Expanded referenced skill files", ""];
|
|
18203
|
+
for (const reference of references) {
|
|
18204
|
+
output.push(`#### ${reference.relativePath}`);
|
|
18205
|
+
output.push(`**Absolute path**: ${reference.absolutePath}`);
|
|
18206
|
+
output.push(`**MIME type**: ${reference.mimeType}`);
|
|
18207
|
+
output.push("");
|
|
18208
|
+
output.push("````text");
|
|
18209
|
+
output.push(reference.content.trim());
|
|
18210
|
+
output.push("````");
|
|
18211
|
+
output.push("");
|
|
18212
|
+
}
|
|
18213
|
+
return output;
|
|
18214
|
+
}
|
|
18215
|
+
async function formatLoadedSkill(args) {
|
|
18216
|
+
const invocationName = args.invocationName ?? args.skill.name;
|
|
18217
|
+
const normalizedSkillRoot = `${args.skill.fullPath.replace(/\\/g, "/")}/`;
|
|
18218
|
+
const resourceIndex = buildSkillResourceIndex(args.skill);
|
|
18219
|
+
const referencedFiles = await collectReferencedSkillFiles({
|
|
18220
|
+
skill: args.skill,
|
|
18221
|
+
content: args.skill.content,
|
|
18222
|
+
resourceIndex
|
|
18223
|
+
});
|
|
17957
18224
|
const output = [
|
|
17958
|
-
|
|
18225
|
+
`# /${invocationName} Command`,
|
|
17959
18226
|
"",
|
|
18227
|
+
`**Description**: ${args.skill.description}`,
|
|
17960
18228
|
`**Skill identifier**: ${args.skill.toolName}`,
|
|
17961
18229
|
`**Base directory**: ${args.skill.fullPath}`
|
|
17962
18230
|
];
|
|
@@ -17966,21 +18234,28 @@ function formatLoadedSkill(args) {
|
|
|
17966
18234
|
if (args.userMessage?.trim()) {
|
|
17967
18235
|
output.push(`**User request**: ${args.userMessage.trim()}`);
|
|
17968
18236
|
}
|
|
17969
|
-
output.push("", "
|
|
17970
|
-
output.push("
|
|
17971
|
-
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
18237
|
+
output.push("", "---", "", "## Command Instructions", "");
|
|
18238
|
+
output.push("<skill-instruction>");
|
|
18239
|
+
output.push(`Base directory for this skill: ${normalizedSkillRoot}`);
|
|
18240
|
+
output.push("File references (@path), markdown links, and relative file mentions in this skill resolve from the skill root.");
|
|
18241
|
+
output.push("Use skill_resource with the exact root-relative path if you need additional files beyond the expanded references below.");
|
|
18242
|
+
output.push("", args.skill.content, "</skill-instruction>");
|
|
18243
|
+
if (args.skill.files.size > 0) {
|
|
18244
|
+
output.push("", "### Available files", "");
|
|
18245
|
+
for (const relativePath of Array.from(args.skill.files.keys()).sort()) {
|
|
18246
|
+
output.push(`- ${relativePath}`);
|
|
17975
18247
|
}
|
|
17976
18248
|
}
|
|
17977
|
-
output.push(
|
|
18249
|
+
output.push(...formatExpandedReferences(referencedFiles));
|
|
18250
|
+
if (args.userMessage?.trim()) {
|
|
18251
|
+
output.push("", "<user-request>", args.userMessage.trim(), "</user-request>");
|
|
18252
|
+
}
|
|
17978
18253
|
return output.join(`
|
|
17979
18254
|
`);
|
|
17980
18255
|
}
|
|
17981
18256
|
|
|
17982
18257
|
// src/tools/Skill.ts
|
|
17983
|
-
function
|
|
18258
|
+
function normalizeSkillSelector2(selector) {
|
|
17984
18259
|
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
17985
18260
|
}
|
|
17986
18261
|
function findSkill(registry2, selector) {
|
|
@@ -17988,9 +18263,9 @@ function findSkill(registry2, selector) {
|
|
|
17988
18263
|
if (directMatch) {
|
|
17989
18264
|
return directMatch;
|
|
17990
18265
|
}
|
|
17991
|
-
const normalizedSelector =
|
|
18266
|
+
const normalizedSelector = normalizeSkillSelector2(selector);
|
|
17992
18267
|
for (const skill of registry2.controller.skills) {
|
|
17993
|
-
if (
|
|
18268
|
+
if (normalizeSkillSelector2(skill.name) === normalizedSelector || normalizeSkillSelector2(skill.toolName) === normalizedSelector) {
|
|
17994
18269
|
return skill;
|
|
17995
18270
|
}
|
|
17996
18271
|
}
|
|
@@ -18010,7 +18285,7 @@ function createSkillTool(registry2) {
|
|
|
18010
18285
|
const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
|
|
18011
18286
|
throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
|
|
18012
18287
|
}
|
|
18013
|
-
return formatLoadedSkill({
|
|
18288
|
+
return await formatLoadedSkill({
|
|
18014
18289
|
skill,
|
|
18015
18290
|
invocationName: args.name.replace(/^\//, ""),
|
|
18016
18291
|
userMessage: args.user_message
|
|
@@ -18020,7 +18295,7 @@ function createSkillTool(registry2) {
|
|
|
18020
18295
|
}
|
|
18021
18296
|
|
|
18022
18297
|
// src/tools/SkillUser.ts
|
|
18023
|
-
function
|
|
18298
|
+
function normalizeSkillSelector3(selector) {
|
|
18024
18299
|
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
18025
18300
|
}
|
|
18026
18301
|
function createSkillLoader(provider) {
|
|
@@ -18030,8 +18305,8 @@ function createSkillLoader(provider) {
|
|
|
18030
18305
|
const loaded = [];
|
|
18031
18306
|
const notFound = [];
|
|
18032
18307
|
for (const name of skillNames) {
|
|
18033
|
-
const normalizedName =
|
|
18034
|
-
const skill = registry2.get(name) ?? registry2.skills.find((candidate) =>
|
|
18308
|
+
const normalizedName = normalizeSkillSelector3(name);
|
|
18309
|
+
const skill = registry2.get(name) ?? registry2.skills.find((candidate) => normalizeSkillSelector3(candidate.name) === normalizedName || normalizeSkillSelector3(candidate.toolName) === normalizedName);
|
|
18035
18310
|
if (skill) {
|
|
18036
18311
|
loaded.push(skill);
|
|
18037
18312
|
} else {
|
|
@@ -18047,7 +18322,7 @@ function createSkillLoader(provider) {
|
|
|
18047
18322
|
}
|
|
18048
18323
|
|
|
18049
18324
|
// src/api.ts
|
|
18050
|
-
var createApi = async (config2) => {
|
|
18325
|
+
var createApi = async (config2, client) => {
|
|
18051
18326
|
const logger = createLogger(config2);
|
|
18052
18327
|
const registry2 = await createSkillRegistry(config2, logger);
|
|
18053
18328
|
return {
|
|
@@ -18055,7 +18330,10 @@ var createApi = async (config2) => {
|
|
|
18055
18330
|
logger,
|
|
18056
18331
|
config: config2,
|
|
18057
18332
|
findSkills: createSkillFinder(registry2),
|
|
18058
|
-
recommendSkills: createSkillRecommender(registry2
|
|
18333
|
+
recommendSkills: createSkillRecommender(registry2, {
|
|
18334
|
+
client,
|
|
18335
|
+
config: config2.skillRecommend
|
|
18336
|
+
}),
|
|
18059
18337
|
readResource: createSkillResourceReader(registry2),
|
|
18060
18338
|
loadSkill: createSkillLoader(registry2),
|
|
18061
18339
|
skillTool: createSkillTool(registry2)
|
|
@@ -18063,7 +18341,7 @@ var createApi = async (config2) => {
|
|
|
18063
18341
|
};
|
|
18064
18342
|
|
|
18065
18343
|
// node_modules/bunfig/dist/index.js
|
|
18066
|
-
import { existsSync as existsSync2, statSync } from "fs";
|
|
18344
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
18067
18345
|
import { existsSync as existsSync8, mkdirSync as mkdirSync3, readdirSync as readdirSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
18068
18346
|
import { homedir as homedir2 } from "os";
|
|
18069
18347
|
import { dirname as dirname3, resolve as resolve7 } from "path";
|
|
@@ -18185,7 +18463,7 @@ class ConfigCache {
|
|
|
18185
18463
|
if (!existsSync2(configPath)) {
|
|
18186
18464
|
return true;
|
|
18187
18465
|
}
|
|
18188
|
-
const stats =
|
|
18466
|
+
const stats = statSync2(configPath);
|
|
18189
18467
|
return stats.mtime > cachedTimestamp;
|
|
18190
18468
|
} catch {
|
|
18191
18469
|
return true;
|
|
@@ -18204,7 +18482,7 @@ class ConfigCache {
|
|
|
18204
18482
|
}
|
|
18205
18483
|
setWithFileCheck(configName, value, configPath, customTtl) {
|
|
18206
18484
|
try {
|
|
18207
|
-
const stats = existsSync2(configPath) ?
|
|
18485
|
+
const stats = existsSync2(configPath) ? statSync2(configPath) : null;
|
|
18208
18486
|
const fileTimestamp = stats ? stats.mtime : new Date;
|
|
18209
18487
|
this.set(configName, { value, fileTimestamp }, configPath, customTtl);
|
|
18210
18488
|
} catch {
|
|
@@ -18524,10 +18802,10 @@ async function loadConfig({
|
|
|
18524
18802
|
var defaultConfigDir = resolve(process3.cwd(), "config");
|
|
18525
18803
|
var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
|
|
18526
18804
|
function getProjectRoot(filePath, options2 = {}) {
|
|
18527
|
-
let
|
|
18528
|
-
while (
|
|
18529
|
-
|
|
18530
|
-
const finalPath = resolve2(
|
|
18805
|
+
let path4 = process2.cwd();
|
|
18806
|
+
while (path4.includes("storage"))
|
|
18807
|
+
path4 = resolve2(path4, "..");
|
|
18808
|
+
const finalPath = resolve2(path4, filePath || "");
|
|
18531
18809
|
if (options2?.relative)
|
|
18532
18810
|
return relative3(process2.cwd(), finalPath);
|
|
18533
18811
|
return finalPath;
|
|
@@ -19974,10 +20252,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
|
|
|
19974
20252
|
return config3;
|
|
19975
20253
|
const envPrefix = name.toUpperCase().replace(/-/g, "_");
|
|
19976
20254
|
const result = { ...config3 };
|
|
19977
|
-
function processObject(obj,
|
|
20255
|
+
function processObject(obj, path4 = []) {
|
|
19978
20256
|
const result2 = { ...obj };
|
|
19979
20257
|
for (const [key, value] of Object.entries(obj)) {
|
|
19980
|
-
const envPath = [...
|
|
20258
|
+
const envPath = [...path4, key];
|
|
19981
20259
|
const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
19982
20260
|
const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
|
|
19983
20261
|
const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
|
|
@@ -20159,10 +20437,10 @@ async function loadConfig3({
|
|
|
20159
20437
|
var defaultConfigDir2 = resolve3(process6.cwd(), "config");
|
|
20160
20438
|
var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
|
|
20161
20439
|
function getProjectRoot2(filePath, options2 = {}) {
|
|
20162
|
-
let
|
|
20163
|
-
while (
|
|
20164
|
-
|
|
20165
|
-
const finalPath = resolve4(
|
|
20440
|
+
let path4 = process7.cwd();
|
|
20441
|
+
while (path4.includes("storage"))
|
|
20442
|
+
path4 = resolve4(path4, "..");
|
|
20443
|
+
const finalPath = resolve4(path4, filePath || "");
|
|
20166
20444
|
if (options2?.relative)
|
|
20167
20445
|
return relative2(process7.cwd(), finalPath);
|
|
20168
20446
|
return finalPath;
|
|
@@ -21599,10 +21877,10 @@ class EnvVarError extends BunfigError {
|
|
|
21599
21877
|
|
|
21600
21878
|
class FileSystemError extends BunfigError {
|
|
21601
21879
|
code = "FILE_SYSTEM_ERROR";
|
|
21602
|
-
constructor(operation,
|
|
21603
|
-
super(`File system ${operation} failed for "${
|
|
21880
|
+
constructor(operation, path4, cause) {
|
|
21881
|
+
super(`File system ${operation} failed for "${path4}": ${cause.message}`, {
|
|
21604
21882
|
operation,
|
|
21605
|
-
path:
|
|
21883
|
+
path: path4,
|
|
21606
21884
|
originalError: cause.name,
|
|
21607
21885
|
originalMessage: cause.message
|
|
21608
21886
|
});
|
|
@@ -21675,8 +21953,8 @@ var ErrorFactory = {
|
|
|
21675
21953
|
envVar(envKey, envValue, expectedType, configName) {
|
|
21676
21954
|
return new EnvVarError(envKey, envValue, expectedType, configName);
|
|
21677
21955
|
},
|
|
21678
|
-
fileSystem(operation,
|
|
21679
|
-
return new FileSystemError(operation,
|
|
21956
|
+
fileSystem(operation, path4, cause) {
|
|
21957
|
+
return new FileSystemError(operation, path4, cause);
|
|
21680
21958
|
},
|
|
21681
21959
|
typeGeneration(configDir, outputPath, cause) {
|
|
21682
21960
|
return new TypeGenerationError(configDir, outputPath, cause);
|
|
@@ -21812,9 +22090,9 @@ class EnvProcessor {
|
|
|
21812
22090
|
}
|
|
21813
22091
|
return key.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
21814
22092
|
}
|
|
21815
|
-
processObject(obj,
|
|
22093
|
+
processObject(obj, path4, envPrefix, options2) {
|
|
21816
22094
|
for (const [key, value] of Object.entries(obj)) {
|
|
21817
|
-
const envPath = [...
|
|
22095
|
+
const envPath = [...path4, key];
|
|
21818
22096
|
const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
|
|
21819
22097
|
const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
|
|
21820
22098
|
const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
|
|
@@ -21899,9 +22177,9 @@ class EnvProcessor {
|
|
|
21899
22177
|
return this.formatAsText(envVars, configName);
|
|
21900
22178
|
}
|
|
21901
22179
|
}
|
|
21902
|
-
extractEnvVarInfo(obj,
|
|
22180
|
+
extractEnvVarInfo(obj, path4, prefix, envVars) {
|
|
21903
22181
|
for (const [key, value] of Object.entries(obj)) {
|
|
21904
|
-
const envPath = [...
|
|
22182
|
+
const envPath = [...path4, key];
|
|
21905
22183
|
const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
|
|
21906
22184
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
21907
22185
|
this.extractEnvVarInfo(value, envPath, prefix, envVars);
|
|
@@ -22307,8 +22585,8 @@ class ConfigFileLoader {
|
|
|
22307
22585
|
}
|
|
22308
22586
|
async getFileModificationTime(filePath) {
|
|
22309
22587
|
try {
|
|
22310
|
-
const { statSync:
|
|
22311
|
-
const stats =
|
|
22588
|
+
const { statSync: statSync22 } = await import("fs");
|
|
22589
|
+
const stats = statSync22(filePath);
|
|
22312
22590
|
return stats.mtime;
|
|
22313
22591
|
} catch {
|
|
22314
22592
|
return null;
|
|
@@ -22316,15 +22594,15 @@ class ConfigFileLoader {
|
|
|
22316
22594
|
}
|
|
22317
22595
|
async preloadConfigurations(configPaths, options2 = {}) {
|
|
22318
22596
|
const preloaded = new Map;
|
|
22319
|
-
await Promise.allSettled(configPaths.map(async (
|
|
22597
|
+
await Promise.allSettled(configPaths.map(async (path4) => {
|
|
22320
22598
|
try {
|
|
22321
|
-
const result = await this.loadFromPath(
|
|
22599
|
+
const result = await this.loadFromPath(path4, {}, options2);
|
|
22322
22600
|
if (result) {
|
|
22323
|
-
preloaded.set(
|
|
22601
|
+
preloaded.set(path4, result.config);
|
|
22324
22602
|
}
|
|
22325
22603
|
} catch (error45) {
|
|
22326
22604
|
if (options2.verbose) {
|
|
22327
|
-
console.warn(`Failed to preload ${
|
|
22605
|
+
console.warn(`Failed to preload ${path4}:`, error45);
|
|
22328
22606
|
}
|
|
22329
22607
|
}
|
|
22330
22608
|
}));
|
|
@@ -22403,13 +22681,13 @@ class ConfigValidator {
|
|
|
22403
22681
|
warnings
|
|
22404
22682
|
};
|
|
22405
22683
|
}
|
|
22406
|
-
validateObjectAgainstSchema(value, schema,
|
|
22684
|
+
validateObjectAgainstSchema(value, schema, path4, errors3, warnings, options2) {
|
|
22407
22685
|
if (options2.validateTypes && schema.type) {
|
|
22408
22686
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22409
22687
|
const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
22410
22688
|
if (!expectedTypes.includes(actualType)) {
|
|
22411
22689
|
errors3.push({
|
|
22412
|
-
path:
|
|
22690
|
+
path: path4,
|
|
22413
22691
|
message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
|
|
22414
22692
|
expected: expectedTypes.join(" or "),
|
|
22415
22693
|
actual: actualType,
|
|
@@ -22421,7 +22699,7 @@ class ConfigValidator {
|
|
|
22421
22699
|
}
|
|
22422
22700
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
22423
22701
|
errors3.push({
|
|
22424
|
-
path:
|
|
22702
|
+
path: path4,
|
|
22425
22703
|
message: `Value must be one of: ${schema.enum.join(", ")}`,
|
|
22426
22704
|
expected: schema.enum.join(", "),
|
|
22427
22705
|
actual: value,
|
|
@@ -22433,7 +22711,7 @@ class ConfigValidator {
|
|
|
22433
22711
|
if (typeof value === "string") {
|
|
22434
22712
|
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
22435
22713
|
errors3.push({
|
|
22436
|
-
path:
|
|
22714
|
+
path: path4,
|
|
22437
22715
|
message: `String length must be at least ${schema.minLength}`,
|
|
22438
22716
|
expected: `>= ${schema.minLength}`,
|
|
22439
22717
|
actual: value.length,
|
|
@@ -22442,7 +22720,7 @@ class ConfigValidator {
|
|
|
22442
22720
|
}
|
|
22443
22721
|
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
22444
22722
|
errors3.push({
|
|
22445
|
-
path:
|
|
22723
|
+
path: path4,
|
|
22446
22724
|
message: `String length must not exceed ${schema.maxLength}`,
|
|
22447
22725
|
expected: `<= ${schema.maxLength}`,
|
|
22448
22726
|
actual: value.length,
|
|
@@ -22453,7 +22731,7 @@ class ConfigValidator {
|
|
|
22453
22731
|
const regex = new RegExp(schema.pattern);
|
|
22454
22732
|
if (!regex.test(value)) {
|
|
22455
22733
|
errors3.push({
|
|
22456
|
-
path:
|
|
22734
|
+
path: path4,
|
|
22457
22735
|
message: `String does not match pattern ${schema.pattern}`,
|
|
22458
22736
|
expected: schema.pattern,
|
|
22459
22737
|
actual: value,
|
|
@@ -22465,7 +22743,7 @@ class ConfigValidator {
|
|
|
22465
22743
|
if (typeof value === "number") {
|
|
22466
22744
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
22467
22745
|
errors3.push({
|
|
22468
|
-
path:
|
|
22746
|
+
path: path4,
|
|
22469
22747
|
message: `Value must be at least ${schema.minimum}`,
|
|
22470
22748
|
expected: `>= ${schema.minimum}`,
|
|
22471
22749
|
actual: value,
|
|
@@ -22474,7 +22752,7 @@ class ConfigValidator {
|
|
|
22474
22752
|
}
|
|
22475
22753
|
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
22476
22754
|
errors3.push({
|
|
22477
|
-
path:
|
|
22755
|
+
path: path4,
|
|
22478
22756
|
message: `Value must not exceed ${schema.maximum}`,
|
|
22479
22757
|
expected: `<= ${schema.maximum}`,
|
|
22480
22758
|
actual: value,
|
|
@@ -22484,7 +22762,7 @@ class ConfigValidator {
|
|
|
22484
22762
|
}
|
|
22485
22763
|
if (Array.isArray(value) && schema.items) {
|
|
22486
22764
|
for (let i = 0;i < value.length; i++) {
|
|
22487
|
-
const itemPath =
|
|
22765
|
+
const itemPath = path4 ? `${path4}[${i}]` : `[${i}]`;
|
|
22488
22766
|
this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
|
|
22489
22767
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22490
22768
|
return;
|
|
@@ -22496,7 +22774,7 @@ class ConfigValidator {
|
|
|
22496
22774
|
for (const requiredProp of schema.required) {
|
|
22497
22775
|
if (!(requiredProp in obj)) {
|
|
22498
22776
|
errors3.push({
|
|
22499
|
-
path:
|
|
22777
|
+
path: path4 ? `${path4}.${requiredProp}` : requiredProp,
|
|
22500
22778
|
message: `Missing required property '${requiredProp}'`,
|
|
22501
22779
|
expected: "required",
|
|
22502
22780
|
rule: "required"
|
|
@@ -22509,7 +22787,7 @@ class ConfigValidator {
|
|
|
22509
22787
|
if (schema.properties) {
|
|
22510
22788
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
22511
22789
|
if (propName in obj) {
|
|
22512
|
-
const propPath =
|
|
22790
|
+
const propPath = path4 ? `${path4}.${propName}` : propName;
|
|
22513
22791
|
this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
|
|
22514
22792
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22515
22793
|
return;
|
|
@@ -22521,7 +22799,7 @@ class ConfigValidator {
|
|
|
22521
22799
|
for (const propName of Object.keys(obj)) {
|
|
22522
22800
|
if (!allowedProps.has(propName)) {
|
|
22523
22801
|
warnings.push({
|
|
22524
|
-
path:
|
|
22802
|
+
path: path4 ? `${path4}.${propName}` : propName,
|
|
22525
22803
|
message: `Additional property '${propName}' is not allowed`,
|
|
22526
22804
|
rule: "additionalProperties"
|
|
22527
22805
|
});
|
|
@@ -22555,12 +22833,12 @@ class ConfigValidator {
|
|
|
22555
22833
|
warnings
|
|
22556
22834
|
};
|
|
22557
22835
|
}
|
|
22558
|
-
validateWithRule(value, rule,
|
|
22836
|
+
validateWithRule(value, rule, path4) {
|
|
22559
22837
|
const errors3 = [];
|
|
22560
22838
|
if (rule.required && (value === undefined || value === null)) {
|
|
22561
22839
|
errors3.push({
|
|
22562
|
-
path:
|
|
22563
|
-
message: rule.message || `Property '${
|
|
22840
|
+
path: path4,
|
|
22841
|
+
message: rule.message || `Property '${path4}' is required`,
|
|
22564
22842
|
expected: "required",
|
|
22565
22843
|
rule: "required"
|
|
22566
22844
|
});
|
|
@@ -22573,7 +22851,7 @@ class ConfigValidator {
|
|
|
22573
22851
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22574
22852
|
if (actualType !== rule.type) {
|
|
22575
22853
|
errors3.push({
|
|
22576
|
-
path:
|
|
22854
|
+
path: path4,
|
|
22577
22855
|
message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
|
|
22578
22856
|
expected: rule.type,
|
|
22579
22857
|
actual: actualType,
|
|
@@ -22585,7 +22863,7 @@ class ConfigValidator {
|
|
|
22585
22863
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22586
22864
|
if (length < rule.min) {
|
|
22587
22865
|
errors3.push({
|
|
22588
|
-
path:
|
|
22866
|
+
path: path4,
|
|
22589
22867
|
message: rule.message || `Value must be at least ${rule.min}`,
|
|
22590
22868
|
expected: `>= ${rule.min}`,
|
|
22591
22869
|
actual: length,
|
|
@@ -22597,7 +22875,7 @@ class ConfigValidator {
|
|
|
22597
22875
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22598
22876
|
if (length > rule.max) {
|
|
22599
22877
|
errors3.push({
|
|
22600
|
-
path:
|
|
22878
|
+
path: path4,
|
|
22601
22879
|
message: rule.message || `Value must not exceed ${rule.max}`,
|
|
22602
22880
|
expected: `<= ${rule.max}`,
|
|
22603
22881
|
actual: length,
|
|
@@ -22608,7 +22886,7 @@ class ConfigValidator {
|
|
|
22608
22886
|
if (rule.pattern && typeof value === "string") {
|
|
22609
22887
|
if (!rule.pattern.test(value)) {
|
|
22610
22888
|
errors3.push({
|
|
22611
|
-
path:
|
|
22889
|
+
path: path4,
|
|
22612
22890
|
message: rule.message || `Value does not match pattern ${rule.pattern}`,
|
|
22613
22891
|
expected: rule.pattern.toString(),
|
|
22614
22892
|
actual: value,
|
|
@@ -22618,7 +22896,7 @@ class ConfigValidator {
|
|
|
22618
22896
|
}
|
|
22619
22897
|
if (rule.enum && !rule.enum.includes(value)) {
|
|
22620
22898
|
errors3.push({
|
|
22621
|
-
path:
|
|
22899
|
+
path: path4,
|
|
22622
22900
|
message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
|
|
22623
22901
|
expected: rule.enum.join(", "),
|
|
22624
22902
|
actual: value,
|
|
@@ -22629,7 +22907,7 @@ class ConfigValidator {
|
|
|
22629
22907
|
const customError = rule.validator(value);
|
|
22630
22908
|
if (customError) {
|
|
22631
22909
|
errors3.push({
|
|
22632
|
-
path:
|
|
22910
|
+
path: path4,
|
|
22633
22911
|
message: rule.message || customError,
|
|
22634
22912
|
rule: "custom"
|
|
22635
22913
|
});
|
|
@@ -22637,10 +22915,10 @@ class ConfigValidator {
|
|
|
22637
22915
|
}
|
|
22638
22916
|
return errors3;
|
|
22639
22917
|
}
|
|
22640
|
-
getValueByPath(obj,
|
|
22641
|
-
if (!
|
|
22918
|
+
getValueByPath(obj, path4) {
|
|
22919
|
+
if (!path4)
|
|
22642
22920
|
return obj;
|
|
22643
|
-
const keys =
|
|
22921
|
+
const keys = path4.split(".");
|
|
22644
22922
|
let current = obj;
|
|
22645
22923
|
for (const key of keys) {
|
|
22646
22924
|
if (current && typeof current === "object" && key in current) {
|
|
@@ -23066,10 +23344,10 @@ async function loadConfig5(options2) {
|
|
|
23066
23344
|
function applyEnvVarsToConfig2(name, config4, verbose = false) {
|
|
23067
23345
|
const _envProcessor = new EnvProcessor;
|
|
23068
23346
|
const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
23069
|
-
function processConfigLevel(obj,
|
|
23347
|
+
function processConfigLevel(obj, path4 = []) {
|
|
23070
23348
|
const result = { ...obj };
|
|
23071
23349
|
for (const [key, value] of Object.entries(obj)) {
|
|
23072
|
-
const currentPath = [...
|
|
23350
|
+
const currentPath = [...path4, key];
|
|
23073
23351
|
const envKeys = [
|
|
23074
23352
|
`${envPrefix}_${currentPath.join("_").toUpperCase()}`,
|
|
23075
23353
|
`${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
|
|
@@ -23114,8 +23392,47 @@ var defaultConfigDir3 = resolve7(process12.cwd(), "config");
|
|
|
23114
23392
|
var defaultGeneratedDir3 = resolve7(process12.cwd(), "src/generated");
|
|
23115
23393
|
|
|
23116
23394
|
// src/config.ts
|
|
23395
|
+
import { access as access3, mkdir as mkdir3, readFile, writeFile as writeFile3 } from "fs/promises";
|
|
23117
23396
|
import { homedir as homedir3 } from "os";
|
|
23118
23397
|
import { dirname as dirname6, isAbsolute as isAbsolute2, join as join4, normalize, resolve as resolve8 } from "path";
|
|
23398
|
+
var DEFAULT_RESERVED_SLASH_COMMANDS = [
|
|
23399
|
+
"agent",
|
|
23400
|
+
"agents",
|
|
23401
|
+
"compact",
|
|
23402
|
+
"connect",
|
|
23403
|
+
"details",
|
|
23404
|
+
"editor",
|
|
23405
|
+
"exit",
|
|
23406
|
+
"export",
|
|
23407
|
+
"fork",
|
|
23408
|
+
"help",
|
|
23409
|
+
"init",
|
|
23410
|
+
"mcp",
|
|
23411
|
+
"model",
|
|
23412
|
+
"models",
|
|
23413
|
+
"new",
|
|
23414
|
+
"open",
|
|
23415
|
+
"redo",
|
|
23416
|
+
"sessions",
|
|
23417
|
+
"share",
|
|
23418
|
+
"skills",
|
|
23419
|
+
"terminal",
|
|
23420
|
+
"themes",
|
|
23421
|
+
"thinking",
|
|
23422
|
+
"undo",
|
|
23423
|
+
"unshare"
|
|
23424
|
+
];
|
|
23425
|
+
var DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT = [
|
|
23426
|
+
"You are selecting the most relevant dynamic skills for a user request.",
|
|
23427
|
+
"Return strict JSON only with this shape:",
|
|
23428
|
+
'{"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
|
|
23429
|
+
"Only recommend skills from the provided catalog.",
|
|
23430
|
+
"Prefer the smallest set of high-confidence matches."
|
|
23431
|
+
].join(" ");
|
|
23432
|
+
var MANAGED_PLUGIN_CONFIG_DIRECTORY = join4(homedir3(), ".config", "opencode");
|
|
23433
|
+
var MANAGED_TUI_COMMANDS_DIRECTORY = join4(MANAGED_PLUGIN_CONFIG_DIRECTORY, "commands");
|
|
23434
|
+
var MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
|
|
23435
|
+
var MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
|
|
23119
23436
|
function getOpenCodeConfigPaths() {
|
|
23120
23437
|
const home = homedir3();
|
|
23121
23438
|
const paths = [];
|
|
@@ -23135,14 +23452,14 @@ function getOpenCodeConfigPaths() {
|
|
|
23135
23452
|
paths.push(join4(home, ".opencode"));
|
|
23136
23453
|
return paths;
|
|
23137
23454
|
}
|
|
23138
|
-
function expandTildePath(
|
|
23139
|
-
if (
|
|
23455
|
+
function expandTildePath(path4) {
|
|
23456
|
+
if (path4 === "~") {
|
|
23140
23457
|
return homedir3();
|
|
23141
23458
|
}
|
|
23142
|
-
if (
|
|
23143
|
-
return join4(homedir3(),
|
|
23459
|
+
if (path4.startsWith("~/")) {
|
|
23460
|
+
return join4(homedir3(), path4.slice(2));
|
|
23144
23461
|
}
|
|
23145
|
-
return
|
|
23462
|
+
return path4;
|
|
23146
23463
|
}
|
|
23147
23464
|
var createPathKey = (absolutePath) => {
|
|
23148
23465
|
const normalizedPath = normalize(absolutePath);
|
|
@@ -23206,34 +23523,303 @@ var defaultSkillBasePaths = [
|
|
|
23206
23523
|
join4(homedir3(), ".claude", "skills"),
|
|
23207
23524
|
join4(homedir3(), ".agents", "skills")
|
|
23208
23525
|
];
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23526
|
+
function createDefaultSkillRecommendConfig() {
|
|
23527
|
+
return {
|
|
23528
|
+
strategy: "heuristic",
|
|
23529
|
+
model: "",
|
|
23530
|
+
systemPrompt: DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
|
|
23531
|
+
};
|
|
23532
|
+
}
|
|
23533
|
+
function createDefaultNotificationConfig() {
|
|
23534
|
+
return {
|
|
23535
|
+
enabled: false,
|
|
23536
|
+
success: true,
|
|
23537
|
+
errors: true
|
|
23538
|
+
};
|
|
23539
|
+
}
|
|
23540
|
+
function createDefaultTuiCommandMirrorConfig() {
|
|
23541
|
+
return {
|
|
23542
|
+
enabled: true,
|
|
23543
|
+
directory: MANAGED_TUI_COMMANDS_DIRECTORY
|
|
23544
|
+
};
|
|
23545
|
+
}
|
|
23546
|
+
function createDefaultPluginConfig() {
|
|
23547
|
+
return {
|
|
23213
23548
|
debug: false,
|
|
23214
23549
|
basePaths: defaultSkillBasePaths,
|
|
23215
|
-
promptRenderer: "xml",
|
|
23216
|
-
modelRenderers: {},
|
|
23217
23550
|
slashCommandName: "skill",
|
|
23218
|
-
enableSkillAliases: true
|
|
23551
|
+
enableSkillAliases: true,
|
|
23552
|
+
reservedSlashCommands: [...DEFAULT_RESERVED_SLASH_COMMANDS],
|
|
23553
|
+
notifications: createDefaultNotificationConfig(),
|
|
23554
|
+
skillRecommend: createDefaultSkillRecommendConfig(),
|
|
23555
|
+
tuiCommandMirror: createDefaultTuiCommandMirrorConfig()
|
|
23556
|
+
};
|
|
23557
|
+
}
|
|
23558
|
+
function createConfigOptions(defaultConfig3) {
|
|
23559
|
+
return {
|
|
23560
|
+
name: "opencode-dynamic-skills",
|
|
23561
|
+
cwd: "./",
|
|
23562
|
+
defaultConfig: defaultConfig3
|
|
23563
|
+
};
|
|
23564
|
+
}
|
|
23565
|
+
function stripJsonComments(input) {
|
|
23566
|
+
let output = "";
|
|
23567
|
+
let inString = false;
|
|
23568
|
+
let escaped = false;
|
|
23569
|
+
let inLineComment = false;
|
|
23570
|
+
let inBlockComment = false;
|
|
23571
|
+
for (let index = 0;index < input.length; index += 1) {
|
|
23572
|
+
const current = input[index] ?? "";
|
|
23573
|
+
const next = input[index + 1] ?? "";
|
|
23574
|
+
if (inLineComment) {
|
|
23575
|
+
if (current === `
|
|
23576
|
+
` || current === "\r") {
|
|
23577
|
+
inLineComment = false;
|
|
23578
|
+
output += current;
|
|
23579
|
+
}
|
|
23580
|
+
continue;
|
|
23581
|
+
}
|
|
23582
|
+
if (inBlockComment) {
|
|
23583
|
+
if (current === "*" && next === "/") {
|
|
23584
|
+
inBlockComment = false;
|
|
23585
|
+
index += 1;
|
|
23586
|
+
continue;
|
|
23587
|
+
}
|
|
23588
|
+
if (current === `
|
|
23589
|
+
` || current === "\r") {
|
|
23590
|
+
output += current;
|
|
23591
|
+
}
|
|
23592
|
+
continue;
|
|
23593
|
+
}
|
|
23594
|
+
if (inString) {
|
|
23595
|
+
output += current;
|
|
23596
|
+
if (escaped) {
|
|
23597
|
+
escaped = false;
|
|
23598
|
+
continue;
|
|
23599
|
+
}
|
|
23600
|
+
if (current === "\\") {
|
|
23601
|
+
escaped = true;
|
|
23602
|
+
continue;
|
|
23603
|
+
}
|
|
23604
|
+
if (current === '"') {
|
|
23605
|
+
inString = false;
|
|
23606
|
+
}
|
|
23607
|
+
continue;
|
|
23608
|
+
}
|
|
23609
|
+
if (current === "/" && next === "/") {
|
|
23610
|
+
inLineComment = true;
|
|
23611
|
+
index += 1;
|
|
23612
|
+
continue;
|
|
23613
|
+
}
|
|
23614
|
+
if (current === "/" && next === "*") {
|
|
23615
|
+
inBlockComment = true;
|
|
23616
|
+
index += 1;
|
|
23617
|
+
continue;
|
|
23618
|
+
}
|
|
23619
|
+
if (current === '"') {
|
|
23620
|
+
inString = true;
|
|
23621
|
+
}
|
|
23622
|
+
output += current;
|
|
23219
23623
|
}
|
|
23220
|
-
|
|
23624
|
+
return output;
|
|
23625
|
+
}
|
|
23626
|
+
function removeTrailingJsonCommas(input) {
|
|
23627
|
+
let output = "";
|
|
23628
|
+
let inString = false;
|
|
23629
|
+
let escaped = false;
|
|
23630
|
+
for (let index = 0;index < input.length; index += 1) {
|
|
23631
|
+
const current = input[index] ?? "";
|
|
23632
|
+
if (inString) {
|
|
23633
|
+
output += current;
|
|
23634
|
+
if (escaped) {
|
|
23635
|
+
escaped = false;
|
|
23636
|
+
continue;
|
|
23637
|
+
}
|
|
23638
|
+
if (current === "\\") {
|
|
23639
|
+
escaped = true;
|
|
23640
|
+
continue;
|
|
23641
|
+
}
|
|
23642
|
+
if (current === '"') {
|
|
23643
|
+
inString = false;
|
|
23644
|
+
}
|
|
23645
|
+
continue;
|
|
23646
|
+
}
|
|
23647
|
+
if (current === '"') {
|
|
23648
|
+
inString = true;
|
|
23649
|
+
output += current;
|
|
23650
|
+
continue;
|
|
23651
|
+
}
|
|
23652
|
+
if (current === ",") {
|
|
23653
|
+
let lookahead = index + 1;
|
|
23654
|
+
while (lookahead < input.length && /\s/.test(input[lookahead] ?? "")) {
|
|
23655
|
+
lookahead += 1;
|
|
23656
|
+
}
|
|
23657
|
+
const nextToken = input[lookahead];
|
|
23658
|
+
if (nextToken === "}" || nextToken === "]") {
|
|
23659
|
+
continue;
|
|
23660
|
+
}
|
|
23661
|
+
}
|
|
23662
|
+
output += current;
|
|
23663
|
+
}
|
|
23664
|
+
return output;
|
|
23665
|
+
}
|
|
23666
|
+
function parseJsonc(input) {
|
|
23667
|
+
return JSON.parse(removeTrailingJsonCommas(stripJsonComments(input)));
|
|
23668
|
+
}
|
|
23669
|
+
function trimString(value) {
|
|
23670
|
+
return value?.trim() ?? "";
|
|
23671
|
+
}
|
|
23672
|
+
function mergePluginConfig(baseConfig, override) {
|
|
23673
|
+
const skillRecommend = {
|
|
23674
|
+
...baseConfig.skillRecommend,
|
|
23675
|
+
...override.skillRecommend
|
|
23676
|
+
};
|
|
23677
|
+
const notifications = {
|
|
23678
|
+
...baseConfig.notifications,
|
|
23679
|
+
...override.notifications
|
|
23680
|
+
};
|
|
23681
|
+
const tuiCommandMirror = {
|
|
23682
|
+
...baseConfig.tuiCommandMirror,
|
|
23683
|
+
...override.tuiCommandMirror
|
|
23684
|
+
};
|
|
23685
|
+
const overrideReservedSlashCommands = override.reservedSlashCommands?.map((command) => trimString(command)).filter(Boolean) ?? [];
|
|
23686
|
+
return {
|
|
23687
|
+
...baseConfig,
|
|
23688
|
+
...override,
|
|
23689
|
+
skillRecommend: {
|
|
23690
|
+
strategy: skillRecommend.strategy === "model" ? "model" : "heuristic",
|
|
23691
|
+
model: trimString(skillRecommend.model),
|
|
23692
|
+
systemPrompt: trimString(skillRecommend.systemPrompt) || DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT
|
|
23693
|
+
},
|
|
23694
|
+
reservedSlashCommands: overrideReservedSlashCommands.length > 0 ? overrideReservedSlashCommands : baseConfig.reservedSlashCommands,
|
|
23695
|
+
notifications: {
|
|
23696
|
+
enabled: notifications.enabled === true,
|
|
23697
|
+
success: notifications.success !== false,
|
|
23698
|
+
errors: notifications.errors !== false
|
|
23699
|
+
},
|
|
23700
|
+
tuiCommandMirror: {
|
|
23701
|
+
enabled: tuiCommandMirror.enabled === true,
|
|
23702
|
+
directory: trimString(tuiCommandMirror.directory) || MANAGED_TUI_COMMANDS_DIRECTORY
|
|
23703
|
+
}
|
|
23704
|
+
};
|
|
23705
|
+
}
|
|
23706
|
+
function getManagedPluginConfigPaths(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23707
|
+
return [
|
|
23708
|
+
join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME),
|
|
23709
|
+
join4(configDirectory, MANAGED_PLUGIN_JSON_FILENAME)
|
|
23710
|
+
];
|
|
23711
|
+
}
|
|
23712
|
+
async function fileExists(filePath) {
|
|
23713
|
+
try {
|
|
23714
|
+
await access3(filePath);
|
|
23715
|
+
return true;
|
|
23716
|
+
} catch {
|
|
23717
|
+
return false;
|
|
23718
|
+
}
|
|
23719
|
+
}
|
|
23720
|
+
function renderManagedPluginConfigJsonc(config3 = createDefaultPluginConfig()) {
|
|
23721
|
+
const basePaths = config3.basePaths.map((basePath) => ` ${JSON.stringify(basePath)}`).join(`,
|
|
23722
|
+
`);
|
|
23723
|
+
return [
|
|
23724
|
+
"{",
|
|
23725
|
+
" // Enable verbose plugin logging output.",
|
|
23726
|
+
` "debug": ${JSON.stringify(config3.debug)},`,
|
|
23727
|
+
"",
|
|
23728
|
+
" // Global skill roots. Project-local .opencode/.claude/.agents paths are appended automatically.",
|
|
23729
|
+
' "basePaths": [',
|
|
23730
|
+
basePaths,
|
|
23731
|
+
" ],",
|
|
23732
|
+
"",
|
|
23733
|
+
" // Explicit slash entrypoint, for example: /skill git-release",
|
|
23734
|
+
` "slashCommandName": ${JSON.stringify(config3.slashCommandName)},`,
|
|
23735
|
+
"",
|
|
23736
|
+
" // When true, /<skill-name> alias invocations can be intercepted after submit.",
|
|
23737
|
+
` "enableSkillAliases": ${JSON.stringify(config3.enableSkillAliases)},`,
|
|
23738
|
+
"",
|
|
23739
|
+
" // Builtin or protected slash commands that should never be intercepted as /<skill-name> aliases.",
|
|
23740
|
+
' "reservedSlashCommands": [',
|
|
23741
|
+
config3.reservedSlashCommands.map((command) => ` ${JSON.stringify(command)}`).join(`,
|
|
23742
|
+
`),
|
|
23743
|
+
" ],",
|
|
23744
|
+
"",
|
|
23745
|
+
" // Best-effort OS notifications triggered by this plugin.",
|
|
23746
|
+
' "notifications": {',
|
|
23747
|
+
` "enabled": ${JSON.stringify(config3.notifications.enabled)},`,
|
|
23748
|
+
` "success": ${JSON.stringify(config3.notifications.success)},`,
|
|
23749
|
+
` "errors": ${JSON.stringify(config3.notifications.errors)}`,
|
|
23750
|
+
" },",
|
|
23751
|
+
"",
|
|
23752
|
+
' // skill_recommend strategy. Set strategy to "model" and fill model with provider/model to use an internal LLM call.',
|
|
23753
|
+
' "skillRecommend": {',
|
|
23754
|
+
` "strategy": ${JSON.stringify(config3.skillRecommend.strategy)},`,
|
|
23755
|
+
" // Model format: provider/model, for example openai/gpt-5.2",
|
|
23756
|
+
` "model": ${JSON.stringify(config3.skillRecommend.model)},`,
|
|
23757
|
+
' // The model must return strict JSON: {"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
|
|
23758
|
+
` "systemPrompt": ${JSON.stringify(config3.skillRecommend.systemPrompt)}`,
|
|
23759
|
+
" },",
|
|
23760
|
+
"",
|
|
23761
|
+
" // TUI compatibility layer. Enabled by default so dynamic skills are mirrored as proxy command files and can appear in TUI slash autocomplete after restart.",
|
|
23762
|
+
' "tuiCommandMirror": {',
|
|
23763
|
+
` "enabled": ${JSON.stringify(config3.tuiCommandMirror.enabled)},`,
|
|
23764
|
+
` "directory": ${JSON.stringify(config3.tuiCommandMirror.directory)}`,
|
|
23765
|
+
" }",
|
|
23766
|
+
"}",
|
|
23767
|
+
""
|
|
23768
|
+
].join(`
|
|
23769
|
+
`);
|
|
23770
|
+
}
|
|
23771
|
+
async function ensureManagedPluginConfigFile(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23772
|
+
for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
|
|
23773
|
+
if (await fileExists(candidatePath)) {
|
|
23774
|
+
return candidatePath;
|
|
23775
|
+
}
|
|
23776
|
+
}
|
|
23777
|
+
await mkdir3(configDirectory, { recursive: true });
|
|
23778
|
+
const configPath = join4(configDirectory, MANAGED_PLUGIN_JSONC_FILENAME);
|
|
23779
|
+
await writeFile3(configPath, renderManagedPluginConfigJsonc(), "utf8");
|
|
23780
|
+
return configPath;
|
|
23781
|
+
}
|
|
23782
|
+
async function loadManagedPluginConfig(configDirectory = MANAGED_PLUGIN_CONFIG_DIRECTORY) {
|
|
23783
|
+
for (const candidatePath of getManagedPluginConfigPaths(configDirectory)) {
|
|
23784
|
+
if (!await fileExists(candidatePath)) {
|
|
23785
|
+
continue;
|
|
23786
|
+
}
|
|
23787
|
+
const content = await readFile(candidatePath, "utf8");
|
|
23788
|
+
if (candidatePath.endsWith(".jsonc")) {
|
|
23789
|
+
return parseJsonc(content);
|
|
23790
|
+
}
|
|
23791
|
+
return JSON.parse(content);
|
|
23792
|
+
}
|
|
23793
|
+
return {};
|
|
23794
|
+
}
|
|
23221
23795
|
async function getPluginConfig(ctx) {
|
|
23222
|
-
const
|
|
23796
|
+
const defaultConfig3 = createDefaultPluginConfig();
|
|
23797
|
+
await ensureManagedPluginConfigFile();
|
|
23798
|
+
const managedConfig = await loadManagedPluginConfig();
|
|
23799
|
+
const resolvedConfig = await loadConfig5(createConfigOptions(defaultConfig3));
|
|
23800
|
+
const mergedConfig = mergePluginConfig(mergePluginConfig(defaultConfig3, managedConfig), resolvedConfig);
|
|
23223
23801
|
const configuredBasePaths = [
|
|
23224
|
-
...
|
|
23802
|
+
...mergedConfig.basePaths,
|
|
23225
23803
|
...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
|
|
23226
23804
|
];
|
|
23227
|
-
|
|
23228
|
-
|
|
23805
|
+
mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
|
|
23806
|
+
mergedConfig.tuiCommandMirror = {
|
|
23807
|
+
enabled: mergedConfig.tuiCommandMirror.enabled,
|
|
23808
|
+
directory: resolveBasePath(mergedConfig.tuiCommandMirror.directory, ctx.directory)
|
|
23809
|
+
};
|
|
23810
|
+
return mergedConfig;
|
|
23229
23811
|
}
|
|
23230
23812
|
|
|
23231
23813
|
// src/commands/SlashCommand.ts
|
|
23232
23814
|
var SLASH_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:slash-expanded -->";
|
|
23233
23815
|
var RECOMMEND_COMMAND_SENTINEL = "<!-- opencode-dynamic-skills:skill-recommend-expanded -->";
|
|
23234
|
-
function
|
|
23816
|
+
function normalizeSkillSelector4(selector) {
|
|
23235
23817
|
return selector.trim().toLowerCase().replace(/[/-]/g, "_");
|
|
23236
23818
|
}
|
|
23819
|
+
function isReservedSlashCommand(invocationName, reservedSlashCommands) {
|
|
23820
|
+
const normalizedInvocationName = invocationName.trim().toLowerCase();
|
|
23821
|
+
return reservedSlashCommands.some((reservedCommand) => reservedCommand.trim().toLowerCase() === normalizedInvocationName);
|
|
23822
|
+
}
|
|
23237
23823
|
function parseSlashCommand(text, slashCommandName) {
|
|
23238
23824
|
const trimmedText = text.trim();
|
|
23239
23825
|
if (!trimmedText.startsWith("/")) {
|
|
@@ -23268,21 +23854,21 @@ function findSkillBySelector(registry2, selector) {
|
|
|
23268
23854
|
if (directMatch) {
|
|
23269
23855
|
return directMatch;
|
|
23270
23856
|
}
|
|
23271
|
-
const normalizedSelector =
|
|
23857
|
+
const normalizedSelector = normalizeSkillSelector4(selector);
|
|
23272
23858
|
for (const skill of registry2.controller.skills) {
|
|
23273
|
-
if (
|
|
23859
|
+
if (normalizeSkillSelector4(skill.toolName) === normalizedSelector) {
|
|
23274
23860
|
return skill;
|
|
23275
23861
|
}
|
|
23276
|
-
if (
|
|
23862
|
+
if (normalizeSkillSelector4(skill.name) === normalizedSelector) {
|
|
23277
23863
|
return skill;
|
|
23278
23864
|
}
|
|
23279
23865
|
}
|
|
23280
23866
|
return null;
|
|
23281
23867
|
}
|
|
23282
|
-
function renderSlashSkillPrompt(args) {
|
|
23868
|
+
async function renderSlashSkillPrompt(args) {
|
|
23283
23869
|
return [
|
|
23284
23870
|
SLASH_COMMAND_SENTINEL,
|
|
23285
|
-
formatLoadedSkill({
|
|
23871
|
+
await formatLoadedSkill({
|
|
23286
23872
|
invocationName: args.invocationName,
|
|
23287
23873
|
skill: args.skill,
|
|
23288
23874
|
userMessage: args.userPrompt || "Apply this skill to the current request."
|
|
@@ -23328,37 +23914,40 @@ async function rewriteSlashCommandText(args) {
|
|
|
23328
23914
|
if (isAliasInvocation && !args.enableSkillAliases) {
|
|
23329
23915
|
return null;
|
|
23330
23916
|
}
|
|
23917
|
+
if (isAliasInvocation && isReservedSlashCommand(parsedCommand.invocationName, args.reservedSlashCommands ?? [])) {
|
|
23918
|
+
return null;
|
|
23919
|
+
}
|
|
23331
23920
|
await args.registry.controller.ready.whenReady();
|
|
23332
23921
|
const skill = findSkillBySelector(args.registry, parsedCommand.skillSelector);
|
|
23333
23922
|
if (!skill) {
|
|
23334
23923
|
return null;
|
|
23335
23924
|
}
|
|
23336
|
-
return renderSlashSkillPrompt({
|
|
23925
|
+
return await renderSlashSkillPrompt({
|
|
23337
23926
|
invocationName: parsedCommand.invocationName,
|
|
23338
23927
|
skill,
|
|
23339
23928
|
userPrompt: parsedCommand.userPrompt
|
|
23340
23929
|
});
|
|
23341
23930
|
}
|
|
23342
23931
|
|
|
23343
|
-
// src/
|
|
23344
|
-
|
|
23345
|
-
|
|
23346
|
-
|
|
23347
|
-
|
|
23348
|
-
|
|
23349
|
-
|
|
23350
|
-
|
|
23351
|
-
|
|
23352
|
-
|
|
23353
|
-
|
|
23354
|
-
|
|
23355
|
-
|
|
23356
|
-
|
|
23357
|
-
|
|
23358
|
-
|
|
23359
|
-
};
|
|
23360
|
-
return
|
|
23361
|
-
}
|
|
23932
|
+
// src/commands/MirroredSkillCommand.ts
|
|
23933
|
+
async function rewriteMirroredSkillCommandParts(args) {
|
|
23934
|
+
if (!args.mirroredCommands.has(args.commandName)) {
|
|
23935
|
+
return false;
|
|
23936
|
+
}
|
|
23937
|
+
await args.registry.controller.ready.whenReady();
|
|
23938
|
+
const skill = findSkillBySelector(args.registry, args.commandName);
|
|
23939
|
+
if (!skill) {
|
|
23940
|
+
return false;
|
|
23941
|
+
}
|
|
23942
|
+
const preservedParts = args.output.parts.filter((part) => part.type !== "text");
|
|
23943
|
+
const text = await formatLoadedSkill({
|
|
23944
|
+
skill,
|
|
23945
|
+
invocationName: args.commandName,
|
|
23946
|
+
userMessage: args.commandArguments
|
|
23947
|
+
});
|
|
23948
|
+
args.output.parts = [{ type: "text", text }, ...preservedParts];
|
|
23949
|
+
return true;
|
|
23950
|
+
}
|
|
23362
23951
|
|
|
23363
23952
|
// src/lib/xml.ts
|
|
23364
23953
|
function escapeXml(str2) {
|
|
@@ -23413,6 +24002,7 @@ var createXmlPromptRenderer = () => {
|
|
|
23413
24002
|
...skill,
|
|
23414
24003
|
linkedResources: extractSkillLinks(skill.content),
|
|
23415
24004
|
skillRootInstruction: "Resolve linked files relative to the skill root and use skill_resource with the exact root-relative path.",
|
|
24005
|
+
files: resourceMapToArray(skill.files),
|
|
23416
24006
|
references: resourceMapToArray(skill.references),
|
|
23417
24007
|
scripts: resourceMapToArray(skill.scripts),
|
|
23418
24008
|
assets: resourceMapToArray(skill.assets)
|
|
@@ -23443,362 +24033,244 @@ var createXmlPromptRenderer = () => {
|
|
|
23443
24033
|
return renderer;
|
|
23444
24034
|
};
|
|
23445
24035
|
|
|
23446
|
-
//
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
}
|
|
23455
|
-
return keys;
|
|
23456
|
-
}
|
|
23457
|
-
function _objectSpread(target) {
|
|
23458
|
-
for (var i = 1;i < arguments.length; i++) {
|
|
23459
|
-
var source = arguments[i] != null ? arguments[i] : {};
|
|
23460
|
-
i % 2 ? ownKeys(Object(source), true).forEach(function(key) {
|
|
23461
|
-
_defineProperty(target, key, source[key]);
|
|
23462
|
-
}) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function(key) {
|
|
23463
|
-
Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
|
|
23464
|
-
});
|
|
23465
|
-
}
|
|
23466
|
-
return target;
|
|
24036
|
+
// src/services/Notifier.ts
|
|
24037
|
+
import { execFile } from "child_process";
|
|
24038
|
+
import { promisify } from "util";
|
|
24039
|
+
var execFileAsync = promisify(execFile);
|
|
24040
|
+
var NOTIFICATION_TITLE = "OpenCode Dynamic Skills";
|
|
24041
|
+
function createDefaultRunner() {
|
|
24042
|
+
return async (command, args) => {
|
|
24043
|
+
await execFileAsync(command, args);
|
|
24044
|
+
};
|
|
23467
24045
|
}
|
|
23468
|
-
function
|
|
23469
|
-
|
|
23470
|
-
if (
|
|
23471
|
-
|
|
23472
|
-
} else {
|
|
23473
|
-
obj[key] = value;
|
|
24046
|
+
function truncateNotificationText(value, maxLength = 180) {
|
|
24047
|
+
const normalized = value.replace(/\s+/g, " ").trim();
|
|
24048
|
+
if (normalized.length <= maxLength) {
|
|
24049
|
+
return normalized;
|
|
23474
24050
|
}
|
|
23475
|
-
return
|
|
24051
|
+
return `${normalized.slice(0, maxLength - 1)}\u2026`;
|
|
23476
24052
|
}
|
|
23477
|
-
function
|
|
23478
|
-
|
|
23479
|
-
return typeof key === "symbol" ? key : String(key);
|
|
24053
|
+
function escapeAppleScript(value) {
|
|
24054
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
23480
24055
|
}
|
|
23481
|
-
function
|
|
23482
|
-
|
|
23483
|
-
return input;
|
|
23484
|
-
var prim = input[Symbol.toPrimitive];
|
|
23485
|
-
if (prim !== undefined) {
|
|
23486
|
-
var res = prim.call(input, hint || "default");
|
|
23487
|
-
if (typeof res !== "object")
|
|
23488
|
-
return res;
|
|
23489
|
-
throw new TypeError("@@toPrimitive must return a primitive value.");
|
|
23490
|
-
}
|
|
23491
|
-
return (hint === "string" ? String : Number)(input);
|
|
23492
|
-
}
|
|
23493
|
-
var dedent = createDedent({});
|
|
23494
|
-
var dedent_default = dedent;
|
|
23495
|
-
function createDedent(options3) {
|
|
23496
|
-
dedent2.withOptions = (newOptions) => createDedent(_objectSpread(_objectSpread({}, options3), newOptions));
|
|
23497
|
-
return dedent2;
|
|
23498
|
-
function dedent2(strings, ...values) {
|
|
23499
|
-
const raw = typeof strings === "string" ? [strings] : strings.raw;
|
|
23500
|
-
const {
|
|
23501
|
-
alignValues = false,
|
|
23502
|
-
escapeSpecialCharacters = Array.isArray(strings),
|
|
23503
|
-
trimWhitespace = true
|
|
23504
|
-
} = options3;
|
|
23505
|
-
let result = "";
|
|
23506
|
-
for (let i = 0;i < raw.length; i++) {
|
|
23507
|
-
let next = raw[i];
|
|
23508
|
-
if (escapeSpecialCharacters) {
|
|
23509
|
-
next = next.replace(/\\\n[ \t]*/g, "").replace(/\\`/g, "`").replace(/\\\$/g, "$").replace(/\\\{/g, "{");
|
|
23510
|
-
}
|
|
23511
|
-
result += next;
|
|
23512
|
-
if (i < values.length) {
|
|
23513
|
-
const value = alignValues ? alignValue(values[i], result) : values[i];
|
|
23514
|
-
result += value;
|
|
23515
|
-
}
|
|
23516
|
-
}
|
|
23517
|
-
const lines = result.split(`
|
|
23518
|
-
`);
|
|
23519
|
-
let mindent = null;
|
|
23520
|
-
for (const l of lines) {
|
|
23521
|
-
const m = l.match(/^(\s+)\S+/);
|
|
23522
|
-
if (m) {
|
|
23523
|
-
const indent = m[1].length;
|
|
23524
|
-
if (!mindent) {
|
|
23525
|
-
mindent = indent;
|
|
23526
|
-
} else {
|
|
23527
|
-
mindent = Math.min(mindent, indent);
|
|
23528
|
-
}
|
|
23529
|
-
}
|
|
23530
|
-
}
|
|
23531
|
-
if (mindent !== null) {
|
|
23532
|
-
const m = mindent;
|
|
23533
|
-
result = lines.map((l) => l[0] === " " || l[0] === "\t" ? l.slice(m) : l).join(`
|
|
23534
|
-
`);
|
|
23535
|
-
}
|
|
23536
|
-
if (trimWhitespace) {
|
|
23537
|
-
result = result.trim();
|
|
23538
|
-
}
|
|
23539
|
-
if (escapeSpecialCharacters) {
|
|
23540
|
-
result = result.replace(/\\n/g, `
|
|
23541
|
-
`).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)));
|
|
23542
|
-
}
|
|
23543
|
-
if (typeof Bun !== "undefined") {
|
|
23544
|
-
result = result.replace(/\\u(?:\{([\da-fA-F]{1,6})\}|([\da-fA-F]{4}))/g, (_, braced, unbraced) => {
|
|
23545
|
-
var _ref;
|
|
23546
|
-
const hex3 = (_ref = braced !== null && braced !== undefined ? braced : unbraced) !== null && _ref !== undefined ? _ref : "";
|
|
23547
|
-
return String.fromCodePoint(parseInt(hex3, 16));
|
|
23548
|
-
});
|
|
23549
|
-
}
|
|
23550
|
-
return result;
|
|
23551
|
-
}
|
|
24056
|
+
function escapeXml2(value) {
|
|
24057
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
23552
24058
|
}
|
|
23553
|
-
function
|
|
23554
|
-
|
|
23555
|
-
|
|
23556
|
-
|
|
23557
|
-
|
|
23558
|
-
|
|
23559
|
-
|
|
23560
|
-
|
|
23561
|
-
|
|
23562
|
-
|
|
23563
|
-
|
|
23564
|
-
|
|
24059
|
+
function buildWindowsToastScript(title, message) {
|
|
24060
|
+
const xml = `<toast><visual><binding template='ToastGeneric'><text>${escapeXml2(title)}</text><text>${escapeXml2(message)}</text></binding></visual></toast>`;
|
|
24061
|
+
return [
|
|
24062
|
+
"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
|
|
24063
|
+
"[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] > $null",
|
|
24064
|
+
"$xml = New-Object Windows.Data.Xml.Dom.XmlDocument",
|
|
24065
|
+
`$xml.LoadXml(@'${xml}'@)`,
|
|
24066
|
+
"$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)",
|
|
24067
|
+
`$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('${NOTIFICATION_TITLE}')`,
|
|
24068
|
+
"$notifier.Show($toast)"
|
|
24069
|
+
].join("; ");
|
|
24070
|
+
}
|
|
24071
|
+
function getNotificationCommand(platform, title, message) {
|
|
24072
|
+
switch (platform) {
|
|
24073
|
+
case "darwin":
|
|
24074
|
+
return {
|
|
24075
|
+
command: "osascript",
|
|
24076
|
+
args: [
|
|
24077
|
+
"-e",
|
|
24078
|
+
`display notification "${escapeAppleScript(message)}" with title "${escapeAppleScript(title)}"`
|
|
24079
|
+
]
|
|
24080
|
+
};
|
|
24081
|
+
case "linux":
|
|
24082
|
+
return {
|
|
24083
|
+
command: "notify-send",
|
|
24084
|
+
args: [title, message]
|
|
24085
|
+
};
|
|
24086
|
+
case "win32":
|
|
24087
|
+
return {
|
|
24088
|
+
command: "powershell",
|
|
24089
|
+
args: ["-NoProfile", "-Command", buildWindowsToastScript(title, message)]
|
|
24090
|
+
};
|
|
24091
|
+
default:
|
|
24092
|
+
return null;
|
|
23565
24093
|
}
|
|
23566
|
-
return value;
|
|
23567
24094
|
}
|
|
23568
|
-
|
|
23569
|
-
|
|
23570
|
-
|
|
23571
|
-
|
|
23572
|
-
|
|
23573
|
-
|
|
23574
|
-
for (const [key, value] of entries) {
|
|
23575
|
-
if (value === null || value === undefined) {
|
|
23576
|
-
continue;
|
|
23577
|
-
}
|
|
23578
|
-
const heading = "#".repeat(headingLevel);
|
|
23579
|
-
output += `${heading} ${key}`;
|
|
23580
|
-
if (typeof value === "object" && !Array.isArray(value)) {
|
|
23581
|
-
output += renderObject(value, Math.min(headingLevel + 1, 6), indentLevel);
|
|
23582
|
-
} else if (Array.isArray(value)) {
|
|
23583
|
-
output += renderArray(value, indentLevel);
|
|
23584
|
-
} else {
|
|
23585
|
-
const indent = " ".repeat(indentLevel);
|
|
23586
|
-
const escapedValue = htmlEscape(String(value));
|
|
23587
|
-
output += `${indent}- **${key}**: *${escapedValue}*`;
|
|
23588
|
-
}
|
|
23589
|
-
output += `
|
|
23590
|
-
`;
|
|
24095
|
+
function createNotifier(args) {
|
|
24096
|
+
const runner = args.shell ?? createDefaultRunner();
|
|
24097
|
+
const platform = args.platform ?? process.platform;
|
|
24098
|
+
async function notify(title, message) {
|
|
24099
|
+
if (!args.config.enabled) {
|
|
24100
|
+
return;
|
|
23591
24101
|
}
|
|
23592
|
-
|
|
23593
|
-
|
|
23594
|
-
|
|
23595
|
-
|
|
23596
|
-
let output = "";
|
|
23597
|
-
for (const item of arr) {
|
|
23598
|
-
if (item === null || item === undefined) {
|
|
23599
|
-
continue;
|
|
23600
|
-
}
|
|
23601
|
-
if (typeof item === "object" && !Array.isArray(item)) {
|
|
23602
|
-
const nestedObj = item;
|
|
23603
|
-
for (const [key, value] of Object.entries(nestedObj)) {
|
|
23604
|
-
if (value === null || value === undefined) {
|
|
23605
|
-
continue;
|
|
23606
|
-
}
|
|
23607
|
-
if (typeof value === "object") {
|
|
23608
|
-
if (Array.isArray(value)) {
|
|
23609
|
-
output += `${indent}- **${key}**:
|
|
23610
|
-
`;
|
|
23611
|
-
output += renderArray(value, indentLevel + 1);
|
|
23612
|
-
} else {
|
|
23613
|
-
output += `${indent}- **${key}**
|
|
23614
|
-
`;
|
|
23615
|
-
output += renderObject(value, 4, indentLevel + 1);
|
|
23616
|
-
}
|
|
23617
|
-
} else {
|
|
23618
|
-
const escapedValue = htmlEscape(String(value));
|
|
23619
|
-
output += `${indent}- **${key}**: *${escapedValue}*
|
|
23620
|
-
`;
|
|
23621
|
-
}
|
|
23622
|
-
}
|
|
23623
|
-
} else if (Array.isArray(item)) {
|
|
23624
|
-
output += renderArray(item, indentLevel + 1);
|
|
23625
|
-
} else {
|
|
23626
|
-
const escapedValue = htmlEscape(String(item));
|
|
23627
|
-
output += `${indent}- *${escapedValue}*
|
|
23628
|
-
`;
|
|
23629
|
-
}
|
|
24102
|
+
const notificationCommand = getNotificationCommand(platform, truncateNotificationText(title, 80), truncateNotificationText(message));
|
|
24103
|
+
if (!notificationCommand) {
|
|
24104
|
+
args.logger.debug("Notifications are not supported on this platform.", platform);
|
|
24105
|
+
return;
|
|
23630
24106
|
}
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23635
|
-
|
|
23636
|
-
|
|
23637
|
-
|
|
23638
|
-
|
|
23639
|
-
|
|
23640
|
-
|
|
23641
|
-
> Skill root:
|
|
23642
|
-
> ${skill.fullPath}
|
|
23643
|
-
|
|
23644
|
-
Relative file references in this skill resolve from the skill root directory.
|
|
23645
|
-
Always use skill_resource with the exact root-relative path for linked files.
|
|
23646
|
-
|
|
23647
|
-
${linkedResources.length > 0 ? dedent_default`
|
|
23648
|
-
## Linked files detected in skill content
|
|
23649
|
-
|
|
23650
|
-
${linkedResources.map((link) => `- [${link.label}](${link.originalPath}) \u2192 use skill_resource with ${link.resourcePath}`).join(`
|
|
23651
|
-
`)}
|
|
23652
|
-
` : ""}
|
|
23653
|
-
|
|
23654
|
-
${skill.content}
|
|
23655
|
-
|
|
23656
|
-
## Metadata
|
|
23657
|
-
|
|
23658
|
-
${skill.metadata ? renderObject(skill.metadata, 3) : ""}
|
|
23659
|
-
|
|
23660
|
-
## References
|
|
23661
|
-
|
|
23662
|
-
${skill.references ? renderArray(resourceMapToArray(skill.references), 1) : ""}
|
|
23663
|
-
|
|
23664
|
-
## Scripts
|
|
23665
|
-
|
|
23666
|
-
${skill.scripts ? renderArray(resourceMapToArray(skill.scripts), 1) : ""}
|
|
23667
|
-
|
|
23668
|
-
## Assets
|
|
23669
|
-
|
|
23670
|
-
${skill.assets ? renderArray(resourceMapToArray(skill.assets), 1) : ""}
|
|
23671
|
-
`;
|
|
23672
|
-
};
|
|
23673
|
-
const renderResource = (resource) => {
|
|
23674
|
-
return renderObject(resource, 3);
|
|
23675
|
-
};
|
|
23676
|
-
const renderSearchResult = (result) => {
|
|
23677
|
-
return renderObject(result, 3);
|
|
23678
|
-
};
|
|
23679
|
-
const renderer = {
|
|
23680
|
-
format: "md",
|
|
23681
|
-
render(args) {
|
|
23682
|
-
if (args.type === "Skill") {
|
|
23683
|
-
return renderSkill(args.data);
|
|
24107
|
+
try {
|
|
24108
|
+
await runner(notificationCommand.command, notificationCommand.args);
|
|
24109
|
+
} catch (error45) {
|
|
24110
|
+
args.logger.warn("Failed to send notification.", error45);
|
|
24111
|
+
}
|
|
24112
|
+
}
|
|
24113
|
+
return {
|
|
24114
|
+
async skillLoaded(skillNames) {
|
|
24115
|
+
if (!args.config.success || skillNames.length === 0) {
|
|
24116
|
+
return;
|
|
23684
24117
|
}
|
|
23685
|
-
|
|
23686
|
-
|
|
24118
|
+
const count = skillNames.length;
|
|
24119
|
+
const message = count === 1 ? `Injected skill: ${skillNames[0]}` : `Injected ${count} skills: ${skillNames.join(", ")}`;
|
|
24120
|
+
await notify(NOTIFICATION_TITLE, message);
|
|
24121
|
+
},
|
|
24122
|
+
async resourceLoaded(skillName, relativePath) {
|
|
24123
|
+
if (!args.config.success) {
|
|
24124
|
+
return;
|
|
23687
24125
|
}
|
|
23688
|
-
|
|
23689
|
-
|
|
24126
|
+
await notify(NOTIFICATION_TITLE, `Injected ${relativePath} from ${skillName}`);
|
|
24127
|
+
},
|
|
24128
|
+
async error(title, message) {
|
|
24129
|
+
if (!args.config.errors) {
|
|
24130
|
+
return;
|
|
23690
24131
|
}
|
|
23691
|
-
|
|
24132
|
+
await notify(title, message);
|
|
23692
24133
|
}
|
|
23693
24134
|
};
|
|
23694
|
-
return renderer;
|
|
23695
|
-
};
|
|
23696
|
-
|
|
23697
|
-
// src/lib/createPromptRenderer.ts
|
|
23698
|
-
function createPromptRenderer() {
|
|
23699
|
-
const renderers = {
|
|
23700
|
-
json: createJsonPromptRenderer(),
|
|
23701
|
-
xml: createXmlPromptRenderer(),
|
|
23702
|
-
md: createMdPromptRenderer()
|
|
23703
|
-
};
|
|
23704
|
-
const getFormatter = (format) => {
|
|
23705
|
-
switch (format) {
|
|
23706
|
-
case "json":
|
|
23707
|
-
return renderers.json.render;
|
|
23708
|
-
case "xml":
|
|
23709
|
-
return renderers.xml.render;
|
|
23710
|
-
case "md":
|
|
23711
|
-
return renderers.md.render;
|
|
23712
|
-
default:
|
|
23713
|
-
throw new Error(`Unsupported format: ${format}`);
|
|
23714
|
-
}
|
|
23715
|
-
};
|
|
23716
|
-
return {
|
|
23717
|
-
getFormatter
|
|
23718
|
-
};
|
|
23719
24135
|
}
|
|
23720
24136
|
|
|
23721
|
-
// src/
|
|
23722
|
-
|
|
23723
|
-
|
|
23724
|
-
|
|
23725
|
-
|
|
23726
|
-
|
|
23727
|
-
if (combinedKey in modelRenderers) {
|
|
23728
|
-
return modelRenderers[combinedKey];
|
|
23729
|
-
}
|
|
23730
|
-
}
|
|
23731
|
-
if (modelId && modelId in modelRenderers) {
|
|
23732
|
-
return modelRenderers[modelId];
|
|
23733
|
-
}
|
|
23734
|
-
return config3.promptRenderer;
|
|
24137
|
+
// src/services/TuiCommandMirror.ts
|
|
24138
|
+
import { mkdir as mkdir5, readdir as readdir3, readFile as readFile2, rm, writeFile as writeFile5 } from "fs/promises";
|
|
24139
|
+
import { join as join6 } from "path";
|
|
24140
|
+
var SHADOW_COMMAND_MARKER = "<!-- opencode-dynamic-skills:tui-command-mirror -->";
|
|
24141
|
+
function normalizeName(name) {
|
|
24142
|
+
return name.trim().toLowerCase();
|
|
23735
24143
|
}
|
|
23736
|
-
|
|
23737
|
-
|
|
23738
|
-
|
|
23739
|
-
|
|
23740
|
-
|
|
23741
|
-
|
|
23742
|
-
|
|
23743
|
-
|
|
23744
|
-
|
|
23745
|
-
|
|
23746
|
-
|
|
23747
|
-
|
|
24144
|
+
function isManagedShadowCommand(content) {
|
|
24145
|
+
return content.includes(SHADOW_COMMAND_MARKER);
|
|
24146
|
+
}
|
|
24147
|
+
function renderShadowCommandFile(skill) {
|
|
24148
|
+
return [
|
|
24149
|
+
"---",
|
|
24150
|
+
`description: ${JSON.stringify(skill.description)}`,
|
|
24151
|
+
"---",
|
|
24152
|
+
"",
|
|
24153
|
+
SHADOW_COMMAND_MARKER,
|
|
24154
|
+
`Dynamic skill proxy for ${skill.name}.`,
|
|
24155
|
+
"This file is managed by opencode-dynamic-skills so OpenCode TUI can surface the skill as a slash command.",
|
|
24156
|
+
"The plugin replaces this template before execution.",
|
|
24157
|
+
"",
|
|
24158
|
+
"$ARGUMENTS",
|
|
24159
|
+
""
|
|
24160
|
+
].join(`
|
|
24161
|
+
`);
|
|
24162
|
+
}
|
|
24163
|
+
async function syncTuiCommandMirror(args) {
|
|
24164
|
+
if (!args.enabled) {
|
|
24165
|
+
return {
|
|
24166
|
+
mirrored: [],
|
|
24167
|
+
written: [],
|
|
24168
|
+
skipped: [],
|
|
24169
|
+
removed: []
|
|
23748
24170
|
};
|
|
23749
|
-
}
|
|
23750
|
-
|
|
23751
|
-
|
|
23752
|
-
|
|
23753
|
-
|
|
23754
|
-
|
|
23755
|
-
|
|
24171
|
+
}
|
|
24172
|
+
await mkdir5(args.directory, { recursive: true });
|
|
24173
|
+
const reservedCommands = new Set(args.reservedSlashCommands.map(normalizeName));
|
|
24174
|
+
const mirrored = [];
|
|
24175
|
+
const written = [];
|
|
24176
|
+
const skipped = [];
|
|
24177
|
+
const removed = [];
|
|
24178
|
+
const desiredNames = new Set;
|
|
24179
|
+
for (const skill of args.skills) {
|
|
24180
|
+
const normalizedSkillName = normalizeName(skill.name);
|
|
24181
|
+
if (reservedCommands.has(normalizedSkillName)) {
|
|
24182
|
+
skipped.push(skill.name);
|
|
24183
|
+
continue;
|
|
24184
|
+
}
|
|
24185
|
+
desiredNames.add(normalizedSkillName);
|
|
24186
|
+
const commandPath = join6(args.directory, `${skill.name}.md`);
|
|
24187
|
+
const renderedCommand = renderShadowCommandFile(skill);
|
|
24188
|
+
try {
|
|
24189
|
+
const existingContent = await readFile2(commandPath, "utf8");
|
|
24190
|
+
if (!isManagedShadowCommand(existingContent)) {
|
|
24191
|
+
skipped.push(skill.name);
|
|
24192
|
+
continue;
|
|
24193
|
+
}
|
|
24194
|
+
if (existingContent === renderedCommand) {
|
|
24195
|
+
mirrored.push(skill.name);
|
|
24196
|
+
continue;
|
|
23756
24197
|
}
|
|
24198
|
+
} catch {}
|
|
24199
|
+
await writeFile5(commandPath, renderedCommand, "utf8");
|
|
24200
|
+
mirrored.push(skill.name);
|
|
24201
|
+
written.push(skill.name);
|
|
24202
|
+
}
|
|
24203
|
+
for (const entry of await readdir3(args.directory, { withFileTypes: true })) {
|
|
24204
|
+
if (!entry.isFile() || !entry.name.endsWith(".md")) {
|
|
24205
|
+
continue;
|
|
23757
24206
|
}
|
|
23758
|
-
|
|
23759
|
-
|
|
23760
|
-
|
|
23761
|
-
|
|
23762
|
-
|
|
23763
|
-
const
|
|
23764
|
-
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
24207
|
+
const commandPath = join6(args.directory, entry.name);
|
|
24208
|
+
const content = await readFile2(commandPath, "utf8").catch(() => "");
|
|
24209
|
+
if (!isManagedShadowCommand(content)) {
|
|
24210
|
+
continue;
|
|
24211
|
+
}
|
|
24212
|
+
const name = entry.name.replace(/\.md$/i, "");
|
|
24213
|
+
if (desiredNames.has(normalizeName(name))) {
|
|
24214
|
+
continue;
|
|
24215
|
+
}
|
|
24216
|
+
await rm(commandPath, { force: true });
|
|
24217
|
+
removed.push(name);
|
|
24218
|
+
}
|
|
24219
|
+
args.logger.debug("[OpenCodeDynamicSkills] TUI command mirror synced", {
|
|
24220
|
+
directory: args.directory,
|
|
24221
|
+
mirrored,
|
|
24222
|
+
written,
|
|
24223
|
+
removed,
|
|
24224
|
+
skipped
|
|
24225
|
+
});
|
|
23769
24226
|
return {
|
|
23770
|
-
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
getModelInfo
|
|
24227
|
+
mirrored,
|
|
24228
|
+
written,
|
|
24229
|
+
skipped,
|
|
24230
|
+
removed
|
|
23775
24231
|
};
|
|
23776
24232
|
}
|
|
23777
24233
|
|
|
23778
24234
|
// src/index.ts
|
|
23779
24235
|
var SkillsPlugin = async (ctx) => {
|
|
23780
24236
|
const config3 = await getPluginConfig(ctx);
|
|
23781
|
-
const api2 = await createApi(config3);
|
|
24237
|
+
const api2 = await createApi(config3, ctx.client);
|
|
23782
24238
|
const sendPrompt = createInstructionInjector(ctx);
|
|
23783
|
-
const
|
|
23784
|
-
const
|
|
23785
|
-
|
|
23786
|
-
|
|
23787
|
-
|
|
23788
|
-
|
|
23789
|
-
|
|
23790
|
-
|
|
23791
|
-
|
|
23792
|
-
|
|
23793
|
-
|
|
24239
|
+
const renderXmlPrompt = createXmlPromptRenderer().render;
|
|
24240
|
+
const notifier = createNotifier({
|
|
24241
|
+
config: config3.notifications,
|
|
24242
|
+
logger: api2.logger
|
|
24243
|
+
});
|
|
24244
|
+
const mirroredSkillCommands = new Set;
|
|
24245
|
+
const registryInitialisation = api2.registry.initialise();
|
|
24246
|
+
if (config3.tuiCommandMirror.enabled) {
|
|
24247
|
+
try {
|
|
24248
|
+
await registryInitialisation;
|
|
24249
|
+
const mirrorResult = await syncTuiCommandMirror({
|
|
24250
|
+
skills: api2.registry.controller.skills,
|
|
24251
|
+
directory: config3.tuiCommandMirror.directory,
|
|
24252
|
+
enabled: config3.tuiCommandMirror.enabled,
|
|
24253
|
+
logger: api2.logger,
|
|
24254
|
+
reservedSlashCommands: config3.reservedSlashCommands
|
|
24255
|
+
});
|
|
24256
|
+
for (const commandName of mirrorResult.mirrored) {
|
|
24257
|
+
mirroredSkillCommands.add(commandName);
|
|
24258
|
+
}
|
|
24259
|
+
if (mirrorResult.written.length > 0 || mirrorResult.removed.length > 0) {
|
|
24260
|
+
api2.logger.warn("[OpenCodeDynamicSkills] TUI command mirror updated. Restart OpenCode to refresh slash autocomplete.", {
|
|
24261
|
+
directory: config3.tuiCommandMirror.directory,
|
|
24262
|
+
mirrored: mirrorResult.mirrored,
|
|
24263
|
+
written: mirrorResult.written,
|
|
24264
|
+
removed: mirrorResult.removed,
|
|
24265
|
+
skipped: mirrorResult.skipped
|
|
23794
24266
|
});
|
|
23795
24267
|
}
|
|
23796
|
-
|
|
23797
|
-
|
|
23798
|
-
|
|
23799
|
-
|
|
23800
|
-
|
|
23801
|
-
|
|
24268
|
+
} catch (error45) {
|
|
24269
|
+
api2.logger.warn("[OpenCodeDynamicSkills] Failed to sync TUI command mirror. Dynamic skills will still work after submit, but TUI slash autocomplete may miss them.", error45 instanceof Error ? error45.message : String(error45));
|
|
24270
|
+
}
|
|
24271
|
+
}
|
|
24272
|
+
return {
|
|
24273
|
+
"chat.message": async (_input, output) => {
|
|
23802
24274
|
for (const part of output.parts) {
|
|
23803
24275
|
if (part.type !== "text") {
|
|
23804
24276
|
continue;
|
|
@@ -23812,22 +24284,40 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23812
24284
|
text: part.text,
|
|
23813
24285
|
registry: api2.registry,
|
|
23814
24286
|
slashCommandName: config3.slashCommandName,
|
|
23815
|
-
enableSkillAliases: config3.enableSkillAliases
|
|
24287
|
+
enableSkillAliases: config3.enableSkillAliases,
|
|
24288
|
+
reservedSlashCommands: config3.reservedSlashCommands
|
|
23816
24289
|
});
|
|
23817
24290
|
if (!rewrittenText) {
|
|
23818
24291
|
continue;
|
|
23819
24292
|
}
|
|
23820
24293
|
part.text = rewrittenText;
|
|
24294
|
+
const parsedSkillName = part.text.match(/\*\*Skill identifier\*\*: ([^\n]+)/)?.[1];
|
|
24295
|
+
if (parsedSkillName) {
|
|
24296
|
+
await notifier.skillLoaded([parsedSkillName]);
|
|
24297
|
+
}
|
|
23821
24298
|
break;
|
|
23822
24299
|
}
|
|
23823
24300
|
},
|
|
24301
|
+
"command.execute.before": async (input, output) => {
|
|
24302
|
+
const rewritten = await rewriteMirroredSkillCommandParts({
|
|
24303
|
+
commandName: input.command,
|
|
24304
|
+
commandArguments: input.arguments,
|
|
24305
|
+
mirroredCommands: mirroredSkillCommands,
|
|
24306
|
+
registry: api2.registry,
|
|
24307
|
+
output
|
|
24308
|
+
});
|
|
24309
|
+
if (!rewritten) {
|
|
24310
|
+
return;
|
|
24311
|
+
}
|
|
24312
|
+
const skill = api2.registry.controller.skills.find((entry) => entry.name === input.command);
|
|
24313
|
+
if (skill) {
|
|
24314
|
+
await notifier.skillLoaded([skill.toolName]);
|
|
24315
|
+
}
|
|
24316
|
+
},
|
|
23824
24317
|
async event(args) {
|
|
23825
24318
|
switch (args.event.type) {
|
|
23826
|
-
case "
|
|
23827
|
-
|
|
23828
|
-
break;
|
|
23829
|
-
case "session.deleted":
|
|
23830
|
-
modelIdAccountant.untrackSession(args.event.properties.info.id);
|
|
24319
|
+
case "session.error":
|
|
24320
|
+
await notifier.error("OpenCode session error", "A session error occurred in the current session.");
|
|
23831
24321
|
break;
|
|
23832
24322
|
}
|
|
23833
24323
|
},
|
|
@@ -23839,29 +24329,23 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23839
24329
|
skill_names: tool.schema.array(tool.schema.string()).describe("An array of skill names to load.")
|
|
23840
24330
|
},
|
|
23841
24331
|
execute: async (args, toolCtx) => {
|
|
23842
|
-
|
|
23843
|
-
|
|
23844
|
-
|
|
23845
|
-
|
|
23846
|
-
|
|
23847
|
-
|
|
23848
|
-
|
|
23849
|
-
|
|
23850
|
-
|
|
23851
|
-
|
|
23852
|
-
|
|
23853
|
-
|
|
23854
|
-
const results = await api2.loadSkill(args.skill_names);
|
|
23855
|
-
for await (const skill of results.loaded) {
|
|
23856
|
-
await sendPrompt(renderer({ data: skill, type: "Skill" }), {
|
|
23857
|
-
sessionId: toolCtx.sessionID,
|
|
23858
|
-
agent: toolCtx.agent
|
|
24332
|
+
try {
|
|
24333
|
+
const results = await api2.loadSkill(args.skill_names);
|
|
24334
|
+
for await (const skill of results.loaded) {
|
|
24335
|
+
await sendPrompt(await formatLoadedSkill({ skill }), {
|
|
24336
|
+
sessionId: toolCtx.sessionID,
|
|
24337
|
+
agent: toolCtx.agent
|
|
24338
|
+
});
|
|
24339
|
+
}
|
|
24340
|
+
await notifier.skillLoaded(results.loaded.map((skill) => skill.toolName));
|
|
24341
|
+
return JSON.stringify({
|
|
24342
|
+
loaded: results.loaded.map((skill) => skill.toolName),
|
|
24343
|
+
not_found: results.notFound
|
|
23859
24344
|
});
|
|
24345
|
+
} catch (error45) {
|
|
24346
|
+
await notifier.error("skill_use failed", error45 instanceof Error ? error45.message : String(error45));
|
|
24347
|
+
throw error45;
|
|
23860
24348
|
}
|
|
23861
|
-
return JSON.stringify({
|
|
23862
|
-
loaded: results.loaded.map((skill) => skill.toolName),
|
|
23863
|
-
not_found: results.notFound
|
|
23864
|
-
});
|
|
23865
24349
|
}
|
|
23866
24350
|
}),
|
|
23867
24351
|
skill_find: tool({
|
|
@@ -23869,21 +24353,9 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23869
24353
|
args: {
|
|
23870
24354
|
query: tool.schema.union([tool.schema.string(), tool.schema.array(tool.schema.string())]).describe("The search query string or array of strings.")
|
|
23871
24355
|
},
|
|
23872
|
-
execute: async (args
|
|
23873
|
-
const messageID = toolCtx.messageID;
|
|
23874
|
-
const sessionID = toolCtx.sessionID;
|
|
23875
|
-
const modelInfo = modelIdAccountant.getModelInfo({
|
|
23876
|
-
messageID,
|
|
23877
|
-
sessionID
|
|
23878
|
-
});
|
|
23879
|
-
const format = getModelFormat({
|
|
23880
|
-
config: config3,
|
|
23881
|
-
modelId: modelInfo?.modelID,
|
|
23882
|
-
providerId: modelInfo?.providerID
|
|
23883
|
-
});
|
|
23884
|
-
const renderer = promptRenderer.getFormatter(format);
|
|
24356
|
+
execute: async (args) => {
|
|
23885
24357
|
const results = await api2.findSkills(args);
|
|
23886
|
-
const output =
|
|
24358
|
+
const output = renderXmlPrompt({
|
|
23887
24359
|
data: results,
|
|
23888
24360
|
type: "SkillSearchResults"
|
|
23889
24361
|
});
|
|
@@ -23896,21 +24368,9 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23896
24368
|
task: tool.schema.string().describe("The task or request to recommend skills for."),
|
|
23897
24369
|
limit: tool.schema.number().int().min(1).max(10).optional().describe("Maximum number of recommendations to return.")
|
|
23898
24370
|
},
|
|
23899
|
-
execute: async (args
|
|
23900
|
-
const messageID = toolCtx.messageID;
|
|
23901
|
-
const sessionID = toolCtx.sessionID;
|
|
23902
|
-
const modelInfo = modelIdAccountant.getModelInfo({
|
|
23903
|
-
messageID,
|
|
23904
|
-
sessionID
|
|
23905
|
-
});
|
|
23906
|
-
const format = getModelFormat({
|
|
23907
|
-
config: config3,
|
|
23908
|
-
modelId: modelInfo?.modelID,
|
|
23909
|
-
providerId: modelInfo?.providerID
|
|
23910
|
-
});
|
|
23911
|
-
const renderer = promptRenderer.getFormatter(format);
|
|
24371
|
+
execute: async (args) => {
|
|
23912
24372
|
const results = await api2.recommendSkills(args);
|
|
23913
|
-
return
|
|
24373
|
+
return renderXmlPrompt({
|
|
23914
24374
|
data: results,
|
|
23915
24375
|
type: "SkillSearchResults"
|
|
23916
24376
|
});
|
|
@@ -23923,31 +24383,25 @@ var SkillsPlugin = async (ctx) => {
|
|
|
23923
24383
|
relative_path: tool.schema.string().describe("The relative path to the resource file within the skill directory.")
|
|
23924
24384
|
},
|
|
23925
24385
|
execute: async (args, toolCtx) => {
|
|
23926
|
-
|
|
23927
|
-
|
|
23928
|
-
|
|
23929
|
-
|
|
23930
|
-
|
|
23931
|
-
|
|
23932
|
-
|
|
23933
|
-
|
|
23934
|
-
|
|
23935
|
-
|
|
23936
|
-
|
|
23937
|
-
|
|
23938
|
-
|
|
23939
|
-
|
|
23940
|
-
|
|
24386
|
+
try {
|
|
24387
|
+
const result = await api2.readResource(args);
|
|
24388
|
+
if (!result.injection) {
|
|
24389
|
+
throw new Error("Failed to read resource");
|
|
24390
|
+
}
|
|
24391
|
+
await sendPrompt(renderXmlPrompt({ data: result.injection, type: "SkillResource" }), {
|
|
24392
|
+
sessionId: toolCtx.sessionID,
|
|
24393
|
+
agent: toolCtx.agent
|
|
24394
|
+
});
|
|
24395
|
+
await notifier.resourceLoaded(args.skill_name, result.injection.resource_path);
|
|
24396
|
+
return JSON.stringify({
|
|
24397
|
+
result: "Resource injected successfully",
|
|
24398
|
+
resource_path: result.injection.resource_path,
|
|
24399
|
+
resource_mimetype: result.injection.resource_mimetype
|
|
24400
|
+
});
|
|
24401
|
+
} catch (error45) {
|
|
24402
|
+
await notifier.error("skill_resource failed", error45 instanceof Error ? error45.message : String(error45));
|
|
24403
|
+
throw error45;
|
|
23941
24404
|
}
|
|
23942
|
-
await sendPrompt(renderer({ data: result.injection, type: "SkillResource" }), {
|
|
23943
|
-
sessionId: toolCtx.sessionID,
|
|
23944
|
-
agent: toolCtx.agent
|
|
23945
|
-
});
|
|
23946
|
-
return JSON.stringify({
|
|
23947
|
-
result: "Resource injected successfully",
|
|
23948
|
-
resource_path: result.injection.resource_path,
|
|
23949
|
-
resource_mimetype: result.injection.resource_mimetype
|
|
23950
|
-
});
|
|
23951
24405
|
}
|
|
23952
24406
|
})
|
|
23953
24407
|
}
|