create-workframe 0.1.5 → 0.1.7

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 (185) hide show
  1. package/.dockerignore +22 -22
  2. package/.gitignore +73 -73
  3. package/LICENSE +201 -201
  4. package/NOTICE +12 -12
  5. package/README.md +40 -40
  6. package/SECURITY.md +38 -38
  7. package/bin/create-workframe.js +2816 -2814
  8. package/bin/workframe.js +329 -329
  9. package/docs/workspace-instructions/WORKFRAME_DISCORD.md +20 -20
  10. package/docs/workspace-instructions/WORKFRAME_DOCUMENTS_AND_ARTIFACTS.md +20 -20
  11. package/docs/workspace-instructions/WORKFRAME_KANBAN.md +20 -20
  12. package/docs/workspace-instructions/WORKFRAME_ONBOARDING.md +21 -21
  13. package/docs/workspace-instructions/WORKFRAME_ROUTING.md +29 -29
  14. package/docs/workspace-instructions/WORKFRAME_TELEGRAM.md +19 -19
  15. package/package.json +64 -64
  16. package/profiles/README.md +15 -15
  17. package/profiles/architect/AGENTS.md +29 -29
  18. package/profiles/architect/SOUL.md +44 -44
  19. package/profiles/architect/skills/devops/kanban-worker/SKILL.md +27 -27
  20. package/profiles/designer/AGENTS.md +26 -26
  21. package/profiles/designer/SOUL.md +31 -31
  22. package/profiles/designer/skills/devops/kanban-worker/SKILL.md +27 -27
  23. package/profiles/dev/AGENTS.md +28 -28
  24. package/profiles/dev/SOUL.md +31 -31
  25. package/profiles/dev/skills/devops/kanban-worker/SKILL.md +27 -27
  26. package/profiles/docs/AGENTS.md +27 -27
  27. package/profiles/docs/SOUL.md +30 -30
  28. package/profiles/docs/skills/devops/kanban-worker/SKILL.md +27 -27
  29. package/profiles/research/AGENTS.md +26 -26
  30. package/profiles/research/SOUL.md +31 -31
  31. package/profiles/research/skills/devops/kanban-worker/SKILL.md +27 -27
  32. package/profiles/visionary/AGENTS.md +25 -25
  33. package/profiles/visionary/SOUL.md +31 -31
  34. package/profiles/visionary/skills/devops/kanban-worker/SKILL.md +27 -27
  35. package/profiles/workframe-agent/AGENTS.md +37 -37
  36. package/profiles/workframe-agent/SETUP.md +185 -185
  37. package/profiles/workframe-agent/SOUL.md +61 -61
  38. package/profiles/workframe-agent/skills/devops/botfather/SKILL.md +85 -85
  39. package/profiles/workframe-agent/skills/devops/kanban-handoff-pattern/SKILL.md +58 -58
  40. package/profiles/workframe-agent/skills/devops/workframe-cohort/SKILL.md +54 -54
  41. package/prompts/WORKFRAME_PROMPT_TEMPLATES.md +16 -16
  42. package/rules/.hermes.md +11 -11
  43. package/rules/AGENTS.md +22 -22
  44. package/rules/workspace-README.md +5 -5
  45. package/scripts/apply-update-workframe.sh +91 -21
  46. package/scripts/bundle-workframe-ui.mjs +83 -83
  47. package/scripts/create_workframe_scaffold.py +648 -648
  48. package/scripts/ensure-compose-host-paths.mjs +51 -51
  49. package/scripts/lib/install-identity.mjs +212 -212
  50. package/scripts/lib/workframe-registry.mjs +290 -290
  51. package/scripts/new-project.mjs +68 -68
  52. package/scripts/select_agent_pack.py +31 -31
  53. package/scripts/set-compose-public-url.mjs +92 -92
  54. package/scripts/sync-canonical-to-package.mjs +164 -164
  55. package/shared/WORKFRAME_AGENT_LIBRARY.md +31 -31
  56. package/shared/WORKFRAME_AGENT_OPERATIONS.md +29 -29
  57. package/shared/WORKFRAME_AGENT_PACKS.json +64 -64
  58. package/shared/WORKFRAME_AGENT_PACKS.yaml +20 -20
  59. package/shared/WORKFRAME_CHAT_PERMISSION_MODEL.md +20 -20
  60. package/shared/WORKFRAME_HANDOFF_SCHEMA.md +25 -25
  61. package/shared/WORKFRAME_SKILL_CURATION.md +27 -27
  62. package/workframe-api/Dockerfile +14 -14
  63. package/workframe-api/README.md +24 -24
  64. package/workframe-api/action_proxy.py +131 -131
  65. package/workframe-api/auth_rate_limit.py +49 -49
  66. package/workframe-api/credential_vault.py +445 -445
  67. package/workframe-api/data/avatar-catalog.json +41 -41
  68. package/workframe-api/data/logo-catalog.json +14 -14
  69. package/workframe-api/data/user-avatar-catalog.json +18 -18
  70. package/workframe-api/email_sender.py +220 -220
  71. package/workframe-api/google_auth.py +90 -90
  72. package/workframe-api/install_api.py +359 -359
  73. package/workframe-api/internal_proxy_auth.py +150 -150
  74. package/workframe-api/llm_proxy.py +280 -280
  75. package/workframe-api/oidc_jwt.py +108 -108
  76. package/workframe-api/package.json +12 -12
  77. package/workframe-api/platform_auth.py +194 -194
  78. package/workframe-api/profile_secret_policy.py +86 -86
  79. package/workframe-api/public/assets/index-DPXu_lGn.css +1 -1
  80. package/workframe-api/public/assets/index-DYnLrCZZ.js +8 -8
  81. package/workframe-api/public/assets/index-DglUqFB_.js +8 -8
  82. package/workframe-api/public/index.html +12 -12
  83. package/workframe-api/requirements.txt +2 -2
  84. package/workframe-api/server.py +20295 -19866
  85. package/workframe-api/site_meta.py +271 -271
  86. package/workframe-api/stack_config.py +435 -427
  87. package/workframe-api/test_ensure_native_profile.py +29 -0
  88. package/workframe-api/test_oauth_llm.py +58 -0
  89. package/workframe-api/test_provider_model_surface.py +31 -0
  90. package/workframe-api/test_stack_config_install_smtp.py +51 -0
  91. package/workframe-api/time-bind-chat.py +99 -99
  92. package/workframe-api/turn_credentials.py +226 -226
  93. package/workframe-api/updates.py +506 -506
  94. package/workframe-api/vault_kek.py +159 -159
  95. package/workframe-api/zk_auth.py +633 -633
  96. package/workframe-supervisor/Dockerfile +11 -11
  97. package/workframe-supervisor/profile_secret_policy.py +76 -76
  98. package/workframe-supervisor/server.py +860 -860
  99. package/workframe-ui/docker/nginx.conf +85 -85
  100. package/workframe-ui/public/assets/{arc-CnriPN3G.js → arc-Cn-lt9fT.js} +1 -1
  101. package/workframe-ui/public/assets/architecture-7EHR7CIX-CCMNXHod.js +1 -0
  102. package/workframe-ui/public/assets/{architectureDiagram-3BPJPVTR-DYYB1Ic2.js → architectureDiagram-3BPJPVTR-qXoGCqfH.js} +1 -1
  103. package/workframe-ui/public/assets/{blockDiagram-GPEHLZMM-Bn-fFOyj.js → blockDiagram-GPEHLZMM-DoopFlWj.js} +1 -1
  104. package/workframe-ui/public/assets/{c4Diagram-AAUBKEIU-BPPDtAnD.js → c4Diagram-AAUBKEIU-BRa7hfzE.js} +1 -1
  105. package/workframe-ui/public/assets/channel-CtFV3sE3.js +1 -0
  106. package/workframe-ui/public/assets/{chunk-2J33WTMH-Bz2CJiC3.js → chunk-2J33WTMH-Cl33E2hh.js} +1 -1
  107. package/workframe-ui/public/assets/{chunk-3OPIFGDE-B8aMeE1B.js → chunk-3OPIFGDE-BRywYZQO.js} +1 -1
  108. package/workframe-ui/public/assets/{chunk-4BX2VUAB-bLFHQTuT.js → chunk-4BX2VUAB-CI3xzp-0.js} +1 -1
  109. package/workframe-ui/public/assets/{chunk-55IACEB6-BRxhx-lG.js → chunk-55IACEB6-Bkf3Ye3F.js} +1 -1
  110. package/workframe-ui/public/assets/{chunk-5ZQYHXKU-BLyOgEJ8.js → chunk-5ZQYHXKU-C5RN3hUT.js} +1 -1
  111. package/workframe-ui/public/assets/{chunk-727SXJPM-DiuvBl7G.js → chunk-727SXJPM-BaY0yDwt.js} +1 -1
  112. package/workframe-ui/public/assets/{chunk-AQP2D5EJ-BE1_aBdC.js → chunk-AQP2D5EJ-VMMBaDqd.js} +1 -1
  113. package/workframe-ui/public/assets/{chunk-BSJP7CBP-BicGsmBs.js → chunk-BSJP7CBP-DlNZ1UV3.js} +1 -1
  114. package/workframe-ui/public/assets/{chunk-CSCIHK7Q-C3VTns3d.js → chunk-CSCIHK7Q-tT2HyVy0.js} +1 -1
  115. package/workframe-ui/public/assets/{chunk-FMBD7UC4-DSiXY3bY.js → chunk-FMBD7UC4-CyACRaUT.js} +1 -1
  116. package/workframe-ui/public/assets/{chunk-KSCS5N6A-Cvw5s3fR.js → chunk-KSCS5N6A-seK5WAZs.js} +1 -1
  117. package/workframe-ui/public/assets/{chunk-L5ZTLDWV-jJifGdIQ.js → chunk-L5ZTLDWV-r5cyG5AH.js} +1 -1
  118. package/workframe-ui/public/assets/{chunk-LZXEDZCA-Maee1aqo.js → chunk-LZXEDZCA-SnbeVxsJ.js} +2 -2
  119. package/workframe-ui/public/assets/{chunk-ND2GUHAM-C77N3VAM.js → chunk-ND2GUHAM-zbFgEXSJ.js} +1 -1
  120. package/workframe-ui/public/assets/{chunk-NZK2D7GU-BY68lrPB.js → chunk-NZK2D7GU-UhZc8kOk.js} +1 -1
  121. package/workframe-ui/public/assets/{chunk-O5CBEL6O-D_c4kWzh.js → chunk-O5CBEL6O-DHvu2iZ-.js} +1 -1
  122. package/workframe-ui/public/assets/chunk-QZHKN3VN-B1g7l1_B.js +1 -0
  123. package/workframe-ui/public/assets/chunk-WU5MYG2G-B_qfZ6QP.js +1 -0
  124. package/workframe-ui/public/assets/{chunk-XPW4576I-DlQgvqBp.js → chunk-XPW4576I-CCPNKbzG.js} +1 -1
  125. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-CJoeCNq6.js +1 -0
  126. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-CJoeCNq6.js +1 -0
  127. package/workframe-ui/public/assets/{cose-bilkent-S5V4N54A-CHCOHDSd.js → cose-bilkent-S5V4N54A-D1CLe6Y0.js} +1 -1
  128. package/workframe-ui/public/assets/{dagre-BM42HDAG-CKPcSYwU.js → dagre-BM42HDAG-DYHcGVs_.js} +1 -1
  129. package/workframe-ui/public/assets/{diagram-2AECGRRQ-C6qe0jP-.js → diagram-2AECGRRQ-DVUkJ-II.js} +1 -1
  130. package/workframe-ui/public/assets/{diagram-5GNKFQAL-DhQhrieU.js → diagram-5GNKFQAL-C9Y1Acc2.js} +1 -1
  131. package/workframe-ui/public/assets/{diagram-KO2AKTUF-Bqf6Iw1r.js → diagram-KO2AKTUF-Db3zez-n.js} +1 -1
  132. package/workframe-ui/public/assets/{diagram-LMA3HP47-Bt1ms2D2.js → diagram-LMA3HP47-BaOR5sIr.js} +1 -1
  133. package/workframe-ui/public/assets/{diagram-OG6HWLK6-C0Rrxe1i.js → diagram-OG6HWLK6-fLBzMY5W.js} +1 -1
  134. package/workframe-ui/public/assets/{dist-BLcLhTn6.js → dist-uZYeD3o1.js} +1 -1
  135. package/workframe-ui/public/assets/{erDiagram-TEJ5UH35-Cy1GbZVu.js → erDiagram-TEJ5UH35-BoCxDSyE.js} +1 -1
  136. package/workframe-ui/public/assets/eventmodeling-FCH6USID-Bw910jho.js +1 -0
  137. package/workframe-ui/public/assets/{flowDiagram-I6XJVG4X-DL3ALqDG.js → flowDiagram-I6XJVG4X-BnYa3CAj.js} +1 -1
  138. package/workframe-ui/public/assets/{ganttDiagram-6RSMTGT7-CMpwBhXQ.js → ganttDiagram-6RSMTGT7-Dm_e_Kaa.js} +1 -1
  139. package/workframe-ui/public/assets/{gitGraph-WXDBUCRP-C0mwHUXL.js → gitGraph-WXDBUCRP-CBAj8ntv.js} +1 -1
  140. package/workframe-ui/public/assets/{gitGraphDiagram-PVQCEYII-6gzhWaF0.js → gitGraphDiagram-PVQCEYII-D5auJjJh.js} +1 -1
  141. package/workframe-ui/public/assets/index-DHLQNAXd.js +129 -0
  142. package/workframe-ui/public/assets/index-auJ0bLMU.css +1 -0
  143. package/workframe-ui/public/assets/{info-J43DQDTF-Bb_wZTxU.js → info-J43DQDTF-BNMADxE2.js} +1 -1
  144. package/workframe-ui/public/assets/{infoDiagram-5YYISTIA-601evYFV.js → infoDiagram-5YYISTIA-C8c45T4J.js} +1 -1
  145. package/workframe-ui/public/assets/{ishikawaDiagram-YF4QCWOH-BYtF-wHm.js → ishikawaDiagram-YF4QCWOH-CyWuHB4e.js} +1 -1
  146. package/workframe-ui/public/assets/{journeyDiagram-JHISSGLW-BMizyHzC.js → journeyDiagram-JHISSGLW-B43pCuDT.js} +1 -1
  147. package/workframe-ui/public/assets/{kanban-definition-UN3LZRKU-CLLJsdnh.js → kanban-definition-UN3LZRKU-Dyn480BI.js} +1 -1
  148. package/workframe-ui/public/assets/{line-BFiSvu-q.js → line-CNtIbnmt.js} +1 -1
  149. package/workframe-ui/public/assets/{linear-D6HYZ18_.js → linear-pmSg_4uV.js} +1 -1
  150. package/workframe-ui/public/assets/{mermaid-parser.core-Cid5yDBQ.js → mermaid-parser.core-BX4tnojE.js} +2 -2
  151. package/workframe-ui/public/assets/{mermaid.core-DqIiju5Z.js → mermaid.core-BtpBva5o.js} +3 -3
  152. package/workframe-ui/public/assets/{mindmap-definition-RKZ34NQL-Bg7xJdsz.js → mindmap-definition-RKZ34NQL-DcMcB_69.js} +1 -1
  153. package/workframe-ui/public/assets/{packet-YPE3B663-1ebGt86D.js → packet-YPE3B663-KSzK9WlD.js} +1 -1
  154. package/workframe-ui/public/assets/{pie-LRSECV5Y-DJ9wOBC_.js → pie-LRSECV5Y-DLOH6Mg8.js} +1 -1
  155. package/workframe-ui/public/assets/{pieDiagram-4H26LBE5-BmK96Aus.js → pieDiagram-4H26LBE5-BhrX2aGk.js} +1 -1
  156. package/workframe-ui/public/assets/{quadrantDiagram-W4KKPZXB-ByUqtWPZ.js → quadrantDiagram-W4KKPZXB-B5dDjbTX.js} +1 -1
  157. package/workframe-ui/public/assets/{radar-GUYGQ44K-DN-Hkt96.js → radar-GUYGQ44K-BPdjqZiu.js} +1 -1
  158. package/workframe-ui/public/assets/{requirementDiagram-4Y6WPE33-ByU2r8gb.js → requirementDiagram-4Y6WPE33-21Dq5e0N.js} +1 -1
  159. package/workframe-ui/public/assets/{sankeyDiagram-5OEKKPKP-elyWa0GK.js → sankeyDiagram-5OEKKPKP-CiA99dbO.js} +1 -1
  160. package/workframe-ui/public/assets/{sequenceDiagram-3UESZ5HK-CzZwsAiK.js → sequenceDiagram-3UESZ5HK-Bwpui7_x.js} +1 -1
  161. package/workframe-ui/public/assets/{src-5vgsM_dl.js → src-B3Axsi1U.js} +1 -1
  162. package/workframe-ui/public/assets/{stateDiagram-AJRCARHV-B0O5w5lJ.js → stateDiagram-AJRCARHV-C76qwtE8.js} +1 -1
  163. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-Dn0CDWJD.js +1 -0
  164. package/workframe-ui/public/assets/{timeline-definition-PNZ67QCA-oyqa5w5y.js → timeline-definition-PNZ67QCA-CufInOyk.js} +1 -1
  165. package/workframe-ui/public/assets/{treeView-BLDUP644-MCV12-eP.js → treeView-BLDUP644-C4sxh0R-.js} +1 -1
  166. package/workframe-ui/public/assets/{treemap-LRROVOQU-DiA1P9n5.js → treemap-LRROVOQU-BjV72ANV.js} +1 -1
  167. package/workframe-ui/public/assets/{vennDiagram-CIIHVFJN-CO_4x7OR.js → vennDiagram-CIIHVFJN-DgsdR0Rl.js} +1 -1
  168. package/workframe-ui/public/assets/{wardley-L42UT6IY-CmQNv6Xv.js → wardley-L42UT6IY-DZ279G_t.js} +1 -1
  169. package/workframe-ui/public/assets/{wardleyDiagram-YWT4CUSO-CJxDkbIj.js → wardleyDiagram-YWT4CUSO-Bde25kvz.js} +1 -1
  170. package/workframe-ui/public/assets/{xychartDiagram-2RQKCTM6-B02MxgzG.js → xychartDiagram-2RQKCTM6-B8VnS2Pb.js} +1 -1
  171. package/workframe-ui/public/favicon.svg +7 -7
  172. package/workframe-ui/public/icons.svg +24 -24
  173. package/workframe-ui/public/index.html +47 -47
  174. package/workframe-ui/public/manifest.webmanifest +18 -18
  175. package/workframe-ui/public/workframe-config.json +3 -3
  176. package/workframe-ui/public/assets/architecture-7EHR7CIX-CFhr7xAe.js +0 -1
  177. package/workframe-ui/public/assets/channel-Dok_-T0o.js +0 -1
  178. package/workframe-ui/public/assets/chunk-QZHKN3VN-C3wwLrTa.js +0 -1
  179. package/workframe-ui/public/assets/chunk-WU5MYG2G-GFFSg2kf.js +0 -1
  180. package/workframe-ui/public/assets/classDiagram-4FO5ZUOK-DKbGfT5Z.js +0 -1
  181. package/workframe-ui/public/assets/classDiagram-v2-Q7XG4LA2-DKbGfT5Z.js +0 -1
  182. package/workframe-ui/public/assets/eventmodeling-FCH6USID-C_PrceHe.js +0 -1
  183. package/workframe-ui/public/assets/index-DyO6K0TD.css +0 -1
  184. package/workframe-ui/public/assets/index-yvpSUs8T.js +0 -133
  185. package/workframe-ui/public/assets/stateDiagram-v2-BHNVJYJU-RXZRaSgT.js +0 -1
@@ -1,41 +1,41 @@
1
- {
2
- "version": 2,
3
- "public_base": "/assets/agents",
4
- "avatars": [
5
- { "id": "ada", "file": "ada.png", "label": "Ada" },
6
- { "id": "aibert", "file": "aibert.png", "label": "Aibert" },
7
- { "id": "amelia", "file": "amelia.png", "label": "Amelia" },
8
- { "id": "andy", "file": "andy.png", "label": "Andy" },
9
- { "id": "bob", "file": "bob.png", "label": "Bob" },
10
- { "id": "buzz", "file": "buzz.png", "label": "Buzz" },
11
- { "id": "carl", "file": "carl.png", "label": "Carl" },
12
- { "id": "corbu", "file": "corbu.png", "label": "Corbu" },
13
- { "id": "diana", "file": "diana.png", "label": "Diana" },
14
- { "id": "ella", "file": "ella.png", "label": "Ella" },
15
- { "id": "elvis", "file": "elvis.png", "label": "Elvis" },
16
- { "id": "frida", "file": "frida.png", "label": "Frida" },
17
- { "id": "george", "file": "george.png", "label": "George" },
18
- { "id": "grace", "file": "grace.png", "label": "Grace" },
19
- { "id": "hedy", "file": "hedy.png", "label": "Hedy" },
20
- { "id": "isaac", "file": "isaac.png", "label": "Isaac" },
21
- { "id": "john", "file": "john.png", "label": "John" },
22
- { "id": "joni", "file": "joni.png", "label": "Joni" },
23
- { "id": "leo", "file": "leo.png", "label": "Leo" },
24
- { "id": "louis", "file": "louis.png", "label": "Louis" },
25
- { "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
26
- { "id": "marie", "file": "marie.png", "label": "Marie" },
27
- { "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
28
- { "id": "neil", "file": "neil.png", "label": "Neil" },
29
- { "id": "nikola", "file": "nikola.png", "label": "Nikola" },
30
- { "id": "nina", "file": "nina.png", "label": "Nina" },
31
- { "id": "paul", "file": "paul.png", "label": "Paul" },
32
- { "id": "ringo", "file": "ringo.png", "label": "Ringo" },
33
- { "id": "rosie", "file": "rosie.png", "label": "Rosie" },
34
- { "id": "steve", "file": "steve.png", "label": "Steve" },
35
- { "id": "sun", "file": "sun.png", "label": "Sun" },
36
- { "id": "tom", "file": "tom.png", "label": "Tom" },
37
- { "id": "warren", "file": "warren.png", "label": "Warren" },
38
- { "id": "woz", "file": "woz.png", "label": "Woz" },
39
- { "id": "zaha", "file": "zaha.png", "label": "Zaha" }
40
- ]
41
- }
1
+ {
2
+ "version": 2,
3
+ "public_base": "/assets/agents",
4
+ "avatars": [
5
+ { "id": "ada", "file": "ada.png", "label": "Ada" },
6
+ { "id": "aibert", "file": "aibert.png", "label": "Aibert" },
7
+ { "id": "amelia", "file": "amelia.png", "label": "Amelia" },
8
+ { "id": "andy", "file": "andy.png", "label": "Andy" },
9
+ { "id": "bob", "file": "bob.png", "label": "Bob" },
10
+ { "id": "buzz", "file": "buzz.png", "label": "Buzz" },
11
+ { "id": "carl", "file": "carl.png", "label": "Carl" },
12
+ { "id": "corbu", "file": "corbu.png", "label": "Corbu" },
13
+ { "id": "diana", "file": "diana.png", "label": "Diana" },
14
+ { "id": "ella", "file": "ella.png", "label": "Ella" },
15
+ { "id": "elvis", "file": "elvis.png", "label": "Elvis" },
16
+ { "id": "frida", "file": "frida.png", "label": "Frida" },
17
+ { "id": "george", "file": "george.png", "label": "George" },
18
+ { "id": "grace", "file": "grace.png", "label": "Grace" },
19
+ { "id": "hedy", "file": "hedy.png", "label": "Hedy" },
20
+ { "id": "isaac", "file": "isaac.png", "label": "Isaac" },
21
+ { "id": "john", "file": "john.png", "label": "John" },
22
+ { "id": "joni", "file": "joni.png", "label": "Joni" },
23
+ { "id": "leo", "file": "leo.png", "label": "Leo" },
24
+ { "id": "louis", "file": "louis.png", "label": "Louis" },
25
+ { "id": "ludwig", "file": "ludwig.png", "label": "Ludwig" },
26
+ { "id": "marie", "file": "marie.png", "label": "Marie" },
27
+ { "id": "marilyn", "file": "marilyn.png", "label": "Marilyn" },
28
+ { "id": "neil", "file": "neil.png", "label": "Neil" },
29
+ { "id": "nikola", "file": "nikola.png", "label": "Nikola" },
30
+ { "id": "nina", "file": "nina.png", "label": "Nina" },
31
+ { "id": "paul", "file": "paul.png", "label": "Paul" },
32
+ { "id": "ringo", "file": "ringo.png", "label": "Ringo" },
33
+ { "id": "rosie", "file": "rosie.png", "label": "Rosie" },
34
+ { "id": "steve", "file": "steve.png", "label": "Steve" },
35
+ { "id": "sun", "file": "sun.png", "label": "Sun" },
36
+ { "id": "tom", "file": "tom.png", "label": "Tom" },
37
+ { "id": "warren", "file": "warren.png", "label": "Warren" },
38
+ { "id": "woz", "file": "woz.png", "label": "Woz" },
39
+ { "id": "zaha", "file": "zaha.png", "label": "Zaha" }
40
+ ]
41
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "version": 1,
3
- "public_base": "/assets/project-logos",
4
- "logos": [
5
- { "id": "1", "file": "1.png", "label": "Logo 1" },
6
- { "id": "2", "file": "2.png", "label": "Logo 2" },
7
- { "id": "3", "file": "3.png", "label": "Logo 3" },
8
- { "id": "4", "file": "4.png", "label": "Logo 4" },
9
- { "id": "5", "file": "5.png", "label": "Logo 5" },
10
- { "id": "6", "file": "6.png", "label": "Logo 6" },
11
- { "id": "7", "file": "7.png", "label": "Logo 7" },
12
- { "id": "8", "file": "8.png", "label": "Logo 8" }
13
- ]
14
- }
1
+ {
2
+ "version": 1,
3
+ "public_base": "/assets/project-logos",
4
+ "logos": [
5
+ { "id": "1", "file": "1.png", "label": "Logo 1" },
6
+ { "id": "2", "file": "2.png", "label": "Logo 2" },
7
+ { "id": "3", "file": "3.png", "label": "Logo 3" },
8
+ { "id": "4", "file": "4.png", "label": "Logo 4" },
9
+ { "id": "5", "file": "5.png", "label": "Logo 5" },
10
+ { "id": "6", "file": "6.png", "label": "Logo 6" },
11
+ { "id": "7", "file": "7.png", "label": "Logo 7" },
12
+ { "id": "8", "file": "8.png", "label": "Logo 8" }
13
+ ]
14
+ }
@@ -1,18 +1,18 @@
1
- {
2
- "version": 1,
3
- "public_base": "/assets/users",
4
- "avatars": [
5
- { "id": "m1", "file": "m1.png", "label": "M1" },
6
- { "id": "m2", "file": "m2.png", "label": "M2" },
7
- { "id": "m3", "file": "m3.png", "label": "M3" },
8
- { "id": "m4", "file": "m4.png", "label": "M4" },
9
- { "id": "m5", "file": "m5.png", "label": "M5" },
10
- { "id": "m6", "file": "m6.png", "label": "M6" },
11
- { "id": "f1", "file": "f1.png", "label": "F1" },
12
- { "id": "f2", "file": "f2.png", "label": "F2" },
13
- { "id": "f3", "file": "f3.png", "label": "F3" },
14
- { "id": "f4", "file": "f4.png", "label": "F4" },
15
- { "id": "f5", "file": "f5.png", "label": "F5" },
16
- { "id": "f6", "file": "f6.png", "label": "F6" }
17
- ]
18
- }
1
+ {
2
+ "version": 1,
3
+ "public_base": "/assets/users",
4
+ "avatars": [
5
+ { "id": "m1", "file": "m1.png", "label": "M1" },
6
+ { "id": "m2", "file": "m2.png", "label": "M2" },
7
+ { "id": "m3", "file": "m3.png", "label": "M3" },
8
+ { "id": "m4", "file": "m4.png", "label": "M4" },
9
+ { "id": "m5", "file": "m5.png", "label": "M5" },
10
+ { "id": "m6", "file": "m6.png", "label": "M6" },
11
+ { "id": "f1", "file": "f1.png", "label": "F1" },
12
+ { "id": "f2", "file": "f2.png", "label": "F2" },
13
+ { "id": "f3", "file": "f3.png", "label": "F3" },
14
+ { "id": "f4", "file": "f4.png", "label": "F4" },
15
+ { "id": "f5", "file": "f5.png", "label": "F5" },
16
+ { "id": "f6", "file": "f6.png", "label": "F6" }
17
+ ]
18
+ }
@@ -1,220 +1,220 @@
1
- """
2
- Email sender module for Workframe API.
3
- Sends OTP verification emails via SMTP (env or stack_config).
4
- """
5
-
6
- from __future__ import annotations
7
-
8
- import os
9
- import smtplib
10
- from email.mime.multipart import MIMEMultipart
11
- from email.mime.text import MIMEText
12
- from typing import Any
13
-
14
- import stack_config
15
-
16
- APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
17
-
18
-
19
- def _tls_flags(cfg: dict[str, Any]) -> tuple[bool, bool]:
20
- port = int(cfg.get("port") or 587)
21
- secure = stack_config.normalize_smtp_secure(port, str(cfg.get("secure") or ""))
22
- use_ssl = secure == "ssl"
23
- use_tls = secure == "starttls"
24
- return use_ssl, use_tls
25
-
26
-
27
- def _smtp_send(msg: MIMEMultipart, to_email: str, cfg: dict[str, Any]) -> None:
28
- host = str(cfg.get("host") or "").strip()
29
- if not host:
30
- raise RuntimeError("SMTP_HOST not configured")
31
- port = int(cfg.get("port") or 587)
32
- user = str(cfg.get("user") or "").strip()
33
- password = str(cfg.get("password") or "").strip().replace(" ", "")
34
- from_addr = str(cfg.get("from") or user or "").strip()
35
- if not from_addr:
36
- raise RuntimeError("SMTP from address not configured")
37
- if user and not password:
38
- raise RuntimeError("SMTP password is required when SMTP user is set")
39
- msg["From"] = from_addr
40
- use_ssl, use_tls = _tls_flags(cfg)
41
- try:
42
- if use_ssl:
43
- server = smtplib.SMTP_SSL(host, port, timeout=30)
44
- else:
45
- server = smtplib.SMTP(host, port, timeout=30)
46
- with server:
47
- server.ehlo()
48
- if use_tls and not use_ssl:
49
- server.starttls()
50
- server.ehlo()
51
- if user:
52
- server.login(user, password)
53
- server.sendmail(from_addr, [to_email], msg.as_string())
54
- except smtplib.SMTPAuthenticationError as exc:
55
- raise RuntimeError(f"SMTP login failed: {exc}") from exc
56
- except smtplib.SMTPSenderRefused as exc:
57
- sender = str(getattr(exc, "sender", "") or from_addr)
58
- if user and sender.lower() != user.lower():
59
- raise RuntimeError(
60
- f"SMTP rejected From address {sender!r} for login {user!r}"
61
- ) from exc
62
- raise RuntimeError(f"SMTP error: {exc}") from exc
63
- except smtplib.SMTPException as exc:
64
- raise RuntimeError(f"SMTP error: {exc}") from exc
65
- except OSError as exc:
66
- raise RuntimeError(f"Network error sending email: {exc}") from exc
67
-
68
-
69
- def _active_smtp() -> dict[str, Any]:
70
- cfg = stack_config.resolved_smtp()
71
- if cfg.get("source") == "none":
72
- return {}
73
- return cfg
74
-
75
-
76
- def send_email_with_config(
77
- to_email: str,
78
- subject: str,
79
- body_text: str,
80
- body_html: str = "",
81
- cfg: dict[str, Any] | None = None,
82
- ) -> None:
83
- smtp = cfg or _active_smtp()
84
- if not smtp.get("host"):
85
- raise RuntimeError("SMTP_HOST not configured")
86
- msg = MIMEMultipart("alternative")
87
- msg["Subject"] = subject
88
- msg["To"] = to_email
89
- msg.attach(MIMEText(body_text, "plain"))
90
- if body_html:
91
- msg.attach(MIMEText(body_html, "html"))
92
- _smtp_send(msg, to_email, smtp)
93
-
94
-
95
- def _branded_html(
96
- *,
97
- brand: str,
98
- headline: str,
99
- intro: str,
100
- body_html: str,
101
- logo_url: str = "",
102
- footer: str = "If you didn't request this, you can safely ignore this email.",
103
- ) -> str:
104
- logo_block = (
105
- f'<img src="{logo_url}" alt="" style="height:40px;margin-bottom:12px;" />'
106
- if logo_url
107
- else ""
108
- )
109
- return f"""\
110
- <html>
111
- <body style="margin:0; background:#f6f7fb; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color:#1d2340;">
112
- <div style="max-width:560px; margin:0 auto; padding:32px 20px;">
113
- <div style="background:#ffffff; border:1px solid #e4e7f2; border-radius:20px; padding:32px; box-shadow:0 24px 64px rgba(14,20,49,0.08);">
114
- {logo_block}
115
- <div style="font-size:12px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#6c5ce7; margin-bottom:12px;">{brand}</div>
116
- <h1 style="margin:0 0 12px; font-size:28px; line-height:1.2;">{headline}</h1>
117
- <p style="margin:0 0 24px; color:#4f5878; font-size:16px; line-height:1.6;">{intro}</p>
118
- {body_html}
119
- <p style="margin:24px 0 0; color:#7b849d; font-size:12px; line-height:1.6;">{footer}</p>
120
- </div>
121
- </div>
122
- </body>
123
- </html>
124
- """
125
-
126
-
127
- def _code_block_html(code: str) -> str:
128
- return f"""\
129
- <div style="background:linear-gradient(180deg, #f5f7ff 0%, #eef1ff 100%); border:1px solid #d9defa; border-radius:16px; padding:18px 20px; text-align:center; margin-bottom:20px;">
130
- <div style="font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#6b7280; margin-bottom:10px;">Verification code</div>
131
- <div style="font-size:34px; font-weight:800; letter-spacing:10px; color:#111827; font-variant-numeric:tabular-nums;">{code}</div>
132
- </div>
133
- <p style="margin:0 0 20px; color:#4f5878; font-size:14px; line-height:1.6;">This code expires in 10 minutes.</p>
134
- """
135
-
136
-
137
- def _cta_button_html(url: str, label: str) -> str:
138
- return (
139
- f'<a href="{url}" style="display:inline-block; padding:12px 20px; background:#6c5ce7; '
140
- f'color:#ffffff; text-decoration:none; border-radius:10px; font-weight:700;">{label}</a>'
141
- )
142
-
143
-
144
- def _build_verification_email(
145
- to_email: str,
146
- code: str,
147
- verification_url: str,
148
- workspace_name: str = "Workframe",
149
- logo_url: str = "",
150
- ) -> MIMEMultipart:
151
- msg = MIMEMultipart("alternative")
152
- brand = workspace_name or "Workframe"
153
- msg["Subject"] = f"Your {brand} sign-in code"
154
- msg["To"] = to_email
155
- text_body = f"""\
156
- {brand} sign-in code
157
-
158
- Your verification code is:
159
-
160
- {code}
161
-
162
- This code expires in 10 minutes.
163
-
164
- Or click the link to verify:
165
- {verification_url}
166
-
167
- If you didn't request this, ignore this email.
168
- """
169
- body_html = _code_block_html(code) + _cta_button_html(verification_url, "Verify now")
170
- html_body = _branded_html(
171
- brand=brand,
172
- headline="Sign in with your code",
173
- intro="Use this one-time code to finish signing in to Workframe.",
174
- body_html=body_html,
175
- logo_url=logo_url,
176
- )
177
- msg.attach(MIMEText(text_body, "plain"))
178
- msg.attach(MIMEText(html_body, "html"))
179
- return msg
180
-
181
-
182
- def send_verification_email(
183
- to_email: str,
184
- code: str,
185
- verification_url: str,
186
- workspace_name: str = "",
187
- logo_url: str = "",
188
- ) -> None:
189
- msg = _build_verification_email(to_email, code, verification_url, workspace_name, logo_url)
190
- _smtp_send(msg, to_email, _active_smtp())
191
-
192
-
193
- def send_email(to_email: str, subject: str, body_text: str, body_html: str = "") -> None:
194
- send_email_with_config(to_email, subject, body_text, body_html)
195
-
196
-
197
- def send_branded_invite_email(
198
- to_email: str,
199
- workspace_name: str,
200
- invite_url: str,
201
- logo_url: str = "",
202
- ) -> None:
203
- brand = workspace_name or "Workframe"
204
- subject = f"Join {brand} on Workframe"
205
- text = f"You were invited to {brand}.\n\nAccept: {invite_url}\n"
206
- body_html = _cta_button_html(invite_url, "Accept invite")
207
- html = _branded_html(
208
- brand=brand,
209
- headline=f"Join {brand}",
210
- intro="You were invited to collaborate on Workframe.",
211
- body_html=body_html,
212
- logo_url=logo_url,
213
- footer="If you weren't expecting this invite, you can ignore this email.",
214
- )
215
- send_email_with_config(to_email, subject, text, html)
216
-
217
-
218
- if __name__ == "__main__":
219
- html = _build_verification_email("test@example.com", "123456", "http://127.0.0.1:18644/?code=123456").as_string()
220
- assert "Verification code" in html and "Verify now" in html
1
+ """
2
+ Email sender module for Workframe API.
3
+ Sends OTP verification emails via SMTP (env or stack_config).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import smtplib
10
+ from email.mime.multipart import MIMEMultipart
11
+ from email.mime.text import MIMEText
12
+ from typing import Any
13
+
14
+ import stack_config
15
+
16
+ APP_BASE_URL = os.environ.get("APP_BASE_URL", "http://127.0.0.1:18644").rstrip("/")
17
+
18
+
19
+ def _tls_flags(cfg: dict[str, Any]) -> tuple[bool, bool]:
20
+ port = int(cfg.get("port") or 587)
21
+ secure = stack_config.normalize_smtp_secure(port, str(cfg.get("secure") or ""))
22
+ use_ssl = secure == "ssl"
23
+ use_tls = secure == "starttls"
24
+ return use_ssl, use_tls
25
+
26
+
27
+ def _smtp_send(msg: MIMEMultipart, to_email: str, cfg: dict[str, Any]) -> None:
28
+ host = str(cfg.get("host") or "").strip()
29
+ if not host:
30
+ raise RuntimeError("SMTP_HOST not configured")
31
+ port = int(cfg.get("port") or 587)
32
+ user = str(cfg.get("user") or "").strip()
33
+ password = str(cfg.get("password") or "").strip().replace(" ", "")
34
+ from_addr = str(cfg.get("from") or user or "").strip()
35
+ if not from_addr:
36
+ raise RuntimeError("SMTP from address not configured")
37
+ if user and not password:
38
+ raise RuntimeError("SMTP password is required when SMTP user is set")
39
+ msg["From"] = from_addr
40
+ use_ssl, use_tls = _tls_flags(cfg)
41
+ try:
42
+ if use_ssl:
43
+ server = smtplib.SMTP_SSL(host, port, timeout=30)
44
+ else:
45
+ server = smtplib.SMTP(host, port, timeout=30)
46
+ with server:
47
+ server.ehlo()
48
+ if use_tls and not use_ssl:
49
+ server.starttls()
50
+ server.ehlo()
51
+ if user:
52
+ server.login(user, password)
53
+ server.sendmail(from_addr, [to_email], msg.as_string())
54
+ except smtplib.SMTPAuthenticationError as exc:
55
+ raise RuntimeError(f"SMTP login failed: {exc}") from exc
56
+ except smtplib.SMTPSenderRefused as exc:
57
+ sender = str(getattr(exc, "sender", "") or from_addr)
58
+ if user and sender.lower() != user.lower():
59
+ raise RuntimeError(
60
+ f"SMTP rejected From address {sender!r} for login {user!r}"
61
+ ) from exc
62
+ raise RuntimeError(f"SMTP error: {exc}") from exc
63
+ except smtplib.SMTPException as exc:
64
+ raise RuntimeError(f"SMTP error: {exc}") from exc
65
+ except OSError as exc:
66
+ raise RuntimeError(f"Network error sending email: {exc}") from exc
67
+
68
+
69
+ def _active_smtp() -> dict[str, Any]:
70
+ cfg = stack_config.resolved_smtp()
71
+ if cfg.get("source") == "none":
72
+ return {}
73
+ return cfg
74
+
75
+
76
+ def send_email_with_config(
77
+ to_email: str,
78
+ subject: str,
79
+ body_text: str,
80
+ body_html: str = "",
81
+ cfg: dict[str, Any] | None = None,
82
+ ) -> None:
83
+ smtp = cfg or _active_smtp()
84
+ if not smtp.get("host"):
85
+ raise RuntimeError("SMTP_HOST not configured")
86
+ msg = MIMEMultipart("alternative")
87
+ msg["Subject"] = subject
88
+ msg["To"] = to_email
89
+ msg.attach(MIMEText(body_text, "plain"))
90
+ if body_html:
91
+ msg.attach(MIMEText(body_html, "html"))
92
+ _smtp_send(msg, to_email, smtp)
93
+
94
+
95
+ def _branded_html(
96
+ *,
97
+ brand: str,
98
+ headline: str,
99
+ intro: str,
100
+ body_html: str,
101
+ logo_url: str = "",
102
+ footer: str = "If you didn't request this, you can safely ignore this email.",
103
+ ) -> str:
104
+ logo_block = (
105
+ f'<img src="{logo_url}" alt="" style="height:40px;margin-bottom:12px;" />'
106
+ if logo_url
107
+ else ""
108
+ )
109
+ return f"""\
110
+ <html>
111
+ <body style="margin:0; background:#f6f7fb; font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color:#1d2340;">
112
+ <div style="max-width:560px; margin:0 auto; padding:32px 20px;">
113
+ <div style="background:#ffffff; border:1px solid #e4e7f2; border-radius:20px; padding:32px; box-shadow:0 24px 64px rgba(14,20,49,0.08);">
114
+ {logo_block}
115
+ <div style="font-size:12px; font-weight:700; letter-spacing:0.14em; text-transform:uppercase; color:#6c5ce7; margin-bottom:12px;">{brand}</div>
116
+ <h1 style="margin:0 0 12px; font-size:28px; line-height:1.2;">{headline}</h1>
117
+ <p style="margin:0 0 24px; color:#4f5878; font-size:16px; line-height:1.6;">{intro}</p>
118
+ {body_html}
119
+ <p style="margin:24px 0 0; color:#7b849d; font-size:12px; line-height:1.6;">{footer}</p>
120
+ </div>
121
+ </div>
122
+ </body>
123
+ </html>
124
+ """
125
+
126
+
127
+ def _code_block_html(code: str) -> str:
128
+ return f"""\
129
+ <div style="background:linear-gradient(180deg, #f5f7ff 0%, #eef1ff 100%); border:1px solid #d9defa; border-radius:16px; padding:18px 20px; text-align:center; margin-bottom:20px;">
130
+ <div style="font-size:12px; font-weight:700; letter-spacing:0.08em; text-transform:uppercase; color:#6b7280; margin-bottom:10px;">Verification code</div>
131
+ <div style="font-size:34px; font-weight:800; letter-spacing:10px; color:#111827; font-variant-numeric:tabular-nums;">{code}</div>
132
+ </div>
133
+ <p style="margin:0 0 20px; color:#4f5878; font-size:14px; line-height:1.6;">This code expires in 10 minutes.</p>
134
+ """
135
+
136
+
137
+ def _cta_button_html(url: str, label: str) -> str:
138
+ return (
139
+ f'<a href="{url}" style="display:inline-block; padding:12px 20px; background:#6c5ce7; '
140
+ f'color:#ffffff; text-decoration:none; border-radius:10px; font-weight:700;">{label}</a>'
141
+ )
142
+
143
+
144
+ def _build_verification_email(
145
+ to_email: str,
146
+ code: str,
147
+ verification_url: str,
148
+ workspace_name: str = "Workframe",
149
+ logo_url: str = "",
150
+ ) -> MIMEMultipart:
151
+ msg = MIMEMultipart("alternative")
152
+ brand = workspace_name or "Workframe"
153
+ msg["Subject"] = f"Your {brand} sign-in code"
154
+ msg["To"] = to_email
155
+ text_body = f"""\
156
+ {brand} sign-in code
157
+
158
+ Your verification code is:
159
+
160
+ {code}
161
+
162
+ This code expires in 10 minutes.
163
+
164
+ Or click the link to verify:
165
+ {verification_url}
166
+
167
+ If you didn't request this, ignore this email.
168
+ """
169
+ body_html = _code_block_html(code) + _cta_button_html(verification_url, "Verify now")
170
+ html_body = _branded_html(
171
+ brand=brand,
172
+ headline="Sign in with your code",
173
+ intro="Use this one-time code to finish signing in to Workframe.",
174
+ body_html=body_html,
175
+ logo_url=logo_url,
176
+ )
177
+ msg.attach(MIMEText(text_body, "plain"))
178
+ msg.attach(MIMEText(html_body, "html"))
179
+ return msg
180
+
181
+
182
+ def send_verification_email(
183
+ to_email: str,
184
+ code: str,
185
+ verification_url: str,
186
+ workspace_name: str = "",
187
+ logo_url: str = "",
188
+ ) -> None:
189
+ msg = _build_verification_email(to_email, code, verification_url, workspace_name, logo_url)
190
+ _smtp_send(msg, to_email, _active_smtp())
191
+
192
+
193
+ def send_email(to_email: str, subject: str, body_text: str, body_html: str = "") -> None:
194
+ send_email_with_config(to_email, subject, body_text, body_html)
195
+
196
+
197
+ def send_branded_invite_email(
198
+ to_email: str,
199
+ workspace_name: str,
200
+ invite_url: str,
201
+ logo_url: str = "",
202
+ ) -> None:
203
+ brand = workspace_name or "Workframe"
204
+ subject = f"Join {brand} on Workframe"
205
+ text = f"You were invited to {brand}.\n\nAccept: {invite_url}\n"
206
+ body_html = _cta_button_html(invite_url, "Accept invite")
207
+ html = _branded_html(
208
+ brand=brand,
209
+ headline=f"Join {brand}",
210
+ intro="You were invited to collaborate on Workframe.",
211
+ body_html=body_html,
212
+ logo_url=logo_url,
213
+ footer="If you weren't expecting this invite, you can ignore this email.",
214
+ )
215
+ send_email_with_config(to_email, subject, text, html)
216
+
217
+
218
+ if __name__ == "__main__":
219
+ html = _build_verification_email("test@example.com", "123456", "http://127.0.0.1:18644/?code=123456").as_string()
220
+ assert "Verification code" in html and "Verify now" in html