opencode-dynamic-skills 1.2.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 +7 -0
- package/dist/commands/MirroredSkillCommand.d.ts +15 -0
- package/dist/commands/MirroredSkillCommand.test.d.ts +1 -0
- package/dist/commands/SlashCommand.d.ts +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/index.js +371 -76
- package/dist/lib/formatLoadedSkill.d.ts +1 -1
- package/dist/lib/formatLoadedSkill.test.d.ts +1 -0
- package/dist/services/TuiCommandMirror.d.ts +15 -0
- package/dist/services/TuiCommandMirror.test.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -143,6 +143,10 @@ Example:
|
|
|
143
143
|
"model": "openai/gpt-5.2",
|
|
144
144
|
"systemPrompt": "You are selecting the most relevant dynamic skills for a user request. Return strict JSON only with this shape: {\"recommendations\":[{\"name\":\"skill_tool_name\",\"reason\":\"why it matches\"}]}. Only recommend skills from the provided catalog. Prefer the smallest set of high-confidence matches.",
|
|
145
145
|
},
|
|
146
|
+
"tuiCommandMirror": {
|
|
147
|
+
"enabled": true,
|
|
148
|
+
"directory": "~/.config/opencode/commands",
|
|
149
|
+
},
|
|
146
150
|
}
|
|
147
151
|
```
|
|
148
152
|
|
|
@@ -155,6 +159,9 @@ Fields:
|
|
|
155
159
|
- `reservedSlashCommands`
|
|
156
160
|
- `notifications`
|
|
157
161
|
- `skillRecommend`
|
|
162
|
+
- `tuiCommandMirror`
|
|
163
|
+
|
|
164
|
+
`tuiCommandMirror` is enabled by default. The plugin mirrors dynamic skills into proxy command files so TUI slash autocomplete can surface `/<skill-name>` after an OpenCode restart.
|
|
158
165
|
|
|
159
166
|
Injected skill content now always uses a single XML renderer. The previous custom renderer selection feature was removed.
|
|
160
167
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SkillRegistry } from '../types';
|
|
2
|
+
type HookPart = {
|
|
3
|
+
type: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
} & Record<string, unknown>;
|
|
6
|
+
export declare function rewriteMirroredSkillCommandParts(args: {
|
|
7
|
+
commandName: string;
|
|
8
|
+
commandArguments: string;
|
|
9
|
+
mirroredCommands: Set<string>;
|
|
10
|
+
registry: SkillRegistry;
|
|
11
|
+
output: {
|
|
12
|
+
parts: HookPart[];
|
|
13
|
+
};
|
|
14
|
+
}): Promise<boolean>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -12,7 +12,7 @@ export declare function renderSlashSkillPrompt(args: {
|
|
|
12
12
|
invocationName: string;
|
|
13
13
|
skill: Skill;
|
|
14
14
|
userPrompt: string;
|
|
15
|
-
}): string
|
|
15
|
+
}): Promise<string>;
|
|
16
16
|
export declare function renderRecommendSlashPrompt(task: string): string;
|
|
17
17
|
export declare function rewriteRecommendSlashCommandText(text: string): Promise<string | null>;
|
|
18
18
|
export declare function rewriteSlashCommandText(args: {
|
package/dist/config.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import type { PluginInput } from '@opencode-ai/plugin';
|
|
2
|
-
import type { NotificationConfig, PluginConfig, SkillRecommendConfig } from './types';
|
|
2
|
+
import type { NotificationConfig, PluginConfig, SkillRecommendConfig, TuiCommandMirrorConfig } from './types';
|
|
3
3
|
export declare const DEFAULT_RESERVED_SLASH_COMMANDS: readonly ["agent", "agents", "compact", "connect", "details", "editor", "exit", "export", "fork", "help", "init", "mcp", "model", "models", "new", "open", "redo", "sessions", "share", "skills", "terminal", "themes", "thinking", "undo", "unshare"];
|
|
4
4
|
export declare const DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT: string;
|
|
5
5
|
export declare const MANAGED_PLUGIN_CONFIG_DIRECTORY: string;
|
|
6
|
+
export declare const MANAGED_TUI_COMMANDS_DIRECTORY: string;
|
|
6
7
|
export declare const MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
|
|
7
8
|
export declare const MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
|
|
8
9
|
type PartialPluginConfig = Partial<Omit<PluginConfig, 'skillRecommend' | 'notifications'>> & {
|
|
9
10
|
skillRecommend?: Partial<SkillRecommendConfig>;
|
|
10
11
|
notifications?: Partial<NotificationConfig>;
|
|
12
|
+
tuiCommandMirror?: Partial<TuiCommandMirrorConfig>;
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* Gets OpenCode-compatible config paths for the current platform.
|
|
@@ -55,6 +57,7 @@ export declare function normalizeBasePaths(basePaths: string[], projectDirectory
|
|
|
55
57
|
export declare function getProjectSkillBasePaths(directory: string, worktree?: string): string[];
|
|
56
58
|
export declare function createDefaultSkillRecommendConfig(): SkillRecommendConfig;
|
|
57
59
|
export declare function createDefaultNotificationConfig(): NotificationConfig;
|
|
60
|
+
export declare function createDefaultTuiCommandMirrorConfig(): TuiCommandMirrorConfig;
|
|
58
61
|
export declare function createDefaultPluginConfig(): PluginConfig;
|
|
59
62
|
export declare function stripJsonComments(input: string): string;
|
|
60
63
|
export declare function removeTrailingJsonCommas(input: string): string;
|
package/dist/index.js
CHANGED
|
@@ -18059,6 +18059,9 @@ function createSkillResourceReader(provider) {
|
|
|
18059
18059
|
};
|
|
18060
18060
|
}
|
|
18061
18061
|
|
|
18062
|
+
// src/lib/formatLoadedSkill.ts
|
|
18063
|
+
import path3 from "path";
|
|
18064
|
+
|
|
18062
18065
|
// src/lib/SkillLinks.ts
|
|
18063
18066
|
import path2 from "path";
|
|
18064
18067
|
var SCHEME_PATTERN = /^[a-z][a-z0-9+.-]*:/i;
|
|
@@ -18112,11 +18115,116 @@ function extractSkillLinks(content) {
|
|
|
18112
18115
|
}
|
|
18113
18116
|
|
|
18114
18117
|
// src/lib/formatLoadedSkill.ts
|
|
18115
|
-
|
|
18116
|
-
|
|
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
|
+
});
|
|
18117
18224
|
const output = [
|
|
18118
|
-
|
|
18225
|
+
`# /${invocationName} Command`,
|
|
18119
18226
|
"",
|
|
18227
|
+
`**Description**: ${args.skill.description}`,
|
|
18120
18228
|
`**Skill identifier**: ${args.skill.toolName}`,
|
|
18121
18229
|
`**Base directory**: ${args.skill.fullPath}`
|
|
18122
18230
|
];
|
|
@@ -18126,21 +18234,22 @@ function formatLoadedSkill(args) {
|
|
|
18126
18234
|
if (args.userMessage?.trim()) {
|
|
18127
18235
|
output.push(`**User request**: ${args.userMessage.trim()}`);
|
|
18128
18236
|
}
|
|
18129
|
-
output.push("", "
|
|
18130
|
-
output.push("
|
|
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>");
|
|
18131
18243
|
if (args.skill.files.size > 0) {
|
|
18132
18244
|
output.push("", "### Available files", "");
|
|
18133
18245
|
for (const relativePath of Array.from(args.skill.files.keys()).sort()) {
|
|
18134
18246
|
output.push(`- ${relativePath}`);
|
|
18135
18247
|
}
|
|
18136
18248
|
}
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
|
|
18140
|
-
output.push(`- ${link.originalPath} -> ${link.resourcePath}`);
|
|
18141
|
-
}
|
|
18249
|
+
output.push(...formatExpandedReferences(referencedFiles));
|
|
18250
|
+
if (args.userMessage?.trim()) {
|
|
18251
|
+
output.push("", "<user-request>", args.userMessage.trim(), "</user-request>");
|
|
18142
18252
|
}
|
|
18143
|
-
output.push("", args.skill.content);
|
|
18144
18253
|
return output.join(`
|
|
18145
18254
|
`);
|
|
18146
18255
|
}
|
|
@@ -18176,7 +18285,7 @@ function createSkillTool(registry2) {
|
|
|
18176
18285
|
const available = registry2.controller.skills.map((entry) => entry.name).join(", ");
|
|
18177
18286
|
throw new Error(`Skill "${args.name}" not found. Available: ${available || "none"}`);
|
|
18178
18287
|
}
|
|
18179
|
-
return formatLoadedSkill({
|
|
18288
|
+
return await formatLoadedSkill({
|
|
18180
18289
|
skill,
|
|
18181
18290
|
invocationName: args.name.replace(/^\//, ""),
|
|
18182
18291
|
userMessage: args.user_message
|
|
@@ -18693,10 +18802,10 @@ async function loadConfig({
|
|
|
18693
18802
|
var defaultConfigDir = resolve(process3.cwd(), "config");
|
|
18694
18803
|
var defaultGeneratedDir = resolve(process3.cwd(), "src/generated");
|
|
18695
18804
|
function getProjectRoot(filePath, options2 = {}) {
|
|
18696
|
-
let
|
|
18697
|
-
while (
|
|
18698
|
-
|
|
18699
|
-
const finalPath = resolve2(
|
|
18805
|
+
let path4 = process2.cwd();
|
|
18806
|
+
while (path4.includes("storage"))
|
|
18807
|
+
path4 = resolve2(path4, "..");
|
|
18808
|
+
const finalPath = resolve2(path4, filePath || "");
|
|
18700
18809
|
if (options2?.relative)
|
|
18701
18810
|
return relative3(process2.cwd(), finalPath);
|
|
18702
18811
|
return finalPath;
|
|
@@ -20143,10 +20252,10 @@ function applyEnvVarsToConfig(name, config3, verbose = false) {
|
|
|
20143
20252
|
return config3;
|
|
20144
20253
|
const envPrefix = name.toUpperCase().replace(/-/g, "_");
|
|
20145
20254
|
const result = { ...config3 };
|
|
20146
|
-
function processObject(obj,
|
|
20255
|
+
function processObject(obj, path4 = []) {
|
|
20147
20256
|
const result2 = { ...obj };
|
|
20148
20257
|
for (const [key, value] of Object.entries(obj)) {
|
|
20149
|
-
const envPath = [...
|
|
20258
|
+
const envPath = [...path4, key];
|
|
20150
20259
|
const formatKey = (k) => k.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
20151
20260
|
const envKey = `${envPrefix}_${envPath.map(formatKey).join("_")}`;
|
|
20152
20261
|
const oldEnvKey = `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}`;
|
|
@@ -20328,10 +20437,10 @@ async function loadConfig3({
|
|
|
20328
20437
|
var defaultConfigDir2 = resolve3(process6.cwd(), "config");
|
|
20329
20438
|
var defaultGeneratedDir2 = resolve3(process6.cwd(), "src/generated");
|
|
20330
20439
|
function getProjectRoot2(filePath, options2 = {}) {
|
|
20331
|
-
let
|
|
20332
|
-
while (
|
|
20333
|
-
|
|
20334
|
-
const finalPath = resolve4(
|
|
20440
|
+
let path4 = process7.cwd();
|
|
20441
|
+
while (path4.includes("storage"))
|
|
20442
|
+
path4 = resolve4(path4, "..");
|
|
20443
|
+
const finalPath = resolve4(path4, filePath || "");
|
|
20335
20444
|
if (options2?.relative)
|
|
20336
20445
|
return relative2(process7.cwd(), finalPath);
|
|
20337
20446
|
return finalPath;
|
|
@@ -21768,10 +21877,10 @@ class EnvVarError extends BunfigError {
|
|
|
21768
21877
|
|
|
21769
21878
|
class FileSystemError extends BunfigError {
|
|
21770
21879
|
code = "FILE_SYSTEM_ERROR";
|
|
21771
|
-
constructor(operation,
|
|
21772
|
-
super(`File system ${operation} failed for "${
|
|
21880
|
+
constructor(operation, path4, cause) {
|
|
21881
|
+
super(`File system ${operation} failed for "${path4}": ${cause.message}`, {
|
|
21773
21882
|
operation,
|
|
21774
|
-
path:
|
|
21883
|
+
path: path4,
|
|
21775
21884
|
originalError: cause.name,
|
|
21776
21885
|
originalMessage: cause.message
|
|
21777
21886
|
});
|
|
@@ -21844,8 +21953,8 @@ var ErrorFactory = {
|
|
|
21844
21953
|
envVar(envKey, envValue, expectedType, configName) {
|
|
21845
21954
|
return new EnvVarError(envKey, envValue, expectedType, configName);
|
|
21846
21955
|
},
|
|
21847
|
-
fileSystem(operation,
|
|
21848
|
-
return new FileSystemError(operation,
|
|
21956
|
+
fileSystem(operation, path4, cause) {
|
|
21957
|
+
return new FileSystemError(operation, path4, cause);
|
|
21849
21958
|
},
|
|
21850
21959
|
typeGeneration(configDir, outputPath, cause) {
|
|
21851
21960
|
return new TypeGenerationError(configDir, outputPath, cause);
|
|
@@ -21981,9 +22090,9 @@ class EnvProcessor {
|
|
|
21981
22090
|
}
|
|
21982
22091
|
return key.replace(/([A-Z])/g, "_$1").toUpperCase();
|
|
21983
22092
|
}
|
|
21984
|
-
processObject(obj,
|
|
22093
|
+
processObject(obj, path4, envPrefix, options2) {
|
|
21985
22094
|
for (const [key, value] of Object.entries(obj)) {
|
|
21986
|
-
const envPath = [...
|
|
22095
|
+
const envPath = [...path4, key];
|
|
21987
22096
|
const formattedKeys = envPath.map((k) => this.formatEnvKey(k, options2.useCamelCase));
|
|
21988
22097
|
const envKey = `${envPrefix}_${formattedKeys.join("_")}`;
|
|
21989
22098
|
const oldEnvKey = options2.useBackwardCompatibility ? `${envPrefix}_${envPath.map((p) => p.toUpperCase()).join("_")}` : null;
|
|
@@ -22068,9 +22177,9 @@ class EnvProcessor {
|
|
|
22068
22177
|
return this.formatAsText(envVars, configName);
|
|
22069
22178
|
}
|
|
22070
22179
|
}
|
|
22071
|
-
extractEnvVarInfo(obj,
|
|
22180
|
+
extractEnvVarInfo(obj, path4, prefix, envVars) {
|
|
22072
22181
|
for (const [key, value] of Object.entries(obj)) {
|
|
22073
|
-
const envPath = [...
|
|
22182
|
+
const envPath = [...path4, key];
|
|
22074
22183
|
const envKey = `${prefix}_${envPath.map((k) => this.formatEnvKey(k, true)).join("_")}`;
|
|
22075
22184
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
22076
22185
|
this.extractEnvVarInfo(value, envPath, prefix, envVars);
|
|
@@ -22485,15 +22594,15 @@ class ConfigFileLoader {
|
|
|
22485
22594
|
}
|
|
22486
22595
|
async preloadConfigurations(configPaths, options2 = {}) {
|
|
22487
22596
|
const preloaded = new Map;
|
|
22488
|
-
await Promise.allSettled(configPaths.map(async (
|
|
22597
|
+
await Promise.allSettled(configPaths.map(async (path4) => {
|
|
22489
22598
|
try {
|
|
22490
|
-
const result = await this.loadFromPath(
|
|
22599
|
+
const result = await this.loadFromPath(path4, {}, options2);
|
|
22491
22600
|
if (result) {
|
|
22492
|
-
preloaded.set(
|
|
22601
|
+
preloaded.set(path4, result.config);
|
|
22493
22602
|
}
|
|
22494
22603
|
} catch (error45) {
|
|
22495
22604
|
if (options2.verbose) {
|
|
22496
|
-
console.warn(`Failed to preload ${
|
|
22605
|
+
console.warn(`Failed to preload ${path4}:`, error45);
|
|
22497
22606
|
}
|
|
22498
22607
|
}
|
|
22499
22608
|
}));
|
|
@@ -22572,13 +22681,13 @@ class ConfigValidator {
|
|
|
22572
22681
|
warnings
|
|
22573
22682
|
};
|
|
22574
22683
|
}
|
|
22575
|
-
validateObjectAgainstSchema(value, schema,
|
|
22684
|
+
validateObjectAgainstSchema(value, schema, path4, errors3, warnings, options2) {
|
|
22576
22685
|
if (options2.validateTypes && schema.type) {
|
|
22577
22686
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22578
22687
|
const expectedTypes = Array.isArray(schema.type) ? schema.type : [schema.type];
|
|
22579
22688
|
if (!expectedTypes.includes(actualType)) {
|
|
22580
22689
|
errors3.push({
|
|
22581
|
-
path:
|
|
22690
|
+
path: path4,
|
|
22582
22691
|
message: `Expected type ${expectedTypes.join(" or ")}, got ${actualType}`,
|
|
22583
22692
|
expected: expectedTypes.join(" or "),
|
|
22584
22693
|
actual: actualType,
|
|
@@ -22590,7 +22699,7 @@ class ConfigValidator {
|
|
|
22590
22699
|
}
|
|
22591
22700
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
22592
22701
|
errors3.push({
|
|
22593
|
-
path:
|
|
22702
|
+
path: path4,
|
|
22594
22703
|
message: `Value must be one of: ${schema.enum.join(", ")}`,
|
|
22595
22704
|
expected: schema.enum.join(", "),
|
|
22596
22705
|
actual: value,
|
|
@@ -22602,7 +22711,7 @@ class ConfigValidator {
|
|
|
22602
22711
|
if (typeof value === "string") {
|
|
22603
22712
|
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
22604
22713
|
errors3.push({
|
|
22605
|
-
path:
|
|
22714
|
+
path: path4,
|
|
22606
22715
|
message: `String length must be at least ${schema.minLength}`,
|
|
22607
22716
|
expected: `>= ${schema.minLength}`,
|
|
22608
22717
|
actual: value.length,
|
|
@@ -22611,7 +22720,7 @@ class ConfigValidator {
|
|
|
22611
22720
|
}
|
|
22612
22721
|
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
22613
22722
|
errors3.push({
|
|
22614
|
-
path:
|
|
22723
|
+
path: path4,
|
|
22615
22724
|
message: `String length must not exceed ${schema.maxLength}`,
|
|
22616
22725
|
expected: `<= ${schema.maxLength}`,
|
|
22617
22726
|
actual: value.length,
|
|
@@ -22622,7 +22731,7 @@ class ConfigValidator {
|
|
|
22622
22731
|
const regex = new RegExp(schema.pattern);
|
|
22623
22732
|
if (!regex.test(value)) {
|
|
22624
22733
|
errors3.push({
|
|
22625
|
-
path:
|
|
22734
|
+
path: path4,
|
|
22626
22735
|
message: `String does not match pattern ${schema.pattern}`,
|
|
22627
22736
|
expected: schema.pattern,
|
|
22628
22737
|
actual: value,
|
|
@@ -22634,7 +22743,7 @@ class ConfigValidator {
|
|
|
22634
22743
|
if (typeof value === "number") {
|
|
22635
22744
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
22636
22745
|
errors3.push({
|
|
22637
|
-
path:
|
|
22746
|
+
path: path4,
|
|
22638
22747
|
message: `Value must be at least ${schema.minimum}`,
|
|
22639
22748
|
expected: `>= ${schema.minimum}`,
|
|
22640
22749
|
actual: value,
|
|
@@ -22643,7 +22752,7 @@ class ConfigValidator {
|
|
|
22643
22752
|
}
|
|
22644
22753
|
if (schema.maximum !== undefined && value > schema.maximum) {
|
|
22645
22754
|
errors3.push({
|
|
22646
|
-
path:
|
|
22755
|
+
path: path4,
|
|
22647
22756
|
message: `Value must not exceed ${schema.maximum}`,
|
|
22648
22757
|
expected: `<= ${schema.maximum}`,
|
|
22649
22758
|
actual: value,
|
|
@@ -22653,7 +22762,7 @@ class ConfigValidator {
|
|
|
22653
22762
|
}
|
|
22654
22763
|
if (Array.isArray(value) && schema.items) {
|
|
22655
22764
|
for (let i = 0;i < value.length; i++) {
|
|
22656
|
-
const itemPath =
|
|
22765
|
+
const itemPath = path4 ? `${path4}[${i}]` : `[${i}]`;
|
|
22657
22766
|
this.validateObjectAgainstSchema(value[i], schema.items, itemPath, errors3, warnings, options2);
|
|
22658
22767
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22659
22768
|
return;
|
|
@@ -22665,7 +22774,7 @@ class ConfigValidator {
|
|
|
22665
22774
|
for (const requiredProp of schema.required) {
|
|
22666
22775
|
if (!(requiredProp in obj)) {
|
|
22667
22776
|
errors3.push({
|
|
22668
|
-
path:
|
|
22777
|
+
path: path4 ? `${path4}.${requiredProp}` : requiredProp,
|
|
22669
22778
|
message: `Missing required property '${requiredProp}'`,
|
|
22670
22779
|
expected: "required",
|
|
22671
22780
|
rule: "required"
|
|
@@ -22678,7 +22787,7 @@ class ConfigValidator {
|
|
|
22678
22787
|
if (schema.properties) {
|
|
22679
22788
|
for (const [propName, propSchema] of Object.entries(schema.properties)) {
|
|
22680
22789
|
if (propName in obj) {
|
|
22681
|
-
const propPath =
|
|
22790
|
+
const propPath = path4 ? `${path4}.${propName}` : propName;
|
|
22682
22791
|
this.validateObjectAgainstSchema(obj[propName], propSchema, propPath, errors3, warnings, options2);
|
|
22683
22792
|
if (options2.stopOnFirstError && errors3.length > 0)
|
|
22684
22793
|
return;
|
|
@@ -22690,7 +22799,7 @@ class ConfigValidator {
|
|
|
22690
22799
|
for (const propName of Object.keys(obj)) {
|
|
22691
22800
|
if (!allowedProps.has(propName)) {
|
|
22692
22801
|
warnings.push({
|
|
22693
|
-
path:
|
|
22802
|
+
path: path4 ? `${path4}.${propName}` : propName,
|
|
22694
22803
|
message: `Additional property '${propName}' is not allowed`,
|
|
22695
22804
|
rule: "additionalProperties"
|
|
22696
22805
|
});
|
|
@@ -22724,12 +22833,12 @@ class ConfigValidator {
|
|
|
22724
22833
|
warnings
|
|
22725
22834
|
};
|
|
22726
22835
|
}
|
|
22727
|
-
validateWithRule(value, rule,
|
|
22836
|
+
validateWithRule(value, rule, path4) {
|
|
22728
22837
|
const errors3 = [];
|
|
22729
22838
|
if (rule.required && (value === undefined || value === null)) {
|
|
22730
22839
|
errors3.push({
|
|
22731
|
-
path:
|
|
22732
|
-
message: rule.message || `Property '${
|
|
22840
|
+
path: path4,
|
|
22841
|
+
message: rule.message || `Property '${path4}' is required`,
|
|
22733
22842
|
expected: "required",
|
|
22734
22843
|
rule: "required"
|
|
22735
22844
|
});
|
|
@@ -22742,7 +22851,7 @@ class ConfigValidator {
|
|
|
22742
22851
|
const actualType = Array.isArray(value) ? "array" : typeof value;
|
|
22743
22852
|
if (actualType !== rule.type) {
|
|
22744
22853
|
errors3.push({
|
|
22745
|
-
path:
|
|
22854
|
+
path: path4,
|
|
22746
22855
|
message: rule.message || `Expected type ${rule.type}, got ${actualType}`,
|
|
22747
22856
|
expected: rule.type,
|
|
22748
22857
|
actual: actualType,
|
|
@@ -22754,7 +22863,7 @@ class ConfigValidator {
|
|
|
22754
22863
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22755
22864
|
if (length < rule.min) {
|
|
22756
22865
|
errors3.push({
|
|
22757
|
-
path:
|
|
22866
|
+
path: path4,
|
|
22758
22867
|
message: rule.message || `Value must be at least ${rule.min}`,
|
|
22759
22868
|
expected: `>= ${rule.min}`,
|
|
22760
22869
|
actual: length,
|
|
@@ -22766,7 +22875,7 @@ class ConfigValidator {
|
|
|
22766
22875
|
const length = Array.isArray(value) ? value.length : typeof value === "string" ? value.length : typeof value === "number" ? value : 0;
|
|
22767
22876
|
if (length > rule.max) {
|
|
22768
22877
|
errors3.push({
|
|
22769
|
-
path:
|
|
22878
|
+
path: path4,
|
|
22770
22879
|
message: rule.message || `Value must not exceed ${rule.max}`,
|
|
22771
22880
|
expected: `<= ${rule.max}`,
|
|
22772
22881
|
actual: length,
|
|
@@ -22777,7 +22886,7 @@ class ConfigValidator {
|
|
|
22777
22886
|
if (rule.pattern && typeof value === "string") {
|
|
22778
22887
|
if (!rule.pattern.test(value)) {
|
|
22779
22888
|
errors3.push({
|
|
22780
|
-
path:
|
|
22889
|
+
path: path4,
|
|
22781
22890
|
message: rule.message || `Value does not match pattern ${rule.pattern}`,
|
|
22782
22891
|
expected: rule.pattern.toString(),
|
|
22783
22892
|
actual: value,
|
|
@@ -22787,7 +22896,7 @@ class ConfigValidator {
|
|
|
22787
22896
|
}
|
|
22788
22897
|
if (rule.enum && !rule.enum.includes(value)) {
|
|
22789
22898
|
errors3.push({
|
|
22790
|
-
path:
|
|
22899
|
+
path: path4,
|
|
22791
22900
|
message: rule.message || `Value must be one of: ${rule.enum.join(", ")}`,
|
|
22792
22901
|
expected: rule.enum.join(", "),
|
|
22793
22902
|
actual: value,
|
|
@@ -22798,7 +22907,7 @@ class ConfigValidator {
|
|
|
22798
22907
|
const customError = rule.validator(value);
|
|
22799
22908
|
if (customError) {
|
|
22800
22909
|
errors3.push({
|
|
22801
|
-
path:
|
|
22910
|
+
path: path4,
|
|
22802
22911
|
message: rule.message || customError,
|
|
22803
22912
|
rule: "custom"
|
|
22804
22913
|
});
|
|
@@ -22806,10 +22915,10 @@ class ConfigValidator {
|
|
|
22806
22915
|
}
|
|
22807
22916
|
return errors3;
|
|
22808
22917
|
}
|
|
22809
|
-
getValueByPath(obj,
|
|
22810
|
-
if (!
|
|
22918
|
+
getValueByPath(obj, path4) {
|
|
22919
|
+
if (!path4)
|
|
22811
22920
|
return obj;
|
|
22812
|
-
const keys =
|
|
22921
|
+
const keys = path4.split(".");
|
|
22813
22922
|
let current = obj;
|
|
22814
22923
|
for (const key of keys) {
|
|
22815
22924
|
if (current && typeof current === "object" && key in current) {
|
|
@@ -23235,10 +23344,10 @@ async function loadConfig5(options2) {
|
|
|
23235
23344
|
function applyEnvVarsToConfig2(name, config4, verbose = false) {
|
|
23236
23345
|
const _envProcessor = new EnvProcessor;
|
|
23237
23346
|
const envPrefix = name.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
23238
|
-
function processConfigLevel(obj,
|
|
23347
|
+
function processConfigLevel(obj, path4 = []) {
|
|
23239
23348
|
const result = { ...obj };
|
|
23240
23349
|
for (const [key, value] of Object.entries(obj)) {
|
|
23241
|
-
const currentPath = [...
|
|
23350
|
+
const currentPath = [...path4, key];
|
|
23242
23351
|
const envKeys = [
|
|
23243
23352
|
`${envPrefix}_${currentPath.join("_").toUpperCase()}`,
|
|
23244
23353
|
`${envPrefix}_${currentPath.map((k) => k.toUpperCase()).join("")}`,
|
|
@@ -23321,6 +23430,7 @@ var DEFAULT_SKILL_RECOMMEND_SYSTEM_PROMPT = [
|
|
|
23321
23430
|
"Prefer the smallest set of high-confidence matches."
|
|
23322
23431
|
].join(" ");
|
|
23323
23432
|
var MANAGED_PLUGIN_CONFIG_DIRECTORY = join4(homedir3(), ".config", "opencode");
|
|
23433
|
+
var MANAGED_TUI_COMMANDS_DIRECTORY = join4(MANAGED_PLUGIN_CONFIG_DIRECTORY, "commands");
|
|
23324
23434
|
var MANAGED_PLUGIN_JSONC_FILENAME = "opencode-dynamic-skills.config.jsonc";
|
|
23325
23435
|
var MANAGED_PLUGIN_JSON_FILENAME = "opencode-dynamic-skills.config.json";
|
|
23326
23436
|
function getOpenCodeConfigPaths() {
|
|
@@ -23342,14 +23452,14 @@ function getOpenCodeConfigPaths() {
|
|
|
23342
23452
|
paths.push(join4(home, ".opencode"));
|
|
23343
23453
|
return paths;
|
|
23344
23454
|
}
|
|
23345
|
-
function expandTildePath(
|
|
23346
|
-
if (
|
|
23455
|
+
function expandTildePath(path4) {
|
|
23456
|
+
if (path4 === "~") {
|
|
23347
23457
|
return homedir3();
|
|
23348
23458
|
}
|
|
23349
|
-
if (
|
|
23350
|
-
return join4(homedir3(),
|
|
23459
|
+
if (path4.startsWith("~/")) {
|
|
23460
|
+
return join4(homedir3(), path4.slice(2));
|
|
23351
23461
|
}
|
|
23352
|
-
return
|
|
23462
|
+
return path4;
|
|
23353
23463
|
}
|
|
23354
23464
|
var createPathKey = (absolutePath) => {
|
|
23355
23465
|
const normalizedPath = normalize(absolutePath);
|
|
@@ -23427,6 +23537,12 @@ function createDefaultNotificationConfig() {
|
|
|
23427
23537
|
errors: true
|
|
23428
23538
|
};
|
|
23429
23539
|
}
|
|
23540
|
+
function createDefaultTuiCommandMirrorConfig() {
|
|
23541
|
+
return {
|
|
23542
|
+
enabled: true,
|
|
23543
|
+
directory: MANAGED_TUI_COMMANDS_DIRECTORY
|
|
23544
|
+
};
|
|
23545
|
+
}
|
|
23430
23546
|
function createDefaultPluginConfig() {
|
|
23431
23547
|
return {
|
|
23432
23548
|
debug: false,
|
|
@@ -23435,7 +23551,8 @@ function createDefaultPluginConfig() {
|
|
|
23435
23551
|
enableSkillAliases: true,
|
|
23436
23552
|
reservedSlashCommands: [...DEFAULT_RESERVED_SLASH_COMMANDS],
|
|
23437
23553
|
notifications: createDefaultNotificationConfig(),
|
|
23438
|
-
skillRecommend: createDefaultSkillRecommendConfig()
|
|
23554
|
+
skillRecommend: createDefaultSkillRecommendConfig(),
|
|
23555
|
+
tuiCommandMirror: createDefaultTuiCommandMirrorConfig()
|
|
23439
23556
|
};
|
|
23440
23557
|
}
|
|
23441
23558
|
function createConfigOptions(defaultConfig3) {
|
|
@@ -23561,6 +23678,10 @@ function mergePluginConfig(baseConfig, override) {
|
|
|
23561
23678
|
...baseConfig.notifications,
|
|
23562
23679
|
...override.notifications
|
|
23563
23680
|
};
|
|
23681
|
+
const tuiCommandMirror = {
|
|
23682
|
+
...baseConfig.tuiCommandMirror,
|
|
23683
|
+
...override.tuiCommandMirror
|
|
23684
|
+
};
|
|
23564
23685
|
const overrideReservedSlashCommands = override.reservedSlashCommands?.map((command) => trimString(command)).filter(Boolean) ?? [];
|
|
23565
23686
|
return {
|
|
23566
23687
|
...baseConfig,
|
|
@@ -23575,6 +23696,10 @@ function mergePluginConfig(baseConfig, override) {
|
|
|
23575
23696
|
enabled: notifications.enabled === true,
|
|
23576
23697
|
success: notifications.success !== false,
|
|
23577
23698
|
errors: notifications.errors !== false
|
|
23699
|
+
},
|
|
23700
|
+
tuiCommandMirror: {
|
|
23701
|
+
enabled: tuiCommandMirror.enabled === true,
|
|
23702
|
+
directory: trimString(tuiCommandMirror.directory) || MANAGED_TUI_COMMANDS_DIRECTORY
|
|
23578
23703
|
}
|
|
23579
23704
|
};
|
|
23580
23705
|
}
|
|
@@ -23631,6 +23756,12 @@ function renderManagedPluginConfigJsonc(config3 = createDefaultPluginConfig()) {
|
|
|
23631
23756
|
` "model": ${JSON.stringify(config3.skillRecommend.model)},`,
|
|
23632
23757
|
' // The model must return strict JSON: {"recommendations":[{"name":"skill_tool_name","reason":"why it matches"}]}',
|
|
23633
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)}`,
|
|
23634
23765
|
" }",
|
|
23635
23766
|
"}",
|
|
23636
23767
|
""
|
|
@@ -23672,6 +23803,10 @@ async function getPluginConfig(ctx) {
|
|
|
23672
23803
|
...getProjectSkillBasePaths(ctx.directory, ctx.worktree)
|
|
23673
23804
|
];
|
|
23674
23805
|
mergedConfig.basePaths = normalizeBasePaths(configuredBasePaths, ctx.directory);
|
|
23806
|
+
mergedConfig.tuiCommandMirror = {
|
|
23807
|
+
enabled: mergedConfig.tuiCommandMirror.enabled,
|
|
23808
|
+
directory: resolveBasePath(mergedConfig.tuiCommandMirror.directory, ctx.directory)
|
|
23809
|
+
};
|
|
23675
23810
|
return mergedConfig;
|
|
23676
23811
|
}
|
|
23677
23812
|
|
|
@@ -23730,10 +23865,10 @@ function findSkillBySelector(registry2, selector) {
|
|
|
23730
23865
|
}
|
|
23731
23866
|
return null;
|
|
23732
23867
|
}
|
|
23733
|
-
function renderSlashSkillPrompt(args) {
|
|
23868
|
+
async function renderSlashSkillPrompt(args) {
|
|
23734
23869
|
return [
|
|
23735
23870
|
SLASH_COMMAND_SENTINEL,
|
|
23736
|
-
formatLoadedSkill({
|
|
23871
|
+
await formatLoadedSkill({
|
|
23737
23872
|
invocationName: args.invocationName,
|
|
23738
23873
|
skill: args.skill,
|
|
23739
23874
|
userMessage: args.userPrompt || "Apply this skill to the current request."
|
|
@@ -23787,13 +23922,33 @@ async function rewriteSlashCommandText(args) {
|
|
|
23787
23922
|
if (!skill) {
|
|
23788
23923
|
return null;
|
|
23789
23924
|
}
|
|
23790
|
-
return renderSlashSkillPrompt({
|
|
23925
|
+
return await renderSlashSkillPrompt({
|
|
23791
23926
|
invocationName: parsedCommand.invocationName,
|
|
23792
23927
|
skill,
|
|
23793
23928
|
userPrompt: parsedCommand.userPrompt
|
|
23794
23929
|
});
|
|
23795
23930
|
}
|
|
23796
23931
|
|
|
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
|
+
}
|
|
23951
|
+
|
|
23797
23952
|
// src/lib/xml.ts
|
|
23798
23953
|
function escapeXml(str2) {
|
|
23799
23954
|
return String(str2).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
@@ -23979,17 +24134,141 @@ function createNotifier(args) {
|
|
|
23979
24134
|
};
|
|
23980
24135
|
}
|
|
23981
24136
|
|
|
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();
|
|
24143
|
+
}
|
|
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: []
|
|
24170
|
+
};
|
|
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;
|
|
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;
|
|
24206
|
+
}
|
|
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
|
+
});
|
|
24226
|
+
return {
|
|
24227
|
+
mirrored,
|
|
24228
|
+
written,
|
|
24229
|
+
skipped,
|
|
24230
|
+
removed
|
|
24231
|
+
};
|
|
24232
|
+
}
|
|
24233
|
+
|
|
23982
24234
|
// src/index.ts
|
|
23983
24235
|
var SkillsPlugin = async (ctx) => {
|
|
23984
24236
|
const config3 = await getPluginConfig(ctx);
|
|
23985
24237
|
const api2 = await createApi(config3, ctx.client);
|
|
23986
24238
|
const sendPrompt = createInstructionInjector(ctx);
|
|
23987
|
-
const
|
|
24239
|
+
const renderXmlPrompt = createXmlPromptRenderer().render;
|
|
23988
24240
|
const notifier = createNotifier({
|
|
23989
24241
|
config: config3.notifications,
|
|
23990
24242
|
logger: api2.logger
|
|
23991
24243
|
});
|
|
23992
|
-
|
|
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
|
|
24266
|
+
});
|
|
24267
|
+
}
|
|
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
|
+
}
|
|
23993
24272
|
return {
|
|
23994
24273
|
"chat.message": async (_input, output) => {
|
|
23995
24274
|
for (const part of output.parts) {
|
|
@@ -24019,6 +24298,22 @@ var SkillsPlugin = async (ctx) => {
|
|
|
24019
24298
|
break;
|
|
24020
24299
|
}
|
|
24021
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
|
+
},
|
|
24022
24317
|
async event(args) {
|
|
24023
24318
|
switch (args.event.type) {
|
|
24024
24319
|
case "session.error":
|
|
@@ -24037,7 +24332,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
24037
24332
|
try {
|
|
24038
24333
|
const results = await api2.loadSkill(args.skill_names);
|
|
24039
24334
|
for await (const skill of results.loaded) {
|
|
24040
|
-
await sendPrompt(
|
|
24335
|
+
await sendPrompt(await formatLoadedSkill({ skill }), {
|
|
24041
24336
|
sessionId: toolCtx.sessionID,
|
|
24042
24337
|
agent: toolCtx.agent
|
|
24043
24338
|
});
|
|
@@ -24060,7 +24355,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
24060
24355
|
},
|
|
24061
24356
|
execute: async (args) => {
|
|
24062
24357
|
const results = await api2.findSkills(args);
|
|
24063
|
-
const output =
|
|
24358
|
+
const output = renderXmlPrompt({
|
|
24064
24359
|
data: results,
|
|
24065
24360
|
type: "SkillSearchResults"
|
|
24066
24361
|
});
|
|
@@ -24075,7 +24370,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
24075
24370
|
},
|
|
24076
24371
|
execute: async (args) => {
|
|
24077
24372
|
const results = await api2.recommendSkills(args);
|
|
24078
|
-
return
|
|
24373
|
+
return renderXmlPrompt({
|
|
24079
24374
|
data: results,
|
|
24080
24375
|
type: "SkillSearchResults"
|
|
24081
24376
|
});
|
|
@@ -24093,7 +24388,7 @@ var SkillsPlugin = async (ctx) => {
|
|
|
24093
24388
|
if (!result.injection) {
|
|
24094
24389
|
throw new Error("Failed to read resource");
|
|
24095
24390
|
}
|
|
24096
|
-
await sendPrompt(
|
|
24391
|
+
await sendPrompt(renderXmlPrompt({ data: result.injection, type: "SkillResource" }), {
|
|
24097
24392
|
sessionId: toolCtx.sessionID,
|
|
24098
24393
|
agent: toolCtx.agent
|
|
24099
24394
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PluginLogger, Skill } from '../types';
|
|
2
|
+
export declare const SHADOW_COMMAND_MARKER = "<!-- opencode-dynamic-skills:tui-command-mirror -->";
|
|
3
|
+
export declare function renderShadowCommandFile(skill: Skill): string;
|
|
4
|
+
export declare function syncTuiCommandMirror(args: {
|
|
5
|
+
skills: Skill[];
|
|
6
|
+
directory: string;
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
logger: PluginLogger;
|
|
9
|
+
reservedSlashCommands: string[];
|
|
10
|
+
}): Promise<{
|
|
11
|
+
mirrored: string[];
|
|
12
|
+
written: string[];
|
|
13
|
+
skipped: string[];
|
|
14
|
+
removed: string[];
|
|
15
|
+
}>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -159,6 +159,7 @@ export type PluginConfig = {
|
|
|
159
159
|
reservedSlashCommands: string[];
|
|
160
160
|
notifications: NotificationConfig;
|
|
161
161
|
skillRecommend: SkillRecommendConfig;
|
|
162
|
+
tuiCommandMirror: TuiCommandMirrorConfig;
|
|
162
163
|
};
|
|
163
164
|
export type NotificationConfig = {
|
|
164
165
|
enabled: boolean;
|
|
@@ -170,6 +171,10 @@ export type SkillRecommendConfig = {
|
|
|
170
171
|
model: string;
|
|
171
172
|
systemPrompt: string;
|
|
172
173
|
};
|
|
174
|
+
export type TuiCommandMirrorConfig = {
|
|
175
|
+
enabled: boolean;
|
|
176
|
+
directory: string;
|
|
177
|
+
};
|
|
173
178
|
export type LogType = 'log' | 'debug' | 'error' | 'warn';
|
|
174
179
|
export type PluginLogger = Record<LogType, (...message: unknown[]) => void>;
|
|
175
180
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-dynamic-skills",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "OpenCode plugin for dynamic skill loading, slash skill commands, and skill-root-relative resources",
|
|
5
5
|
"homepage": "https://github.com/Wu-H-Y/opencode-dynamic-skills#readme",
|
|
6
6
|
"bugs": {
|