@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.
Files changed (247) hide show
  1. package/README.md +433 -0
  2. package/_opensquad/config/playwright.config.json +11 -0
  3. package/_opensquad/core/architect.agent.yaml +112 -0
  4. package/_opensquad/core/best-practices/_catalog.yaml +126 -0
  5. package/_opensquad/core/best-practices/blog-post.md +132 -0
  6. package/_opensquad/core/best-practices/blog-seo.md +127 -0
  7. package/_opensquad/core/best-practices/brand-resolution-checklist.md +172 -0
  8. package/_opensquad/core/best-practices/copywriting.md +441 -0
  9. package/_opensquad/core/best-practices/data-analysis.md +401 -0
  10. package/_opensquad/core/best-practices/email-newsletter.md +118 -0
  11. package/_opensquad/core/best-practices/email-sales.md +110 -0
  12. package/_opensquad/core/best-practices/image-design.md +348 -0
  13. package/_opensquad/core/best-practices/instagram-feed.md +235 -0
  14. package/_opensquad/core/best-practices/instagram-reels.md +112 -0
  15. package/_opensquad/core/best-practices/instagram-stories.md +107 -0
  16. package/_opensquad/core/best-practices/linkedin-article.md +116 -0
  17. package/_opensquad/core/best-practices/linkedin-post.md +121 -0
  18. package/_opensquad/core/best-practices/researching.md +349 -0
  19. package/_opensquad/core/best-practices/review.md +269 -0
  20. package/_opensquad/core/best-practices/run-recovery.md +61 -0
  21. package/_opensquad/core/best-practices/social-networks-publishing.md +327 -0
  22. package/_opensquad/core/best-practices/squad-creation-checklist.md +32 -0
  23. package/_opensquad/core/best-practices/strategist.md +344 -0
  24. package/_opensquad/core/best-practices/technical-writing.md +365 -0
  25. package/_opensquad/core/best-practices/twitter-post.md +105 -0
  26. package/_opensquad/core/best-practices/twitter-thread.md +122 -0
  27. package/_opensquad/core/best-practices/whatsapp-broadcast.md +107 -0
  28. package/_opensquad/core/best-practices/youtube-script.md +122 -0
  29. package/_opensquad/core/best-practices/youtube-shorts.md +112 -0
  30. package/_opensquad/core/defaults/youtube-video-assembly.json +84 -0
  31. package/_opensquad/core/prompts/build.prompt.md +613 -0
  32. package/_opensquad/core/prompts/design.prompt.md +606 -0
  33. package/_opensquad/core/prompts/discovery.prompt.md +377 -0
  34. package/_opensquad/core/prompts/sherlock-instagram.md +123 -0
  35. package/_opensquad/core/prompts/sherlock-linkedin.md +73 -0
  36. package/_opensquad/core/prompts/sherlock-shared.md +684 -0
  37. package/_opensquad/core/prompts/sherlock-twitter.md +78 -0
  38. package/_opensquad/core/prompts/sherlock-youtube.md +85 -0
  39. package/_opensquad/core/runner.pipeline.md +743 -0
  40. package/_opensquad/core/skills.engine.md +384 -0
  41. package/bin/opensquad.js +108 -0
  42. package/dashboard/index.html +15 -0
  43. package/dashboard/package-lock.json +1964 -0
  44. package/dashboard/package.json +28 -0
  45. package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
  46. package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
  47. package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
  48. package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
  49. package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
  50. package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
  51. package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
  52. package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
  53. package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
  54. package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
  55. package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
  56. package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
  57. package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
  58. package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
  59. package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
  60. package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
  61. package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
  62. package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
  63. package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
  64. package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
  65. package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
  66. package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
  67. package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
  68. package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
  69. package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
  70. package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
  71. package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
  72. package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
  73. package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
  74. package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
  75. package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
  76. package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
  77. package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
  78. package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
  79. package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
  80. package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
  81. package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
  82. package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
  83. package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
  84. package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
  85. package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
  86. package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
  87. package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
  88. package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
  89. package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
  90. package/dashboard/public/assets/furniture/backpack_red.png +0 -0
  91. package/dashboard/public/assets/furniture/blinds.png +0 -0
  92. package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
  93. package/dashboard/public/assets/furniture/bookshelf.png +0 -0
  94. package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
  95. package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
  96. package/dashboard/public/assets/furniture/clock.png +0 -0
  97. package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
  98. package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
  99. package/dashboard/public/assets/furniture/coffee_table.png +0 -0
  100. package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
  101. package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
  102. package/dashboard/public/assets/furniture/couch.png +0 -0
  103. package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
  104. package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
  105. package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
  106. package/dashboard/public/assets/furniture/desk_wood.png +0 -0
  107. package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
  108. package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
  109. package/dashboard/public/assets/furniture/flowers1.png +0 -0
  110. package/dashboard/public/assets/furniture/flowers2.png +0 -0
  111. package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
  112. package/dashboard/public/assets/furniture/lantern.png +0 -0
  113. package/dashboard/public/assets/furniture/monstera.png +0 -0
  114. package/dashboard/public/assets/furniture/monstera_small.png +0 -0
  115. package/dashboard/public/assets/furniture/picture_frame.png +0 -0
  116. package/dashboard/public/assets/furniture/plant1.png +0 -0
  117. package/dashboard/public/assets/furniture/plant2.png +0 -0
  118. package/dashboard/public/assets/furniture/plant3.png +0 -0
  119. package/dashboard/public/assets/furniture/plant_poof.png +0 -0
  120. package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
  121. package/dashboard/public/assets/furniture/poster_blue.png +0 -0
  122. package/dashboard/public/assets/furniture/rug.png +0 -0
  123. package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
  124. package/dashboard/public/assets/furniture/succulent_green.png +0 -0
  125. package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
  126. package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
  127. package/dashboard/public/assets/furniture/whiteboard.png +0 -0
  128. package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
  129. package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
  130. package/dashboard/src/App.tsx +46 -0
  131. package/dashboard/src/components/RunDashboardButton.tsx +92 -0
  132. package/dashboard/src/components/SquadCard.tsx +49 -0
  133. package/dashboard/src/components/SquadSelector.tsx +67 -0
  134. package/dashboard/src/components/StatusBadge.tsx +32 -0
  135. package/dashboard/src/components/StatusBar.tsx +116 -0
  136. package/dashboard/src/hooks/useSquadSocket.ts +135 -0
  137. package/dashboard/src/lib/formatTime.ts +16 -0
  138. package/dashboard/src/lib/normalizeState.ts +25 -0
  139. package/dashboard/src/main.tsx +10 -0
  140. package/dashboard/src/office/AgentSprite.ts +241 -0
  141. package/dashboard/src/office/OfficeScene.ts +153 -0
  142. package/dashboard/src/office/PhaserGame.tsx +80 -0
  143. package/dashboard/src/office/RoomBuilder.ts +190 -0
  144. package/dashboard/src/office/assetKeys.ts +150 -0
  145. package/dashboard/src/office/palette.ts +32 -0
  146. package/dashboard/src/plugin/squadWatcher.ts +397 -0
  147. package/dashboard/src/store/useSquadStore.ts +56 -0
  148. package/dashboard/src/styles/globals.css +36 -0
  149. package/dashboard/src/types/state.ts +63 -0
  150. package/dashboard/src/vite-env.d.ts +1 -0
  151. package/dashboard/tsconfig.json +24 -0
  152. package/dashboard/vite.config.ts +13 -0
  153. package/package.json +59 -0
  154. package/public/sfx/slide-transition-sfx.mp3 +0 -0
  155. package/skills/README.md +84 -0
  156. package/skills/apify/SKILL.md +55 -0
  157. package/skills/blotato/SKILL.md +63 -0
  158. package/skills/canva/SKILL.md +60 -0
  159. package/skills/higgsfield/SKILL.md +147 -0
  160. package/skills/image-ai-generator/SKILL.md +124 -0
  161. package/skills/image-ai-generator/scripts/generate.py +175 -0
  162. package/skills/image-creator/SKILL.md +166 -0
  163. package/skills/image-creator/editorial-slide-template.js +645 -0
  164. package/skills/image-fetcher/SKILL.md +91 -0
  165. package/skills/imgbb-uploader/SKILL.md +73 -0
  166. package/skills/imgbb-uploader/scripts/upload.js +125 -0
  167. package/skills/instagram-publisher/README.md +36 -0
  168. package/skills/instagram-publisher/SKILL.md +231 -0
  169. package/skills/instagram-publisher/scripts/publish-playwright.js +418 -0
  170. package/skills/instagram-publisher/scripts/publish.js +521 -0
  171. package/skills/opensquad-agent-creator/SKILL.md +192 -0
  172. package/skills/opensquad-skill-creator/SKILL.md +420 -0
  173. package/skills/opensquad-skill-creator/agents/analyzer.md +274 -0
  174. package/skills/opensquad-skill-creator/agents/comparator.md +202 -0
  175. package/skills/opensquad-skill-creator/agents/grader.md +223 -0
  176. package/skills/opensquad-skill-creator/assets/eval_review.html +146 -0
  177. package/skills/opensquad-skill-creator/eval-viewer/generate_review.py +471 -0
  178. package/skills/opensquad-skill-creator/eval-viewer/viewer.html +1325 -0
  179. package/skills/opensquad-skill-creator/references/schemas.md +430 -0
  180. package/skills/opensquad-skill-creator/references/skill-format.md +235 -0
  181. package/skills/opensquad-skill-creator/scripts/__init__.py +0 -0
  182. package/skills/opensquad-skill-creator/scripts/aggregate_benchmark.py +401 -0
  183. package/skills/opensquad-skill-creator/scripts/quick_validate.py +103 -0
  184. package/skills/opensquad-skill-creator/scripts/run_eval.py +310 -0
  185. package/skills/opensquad-skill-creator/scripts/utils.py +47 -0
  186. package/skills/pdf-extractor/SKILL.md +57 -0
  187. package/skills/pdf-extractor/scripts/extract.py +82 -0
  188. package/skills/resend/SKILL.md +80 -0
  189. package/skills/run-dashboard/README.md +93 -0
  190. package/skills/run-dashboard/SKILL.md +173 -0
  191. package/skills/run-dashboard/scripts/finalize-state.js +273 -0
  192. package/skills/run-dashboard/scripts/generate.js +1296 -0
  193. package/skills/run-dashboard/scripts/serve.js +135 -0
  194. package/skills/run-dashboard/templates/run-dashboard-simple.template.html +191 -0
  195. package/skills/run-dashboard/templates/run-dashboard.template.html +1164 -0
  196. package/skills/smtp-sender/SKILL.md +88 -0
  197. package/skills/smtp-sender/scripts/send.js +478 -0
  198. package/skills/template-designer/SKILL.md +201 -0
  199. package/skills/template-designer/base-templates/model-a.html +27 -0
  200. package/skills/template-designer/base-templates/model-b.html +31 -0
  201. package/skills/template-designer/base-templates/model-c.html +42 -0
  202. package/skills/youtube-publisher/SKILL.md +232 -0
  203. package/skills/youtube-publisher/scripts/publish.js +2078 -0
  204. package/src/agents-cli.js +158 -0
  205. package/src/agents.js +134 -0
  206. package/src/i18n.js +48 -0
  207. package/src/init.js +442 -0
  208. package/src/locales/en.json +79 -0
  209. package/src/locales/es.json +78 -0
  210. package/src/locales/pt-BR.json +78 -0
  211. package/src/logger.js +38 -0
  212. package/src/prompt.js +46 -0
  213. package/src/readme/README.md +146 -0
  214. package/src/runs.js +318 -0
  215. package/src/skills-cli.js +157 -0
  216. package/src/skills.js +146 -0
  217. package/src/supabase-cli.js +584 -0
  218. package/src/update.js +169 -0
  219. package/templates/_opensquad/.opensquad-version +1 -0
  220. package/templates/_opensquad/_investigations/.gitkeep +0 -0
  221. package/templates/ide-templates/antigravity/.agent/rules/opensquad.md +68 -0
  222. package/templates/ide-templates/antigravity/.agent/workflows/opensquad.md +102 -0
  223. package/templates/ide-templates/claude-code/.claude/skills/opensquad/SKILL.md +182 -0
  224. package/templates/ide-templates/claude-code/.mcp.json +8 -0
  225. package/templates/ide-templates/claude-code/CLAUDE.md +57 -0
  226. package/templates/ide-templates/codex/.agents/skills/opensquad/SKILL.md +6 -0
  227. package/templates/ide-templates/codex/AGENTS.md +120 -0
  228. package/templates/ide-templates/cursor/.cursor/commands/opensquad.md +9 -0
  229. package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
  230. package/templates/ide-templates/cursor/.cursor/rules/opensquad.mdc +62 -0
  231. package/templates/ide-templates/cursor/.cursorignore +3 -0
  232. package/templates/ide-templates/gemini-cli/.gemini/settings.json +8 -0
  233. package/templates/ide-templates/gemini-cli/.gemini/skills/opensquad/SKILL.md +186 -0
  234. package/templates/ide-templates/gemini-cli/GEMINI.md +57 -0
  235. package/templates/ide-templates/opencode/.opencode/commands/opensquad.md +9 -0
  236. package/templates/ide-templates/opencode/AGENTS.md +120 -0
  237. package/templates/ide-templates/qwen-code/.qwen/settings.json +8 -0
  238. package/templates/ide-templates/qwen-code/.qwen/skills/opensquad/SKILL.md +182 -0
  239. package/templates/ide-templates/qwen-code/QWEN.md +57 -0
  240. package/templates/ide-templates/trae/.trae/mcp.json +8 -0
  241. package/templates/ide-templates/trae/.trae/rules/opensquad.md +64 -0
  242. package/templates/ide-templates/vscode-copilot/.github/copilot-instructions.md +59 -0
  243. package/templates/ide-templates/vscode-copilot/.github/prompts/opensquad.prompt.md +209 -0
  244. package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
  245. package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
  246. package/templates/package.json +8 -0
  247. package/templates/squads/.gitkeep +0 -0
@@ -0,0 +1,397 @@
1
+ import type { Plugin, ViteDevServer } from "vite";
2
+ import { WebSocketServer, WebSocket } from "ws";
3
+ import type { Server, IncomingMessage } from "node:http";
4
+ import type { Duplex } from "node:stream";
5
+ import fs from "node:fs";
6
+ import fsp from "node:fs/promises";
7
+ import { watch as chokidarWatch } from "chokidar";
8
+ import path from "node:path";
9
+ import { pathToFileURL } from "node:url";
10
+ import { parse as parseYaml } from "yaml";
11
+ import type { SquadInfo, SquadState, WsMessage } from "../types/state";
12
+
13
+ type WriteDashboardFilesFn = (options: {
14
+ runDir: string;
15
+ workspaceRoot: string;
16
+ outputHtml: string;
17
+ outputJson: string;
18
+ templatePath: string;
19
+ refreshMetrics: boolean;
20
+ }) => Promise<{ payload: unknown }>;
21
+
22
+ let cachedWriteDashboardFiles: WriteDashboardFilesFn | null = null;
23
+
24
+ function resolveSquadsDir(): string {
25
+ const candidates = [
26
+ path.resolve(process.cwd(), "../squads"), // started from dashboard/
27
+ path.resolve(process.cwd(), "squads"), // started from project root
28
+ ];
29
+ for (const c of candidates) {
30
+ if (fs.existsSync(c)) return c;
31
+ }
32
+ return path.resolve(process.cwd(), "../squads"); // default (will be created on demand)
33
+ }
34
+
35
+ async function discoverSquads(squadsDir: string): Promise<SquadInfo[]> {
36
+ let entries;
37
+ try {
38
+ entries = await fsp.readdir(squadsDir, { withFileTypes: true });
39
+ } catch {
40
+ return [];
41
+ }
42
+
43
+ const squads: SquadInfo[] = [];
44
+
45
+ for (const entry of entries) {
46
+ if (!entry.isDirectory()) continue;
47
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
48
+
49
+ const yamlPath = path.join(squadsDir, entry.name, "squad.yaml");
50
+ try {
51
+ const raw = await fsp.readFile(yamlPath, "utf-8");
52
+ const parsed = parseYaml(raw);
53
+ const s = parsed?.squad;
54
+ if (s) {
55
+ squads.push({
56
+ code: typeof s.code === "string" ? s.code : entry.name,
57
+ name: typeof s.name === "string" ? s.name : entry.name,
58
+ description: typeof s.description === "string" ? s.description : "",
59
+ icon: typeof s.icon === "string" ? s.icon : "\u{1F4CB}",
60
+ agents: Array.isArray(s.agents) ? (s.agents as unknown[]).filter((a): a is string => typeof a === "string") : [],
61
+ });
62
+ continue;
63
+ }
64
+ } catch {
65
+ // No squad.yaml or invalid YAML — fall through to default
66
+ }
67
+
68
+ squads.push({
69
+ code: entry.name,
70
+ name: entry.name,
71
+ description: "",
72
+ icon: "\u{1F4CB}",
73
+ agents: [],
74
+ });
75
+ }
76
+
77
+ return squads;
78
+ }
79
+
80
+ function isValidState(data: unknown): data is SquadState {
81
+ if (!data || typeof data !== "object") return false;
82
+ const d = data as Record<string, unknown>;
83
+ return (
84
+ typeof d.status === "string" &&
85
+ d.step != null && typeof d.step === "object" &&
86
+ Array.isArray(d.agents)
87
+ );
88
+ }
89
+
90
+ async function readActiveStates(squadsDir: string): Promise<Record<string, SquadState>> {
91
+ const states: Record<string, SquadState> = {};
92
+
93
+ let entries;
94
+ try {
95
+ entries = await fsp.readdir(squadsDir, { withFileTypes: true });
96
+ } catch {
97
+ return states;
98
+ }
99
+
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory()) continue;
102
+ const statePath = path.join(squadsDir, entry.name, "state.json");
103
+
104
+ try {
105
+ const raw = await fsp.readFile(statePath, "utf-8");
106
+ const parsed = JSON.parse(raw);
107
+ if (isValidState(parsed)) {
108
+ states[entry.name] = parsed;
109
+ }
110
+ } catch {
111
+ // Skip missing or invalid JSON
112
+ }
113
+ }
114
+
115
+ return states;
116
+ }
117
+
118
+ async function buildSnapshot(squadsDir: string): Promise<WsMessage> {
119
+ return {
120
+ type: "SNAPSHOT",
121
+ squads: await discoverSquads(squadsDir),
122
+ activeStates: await readActiveStates(squadsDir),
123
+ };
124
+ }
125
+
126
+ function broadcast(wss: WebSocketServer, msg: WsMessage) {
127
+ const data = JSON.stringify(msg);
128
+ for (const client of wss.clients) {
129
+ if (client.readyState === WebSocket.OPEN) {
130
+ try {
131
+ client.send(data);
132
+ } catch {
133
+ // Client connection dying — ws library will clean it up
134
+ }
135
+ }
136
+ }
137
+ }
138
+
139
+ async function resolveDashboardRunDir(squadsDir: string, squadCode: string): Promise<string | null> {
140
+ const squadDir = path.join(squadsDir, squadCode);
141
+ const outputDir = path.join(squadDir, "output");
142
+
143
+ if (!fs.existsSync(outputDir)) return null;
144
+
145
+ const entries = await fsp.readdir(outputDir, { withFileTypes: true });
146
+ const candidateDirs = entries
147
+ .filter((entry) => entry.isDirectory() && entry.name !== "archive" && !entry.name.startsWith("."))
148
+ .map((entry) => path.join(outputDir, entry.name))
149
+ .sort((left, right) => path.basename(right).localeCompare(path.basename(left)));
150
+
151
+ for (const candidate of candidateDirs) {
152
+ if (await directoryHasRunDashboardArtifacts(candidate)) return candidate;
153
+ }
154
+
155
+ if (await directoryHasRunDashboardArtifacts(outputDir)) return outputDir;
156
+ return null;
157
+ }
158
+
159
+ async function directoryHasRunDashboardArtifacts(targetDir: string): Promise<boolean> {
160
+ const candidateFiles = [
161
+ "publish-result.md",
162
+ "content-package.md",
163
+ "run-dashboard.html",
164
+ "run-dashboard.data.json",
165
+ "state.json",
166
+ ];
167
+
168
+ for (const fileName of candidateFiles) {
169
+ try {
170
+ const stats = await fsp.stat(path.join(targetDir, fileName));
171
+ if (stats.isFile()) return true;
172
+ } catch {
173
+ // Keep scanning
174
+ }
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ function detectContentType(filePath: string) {
181
+ const extension = path.extname(filePath).toLowerCase();
182
+ if (extension === ".html") return "text/html; charset=utf-8";
183
+ if (extension === ".json") return "application/json; charset=utf-8";
184
+ if (extension === ".md") return "text/markdown; charset=utf-8";
185
+ if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
186
+ if (extension === ".png") return "image/png";
187
+ if (extension === ".webp") return "image/webp";
188
+ return "application/octet-stream";
189
+ }
190
+
191
+ function sendJson(res: { setHeader: (name: string, value: string) => void; end: (body: string) => void; writeHead: (code: number) => void }, statusCode: number, payload: unknown) {
192
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
193
+ res.setHeader("Cache-Control", "no-store");
194
+ res.writeHead(statusCode);
195
+ res.end(JSON.stringify(payload));
196
+ }
197
+
198
+ async function loadWriteDashboardFiles(workspaceRoot: string): Promise<WriteDashboardFilesFn> {
199
+ if (cachedWriteDashboardFiles) return cachedWriteDashboardFiles;
200
+
201
+ const moduleUrl = pathToFileURL(
202
+ path.join(workspaceRoot, "skills", "run-dashboard", "scripts", "generate.js"),
203
+ ).href;
204
+ const mod = await import(/* @vite-ignore */ moduleUrl) as { writeDashboardFiles: WriteDashboardFilesFn };
205
+ cachedWriteDashboardFiles = mod.writeDashboardFiles;
206
+ return cachedWriteDashboardFiles;
207
+ }
208
+
209
+ export function squadWatcherPlugin(): Plugin {
210
+ return {
211
+ name: "squad-watcher",
212
+ configureServer(server: ViteDevServer) {
213
+ if (!server.httpServer) {
214
+ server.config.logger.warn("[squad-watcher] no httpServer — skipping");
215
+ return;
216
+ }
217
+
218
+ const squadsDir = resolveSquadsDir();
219
+ server.config.logger.info(`[squad-watcher] squads dir: ${squadsDir}`);
220
+
221
+ // Create WebSocket server with noServer to avoid intercepting Vite's HMR
222
+ const wss = new WebSocketServer({ noServer: true });
223
+ (server.httpServer as Server).on("upgrade", (req: IncomingMessage, socket: Duplex, head: Buffer) => {
224
+ if (req.url === "/__squads_ws") {
225
+ wss.handleUpgrade(req, socket, head, (ws) => {
226
+ wss.emit("connection", ws, req);
227
+ });
228
+ }
229
+ // Let Vite handle all other upgrade requests (HMR)
230
+ });
231
+
232
+ // Send snapshot on new connection
233
+ wss.on("connection", async (ws) => {
234
+ try {
235
+ const snap = await buildSnapshot(squadsDir);
236
+ ws.send(JSON.stringify(snap));
237
+ } catch {
238
+ // Connection may have closed before snapshot was ready
239
+ }
240
+ });
241
+
242
+ // Ensure squads directory exists
243
+ fsp.mkdir(squadsDir, { recursive: true }).catch((err) => {
244
+ server.config.logger.error(`[squad-watcher] failed to create squads dir: ${err.message}`);
245
+ });
246
+
247
+ // REST API fallback — serves snapshot over HTTP for polling clients
248
+ server.middlewares.use(async (req, res, next) => {
249
+ const requestUrl = new URL(req.url || "/", "http://dashboard.local");
250
+
251
+ if (requestUrl.pathname === "/api/run-dashboard") {
252
+ const squadCode = requestUrl.searchParams.get("squad");
253
+ if (!squadCode) {
254
+ return sendJson(res, 400, { error: "Missing squad parameter" });
255
+ }
256
+
257
+ const runDir = await resolveDashboardRunDir(squadsDir, squadCode);
258
+ if (!runDir) {
259
+ return sendJson(res, 404, { available: false, url: null, runId: null, isLegacyOutputRoot: false });
260
+ }
261
+
262
+ return sendJson(res, 200, {
263
+ available: true,
264
+ url: `/__run_dashboard/${encodeURIComponent(squadCode)}/run-dashboard.html`,
265
+ runId: path.basename(runDir) === "output" ? "legacy-output-root" : path.basename(runDir),
266
+ isLegacyOutputRoot: path.basename(runDir) === "output",
267
+ });
268
+ }
269
+
270
+ if (requestUrl.pathname.startsWith("/__run_dashboard/")) {
271
+ const [, , encodedSquad, ...rest] = requestUrl.pathname.split("/");
272
+ const squadCode = decodeURIComponent(encodedSquad || "");
273
+ const runDir = await resolveDashboardRunDir(squadsDir, squadCode);
274
+ if (!runDir) {
275
+ res.writeHead(404);
276
+ res.end("Run dashboard not found");
277
+ return;
278
+ }
279
+
280
+ const workspaceRoot = path.dirname(squadsDir);
281
+ const templatePath = path.join(workspaceRoot, "skills", "run-dashboard", "templates", "run-dashboard.template.html");
282
+ const outputHtml = path.join(runDir, "run-dashboard.html");
283
+ const outputJson = path.join(runDir, "run-dashboard.data.json");
284
+ const relativeTarget = rest.join("/") || "run-dashboard.html";
285
+ const writeDashboardFiles = await loadWriteDashboardFiles(workspaceRoot);
286
+
287
+ if (req.method === "POST" && relativeTarget === "__run_dashboard_refresh__") {
288
+ const { payload } = await writeDashboardFiles({
289
+ runDir,
290
+ workspaceRoot,
291
+ outputHtml,
292
+ outputJson,
293
+ templatePath,
294
+ refreshMetrics: true,
295
+ });
296
+ return sendJson(res, 200, payload);
297
+ }
298
+
299
+ if (relativeTarget === "run-dashboard.html" || relativeTarget === "run-dashboard.data.json") {
300
+ await writeDashboardFiles({
301
+ runDir,
302
+ workspaceRoot,
303
+ outputHtml,
304
+ outputJson,
305
+ templatePath,
306
+ refreshMetrics: false,
307
+ });
308
+ }
309
+
310
+ const requestedPath = path.resolve(path.join(runDir, relativeTarget));
311
+ const normalizedRunDir = runDir.toLowerCase();
312
+ const normalizedRequestedPath = requestedPath.toLowerCase();
313
+ if (!(normalizedRequestedPath === normalizedRunDir || normalizedRequestedPath.startsWith(`${normalizedRunDir}${path.sep}`))) {
314
+ res.writeHead(403);
315
+ res.end("Forbidden");
316
+ return;
317
+ }
318
+
319
+ try {
320
+ const content = await fsp.readFile(requestedPath);
321
+ res.setHeader("Content-Type", detectContentType(requestedPath));
322
+ res.setHeader("Cache-Control", "no-store");
323
+ res.end(content);
324
+ } catch {
325
+ res.writeHead(404);
326
+ res.end("Not found");
327
+ }
328
+ return;
329
+ }
330
+
331
+ if (requestUrl.pathname !== "/api/snapshot") return next();
332
+ try {
333
+ const snapshot = await buildSnapshot(squadsDir);
334
+ res.setHeader("Content-Type", "application/json");
335
+ res.setHeader("Cache-Control", "no-cache");
336
+ res.end(JSON.stringify(snapshot));
337
+ } catch {
338
+ res.writeHead(500);
339
+ res.end("Internal Server Error");
340
+ }
341
+ });
342
+
343
+ // File watcher using chokidar — reliable cross-platform, handles partial writes
344
+ const watcher = chokidarWatch(squadsDir, {
345
+ ignoreInitial: true,
346
+ awaitWriteFinish: { stabilityThreshold: 300, pollInterval: 50 },
347
+ ignored: [/(^|[/\\])\./, /node_modules/, /output[/\\]/],
348
+ depth: 2,
349
+ });
350
+
351
+ function handleFileChange(filePath: string) {
352
+ const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
353
+ const parts = relative.split("/");
354
+ if (parts.length < 2) return;
355
+
356
+ const squadName = parts[0];
357
+ const fileName = parts[1];
358
+
359
+ if (fileName === "state.json") {
360
+ fsp.readFile(filePath, "utf-8").then((raw) => {
361
+ const parsed = JSON.parse(raw);
362
+ if (!isValidState(parsed)) return;
363
+ broadcast(wss, { type: "SQUAD_UPDATE", squad: squadName, state: parsed });
364
+ }).catch(() => {
365
+ // Invalid JSON — next change event will retry
366
+ });
367
+ } else if (fileName === "squad.yaml") {
368
+ buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
369
+ }
370
+ }
371
+
372
+ function handleFileRemoval(filePath: string) {
373
+ const relative = path.relative(squadsDir, filePath).replace(/\\/g, "/");
374
+ const parts = relative.split("/");
375
+ if (parts.length < 2) return;
376
+
377
+ const squadName = parts[0];
378
+ const fileName = parts[1];
379
+
380
+ if (fileName === "state.json") {
381
+ broadcast(wss, { type: "SQUAD_INACTIVE", squad: squadName });
382
+ } else if (fileName === "squad.yaml") {
383
+ buildSnapshot(squadsDir).then((snap) => broadcast(wss, snap));
384
+ }
385
+ }
386
+
387
+ watcher.on("add", handleFileChange);
388
+ watcher.on("change", handleFileChange);
389
+ watcher.on("unlink", handleFileRemoval);
390
+
391
+ server.httpServer.on("close", () => {
392
+ watcher.close();
393
+ });
394
+ },
395
+ };
396
+ }
397
+
@@ -0,0 +1,56 @@
1
+ import { create } from "zustand";
2
+ import type { SquadInfo, SquadState } from "@/types/state";
3
+
4
+ interface SquadStore {
5
+ // State
6
+ squads: Map<string, SquadInfo>;
7
+ activeStates: Map<string, SquadState>;
8
+ selectedSquad: string | null;
9
+ isConnected: boolean;
10
+
11
+ // Actions
12
+ selectSquad: (name: string | null) => void;
13
+ setConnected: (connected: boolean) => void;
14
+ setSnapshot: (squads: SquadInfo[], activeStates: Record<string, SquadState>) => void;
15
+ setSquadActive: (squad: string, state: SquadState) => void;
16
+ updateSquadState: (squad: string, state: SquadState) => void;
17
+ setSquadInactive: (squad: string) => void;
18
+ }
19
+
20
+ export const useSquadStore = create<SquadStore>((set) => ({
21
+ squads: new Map(),
22
+ activeStates: new Map(),
23
+ selectedSquad: null,
24
+ isConnected: false,
25
+
26
+ selectSquad: (name) => set({ selectedSquad: name }),
27
+
28
+ setConnected: (connected) => set({ isConnected: connected }),
29
+
30
+ setSnapshot: (squads, activeStates) =>
31
+ set({
32
+ squads: new Map(squads.map((s) => [s.code, s])),
33
+ activeStates: new Map(Object.entries(activeStates)),
34
+ }),
35
+
36
+ setSquadActive: (squad, state) =>
37
+ set((prev) => ({
38
+ activeStates: new Map(prev.activeStates).set(squad, state),
39
+ })),
40
+
41
+ updateSquadState: (squad, state) =>
42
+ set((prev) => ({
43
+ activeStates: new Map(prev.activeStates).set(squad, state),
44
+ })),
45
+
46
+ setSquadInactive: (squad) =>
47
+ set((prev) => {
48
+ const next = new Map(prev.activeStates);
49
+ next.delete(squad);
50
+ return {
51
+ activeStates: next,
52
+ // Reset selection if the inactive squad was selected
53
+ selectedSquad: prev.selectedSquad === squad ? null : prev.selectedSquad,
54
+ };
55
+ }),
56
+ }));
@@ -0,0 +1,36 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ :root {
10
+ --bg-primary: #101018;
11
+ --bg-secondary: #1a1a2e;
12
+ --bg-sidebar: #141422;
13
+ --border: #2a2a3e;
14
+ --text-primary: #e0e0e0;
15
+ --text-secondary: #8888a0;
16
+ --accent-cyan: #00d4ff;
17
+ --accent-green: #00e676;
18
+ --accent-amber: #ffab00;
19
+ --accent-red: #ff5252;
20
+ }
21
+
22
+ html,
23
+ body,
24
+ #root {
25
+ height: 100%;
26
+ width: 100%;
27
+ overflow: hidden;
28
+ font-family: "JetBrains Mono", "Fira Code", monospace;
29
+ background: var(--bg-primary);
30
+ color: var(--text-primary);
31
+ }
32
+
33
+ @keyframes pulse {
34
+ 0%, 100% { opacity: 1; }
35
+ 50% { opacity: 0.4; }
36
+ }
@@ -0,0 +1,63 @@
1
+ // state.json structure — matches Pipeline Runner output
2
+ export interface AgentDesk {
3
+ col: number;
4
+ row: number;
5
+ }
6
+
7
+ export type AgentStatus =
8
+ | "idle"
9
+ | "working"
10
+ | "delivering"
11
+ | "done"
12
+ | "checkpoint";
13
+
14
+ export interface Agent {
15
+ id: string;
16
+ name: string;
17
+ icon: string;
18
+ status: AgentStatus;
19
+ gender?: "male" | "female";
20
+ desk: AgentDesk;
21
+ }
22
+
23
+ export interface Handoff {
24
+ from: string;
25
+ to: string;
26
+ message: string;
27
+ completedAt: string;
28
+ }
29
+
30
+ export type SquadStatus =
31
+ | "idle"
32
+ | "running"
33
+ | "completed"
34
+ | "checkpoint";
35
+
36
+ export interface SquadState {
37
+ squad: string;
38
+ status: SquadStatus;
39
+ step: {
40
+ current: number;
41
+ total: number;
42
+ label: string;
43
+ };
44
+ agents: Agent[];
45
+ handoff: Handoff | null;
46
+ startedAt: string | null;
47
+ updatedAt: string;
48
+ }
49
+
50
+ // Squad metadata from squad.yaml
51
+ export interface SquadInfo {
52
+ code: string;
53
+ name: string;
54
+ description: string;
55
+ icon: string;
56
+ agents: string[]; // agent file paths
57
+ }
58
+
59
+ // WebSocket messages
60
+ export type WsMessage =
61
+ | { type: "SNAPSHOT"; squads: SquadInfo[]; activeStates: Record<string, SquadState> }
62
+ | { type: "SQUAD_UPDATE"; squad: string; state: SquadState }
63
+ | { type: "SQUAD_INACTIVE"; squad: string };
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "isolatedModules": true,
11
+ "moduleDetection": "force",
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true,
18
+ "noUncheckedSideEffectImports": true,
19
+ "paths": {
20
+ "@/*": ["./src/*"]
21
+ }
22
+ },
23
+ "include": ["src"]
24
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import path from "node:path";
4
+ import { squadWatcherPlugin } from "./src/plugin/squadWatcher";
5
+
6
+ export default defineConfig({
7
+ plugins: [react(), squadWatcherPlugin()],
8
+ resolve: {
9
+ alias: {
10
+ "@": path.resolve(__dirname, "./src"),
11
+ },
12
+ },
13
+ });
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@julioventura/opensquad",
3
+ "version": "0.1.17",
4
+ "description": "Multi-agent orchestration framework — create AI squads that work together",
5
+ "type": "module",
6
+ "bin": {
7
+ "opensquad": "bin/opensquad.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test tests/*.test.js",
11
+ "lint": "eslint src/ bin/ tests/",
12
+ "version": "node -e \"require('fs').writeFileSync('templates/_opensquad/.opensquad-version', require('./package.json').version + '\\n')\" && git add templates/_opensquad/.opensquad-version"
13
+ },
14
+ "keywords": [
15
+ "claude-code",
16
+ "ai-agents",
17
+ "multi-agent",
18
+ "orchestration",
19
+ "squads"
20
+ ],
21
+ "author": "Renato Asse (https://github.com/renatoasse)",
22
+ "homepage": "https://github.com/julioventura/opensquad#readme",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/julioventura/opensquad.git"
26
+ },
27
+ "bugs": {
28
+ "url": "https://github.com/julioventura/opensquad/issues"
29
+ },
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": ">=20.0.0"
33
+ },
34
+ "files": [
35
+ "bin/",
36
+ "src/",
37
+ "agents/",
38
+ "skills/",
39
+ "templates/",
40
+ "_opensquad/config/",
41
+ "_opensquad/core/",
42
+ "dashboard/",
43
+ "public/sfx/"
44
+ ],
45
+ "dependencies": {
46
+ "@inquirer/checkbox": "^5.1.0",
47
+ "@inquirer/input": "^5.0.0",
48
+ "@inquirer/select": "^5.1.0",
49
+ "@supabase/supabase-js": "^2.107.0",
50
+ "basic-ftp": "^5.3.1"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^10.0.1",
54
+ "eslint": "^10.0.3",
55
+ "globals": "^17.4.0",
56
+ "playwright": "^1.60.0",
57
+ "qrcode": "^1.5.4"
58
+ }
59
+ }