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.
Files changed (205) hide show
  1. package/bin/swarm.serve.test.ts +6 -4
  2. package/bin/swarm.ts +16 -10
  3. package/dist/compaction-prompt-scoring.js +139 -0
  4. package/dist/eval-capture.js +12811 -0
  5. package/dist/hive.d.ts.map +1 -1
  6. package/dist/index.js +7644 -62599
  7. package/dist/plugin.js +23766 -78721
  8. package/dist/swarm-orchestrate.d.ts.map +1 -1
  9. package/dist/swarm-prompts.d.ts.map +1 -1
  10. package/dist/swarm-review.d.ts.map +1 -1
  11. package/package.json +17 -5
  12. package/.changeset/swarm-insights-data-layer.md +0 -63
  13. package/.hive/analysis/eval-failure-analysis-2025-12-25.md +0 -331
  14. package/.hive/analysis/session-data-quality-audit.md +0 -320
  15. package/.hive/eval-results.json +0 -483
  16. package/.hive/issues.jsonl +0 -138
  17. package/.hive/memories.jsonl +0 -729
  18. package/.opencode/eval-history.jsonl +0 -327
  19. package/.turbo/turbo-build.log +0 -9
  20. package/CHANGELOG.md +0 -2286
  21. package/SCORER-ANALYSIS.md +0 -598
  22. package/docs/analysis/subagent-coordination-patterns.md +0 -902
  23. package/docs/analysis-socratic-planner-pattern.md +0 -504
  24. package/docs/planning/ADR-001-monorepo-structure.md +0 -171
  25. package/docs/planning/ADR-002-package-extraction.md +0 -393
  26. package/docs/planning/ADR-003-performance-improvements.md +0 -451
  27. package/docs/planning/ADR-004-message-queue-features.md +0 -187
  28. package/docs/planning/ADR-005-devtools-observability.md +0 -202
  29. package/docs/planning/ADR-007-swarm-enhancements-worktree-review.md +0 -168
  30. package/docs/planning/ADR-008-worker-handoff-protocol.md +0 -293
  31. package/docs/planning/ADR-009-oh-my-opencode-patterns.md +0 -353
  32. package/docs/planning/ADR-010-cass-inhousing.md +0 -1215
  33. package/docs/planning/ROADMAP.md +0 -368
  34. package/docs/semantic-memory-cli-syntax.md +0 -123
  35. package/docs/swarm-mail-architecture.md +0 -1147
  36. package/docs/testing/context-recovery-test.md +0 -470
  37. package/evals/ARCHITECTURE.md +0 -1189
  38. package/evals/README.md +0 -768
  39. package/evals/compaction-prompt.eval.ts +0 -149
  40. package/evals/compaction-resumption.eval.ts +0 -289
  41. package/evals/coordinator-behavior.eval.ts +0 -307
  42. package/evals/coordinator-session.eval.ts +0 -154
  43. package/evals/evalite.config.ts.bak +0 -15
  44. package/evals/example.eval.ts +0 -31
  45. package/evals/fixtures/cass-baseline.ts +0 -217
  46. package/evals/fixtures/compaction-cases.ts +0 -350
  47. package/evals/fixtures/compaction-prompt-cases.ts +0 -311
  48. package/evals/fixtures/coordinator-sessions.ts +0 -328
  49. package/evals/fixtures/decomposition-cases.ts +0 -105
  50. package/evals/lib/compaction-loader.test.ts +0 -248
  51. package/evals/lib/compaction-loader.ts +0 -320
  52. package/evals/lib/data-loader.evalite-test.ts +0 -289
  53. package/evals/lib/data-loader.test.ts +0 -345
  54. package/evals/lib/data-loader.ts +0 -281
  55. package/evals/lib/llm.ts +0 -115
  56. package/evals/scorers/compaction-prompt-scorers.ts +0 -145
  57. package/evals/scorers/compaction-scorers.ts +0 -305
  58. package/evals/scorers/coordinator-discipline.evalite-test.ts +0 -539
  59. package/evals/scorers/coordinator-discipline.ts +0 -325
  60. package/evals/scorers/index.test.ts +0 -146
  61. package/evals/scorers/index.ts +0 -328
  62. package/evals/scorers/outcome-scorers.evalite-test.ts +0 -27
  63. package/evals/scorers/outcome-scorers.ts +0 -349
  64. package/evals/swarm-decomposition.eval.ts +0 -121
  65. package/examples/commands/swarm.md +0 -745
  66. package/examples/plugin-wrapper-template.ts +0 -2515
  67. package/examples/skills/hive-workflow/SKILL.md +0 -212
  68. package/examples/skills/skill-creator/SKILL.md +0 -223
  69. package/examples/skills/swarm-coordination/SKILL.md +0 -292
  70. package/global-skills/cli-builder/SKILL.md +0 -344
  71. package/global-skills/cli-builder/references/advanced-patterns.md +0 -244
  72. package/global-skills/learning-systems/SKILL.md +0 -644
  73. package/global-skills/skill-creator/LICENSE.txt +0 -202
  74. package/global-skills/skill-creator/SKILL.md +0 -352
  75. package/global-skills/skill-creator/references/output-patterns.md +0 -82
  76. package/global-skills/skill-creator/references/workflows.md +0 -28
  77. package/global-skills/swarm-coordination/SKILL.md +0 -995
  78. package/global-skills/swarm-coordination/references/coordinator-patterns.md +0 -235
  79. package/global-skills/swarm-coordination/references/strategies.md +0 -138
  80. package/global-skills/system-design/SKILL.md +0 -213
  81. package/global-skills/testing-patterns/SKILL.md +0 -430
  82. package/global-skills/testing-patterns/references/dependency-breaking-catalog.md +0 -586
  83. package/opencode-swarm-plugin-0.30.7.tgz +0 -0
  84. package/opencode-swarm-plugin-0.31.0.tgz +0 -0
  85. package/scripts/cleanup-test-memories.ts +0 -346
  86. package/scripts/init-skill.ts +0 -222
  87. package/scripts/migrate-unknown-sessions.ts +0 -349
  88. package/scripts/validate-skill.ts +0 -204
  89. package/src/agent-mail.ts +0 -1724
  90. package/src/anti-patterns.test.ts +0 -1167
  91. package/src/anti-patterns.ts +0 -448
  92. package/src/compaction-capture.integration.test.ts +0 -257
  93. package/src/compaction-hook.test.ts +0 -838
  94. package/src/compaction-hook.ts +0 -1204
  95. package/src/compaction-observability.integration.test.ts +0 -139
  96. package/src/compaction-observability.test.ts +0 -187
  97. package/src/compaction-observability.ts +0 -324
  98. package/src/compaction-prompt-scorers.test.ts +0 -475
  99. package/src/compaction-prompt-scoring.ts +0 -300
  100. package/src/contributor-tools.test.ts +0 -133
  101. package/src/contributor-tools.ts +0 -201
  102. package/src/dashboard.test.ts +0 -611
  103. package/src/dashboard.ts +0 -462
  104. package/src/error-enrichment.test.ts +0 -403
  105. package/src/error-enrichment.ts +0 -219
  106. package/src/eval-capture.test.ts +0 -1015
  107. package/src/eval-capture.ts +0 -929
  108. package/src/eval-gates.test.ts +0 -306
  109. package/src/eval-gates.ts +0 -218
  110. package/src/eval-history.test.ts +0 -508
  111. package/src/eval-history.ts +0 -214
  112. package/src/eval-learning.test.ts +0 -378
  113. package/src/eval-learning.ts +0 -360
  114. package/src/eval-runner.test.ts +0 -223
  115. package/src/eval-runner.ts +0 -402
  116. package/src/export-tools.test.ts +0 -476
  117. package/src/export-tools.ts +0 -257
  118. package/src/hive.integration.test.ts +0 -2241
  119. package/src/hive.ts +0 -1628
  120. package/src/index.ts +0 -940
  121. package/src/learning.integration.test.ts +0 -1815
  122. package/src/learning.ts +0 -1079
  123. package/src/logger.test.ts +0 -189
  124. package/src/logger.ts +0 -135
  125. package/src/mandate-promotion.test.ts +0 -473
  126. package/src/mandate-promotion.ts +0 -239
  127. package/src/mandate-storage.integration.test.ts +0 -601
  128. package/src/mandate-storage.test.ts +0 -578
  129. package/src/mandate-storage.ts +0 -794
  130. package/src/mandates.ts +0 -540
  131. package/src/memory-tools.test.ts +0 -195
  132. package/src/memory-tools.ts +0 -344
  133. package/src/memory.integration.test.ts +0 -334
  134. package/src/memory.test.ts +0 -158
  135. package/src/memory.ts +0 -527
  136. package/src/model-selection.test.ts +0 -188
  137. package/src/model-selection.ts +0 -68
  138. package/src/observability-tools.test.ts +0 -359
  139. package/src/observability-tools.ts +0 -871
  140. package/src/output-guardrails.test.ts +0 -438
  141. package/src/output-guardrails.ts +0 -381
  142. package/src/pattern-maturity.test.ts +0 -1160
  143. package/src/pattern-maturity.ts +0 -525
  144. package/src/planning-guardrails.test.ts +0 -491
  145. package/src/planning-guardrails.ts +0 -438
  146. package/src/plugin.ts +0 -23
  147. package/src/post-compaction-tracker.test.ts +0 -251
  148. package/src/post-compaction-tracker.ts +0 -237
  149. package/src/query-tools.test.ts +0 -636
  150. package/src/query-tools.ts +0 -324
  151. package/src/rate-limiter.integration.test.ts +0 -466
  152. package/src/rate-limiter.ts +0 -774
  153. package/src/replay-tools.test.ts +0 -496
  154. package/src/replay-tools.ts +0 -240
  155. package/src/repo-crawl.integration.test.ts +0 -441
  156. package/src/repo-crawl.ts +0 -610
  157. package/src/schemas/cell-events.test.ts +0 -347
  158. package/src/schemas/cell-events.ts +0 -807
  159. package/src/schemas/cell.ts +0 -257
  160. package/src/schemas/evaluation.ts +0 -166
  161. package/src/schemas/index.test.ts +0 -199
  162. package/src/schemas/index.ts +0 -286
  163. package/src/schemas/mandate.ts +0 -232
  164. package/src/schemas/swarm-context.ts +0 -115
  165. package/src/schemas/task.ts +0 -161
  166. package/src/schemas/worker-handoff.test.ts +0 -302
  167. package/src/schemas/worker-handoff.ts +0 -131
  168. package/src/sessions/agent-discovery.test.ts +0 -137
  169. package/src/sessions/agent-discovery.ts +0 -112
  170. package/src/sessions/index.ts +0 -15
  171. package/src/skills.integration.test.ts +0 -1192
  172. package/src/skills.test.ts +0 -643
  173. package/src/skills.ts +0 -1549
  174. package/src/storage.integration.test.ts +0 -341
  175. package/src/storage.ts +0 -884
  176. package/src/structured.integration.test.ts +0 -817
  177. package/src/structured.test.ts +0 -1046
  178. package/src/structured.ts +0 -762
  179. package/src/swarm-decompose.test.ts +0 -188
  180. package/src/swarm-decompose.ts +0 -1302
  181. package/src/swarm-deferred.integration.test.ts +0 -157
  182. package/src/swarm-deferred.test.ts +0 -38
  183. package/src/swarm-insights.test.ts +0 -214
  184. package/src/swarm-insights.ts +0 -459
  185. package/src/swarm-mail.integration.test.ts +0 -970
  186. package/src/swarm-mail.ts +0 -739
  187. package/src/swarm-orchestrate.integration.test.ts +0 -282
  188. package/src/swarm-orchestrate.test.ts +0 -548
  189. package/src/swarm-orchestrate.ts +0 -3084
  190. package/src/swarm-prompts.test.ts +0 -1270
  191. package/src/swarm-prompts.ts +0 -2077
  192. package/src/swarm-research.integration.test.ts +0 -701
  193. package/src/swarm-research.test.ts +0 -698
  194. package/src/swarm-research.ts +0 -472
  195. package/src/swarm-review.integration.test.ts +0 -285
  196. package/src/swarm-review.test.ts +0 -879
  197. package/src/swarm-review.ts +0 -709
  198. package/src/swarm-strategies.ts +0 -407
  199. package/src/swarm-worktree.test.ts +0 -501
  200. package/src/swarm-worktree.ts +0 -575
  201. package/src/swarm.integration.test.ts +0 -2377
  202. package/src/swarm.ts +0 -38
  203. package/src/tool-adapter.integration.test.ts +0 -1221
  204. package/src/tool-availability.ts +0 -461
  205. 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
- }