@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,88 @@
1
+ ---
2
+ name: smtp-sender
3
+ description: >
4
+ Send HTML plus plain-text emails through a native SMTP connection using local .env credentials.
5
+ Useful when a squad needs direct SMTP delivery without relying on an external email API.
6
+ description_pt-BR: >
7
+ Envia emails HTML e texto puro por conexao SMTP nativa usando credenciais locais do .env.
8
+ Util quando um squad precisa de entrega SMTP direta sem depender de uma API externa.
9
+ description_es: >
10
+ Envia correos HTML y texto plano mediante una conexion SMTP nativa usando credenciales locales del .env.
11
+ Util cuando un squad necesita entrega SMTP directa sin depender de una API externa.
12
+ type: script
13
+ version: "1.0.0"
14
+ script:
15
+ path: scripts/send.js
16
+ runtime: node
17
+ invoke: "node --env-file=.env {skill_path}/scripts/send.js --html \"{html}\" --text \"{text}\" --newsletter-preview \"{newsletter_preview}\" --preview \"{send_preview}\" --output \"{output}\" --smtp-host-env \"{smtp_host_env}\" --smtp-port-env \"{smtp_port_env}\" --smtp-user-env \"{smtp_user_env}\" --smtp-pass-env \"{smtp_pass_env}\""
18
+ env:
19
+ - SMTP_HOST
20
+ - SMTP_PORT
21
+ - SMTP_USER
22
+ - SMTP_PASS
23
+ categories: [email, smtp, automation]
24
+ ---
25
+
26
+ # SMTP Sender
27
+
28
+ ## When to use
29
+
30
+ Use this skill when a squad already has the final `newsletter.html` and `newsletter.txt` files and needs to deliver them through a direct SMTP server such as a brand-owned mailbox.
31
+
32
+ ## Workflow
33
+
34
+ 1. Confirm the final send metadata in `send-preview.md` and `newsletter-preview.md`.
35
+ 2. Run the sender script with the HTML file, text file, preview files, and output path.
36
+ 3. Pass the brand-specific SMTP env keys explicitly when the workspace serves multiple brands, or persist them in `send-preview.md` so the sender can resolve them automatically.
37
+ 4. Read `email-send-result.md` after the command finishes and confirm status, message ID, sender, and audience.
38
+
39
+ ## Command
40
+
41
+ ```bash
42
+ node --env-file=.env {skill_path}/scripts/send.js \
43
+ --html "squads/jornal-matutino/output/2026-05-18-201500/newsletter.html" \
44
+ --text "squads/jornal-matutino/output/2026-05-18-201500/newsletter.txt" \
45
+ --newsletter-preview "squads/jornal-matutino/output/2026-05-18-201500/newsletter-preview.md" \
46
+ --preview "squads/jornal-matutino/output/2026-05-18-201500/send-preview.md" \
47
+ --output "squads/jornal-matutino/output/2026-05-18-201500/email-send-result.md"
48
+ ```
49
+
50
+ When `send-preview.md` does not include the SMTP env key mapping, append the brand-specific overrides explicitly:
51
+
52
+ ```bash
53
+ --smtp-host-env "BRAND_SMTP_HOST" \
54
+ --smtp-port-env "BRAND_SMTP_PORT" \
55
+ --smtp-user-env "BRAND_SMTP_USER" \
56
+ --smtp-pass-env "BRAND_SMTP_PASS"
57
+ ```
58
+
59
+ ## Output
60
+
61
+ The script writes `email-send-result.md` in this format:
62
+
63
+ ```markdown
64
+ # Email Send Result
65
+
66
+ **Status:** sent | failed
67
+ **Mode:** test | live
68
+ **Provider:** SMTP
69
+ **Message ID:** ...
70
+ **From:** ...
71
+ **To/Audience:** ...
72
+ **Scheduled at:** ...
73
+ **Sent at:** ...
74
+ **Notes:** ...
75
+ ```
76
+
77
+ ## Constraints
78
+
79
+ - Current native flow supports immediate `test` and `live` sends.
80
+ - `scheduled` mode is not implemented in the local SMTP sender.
81
+ - The sender always opens a secure `SSL/TLS` SMTP connection.
82
+ - If `send-preview.md` contains `SMTP Host Env`, `SMTP Port Env`, `SMTP User Env`, and `SMTP Pass Env`, those mappings are used automatically.
83
+
84
+ ## Available operations
85
+
86
+ - **Send multipart email** -- Deliver HTML plus plain-text content over SMTP
87
+ - **Brand env mapping** -- Use explicit env key names for multi-brand workspaces
88
+ - **Delivery logging** -- Persist the provider result to `email-send-result.md`
@@ -0,0 +1,478 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import net from 'node:net';
5
+ import tls from 'node:tls';
6
+ import { dirname, resolve } from 'node:path';
7
+ import process from 'node:process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const DEFAULT_ENV_KEYS = {
11
+ smtpHost: 'SMTP_HOST',
12
+ smtpPort: 'SMTP_PORT',
13
+ smtpUser: 'SMTP_USER',
14
+ smtpPass: 'SMTP_PASS',
15
+ };
16
+
17
+ function stripWrapping(value) {
18
+ return value
19
+ .replace(/^[<'"`]+/, '')
20
+ .replace(/[>'"`]+$/, '')
21
+ .trim();
22
+ }
23
+
24
+ function normalizeLineEndings(value) {
25
+ return value.replace(/\r?\n/g, '\r\n');
26
+ }
27
+
28
+ export function parseArgs(argv) {
29
+ const args = {
30
+ htmlPath: '',
31
+ textPath: '',
32
+ newsletterPreviewPath: '',
33
+ previewPath: '',
34
+ outputPath: '',
35
+ subject: '',
36
+ from: '',
37
+ to: '',
38
+ replyTo: '',
39
+ mode: '',
40
+ scheduledAt: '',
41
+ smtpHostEnv: null,
42
+ smtpPortEnv: null,
43
+ smtpUserEnv: null,
44
+ smtpPassEnv: null,
45
+ dryRun: false,
46
+ };
47
+
48
+ for (let index = 2; index < argv.length; index++) {
49
+ const current = argv[index];
50
+
51
+ if (current === '--html' && index + 1 < argv.length) {
52
+ args.htmlPath = argv[++index];
53
+ } else if (current === '--text' && index + 1 < argv.length) {
54
+ args.textPath = argv[++index];
55
+ } else if (current === '--newsletter-preview' && index + 1 < argv.length) {
56
+ args.newsletterPreviewPath = argv[++index];
57
+ } else if (current === '--preview' && index + 1 < argv.length) {
58
+ args.previewPath = argv[++index];
59
+ } else if (current === '--output' && index + 1 < argv.length) {
60
+ args.outputPath = argv[++index];
61
+ } else if (current === '--subject' && index + 1 < argv.length) {
62
+ args.subject = argv[++index];
63
+ } else if (current === '--from' && index + 1 < argv.length) {
64
+ args.from = argv[++index];
65
+ } else if (current === '--to' && index + 1 < argv.length) {
66
+ args.to = argv[++index];
67
+ } else if (current === '--reply-to' && index + 1 < argv.length) {
68
+ args.replyTo = argv[++index];
69
+ } else if (current === '--mode' && index + 1 < argv.length) {
70
+ args.mode = argv[++index];
71
+ } else if (current === '--scheduled-at' && index + 1 < argv.length) {
72
+ args.scheduledAt = argv[++index];
73
+ } else if (current === '--smtp-host-env' && index + 1 < argv.length) {
74
+ args.smtpHostEnv = argv[++index];
75
+ } else if (current === '--smtp-port-env' && index + 1 < argv.length) {
76
+ args.smtpPortEnv = argv[++index];
77
+ } else if (current === '--smtp-user-env' && index + 1 < argv.length) {
78
+ args.smtpUserEnv = argv[++index];
79
+ } else if (current === '--smtp-pass-env' && index + 1 < argv.length) {
80
+ args.smtpPassEnv = argv[++index];
81
+ } else if (current === '--dry-run') {
82
+ args.dryRun = true;
83
+ }
84
+ }
85
+
86
+ return args;
87
+ }
88
+
89
+ export async function loadEnvVars(targetDir = process.cwd()) {
90
+ const envVars = { ...process.env };
91
+
92
+ try {
93
+ const raw = await readFile(resolve(targetDir, '.env'), 'utf-8');
94
+ for (const line of raw.split(/\r?\n/)) {
95
+ const trimmed = line.trim();
96
+ if (!trimmed || trimmed.startsWith('#')) continue;
97
+ const separatorIndex = trimmed.indexOf('=');
98
+ if (separatorIndex === -1) continue;
99
+ const key = trimmed.slice(0, separatorIndex).trim();
100
+ const value = trimmed.slice(separatorIndex + 1).trim().replace(/^['"]|['"]$/g, '');
101
+ if (key) {
102
+ envVars[key] = value;
103
+ }
104
+ }
105
+ } catch {
106
+ // Optional .env
107
+ }
108
+
109
+ return envVars;
110
+ }
111
+
112
+ export function resolveConfiguredEnv(env, options = {}) {
113
+ const smtpHostKey = options.smtpHostEnv || DEFAULT_ENV_KEYS.smtpHost;
114
+ const smtpPortKey = options.smtpPortEnv || DEFAULT_ENV_KEYS.smtpPort;
115
+ const smtpUserKey = options.smtpUserEnv || DEFAULT_ENV_KEYS.smtpUser;
116
+ const smtpPassKey = options.smtpPassEnv || DEFAULT_ENV_KEYS.smtpPass;
117
+
118
+ return {
119
+ smtpHostKey,
120
+ smtpPortKey,
121
+ smtpUserKey,
122
+ smtpPassKey,
123
+ smtpHost: env[smtpHostKey],
124
+ smtpPort: env[smtpPortKey],
125
+ smtpUser: env[smtpUserKey],
126
+ smtpPass: env[smtpPassKey],
127
+ secure: true,
128
+ };
129
+ }
130
+
131
+ export function parseMarkdownFields(content) {
132
+ const result = {};
133
+ const matches = content.matchAll(/^\*\*(.+?):\*\*\s*(.+)$/gm);
134
+ for (const match of matches) {
135
+ result[match[1].trim()] = stripWrapping(match[2].trim());
136
+ }
137
+ return result;
138
+ }
139
+
140
+ export function resolveSmtpEnvOptions(args, previewFields = {}) {
141
+ return {
142
+ smtpHostEnv: args.smtpHostEnv || previewFields['SMTP Host Env'] || null,
143
+ smtpPortEnv: args.smtpPortEnv || previewFields['SMTP Port Env'] || null,
144
+ smtpUserEnv: args.smtpUserEnv || previewFields['SMTP User Env'] || null,
145
+ smtpPassEnv: args.smtpPassEnv || previewFields['SMTP Pass Env'] || null,
146
+ };
147
+ }
148
+
149
+ function parseRecipients(value) {
150
+ return value
151
+ .split(/[;,\n]/)
152
+ .map((entry) => stripWrapping(entry.trim()))
153
+ .filter(Boolean);
154
+ }
155
+
156
+ function extractEmailAddress(value) {
157
+ const match = value.match(/<([^>]+)>/);
158
+ return stripWrapping(match ? match[1] : value);
159
+ }
160
+
161
+ function formatMailboxAddress(name, email) {
162
+ if (!name) return email;
163
+ return `${encodeHeader(name)} <${email}>`;
164
+ }
165
+
166
+ function encodeHeader(value) {
167
+ if (/^[\x20-\x7E]*$/.test(value)) return value;
168
+ return `=?UTF-8?B?${Buffer.from(value, 'utf8').toString('base64')}?=`;
169
+ }
170
+
171
+ function encodeBodyPart(value) {
172
+ return Buffer.from(normalizeLineEndings(value), 'utf8').toString('base64').replace(/.{1,76}/g, '$&\r\n').trimEnd();
173
+ }
174
+
175
+ function buildMessageHeaderId(from) {
176
+ const domain = (from.split('@')[1] || 'opensquad.local').replace(/[^a-z0-9.-]/gi, '') || 'opensquad.local';
177
+ return `<${Date.now()}.${Math.random().toString(16).slice(2)}@${domain}>`;
178
+ }
179
+
180
+ function formatUnknownError(error) {
181
+ if (error instanceof Error && error.message) return error.message;
182
+ if (typeof error === 'string' && error.trim()) return error;
183
+ if (error && typeof error === 'object') {
184
+ const details = [error.code, error.reason, error.name, error.command].filter(Boolean).join(' | ');
185
+ if (details) return details;
186
+
187
+ try {
188
+ const json = JSON.stringify(error);
189
+ if (json && json !== '{}') return json;
190
+ } catch {
191
+ // Fall through
192
+ }
193
+ }
194
+
195
+ return 'Unknown SMTP sender failure';
196
+ }
197
+
198
+ function escapeDataBody(value) {
199
+ return normalizeLineEndings(value).replace(/^\./gm, '..');
200
+ }
201
+
202
+ export function buildMimeMessage({ from, to, replyTo, subject, html, text }) {
203
+ const fromEmail = extractEmailAddress(from);
204
+ const boundary = `opensquad_${Date.now().toString(16)}_${Math.random().toString(16).slice(2)}`;
205
+ const messageHeaderId = buildMessageHeaderId(fromEmail);
206
+ const headers = [
207
+ `From: ${from}`,
208
+ `To: ${to.join(', ')}`,
209
+ replyTo ? `Reply-To: ${replyTo}` : null,
210
+ `Subject: ${encodeHeader(subject)}`,
211
+ `Message-ID: ${messageHeaderId}`,
212
+ `Date: ${new Date().toUTCString()}`,
213
+ 'X-Mailer: Opensquad SMTP Sender',
214
+ 'MIME-Version: 1.0',
215
+ `Content-Type: multipart/alternative; boundary="${boundary}"`,
216
+ '',
217
+ `--${boundary}`,
218
+ 'Content-Type: text/plain; charset="UTF-8"',
219
+ 'Content-Transfer-Encoding: base64',
220
+ '',
221
+ escapeDataBody(encodeBodyPart(text)),
222
+ `--${boundary}`,
223
+ 'Content-Type: text/html; charset="UTF-8"',
224
+ 'Content-Transfer-Encoding: base64',
225
+ '',
226
+ escapeDataBody(encodeBodyPart(html)),
227
+ `--${boundary}--`,
228
+ '',
229
+ ].filter((entry) => entry !== null);
230
+
231
+ return headers.join('\r\n');
232
+ }
233
+
234
+ function createResponseReader(socket) {
235
+ let buffer = '';
236
+ let currentLines = [];
237
+ const queue = [];
238
+
239
+ function flush(response) {
240
+ const next = queue.shift();
241
+ if (next) {
242
+ clearTimeout(next.timeoutId);
243
+ next.resolve(response);
244
+ }
245
+ }
246
+
247
+ socket.on('data', (chunk) => {
248
+ buffer += chunk.toString('utf8');
249
+
250
+ while (buffer.includes('\n')) {
251
+ const newlineIndex = buffer.indexOf('\n');
252
+ const line = buffer.slice(0, newlineIndex).replace(/\r$/, '');
253
+ buffer = buffer.slice(newlineIndex + 1);
254
+ if (!line) continue;
255
+
256
+ currentLines.push(line);
257
+ const match = line.match(/^(\d{3})([ -])(.*)$/);
258
+ if (!match) continue;
259
+ if (match[2] === ' ') {
260
+ const code = Number(match[1]);
261
+ const lines = currentLines;
262
+ currentLines = [];
263
+ flush({ code, lines, message: lines.join('\n') });
264
+ }
265
+ }
266
+ });
267
+
268
+ function waitForResponse(timeoutMs = 15000) {
269
+ return new Promise((resolve, reject) => {
270
+ const timeoutId = setTimeout(() => {
271
+ const pending = queue.findIndex((entry) => entry.timeoutId === timeoutId);
272
+ if (pending !== -1) queue.splice(pending, 1);
273
+ reject(new Error(`SMTP response timeout after ${timeoutMs}ms`));
274
+ }, timeoutMs);
275
+
276
+ queue.push({ resolve, reject, timeoutId });
277
+ });
278
+ }
279
+
280
+ return { waitForResponse };
281
+ }
282
+
283
+ async function connectSmtp({ host, port, secure }) {
284
+ return new Promise((resolve, reject) => {
285
+ const options = { host, port, servername: host, family: 4 };
286
+ const socket = secure
287
+ ? tls.connect(options, () => resolve(socket))
288
+ : net.createConnection({ host, port }, () => resolve(socket));
289
+
290
+ socket.once('error', reject);
291
+ });
292
+ }
293
+
294
+ async function sendCommand(socket, reader, command, expectedCodes) {
295
+ if (command != null) {
296
+ socket.write(`${command}\r\n`);
297
+ }
298
+ const response = await reader.waitForResponse();
299
+ if (expectedCodes && !expectedCodes.includes(response.code)) {
300
+ throw new Error(`SMTP command failed for '${command ?? '[initial]'}': ${response.message}`);
301
+ }
302
+ return response;
303
+ }
304
+
305
+ function extractProviderMessageId(response) {
306
+ const match = response.message.match(/\bid=([^\s]+)/i);
307
+ return match ? match[1] : 'not-returned';
308
+ }
309
+
310
+ export async function sendViaSmtp({ host, port, secure, username, password, from, to, message }) {
311
+ const socket = await connectSmtp({ host, port, secure });
312
+ const reader = createResponseReader(socket);
313
+
314
+ try {
315
+ await sendCommand(socket, reader, null, [220]);
316
+ await sendCommand(socket, reader, 'EHLO opensquad.local', [250]);
317
+ await sendCommand(socket, reader, 'AUTH LOGIN', [334]);
318
+ await sendCommand(socket, reader, Buffer.from(username, 'utf8').toString('base64'), [334]);
319
+ await sendCommand(socket, reader, Buffer.from(password, 'utf8').toString('base64'), [235]);
320
+ await sendCommand(socket, reader, `MAIL FROM:<${from}>`, [250]);
321
+ for (const recipient of to) {
322
+ await sendCommand(socket, reader, `RCPT TO:<${recipient}>`, [250, 251]);
323
+ }
324
+ await sendCommand(socket, reader, 'DATA', [354]);
325
+ socket.write(`${message}\r\n.\r\n`);
326
+ const deliveryResponse = await reader.waitForResponse();
327
+ if (deliveryResponse.code !== 250) {
328
+ throw new Error(`SMTP delivery failed: ${deliveryResponse.message}`);
329
+ }
330
+ await sendCommand(socket, reader, 'QUIT', [221]);
331
+ socket.end();
332
+
333
+ return {
334
+ messageId: extractProviderMessageId(deliveryResponse),
335
+ response: deliveryResponse.message,
336
+ };
337
+ } catch (error) {
338
+ socket.destroy();
339
+ throw error;
340
+ }
341
+ }
342
+
343
+ export function buildEmailSendResult({ status, mode, messageId, from, to, scheduledAt, sentAt, notes }) {
344
+ return [
345
+ '# Email Send Result',
346
+ '',
347
+ `**Status:** ${status}`,
348
+ `**Mode:** ${mode}`,
349
+ '**Provider:** SMTP',
350
+ `**Message ID:** ${messageId}`,
351
+ `**From:** ${from}`,
352
+ `**To/Audience:** ${to.join(', ')}`,
353
+ `**Scheduled at:** ${scheduledAt || 'not scheduled'}`,
354
+ `**Sent at:** ${sentAt}`,
355
+ `**Notes:** ${notes}`,
356
+ '',
357
+ ].join('\n');
358
+ }
359
+
360
+ async function writeResult(outputPath, content) {
361
+ const absoluteOutputPath = resolve(outputPath);
362
+ await mkdir(dirname(absoluteOutputPath), { recursive: true });
363
+ await writeFile(absoluteOutputPath, content, 'utf8');
364
+ }
365
+
366
+ async function main() {
367
+ const args = parseArgs(process.argv);
368
+ if (!args.htmlPath || !args.textPath || !args.newsletterPreviewPath || !args.previewPath || !args.outputPath) {
369
+ throw new Error('Missing required arguments. Use --html, --text, --newsletter-preview, --preview, and --output.');
370
+ }
371
+
372
+ const [html, text, newsletterPreview, sendPreview] = await Promise.all([
373
+ readFile(resolve(args.htmlPath), 'utf8'),
374
+ readFile(resolve(args.textPath), 'utf8'),
375
+ readFile(resolve(args.newsletterPreviewPath), 'utf8'),
376
+ readFile(resolve(args.previewPath), 'utf8'),
377
+ ]);
378
+
379
+ const newsletterFields = parseMarkdownFields(newsletterPreview);
380
+ const previewFields = parseMarkdownFields(sendPreview);
381
+ const env = await loadEnvVars();
382
+ const smtpEnvOptions = resolveSmtpEnvOptions(args, previewFields);
383
+ const configuredEnv = resolveConfiguredEnv(env, smtpEnvOptions);
384
+
385
+ const subject = args.subject || newsletterFields.Subject;
386
+ const senderName = stripWrapping(newsletterFields.Brand || previewFields['Sender Name'] || '');
387
+ const fromEmail = extractEmailAddress(stripWrapping(args.from || previewFields.From || configuredEnv.smtpUser || ''));
388
+ const from = formatMailboxAddress(senderName, fromEmail);
389
+ const recipients = parseRecipients(args.to || previewFields.To || '');
390
+ const replyTo = stripWrapping(args.replyTo || '');
391
+ const mode = stripWrapping(args.mode || previewFields.Mode || 'test').toLowerCase();
392
+ const scheduledAt = stripWrapping(args.scheduledAt || previewFields['Scheduled at'] || 'not scheduled');
393
+ const secure = configuredEnv.secure;
394
+
395
+ if (!subject) throw new Error('Missing subject. Provide --subject or ensure newsletter-preview.md includes **Subject:**.');
396
+ if (!fromEmail) throw new Error('Missing sender. Provide --from or ensure send-preview.md includes **From:**.');
397
+ if (recipients.length === 0) throw new Error('Missing recipients. Provide --to or ensure send-preview.md includes **To:**.');
398
+ if (mode === 'scheduled') throw new Error('Scheduled mode is not implemented in the native SMTP sender.');
399
+ if (!configuredEnv.smtpHost || !configuredEnv.smtpPort || !configuredEnv.smtpUser || !configuredEnv.smtpPass) {
400
+ throw new Error(
401
+ `Missing SMTP configuration. Checked ${configuredEnv.smtpHostKey}, ${configuredEnv.smtpPortKey}, ${configuredEnv.smtpUserKey}, ${configuredEnv.smtpPassKey}.`
402
+ );
403
+ }
404
+
405
+ const sentAt = new Date().toISOString();
406
+
407
+ try {
408
+ if (args.dryRun) {
409
+ const result = buildEmailSendResult({
410
+ status: 'sent',
411
+ mode,
412
+ messageId: 'dry-run',
413
+ from,
414
+ to: recipients,
415
+ scheduledAt,
416
+ sentAt,
417
+ notes: `Dry run only. SMTP connection not opened. Expected server ${configuredEnv.smtpHost}:${configuredEnv.smtpPort}.`,
418
+ });
419
+ await writeResult(args.outputPath, result);
420
+ return;
421
+ }
422
+
423
+ const message = buildMimeMessage({
424
+ from,
425
+ to: recipients,
426
+ replyTo,
427
+ subject,
428
+ html,
429
+ text,
430
+ });
431
+
432
+ const delivery = await sendViaSmtp({
433
+ host: configuredEnv.smtpHost,
434
+ port: Number(configuredEnv.smtpPort),
435
+ secure,
436
+ username: configuredEnv.smtpUser,
437
+ password: configuredEnv.smtpPass,
438
+ from: fromEmail,
439
+ to: recipients,
440
+ message,
441
+ });
442
+
443
+ const result = buildEmailSendResult({
444
+ status: 'sent',
445
+ mode,
446
+ messageId: delivery.messageId,
447
+ from,
448
+ to: recipients,
449
+ scheduledAt,
450
+ sentAt,
451
+ notes: `Accepted by SMTP server ${configuredEnv.smtpHost}:${configuredEnv.smtpPort}. ${delivery.response}`,
452
+ });
453
+ await writeResult(args.outputPath, result);
454
+ console.log(`SMTP send accepted with message ID ${delivery.messageId}`);
455
+ } catch (error) {
456
+ const errorMessage = formatUnknownError(error);
457
+ const result = buildEmailSendResult({
458
+ status: 'failed',
459
+ mode,
460
+ messageId: 'not-returned',
461
+ from,
462
+ to: recipients,
463
+ scheduledAt,
464
+ sentAt,
465
+ notes: errorMessage,
466
+ });
467
+ await writeResult(args.outputPath, result);
468
+ throw new Error(errorMessage);
469
+ }
470
+ }
471
+
472
+ const isMain = process.argv[1] === fileURLToPath(import.meta.url);
473
+ if (isMain) {
474
+ main().catch((error) => {
475
+ console.error(`\n❌ ${formatUnknownError(error)}`);
476
+ process.exit(1);
477
+ });
478
+ }