opencode-swarm-plugin 0.12.19 → 0.12.22

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.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Skill Initializer - Creates a new skill from template
4
+ *
5
+ * Usage:
6
+ * bun scripts/init-skill.ts <skill-name> [--path <path>] [--global]
7
+ *
8
+ * Examples:
9
+ * bun scripts/init-skill.ts my-skill
10
+ * bun scripts/init-skill.ts my-skill --path .claude/skills
11
+ * bun scripts/init-skill.ts my-skill --global
12
+ */
13
+
14
+ import { mkdir, writeFile } from "fs/promises";
15
+ import { existsSync } from "fs";
16
+ import { join } from "path";
17
+ import { parseArgs } from "util";
18
+
19
+ const SKILL_TEMPLATE = (name: string, title: string) => `---
20
+ name: ${name}
21
+ description: [TODO: Complete description of what this skill does and WHEN to use it. Be specific about scenarios that trigger this skill.]
22
+ tags:
23
+ - [TODO: add tags]
24
+ ---
25
+
26
+ # ${title}
27
+
28
+ ## Overview
29
+
30
+ [TODO: 1-2 sentences explaining what this skill enables]
31
+
32
+ ## When to Use This Skill
33
+
34
+ [TODO: List specific scenarios when this skill should be activated:
35
+ - When working on X type of task
36
+ - When files matching Y pattern are involved
37
+ - When the user asks about Z topic]
38
+
39
+ ## Instructions
40
+
41
+ [TODO: Add actionable instructions for the agent. Use imperative form:
42
+ - "Read the configuration file first"
43
+ - "Check for existing patterns before creating new ones"
44
+ - "Always validate output before completing"]
45
+
46
+ ## Examples
47
+
48
+ ### Example 1: [TODO: Realistic scenario]
49
+
50
+ **User**: "[TODO: Example user request]"
51
+
52
+ **Process**:
53
+ 1. [TODO: Step-by-step process]
54
+ 2. [TODO: Next step]
55
+ 3. [TODO: Final step]
56
+
57
+ ## Resources
58
+
59
+ This skill may include additional resources:
60
+
61
+ ### scripts/
62
+ Executable scripts for automation. Run with \`skills_execute\`.
63
+
64
+ ### references/
65
+ Documentation loaded on-demand. Access with \`skills_read\`.
66
+
67
+ ---
68
+ *Delete any unused sections and this line when skill is complete.*
69
+ `;
70
+
71
+ const EXAMPLE_SCRIPT = (name: string) => `#!/usr/bin/env bash
72
+ # Example helper script for ${name}
73
+ #
74
+ # This is a placeholder. Replace with actual implementation or delete.
75
+ #
76
+ # Usage: skills_execute(skill: "${name}", script: "example.sh")
77
+
78
+ echo "Hello from ${name} skill!"
79
+ echo "Project directory: $1"
80
+
81
+ # TODO: Add actual script logic
82
+ `;
83
+
84
+ const REFERENCE_TEMPLATE = (title: string) => `# Reference Documentation for ${title}
85
+
86
+ ## Overview
87
+
88
+ [TODO: Detailed reference material for this skill]
89
+
90
+ ## API Reference
91
+
92
+ [TODO: If applicable, document APIs, schemas, or interfaces]
93
+
94
+ ## Detailed Workflows
95
+
96
+ [TODO: Complex multi-step workflows that don't fit in SKILL.md]
97
+
98
+ ## Troubleshooting
99
+
100
+ [TODO: Common issues and solutions]
101
+ `;
102
+
103
+ function titleCase(name: string): string {
104
+ return name
105
+ .split("-")
106
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
107
+ .join(" ");
108
+ }
109
+
110
+ async function initSkill(
111
+ name: string,
112
+ basePath: string,
113
+ isGlobal: boolean
114
+ ): Promise<void> {
115
+ // Validate name
116
+ if (!/^[a-z0-9-]+$/.test(name)) {
117
+ console.error("❌ Error: Skill name must be lowercase with hyphens only");
118
+ process.exit(1);
119
+ }
120
+
121
+ if (name.length > 64) {
122
+ console.error("❌ Error: Skill name must be 64 characters or less");
123
+ process.exit(1);
124
+ }
125
+
126
+ // Determine target directory
127
+ let skillDir: string;
128
+ if (isGlobal) {
129
+ const home = process.env.HOME || process.env.USERPROFILE || "~";
130
+ skillDir = join(home, ".config", "opencode", "skills", name);
131
+ } else {
132
+ skillDir = join(basePath, name);
133
+ }
134
+
135
+ // Check if exists
136
+ if (existsSync(skillDir)) {
137
+ console.error(`❌ Error: Skill directory already exists: ${skillDir}`);
138
+ process.exit(1);
139
+ }
140
+
141
+ const title = titleCase(name);
142
+ const createdFiles: string[] = [];
143
+
144
+ try {
145
+ // Create skill directory
146
+ await mkdir(skillDir, { recursive: true });
147
+ console.log(`✅ Created skill directory: ${skillDir}`);
148
+
149
+ // Create SKILL.md
150
+ const skillPath = join(skillDir, "SKILL.md");
151
+ await writeFile(skillPath, SKILL_TEMPLATE(name, title));
152
+ createdFiles.push("SKILL.md");
153
+ console.log("✅ Created SKILL.md");
154
+
155
+ // Create scripts/ directory with example
156
+ const scriptsDir = join(skillDir, "scripts");
157
+ await mkdir(scriptsDir, { recursive: true });
158
+ const scriptPath = join(scriptsDir, "example.sh");
159
+ await writeFile(scriptPath, EXAMPLE_SCRIPT(name), { mode: 0o755 });
160
+ createdFiles.push("scripts/example.sh");
161
+ console.log("✅ Created scripts/example.sh");
162
+
163
+ // Create references/ directory with example
164
+ const refsDir = join(skillDir, "references");
165
+ await mkdir(refsDir, { recursive: true });
166
+ const refPath = join(refsDir, "guide.md");
167
+ await writeFile(refPath, REFERENCE_TEMPLATE(title));
168
+ createdFiles.push("references/guide.md");
169
+ console.log("✅ Created references/guide.md");
170
+
171
+ console.log(`\n✅ Skill '${name}' initialized successfully at ${skillDir}`);
172
+ console.log("\nNext steps:");
173
+ console.log(" 1. Edit SKILL.md to complete TODO placeholders");
174
+ console.log(" 2. Update the description in frontmatter");
175
+ console.log(" 3. Add specific 'When to Use' scenarios");
176
+ console.log(" 4. Delete unused sections and placeholder files");
177
+ console.log(" 5. Test with skills_use to verify it works");
178
+ } catch (error) {
179
+ console.error(
180
+ `❌ Error: ${error instanceof Error ? error.message : String(error)}`
181
+ );
182
+ process.exit(1);
183
+ }
184
+ }
185
+
186
+ // Parse arguments
187
+ const { values, positionals } = parseArgs({
188
+ args: process.argv.slice(2),
189
+ options: {
190
+ path: { type: "string", default: ".opencode/skills" },
191
+ global: { type: "boolean", default: false },
192
+ help: { type: "boolean", short: "h", default: false },
193
+ },
194
+ allowPositionals: true,
195
+ });
196
+
197
+ if (values.help || positionals.length === 0) {
198
+ console.log(`
199
+ Skill Initializer - Creates a new skill from template
200
+
201
+ Usage:
202
+ bun scripts/init-skill.ts <skill-name> [options]
203
+
204
+ Options:
205
+ --path <path> Directory to create skill in (default: .opencode/skills)
206
+ --global Create in global ~/.config/opencode/skills directory
207
+ -h, --help Show this help message
208
+
209
+ Examples:
210
+ bun scripts/init-skill.ts my-skill
211
+ bun scripts/init-skill.ts my-skill --path .claude/skills
212
+ bun scripts/init-skill.ts my-skill --global
213
+
214
+ Skill name requirements:
215
+ - Lowercase letters, digits, and hyphens only
216
+ - Max 64 characters
217
+ `);
218
+ process.exit(values.help ? 0 : 1);
219
+ }
220
+
221
+ const skillName = positionals[0];
222
+ await initSkill(skillName, values.path!, values.global!);
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Skill Validator - Validates skill structure and content
4
+ *
5
+ * Usage:
6
+ * bun scripts/validate-skill.ts <path/to/skill>
7
+ *
8
+ * Examples:
9
+ * bun scripts/validate-skill.ts .opencode/skills/my-skill
10
+ * bun scripts/validate-skill.ts global-skills/debugging
11
+ */
12
+
13
+ import { readFile, readdir } from "fs/promises";
14
+ import { existsSync } from "fs";
15
+ import { join, basename } from "path";
16
+ import { parseFrontmatter } from "../src/skills.js";
17
+
18
+ interface ValidationResult {
19
+ valid: boolean;
20
+ errors: string[];
21
+ warnings: string[];
22
+ info: string[];
23
+ }
24
+
25
+ async function validateSkill(skillPath: string): Promise<ValidationResult> {
26
+ const result: ValidationResult = {
27
+ valid: true,
28
+ errors: [],
29
+ warnings: [],
30
+ info: [],
31
+ };
32
+
33
+ const skillName = basename(skillPath);
34
+
35
+ // Check directory exists
36
+ if (!existsSync(skillPath)) {
37
+ result.errors.push(`Skill directory does not exist: ${skillPath}`);
38
+ result.valid = false;
39
+ return result;
40
+ }
41
+
42
+ // Check SKILL.md exists
43
+ const skillMdPath = join(skillPath, "SKILL.md");
44
+ if (!existsSync(skillMdPath)) {
45
+ result.errors.push("Missing required SKILL.md file");
46
+ result.valid = false;
47
+ return result;
48
+ }
49
+
50
+ // Read and parse SKILL.md
51
+ const content = await readFile(skillMdPath, "utf-8");
52
+
53
+ // Check frontmatter
54
+ if (!content.startsWith("---\n") && !content.startsWith("---\r\n")) {
55
+ result.errors.push("SKILL.md must start with YAML frontmatter (---)");
56
+ result.valid = false;
57
+ return result;
58
+ }
59
+
60
+ const { metadata: frontmatter, body } = parseFrontmatter(content);
61
+ if (Object.keys(frontmatter).length === 0) {
62
+ result.errors.push("Invalid YAML frontmatter format");
63
+ result.valid = false;
64
+ return result;
65
+ }
66
+
67
+ // Validate required fields
68
+ if (!frontmatter.name) {
69
+ result.errors.push("Missing required 'name' field in frontmatter");
70
+ result.valid = false;
71
+ } else if (frontmatter.name !== skillName) {
72
+ result.warnings.push(
73
+ `Frontmatter name '${frontmatter.name}' doesn't match directory name '${skillName}'`,
74
+ );
75
+ }
76
+
77
+ if (!frontmatter.description) {
78
+ result.errors.push("Missing required 'description' field in frontmatter");
79
+ result.valid = false;
80
+ } else {
81
+ const desc = String(frontmatter.description);
82
+ if (desc.includes("[TODO")) {
83
+ result.warnings.push("Description contains TODO placeholder");
84
+ }
85
+ if (desc.length < 20) {
86
+ result.warnings.push("Description is very short (< 20 chars)");
87
+ }
88
+ if (desc.length > 500) {
89
+ result.warnings.push("Description is very long (> 500 chars)");
90
+ }
91
+ }
92
+
93
+ // Check for TODO placeholders in body
94
+ const todoCount = (body.match(/\[TODO/g) || []).length;
95
+ if (todoCount > 0) {
96
+ result.warnings.push(`Found ${todoCount} TODO placeholder(s) in body`);
97
+ }
98
+
99
+ // Check body length (body is already extracted by parseFrontmatter)
100
+ const lineCount = body.split("\n").length;
101
+ if (lineCount > 500) {
102
+ result.warnings.push(
103
+ `SKILL.md body is ${lineCount} lines (recommended < 500)`,
104
+ );
105
+ }
106
+
107
+ // Check for optional directories
108
+ const scriptsDir = join(skillPath, "scripts");
109
+ const refsDir = join(skillPath, "references");
110
+ const assetsDir = join(skillPath, "assets");
111
+
112
+ if (existsSync(scriptsDir)) {
113
+ const scripts = await readdir(scriptsDir);
114
+ result.info.push(`Found ${scripts.length} script(s) in scripts/`);
115
+
116
+ // Check for example placeholders
117
+ if (scripts.includes("example.sh") || scripts.includes("example.py")) {
118
+ result.warnings.push("Contains placeholder example script");
119
+ }
120
+ }
121
+
122
+ if (existsSync(refsDir)) {
123
+ const refs = await readdir(refsDir);
124
+ result.info.push(`Found ${refs.length} reference(s) in references/`);
125
+ }
126
+
127
+ if (existsSync(assetsDir)) {
128
+ const assets = await readdir(assetsDir);
129
+ result.info.push(`Found ${assets.length} asset(s) in assets/`);
130
+ }
131
+
132
+ // Check for unwanted files
133
+ const unwantedFiles = [
134
+ "README.md",
135
+ "CHANGELOG.md",
136
+ "INSTALLATION.md",
137
+ "CONTRIBUTING.md",
138
+ ];
139
+ for (const file of unwantedFiles) {
140
+ if (existsSync(join(skillPath, file))) {
141
+ result.warnings.push(
142
+ `Found ${file} - skills should only contain SKILL.md and resources`,
143
+ );
144
+ }
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ // Main
151
+ const skillPath = process.argv[2];
152
+
153
+ if (!skillPath) {
154
+ console.log(`
155
+ Skill Validator - Validates skill structure and content
156
+
157
+ Usage:
158
+ bun scripts/validate-skill.ts <path/to/skill>
159
+
160
+ Examples:
161
+ bun scripts/validate-skill.ts .opencode/skills/my-skill
162
+ bun scripts/validate-skill.ts global-skills/debugging
163
+ `);
164
+ process.exit(1);
165
+ }
166
+
167
+ console.log(`Validating skill: ${skillPath}\n`);
168
+
169
+ const result = await validateSkill(skillPath);
170
+
171
+ // Print results
172
+ if (result.errors.length > 0) {
173
+ console.log("❌ Errors:");
174
+ for (const error of result.errors) {
175
+ console.log(` - ${error}`);
176
+ }
177
+ console.log();
178
+ }
179
+
180
+ if (result.warnings.length > 0) {
181
+ console.log("⚠️ Warnings:");
182
+ for (const warning of result.warnings) {
183
+ console.log(` - ${warning}`);
184
+ }
185
+ console.log();
186
+ }
187
+
188
+ if (result.info.length > 0) {
189
+ console.log("ℹ️ Info:");
190
+ for (const info of result.info) {
191
+ console.log(` - ${info}`);
192
+ }
193
+ console.log();
194
+ }
195
+
196
+ if (result.valid) {
197
+ console.log("✅ Skill is valid!");
198
+ if (result.warnings.length > 0) {
199
+ console.log(" (but consider addressing warnings above)");
200
+ }
201
+ } else {
202
+ console.log("❌ Skill validation failed");
203
+ process.exit(1);
204
+ }
@@ -11,12 +11,15 @@
11
11
  import { describe, it, expect, beforeAll } from "vitest";
12
12
  import {
13
13
  mcpCall,
14
+ mcpCallWithAutoInit,
14
15
  sessionStates,
15
16
  setState,
16
17
  clearState,
17
18
  requireState,
18
19
  MAX_INBOX_LIMIT,
19
20
  AgentMailNotInitializedError,
21
+ isProjectNotFoundError,
22
+ isAgentNotFoundError,
20
23
  type AgentMailState,
21
24
  } from "./agent-mail";
22
25
 
@@ -1318,4 +1321,109 @@ describe("agent-mail integration", () => {
1318
1321
  clearState(worker2Ctx.sessionID);
1319
1322
  });
1320
1323
  });
1324
+
1325
+ // ============================================================================
1326
+ // Self-Healing Tests (mcpCallWithAutoInit)
1327
+ // ============================================================================
1328
+
1329
+ describe("self-healing (mcpCallWithAutoInit)", () => {
1330
+ it("detects project not found errors correctly", () => {
1331
+ const projectError = new Error("Project 'migrate-egghead' not found.");
1332
+ const agentError = new Error("Agent 'BlueLake' not found in project");
1333
+ const otherError = new Error("Network timeout");
1334
+
1335
+ expect(isProjectNotFoundError(projectError)).toBe(true);
1336
+ expect(isProjectNotFoundError(agentError)).toBe(false);
1337
+ expect(isProjectNotFoundError(otherError)).toBe(false);
1338
+
1339
+ expect(isAgentNotFoundError(agentError)).toBe(true);
1340
+ expect(isAgentNotFoundError(projectError)).toBe(false);
1341
+ expect(isAgentNotFoundError(otherError)).toBe(false);
1342
+ });
1343
+
1344
+ it("auto-registers project on 'not found' error", async () => {
1345
+ const ctx = createTestContext();
1346
+
1347
+ // First, ensure project exists and register an agent
1348
+ const { state } = await initTestAgent(ctx, `AutoInit_${Date.now()}`);
1349
+
1350
+ // Now use mcpCallWithAutoInit - it should work normally
1351
+ // (no error to recover from, but verifies the wrapper works)
1352
+ await mcpCallWithAutoInit("send_message", {
1353
+ project_key: state.projectKey,
1354
+ agent_name: state.agentName,
1355
+ sender_name: state.agentName,
1356
+ to: [],
1357
+ subject: "Test auto-init wrapper",
1358
+ body_md: "This should work normally",
1359
+ thread_id: "test-thread",
1360
+ importance: "normal",
1361
+ });
1362
+
1363
+ // Verify message was sent by checking inbox
1364
+ const inbox = await mcpCall<Array<{ subject: string }>>("fetch_inbox", {
1365
+ project_key: state.projectKey,
1366
+ agent_name: state.agentName,
1367
+ limit: 5,
1368
+ include_bodies: false,
1369
+ });
1370
+
1371
+ // The message should be in the inbox (sent to empty 'to' = broadcast)
1372
+ // Note: depending on Agent Mail behavior, broadcast might not show in sender's inbox
1373
+ // This test mainly verifies the wrapper doesn't break normal operation
1374
+
1375
+ // Cleanup
1376
+ clearState(ctx.sessionID);
1377
+ });
1378
+
1379
+ it("recovers from simulated project not found by re-registering", async () => {
1380
+ const ctx = createTestContext();
1381
+
1382
+ // Create a fresh project key that doesn't exist yet
1383
+ const freshProjectKey = `/test/fresh-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1384
+ const agentName = `Recovery_${Date.now()}`;
1385
+
1386
+ // First ensure the project exists (simulating initial setup)
1387
+ await mcpCall("ensure_project", { human_key: freshProjectKey });
1388
+ await mcpCall("register_agent", {
1389
+ project_key: freshProjectKey,
1390
+ program: "opencode-test",
1391
+ model: "test-model",
1392
+ name: agentName,
1393
+ task_description: "Recovery test agent",
1394
+ });
1395
+
1396
+ // Now use mcpCallWithAutoInit for an operation
1397
+ // This should work, and if the project somehow got lost, it would re-register
1398
+ await mcpCallWithAutoInit("send_message", {
1399
+ project_key: freshProjectKey,
1400
+ agent_name: agentName,
1401
+ sender_name: agentName,
1402
+ to: [],
1403
+ subject: "Recovery test",
1404
+ body_md: "Testing self-healing",
1405
+ thread_id: "recovery-test",
1406
+ importance: "normal",
1407
+ });
1408
+
1409
+ // If we got here without error, the wrapper is working
1410
+ // (In a real scenario where the server restarted, it would have re-registered)
1411
+ });
1412
+
1413
+ it("passes through non-recoverable errors", async () => {
1414
+ const ctx = createTestContext();
1415
+ const { state } = await initTestAgent(ctx, `ErrorPass_${Date.now()}`);
1416
+
1417
+ // Try to call a non-existent tool - should throw, not retry forever
1418
+ await expect(
1419
+ mcpCallWithAutoInit("nonexistent_tool_xyz", {
1420
+ project_key: state.projectKey,
1421
+ agent_name: state.agentName,
1422
+ }),
1423
+ ).rejects.toThrow(/Unknown tool/);
1424
+
1425
+ // Cleanup
1426
+ clearState(ctx.sessionID);
1427
+ });
1428
+ });
1321
1429
  });