oh-my-opencode-medium 0.8.2
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/LICENSE +21 -0
- package/README.md +284 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +22 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +15 -0
- package/dist/background/background-manager.d.ts +175 -0
- package/dist/background/index.d.ts +2 -0
- package/dist/background/tmux-session-manager.d.ts +63 -0
- package/dist/cli/chutes-selection.d.ts +3 -0
- package/dist/cli/config-io.d.ts +26 -0
- package/dist/cli/config-manager.d.ts +12 -0
- package/dist/cli/custom-skills.d.ts +29 -0
- package/dist/cli/dynamic-model-selection.d.ts +14 -0
- package/dist/cli/external-rankings.d.ts +8 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +17077 -0
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/model-selection.d.ts +30 -0
- package/dist/cli/opencode-models.d.ts +18 -0
- package/dist/cli/opencode-selection.d.ts +3 -0
- package/dist/cli/paths.d.ts +9 -0
- package/dist/cli/precedence-resolver.d.ts +16 -0
- package/dist/cli/providers.d.ts +204 -0
- package/dist/cli/scoring-v2/engine.d.ts +4 -0
- package/dist/cli/scoring-v2/features.d.ts +3 -0
- package/dist/cli/scoring-v2/index.d.ts +4 -0
- package/dist/cli/scoring-v2/types.d.ts +17 -0
- package/dist/cli/scoring-v2/weights.d.ts +2 -0
- package/dist/cli/skills.d.ts +52 -0
- package/dist/cli/system.d.ts +6 -0
- package/dist/cli/types.d.ts +138 -0
- package/dist/config/agent-mcps.d.ts +15 -0
- package/dist/config/constants.d.ts +14 -0
- package/dist/config/index.d.ts +4 -0
- package/dist/config/loader.d.ts +30 -0
- package/dist/config/schema.d.ts +217 -0
- package/dist/config/utils.d.ts +10 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +6 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +28 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +11 -0
- package/dist/hooks/auto-update-checker/index.d.ts +17 -0
- package/dist/hooks/auto-update-checker/types.d.ts +23 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/delegate-task-retry/guidance.d.ts +2 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/index.d.ts +4 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/json-error-recovery/index.d.ts +1 -0
- package/dist/hooks/phase-reminder/index.d.ts +26 -0
- package/dist/hooks/post-read-nudge/index.d.ts +18 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +33970 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +6 -0
- package/dist/skills/loader.d.ts +13 -0
- package/dist/skills/register.d.ts +2 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/background.d.ts +13 -0
- package/dist/tools/grep/cli.d.ts +3 -0
- package/dist/tools/grep/constants.d.ts +18 -0
- package/dist/tools/grep/downloader.d.ts +3 -0
- package/dist/tools/grep/index.d.ts +5 -0
- package/dist/tools/grep/tools.d.ts +2 -0
- package/dist/tools/grep/types.d.ts +35 -0
- package/dist/tools/grep/utils.d.ts +2 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/lsp/client.d.ts +42 -0
- package/dist/tools/lsp/config.d.ts +4 -0
- package/dist/tools/lsp/constants.d.ts +8 -0
- package/dist/tools/lsp/index.d.ts +3 -0
- package/dist/tools/lsp/tools.d.ts +5 -0
- package/dist/tools/lsp/types.d.ts +28 -0
- package/dist/tools/lsp/utils.d.ts +21 -0
- package/dist/utils/agent-variant.d.ts +47 -0
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/frontmatter.d.ts +15 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/internal-initiator.d.ts +6 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/tmux.d.ts +32 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/package.json +68 -0
- package/src/skills/cartography/README.md +57 -0
- package/src/skills/cartography/SKILL.md +137 -0
- package/src/skills/cartography/scripts/cartographer.py +456 -0
- package/src/skills/cartography/scripts/test_cartographer.py +87 -0
- package/src/skills/loader.test.ts +452 -0
- package/src/skills/loader.ts +300 -0
- package/src/skills/register.test.ts +55 -0
- package/src/skills/register.ts +15 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { type Dirent, existsSync } from 'node:fs';
|
|
2
|
+
import { readdir, readFile, realpath, stat } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, join, relative } from 'node:path';
|
|
5
|
+
import { parseFrontmatter } from '../utils';
|
|
6
|
+
import { log } from '../utils/logger';
|
|
7
|
+
|
|
8
|
+
export type SkillSource = 'agents' | 'opencode';
|
|
9
|
+
|
|
10
|
+
export interface CommandDefinition {
|
|
11
|
+
description?: string;
|
|
12
|
+
template: string;
|
|
13
|
+
model?: string;
|
|
14
|
+
agent?: string;
|
|
15
|
+
subtask?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SkillFile {
|
|
19
|
+
filePath: string;
|
|
20
|
+
commandName: string;
|
|
21
|
+
root: string;
|
|
22
|
+
source: SkillSource;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LoadedCommand {
|
|
26
|
+
definition: CommandDefinition;
|
|
27
|
+
sourcePath: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface ParsedSkillFile {
|
|
31
|
+
skillFile: SkillFile;
|
|
32
|
+
content?: string;
|
|
33
|
+
readError?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const discoveredSkillsCache = new Map<
|
|
37
|
+
string,
|
|
38
|
+
Promise<Record<string, CommandDefinition>>
|
|
39
|
+
>();
|
|
40
|
+
|
|
41
|
+
function toSubtask(value?: string): boolean | undefined {
|
|
42
|
+
if (value === 'true') return true;
|
|
43
|
+
if (value === 'false') return false;
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sanitizeModelField(
|
|
48
|
+
model: string | undefined,
|
|
49
|
+
source: SkillSource,
|
|
50
|
+
): string | undefined {
|
|
51
|
+
if (source === 'agents') return undefined;
|
|
52
|
+
if (!model?.trim()) return undefined;
|
|
53
|
+
return model.trim();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveSkillPathReferences(body: string, dir: string): string {
|
|
57
|
+
const normalizedDir = dir.endsWith('/') ? dir.slice(0, -1) : dir;
|
|
58
|
+
return body.replace(
|
|
59
|
+
/(?<![a-zA-Z0-9])@((?=[a-zA-Z0-9_.\-/]*[/.])[a-zA-Z0-9_.\-/]+)/g,
|
|
60
|
+
(_, target: string) => join(normalizedDir, target),
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function wrapSkillTemplate(body: string, dir: string): string {
|
|
65
|
+
const resolvedBody = resolveSkillPathReferences(body, dir);
|
|
66
|
+
const requestBlock = resolvedBody.includes('$ARGUMENTS')
|
|
67
|
+
? ''
|
|
68
|
+
: '\n\n<user-request>\n$ARGUMENTS\n</user-request>';
|
|
69
|
+
return `<skill-instruction>\nBase directory for this skill: ${dir}/\nFile references (@path) in this skill are relative to this directory.\n\n${resolvedBody}</skill-instruction>${requestBlock}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function createSkillTemplate(
|
|
73
|
+
body: string,
|
|
74
|
+
dir: string,
|
|
75
|
+
isWrapped: boolean,
|
|
76
|
+
): string {
|
|
77
|
+
if (!isWrapped) {
|
|
78
|
+
return resolveSkillPathReferences(body, dir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return wrapSkillTemplate(body, dir);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function toWrappedCommandName(root: string, skillDir: string): string {
|
|
85
|
+
return relative(root, skillDir).split(/[/\\]/).join(':');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveCommandName(
|
|
89
|
+
configuredName: string | undefined,
|
|
90
|
+
discoveredName: string,
|
|
91
|
+
isWrappedSkill: boolean,
|
|
92
|
+
): string {
|
|
93
|
+
if (!configuredName) return discoveredName;
|
|
94
|
+
if (!isWrappedSkill) return configuredName;
|
|
95
|
+
if (!discoveredName.includes(':')) return configuredName;
|
|
96
|
+
if (configuredName.includes(':')) return configuredName;
|
|
97
|
+
|
|
98
|
+
const namespace = discoveredName.split(':').slice(0, -1).join(':');
|
|
99
|
+
return `${namespace}:${configuredName}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function collectSkillFiles(
|
|
103
|
+
root: string,
|
|
104
|
+
source: SkillSource,
|
|
105
|
+
currentDir = root,
|
|
106
|
+
visitedDirs = new Set<string>(),
|
|
107
|
+
): Promise<SkillFile[]> {
|
|
108
|
+
let resolvedDir: string;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
resolvedDir = await realpath(currentDir);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
log('[skills] failed to resolve skill directory', {
|
|
114
|
+
root: relative(process.cwd(), currentDir),
|
|
115
|
+
source,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
});
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (visitedDirs.has(resolvedDir)) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
visitedDirs.add(resolvedDir);
|
|
126
|
+
|
|
127
|
+
let entries: Dirent[];
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
entries = await readdir(currentDir, { withFileTypes: true });
|
|
131
|
+
} catch (error) {
|
|
132
|
+
log('[skills] failed to read skill directory', {
|
|
133
|
+
root: relative(process.cwd(), currentDir),
|
|
134
|
+
source,
|
|
135
|
+
error: error instanceof Error ? error.message : String(error),
|
|
136
|
+
});
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
141
|
+
const files: SkillFile[] = [];
|
|
142
|
+
|
|
143
|
+
for (const entry of entries) {
|
|
144
|
+
const entryPath = join(currentDir, entry.name);
|
|
145
|
+
const isDirectory =
|
|
146
|
+
entry.isDirectory() ||
|
|
147
|
+
(entry.isSymbolicLink() &&
|
|
148
|
+
(await stat(entryPath).then(
|
|
149
|
+
(stats) => stats.isDirectory(),
|
|
150
|
+
() => false,
|
|
151
|
+
)));
|
|
152
|
+
|
|
153
|
+
if (isDirectory) {
|
|
154
|
+
const skillFilePath = join(entryPath, 'SKILL.md');
|
|
155
|
+
if (existsSync(skillFilePath)) {
|
|
156
|
+
files.push({
|
|
157
|
+
filePath: skillFilePath,
|
|
158
|
+
commandName: toWrappedCommandName(root, entryPath),
|
|
159
|
+
root,
|
|
160
|
+
source,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
files.push(
|
|
164
|
+
...(await collectSkillFiles(root, source, entryPath, visitedDirs)),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (currentDir !== root) continue;
|
|
172
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
173
|
+
if (entry.name === 'SKILL.md') continue;
|
|
174
|
+
|
|
175
|
+
files.push({
|
|
176
|
+
filePath: entryPath,
|
|
177
|
+
commandName: basename(entry.name, '.md'),
|
|
178
|
+
root,
|
|
179
|
+
source,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return files;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function loadSkillsFromDirectories(
|
|
187
|
+
directories: Array<{ path: string; source: SkillSource }>,
|
|
188
|
+
): Promise<Record<string, CommandDefinition>> {
|
|
189
|
+
const commands: Record<string, LoadedCommand> = {};
|
|
190
|
+
|
|
191
|
+
const skillFilesByDirectory = await Promise.all(
|
|
192
|
+
directories.map(async (dir) => {
|
|
193
|
+
if (!existsSync(dir.path)) return [];
|
|
194
|
+
return collectSkillFiles(dir.path, dir.source);
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
for (const skillFiles of skillFilesByDirectory) {
|
|
199
|
+
const parsedSkillFiles: ParsedSkillFile[] = await Promise.all(
|
|
200
|
+
skillFiles.map(async (skillFile) => {
|
|
201
|
+
try {
|
|
202
|
+
const content = await readFile(skillFile.filePath, 'utf8');
|
|
203
|
+
return { skillFile, content };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
skillFile,
|
|
207
|
+
readError: error instanceof Error ? error.message : String(error),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
for (const parsedSkillFile of parsedSkillFiles) {
|
|
214
|
+
if (parsedSkillFile.readError) {
|
|
215
|
+
log('[skills] failed to read skill file', {
|
|
216
|
+
filePath: relative(process.cwd(), parsedSkillFile.skillFile.filePath),
|
|
217
|
+
source: parsedSkillFile.skillFile.source,
|
|
218
|
+
error: parsedSkillFile.readError,
|
|
219
|
+
});
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const { skillFile, content } = parsedSkillFile;
|
|
224
|
+
if (!content) continue;
|
|
225
|
+
|
|
226
|
+
const { data, body, parseError } = parseFrontmatter(content);
|
|
227
|
+
|
|
228
|
+
if (parseError) {
|
|
229
|
+
log('[skills] skipped malformed skill frontmatter', {
|
|
230
|
+
filePath: relative(process.cwd(), skillFile.filePath),
|
|
231
|
+
source: skillFile.source,
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const isWrappedSkill = basename(skillFile.filePath) === 'SKILL.md';
|
|
237
|
+
const commandName = resolveCommandName(
|
|
238
|
+
data.name,
|
|
239
|
+
skillFile.commandName,
|
|
240
|
+
isWrappedSkill,
|
|
241
|
+
);
|
|
242
|
+
const skillDir = isWrappedSkill
|
|
243
|
+
? dirname(skillFile.filePath)
|
|
244
|
+
: skillFile.root;
|
|
245
|
+
|
|
246
|
+
if (commands[commandName]) {
|
|
247
|
+
log('[skills] overriding command', {
|
|
248
|
+
commandName,
|
|
249
|
+
previousPath: relative(
|
|
250
|
+
process.cwd(),
|
|
251
|
+
commands[commandName].sourcePath,
|
|
252
|
+
),
|
|
253
|
+
filePath: relative(process.cwd(), skillFile.filePath),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
commands[commandName] = {
|
|
258
|
+
definition: {
|
|
259
|
+
description: data.description,
|
|
260
|
+
template: createSkillTemplate(body, skillDir, isWrappedSkill),
|
|
261
|
+
model: sanitizeModelField(data.model, skillFile.source),
|
|
262
|
+
agent: data.agent,
|
|
263
|
+
subtask: toSubtask(data.subtask),
|
|
264
|
+
},
|
|
265
|
+
sourcePath: skillFile.filePath,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return Object.fromEntries(
|
|
271
|
+
Object.entries(commands).map(([name, loaded]) => [name, loaded.definition]),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function discoverAllSkills(
|
|
276
|
+
projectDir: string,
|
|
277
|
+
homeDir = homedir(),
|
|
278
|
+
): Promise<Record<string, CommandDefinition>> {
|
|
279
|
+
const opencodeConfigDir =
|
|
280
|
+
process.env.XDG_CONFIG_HOME ?? join(homeDir, '.config');
|
|
281
|
+
const cacheKey = `${projectDir}\u0000${homeDir}\u0000${opencodeConfigDir}`;
|
|
282
|
+
const cached = discoveredSkillsCache.get(cacheKey);
|
|
283
|
+
if (cached) return cached;
|
|
284
|
+
|
|
285
|
+
const pending = loadSkillsFromDirectories([
|
|
286
|
+
{ path: join(homeDir, '.agents', 'skills'), source: 'agents' },
|
|
287
|
+
{
|
|
288
|
+
path: join(opencodeConfigDir, 'opencode', 'skills'),
|
|
289
|
+
source: 'opencode',
|
|
290
|
+
},
|
|
291
|
+
{ path: join(projectDir, '.agents', 'skills'), source: 'agents' },
|
|
292
|
+
{ path: join(projectDir, '.opencode', 'skills'), source: 'opencode' },
|
|
293
|
+
]).catch((error) => {
|
|
294
|
+
discoveredSkillsCache.delete(cacheKey);
|
|
295
|
+
throw error;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
discoveredSkillsCache.set(cacheKey, pending);
|
|
299
|
+
return pending;
|
|
300
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { mergeSkillCommands } from './register';
|
|
3
|
+
|
|
4
|
+
describe('mergeSkillCommands', () => {
|
|
5
|
+
it('preserves existing commands and appends loaded skill commands', () => {
|
|
6
|
+
const config = {
|
|
7
|
+
command: {
|
|
8
|
+
existing: {
|
|
9
|
+
description: 'Existing command',
|
|
10
|
+
template: 'Existing template',
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
mergeSkillCommands(config, {
|
|
16
|
+
atlas: {
|
|
17
|
+
description: 'Map repo',
|
|
18
|
+
template: 'Map the repository',
|
|
19
|
+
agent: 'explorer',
|
|
20
|
+
subtask: true,
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(config.command.existing).toBeDefined();
|
|
25
|
+
expect(config.command.atlas).toEqual({
|
|
26
|
+
description: 'Map repo',
|
|
27
|
+
template: 'Map the repository',
|
|
28
|
+
agent: 'explorer',
|
|
29
|
+
subtask: true,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('keeps existing commands when a loaded skill uses the same name', () => {
|
|
34
|
+
const config = {
|
|
35
|
+
command: {
|
|
36
|
+
atlas: {
|
|
37
|
+
description: 'Existing command',
|
|
38
|
+
template: 'Existing template',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
mergeSkillCommands(config, {
|
|
44
|
+
atlas: {
|
|
45
|
+
description: 'Loaded skill',
|
|
46
|
+
template: 'Loaded template',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(config.command.atlas).toEqual({
|
|
51
|
+
description: 'Existing command',
|
|
52
|
+
template: 'Existing template',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CommandDefinition } from './loader';
|
|
2
|
+
|
|
3
|
+
export function mergeSkillCommands(
|
|
4
|
+
opencodeConfig: Record<string, unknown>,
|
|
5
|
+
loadedSkills: Record<string, CommandDefinition>,
|
|
6
|
+
): void {
|
|
7
|
+
const existing =
|
|
8
|
+
(opencodeConfig.command as Record<string, CommandDefinition> | undefined) ??
|
|
9
|
+
{};
|
|
10
|
+
|
|
11
|
+
opencodeConfig.command = {
|
|
12
|
+
...loadedSkills,
|
|
13
|
+
...existing,
|
|
14
|
+
};
|
|
15
|
+
}
|