opencode-swarm-plugin 0.12.20 → 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.
- package/.beads/issues.jsonl +12 -1
- package/.github/workflows/ci.yml +26 -0
- package/bun.lock +21 -0
- package/dist/index.js +24951 -20070
- package/dist/plugin.js +24937 -20070
- package/examples/skills/beads-workflow/SKILL.md +165 -0
- package/examples/skills/skill-creator/SKILL.md +223 -0
- package/examples/skills/swarm-coordination/SKILL.md +148 -0
- package/global-skills/cli-builder/SKILL.md +344 -0
- package/global-skills/cli-builder/references/advanced-patterns.md +244 -0
- package/global-skills/code-review/SKILL.md +166 -0
- package/global-skills/debugging/SKILL.md +150 -0
- package/global-skills/skill-creator/LICENSE.txt +202 -0
- package/global-skills/skill-creator/SKILL.md +352 -0
- package/global-skills/skill-creator/references/output-patterns.md +82 -0
- package/global-skills/skill-creator/references/workflows.md +28 -0
- package/global-skills/swarm-coordination/SKILL.md +166 -0
- package/package.json +6 -5
- package/scripts/init-skill.ts +222 -0
- package/scripts/validate-skill.ts +204 -0
- package/src/agent-mail.ts +40 -4
- package/src/beads.ts +24 -0
- package/src/index.ts +49 -0
- package/src/schemas/bead.ts +21 -1
- package/src/skills.test.ts +408 -0
- package/src/skills.ts +1364 -0
- package/src/swarm.ts +282 -4
|
@@ -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
|
+
}
|
package/src/agent-mail.ts
CHANGED
|
@@ -27,6 +27,35 @@ const AGENT_MAIL_URL = "http://127.0.0.1:8765";
|
|
|
27
27
|
const DEFAULT_TTL_SECONDS = 3600; // 1 hour
|
|
28
28
|
const MAX_INBOX_LIMIT = 5; // HARD CAP - never exceed this
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Default project directory for Agent Mail operations
|
|
32
|
+
*
|
|
33
|
+
* This is set by the plugin init to the actual working directory (from OpenCode).
|
|
34
|
+
* Without this, tools might use the plugin's directory instead of the project's.
|
|
35
|
+
*
|
|
36
|
+
* Set this via setAgentMailProjectDirectory() before using tools.
|
|
37
|
+
*/
|
|
38
|
+
let agentMailProjectDirectory: string | null = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set the default project directory for Agent Mail operations
|
|
42
|
+
*
|
|
43
|
+
* Called during plugin initialization with the actual project directory.
|
|
44
|
+
* This ensures agentmail_init uses the correct project path by default.
|
|
45
|
+
*/
|
|
46
|
+
export function setAgentMailProjectDirectory(directory: string): void {
|
|
47
|
+
agentMailProjectDirectory = directory;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the default project directory
|
|
52
|
+
*
|
|
53
|
+
* Returns the configured directory, or falls back to cwd if not set.
|
|
54
|
+
*/
|
|
55
|
+
export function getAgentMailProjectDirectory(): string {
|
|
56
|
+
return agentMailProjectDirectory || process.cwd();
|
|
57
|
+
}
|
|
58
|
+
|
|
30
59
|
// Retry configuration
|
|
31
60
|
const RETRY_CONFIG = {
|
|
32
61
|
maxRetries: parseInt(process.env.OPENCODE_AGENT_MAIL_MAX_RETRIES || "3"),
|
|
@@ -1077,7 +1106,10 @@ export const agentmail_init = tool({
|
|
|
1077
1106
|
args: {
|
|
1078
1107
|
project_path: tool.schema
|
|
1079
1108
|
.string()
|
|
1080
|
-
.
|
|
1109
|
+
.optional()
|
|
1110
|
+
.describe(
|
|
1111
|
+
"Absolute path to the project/repo (defaults to current working directory)",
|
|
1112
|
+
),
|
|
1081
1113
|
agent_name: tool.schema
|
|
1082
1114
|
.string()
|
|
1083
1115
|
.optional()
|
|
@@ -1088,6 +1120,10 @@ export const agentmail_init = tool({
|
|
|
1088
1120
|
.describe("Description of current task"),
|
|
1089
1121
|
},
|
|
1090
1122
|
async execute(args, ctx) {
|
|
1123
|
+
// Use provided path or fall back to configured project directory
|
|
1124
|
+
// This prevents using the plugin's directory when working in a different project
|
|
1125
|
+
const projectPath = args.project_path || getAgentMailProjectDirectory();
|
|
1126
|
+
|
|
1091
1127
|
// Check if Agent Mail is available
|
|
1092
1128
|
const available = await checkAgentMailAvailable();
|
|
1093
1129
|
if (!available) {
|
|
@@ -1113,12 +1149,12 @@ export const agentmail_init = tool({
|
|
|
1113
1149
|
try {
|
|
1114
1150
|
// 1. Ensure project exists
|
|
1115
1151
|
const project = await mcpCall<ProjectInfo>("ensure_project", {
|
|
1116
|
-
human_key:
|
|
1152
|
+
human_key: projectPath,
|
|
1117
1153
|
});
|
|
1118
1154
|
|
|
1119
1155
|
// 2. Register agent
|
|
1120
1156
|
const agent = await mcpCall<AgentInfo>("register_agent", {
|
|
1121
|
-
project_key:
|
|
1157
|
+
project_key: projectPath,
|
|
1122
1158
|
program: "opencode",
|
|
1123
1159
|
model: "claude-opus-4",
|
|
1124
1160
|
name: args.agent_name, // undefined = auto-generate
|
|
@@ -1127,7 +1163,7 @@ export const agentmail_init = tool({
|
|
|
1127
1163
|
|
|
1128
1164
|
// 3. Store state using sessionID
|
|
1129
1165
|
const state: AgentMailState = {
|
|
1130
|
-
projectKey:
|
|
1166
|
+
projectKey: projectPath,
|
|
1131
1167
|
agentName: agent.name,
|
|
1132
1168
|
reservations: [],
|
|
1133
1169
|
startedAt: new Date().toISOString(),
|
package/src/beads.ts
CHANGED
|
@@ -159,6 +159,11 @@ function buildCreateCommand(args: BeadCreateArgs): string[] {
|
|
|
159
159
|
parts.push("--parent", args.parent_id);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Custom ID for human-readable bead names (e.g., 'phase-0', 'phase-1.e2e-test')
|
|
163
|
+
if (args.id) {
|
|
164
|
+
parts.push("--id", args.id);
|
|
165
|
+
}
|
|
166
|
+
|
|
162
167
|
parts.push("--json");
|
|
163
168
|
return parts;
|
|
164
169
|
}
|
|
@@ -288,12 +293,22 @@ export const beads_create_epic = tool({
|
|
|
288
293
|
.string()
|
|
289
294
|
.optional()
|
|
290
295
|
.describe("Epic description"),
|
|
296
|
+
epic_id: tool.schema
|
|
297
|
+
.string()
|
|
298
|
+
.optional()
|
|
299
|
+
.describe("Custom ID for the epic (e.g., 'phase-0')"),
|
|
291
300
|
subtasks: tool.schema
|
|
292
301
|
.array(
|
|
293
302
|
tool.schema.object({
|
|
294
303
|
title: tool.schema.string(),
|
|
295
304
|
priority: tool.schema.number().min(0).max(3).optional(),
|
|
296
305
|
files: tool.schema.array(tool.schema.string()).optional(),
|
|
306
|
+
id_suffix: tool.schema
|
|
307
|
+
.string()
|
|
308
|
+
.optional()
|
|
309
|
+
.describe(
|
|
310
|
+
"Custom ID suffix (e.g., 'e2e-test' becomes 'phase-0.e2e-test')",
|
|
311
|
+
),
|
|
297
312
|
}),
|
|
298
313
|
)
|
|
299
314
|
.describe("Subtasks to create under the epic"),
|
|
@@ -309,6 +324,7 @@ export const beads_create_epic = tool({
|
|
|
309
324
|
type: "epic",
|
|
310
325
|
priority: 1,
|
|
311
326
|
description: validated.epic_description,
|
|
327
|
+
id: validated.epic_id,
|
|
312
328
|
});
|
|
313
329
|
|
|
314
330
|
const epicResult = await runBdCommand(epicCmd.slice(1)); // Remove 'bd' prefix
|
|
@@ -326,11 +342,19 @@ export const beads_create_epic = tool({
|
|
|
326
342
|
|
|
327
343
|
// 2. Create subtasks
|
|
328
344
|
for (const subtask of validated.subtasks) {
|
|
345
|
+
// Build subtask ID: if epic has custom ID and subtask has suffix, combine them
|
|
346
|
+
// e.g., epic_id='phase-0', id_suffix='e2e-test' → 'phase-0.e2e-test'
|
|
347
|
+
let subtaskId: string | undefined;
|
|
348
|
+
if (validated.epic_id && subtask.id_suffix) {
|
|
349
|
+
subtaskId = `${validated.epic_id}.${subtask.id_suffix}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
329
352
|
const subtaskCmd = buildCreateCommand({
|
|
330
353
|
title: subtask.title,
|
|
331
354
|
type: "task",
|
|
332
355
|
priority: subtask.priority ?? 2,
|
|
333
356
|
parent_id: epic.id,
|
|
357
|
+
id: subtaskId,
|
|
334
358
|
});
|
|
335
359
|
|
|
336
360
|
const subtaskResult = await runBdCommand(subtaskCmd.slice(1)); // Remove 'bd' prefix
|
package/src/index.ts
CHANGED
|
@@ -25,12 +25,14 @@ import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
|
25
25
|
import { beadsTools, setBeadsWorkingDirectory } from "./beads";
|
|
26
26
|
import {
|
|
27
27
|
agentMailTools,
|
|
28
|
+
setAgentMailProjectDirectory,
|
|
28
29
|
type AgentMailState,
|
|
29
30
|
AGENT_MAIL_URL,
|
|
30
31
|
} from "./agent-mail";
|
|
31
32
|
import { structuredTools } from "./structured";
|
|
32
33
|
import { swarmTools } from "./swarm";
|
|
33
34
|
import { repoCrawlTools } from "./repo-crawl";
|
|
35
|
+
import { skillsTools, setSkillsProjectDirectory } from "./skills";
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* OpenCode Swarm Plugin
|
|
@@ -41,6 +43,7 @@ import { repoCrawlTools } from "./repo-crawl";
|
|
|
41
43
|
* - structured:* - Structured output parsing and validation
|
|
42
44
|
* - swarm:* - Swarm orchestration and task decomposition
|
|
43
45
|
* - repo-crawl:* - GitHub API tools for repository research
|
|
46
|
+
* - skills:* - Agent skills discovery, activation, and execution
|
|
44
47
|
*
|
|
45
48
|
* @param input - Plugin context from OpenCode
|
|
46
49
|
* @returns Plugin hooks including tools, events, and tool execution hooks
|
|
@@ -54,6 +57,15 @@ export const SwarmPlugin: Plugin = async (
|
|
|
54
57
|
// This ensures bd runs in the project directory, not ~/.config/opencode
|
|
55
58
|
setBeadsWorkingDirectory(directory);
|
|
56
59
|
|
|
60
|
+
// Set the project directory for skills discovery
|
|
61
|
+
// Skills are discovered from .opencode/skills/, .claude/skills/, or skills/
|
|
62
|
+
setSkillsProjectDirectory(directory);
|
|
63
|
+
|
|
64
|
+
// Set the project directory for Agent Mail
|
|
65
|
+
// This ensures agentmail_init uses the correct project path by default
|
|
66
|
+
// (prevents using plugin directory when working in a different project)
|
|
67
|
+
setAgentMailProjectDirectory(directory);
|
|
68
|
+
|
|
57
69
|
/** Track active sessions for cleanup */
|
|
58
70
|
let activeAgentMailState: AgentMailState | null = null;
|
|
59
71
|
|
|
@@ -116,6 +128,7 @@ export const SwarmPlugin: Plugin = async (
|
|
|
116
128
|
...structuredTools,
|
|
117
129
|
...swarmTools,
|
|
118
130
|
...repoCrawlTools,
|
|
131
|
+
...skillsTools,
|
|
119
132
|
},
|
|
120
133
|
|
|
121
134
|
/**
|
|
@@ -239,6 +252,11 @@ export {
|
|
|
239
252
|
AgentMailNotInitializedError,
|
|
240
253
|
FileReservationConflictError,
|
|
241
254
|
createAgentMailError,
|
|
255
|
+
setAgentMailProjectDirectory,
|
|
256
|
+
getAgentMailProjectDirectory,
|
|
257
|
+
mcpCallWithAutoInit,
|
|
258
|
+
isProjectNotFoundError,
|
|
259
|
+
isAgentNotFoundError,
|
|
242
260
|
type AgentMailState,
|
|
243
261
|
} from "./agent-mail";
|
|
244
262
|
|
|
@@ -305,6 +323,7 @@ export const allTools = {
|
|
|
305
323
|
...structuredTools,
|
|
306
324
|
...swarmTools,
|
|
307
325
|
...repoCrawlTools,
|
|
326
|
+
...skillsTools,
|
|
308
327
|
} as const;
|
|
309
328
|
|
|
310
329
|
/**
|
|
@@ -387,3 +406,33 @@ export {
|
|
|
387
406
|
* - Graceful rate limit handling
|
|
388
407
|
*/
|
|
389
408
|
export { repoCrawlTools, RepoCrawlError } from "./repo-crawl";
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Re-export skills module
|
|
412
|
+
*
|
|
413
|
+
* Implements Anthropic's Agent Skills specification for OpenCode.
|
|
414
|
+
*
|
|
415
|
+
* Includes:
|
|
416
|
+
* - skillsTools - All skills tools (list, use, execute, read)
|
|
417
|
+
* - discoverSkills, getSkill, listSkills - Discovery functions
|
|
418
|
+
* - parseFrontmatter - YAML frontmatter parser
|
|
419
|
+
* - getSkillsContextForSwarm - Swarm integration helper
|
|
420
|
+
* - findRelevantSkills - Task-based skill matching
|
|
421
|
+
*
|
|
422
|
+
* Types:
|
|
423
|
+
* - Skill, SkillMetadata, SkillRef - Skill data types
|
|
424
|
+
*/
|
|
425
|
+
export {
|
|
426
|
+
skillsTools,
|
|
427
|
+
discoverSkills,
|
|
428
|
+
getSkill,
|
|
429
|
+
listSkills,
|
|
430
|
+
parseFrontmatter,
|
|
431
|
+
setSkillsProjectDirectory,
|
|
432
|
+
invalidateSkillsCache,
|
|
433
|
+
getSkillsContextForSwarm,
|
|
434
|
+
findRelevantSkills,
|
|
435
|
+
type Skill,
|
|
436
|
+
type SkillMetadata,
|
|
437
|
+
type SkillRef,
|
|
438
|
+
} from "./skills";
|