opencode-swarm-plugin 0.44.0 → 0.44.2

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