opencode-swarm-plugin 0.44.0 → 0.44.1
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/bin/swarm.serve.test.ts +6 -4
- package/bin/swarm.ts +16 -10
- package/dist/compaction-prompt-scoring.js +139 -0
- package/dist/eval-capture.js +12811 -0
- package/dist/hive.d.ts.map +1 -1
- package/dist/index.js +7644 -62599
- package/dist/plugin.js +23766 -78721
- package/dist/swarm-orchestrate.d.ts.map +1 -1
- package/dist/swarm-prompts.d.ts.map +1 -1
- package/dist/swarm-review.d.ts.map +1 -1
- package/package.json +17 -5
- package/.changeset/swarm-insights-data-layer.md +0 -63
- package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
- package/.hive/analysis/session-data-quality-audit.md +0 -320
- package/.hive/eval-results.json +0 -483
- package/.hive/issues.jsonl +0 -138
- package/.hive/memories.jsonl +0 -729
- package/.opencode/eval-history.jsonl +0 -327
- package/.turbo/turbo-build.log +0 -9
- package/CHANGELOG.md +0 -2286
- package/SCORER-ANALYSIS.md +0 -598
- package/docs/analysis/subagent-coordination-patterns.md +0 -902
- package/docs/analysis-socratic-planner-pattern.md +0 -504
- package/docs/planning/ADR-001-monorepo-structure.md +0 -171
- package/docs/planning/ADR-002-package-extraction.md +0 -393
- package/docs/planning/ADR-003-performance-improvements.md +0 -451
- package/docs/planning/ADR-004-message-queue-features.md +0 -187
- package/docs/planning/ADR-005-devtools-observability.md +0 -202
- package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
- package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
- package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
- package/docs/planning/ADR-010-cass-inhousing.md +0 -1215
- package/docs/planning/ROADMAP.md +0 -368
- package/docs/semantic-memory-cli-syntax.md +0 -123
- package/docs/swarm-mail-architecture.md +0 -1147
- package/docs/testing/context-recovery-test.md +0 -470
- package/evals/ARCHITECTURE.md +0 -1189
- package/evals/README.md +0 -768
- package/evals/compaction-prompt.eval.ts +0 -149
- package/evals/compaction-resumption.eval.ts +0 -289
- package/evals/coordinator-behavior.eval.ts +0 -307
- package/evals/coordinator-session.eval.ts +0 -154
- package/evals/evalite.config.ts.bak +0 -15
- package/evals/example.eval.ts +0 -31
- package/evals/fixtures/cass-baseline.ts +0 -217
- package/evals/fixtures/compaction-cases.ts +0 -350
- package/evals/fixtures/compaction-prompt-cases.ts +0 -311
- package/evals/fixtures/coordinator-sessions.ts +0 -328
- package/evals/fixtures/decomposition-cases.ts +0 -105
- package/evals/lib/compaction-loader.test.ts +0 -248
- package/evals/lib/compaction-loader.ts +0 -320
- package/evals/lib/data-loader.evalite-test.ts +0 -289
- package/evals/lib/data-loader.test.ts +0 -345
- package/evals/lib/data-loader.ts +0 -281
- package/evals/lib/llm.ts +0 -115
- package/evals/scorers/compaction-prompt-scorers.ts +0 -145
- package/evals/scorers/compaction-scorers.ts +0 -305
- package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
- package/evals/scorers/coordinator-discipline.ts +0 -325
- package/evals/scorers/index.test.ts +0 -146
- package/evals/scorers/index.ts +0 -328
- package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
- package/evals/scorers/outcome-scorers.ts +0 -349
- package/evals/swarm-decomposition.eval.ts +0 -121
- package/examples/commands/swarm.md +0 -745
- package/examples/plugin-wrapper-template.ts +0 -2515
- package/examples/skills/hive-workflow/SKILL.md +0 -212
- package/examples/skills/skill-creator/SKILL.md +0 -223
- package/examples/skills/swarm-coordination/SKILL.md +0 -292
- package/global-skills/cli-builder/SKILL.md +0 -344
- package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
- package/global-skills/learning-systems/SKILL.md +0 -644
- package/global-skills/skill-creator/LICENSE.txt +0 -202
- package/global-skills/skill-creator/SKILL.md +0 -352
- package/global-skills/skill-creator/references/output-patterns.md +0 -82
- package/global-skills/skill-creator/references/workflows.md +0 -28
- package/global-skills/swarm-coordination/SKILL.md +0 -995
- package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
- package/global-skills/swarm-coordination/references/strategies.md +0 -138
- package/global-skills/system-design/SKILL.md +0 -213
- package/global-skills/testing-patterns/SKILL.md +0 -430
- package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
- package/opencode-swarm-plugin-0.30.7.tgz +0 -0
- package/opencode-swarm-plugin-0.31.0.tgz +0 -0
- package/scripts/cleanup-test-memories.ts +0 -346
- package/scripts/init-skill.ts +0 -222
- package/scripts/migrate-unknown-sessions.ts +0 -349
- package/scripts/validate-skill.ts +0 -204
- package/src/agent-mail.ts +0 -1724
- package/src/anti-patterns.test.ts +0 -1167
- package/src/anti-patterns.ts +0 -448
- package/src/compaction-capture.integration.test.ts +0 -257
- package/src/compaction-hook.test.ts +0 -838
- package/src/compaction-hook.ts +0 -1204
- package/src/compaction-observability.integration.test.ts +0 -139
- package/src/compaction-observability.test.ts +0 -187
- package/src/compaction-observability.ts +0 -324
- package/src/compaction-prompt-scorers.test.ts +0 -475
- package/src/compaction-prompt-scoring.ts +0 -300
- package/src/contributor-tools.test.ts +0 -133
- package/src/contributor-tools.ts +0 -201
- package/src/dashboard.test.ts +0 -611
- package/src/dashboard.ts +0 -462
- package/src/error-enrichment.test.ts +0 -403
- package/src/error-enrichment.ts +0 -219
- package/src/eval-capture.test.ts +0 -1015
- package/src/eval-capture.ts +0 -929
- package/src/eval-gates.test.ts +0 -306
- package/src/eval-gates.ts +0 -218
- package/src/eval-history.test.ts +0 -508
- package/src/eval-history.ts +0 -214
- package/src/eval-learning.test.ts +0 -378
- package/src/eval-learning.ts +0 -360
- package/src/eval-runner.test.ts +0 -223
- package/src/eval-runner.ts +0 -402
- package/src/export-tools.test.ts +0 -476
- package/src/export-tools.ts +0 -257
- package/src/hive.integration.test.ts +0 -2241
- package/src/hive.ts +0 -1628
- package/src/index.ts +0 -940
- package/src/learning.integration.test.ts +0 -1815
- package/src/learning.ts +0 -1079
- package/src/logger.test.ts +0 -189
- package/src/logger.ts +0 -135
- package/src/mandate-promotion.test.ts +0 -473
- package/src/mandate-promotion.ts +0 -239
- package/src/mandate-storage.integration.test.ts +0 -601
- package/src/mandate-storage.test.ts +0 -578
- package/src/mandate-storage.ts +0 -794
- package/src/mandates.ts +0 -540
- package/src/memory-tools.test.ts +0 -195
- package/src/memory-tools.ts +0 -344
- package/src/memory.integration.test.ts +0 -334
- package/src/memory.test.ts +0 -158
- package/src/memory.ts +0 -527
- package/src/model-selection.test.ts +0 -188
- package/src/model-selection.ts +0 -68
- package/src/observability-tools.test.ts +0 -359
- package/src/observability-tools.ts +0 -871
- package/src/output-guardrails.test.ts +0 -438
- package/src/output-guardrails.ts +0 -381
- package/src/pattern-maturity.test.ts +0 -1160
- package/src/pattern-maturity.ts +0 -525
- package/src/planning-guardrails.test.ts +0 -491
- package/src/planning-guardrails.ts +0 -438
- package/src/plugin.ts +0 -23
- package/src/post-compaction-tracker.test.ts +0 -251
- package/src/post-compaction-tracker.ts +0 -237
- package/src/query-tools.test.ts +0 -636
- package/src/query-tools.ts +0 -324
- package/src/rate-limiter.integration.test.ts +0 -466
- package/src/rate-limiter.ts +0 -774
- package/src/replay-tools.test.ts +0 -496
- package/src/replay-tools.ts +0 -240
- package/src/repo-crawl.integration.test.ts +0 -441
- package/src/repo-crawl.ts +0 -610
- package/src/schemas/cell-events.test.ts +0 -347
- package/src/schemas/cell-events.ts +0 -807
- package/src/schemas/cell.ts +0 -257
- package/src/schemas/evaluation.ts +0 -166
- package/src/schemas/index.test.ts +0 -199
- package/src/schemas/index.ts +0 -286
- package/src/schemas/mandate.ts +0 -232
- package/src/schemas/swarm-context.ts +0 -115
- package/src/schemas/task.ts +0 -161
- package/src/schemas/worker-handoff.test.ts +0 -302
- package/src/schemas/worker-handoff.ts +0 -131
- package/src/sessions/agent-discovery.test.ts +0 -137
- package/src/sessions/agent-discovery.ts +0 -112
- package/src/sessions/index.ts +0 -15
- package/src/skills.integration.test.ts +0 -1192
- package/src/skills.test.ts +0 -643
- package/src/skills.ts +0 -1549
- package/src/storage.integration.test.ts +0 -341
- package/src/storage.ts +0 -884
- package/src/structured.integration.test.ts +0 -817
- package/src/structured.test.ts +0 -1046
- package/src/structured.ts +0 -762
- package/src/swarm-decompose.test.ts +0 -188
- package/src/swarm-decompose.ts +0 -1302
- package/src/swarm-deferred.integration.test.ts +0 -157
- package/src/swarm-deferred.test.ts +0 -38
- package/src/swarm-insights.test.ts +0 -214
- package/src/swarm-insights.ts +0 -459
- package/src/swarm-mail.integration.test.ts +0 -970
- package/src/swarm-mail.ts +0 -739
- package/src/swarm-orchestrate.integration.test.ts +0 -282
- package/src/swarm-orchestrate.test.ts +0 -548
- package/src/swarm-orchestrate.ts +0 -3084
- package/src/swarm-prompts.test.ts +0 -1270
- package/src/swarm-prompts.ts +0 -2077
- package/src/swarm-research.integration.test.ts +0 -701
- package/src/swarm-research.test.ts +0 -698
- package/src/swarm-research.ts +0 -472
- package/src/swarm-review.integration.test.ts +0 -285
- package/src/swarm-review.test.ts +0 -879
- package/src/swarm-review.ts +0 -709
- package/src/swarm-strategies.ts +0 -407
- package/src/swarm-worktree.test.ts +0 -501
- package/src/swarm-worktree.ts +0 -575
- package/src/swarm.integration.test.ts +0 -2377
- package/src/swarm.ts +0 -38
- package/src/tool-adapter.integration.test.ts +0 -1221
- package/src/tool-availability.ts +0 -461
- package/tsconfig.json +0 -28
package/src/skills.ts
DELETED
|
@@ -1,1549 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skills Module for OpenCode
|
|
3
|
-
*
|
|
4
|
-
* Implements Anthropic's Agent Skills specification for OpenCode.
|
|
5
|
-
* Skills are markdown files with YAML frontmatter that provide
|
|
6
|
-
* domain-specific instructions the model can activate when relevant.
|
|
7
|
-
*
|
|
8
|
-
* Discovery locations (in priority order):
|
|
9
|
-
* 1. {projectDir}/.opencode/skills/
|
|
10
|
-
* 2. {projectDir}/.claude/skills/ (compatibility)
|
|
11
|
-
* 3. {projectDir}/skills/ (simple projects)
|
|
12
|
-
*
|
|
13
|
-
* Skill format:
|
|
14
|
-
* ```markdown
|
|
15
|
-
* ---
|
|
16
|
-
* name: my-skill
|
|
17
|
-
* description: What it does. Use when X.
|
|
18
|
-
* ---
|
|
19
|
-
*
|
|
20
|
-
* # Skill Instructions
|
|
21
|
-
* ...
|
|
22
|
-
* ```
|
|
23
|
-
*
|
|
24
|
-
* @module skills
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
import { tool } from "@opencode-ai/plugin";
|
|
28
|
-
import { readdir, readFile, stat, mkdir, writeFile, rm } from "fs/promises";
|
|
29
|
-
import {
|
|
30
|
-
join,
|
|
31
|
-
basename,
|
|
32
|
-
dirname,
|
|
33
|
-
resolve,
|
|
34
|
-
relative,
|
|
35
|
-
isAbsolute,
|
|
36
|
-
sep,
|
|
37
|
-
} from "path";
|
|
38
|
-
import { fileURLToPath } from "url";
|
|
39
|
-
import matter from "gray-matter";
|
|
40
|
-
|
|
41
|
-
// =============================================================================
|
|
42
|
-
// Types
|
|
43
|
-
// =============================================================================
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Skill metadata from YAML frontmatter
|
|
47
|
-
*/
|
|
48
|
-
export interface SkillMetadata {
|
|
49
|
-
/** Unique skill identifier (lowercase, hyphens) */
|
|
50
|
-
name: string;
|
|
51
|
-
/** Description of what the skill does and when to use it */
|
|
52
|
-
description: string;
|
|
53
|
-
/** Optional list of tools this skill works with */
|
|
54
|
-
tools?: string[];
|
|
55
|
-
/** Optional tags for categorization */
|
|
56
|
-
tags?: string[];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Full skill definition including content
|
|
61
|
-
*/
|
|
62
|
-
export interface Skill {
|
|
63
|
-
/** Parsed frontmatter metadata */
|
|
64
|
-
metadata: SkillMetadata;
|
|
65
|
-
/** Raw markdown body (instructions) */
|
|
66
|
-
body: string;
|
|
67
|
-
/** Absolute path to the SKILL.md file */
|
|
68
|
-
path: string;
|
|
69
|
-
/** Directory containing the skill */
|
|
70
|
-
directory: string;
|
|
71
|
-
/** Whether this skill has executable scripts */
|
|
72
|
-
hasScripts: boolean;
|
|
73
|
-
/** List of script files in the skill directory */
|
|
74
|
-
scripts: string[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Lightweight skill reference for listing
|
|
79
|
-
*/
|
|
80
|
-
export interface SkillRef {
|
|
81
|
-
name: string;
|
|
82
|
-
description: string;
|
|
83
|
-
path: string;
|
|
84
|
-
hasScripts: boolean;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// =============================================================================
|
|
88
|
-
// State
|
|
89
|
-
// =============================================================================
|
|
90
|
-
|
|
91
|
-
/** Cached project directory for skill discovery */
|
|
92
|
-
let skillsProjectDirectory: string = process.cwd();
|
|
93
|
-
|
|
94
|
-
/** Cached discovered skills (lazy-loaded) */
|
|
95
|
-
let skillsCache: Map<string, Skill> | null = null;
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Set the project directory for skill discovery
|
|
99
|
-
*/
|
|
100
|
-
export function setSkillsProjectDirectory(dir: string): void {
|
|
101
|
-
skillsProjectDirectory = dir;
|
|
102
|
-
skillsCache = null; // Invalidate cache when directory changes
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// =============================================================================
|
|
106
|
-
// YAML Frontmatter Parser
|
|
107
|
-
// =============================================================================
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Parse YAML frontmatter from markdown content using gray-matter
|
|
111
|
-
*
|
|
112
|
-
* Handles the common frontmatter format:
|
|
113
|
-
* ```
|
|
114
|
-
* ---
|
|
115
|
-
* key: value
|
|
116
|
-
* ---
|
|
117
|
-
* body content
|
|
118
|
-
* ```
|
|
119
|
-
*/
|
|
120
|
-
export function parseFrontmatter(content: string): {
|
|
121
|
-
metadata: Record<string, unknown>;
|
|
122
|
-
body: string;
|
|
123
|
-
} {
|
|
124
|
-
try {
|
|
125
|
-
const { data, content: body } = matter(content);
|
|
126
|
-
return { metadata: data, body: body.trim() };
|
|
127
|
-
} catch {
|
|
128
|
-
// If gray-matter fails, return empty metadata and full content as body
|
|
129
|
-
return { metadata: {}, body: content };
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Validate and extract skill metadata from parsed frontmatter
|
|
135
|
-
*/
|
|
136
|
-
function validateSkillMetadata(
|
|
137
|
-
raw: Record<string, unknown>,
|
|
138
|
-
filePath: string,
|
|
139
|
-
): SkillMetadata {
|
|
140
|
-
const name = raw.name;
|
|
141
|
-
const description = raw.description;
|
|
142
|
-
|
|
143
|
-
if (typeof name !== "string" || !name) {
|
|
144
|
-
throw new Error(`Skill at ${filePath} missing required 'name' field`);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (typeof description !== "string" || !description) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Skill at ${filePath} missing required 'description' field`,
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Validate name format
|
|
154
|
-
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
155
|
-
throw new Error(`Skill name '${name}' must be lowercase with hyphens only`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (name.length > 64) {
|
|
159
|
-
throw new Error(`Skill name '${name}' exceeds 64 character limit`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (description.length > 1024) {
|
|
163
|
-
throw new Error(
|
|
164
|
-
`Skill description for '${name}' exceeds 1024 character limit`,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
name,
|
|
170
|
-
description,
|
|
171
|
-
tools: Array.isArray(raw.tools)
|
|
172
|
-
? raw.tools.filter((t): t is string => typeof t === "string")
|
|
173
|
-
: undefined,
|
|
174
|
-
tags: Array.isArray(raw.tags)
|
|
175
|
-
? raw.tags.filter((t): t is string => typeof t === "string")
|
|
176
|
-
: undefined,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// =============================================================================
|
|
181
|
-
// Discovery
|
|
182
|
-
// =============================================================================
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Skill discovery locations relative to project root (checked first)
|
|
186
|
-
*/
|
|
187
|
-
const PROJECT_SKILL_DIRECTORIES = [
|
|
188
|
-
".opencode/skills",
|
|
189
|
-
".claude/skills",
|
|
190
|
-
"skills",
|
|
191
|
-
] as const;
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Global skills directory (user-level, checked after project)
|
|
195
|
-
*/
|
|
196
|
-
function getGlobalSkillsDir(): string {
|
|
197
|
-
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
198
|
-
return join(home, ".config", "opencode", "skills");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Claude Code global skills directory (compatibility)
|
|
203
|
-
*/
|
|
204
|
-
function getClaudeGlobalSkillsDir(): string {
|
|
205
|
-
const home = process.env.HOME || process.env.USERPROFILE || "~";
|
|
206
|
-
return join(home, ".claude", "skills");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Bundled skills from the package (lowest priority)
|
|
211
|
-
*/
|
|
212
|
-
function getPackageSkillsDir(): string {
|
|
213
|
-
// Resolve relative to this file (handles URL-encoding like spaces)
|
|
214
|
-
try {
|
|
215
|
-
const currentFilePath = fileURLToPath(import.meta.url);
|
|
216
|
-
return join(dirname(currentFilePath), "..", "global-skills");
|
|
217
|
-
} catch {
|
|
218
|
-
// Fallback for non-file URLs (best-effort)
|
|
219
|
-
const currentDir = decodeURIComponent(new URL(".", import.meta.url).pathname);
|
|
220
|
-
return join(currentDir, "..", "global-skills");
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Find all SKILL.md files in a directory
|
|
226
|
-
*/
|
|
227
|
-
async function findSkillFiles(baseDir: string): Promise<string[]> {
|
|
228
|
-
const skillFiles: string[] = [];
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const entries = await readdir(baseDir, { withFileTypes: true });
|
|
232
|
-
|
|
233
|
-
for (const entry of entries) {
|
|
234
|
-
if (entry.isDirectory()) {
|
|
235
|
-
const skillPath = join(baseDir, entry.name, "SKILL.md");
|
|
236
|
-
try {
|
|
237
|
-
const s = await stat(skillPath);
|
|
238
|
-
if (s.isFile()) {
|
|
239
|
-
skillFiles.push(skillPath);
|
|
240
|
-
}
|
|
241
|
-
} catch {
|
|
242
|
-
// SKILL.md doesn't exist in this subdirectory
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
} catch {
|
|
247
|
-
// Directory doesn't exist
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return skillFiles;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Find script files in a skill directory
|
|
255
|
-
*/
|
|
256
|
-
async function findSkillScripts(skillDir: string): Promise<string[]> {
|
|
257
|
-
const scripts: string[] = [];
|
|
258
|
-
const scriptsDir = join(skillDir, "scripts");
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const entries = await readdir(scriptsDir, { withFileTypes: true });
|
|
262
|
-
for (const entry of entries) {
|
|
263
|
-
if (entry.isFile()) {
|
|
264
|
-
scripts.push(entry.name);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
} catch {
|
|
268
|
-
// No scripts directory
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return scripts;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Load a skill from its SKILL.md file
|
|
276
|
-
*/
|
|
277
|
-
async function loadSkill(skillPath: string): Promise<Skill> {
|
|
278
|
-
const content = await readFile(skillPath, "utf-8");
|
|
279
|
-
const { metadata: rawMetadata, body } = parseFrontmatter(content);
|
|
280
|
-
const metadata = validateSkillMetadata(rawMetadata, skillPath);
|
|
281
|
-
const directory = dirname(skillPath);
|
|
282
|
-
const scripts = await findSkillScripts(directory);
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
metadata,
|
|
286
|
-
body,
|
|
287
|
-
path: skillPath,
|
|
288
|
-
directory,
|
|
289
|
-
hasScripts: scripts.length > 0,
|
|
290
|
-
scripts,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Discover all skills in the project and global directories
|
|
296
|
-
*
|
|
297
|
-
* Priority order (first match wins):
|
|
298
|
-
* 1. Project: .opencode/skills/
|
|
299
|
-
* 2. Project: .claude/skills/
|
|
300
|
-
* 3. Project: skills/
|
|
301
|
-
* 4. Global: ~/.config/opencode/skills/
|
|
302
|
-
* 5. Global: ~/.claude/skills/
|
|
303
|
-
*/
|
|
304
|
-
export async function discoverSkills(
|
|
305
|
-
projectDir?: string,
|
|
306
|
-
): Promise<Map<string, Skill>> {
|
|
307
|
-
const dir = projectDir || skillsProjectDirectory;
|
|
308
|
-
|
|
309
|
-
// Return cached skills if available
|
|
310
|
-
if (skillsCache && !projectDir) {
|
|
311
|
-
return skillsCache;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const skills = new Map<string, Skill>();
|
|
315
|
-
const seenNames = new Set<string>();
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Helper to load skills from a directory
|
|
319
|
-
*/
|
|
320
|
-
async function loadSkillsFromDir(skillsDir: string): Promise<void> {
|
|
321
|
-
const skillFiles = await findSkillFiles(skillsDir);
|
|
322
|
-
|
|
323
|
-
for (const skillPath of skillFiles) {
|
|
324
|
-
try {
|
|
325
|
-
const skill = await loadSkill(skillPath);
|
|
326
|
-
|
|
327
|
-
// First definition wins (project overrides global)
|
|
328
|
-
if (!seenNames.has(skill.metadata.name)) {
|
|
329
|
-
skills.set(skill.metadata.name, skill);
|
|
330
|
-
seenNames.add(skill.metadata.name);
|
|
331
|
-
}
|
|
332
|
-
} catch (error) {
|
|
333
|
-
// Log but don't fail on individual skill parse errors
|
|
334
|
-
console.warn(
|
|
335
|
-
`[skills] Failed to load ${skillPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
336
|
-
);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// 1. Check project skill directories first (highest priority)
|
|
342
|
-
for (const relPath of PROJECT_SKILL_DIRECTORIES) {
|
|
343
|
-
await loadSkillsFromDir(join(dir, relPath));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// 2. Check global OpenCode skills directory
|
|
347
|
-
await loadSkillsFromDir(getGlobalSkillsDir());
|
|
348
|
-
|
|
349
|
-
// 3. Check global Claude skills directory (compatibility)
|
|
350
|
-
await loadSkillsFromDir(getClaudeGlobalSkillsDir());
|
|
351
|
-
|
|
352
|
-
// 4. Check bundled package skills (lowest priority)
|
|
353
|
-
await loadSkillsFromDir(getPackageSkillsDir());
|
|
354
|
-
|
|
355
|
-
// Cache for future lookups
|
|
356
|
-
if (!projectDir) {
|
|
357
|
-
skillsCache = skills;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
return skills;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Get a single skill by name
|
|
365
|
-
*/
|
|
366
|
-
export async function getSkill(name: string): Promise<Skill | null> {
|
|
367
|
-
const skills = await discoverSkills();
|
|
368
|
-
return skills.get(name) || null;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/**
|
|
372
|
-
* List all available skills (lightweight refs only)
|
|
373
|
-
*/
|
|
374
|
-
export async function listSkills(): Promise<SkillRef[]> {
|
|
375
|
-
const skills = await discoverSkills();
|
|
376
|
-
return Array.from(skills.values()).map((skill) => ({
|
|
377
|
-
name: skill.metadata.name,
|
|
378
|
-
description: skill.metadata.description,
|
|
379
|
-
path: skill.path,
|
|
380
|
-
hasScripts: skill.hasScripts,
|
|
381
|
-
}));
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
/**
|
|
385
|
-
* Invalidate the skills cache (call when skills may have changed)
|
|
386
|
-
*/
|
|
387
|
-
export function invalidateSkillsCache(): void {
|
|
388
|
-
skillsCache = null;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// =============================================================================
|
|
392
|
-
// Tools
|
|
393
|
-
// =============================================================================
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* List available skills with metadata
|
|
397
|
-
*
|
|
398
|
-
* Returns lightweight skill references for the model to evaluate
|
|
399
|
-
* which skills are relevant to the current task.
|
|
400
|
-
*/
|
|
401
|
-
export const skills_list = tool({
|
|
402
|
-
description: `[DEPRECATED] List all available skills in the project.
|
|
403
|
-
|
|
404
|
-
Skills are specialized instructions that help with specific domains or tasks.
|
|
405
|
-
Use this tool to discover what skills are available, then use skills_use to
|
|
406
|
-
activate a relevant skill.
|
|
407
|
-
|
|
408
|
-
Returns skill names, descriptions, and whether they have executable scripts.`,
|
|
409
|
-
args: {
|
|
410
|
-
tag: tool.schema
|
|
411
|
-
.string()
|
|
412
|
-
.optional()
|
|
413
|
-
.describe("Optional tag to filter skills by"),
|
|
414
|
-
},
|
|
415
|
-
async execute(args) {
|
|
416
|
-
console.warn('[DEPRECATED] skills_list is deprecated. OpenCode now provides native skills support. This tool will be removed in a future version.');
|
|
417
|
-
const skills = await discoverSkills();
|
|
418
|
-
let refs = Array.from(skills.values());
|
|
419
|
-
|
|
420
|
-
// Filter by tag if provided
|
|
421
|
-
if (args.tag) {
|
|
422
|
-
refs = refs.filter((s) => s.metadata.tags?.includes(args.tag as string));
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (refs.length === 0) {
|
|
426
|
-
return args.tag
|
|
427
|
-
? `No skills found with tag '${args.tag}'. Try skills_list without a tag filter.`
|
|
428
|
-
: `No skills found. Skills should be in .opencode/skills/, .claude/skills/, or skills/ directories with SKILL.md files.`;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const formatted = refs
|
|
432
|
-
.map((s) => {
|
|
433
|
-
const scripts = s.hasScripts ? " [has scripts]" : "";
|
|
434
|
-
const tags = s.metadata.tags?.length
|
|
435
|
-
? ` (${s.metadata.tags.join(", ")})`
|
|
436
|
-
: "";
|
|
437
|
-
return `• ${s.metadata.name}${tags}${scripts}\n ${s.metadata.description}`;
|
|
438
|
-
})
|
|
439
|
-
.join("\n\n");
|
|
440
|
-
|
|
441
|
-
return `Found ${refs.length} skill(s):\n\n${formatted}`;
|
|
442
|
-
},
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Load and activate a skill by name
|
|
447
|
-
*
|
|
448
|
-
* Loads the full skill content for injection into context.
|
|
449
|
-
* The skill's instructions become available for the model to follow.
|
|
450
|
-
*/
|
|
451
|
-
export const skills_use = tool({
|
|
452
|
-
description: `[DEPRECATED] Activate a skill by loading its full instructions.
|
|
453
|
-
|
|
454
|
-
After calling this tool, follow the skill's instructions for the current task.
|
|
455
|
-
Skills provide domain-specific guidance and best practices.
|
|
456
|
-
|
|
457
|
-
If the skill has scripts, you can run them with skills_execute.`,
|
|
458
|
-
args: {
|
|
459
|
-
name: tool.schema.string().describe("Name of the skill to activate"),
|
|
460
|
-
include_scripts: tool.schema
|
|
461
|
-
.boolean()
|
|
462
|
-
.optional()
|
|
463
|
-
.describe("Also list available scripts (default: true)"),
|
|
464
|
-
},
|
|
465
|
-
async execute(args) {
|
|
466
|
-
console.warn('[DEPRECATED] skills_use is deprecated. OpenCode now provides native skills support. This tool will be removed in a future version.');
|
|
467
|
-
const skill = await getSkill(args.name);
|
|
468
|
-
|
|
469
|
-
if (!skill) {
|
|
470
|
-
const available = await listSkills();
|
|
471
|
-
const names = available.map((s) => s.name).join(", ");
|
|
472
|
-
return `Skill '${args.name}' not found. Available skills: ${names || "none"}`;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const includeScripts = args.include_scripts !== false;
|
|
476
|
-
let output = `# Skill: ${skill.metadata.name}\n\n`;
|
|
477
|
-
output += `${skill.body}\n`;
|
|
478
|
-
|
|
479
|
-
if (includeScripts && skill.scripts.length > 0) {
|
|
480
|
-
output += `\n---\n\n## Available Scripts\n\n`;
|
|
481
|
-
output += `This skill includes the following scripts in ${skill.directory}/scripts/:\n\n`;
|
|
482
|
-
output += skill.scripts.map((s) => `• ${s}`).join("\n");
|
|
483
|
-
output += `\n\nRun scripts with skills_execute tool.`;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
return output;
|
|
487
|
-
},
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Execute a script from a skill
|
|
492
|
-
*
|
|
493
|
-
* Skills can include helper scripts in their scripts/ directory.
|
|
494
|
-
* This tool runs them with appropriate context.
|
|
495
|
-
*/
|
|
496
|
-
export const skills_execute = tool({
|
|
497
|
-
description: `[DEPRECATED] Execute a script from a skill's scripts/ directory.
|
|
498
|
-
|
|
499
|
-
Some skills include helper scripts for common operations.
|
|
500
|
-
Use skills_use first to see available scripts, then execute them here.
|
|
501
|
-
|
|
502
|
-
Scripts run in the skill's directory with the project directory as an argument.`,
|
|
503
|
-
args: {
|
|
504
|
-
skill: tool.schema.string().describe("Name of the skill"),
|
|
505
|
-
script: tool.schema.string().describe("Name of the script file to execute"),
|
|
506
|
-
args: tool.schema
|
|
507
|
-
.array(tool.schema.string())
|
|
508
|
-
.optional()
|
|
509
|
-
.describe("Additional arguments to pass to the script"),
|
|
510
|
-
},
|
|
511
|
-
async execute(args, ctx) {
|
|
512
|
-
console.warn('[DEPRECATED] skills_execute is deprecated. OpenCode now provides native skills support. This tool will be removed in a future version.');
|
|
513
|
-
const skill = await getSkill(args.skill);
|
|
514
|
-
|
|
515
|
-
if (!skill) {
|
|
516
|
-
return `Skill '${args.skill}' not found.`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
if (!skill.scripts.includes(args.script)) {
|
|
520
|
-
return `Script '${args.script}' not found in skill '${args.skill}'. Available: ${skill.scripts.join(", ") || "none"}`;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const scriptPath = join(skill.directory, "scripts", args.script);
|
|
524
|
-
const scriptArgs = args.args || [];
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
// Execute script using Bun.spawn with timeout
|
|
528
|
-
const TIMEOUT_MS = 60_000; // 60 second timeout
|
|
529
|
-
const proc = Bun.spawn(
|
|
530
|
-
[scriptPath, skillsProjectDirectory, ...scriptArgs],
|
|
531
|
-
{
|
|
532
|
-
cwd: skill.directory,
|
|
533
|
-
stdout: "pipe",
|
|
534
|
-
stderr: "pipe",
|
|
535
|
-
},
|
|
536
|
-
);
|
|
537
|
-
|
|
538
|
-
// Race between script completion and timeout
|
|
539
|
-
const timeoutPromise = new Promise<{ timedOut: true }>((resolve) => {
|
|
540
|
-
setTimeout(() => resolve({ timedOut: true }), TIMEOUT_MS);
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
const resultPromise = (async () => {
|
|
544
|
-
const [stdout, stderr] = await Promise.all([
|
|
545
|
-
new Response(proc.stdout).text(),
|
|
546
|
-
new Response(proc.stderr).text(),
|
|
547
|
-
]);
|
|
548
|
-
const exitCode = await proc.exited;
|
|
549
|
-
return { timedOut: false as const, stdout, stderr, exitCode };
|
|
550
|
-
})();
|
|
551
|
-
|
|
552
|
-
const result = await Promise.race([resultPromise, timeoutPromise]);
|
|
553
|
-
|
|
554
|
-
if (result.timedOut) {
|
|
555
|
-
proc.kill();
|
|
556
|
-
return `Script timed out after ${TIMEOUT_MS / 1000} seconds.`;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const output = result.stdout + result.stderr;
|
|
560
|
-
if (result.exitCode === 0) {
|
|
561
|
-
return output || "Script executed successfully.";
|
|
562
|
-
} else {
|
|
563
|
-
return `Script exited with code ${result.exitCode}:\n${output}`;
|
|
564
|
-
}
|
|
565
|
-
} catch (error) {
|
|
566
|
-
return `Failed to execute script: ${error instanceof Error ? error.message : String(error)}`;
|
|
567
|
-
}
|
|
568
|
-
},
|
|
569
|
-
});
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Read a resource file from a skill directory
|
|
573
|
-
*
|
|
574
|
-
* Skills can include additional resources like examples, templates, or reference docs.
|
|
575
|
-
*/
|
|
576
|
-
export const skills_read = tool({
|
|
577
|
-
description: `[DEPRECATED] Read a resource file from a skill's directory.
|
|
578
|
-
|
|
579
|
-
Skills may include additional files like:
|
|
580
|
-
- examples.md - Example usage
|
|
581
|
-
- reference.md - Reference documentation
|
|
582
|
-
- templates/ - Template files
|
|
583
|
-
|
|
584
|
-
Use this to access supplementary skill resources.`,
|
|
585
|
-
args: {
|
|
586
|
-
skill: tool.schema.string().describe("Name of the skill"),
|
|
587
|
-
file: tool.schema
|
|
588
|
-
.string()
|
|
589
|
-
.describe("Relative path to the file within the skill directory"),
|
|
590
|
-
},
|
|
591
|
-
async execute(args) {
|
|
592
|
-
console.warn('[DEPRECATED] skills_read is deprecated. OpenCode now provides native skills support. This tool will be removed in a future version.');
|
|
593
|
-
const skill = await getSkill(args.skill);
|
|
594
|
-
|
|
595
|
-
if (!skill) {
|
|
596
|
-
return `Skill '${args.skill}' not found.`;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// Security: prevent path traversal (cross-platform)
|
|
600
|
-
// Block absolute paths (Unix / and Windows C:\ or \\)
|
|
601
|
-
if (isAbsolute(args.file)) {
|
|
602
|
-
return "Invalid file path. Use a relative path.";
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Block path traversal attempts
|
|
606
|
-
if (args.file.includes("..")) {
|
|
607
|
-
return "Invalid file path. Path traversal not allowed.";
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
const filePath = resolve(skill.directory, args.file);
|
|
611
|
-
const relativePath = relative(skill.directory, filePath);
|
|
612
|
-
|
|
613
|
-
// Verify resolved path stays within skill directory
|
|
614
|
-
// Check for ".." at start or after separator (handles both Unix and Windows)
|
|
615
|
-
if (
|
|
616
|
-
relativePath === ".." ||
|
|
617
|
-
relativePath.startsWith(".." + sep) ||
|
|
618
|
-
relativePath.startsWith(".." + "/") ||
|
|
619
|
-
relativePath.startsWith(".." + "\\")
|
|
620
|
-
) {
|
|
621
|
-
return "Invalid file path. Must stay within the skill directory.";
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
try {
|
|
625
|
-
const content = await readFile(filePath, "utf-8");
|
|
626
|
-
return content;
|
|
627
|
-
} catch (error) {
|
|
628
|
-
return `Failed to read '${args.file}' from skill '${args.skill}': ${error instanceof Error ? error.message : String(error)}`;
|
|
629
|
-
}
|
|
630
|
-
},
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
// =============================================================================
|
|
634
|
-
// Skill Creation & Maintenance Tools
|
|
635
|
-
// =============================================================================
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Default skills directory for new skills
|
|
639
|
-
*/
|
|
640
|
-
const DEFAULT_SKILLS_DIR = ".opencode/skills";
|
|
641
|
-
|
|
642
|
-
// =============================================================================
|
|
643
|
-
// CSO (Claude Search Optimization) Validation
|
|
644
|
-
// =============================================================================
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* CSO validation warnings for skill metadata
|
|
648
|
-
*/
|
|
649
|
-
export interface CSOValidationWarnings {
|
|
650
|
-
/** Critical warnings (strong indicators of poor discoverability) */
|
|
651
|
-
critical: string[];
|
|
652
|
-
/** Suggestions for improvement */
|
|
653
|
-
suggestions: string[];
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
/**
|
|
657
|
-
* Validate skill metadata against Claude Search Optimization best practices
|
|
658
|
-
*
|
|
659
|
-
* Checks:
|
|
660
|
-
* - 'Use when...' format in description
|
|
661
|
-
* - Description length (warn > 500, max 1024)
|
|
662
|
-
* - Third-person voice (no 'I', 'you')
|
|
663
|
-
* - Name conventions (verb-first, gerunds, hyphens)
|
|
664
|
-
*
|
|
665
|
-
* @returns Warnings object with critical issues and suggestions
|
|
666
|
-
*/
|
|
667
|
-
export function validateCSOCompliance(
|
|
668
|
-
name: string,
|
|
669
|
-
description: string,
|
|
670
|
-
): CSOValidationWarnings {
|
|
671
|
-
const warnings: CSOValidationWarnings = {
|
|
672
|
-
critical: [],
|
|
673
|
-
suggestions: [],
|
|
674
|
-
};
|
|
675
|
-
|
|
676
|
-
// Description: Check for 'Use when...' pattern
|
|
677
|
-
const hasUseWhen = /\buse when\b/i.test(description);
|
|
678
|
-
if (!hasUseWhen) {
|
|
679
|
-
warnings.critical.push(
|
|
680
|
-
"Description should include 'Use when...' to focus on triggering conditions",
|
|
681
|
-
);
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Description: Length checks
|
|
685
|
-
if (description.length > 1024) {
|
|
686
|
-
warnings.critical.push(
|
|
687
|
-
`Description is ${description.length} chars (max 1024) - will be rejected`,
|
|
688
|
-
);
|
|
689
|
-
} else if (description.length > 500) {
|
|
690
|
-
warnings.suggestions.push(
|
|
691
|
-
`Description is ${description.length} chars (aim for <500 for optimal discoverability)`,
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
// Description: Third-person check (no 'I', 'you')
|
|
696
|
-
const firstPersonPattern = /\b(I|I'm|I'll|my|mine|myself)\b/i;
|
|
697
|
-
const secondPersonPattern = /\b(you|you're|you'll|your|yours|yourself)\b/i;
|
|
698
|
-
|
|
699
|
-
if (firstPersonPattern.test(description)) {
|
|
700
|
-
warnings.critical.push(
|
|
701
|
-
"Description uses first-person ('I', 'my') - skills are injected into system prompt, use third-person only",
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
if (secondPersonPattern.test(description)) {
|
|
706
|
-
warnings.critical.push(
|
|
707
|
-
"Description uses second-person ('you', 'your') - use third-person voice (e.g., 'Handles X' not 'You can handle X')",
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// Name: Check for verb-first/gerund patterns
|
|
712
|
-
const nameWords = name.split("-");
|
|
713
|
-
const firstWord = nameWords[0];
|
|
714
|
-
|
|
715
|
-
// Common gerund endings: -ing
|
|
716
|
-
// Common verb forms: -ing, -ize, -ify, -ate
|
|
717
|
-
const isGerund = /ing$/.test(firstWord);
|
|
718
|
-
const isVerbForm = /(ing|ize|ify|ate)$/.test(firstWord);
|
|
719
|
-
|
|
720
|
-
if (!isGerund && !isVerbForm) {
|
|
721
|
-
// Check if it's a common action verb
|
|
722
|
-
const actionVerbs = [
|
|
723
|
-
"test",
|
|
724
|
-
"debug",
|
|
725
|
-
"fix",
|
|
726
|
-
"scan",
|
|
727
|
-
"check",
|
|
728
|
-
"validate",
|
|
729
|
-
"create",
|
|
730
|
-
"build",
|
|
731
|
-
"deploy",
|
|
732
|
-
"run",
|
|
733
|
-
"load",
|
|
734
|
-
"fetch",
|
|
735
|
-
"parse",
|
|
736
|
-
];
|
|
737
|
-
const startsWithAction = actionVerbs.includes(firstWord);
|
|
738
|
-
|
|
739
|
-
if (!startsWithAction) {
|
|
740
|
-
warnings.suggestions.push(
|
|
741
|
-
`Name '${name}' doesn't follow verb-first pattern. Consider gerunds (e.g., 'testing-skills' not 'test-skill') or action verbs for better clarity`,
|
|
742
|
-
);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
// Name: Check length
|
|
747
|
-
if (name.length > 64) {
|
|
748
|
-
warnings.critical.push(
|
|
749
|
-
`Name exceeds 64 character limit (${name.length} chars)`,
|
|
750
|
-
);
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// Name: Validate format (already enforced by schema, but good to document)
|
|
754
|
-
if (!/^[a-z0-9-]+$/.test(name)) {
|
|
755
|
-
warnings.critical.push(
|
|
756
|
-
"Name must be lowercase letters, numbers, and hyphens only",
|
|
757
|
-
);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return warnings;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
/**
|
|
764
|
-
* Format CSO warnings into a readable message for tool output
|
|
765
|
-
*/
|
|
766
|
-
function formatCSOWarnings(warnings: CSOValidationWarnings): string | null {
|
|
767
|
-
if (warnings.critical.length === 0 && warnings.suggestions.length === 0) {
|
|
768
|
-
return null;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
const parts: string[] = [];
|
|
772
|
-
|
|
773
|
-
if (warnings.critical.length > 0) {
|
|
774
|
-
parts.push("**CSO Critical Issues:**");
|
|
775
|
-
for (const warning of warnings.critical) {
|
|
776
|
-
parts.push(` ⚠️ ${warning}`);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (warnings.suggestions.length > 0) {
|
|
781
|
-
parts.push("\n**CSO Suggestions:**");
|
|
782
|
-
for (const suggestion of warnings.suggestions) {
|
|
783
|
-
parts.push(` 💡 ${suggestion}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
parts.push("\n**CSO Guide:**");
|
|
788
|
-
parts.push(
|
|
789
|
-
" • Start description with 'Use when...' (focus on triggering conditions)",
|
|
790
|
-
);
|
|
791
|
-
parts.push(" • Keep description <500 chars (max 1024)");
|
|
792
|
-
parts.push(" • Use third-person voice only (injected into system prompt)");
|
|
793
|
-
parts.push(
|
|
794
|
-
" • Name: verb-first or gerunds (e.g., 'testing-async' not 'async-test')",
|
|
795
|
-
);
|
|
796
|
-
parts.push(
|
|
797
|
-
"\n Example: 'Use when tests have race conditions - replaces arbitrary timeouts with condition polling'",
|
|
798
|
-
);
|
|
799
|
-
|
|
800
|
-
return parts.join("\n");
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
/**
|
|
804
|
-
* Quote a YAML scalar if it contains special characters
|
|
805
|
-
* Uses double quotes and escapes internal quotes/newlines
|
|
806
|
-
*/
|
|
807
|
-
function quoteYamlScalar(value: string): string {
|
|
808
|
-
// Check if quoting is needed (contains :, #, newlines, quotes, or starts with special chars)
|
|
809
|
-
const needsQuoting =
|
|
810
|
-
/[:\n\r#"'`\[\]{}|>&*!?@]/.test(value) ||
|
|
811
|
-
value.startsWith(" ") ||
|
|
812
|
-
value.endsWith(" ") ||
|
|
813
|
-
value === "" ||
|
|
814
|
-
/^[0-9]/.test(value) ||
|
|
815
|
-
["true", "false", "null", "yes", "no", "on", "off"].includes(
|
|
816
|
-
value.toLowerCase(),
|
|
817
|
-
);
|
|
818
|
-
|
|
819
|
-
if (!needsQuoting) {
|
|
820
|
-
return value;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Escape backslashes and double quotes, then wrap in double quotes
|
|
824
|
-
const escaped = value
|
|
825
|
-
.replace(/\\/g, "\\\\")
|
|
826
|
-
.replace(/"/g, '\\"')
|
|
827
|
-
.replace(/\n/g, "\\n");
|
|
828
|
-
return `"${escaped}"`;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
/**
|
|
832
|
-
* Generate SKILL.md content from metadata and body
|
|
833
|
-
*/
|
|
834
|
-
function generateSkillContent(
|
|
835
|
-
name: string,
|
|
836
|
-
description: string,
|
|
837
|
-
body: string,
|
|
838
|
-
options?: { tags?: string[]; tools?: string[] },
|
|
839
|
-
): string {
|
|
840
|
-
const frontmatter: string[] = [
|
|
841
|
-
"---",
|
|
842
|
-
`name: ${quoteYamlScalar(name)}`,
|
|
843
|
-
`description: ${quoteYamlScalar(description)}`,
|
|
844
|
-
];
|
|
845
|
-
|
|
846
|
-
if (options?.tags && options.tags.length > 0) {
|
|
847
|
-
frontmatter.push("tags:");
|
|
848
|
-
for (const tag of options.tags) {
|
|
849
|
-
frontmatter.push(` - ${quoteYamlScalar(tag)}`);
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
if (options?.tools && options.tools.length > 0) {
|
|
854
|
-
frontmatter.push("tools:");
|
|
855
|
-
for (const t of options.tools) {
|
|
856
|
-
frontmatter.push(` - ${quoteYamlScalar(t)}`);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
frontmatter.push("---");
|
|
861
|
-
|
|
862
|
-
return `${frontmatter.join("\n")}\n\n${body}`;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
/**
|
|
866
|
-
* Create a new skill in the project
|
|
867
|
-
*
|
|
868
|
-
* Agents can use this to codify learned patterns, best practices,
|
|
869
|
-
* or domain-specific knowledge into reusable skills.
|
|
870
|
-
*/
|
|
871
|
-
export const skills_create = tool({
|
|
872
|
-
description: `Create a new skill in the project.
|
|
873
|
-
|
|
874
|
-
Use this to codify learned patterns, best practices, or domain knowledge
|
|
875
|
-
into a reusable skill that future agents can discover and use.
|
|
876
|
-
|
|
877
|
-
Skills are stored in .opencode/skills/<name>/SKILL.md by default.
|
|
878
|
-
|
|
879
|
-
Good skills have:
|
|
880
|
-
- Clear, specific descriptions explaining WHEN to use them
|
|
881
|
-
- Actionable instructions with examples
|
|
882
|
-
- Tags for discoverability`,
|
|
883
|
-
args: {
|
|
884
|
-
name: tool.schema
|
|
885
|
-
.string()
|
|
886
|
-
.regex(/^[a-z0-9-]+$/)
|
|
887
|
-
.max(64)
|
|
888
|
-
.describe("Skill name (lowercase, hyphens only, max 64 chars)"),
|
|
889
|
-
description: tool.schema
|
|
890
|
-
.string()
|
|
891
|
-
.max(1024)
|
|
892
|
-
.describe("What the skill does and when to use it (max 1024 chars)"),
|
|
893
|
-
body: tool.schema
|
|
894
|
-
.string()
|
|
895
|
-
.describe("Markdown content with instructions, examples, guidelines"),
|
|
896
|
-
tags: tool.schema
|
|
897
|
-
.array(tool.schema.string())
|
|
898
|
-
.optional()
|
|
899
|
-
.describe("Tags for categorization (e.g., ['testing', 'frontend'])"),
|
|
900
|
-
tools: tool.schema
|
|
901
|
-
.array(tool.schema.string())
|
|
902
|
-
.optional()
|
|
903
|
-
.describe("Tools this skill commonly uses"),
|
|
904
|
-
directory: tool.schema
|
|
905
|
-
.enum([
|
|
906
|
-
".opencode/skills",
|
|
907
|
-
".claude/skills",
|
|
908
|
-
"skills",
|
|
909
|
-
"global",
|
|
910
|
-
"global-claude",
|
|
911
|
-
])
|
|
912
|
-
.optional()
|
|
913
|
-
.describe(
|
|
914
|
-
"Where to create the skill (default: .opencode/skills). Use 'global' for ~/.config/opencode/skills/, 'global-claude' for ~/.claude/skills/",
|
|
915
|
-
),
|
|
916
|
-
},
|
|
917
|
-
async execute(args) {
|
|
918
|
-
// Check if skill already exists
|
|
919
|
-
const existing = await getSkill(args.name);
|
|
920
|
-
if (existing) {
|
|
921
|
-
return `Skill '${args.name}' already exists at ${existing.path}. Use skills_update to modify it.`;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
// Validate CSO compliance (advisory warnings only)
|
|
925
|
-
const csoWarnings = validateCSOCompliance(args.name, args.description);
|
|
926
|
-
|
|
927
|
-
// Determine target directory
|
|
928
|
-
let skillDir: string;
|
|
929
|
-
if (args.directory === "global") {
|
|
930
|
-
skillDir = join(getGlobalSkillsDir(), args.name);
|
|
931
|
-
} else if (args.directory === "global-claude") {
|
|
932
|
-
skillDir = join(getClaudeGlobalSkillsDir(), args.name);
|
|
933
|
-
} else {
|
|
934
|
-
const baseDir = args.directory || DEFAULT_SKILLS_DIR;
|
|
935
|
-
skillDir = join(skillsProjectDirectory, baseDir, args.name);
|
|
936
|
-
}
|
|
937
|
-
const skillPath = join(skillDir, "SKILL.md");
|
|
938
|
-
|
|
939
|
-
try {
|
|
940
|
-
// Create skill directory
|
|
941
|
-
await mkdir(skillDir, { recursive: true });
|
|
942
|
-
|
|
943
|
-
// Generate and write SKILL.md
|
|
944
|
-
const content = generateSkillContent(
|
|
945
|
-
args.name,
|
|
946
|
-
args.description,
|
|
947
|
-
args.body,
|
|
948
|
-
{ tags: args.tags, tools: args.tools },
|
|
949
|
-
);
|
|
950
|
-
|
|
951
|
-
await writeFile(skillPath, content, "utf-8");
|
|
952
|
-
|
|
953
|
-
// Invalidate cache so new skill is discoverable
|
|
954
|
-
invalidateSkillsCache();
|
|
955
|
-
|
|
956
|
-
// Build response with CSO warnings if present
|
|
957
|
-
const response: Record<string, unknown> = {
|
|
958
|
-
success: true,
|
|
959
|
-
skill: args.name,
|
|
960
|
-
path: skillPath,
|
|
961
|
-
message: `Created skill '${args.name}'. It's now discoverable via skills_list.`,
|
|
962
|
-
next_steps: [
|
|
963
|
-
"Test with skills_use to verify instructions are clear",
|
|
964
|
-
"Add examples.md or reference.md for supplementary content",
|
|
965
|
-
"Add scripts/ directory for executable helpers",
|
|
966
|
-
],
|
|
967
|
-
};
|
|
968
|
-
|
|
969
|
-
// Add CSO warnings if any
|
|
970
|
-
const warningsMessage = formatCSOWarnings(csoWarnings);
|
|
971
|
-
if (warningsMessage) {
|
|
972
|
-
response.cso_warnings = warningsMessage;
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
return JSON.stringify(response, null, 2);
|
|
976
|
-
} catch (error) {
|
|
977
|
-
return `Failed to create skill: ${error instanceof Error ? error.message : String(error)}`;
|
|
978
|
-
}
|
|
979
|
-
},
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
/**
|
|
983
|
-
* Update an existing skill
|
|
984
|
-
*
|
|
985
|
-
* Modify a skill's metadata or content based on learned improvements.
|
|
986
|
-
*/
|
|
987
|
-
export const skills_update = tool({
|
|
988
|
-
description: `Update an existing skill's content or metadata.
|
|
989
|
-
|
|
990
|
-
Use this to refine skills based on experience:
|
|
991
|
-
- Clarify instructions that were confusing
|
|
992
|
-
- Add examples from successful usage
|
|
993
|
-
- Update descriptions for better discoverability
|
|
994
|
-
- Add new tags or tool references`,
|
|
995
|
-
args: {
|
|
996
|
-
name: tool.schema.string().describe("Name of the skill to update"),
|
|
997
|
-
description: tool.schema
|
|
998
|
-
.string()
|
|
999
|
-
.max(1024)
|
|
1000
|
-
.optional()
|
|
1001
|
-
.describe("New description (replaces existing)"),
|
|
1002
|
-
content: tool.schema
|
|
1003
|
-
.string()
|
|
1004
|
-
.optional()
|
|
1005
|
-
.describe("New content/body (replaces existing SKILL.md body)"),
|
|
1006
|
-
body: tool.schema
|
|
1007
|
-
.string()
|
|
1008
|
-
.optional()
|
|
1009
|
-
.describe("Alias for content - new body (replaces existing)"),
|
|
1010
|
-
append_body: tool.schema
|
|
1011
|
-
.string()
|
|
1012
|
-
.optional()
|
|
1013
|
-
.describe("Content to append to existing body"),
|
|
1014
|
-
tags: tool.schema
|
|
1015
|
-
.array(tool.schema.string())
|
|
1016
|
-
.optional()
|
|
1017
|
-
.describe("New tags (replaces existing)"),
|
|
1018
|
-
add_tags: tool.schema
|
|
1019
|
-
.array(tool.schema.string())
|
|
1020
|
-
.optional()
|
|
1021
|
-
.describe("Tags to add to existing"),
|
|
1022
|
-
tools: tool.schema
|
|
1023
|
-
.array(tool.schema.string())
|
|
1024
|
-
.optional()
|
|
1025
|
-
.describe("New tools list (replaces existing)"),
|
|
1026
|
-
},
|
|
1027
|
-
async execute(args) {
|
|
1028
|
-
const skill = await getSkill(args.name);
|
|
1029
|
-
if (!skill) {
|
|
1030
|
-
const available = await listSkills();
|
|
1031
|
-
const names = available.map((s) => s.name).join(", ");
|
|
1032
|
-
return `Skill '${args.name}' not found. Available: ${names || "none"}`;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Build updated metadata
|
|
1036
|
-
const newDescription = args.description ?? skill.metadata.description;
|
|
1037
|
-
|
|
1038
|
-
// Handle body updates (content is preferred, body is alias for backwards compat)
|
|
1039
|
-
let newBody = skill.body;
|
|
1040
|
-
const bodyContent = args.content ?? args.body;
|
|
1041
|
-
if (bodyContent) {
|
|
1042
|
-
newBody = bodyContent;
|
|
1043
|
-
} else if (args.append_body) {
|
|
1044
|
-
newBody = `${skill.body}\n\n${args.append_body}`;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Handle tags
|
|
1048
|
-
let newTags = skill.metadata.tags;
|
|
1049
|
-
if (args.tags) {
|
|
1050
|
-
newTags = args.tags;
|
|
1051
|
-
} else if (args.add_tags) {
|
|
1052
|
-
newTags = [...(skill.metadata.tags || []), ...args.add_tags];
|
|
1053
|
-
// Deduplicate
|
|
1054
|
-
newTags = [...new Set(newTags)];
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Handle tools
|
|
1058
|
-
const newTools = args.tools ?? skill.metadata.tools;
|
|
1059
|
-
|
|
1060
|
-
try {
|
|
1061
|
-
// Generate and write updated SKILL.md
|
|
1062
|
-
const content = generateSkillContent(args.name, newDescription, newBody, {
|
|
1063
|
-
tags: newTags,
|
|
1064
|
-
tools: newTools,
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
await writeFile(skill.path, content, "utf-8");
|
|
1068
|
-
|
|
1069
|
-
// Invalidate cache
|
|
1070
|
-
invalidateSkillsCache();
|
|
1071
|
-
|
|
1072
|
-
return JSON.stringify(
|
|
1073
|
-
{
|
|
1074
|
-
success: true,
|
|
1075
|
-
skill: args.name,
|
|
1076
|
-
path: skill.path,
|
|
1077
|
-
updated: {
|
|
1078
|
-
description: args.description ? true : false,
|
|
1079
|
-
content: args.content || args.body || args.append_body ? true : false,
|
|
1080
|
-
tags: args.tags || args.add_tags ? true : false,
|
|
1081
|
-
tools: args.tools ? true : false,
|
|
1082
|
-
},
|
|
1083
|
-
message: `Updated skill '${args.name}'.`,
|
|
1084
|
-
},
|
|
1085
|
-
null,
|
|
1086
|
-
2,
|
|
1087
|
-
);
|
|
1088
|
-
} catch (error) {
|
|
1089
|
-
return `Failed to update skill: ${error instanceof Error ? error.message : String(error)}`;
|
|
1090
|
-
}
|
|
1091
|
-
},
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
/**
|
|
1095
|
-
* Delete a skill from the project
|
|
1096
|
-
*/
|
|
1097
|
-
export const skills_delete = tool({
|
|
1098
|
-
description: `Delete a skill from the project.
|
|
1099
|
-
|
|
1100
|
-
Use sparingly - only delete skills that are:
|
|
1101
|
-
- Obsolete or superseded by better skills
|
|
1102
|
-
- Incorrect or harmful
|
|
1103
|
-
- Duplicates of other skills
|
|
1104
|
-
|
|
1105
|
-
Consider updating instead of deleting when possible.`,
|
|
1106
|
-
args: {
|
|
1107
|
-
name: tool.schema.string().describe("Name of the skill to delete"),
|
|
1108
|
-
confirm: tool.schema.boolean().describe("Must be true to confirm deletion"),
|
|
1109
|
-
},
|
|
1110
|
-
async execute(args) {
|
|
1111
|
-
if (!args.confirm) {
|
|
1112
|
-
return "Deletion not confirmed. Set confirm=true to delete the skill.";
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const skill = await getSkill(args.name);
|
|
1116
|
-
if (!skill) {
|
|
1117
|
-
return `Skill '${args.name}' not found.`;
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
try {
|
|
1121
|
-
// Remove the entire skill directory
|
|
1122
|
-
await rm(skill.directory, { recursive: true, force: true });
|
|
1123
|
-
|
|
1124
|
-
// Invalidate cache
|
|
1125
|
-
invalidateSkillsCache();
|
|
1126
|
-
|
|
1127
|
-
return JSON.stringify(
|
|
1128
|
-
{
|
|
1129
|
-
success: true,
|
|
1130
|
-
skill: args.name,
|
|
1131
|
-
deleted_path: skill.directory,
|
|
1132
|
-
message: `Deleted skill '${args.name}' and its directory.`,
|
|
1133
|
-
},
|
|
1134
|
-
null,
|
|
1135
|
-
2,
|
|
1136
|
-
);
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
return `Failed to delete skill: ${error instanceof Error ? error.message : String(error)}`;
|
|
1139
|
-
}
|
|
1140
|
-
},
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
/**
|
|
1144
|
-
* Add a script to a skill
|
|
1145
|
-
*
|
|
1146
|
-
* Skills can include helper scripts for automation.
|
|
1147
|
-
*/
|
|
1148
|
-
export const skills_add_script = tool({
|
|
1149
|
-
description: `Add a helper script to an existing skill.
|
|
1150
|
-
|
|
1151
|
-
Scripts are stored in the skill's scripts/ directory and can be
|
|
1152
|
-
executed with skills_execute. Use for:
|
|
1153
|
-
- Automation helpers
|
|
1154
|
-
- Validation scripts
|
|
1155
|
-
- Setup/teardown utilities`,
|
|
1156
|
-
args: {
|
|
1157
|
-
skill: tool.schema.string().describe("Name of the skill"),
|
|
1158
|
-
script_name: tool.schema
|
|
1159
|
-
.string()
|
|
1160
|
-
.describe("Script filename (e.g., 'validate.sh', 'setup.py')"),
|
|
1161
|
-
content: tool.schema.string().describe("Script content"),
|
|
1162
|
-
executable: tool.schema
|
|
1163
|
-
.boolean()
|
|
1164
|
-
.default(true)
|
|
1165
|
-
.describe("Make script executable (default: true)"),
|
|
1166
|
-
},
|
|
1167
|
-
async execute(args) {
|
|
1168
|
-
const skill = await getSkill(args.skill);
|
|
1169
|
-
if (!skill) {
|
|
1170
|
-
return `Skill '${args.skill}' not found.`;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
// Security: validate script name (cross-platform)
|
|
1174
|
-
// Block absolute paths, path separators, and traversal
|
|
1175
|
-
if (
|
|
1176
|
-
isAbsolute(args.script_name) ||
|
|
1177
|
-
args.script_name.includes("..") ||
|
|
1178
|
-
args.script_name.includes("/") ||
|
|
1179
|
-
args.script_name.includes("\\") ||
|
|
1180
|
-
basename(args.script_name) !== args.script_name
|
|
1181
|
-
) {
|
|
1182
|
-
return "Invalid script name. Use simple filenames without paths.";
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
const scriptsDir = join(skill.directory, "scripts");
|
|
1186
|
-
const scriptPath = join(scriptsDir, args.script_name);
|
|
1187
|
-
|
|
1188
|
-
try {
|
|
1189
|
-
// Create scripts directory if needed
|
|
1190
|
-
await mkdir(scriptsDir, { recursive: true });
|
|
1191
|
-
|
|
1192
|
-
// Write script
|
|
1193
|
-
await writeFile(scriptPath, args.content, {
|
|
1194
|
-
mode: args.executable ? 0o755 : 0o644,
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
// Invalidate cache to update hasScripts
|
|
1198
|
-
invalidateSkillsCache();
|
|
1199
|
-
|
|
1200
|
-
return JSON.stringify(
|
|
1201
|
-
{
|
|
1202
|
-
success: true,
|
|
1203
|
-
skill: args.skill,
|
|
1204
|
-
script: args.script_name,
|
|
1205
|
-
path: scriptPath,
|
|
1206
|
-
executable: args.executable,
|
|
1207
|
-
message: `Added script '${args.script_name}' to skill '${args.skill}'.`,
|
|
1208
|
-
usage: `Run with: skills_execute(skill: "${args.skill}", script: "${args.script_name}")`,
|
|
1209
|
-
},
|
|
1210
|
-
null,
|
|
1211
|
-
2,
|
|
1212
|
-
);
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
return `Failed to add script: ${error instanceof Error ? error.message : String(error)}`;
|
|
1215
|
-
}
|
|
1216
|
-
},
|
|
1217
|
-
});
|
|
1218
|
-
|
|
1219
|
-
// =============================================================================
|
|
1220
|
-
// Skill Initialization
|
|
1221
|
-
// =============================================================================
|
|
1222
|
-
|
|
1223
|
-
/**
|
|
1224
|
-
* Generate a skill template with TODO placeholders
|
|
1225
|
-
*/
|
|
1226
|
-
function generateSkillTemplate(name: string, description?: string): string {
|
|
1227
|
-
const title = name
|
|
1228
|
-
.split("-")
|
|
1229
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1230
|
-
.join(" ");
|
|
1231
|
-
|
|
1232
|
-
return `---
|
|
1233
|
-
name: ${name}
|
|
1234
|
-
description: ${description || `[TODO: Complete description of what this skill does and WHEN to use it. Be specific about scenarios that trigger this skill.]`}
|
|
1235
|
-
tags:
|
|
1236
|
-
- [TODO: add tags]
|
|
1237
|
-
---
|
|
1238
|
-
|
|
1239
|
-
# ${title}
|
|
1240
|
-
|
|
1241
|
-
## Overview
|
|
1242
|
-
|
|
1243
|
-
[TODO: 1-2 sentences explaining what this skill enables]
|
|
1244
|
-
|
|
1245
|
-
## When to Use This Skill
|
|
1246
|
-
|
|
1247
|
-
[TODO: List specific scenarios when this skill should be activated:
|
|
1248
|
-
- When working on X type of task
|
|
1249
|
-
- When files matching Y pattern are involved
|
|
1250
|
-
- When the user asks about Z topic]
|
|
1251
|
-
|
|
1252
|
-
## Instructions
|
|
1253
|
-
|
|
1254
|
-
[TODO: Add actionable instructions for the agent. Use imperative form:
|
|
1255
|
-
- "Read the configuration file first"
|
|
1256
|
-
- "Check for existing patterns before creating new ones"
|
|
1257
|
-
- "Always validate output before completing"]
|
|
1258
|
-
|
|
1259
|
-
## Examples
|
|
1260
|
-
|
|
1261
|
-
### Example 1: [TODO: Realistic scenario]
|
|
1262
|
-
|
|
1263
|
-
**User**: "[TODO: Example user request]"
|
|
1264
|
-
|
|
1265
|
-
**Process**:
|
|
1266
|
-
1. [TODO: Step-by-step process]
|
|
1267
|
-
2. [TODO: Next step]
|
|
1268
|
-
3. [TODO: Final step]
|
|
1269
|
-
|
|
1270
|
-
## Resources
|
|
1271
|
-
|
|
1272
|
-
This skill may include additional resources:
|
|
1273
|
-
|
|
1274
|
-
### scripts/
|
|
1275
|
-
Executable scripts for automation. Run with \`skills_execute\`.
|
|
1276
|
-
|
|
1277
|
-
### references/
|
|
1278
|
-
Documentation loaded on-demand. Access with \`skills_read\`.
|
|
1279
|
-
|
|
1280
|
-
---
|
|
1281
|
-
*Delete any unused sections and this line when skill is complete.*
|
|
1282
|
-
`;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
/**
|
|
1286
|
-
* Generate a reference template
|
|
1287
|
-
*/
|
|
1288
|
-
function generateReferenceTemplate(skillName: string): string {
|
|
1289
|
-
const title = skillName
|
|
1290
|
-
.split("-")
|
|
1291
|
-
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
1292
|
-
.join(" ");
|
|
1293
|
-
|
|
1294
|
-
return `# Reference Documentation for ${title}
|
|
1295
|
-
|
|
1296
|
-
## Overview
|
|
1297
|
-
|
|
1298
|
-
[TODO: Detailed reference material for this skill]
|
|
1299
|
-
|
|
1300
|
-
## API Reference
|
|
1301
|
-
|
|
1302
|
-
[TODO: If applicable, document APIs, schemas, or interfaces]
|
|
1303
|
-
|
|
1304
|
-
## Detailed Workflows
|
|
1305
|
-
|
|
1306
|
-
[TODO: Complex multi-step workflows that don't fit in SKILL.md]
|
|
1307
|
-
|
|
1308
|
-
## Troubleshooting
|
|
1309
|
-
|
|
1310
|
-
[TODO: Common issues and solutions]
|
|
1311
|
-
`;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
/**
|
|
1315
|
-
* Initialize a new skill with full directory structure
|
|
1316
|
-
*
|
|
1317
|
-
* Creates a skill template following best practices from the
|
|
1318
|
-
* Anthropic Agent Skills specification and community patterns.
|
|
1319
|
-
*/
|
|
1320
|
-
export const skills_init = tool({
|
|
1321
|
-
description: `Initialize a new skill with full directory structure and templates.
|
|
1322
|
-
|
|
1323
|
-
Creates a complete skill directory with:
|
|
1324
|
-
- SKILL.md with frontmatter and TODO placeholders
|
|
1325
|
-
- scripts/ directory for executable helpers
|
|
1326
|
-
- references/ directory for on-demand documentation
|
|
1327
|
-
|
|
1328
|
-
Use this instead of skills_create when you want the full template structure.
|
|
1329
|
-
Perfect for learning to create effective skills.`,
|
|
1330
|
-
args: {
|
|
1331
|
-
name: tool.schema
|
|
1332
|
-
.string()
|
|
1333
|
-
.regex(/^[a-z0-9-]+$/)
|
|
1334
|
-
.max(64)
|
|
1335
|
-
.describe("Skill name (lowercase, hyphens only)"),
|
|
1336
|
-
description: tool.schema
|
|
1337
|
-
.string()
|
|
1338
|
-
.optional()
|
|
1339
|
-
.describe("Initial description (can be a TODO placeholder)"),
|
|
1340
|
-
directory: tool.schema
|
|
1341
|
-
.enum([".opencode/skills", ".claude/skills", "skills", "global"])
|
|
1342
|
-
.optional()
|
|
1343
|
-
.describe("Where to create (default: .opencode/skills)"),
|
|
1344
|
-
include_example_script: tool.schema
|
|
1345
|
-
.boolean()
|
|
1346
|
-
.default(true)
|
|
1347
|
-
.describe("Include example script placeholder (default: true)"),
|
|
1348
|
-
include_reference: tool.schema
|
|
1349
|
-
.boolean()
|
|
1350
|
-
.default(true)
|
|
1351
|
-
.describe("Include reference doc placeholder (default: true)"),
|
|
1352
|
-
},
|
|
1353
|
-
async execute(args) {
|
|
1354
|
-
// Check if skill already exists
|
|
1355
|
-
const existing = await getSkill(args.name);
|
|
1356
|
-
if (existing) {
|
|
1357
|
-
return JSON.stringify(
|
|
1358
|
-
{
|
|
1359
|
-
success: false,
|
|
1360
|
-
error: `Skill '${args.name}' already exists`,
|
|
1361
|
-
existing_path: existing.path,
|
|
1362
|
-
},
|
|
1363
|
-
null,
|
|
1364
|
-
2,
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// Determine target directory
|
|
1369
|
-
let skillDir: string;
|
|
1370
|
-
if (args.directory === "global") {
|
|
1371
|
-
skillDir = join(getGlobalSkillsDir(), args.name);
|
|
1372
|
-
} else {
|
|
1373
|
-
const baseDir = args.directory || DEFAULT_SKILLS_DIR;
|
|
1374
|
-
skillDir = join(skillsProjectDirectory, baseDir, args.name);
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
const createdFiles: string[] = [];
|
|
1378
|
-
|
|
1379
|
-
try {
|
|
1380
|
-
// Create skill directory
|
|
1381
|
-
await mkdir(skillDir, { recursive: true });
|
|
1382
|
-
|
|
1383
|
-
// Create SKILL.md
|
|
1384
|
-
const skillPath = join(skillDir, "SKILL.md");
|
|
1385
|
-
const skillContent = generateSkillTemplate(args.name, args.description);
|
|
1386
|
-
await writeFile(skillPath, skillContent, "utf-8");
|
|
1387
|
-
createdFiles.push("SKILL.md");
|
|
1388
|
-
|
|
1389
|
-
// Create scripts/ directory with example
|
|
1390
|
-
if (args.include_example_script !== false) {
|
|
1391
|
-
const scriptsDir = join(skillDir, "scripts");
|
|
1392
|
-
await mkdir(scriptsDir, { recursive: true });
|
|
1393
|
-
|
|
1394
|
-
const exampleScript = `#!/usr/bin/env bash
|
|
1395
|
-
# Example helper script for ${args.name}
|
|
1396
|
-
#
|
|
1397
|
-
# This is a placeholder. Replace with actual implementation or delete.
|
|
1398
|
-
#
|
|
1399
|
-
# Usage: skills_execute(skill: "${args.name}", script: "example.sh")
|
|
1400
|
-
|
|
1401
|
-
echo "Hello from ${args.name} skill!"
|
|
1402
|
-
echo "Project directory: \$1"
|
|
1403
|
-
|
|
1404
|
-
# TODO: Add actual script logic
|
|
1405
|
-
`;
|
|
1406
|
-
const scriptPath = join(scriptsDir, "example.sh");
|
|
1407
|
-
await writeFile(scriptPath, exampleScript, { mode: 0o755 });
|
|
1408
|
-
createdFiles.push("scripts/example.sh");
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
// Create references/ directory with example
|
|
1412
|
-
if (args.include_reference !== false) {
|
|
1413
|
-
const refsDir = join(skillDir, "references");
|
|
1414
|
-
await mkdir(refsDir, { recursive: true });
|
|
1415
|
-
|
|
1416
|
-
const refContent = generateReferenceTemplate(args.name);
|
|
1417
|
-
const refPath = join(refsDir, "guide.md");
|
|
1418
|
-
await writeFile(refPath, refContent, "utf-8");
|
|
1419
|
-
createdFiles.push("references/guide.md");
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// Invalidate cache
|
|
1423
|
-
invalidateSkillsCache();
|
|
1424
|
-
|
|
1425
|
-
return JSON.stringify(
|
|
1426
|
-
{
|
|
1427
|
-
success: true,
|
|
1428
|
-
skill: args.name,
|
|
1429
|
-
path: skillDir,
|
|
1430
|
-
created_files: createdFiles,
|
|
1431
|
-
next_steps: [
|
|
1432
|
-
"Edit SKILL.md to complete TODO placeholders",
|
|
1433
|
-
"Update the description in frontmatter",
|
|
1434
|
-
"Add specific 'When to Use' scenarios",
|
|
1435
|
-
"Add actionable instructions",
|
|
1436
|
-
"Delete unused sections and placeholder files",
|
|
1437
|
-
"Test with skills_use to verify it works",
|
|
1438
|
-
],
|
|
1439
|
-
tips: [
|
|
1440
|
-
"Good descriptions explain WHEN to use, not just WHAT it does",
|
|
1441
|
-
"Instructions should be imperative: 'Do X' not 'You should do X'",
|
|
1442
|
-
"Include realistic examples with user requests",
|
|
1443
|
-
"Progressive disclosure: keep SKILL.md lean, use references/ for details",
|
|
1444
|
-
],
|
|
1445
|
-
},
|
|
1446
|
-
null,
|
|
1447
|
-
2,
|
|
1448
|
-
);
|
|
1449
|
-
} catch (error) {
|
|
1450
|
-
return JSON.stringify(
|
|
1451
|
-
{
|
|
1452
|
-
success: false,
|
|
1453
|
-
error: `Failed to initialize skill: ${error instanceof Error ? error.message : String(error)}`,
|
|
1454
|
-
partial_files: createdFiles,
|
|
1455
|
-
},
|
|
1456
|
-
null,
|
|
1457
|
-
2,
|
|
1458
|
-
);
|
|
1459
|
-
}
|
|
1460
|
-
},
|
|
1461
|
-
});
|
|
1462
|
-
|
|
1463
|
-
// =============================================================================
|
|
1464
|
-
// Tool Registry
|
|
1465
|
-
// =============================================================================
|
|
1466
|
-
|
|
1467
|
-
/**
|
|
1468
|
-
* All skills tools for plugin registration
|
|
1469
|
-
*/
|
|
1470
|
-
export const skillsTools = {
|
|
1471
|
-
skills_list,
|
|
1472
|
-
skills_use,
|
|
1473
|
-
skills_execute,
|
|
1474
|
-
skills_read,
|
|
1475
|
-
skills_create,
|
|
1476
|
-
skills_update,
|
|
1477
|
-
skills_delete,
|
|
1478
|
-
skills_add_script,
|
|
1479
|
-
skills_init,
|
|
1480
|
-
};
|
|
1481
|
-
|
|
1482
|
-
// =============================================================================
|
|
1483
|
-
// Swarm Integration
|
|
1484
|
-
// =============================================================================
|
|
1485
|
-
|
|
1486
|
-
/**
|
|
1487
|
-
* Get skill context for swarm task decomposition
|
|
1488
|
-
*
|
|
1489
|
-
* Returns a summary of available skills that can be referenced
|
|
1490
|
-
* in subtask prompts for specialized handling.
|
|
1491
|
-
*/
|
|
1492
|
-
export async function getSkillsContextForSwarm(): Promise<string> {
|
|
1493
|
-
const skills = await listSkills();
|
|
1494
|
-
|
|
1495
|
-
if (skills.length === 0) {
|
|
1496
|
-
return "";
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
const skillsList = skills
|
|
1500
|
-
.map((s) => `- ${s.name}: ${s.description}`)
|
|
1501
|
-
.join("\n");
|
|
1502
|
-
|
|
1503
|
-
return `
|
|
1504
|
-
## Available Skills
|
|
1505
|
-
|
|
1506
|
-
The following skills are available in this project and can be activated
|
|
1507
|
-
with \`skills_use\` when relevant to subtasks:
|
|
1508
|
-
|
|
1509
|
-
${skillsList}
|
|
1510
|
-
|
|
1511
|
-
Consider which skills may be helpful for each subtask.`;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
/**
|
|
1515
|
-
* Find skills relevant to a task description
|
|
1516
|
-
*
|
|
1517
|
-
* Simple keyword matching to suggest skills for a task.
|
|
1518
|
-
* Returns skill names that may be relevant.
|
|
1519
|
-
*/
|
|
1520
|
-
export async function findRelevantSkills(
|
|
1521
|
-
taskDescription: string,
|
|
1522
|
-
): Promise<string[]> {
|
|
1523
|
-
const skills = await discoverSkills();
|
|
1524
|
-
const relevant: string[] = [];
|
|
1525
|
-
const taskLower = taskDescription.toLowerCase();
|
|
1526
|
-
|
|
1527
|
-
for (const [name, skill] of skills) {
|
|
1528
|
-
const descLower = skill.metadata.description.toLowerCase();
|
|
1529
|
-
|
|
1530
|
-
// Check if task matches skill description keywords
|
|
1531
|
-
const keywords = descLower.split(/\s+/).filter((w) => w.length > 4);
|
|
1532
|
-
const taskWords = taskLower.split(/\s+/);
|
|
1533
|
-
|
|
1534
|
-
const matches = keywords.filter((k) =>
|
|
1535
|
-
taskWords.some((w) => w.includes(k) || k.includes(w)),
|
|
1536
|
-
);
|
|
1537
|
-
|
|
1538
|
-
// Also check tags
|
|
1539
|
-
const tagMatches =
|
|
1540
|
-
skill.metadata.tags?.filter((t) => taskLower.includes(t.toLowerCase())) ||
|
|
1541
|
-
[];
|
|
1542
|
-
|
|
1543
|
-
if (matches.length >= 2 || tagMatches.length > 0) {
|
|
1544
|
-
relevant.push(name);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
|
|
1548
|
-
return relevant;
|
|
1549
|
-
}
|