@aksp/opencrew 1.0.0
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/CHANGELOG.md +19 -0
- package/LICENSE +24 -0
- package/README.md +118 -0
- package/bin/opencrew.js +8 -0
- package/package.json +57 -0
- package/src/cli.js +70 -0
- package/src/commands/init.js +97 -0
- package/src/commands/update.js +58 -0
- package/src/lib/fsx.js +55 -0
- package/src/lib/ides.js +120 -0
- package/src/lib/paths.js +8 -0
- package/src/lib/prompts.js +35 -0
- package/src/lib/ui.js +20 -0
- package/templates/.env.example +23 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +105 -0
- package/templates/_opencrew/.opencrew-version +1 -0
- package/templates/_opencrew/_investigations/.gitkeep +0 -0
- package/templates/_opencrew/_memory/company.md +4 -0
- package/templates/_opencrew/_memory/preferences.md +9 -0
- package/templates/_opencrew/config/playwright.config.json +11 -0
- package/templates/_opencrew/core/architect.agent.yaml +110 -0
- package/templates/_opencrew/core/best-practices/_catalog.yaml +116 -0
- package/templates/_opencrew/core/best-practices/blog-post.md +151 -0
- package/templates/_opencrew/core/best-practices/blog-seo.md +146 -0
- package/templates/_opencrew/core/best-practices/copywriting.md +446 -0
- package/templates/_opencrew/core/best-practices/data-analysis.md +420 -0
- package/templates/_opencrew/core/best-practices/email-newsletter.md +136 -0
- package/templates/_opencrew/core/best-practices/email-sales.md +127 -0
- package/templates/_opencrew/core/best-practices/image-design.md +365 -0
- package/templates/_opencrew/core/best-practices/instagram-feed.md +252 -0
- package/templates/_opencrew/core/best-practices/instagram-reels.md +128 -0
- package/templates/_opencrew/core/best-practices/instagram-stories.md +123 -0
- package/templates/_opencrew/core/best-practices/linkedin-article.md +133 -0
- package/templates/_opencrew/core/best-practices/linkedin-post.md +138 -0
- package/templates/_opencrew/core/best-practices/researching.md +366 -0
- package/templates/_opencrew/core/best-practices/review.md +286 -0
- package/templates/_opencrew/core/best-practices/social-networks-publishing.md +311 -0
- package/templates/_opencrew/core/best-practices/strategist.md +361 -0
- package/templates/_opencrew/core/best-practices/technical-writing.md +382 -0
- package/templates/_opencrew/core/best-practices/twitter-post.md +122 -0
- package/templates/_opencrew/core/best-practices/twitter-thread.md +139 -0
- package/templates/_opencrew/core/best-practices/whatsapp-broadcast.md +124 -0
- package/templates/_opencrew/core/best-practices/youtube-script.md +139 -0
- package/templates/_opencrew/core/best-practices/youtube-shorts.md +129 -0
- package/templates/_opencrew/core/prompts/build.prompt.md +547 -0
- package/templates/_opencrew/core/prompts/design.prompt.md +469 -0
- package/templates/_opencrew/core/prompts/discovery.prompt.md +269 -0
- package/templates/_opencrew/core/prompts/sherlock-instagram.md +123 -0
- package/templates/_opencrew/core/prompts/sherlock-linkedin.md +73 -0
- package/templates/_opencrew/core/prompts/sherlock-shared.md +684 -0
- package/templates/_opencrew/core/prompts/sherlock-twitter.md +78 -0
- package/templates/_opencrew/core/prompts/sherlock-youtube.md +85 -0
- package/templates/_opencrew/core/runner.pipeline.md +611 -0
- package/templates/_opencrew/core/skills.engine.md +388 -0
- package/templates/_opencrew/logs/.gitkeep +0 -0
- package/templates/crews/.gitkeep +0 -0
- package/templates/gitignore +8 -0
- package/templates/skills/apify/SKILL.md +55 -0
- package/templates/skills/blotato/SKILL.md +63 -0
- package/templates/skills/canva/SKILL.md +60 -0
- package/templates/skills/image-ai-generator/SKILL.md +124 -0
- package/templates/skills/image-ai-generator/scripts/generate.py +175 -0
- package/templates/skills/image-creator/SKILL.md +155 -0
- package/templates/skills/image-fetcher/SKILL.md +91 -0
- package/templates/skills/instagram-publisher/SKILL.md +119 -0
- package/templates/skills/instagram-publisher/scripts/publish.js +165 -0
- package/templates/skills/opencrew-best-practice-creator/SKILL.md +192 -0
- package/templates/skills/opencrew-skill-creator/SKILL.md +420 -0
- package/templates/skills/opencrew-skill-creator/agents/analyzer.md +274 -0
- package/templates/skills/opencrew-skill-creator/agents/comparator.md +202 -0
- package/templates/skills/opencrew-skill-creator/agents/grader.md +223 -0
- package/templates/skills/opencrew-skill-creator/assets/eval_review.html +146 -0
- package/templates/skills/opencrew-skill-creator/eval-viewer/generate_review.py +471 -0
- package/templates/skills/opencrew-skill-creator/eval-viewer/viewer.html +1325 -0
- package/templates/skills/opencrew-skill-creator/references/schemas.md +430 -0
- package/templates/skills/opencrew-skill-creator/references/skill-format.md +235 -0
- package/templates/skills/opencrew-skill-creator/scripts/__init__.py +0 -0
- package/templates/skills/opencrew-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/templates/skills/opencrew-skill-creator/scripts/quick_validate.py +103 -0
- package/templates/skills/opencrew-skill-creator/scripts/run_eval.py +310 -0
- package/templates/skills/opencrew-skill-creator/scripts/utils.py +47 -0
- package/templates/skills/resend/SKILL.md +80 -0
- package/templates/skills/template-designer/SKILL.md +208 -0
- package/templates/skills/template-designer/base-templates/model-a.html +27 -0
- package/templates/skills/template-designer/base-templates/model-b.html +31 -0
- package/templates/skills/template-designer/base-templates/model-c.html +42 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run trigger evaluation for a skill description.
|
|
3
|
+
|
|
4
|
+
Tests whether a skill's description causes Claude to trigger (read the skill)
|
|
5
|
+
for a set of queries. Outputs results as JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import select
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from scripts.utils import parse_skill_md
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_project_root() -> Path:
|
|
23
|
+
"""Find the project root by walking up from cwd looking for .claude/.
|
|
24
|
+
|
|
25
|
+
Mimics how Claude Code discovers its project root, so the command file
|
|
26
|
+
we create ends up where claude -p will look for it.
|
|
27
|
+
"""
|
|
28
|
+
current = Path.cwd()
|
|
29
|
+
for parent in [current, *current.parents]:
|
|
30
|
+
if (parent / ".claude").is_dir():
|
|
31
|
+
return parent
|
|
32
|
+
return current
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_single_query(
|
|
36
|
+
query: str,
|
|
37
|
+
skill_name: str,
|
|
38
|
+
skill_description: str,
|
|
39
|
+
timeout: int,
|
|
40
|
+
project_root: str,
|
|
41
|
+
model: str | None = None,
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""Run a single query and return whether the skill was triggered.
|
|
44
|
+
|
|
45
|
+
Creates a command file in .claude/commands/ so it appears in Claude's
|
|
46
|
+
available_skills list, then runs `claude -p` with the raw query.
|
|
47
|
+
Uses --include-partial-messages to detect triggering early from
|
|
48
|
+
stream events (content_block_start) rather than waiting for the
|
|
49
|
+
full assistant message, which only arrives after tool execution.
|
|
50
|
+
"""
|
|
51
|
+
unique_id = uuid.uuid4().hex[:8]
|
|
52
|
+
clean_name = f"{skill_name}-skill-{unique_id}"
|
|
53
|
+
project_commands_dir = Path(project_root) / ".claude" / "commands"
|
|
54
|
+
command_file = project_commands_dir / f"{clean_name}.md"
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
project_commands_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
# Use YAML block scalar to avoid breaking on quotes in description
|
|
59
|
+
indented_desc = "\n ".join(skill_description.split("\n"))
|
|
60
|
+
command_content = (
|
|
61
|
+
f"---\n"
|
|
62
|
+
f"description: |\n"
|
|
63
|
+
f" {indented_desc}\n"
|
|
64
|
+
f"---\n\n"
|
|
65
|
+
f"# {skill_name}\n\n"
|
|
66
|
+
f"This skill handles: {skill_description}\n"
|
|
67
|
+
)
|
|
68
|
+
command_file.write_text(command_content)
|
|
69
|
+
|
|
70
|
+
cmd = [
|
|
71
|
+
"claude",
|
|
72
|
+
"-p", query,
|
|
73
|
+
"--output-format", "stream-json",
|
|
74
|
+
"--verbose",
|
|
75
|
+
"--include-partial-messages",
|
|
76
|
+
]
|
|
77
|
+
if model:
|
|
78
|
+
cmd.extend(["--model", model])
|
|
79
|
+
|
|
80
|
+
# Remove CLAUDECODE env var to allow nesting claude -p inside a
|
|
81
|
+
# Claude Code session. The guard is for interactive terminal conflicts;
|
|
82
|
+
# programmatic subprocess usage is safe.
|
|
83
|
+
env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
|
|
84
|
+
|
|
85
|
+
process = subprocess.Popen(
|
|
86
|
+
cmd,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.DEVNULL,
|
|
89
|
+
cwd=project_root,
|
|
90
|
+
env=env,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
triggered = False
|
|
94
|
+
start_time = time.time()
|
|
95
|
+
buffer = ""
|
|
96
|
+
# Track state for stream event detection
|
|
97
|
+
pending_tool_name = None
|
|
98
|
+
accumulated_json = ""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
while time.time() - start_time < timeout:
|
|
102
|
+
if process.poll() is not None:
|
|
103
|
+
remaining = process.stdout.read()
|
|
104
|
+
if remaining:
|
|
105
|
+
buffer += remaining.decode("utf-8", errors="replace")
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
ready, _, _ = select.select([process.stdout], [], [], 1.0)
|
|
109
|
+
if not ready:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
chunk = os.read(process.stdout.fileno(), 8192)
|
|
113
|
+
if not chunk:
|
|
114
|
+
break
|
|
115
|
+
buffer += chunk.decode("utf-8", errors="replace")
|
|
116
|
+
|
|
117
|
+
while "\n" in buffer:
|
|
118
|
+
line, buffer = buffer.split("\n", 1)
|
|
119
|
+
line = line.strip()
|
|
120
|
+
if not line:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
event = json.loads(line)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Early detection via stream events
|
|
129
|
+
if event.get("type") == "stream_event":
|
|
130
|
+
se = event.get("event", {})
|
|
131
|
+
se_type = se.get("type", "")
|
|
132
|
+
|
|
133
|
+
if se_type == "content_block_start":
|
|
134
|
+
cb = se.get("content_block", {})
|
|
135
|
+
if cb.get("type") == "tool_use":
|
|
136
|
+
tool_name = cb.get("name", "")
|
|
137
|
+
if tool_name in ("Skill", "Read"):
|
|
138
|
+
pending_tool_name = tool_name
|
|
139
|
+
accumulated_json = ""
|
|
140
|
+
else:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
elif se_type == "content_block_delta" and pending_tool_name:
|
|
144
|
+
delta = se.get("delta", {})
|
|
145
|
+
if delta.get("type") == "input_json_delta":
|
|
146
|
+
accumulated_json += delta.get("partial_json", "")
|
|
147
|
+
if clean_name in accumulated_json:
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
elif se_type in ("content_block_stop", "message_stop"):
|
|
151
|
+
if pending_tool_name:
|
|
152
|
+
return clean_name in accumulated_json
|
|
153
|
+
if se_type == "message_stop":
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Fallback: full assistant message
|
|
157
|
+
elif event.get("type") == "assistant":
|
|
158
|
+
message = event.get("message", {})
|
|
159
|
+
for content_item in message.get("content", []):
|
|
160
|
+
if content_item.get("type") != "tool_use":
|
|
161
|
+
continue
|
|
162
|
+
tool_name = content_item.get("name", "")
|
|
163
|
+
tool_input = content_item.get("input", {})
|
|
164
|
+
if tool_name == "Skill" and clean_name in tool_input.get("skill", ""):
|
|
165
|
+
triggered = True
|
|
166
|
+
elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""):
|
|
167
|
+
triggered = True
|
|
168
|
+
return triggered
|
|
169
|
+
|
|
170
|
+
elif event.get("type") == "result":
|
|
171
|
+
return triggered
|
|
172
|
+
finally:
|
|
173
|
+
# Clean up process on any exit path (return, exception, timeout)
|
|
174
|
+
if process.poll() is None:
|
|
175
|
+
process.kill()
|
|
176
|
+
process.wait()
|
|
177
|
+
|
|
178
|
+
return triggered
|
|
179
|
+
finally:
|
|
180
|
+
if command_file.exists():
|
|
181
|
+
command_file.unlink()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_eval(
|
|
185
|
+
eval_set: list[dict],
|
|
186
|
+
skill_name: str,
|
|
187
|
+
description: str,
|
|
188
|
+
num_workers: int,
|
|
189
|
+
timeout: int,
|
|
190
|
+
project_root: Path,
|
|
191
|
+
runs_per_query: int = 1,
|
|
192
|
+
trigger_threshold: float = 0.5,
|
|
193
|
+
model: str | None = None,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Run the full eval set and return results."""
|
|
196
|
+
results = []
|
|
197
|
+
|
|
198
|
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
|
199
|
+
future_to_info = {}
|
|
200
|
+
for item in eval_set:
|
|
201
|
+
for run_idx in range(runs_per_query):
|
|
202
|
+
future = executor.submit(
|
|
203
|
+
run_single_query,
|
|
204
|
+
item["query"],
|
|
205
|
+
skill_name,
|
|
206
|
+
description,
|
|
207
|
+
timeout,
|
|
208
|
+
str(project_root),
|
|
209
|
+
model,
|
|
210
|
+
)
|
|
211
|
+
future_to_info[future] = (item, run_idx)
|
|
212
|
+
|
|
213
|
+
query_triggers: dict[str, list[bool]] = {}
|
|
214
|
+
query_items: dict[str, dict] = {}
|
|
215
|
+
for future in as_completed(future_to_info):
|
|
216
|
+
item, _ = future_to_info[future]
|
|
217
|
+
query = item["query"]
|
|
218
|
+
query_items[query] = item
|
|
219
|
+
if query not in query_triggers:
|
|
220
|
+
query_triggers[query] = []
|
|
221
|
+
try:
|
|
222
|
+
query_triggers[query].append(future.result())
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"Warning: query failed: {e}", file=sys.stderr)
|
|
225
|
+
query_triggers[query].append(False)
|
|
226
|
+
|
|
227
|
+
for query, triggers in query_triggers.items():
|
|
228
|
+
item = query_items[query]
|
|
229
|
+
trigger_rate = sum(triggers) / len(triggers)
|
|
230
|
+
should_trigger = item["should_trigger"]
|
|
231
|
+
if should_trigger:
|
|
232
|
+
did_pass = trigger_rate >= trigger_threshold
|
|
233
|
+
else:
|
|
234
|
+
did_pass = trigger_rate < trigger_threshold
|
|
235
|
+
results.append({
|
|
236
|
+
"query": query,
|
|
237
|
+
"should_trigger": should_trigger,
|
|
238
|
+
"trigger_rate": trigger_rate,
|
|
239
|
+
"triggers": sum(triggers),
|
|
240
|
+
"runs": len(triggers),
|
|
241
|
+
"pass": did_pass,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
passed = sum(1 for r in results if r["pass"])
|
|
245
|
+
total = len(results)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"skill_name": skill_name,
|
|
249
|
+
"description": description,
|
|
250
|
+
"results": results,
|
|
251
|
+
"summary": {
|
|
252
|
+
"total": total,
|
|
253
|
+
"passed": passed,
|
|
254
|
+
"failed": total - passed,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description")
|
|
261
|
+
parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
|
|
262
|
+
parser.add_argument("--skill-path", required=True, help="Path to skill directory")
|
|
263
|
+
parser.add_argument("--description", default=None, help="Override description to test")
|
|
264
|
+
parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
|
|
265
|
+
parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
|
|
266
|
+
parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
|
|
267
|
+
parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
|
|
268
|
+
parser.add_argument("--model", default=None, help="Model to use for claude -p (default: user's configured model)")
|
|
269
|
+
parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
|
|
270
|
+
args = parser.parse_args()
|
|
271
|
+
|
|
272
|
+
eval_set = json.loads(Path(args.eval_set).read_text())
|
|
273
|
+
skill_path = Path(args.skill_path)
|
|
274
|
+
|
|
275
|
+
if not (skill_path / "SKILL.md").exists():
|
|
276
|
+
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
|
|
279
|
+
name, original_description, content = parse_skill_md(skill_path)
|
|
280
|
+
description = args.description or original_description
|
|
281
|
+
project_root = find_project_root()
|
|
282
|
+
|
|
283
|
+
if args.verbose:
|
|
284
|
+
print(f"Evaluating: {description}", file=sys.stderr)
|
|
285
|
+
|
|
286
|
+
output = run_eval(
|
|
287
|
+
eval_set=eval_set,
|
|
288
|
+
skill_name=name,
|
|
289
|
+
description=description,
|
|
290
|
+
num_workers=args.num_workers,
|
|
291
|
+
timeout=args.timeout,
|
|
292
|
+
project_root=project_root,
|
|
293
|
+
runs_per_query=args.runs_per_query,
|
|
294
|
+
trigger_threshold=args.trigger_threshold,
|
|
295
|
+
model=args.model,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if args.verbose:
|
|
299
|
+
summary = output["summary"]
|
|
300
|
+
print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr)
|
|
301
|
+
for r in output["results"]:
|
|
302
|
+
status = "PASS" if r["pass"] else "FAIL"
|
|
303
|
+
rate_str = f"{r['triggers']}/{r['runs']}"
|
|
304
|
+
print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr)
|
|
305
|
+
|
|
306
|
+
print(json.dumps(output, indent=2))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared utilities for skill-creator scripts."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
|
|
8
|
+
"""Parse a SKILL.md file, returning (name, description, full_content)."""
|
|
9
|
+
content = (skill_path / "SKILL.md").read_text()
|
|
10
|
+
lines = content.split("\n")
|
|
11
|
+
|
|
12
|
+
if lines[0].strip() != "---":
|
|
13
|
+
raise ValueError("SKILL.md missing frontmatter (no opening ---)")
|
|
14
|
+
|
|
15
|
+
end_idx = None
|
|
16
|
+
for i, line in enumerate(lines[1:], start=1):
|
|
17
|
+
if line.strip() == "---":
|
|
18
|
+
end_idx = i
|
|
19
|
+
break
|
|
20
|
+
|
|
21
|
+
if end_idx is None:
|
|
22
|
+
raise ValueError("SKILL.md missing frontmatter (no closing ---)")
|
|
23
|
+
|
|
24
|
+
name = ""
|
|
25
|
+
description = ""
|
|
26
|
+
frontmatter_lines = lines[1:end_idx]
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(frontmatter_lines):
|
|
29
|
+
line = frontmatter_lines[i]
|
|
30
|
+
if line.startswith("name:"):
|
|
31
|
+
name = line[len("name:"):].strip().strip('"').strip("'")
|
|
32
|
+
elif line.startswith("description:"):
|
|
33
|
+
value = line[len("description:"):].strip()
|
|
34
|
+
# Handle YAML multiline indicators (>, |, >-, |-)
|
|
35
|
+
if value in (">", "|", ">-", "|-"):
|
|
36
|
+
continuation_lines: list[str] = []
|
|
37
|
+
i += 1
|
|
38
|
+
while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")):
|
|
39
|
+
continuation_lines.append(frontmatter_lines[i].strip())
|
|
40
|
+
i += 1
|
|
41
|
+
description = " ".join(continuation_lines)
|
|
42
|
+
continue
|
|
43
|
+
else:
|
|
44
|
+
description = value.strip('"').strip("'")
|
|
45
|
+
i += 1
|
|
46
|
+
|
|
47
|
+
return name, description, content
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resend
|
|
3
|
+
description: >
|
|
4
|
+
Send emails through Resend's official MCP server.
|
|
5
|
+
Supports single send, batch send, HTML and plain text bodies,
|
|
6
|
+
attachments, CC/BCC, scheduling, and contact management.
|
|
7
|
+
description_pt-BR: >
|
|
8
|
+
Envie emails pelo servidor MCP oficial da Resend.
|
|
9
|
+
Suporta envio individual, envio em lote, corpo HTML e texto puro,
|
|
10
|
+
anexos, CC/BCC, agendamento e gerenciamento de contatos.
|
|
11
|
+
description_es: >
|
|
12
|
+
Enviar correos electrónicos a través del servidor MCP oficial de Resend.
|
|
13
|
+
Soporta envío individual, envío por lotes, cuerpo HTML y texto plano,
|
|
14
|
+
adjuntos, CC/BCC, programación y gestión de contactos.
|
|
15
|
+
type: mcp
|
|
16
|
+
version: "1.0.0"
|
|
17
|
+
mcp:
|
|
18
|
+
server_name: resend
|
|
19
|
+
command: npx
|
|
20
|
+
args: ["-y", "resend-mcp"]
|
|
21
|
+
transport: stdio
|
|
22
|
+
env:
|
|
23
|
+
- RESEND_API_KEY
|
|
24
|
+
categories: [email, automation, communication]
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Resend — Email Skill
|
|
28
|
+
|
|
29
|
+
## When to use
|
|
30
|
+
|
|
31
|
+
Use this skill when a crew needs to send emails — welcome messages, notifications,
|
|
32
|
+
reports, newsletters, or any transactional/marketing email. Resend handles delivery
|
|
33
|
+
so the crew only needs to compose the content and call the MCP tools.
|
|
34
|
+
|
|
35
|
+
## Instructions
|
|
36
|
+
|
|
37
|
+
### Sending a single email
|
|
38
|
+
|
|
39
|
+
1. Prepare **from**, **to**, **subject**, and **body** (HTML or plain text).
|
|
40
|
+
2. Call the Resend MCP `send_email` tool.
|
|
41
|
+
3. Check the response for a successful `id` — that confirms the email was queued.
|
|
42
|
+
|
|
43
|
+
### Sending a batch
|
|
44
|
+
|
|
45
|
+
1. Build an array of email objects (same fields as single send).
|
|
46
|
+
2. Call the Resend MCP `batch_send_emails` tool.
|
|
47
|
+
3. Each item in the response will have its own `id` or error.
|
|
48
|
+
|
|
49
|
+
### Attachments
|
|
50
|
+
|
|
51
|
+
Pass attachments as an array with `filename`, `path` (local file), `url`, or `content` (base64).
|
|
52
|
+
|
|
53
|
+
### Scheduling
|
|
54
|
+
|
|
55
|
+
Include a `scheduled_at` field (ISO 8601 datetime) to schedule future delivery.
|
|
56
|
+
|
|
57
|
+
## Best practices
|
|
58
|
+
|
|
59
|
+
- Validate **from** against a verified domain before sending — Resend rejects unverified senders.
|
|
60
|
+
- Keep subject lines under 80 characters for better deliverability.
|
|
61
|
+
- For batch sends, group by shared content to reduce payload size.
|
|
62
|
+
- Always check the response for errors and surface them to the user rather than silently failing.
|
|
63
|
+
- When composing HTML emails, keep the markup simple — most email clients ignore complex CSS.
|
|
64
|
+
|
|
65
|
+
## Available operations
|
|
66
|
+
|
|
67
|
+
- **Send Email** — Single email with HTML/text body, attachments, CC/BCC, reply-to
|
|
68
|
+
- **Batch Send** — Multiple emails in one call
|
|
69
|
+
- **Schedule Email** — Queue an email for future delivery
|
|
70
|
+
- **List/Get Emails** — Check delivery status of sent emails
|
|
71
|
+
- **Cancel Email** — Cancel a scheduled email before it sends
|
|
72
|
+
- **Manage Contacts** — Create, list, update, and remove contacts from audiences
|
|
73
|
+
- **Manage Domains** — Add and verify sender domains
|
|
74
|
+
|
|
75
|
+
## Setup
|
|
76
|
+
|
|
77
|
+
1. Create a free account at [resend.com](https://resend.com)
|
|
78
|
+
2. Generate an API key (starts with `re_`)
|
|
79
|
+
3. Add a verified sender domain (or use Resend's shared `onboarding@resend.dev` for testing)
|
|
80
|
+
4. Set `RESEND_API_KEY` in your `.env` file
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: template-designer
|
|
3
|
+
description: Visual template selection for image design agents. Generates template variations, renders them as images for user review, and saves the approved visual identity.
|
|
4
|
+
description_pt-BR: >
|
|
5
|
+
Seleção de template visual para agentes de design de imagem. Gera variações de template,
|
|
6
|
+
renderiza como imagens para o usuário revisar e salva a identidade visual aprovada.
|
|
7
|
+
description_es: >
|
|
8
|
+
Selección de plantilla visual para agentes de diseño de imagen. Genera variaciones de
|
|
9
|
+
plantilla, las renderiza como imágenes para revisión del usuario y guarda la identidad
|
|
10
|
+
visual aprobada.
|
|
11
|
+
type: prompt
|
|
12
|
+
version: "2.0.0"
|
|
13
|
+
categories: [design, visual, templates]
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Template Designer
|
|
17
|
+
|
|
18
|
+
Visual template selection and refinement for crew creation and editing.
|
|
19
|
+
|
|
20
|
+
## When to Use
|
|
21
|
+
|
|
22
|
+
- During crew creation: when the Design phase identifies an image design agent and the user opts to choose a template
|
|
23
|
+
- During crew editing: when the user asks to define, edit, or change the visual identity / template of a design agent
|
|
24
|
+
- Trigger: presence of `image-creator` skill (or similar image-producing skill) in the crew's skill list
|
|
25
|
+
|
|
26
|
+
## Prerequisites
|
|
27
|
+
|
|
28
|
+
- A crew with a design agent that produces images (uses `image-creator` skill)
|
|
29
|
+
- Crew's `_build/` directory must exist (created during Discovery/Design phases)
|
|
30
|
+
- `image-creator` skill installed (for rendering HTML to PNG)
|
|
31
|
+
|
|
32
|
+
## How It Works
|
|
33
|
+
|
|
34
|
+
1. You read context and base templates
|
|
35
|
+
2. You generate 3 adapted HTML template variations
|
|
36
|
+
3. You render each as a PNG image using the `image-creator` skill
|
|
37
|
+
4. You present the image file paths to the user for review
|
|
38
|
+
5. You iterate with feedback until approval
|
|
39
|
+
6. You save the approved template as HTML reference + structured style rules
|
|
40
|
+
|
|
41
|
+
## Generating Templates
|
|
42
|
+
|
|
43
|
+
### Step 0: Read Design Guidelines (MANDATORY)
|
|
44
|
+
|
|
45
|
+
Before generating any template, read and internalize the design best practices:
|
|
46
|
+
- `_opencrew/core/best-practices/image-design.md` — **REQUIRED reading**. Contains platform-specific minimum font sizes, typography rules, spacing guidelines, color palette constraints, contrast requirements, and layout methodology. Every template you generate MUST comply with these rules.
|
|
47
|
+
|
|
48
|
+
Key rules to always follow:
|
|
49
|
+
- **Font sizes**: Hero 58px, Heading 43px, Body 34px, Caption 24px minimum for Instagram carousel (1080x1440). Absolute minimum 20px for any readable text on any platform.
|
|
50
|
+
- **Font weight**: 500 or higher for body text and above.
|
|
51
|
+
- **Colors**: Maximum 5 colors per design system (primary, secondary, accent, background, text).
|
|
52
|
+
- **Contrast**: WCAG AA minimum 4.5:1 for all text against background.
|
|
53
|
+
- **Layout**: CSS Grid or Flexbox only. No absolute positioning for primary content.
|
|
54
|
+
- **Self-contained HTML**: Inline CSS only. Only Google Fonts @import allowed as external resource.
|
|
55
|
+
- **No slide counters**: Never include "1/7" or similar. Instagram has native navigation.
|
|
56
|
+
|
|
57
|
+
You should also apply general web design best practices: proper white space, visual hierarchy through scale and weight, consistent spacing rhythm, and balanced composition.
|
|
58
|
+
|
|
59
|
+
### HARD RULES — Dimensions and Typography
|
|
60
|
+
|
|
61
|
+
These rules are NON-NEGOTIABLE. Every template must comply:
|
|
62
|
+
|
|
63
|
+
**Fixed Dimensions (never use height: auto or flexible height):**
|
|
64
|
+
- Instagram Carousel: `width: 1080px; height: 1440px` (3:4 portrait)
|
|
65
|
+
- Instagram Story/Reel: `width: 1080px; height: 1920px` (9:16 portrait)
|
|
66
|
+
- Instagram Post: `width: 1080px; height: 1080px` (1:1 square)
|
|
67
|
+
- LinkedIn Post: `width: 1200px; height: 627px` (1.91:1 horizontal)
|
|
68
|
+
|
|
69
|
+
The root container of every template MUST set explicit `width` and `height` in pixels. The template must render at exactly these dimensions — no overflow, no scrolling, no flexible height.
|
|
70
|
+
|
|
71
|
+
**Minimum Font Sizes (Instagram at 1080px width):**
|
|
72
|
+
- Hero/Title: **58px** minimum
|
|
73
|
+
- Heading: **43px** minimum
|
|
74
|
+
- Body text: **34px** minimum
|
|
75
|
+
- Caption/small text: **24px** minimum
|
|
76
|
+
- Absolute minimum for ANY readable text on ANY platform: **20px**
|
|
77
|
+
|
|
78
|
+
**Font Weight:** 500 or higher for body text and above. Never use font-weight below 400 for any visible text.
|
|
79
|
+
|
|
80
|
+
Templates that violate these rules are rejected — no exceptions.
|
|
81
|
+
|
|
82
|
+
### Step 1: Read Context
|
|
83
|
+
|
|
84
|
+
Read these files to understand the crew:
|
|
85
|
+
- `crews/{code}/_build/discovery.yaml` — platform, domain, tone, language
|
|
86
|
+
- `crews/{code}/_build/design.yaml` — agents, purpose, skills
|
|
87
|
+
- `crews/{code}/_investigations/consolidated-analysis.md` (if exists) — visual patterns from reference profiles
|
|
88
|
+
- `_opencrew/_memory/company.md` — company name, brand, industry, target audience
|
|
89
|
+
- `_opencrew/_memory/preferences.md` — user preferences (language, style, tone)
|
|
90
|
+
|
|
91
|
+
Use the company context and user preferences to adapt template content: example text should reflect the company's domain and audience, colors should align with brand if available, and language should match the user's Output Language preference.
|
|
92
|
+
|
|
93
|
+
### Step 2: Read Base Templates
|
|
94
|
+
|
|
95
|
+
Read the 3 base templates from `skills/template-designer/base-templates/`:
|
|
96
|
+
- `model-a.html`
|
|
97
|
+
- `model-b.html`
|
|
98
|
+
- `model-c.html`
|
|
99
|
+
|
|
100
|
+
### Step 3: Generate Adapted Variations
|
|
101
|
+
|
|
102
|
+
For each base template, create an adapted version:
|
|
103
|
+
- Adjust colors to match the crew's domain/brand (use Sherlock palette if available, company brand colors from company.md if available)
|
|
104
|
+
- Adjust typography following the platform-specific minimum font sizes from `image-design.md`
|
|
105
|
+
- Replace example content with domain-relevant content that reflects the company's industry, audience, and language
|
|
106
|
+
- Set the root container to the exact fixed dimensions from HARD RULES above. Never use percentage heights or auto heights.
|
|
107
|
+
- Add any visual elements that match the crew's personality
|
|
108
|
+
- Apply proper white space, visual hierarchy, and spacing rhythm per `image-design.md` methodology
|
|
109
|
+
|
|
110
|
+
Write each adapted template as a **complete, self-contained HTML file** (with `<!DOCTYPE html>`, inline CSS, and Google Fonts imports if needed).
|
|
111
|
+
|
|
112
|
+
Save to:
|
|
113
|
+
- `crews/{code}/_build/template-a.html`
|
|
114
|
+
- `crews/{code}/_build/template-b.html`
|
|
115
|
+
- `crews/{code}/_build/template-c.html`
|
|
116
|
+
|
|
117
|
+
### Step 4: Render as Images
|
|
118
|
+
|
|
119
|
+
Use the `image-creator` skill to render each HTML template as a PNG image:
|
|
120
|
+
|
|
121
|
+
1. Read `skills/image-creator/SKILL.md` for rendering instructions
|
|
122
|
+
2. Render each template HTML to PNG using the image-creator workflow
|
|
123
|
+
3. Save rendered images to:
|
|
124
|
+
- `crews/{code}/_build/template-a.png`
|
|
125
|
+
- `crews/{code}/_build/template-b.png`
|
|
126
|
+
- `crews/{code}/_build/template-c.png`
|
|
127
|
+
|
|
128
|
+
### Step 5: Present to User
|
|
129
|
+
|
|
130
|
+
Present the 3 template options to the user using **clickable markdown links with absolute file paths** so they can open and review:
|
|
131
|
+
|
|
132
|
+
> "Here are 3 template options for your crew's visual identity:
|
|
133
|
+
>
|
|
134
|
+
> - [preview-a.png]({absolute_path}/crews/{code}/_build/template-a.png) — Template A
|
|
135
|
+
> - [preview-b.png]({absolute_path}/crews/{code}/_build/template-b.png) — Template B
|
|
136
|
+
> - [preview-c.png]({absolute_path}/crews/{code}/_build/template-c.png) — Template C
|
|
137
|
+
>
|
|
138
|
+
> Click the links above to open each image. Tell me which one you prefer. I can also mix elements from different templates or adjust colors, fonts, and layout."
|
|
139
|
+
|
|
140
|
+
**Important:** Always use the full absolute path (e.g., `d:\Coding Projects\opencrew\crews\my-crew\_build\template-a.png`) inside the markdown link — relative paths are not clickable in the IDE.
|
|
141
|
+
|
|
142
|
+
## Iteration Loop
|
|
143
|
+
|
|
144
|
+
1. Present template images with clickable markdown links using absolute file paths
|
|
145
|
+
2. Wait for user feedback in terminal
|
|
146
|
+
3. Generate new version based on feedback — save as `template-v2.html`, render as `template-v2.png`, etc.
|
|
147
|
+
4. Present updated image with clickable markdown link using absolute file path
|
|
148
|
+
5. Repeat until user approves
|
|
149
|
+
|
|
150
|
+
## Saving the Approved Template
|
|
151
|
+
|
|
152
|
+
When the user approves, create two files:
|
|
153
|
+
|
|
154
|
+
### 1. Template Reference HTML
|
|
155
|
+
|
|
156
|
+
Save to: `crews/{code}/pipeline/data/template-reference.html`
|
|
157
|
+
|
|
158
|
+
The complete, self-contained HTML/CSS of the approved template at full resolution (e.g., 1080x1440). This is the literal example the design agent will use.
|
|
159
|
+
|
|
160
|
+
### 2. Visual Identity Rules
|
|
161
|
+
|
|
162
|
+
Save to: `crews/{code}/pipeline/data/visual-identity.md`
|
|
163
|
+
|
|
164
|
+
Extract structured rules from the approved template:
|
|
165
|
+
|
|
166
|
+
~~~markdown
|
|
167
|
+
# Visual Identity
|
|
168
|
+
|
|
169
|
+
## Color Palette
|
|
170
|
+
- **Primary:** #HEXCODE — usage description
|
|
171
|
+
- **Secondary:** #HEXCODE — usage description
|
|
172
|
+
- **Background:** #HEXCODE
|
|
173
|
+
- **Text:** #HEXCODE
|
|
174
|
+
- **Accent:** #HEXCODE — usage description
|
|
175
|
+
|
|
176
|
+
## Typography
|
|
177
|
+
- **Headings:** Font Family, weight, size range
|
|
178
|
+
- **Body:** Font Family, weight, size range
|
|
179
|
+
- **Caption:** Font Family, weight, size range
|
|
180
|
+
- **Minimum sizes:** body 32px, caption 24px, heading 48px
|
|
181
|
+
|
|
182
|
+
## Layout
|
|
183
|
+
- **Viewport:** WIDTHxHEIGHT px
|
|
184
|
+
- **Padding:** value
|
|
185
|
+
- **Grid:** description
|
|
186
|
+
- **Spacing rules:** description
|
|
187
|
+
|
|
188
|
+
## Composition Rules
|
|
189
|
+
- Logo/profile placement: description
|
|
190
|
+
- Image treatment: description
|
|
191
|
+
- Visual hierarchy: description
|
|
192
|
+
- Footer/CTA pattern: description
|
|
193
|
+
|
|
194
|
+
## Adaptation Rules
|
|
195
|
+
- How to handle different viewport sizes
|
|
196
|
+
- What stays fixed vs. what adapts
|
|
197
|
+
- Color usage rules (when to use primary vs accent)
|
|
198
|
+
~~~
|
|
199
|
+
|
|
200
|
+
### 3. Update Crew Files
|
|
201
|
+
|
|
202
|
+
If the crew is being created (Build phase hasn't run yet):
|
|
203
|
+
- The design.yaml context now includes the template data — Build will pick it up
|
|
204
|
+
|
|
205
|
+
If the crew already exists (editing flow):
|
|
206
|
+
- Add `pipeline/data/template-reference.html` and `pipeline/data/visual-identity.md` to `crew.yaml` `data:` list
|
|
207
|
+
- Update the design agent's `.agent.md` to reference both files
|
|
208
|
+
- Update the design agent's tasks to include the rule: "always follow visual-identity.md and use template-reference.html as the base model"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<!-- Model A: Twitter Editorial — approved 2026-03-28 -->
|
|
2
|
+
<!-- Style: Black background, tweet-style layout with avatar, verified badge, editorial text, contextual image -->
|
|
3
|
+
<!-- Fonts: Inter (body), weight 400-700 -->
|
|
4
|
+
<!-- Colors: #000 bg, #fff text, #1D9BF0 verified, #71767B muted, #2a2a2a borders/avatar, #1a1a2e image gradient -->
|
|
5
|
+
<div style="width:1080px;height:1440px;background:#000;color:#fff;font-family:'Inter',sans-serif;display:flex;flex-direction:column;padding:72px;gap:0;position:relative;overflow:hidden;">
|
|
6
|
+
<!-- Tweet header -->
|
|
7
|
+
<div style="display:flex;gap:24px;align-items:center;">
|
|
8
|
+
<div style="width:80px;height:80px;border-radius:50%;background:#2a2a2a;flex-shrink:0;"></div>
|
|
9
|
+
<div>
|
|
10
|
+
<div style="display:flex;align-items:center;gap:8px;">
|
|
11
|
+
<span style="font-weight:700;font-size:34px;">Nome Sobrenome</span>
|
|
12
|
+
<svg width="28" height="28" viewBox="0 0 24 24" fill="#1D9BF0"><path d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.34 2.19c-1.39-.46-2.9-.2-3.91.81s-1.27 2.52-.81 3.91C2.63 9.33 1.75 10.57 1.75 12s.88 2.67 2.19 3.34c-.46 1.39-.2 2.9.81 3.91s2.52 1.27 3.91.81c.66 1.31 1.91 2.19 3.34 2.19s2.67-.88 3.34-2.19c1.39.46 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34zm-11.08 4.71c-.22.22-.58.22-.8 0L7.4 13.74c-.22-.22-.22-.58 0-.8.22-.22.58-.22.8 0l2.57 2.57 6.03-6.03c.22-.22.58-.22.8 0 .22.22.22.58 0 .8l-6.43 6.43z"/></svg>
|
|
13
|
+
</div>
|
|
14
|
+
<span style="font-size:28px;color:#71767B;">@username</span>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
<!-- Tweet text -->
|
|
18
|
+
<div style="font-size:38px;line-height:1.45;font-weight:400;margin-top:48px;">
|
|
19
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
|
|
20
|
+
<br><br>
|
|
21
|
+
Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit. Neque porro quisquam est qui dolorem.
|
|
22
|
+
</div>
|
|
23
|
+
<!-- Image -->
|
|
24
|
+
<div style="width:100%;height:35%;background:linear-gradient(180deg,#1a1a2e,#0d0d0d);border-radius:24px;display:flex;align-items:center;justify-content:center;border:1px solid #2a2a2a;flex-shrink:0;margin-top:auto;">
|
|
25
|
+
<span style="color:#555;font-size:34px;">Imagem contextual</span>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|