@julioventura/opensquad 0.1.17
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/README.md +433 -0
- package/_opensquad/config/playwright.config.json +11 -0
- package/_opensquad/core/architect.agent.yaml +112 -0
- package/_opensquad/core/best-practices/_catalog.yaml +126 -0
- package/_opensquad/core/best-practices/blog-post.md +132 -0
- package/_opensquad/core/best-practices/blog-seo.md +127 -0
- package/_opensquad/core/best-practices/brand-resolution-checklist.md +172 -0
- package/_opensquad/core/best-practices/copywriting.md +441 -0
- package/_opensquad/core/best-practices/data-analysis.md +401 -0
- package/_opensquad/core/best-practices/email-newsletter.md +118 -0
- package/_opensquad/core/best-practices/email-sales.md +110 -0
- package/_opensquad/core/best-practices/image-design.md +348 -0
- package/_opensquad/core/best-practices/instagram-feed.md +235 -0
- package/_opensquad/core/best-practices/instagram-reels.md +112 -0
- package/_opensquad/core/best-practices/instagram-stories.md +107 -0
- package/_opensquad/core/best-practices/linkedin-article.md +116 -0
- package/_opensquad/core/best-practices/linkedin-post.md +121 -0
- package/_opensquad/core/best-practices/researching.md +349 -0
- package/_opensquad/core/best-practices/review.md +269 -0
- package/_opensquad/core/best-practices/run-recovery.md +61 -0
- package/_opensquad/core/best-practices/social-networks-publishing.md +327 -0
- package/_opensquad/core/best-practices/squad-creation-checklist.md +32 -0
- package/_opensquad/core/best-practices/strategist.md +344 -0
- package/_opensquad/core/best-practices/technical-writing.md +365 -0
- package/_opensquad/core/best-practices/twitter-post.md +105 -0
- package/_opensquad/core/best-practices/twitter-thread.md +122 -0
- package/_opensquad/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_opensquad/core/best-practices/youtube-script.md +122 -0
- package/_opensquad/core/best-practices/youtube-shorts.md +112 -0
- package/_opensquad/core/defaults/youtube-video-assembly.json +84 -0
- package/_opensquad/core/prompts/build.prompt.md +613 -0
- package/_opensquad/core/prompts/design.prompt.md +606 -0
- package/_opensquad/core/prompts/discovery.prompt.md +377 -0
- package/_opensquad/core/prompts/sherlock-instagram.md +123 -0
- package/_opensquad/core/prompts/sherlock-linkedin.md +73 -0
- package/_opensquad/core/prompts/sherlock-shared.md +684 -0
- package/_opensquad/core/prompts/sherlock-twitter.md +78 -0
- package/_opensquad/core/prompts/sherlock-youtube.md +85 -0
- package/_opensquad/core/runner.pipeline.md +743 -0
- package/_opensquad/core/skills.engine.md +384 -0
- package/bin/opensquad.js +108 -0
- package/dashboard/index.html +15 -0
- package/dashboard/package-lock.json +1964 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/RunDashboardButton.tsx +92 -0
- package/dashboard/src/components/SquadCard.tsx +49 -0
- package/dashboard/src/components/SquadSelector.tsx +67 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +116 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +397 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +59 -0
- package/public/sfx/slide-transition-sfx.mp3 +0 -0
- package/skills/README.md +84 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/higgsfield/SKILL.md +147 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +166 -0
- package/skills/image-creator/editorial-slide-template.js +645 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/imgbb-uploader/SKILL.md +73 -0
- package/skills/imgbb-uploader/scripts/upload.js +125 -0
- package/skills/instagram-publisher/README.md +36 -0
- package/skills/instagram-publisher/SKILL.md +231 -0
- package/skills/instagram-publisher/scripts/publish-playwright.js +418 -0
- package/skills/instagram-publisher/scripts/publish.js +521 -0
- package/skills/opensquad-agent-creator/SKILL.md +192 -0
- package/skills/opensquad-skill-creator/SKILL.md +420 -0
- package/skills/opensquad-skill-creator/agents/analyzer.md +274 -0
- package/skills/opensquad-skill-creator/agents/comparator.md +202 -0
- package/skills/opensquad-skill-creator/agents/grader.md +223 -0
- package/skills/opensquad-skill-creator/assets/eval_review.html +146 -0
- package/skills/opensquad-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/opensquad-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/opensquad-skill-creator/references/schemas.md +430 -0
- package/skills/opensquad-skill-creator/references/skill-format.md +235 -0
- package/skills/opensquad-skill-creator/scripts/__init__.py +0 -0
- package/skills/opensquad-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/opensquad-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/opensquad-skill-creator/scripts/run_eval.py +310 -0
- package/skills/opensquad-skill-creator/scripts/utils.py +47 -0
- package/skills/pdf-extractor/SKILL.md +57 -0
- package/skills/pdf-extractor/scripts/extract.py +82 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/run-dashboard/README.md +93 -0
- package/skills/run-dashboard/SKILL.md +173 -0
- package/skills/run-dashboard/scripts/finalize-state.js +273 -0
- package/skills/run-dashboard/scripts/generate.js +1296 -0
- package/skills/run-dashboard/scripts/serve.js +135 -0
- package/skills/run-dashboard/templates/run-dashboard-simple.template.html +191 -0
- package/skills/run-dashboard/templates/run-dashboard.template.html +1164 -0
- package/skills/smtp-sender/SKILL.md +88 -0
- package/skills/smtp-sender/scripts/send.js +478 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/skills/youtube-publisher/SKILL.md +232 -0
- package/skills/youtube-publisher/scripts/publish.js +2078 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +442 -0
- package/src/locales/en.json +79 -0
- package/src/locales/es.json +78 -0
- package/src/locales/pt-BR.json +78 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +146 -0
- package/src/runs.js +318 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/supabase-cli.js +584 -0
- package/src/update.js +169 -0
- package/templates/_opensquad/.opensquad-version +1 -0
- package/templates/_opensquad/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/opensquad.md +68 -0
- package/templates/ide-templates/antigravity/.agent/workflows/opensquad.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +57 -0
- package/templates/ide-templates/codex/.agents/skills/opensquad/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +120 -0
- package/templates/ide-templates/cursor/.cursor/commands/opensquad.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/opensquad.mdc +62 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/gemini-cli/.gemini/settings.json +8 -0
- package/templates/ide-templates/gemini-cli/.gemini/skills/opensquad/SKILL.md +186 -0
- package/templates/ide-templates/gemini-cli/GEMINI.md +57 -0
- package/templates/ide-templates/opencode/.opencode/commands/opensquad.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +120 -0
- package/templates/ide-templates/qwen-code/.qwen/settings.json +8 -0
- package/templates/ide-templates/qwen-code/.qwen/skills/opensquad/SKILL.md +182 -0
- package/templates/ide-templates/qwen-code/QWEN.md +57 -0
- package/templates/ide-templates/trae/.trae/mcp.json +8 -0
- package/templates/ide-templates/trae/.trae/rules/opensquad.md +64 -0
- package/templates/ide-templates/vscode-copilot/.github/copilot-instructions.md +59 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/opensquad.prompt.md +209 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -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,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pdf-extractor
|
|
3
|
+
description: >
|
|
4
|
+
Downloads a PDF from a URL or reads it from a local file, extracts the text using Python's pypdf library, and outputs it to a text file.
|
|
5
|
+
Useful for analyzing research papers, articles, reports, and other document assets in PDF format.
|
|
6
|
+
description_pt-BR: >
|
|
7
|
+
Baixa um PDF a partir de uma URL ou o lê de um arquivo local, extrai o texto usando a biblioteca pypdf do Python e o salva em um arquivo de texto.
|
|
8
|
+
Útil para analisar artigos científicos, relatórios, contratos e outros documentos em formato PDF.
|
|
9
|
+
description_es: >
|
|
10
|
+
Descarga un PDF de una URL o lo lee de un archivo local, extrae el texto usando la biblioteca pypdf de Python y lo guarda en un archivo de texto.
|
|
11
|
+
Útil para analizar artículos científicos, informes, contratos y otros documentos en formato PDF.
|
|
12
|
+
type: script
|
|
13
|
+
version: "1.0.0"
|
|
14
|
+
script:
|
|
15
|
+
path: scripts/extract.py
|
|
16
|
+
runtime: python
|
|
17
|
+
dependencies:
|
|
18
|
+
- pypdf
|
|
19
|
+
invoke: "python {skill_path}/scripts/extract.py --source \"{source}\" --output \"{output}\""
|
|
20
|
+
categories: [scraping, data, text, documents]
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# PDF Extractor
|
|
24
|
+
|
|
25
|
+
## When to use
|
|
26
|
+
|
|
27
|
+
Use the PDF Extractor skill when you need to extract and analyze text from a PDF file. The PDF file can be located online (a web URL starting with `http://` or `https://`) or stored locally in your workspace.
|
|
28
|
+
|
|
29
|
+
## Instructions
|
|
30
|
+
|
|
31
|
+
### Single PDF Text Extraction
|
|
32
|
+
|
|
33
|
+
Run the script by providing the PDF path (local or remote URL) and the desired output text file path:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
python skills/pdf-extractor/scripts/extract.py \
|
|
37
|
+
--source "https://example.com/document.pdf" \
|
|
38
|
+
--output "squads/{squad}/output/{run_id}/document-text.txt"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For a local PDF:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
python skills/pdf-extractor/scripts/extract.py \
|
|
45
|
+
--source "squads/{squad}/reference/article.pdf" \
|
|
46
|
+
--output "squads/{squad}/output/{run_id}/article-text.txt"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Available operations
|
|
50
|
+
|
|
51
|
+
- **Web PDF Extraction** -- Download a PDF from any public URL and extract its text
|
|
52
|
+
- **Local PDF Extraction** -- Extract text from any PDF stored in the local file system
|
|
53
|
+
- **Clean Formatting** -- Strip excess spacing, control characters, and output clean plaintext
|
|
54
|
+
|
|
55
|
+
## Error handling
|
|
56
|
+
|
|
57
|
+
- If the URL is inaccessible or invalid, the script will exit with an error. Verify that the URL is public and does not require authentication.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import sys
|
|
3
|
+
import os
|
|
4
|
+
import argparse
|
|
5
|
+
import urllib.request
|
|
6
|
+
import tempfile
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
parser = argparse.ArgumentParser(description="Extract text from a PDF file (local or web URL).")
|
|
10
|
+
parser.add_argument("--source", required=True, help="Path or URL to the PDF file.")
|
|
11
|
+
parser.add_argument("--output", required=True, help="Path to write the extracted text.")
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
|
|
14
|
+
# Verify pypdf installation
|
|
15
|
+
try:
|
|
16
|
+
from pypdf import PdfReader
|
|
17
|
+
except ImportError:
|
|
18
|
+
print("[ERROR] The 'pypdf' library is not installed. Please run: pip install pypdf", file=sys.stderr)
|
|
19
|
+
sys.exit(1)
|
|
20
|
+
|
|
21
|
+
source = args.source
|
|
22
|
+
output_path = args.output
|
|
23
|
+
temp_file_path = None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Check if source is a URL
|
|
27
|
+
if source.lower().startswith(("http://", "https://")):
|
|
28
|
+
print(f"[INFO] Downloading PDF from: {source}...")
|
|
29
|
+
# Use urllib with a standard User-Agent header to avoid 403 Forbidden blocks
|
|
30
|
+
req = urllib.request.Request(
|
|
31
|
+
source,
|
|
32
|
+
headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
|
|
33
|
+
)
|
|
34
|
+
with urllib.request.urlopen(req) as response:
|
|
35
|
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
|
|
36
|
+
tmp_file.write(response.read())
|
|
37
|
+
temp_file_path = tmp_file.name
|
|
38
|
+
pdf_path = temp_file_path
|
|
39
|
+
else:
|
|
40
|
+
pdf_path = source
|
|
41
|
+
if not os.path.exists(pdf_path):
|
|
42
|
+
raise FileNotFoundError(f"Local PDF file not found: {pdf_path}")
|
|
43
|
+
|
|
44
|
+
print(f"[INFO] Extracting text from: {pdf_path}...")
|
|
45
|
+
reader = PdfReader(pdf_path)
|
|
46
|
+
num_pages = len(reader.pages)
|
|
47
|
+
print(f"[INFO] Found {num_pages} pages.")
|
|
48
|
+
|
|
49
|
+
extracted_text = []
|
|
50
|
+
for i, page in enumerate(reader.pages):
|
|
51
|
+
text = page.extract_text()
|
|
52
|
+
if text:
|
|
53
|
+
extracted_text.append(f"--- PAGE {i + 1} ---\n{text}\n")
|
|
54
|
+
else:
|
|
55
|
+
extracted_text.append(f"--- PAGE {i + 1} ---\n[No text extracted from this page]\n")
|
|
56
|
+
|
|
57
|
+
full_text = "\n".join(extracted_text)
|
|
58
|
+
|
|
59
|
+
# Ensure output directory exists
|
|
60
|
+
output_dir = os.path.dirname(os.path.abspath(output_path))
|
|
61
|
+
if output_dir:
|
|
62
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
65
|
+
f.write(full_text)
|
|
66
|
+
|
|
67
|
+
print(f"[SUCCESS] Extraction complete! Character count: {len(full_text)}")
|
|
68
|
+
print(f"[SUCCESS] Saved to: {output_path}")
|
|
69
|
+
|
|
70
|
+
except Exception as e:
|
|
71
|
+
print(f"[ERROR] Error extracting PDF: {e}", file=sys.stderr)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
finally:
|
|
74
|
+
# Clean up temporary file
|
|
75
|
+
if temp_file_path and os.path.exists(temp_file_path):
|
|
76
|
+
try:
|
|
77
|
+
os.remove(temp_file_path)
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
if __name__ == "__main__":
|
|
82
|
+
main()
|
|
@@ -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 squad needs to send emails — welcome messages, notifications,
|
|
32
|
+
reports, newsletters, or any transactional/marketing email. Resend handles delivery
|
|
33
|
+
so the squad 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,93 @@
|
|
|
1
|
+
# Run Dashboard Skill
|
|
2
|
+
|
|
3
|
+
This skill generates a `DASHBOARD-RUN` HTML snapshot for a completed run and a sibling JSON snapshot used by the refresh button.
|
|
4
|
+
|
|
5
|
+
## Files produced
|
|
6
|
+
|
|
7
|
+
- `run-dashboard.html` — standalone editable HTML with inline CSS and JavaScript
|
|
8
|
+
- `run-dashboard.data.json` — structured snapshot with checklist, links, artifacts, errors, and metrics
|
|
9
|
+
|
|
10
|
+
The HTML branding should read `DASHBOARD-RUN`.
|
|
11
|
+
|
|
12
|
+
## Static website publish
|
|
13
|
+
|
|
14
|
+
When a site has an Apache/Nginx folder such as `/run-dashboard/`, you can publish the current snapshot there with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
node --env-file=.env skills/run-dashboard/scripts/generate.js \
|
|
18
|
+
--workspace-root . \
|
|
19
|
+
--run-dir squads/musicplay-club/output/2026-05-19-020010 \
|
|
20
|
+
--publish-static
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Prefer a concrete run folder for dashboard generation. When another tool needs versioned artifacts, pass `.../output/<run-id>/v1`; the dashboard generator should still point to `.../output/<run-id>`.
|
|
24
|
+
|
|
25
|
+
With `dashboard.static_publish.publish_dir_env` configured in the squad `channel-config.yaml`, the command writes:
|
|
26
|
+
|
|
27
|
+
- `index.html` as the latest public snapshot
|
|
28
|
+
- `<run-id>.html` as the archived snapshot
|
|
29
|
+
- `youtube.html` as the latest OG share/redirect page for the published YouTube run link (when present)
|
|
30
|
+
- `<run-id>-youtube.html` as the archived OG share/redirect page for that run
|
|
31
|
+
- copied supporting files under `assets/latest/` and `assets/<run-id>/`
|
|
32
|
+
|
|
33
|
+
The published page is static and can be opened directly by visitors without the local `serve.js` helper.
|
|
34
|
+
|
|
35
|
+
If the squad config uses `transport: ftp`, the same command uploads the snapshot to the remote server instead of writing only to a local folder.
|
|
36
|
+
|
|
37
|
+
Operational preflight for static publish:
|
|
38
|
+
|
|
39
|
+
- `dashboard.static_publish.publish_dir_env` must resolve to a real target directory or remote folder
|
|
40
|
+
- when transport is `ftp` or `both`, the env keys referenced by `dashboard.static_publish.ftp.host_env`, `port_env`, `user_env`, and `pass_env` must be present before finalization
|
|
41
|
+
|
|
42
|
+
If these env vars are missing, treat the failure as configuration drift in the squad/channel setup, not as a defect in the dashboard HTML generator.
|
|
43
|
+
|
|
44
|
+
## Live refresh server
|
|
45
|
+
|
|
46
|
+
For a real metric refresh from inside the HTML, serve the run folder with the local dashboard server:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
node skills/run-dashboard/scripts/serve.js --run-dir squads/your-squad/output/2026-05-19-020010 --workspace-root .
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Then open `http://127.0.0.1:4319/run-dashboard.html`.
|
|
53
|
+
|
|
54
|
+
In this mode, the `Refresh Data` button regenerates `run-dashboard.data.json` on demand before re-rendering the page.
|
|
55
|
+
|
|
56
|
+
## Data sources
|
|
57
|
+
|
|
58
|
+
The generator reads the run folder and, when present, uses:
|
|
59
|
+
|
|
60
|
+
- `state.json`
|
|
61
|
+
- `publish-result.md`
|
|
62
|
+
- `editorial-plan.md`
|
|
63
|
+
- `research-brief.md`
|
|
64
|
+
- `content-package.md`
|
|
65
|
+
- `images.md`
|
|
66
|
+
- `review.md`
|
|
67
|
+
- `publish-config.md`
|
|
68
|
+
- squad `pipeline/data/channel-config.yaml`
|
|
69
|
+
|
|
70
|
+
## Metrics currently attempted
|
|
71
|
+
|
|
72
|
+
- Instagram: post ID, like count, and the first available primary reach/view metric
|
|
73
|
+
- Facebook: post ID, reactions, comments, shares when the Page token can be resolved
|
|
74
|
+
- YouTube: video ID, views, likes, comments, and publish date when OAuth credentials are available
|
|
75
|
+
|
|
76
|
+
When credentials or API permissions are missing, the dashboard keeps the operational snapshot and shows `N/A` instead of failing.
|
|
77
|
+
|
|
78
|
+
## Visual preview support
|
|
79
|
+
|
|
80
|
+
When the run folder contains rendered slides in `images/` and a YouTube thumbnail in `thumbs/`, the dashboard shows a preview grid with:
|
|
81
|
+
|
|
82
|
+
- slide images linked to their editable `.html` sources when available
|
|
83
|
+
- thumbnail preview linked to the published YouTube URL when available
|
|
84
|
+
|
|
85
|
+
## Supabase naming convention
|
|
86
|
+
|
|
87
|
+
Supabase is optional. If future persistence is added for dashboard snapshots, templates, or profile data, all Opensquad-managed tables must use the `squad_` prefix.
|
|
88
|
+
|
|
89
|
+
Examples:
|
|
90
|
+
|
|
91
|
+
- `squad_profiles`
|
|
92
|
+
- `squad_run_reports`
|
|
93
|
+
- `squad_dashboard_templates`
|