clawed 2.5.2__tar.gz → 2.5.3__tar.gz

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 (258) hide show
  1. {clawed-2.5.2 → clawed-2.5.3}/PKG-INFO +1 -1
  2. {clawed-2.5.2 → clawed-2.5.3}/clawed/__init__.py +1 -1
  3. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/generate.py +35 -0
  4. {clawed-2.5.2 → clawed-2.5.3}/clawed/compile_game.py +17 -10
  5. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_pptx.py +77 -43
  6. clawed-2.5.3/clawed/review_output.py +253 -0
  7. {clawed-2.5.2 → clawed-2.5.3}/pyproject.toml +1 -1
  8. {clawed-2.5.2 → clawed-2.5.3}/.gitignore +0 -0
  9. {clawed-2.5.2 → clawed-2.5.3}/LICENSE +0 -0
  10. {clawed-2.5.2 → clawed-2.5.3}/README.md +0 -0
  11. {clawed-2.5.2 → clawed-2.5.3}/clawed/__main__.py +0 -0
  12. {clawed-2.5.2 → clawed-2.5.3}/clawed/_legacy_gateway.py +0 -0
  13. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent.py +0 -0
  14. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/__init__.py +0 -0
  15. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/approvals.py +0 -0
  16. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/autonomy.py +0 -0
  17. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/context.py +0 -0
  18. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/core.py +0 -0
  19. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/custom_tools.py +0 -0
  20. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/drive/__init__.py +0 -0
  21. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/drive/auth.py +0 -0
  22. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/drive/client.py +0 -0
  23. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/fake_llm.py +0 -0
  24. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/loop.py +0 -0
  25. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/__init__.py +0 -0
  26. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/curriculum.py +0 -0
  27. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/curriculum_kb.py +0 -0
  28. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/embeddings.py +0 -0
  29. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/episodes.py +0 -0
  30. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/identity.py +0 -0
  31. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/loader.py +0 -0
  32. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/memory/preferences.py +0 -0
  33. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/planner.py +0 -0
  34. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/prompt.py +0 -0
  35. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/scheduler.py +0 -0
  36. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/__init__.py +0 -0
  37. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/base.py +0 -0
  38. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/configure_profile.py +0 -0
  39. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/curriculum_map.py +0 -0
  40. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_doc.py +0 -0
  41. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_create_slides.py +0 -0
  42. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_list.py +0 -0
  43. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_organize.py +0 -0
  44. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_read.py +0 -0
  45. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/drive_upload.py +0 -0
  46. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/export_document.py +0 -0
  47. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/gap_analysis.py +0 -0
  48. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/generate_assessment.py +0 -0
  49. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson.py +0 -0
  50. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/generate_lesson_bundle.py +0 -0
  51. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/generate_materials.py +0 -0
  52. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/generate_unit.py +0 -0
  53. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/ingest_materials.py +0 -0
  54. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/parent_comm.py +0 -0
  55. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/read_heartbeat.py +0 -0
  56. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/read_workspace.py +0 -0
  57. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/request_approval.py +0 -0
  58. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/schedule_task.py +0 -0
  59. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/search_lessons.py +0 -0
  60. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/search_my_materials.py +0 -0
  61. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/search_standards.py +0 -0
  62. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/student_insights.py +0 -0
  63. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/sub_packet.py +0 -0
  64. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/switch_model.py +0 -0
  65. {clawed-2.5.2 → clawed-2.5.3}/clawed/agent_core/tools/update_soul.py +0 -0
  66. {clawed-2.5.2 → clawed-2.5.3}/clawed/analytics.py +0 -0
  67. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/__init__.py +0 -0
  68. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/deps.py +0 -0
  69. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/__init__.py +0 -0
  70. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/chat.py +0 -0
  71. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/export.py +0 -0
  72. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/feedback.py +0 -0
  73. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/gateway_chat.py +0 -0
  74. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/generate.py +0 -0
  75. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/ingest.py +0 -0
  76. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/lessons.py +0 -0
  77. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/school.py +0 -0
  78. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/settings.py +0 -0
  79. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/routes/tools.py +0 -0
  80. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/server.py +0 -0
  81. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/static/app.js +0 -0
  82. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/static/style.css +0 -0
  83. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/static/widget.js +0 -0
  84. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/analytics.html +0 -0
  85. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/base.html +0 -0
  86. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/dashboard.html +0 -0
  87. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/generate.html +0 -0
  88. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/index.html +0 -0
  89. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/lesson.html +0 -0
  90. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/profile.html +0 -0
  91. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/settings.html +0 -0
  92. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/stats.html +0 -0
  93. {clawed-2.5.2 → clawed-2.5.3}/clawed/api/templates/students.html +0 -0
  94. {clawed-2.5.2 → clawed-2.5.3}/clawed/assessment.py +0 -0
  95. {clawed-2.5.2 → clawed-2.5.3}/clawed/asset_registry.py +0 -0
  96. {clawed-2.5.2 → clawed-2.5.3}/clawed/async_utils.py +0 -0
  97. {clawed-2.5.2 → clawed-2.5.3}/clawed/auth/__init__.py +0 -0
  98. {clawed-2.5.2 → clawed-2.5.3}/clawed/auth/google_auth.py +0 -0
  99. {clawed-2.5.2 → clawed-2.5.3}/clawed/bot_state.py +0 -0
  100. {clawed-2.5.2 → clawed-2.5.3}/clawed/chat.py +0 -0
  101. {clawed-2.5.2 → clawed-2.5.3}/clawed/cli.py +0 -0
  102. {clawed-2.5.2 → clawed-2.5.3}/clawed/cli_chat.py +0 -0
  103. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/__init__.py +0 -0
  104. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/_helpers.py +0 -0
  105. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/bot.py +0 -0
  106. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/config.py +0 -0
  107. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/config_llm.py +0 -0
  108. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/config_profile.py +0 -0
  109. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/export.py +0 -0
  110. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/game.py +0 -0
  111. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/generate_assessment.py +0 -0
  112. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/generate_unit.py +0 -0
  113. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/queue.py +0 -0
  114. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/schedule_cmd.py +0 -0
  115. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/sub.py +0 -0
  116. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/train.py +0 -0
  117. {clawed-2.5.2 → clawed-2.5.3}/clawed/commands/workspace_cmd.py +0 -0
  118. {clawed-2.5.2 → clawed-2.5.3}/clawed/compile_slides.py +0 -0
  119. {clawed-2.5.2 → clawed-2.5.3}/clawed/compile_student.py +0 -0
  120. {clawed-2.5.2 → clawed-2.5.3}/clawed/compile_teacher.py +0 -0
  121. {clawed-2.5.2 → clawed-2.5.3}/clawed/config.py +0 -0
  122. {clawed-2.5.2 → clawed-2.5.3}/clawed/corpus.py +0 -0
  123. {clawed-2.5.2 → clawed-2.5.3}/clawed/curriculum_map.py +0 -0
  124. {clawed-2.5.2 → clawed-2.5.3}/clawed/database.py +0 -0
  125. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/__init__.py +0 -0
  126. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_assessment.json +0 -0
  127. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_formative_assessment.json +0 -0
  128. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_lesson_materials.json +0 -0
  129. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_lesson_science_g6.json +0 -0
  130. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_lesson_social_studies_g8.json +0 -0
  131. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_master_content.json +0 -0
  132. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_pacing_guide.json +0 -0
  133. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_quiz.json +0 -0
  134. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_rubric.json +0 -0
  135. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_unit_plan.json +0 -0
  136. {clawed-2.5.2 → clawed-2.5.3}/clawed/demo/demo_year_map.json +0 -0
  137. {clawed-2.5.2 → clawed-2.5.3}/clawed/differentiation.py +0 -0
  138. {clawed-2.5.2 → clawed-2.5.3}/clawed/doc_export.py +0 -0
  139. {clawed-2.5.2 → clawed-2.5.3}/clawed/drive.py +0 -0
  140. {clawed-2.5.2 → clawed-2.5.3}/clawed/evaluation.py +0 -0
  141. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_docx.py +0 -0
  142. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_handout.py +0 -0
  143. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_markdown.py +0 -0
  144. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_pdf.py +0 -0
  145. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_templates.py +0 -0
  146. {clawed-2.5.2 → clawed-2.5.3}/clawed/export_theme.py +0 -0
  147. {clawed-2.5.2 → clawed-2.5.3}/clawed/exporter.py +0 -0
  148. {clawed-2.5.2 → clawed-2.5.3}/clawed/failure_codes.py +0 -0
  149. {clawed-2.5.2 → clawed-2.5.3}/clawed/feedback.py +0 -0
  150. {clawed-2.5.2 → clawed-2.5.3}/clawed/formats/__init__.py +0 -0
  151. {clawed-2.5.2 → clawed-2.5.3}/clawed/formats/flipchart.py +0 -0
  152. {clawed-2.5.2 → clawed-2.5.3}/clawed/formats/notebook.py +0 -0
  153. {clawed-2.5.2 → clawed-2.5.3}/clawed/formats/xbk.py +0 -0
  154. {clawed-2.5.2 → clawed-2.5.3}/clawed/gateway.py +0 -0
  155. {clawed-2.5.2 → clawed-2.5.3}/clawed/gateway_response.py +0 -0
  156. {clawed-2.5.2 → clawed-2.5.3}/clawed/generation.py +0 -0
  157. {clawed-2.5.2 → clawed-2.5.3}/clawed/generation_report.py +0 -0
  158. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/__init__.py +0 -0
  159. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/export.py +0 -0
  160. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/feedback.py +0 -0
  161. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/gaps.py +0 -0
  162. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/generate.py +0 -0
  163. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/ingest.py +0 -0
  164. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/misc.py +0 -0
  165. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/onboard.py +0 -0
  166. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/schedule.py +0 -0
  167. {clawed-2.5.2 → clawed-2.5.3}/clawed/handlers/standards.py +0 -0
  168. {clawed-2.5.2 → clawed-2.5.3}/clawed/hermes_plugin.py +0 -0
  169. {clawed-2.5.2 → clawed-2.5.3}/clawed/image_pipeline.py +0 -0
  170. {clawed-2.5.2 → clawed-2.5.3}/clawed/improver.py +0 -0
  171. {clawed-2.5.2 → clawed-2.5.3}/clawed/ingestor.py +0 -0
  172. {clawed-2.5.2 → clawed-2.5.3}/clawed/io.py +0 -0
  173. {clawed-2.5.2 → clawed-2.5.3}/clawed/lesson.py +0 -0
  174. {clawed-2.5.2 → clawed-2.5.3}/clawed/llm.py +0 -0
  175. {clawed-2.5.2 → clawed-2.5.3}/clawed/master_content.py +0 -0
  176. {clawed-2.5.2 → clawed-2.5.3}/clawed/materials.py +0 -0
  177. {clawed-2.5.2 → clawed-2.5.3}/clawed/mcp_server.py +0 -0
  178. {clawed-2.5.2 → clawed-2.5.3}/clawed/memory_engine.py +0 -0
  179. {clawed-2.5.2 → clawed-2.5.3}/clawed/model_router.py +0 -0
  180. {clawed-2.5.2 → clawed-2.5.3}/clawed/models.py +0 -0
  181. {clawed-2.5.2 → clawed-2.5.3}/clawed/multi_agent.py +0 -0
  182. {clawed-2.5.2 → clawed-2.5.3}/clawed/onboarding.py +0 -0
  183. {clawed-2.5.2 → clawed-2.5.3}/clawed/openclaw_plugin.py +0 -0
  184. {clawed-2.5.2 → clawed-2.5.3}/clawed/parent_comm.py +0 -0
  185. {clawed-2.5.2 → clawed-2.5.3}/clawed/persona.py +0 -0
  186. {clawed-2.5.2 → clawed-2.5.3}/clawed/persona_evolution.py +0 -0
  187. {clawed-2.5.2 → clawed-2.5.3}/clawed/planner.py +0 -0
  188. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/504_accommodations.txt +0 -0
  189. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/admin_lesson_plan.txt +0 -0
  190. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/assessment.txt +0 -0
  191. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/curriculum_gaps.txt +0 -0
  192. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/dbq_assessment.txt +0 -0
  193. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/differentiation.txt +0 -0
  194. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/formative_assessment.txt +0 -0
  195. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/iep_modification.txt +0 -0
  196. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/lesson_plan.txt +0 -0
  197. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/master_content.txt +0 -0
  198. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/multi_agent_researcher.txt +0 -0
  199. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/multi_agent_reviewer.txt +0 -0
  200. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/pacing_guide.txt +0 -0
  201. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/parent_note.txt +0 -0
  202. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/persona_extract.txt +0 -0
  203. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/quiz.txt +0 -0
  204. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/rubric.txt +0 -0
  205. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/student_packet.txt +0 -0
  206. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/sub_packet.txt +0 -0
  207. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/summative_assessment.txt +0 -0
  208. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/tiered_assignments.txt +0 -0
  209. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/unit_plan.txt +0 -0
  210. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/worksheet.txt +0 -0
  211. {clawed-2.5.2 → clawed-2.5.3}/clawed/prompts/year_map.txt +0 -0
  212. {clawed-2.5.2 → clawed-2.5.3}/clawed/quality.py +0 -0
  213. {clawed-2.5.2 → clawed-2.5.3}/clawed/reading_report.py +0 -0
  214. {clawed-2.5.2 → clawed-2.5.3}/clawed/router.py +0 -0
  215. {clawed-2.5.2 → clawed-2.5.3}/clawed/sanitize.py +0 -0
  216. {clawed-2.5.2 → clawed-2.5.3}/clawed/scheduler.py +0 -0
  217. {clawed-2.5.2 → clawed-2.5.3}/clawed/school.py +0 -0
  218. {clawed-2.5.2 → clawed-2.5.3}/clawed/search.py +0 -0
  219. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/__init__.py +0 -0
  220. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/art.py +0 -0
  221. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/base.py +0 -0
  222. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/computer_science.py +0 -0
  223. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/ela.py +0 -0
  224. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/foreign_language.py +0 -0
  225. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/history.py +0 -0
  226. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/library.py +0 -0
  227. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/math.py +0 -0
  228. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/music.py +0 -0
  229. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/physical_education.py +0 -0
  230. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/science.py +0 -0
  231. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/social_studies.py +0 -0
  232. {clawed-2.5.2 → clawed-2.5.3}/clawed/skills/special_education.py +0 -0
  233. {clawed-2.5.2 → clawed-2.5.3}/clawed/slide_images.py +0 -0
  234. {clawed-2.5.2 → clawed-2.5.3}/clawed/standards.py +0 -0
  235. {clawed-2.5.2 → clawed-2.5.3}/clawed/state.py +0 -0
  236. {clawed-2.5.2 → clawed-2.5.3}/clawed/state_standards.py +0 -0
  237. {clawed-2.5.2 → clawed-2.5.3}/clawed/student_bot.py +0 -0
  238. {clawed-2.5.2 → clawed-2.5.3}/clawed/student_cli.py +0 -0
  239. {clawed-2.5.2 → clawed-2.5.3}/clawed/student_telegram_bot.py +0 -0
  240. {clawed-2.5.2 → clawed-2.5.3}/clawed/sub_packet.py +0 -0
  241. {clawed-2.5.2 → clawed-2.5.3}/clawed/task_queue.py +0 -0
  242. {clawed-2.5.2 → clawed-2.5.3}/clawed/templates_lib.py +0 -0
  243. {clawed-2.5.2 → clawed-2.5.3}/clawed/tools.py +0 -0
  244. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/__init__.py +0 -0
  245. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/cli.py +0 -0
  246. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/hermes.py +0 -0
  247. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/openclaw.py +0 -0
  248. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/student_telegram.py +0 -0
  249. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/telegram.py +0 -0
  250. {clawed-2.5.2 → clawed-2.5.3}/clawed/transports/web.py +0 -0
  251. {clawed-2.5.2 → clawed-2.5.3}/clawed/tui.py +0 -0
  252. {clawed-2.5.2 → clawed-2.5.3}/clawed/tui_chat.py +0 -0
  253. {clawed-2.5.2 → clawed-2.5.3}/clawed/validation.py +0 -0
  254. {clawed-2.5.2 → clawed-2.5.3}/clawed/voice.py +0 -0
  255. {clawed-2.5.2 → clawed-2.5.3}/clawed/voice_check.py +0 -0
  256. {clawed-2.5.2 → clawed-2.5.3}/clawed/workspace.py +0 -0
  257. {clawed-2.5.2 → clawed-2.5.3}/eduagent/__init__.py +0 -0
  258. {clawed-2.5.2 → clawed-2.5.3}/eduagent/_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawed
3
- Version: 2.5.2
3
+ Version: 2.5.3
4
4
  Summary: Free AI lesson planner for teachers. Learns your teaching voice from your curriculum files and generates complete lesson packages. Open source, runs locally, no subscription.
5
5
  Project-URL: Homepage, https://sirhanmacx.github.io/Claw-ED
6
6
  Project-URL: Documentation, https://github.com/SirhanMacx/Claw-ED#readme
@@ -17,7 +17,7 @@ if hasattr(sys.stderr, "reconfigure"):
17
17
  except Exception:
18
18
  pass
19
19
 
20
- __version__ = "2.5.2"
20
+ __version__ = "2.5.3"
21
21
  __author__ = "Jon Maccarello & Claw-ED contributors"
22
22
  __description__ = "Personal AI teaching agent. Learns your voice, works while you sleep."
23
23
 
@@ -501,6 +501,41 @@ def lesson(
501
501
  console.print(f"\n[green]Lesson saved:[/green] {json_path}")
502
502
  if export_path:
503
503
  console.print(f"[green]Exported:[/green] {export_path}")
504
+
505
+ # ── Quality review ────────────────────────────────────────────
506
+ if export_path:
507
+ try:
508
+ from clawed.review_output import (
509
+ review_docx,
510
+ review_pptx,
511
+ )
512
+ export_p = Path(export_path) if not isinstance(export_path, Path) else export_path
513
+ if export_p.suffix == ".pptx":
514
+ review = review_pptx(export_p)
515
+ elif export_p.suffix == ".docx":
516
+ review = review_docx(export_p)
517
+ else:
518
+ review = None
519
+ if review:
520
+ if review.passed:
521
+ console.print(
522
+ f" Quality: [green]{review.score:.1f}/10[/green]"
523
+ )
524
+ else:
525
+ console.print(
526
+ f" Quality: [red]{review.score:.1f}/10 "
527
+ f"({len(review.issues)} issues)[/red]"
528
+ )
529
+ for issue in review.issues[:5]:
530
+ _sev = {"critical": "red", "major": "yellow", "minor": "dim"}
531
+ console.print(
532
+ f" [{_sev.get(issue['severity'], 'dim')}]"
533
+ f"{issue['location']}: "
534
+ f"{issue['description']}[/{_sev.get(issue['severity'], 'dim')}]"
535
+ )
536
+ except Exception:
537
+ pass
538
+
504
539
  console.print(
505
540
  Panel(
506
541
  f"[bold]Objective:[/bold] {daily.objective}\n"
@@ -48,12 +48,24 @@ without a <head> or <body> wrapper.
48
48
  - The game MUST work on phones, tablets, Chromebooks (responsive, touch).
49
49
  - The game MUST be genuinely FUN — not a boring quiz with buttons.
50
50
  - Include scoring, feedback, progression (levels or rounds).
51
- - Use modern CSS (gradients, animations, shadows) for visual polish.
52
51
  - Include a start screen with the lesson title and instructions.
53
52
  - Include an end screen with score and "play again" button.
54
53
  - ALL educational content must be embedded as data in the JS.
55
54
  - Add a small footer: "Made with Claw-ED — github.com/SirhanMacx/Claw-ED"
56
55
 
56
+ VISUALS — DO NOT USE IMAGE FILES. Create ALL visuals programmatically:
57
+ - Use Three.js for immersive 3D scenes themed to the lesson content. \
58
+ Age of Exploration = 3D ocean with ships. Civil War = battlefields. \
59
+ Renaissance = marble halls. Science = molecular structures.
60
+ - Use CSS gradients, animations, box-shadows, and transforms for 2D polish.
61
+ - Use HTML Canvas for custom diagrams, maps, or illustrations.
62
+ - Use CSS shapes and emoji for icons — never <img> tags.
63
+ - Use SVG inline for any detailed graphics (maps, diagrams, symbols).
64
+ - The game should feel IMMERSIVE — like the student is inside the topic, \
65
+ not reading a quiz. 3D environments, particle effects, ambient animation.
66
+ - Go ABOVE AND BEYOND visually. This should look like a real game, not a \
67
+ school worksheet with buttons.
68
+
57
69
  WHAT MAKES A GREAT LEARNING GAME:
58
70
  - The mechanic TEACHES, not just tests. Students learn through the gameplay.
59
71
  - Wrong answers give feedback that explains WHY it's wrong.
@@ -338,15 +350,10 @@ async def compile_game(
338
350
  output_dir.mkdir(parents=True, exist_ok=True)
339
351
 
340
352
  config = config or AppConfig.load()
341
- # Games require maximum intelligence always Opus.
342
- from clawed.config import get_api_key
343
- from clawed.models import LLMProvider
344
- anthropic_key = get_api_key("anthropic")
345
- if anthropic_key:
346
- config.provider = LLMProvider.ANTHROPIC
347
- config.anthropic_model = "claude-opus-4-6"
348
- else:
349
- config = route_model("game_generate", config)
353
+ # Route to DEEP tier within the teacher's chosen provider.
354
+ # Teacher picked Ollama? Gets their best Ollama model.
355
+ # Teacher picked Anthropic? Gets Opus. Their choice.
356
+ config = route_model("game_generate", config)
350
357
  client = LLMClient(config)
351
358
 
352
359
  # Build the game generation prompt
@@ -829,7 +829,10 @@ def export_lesson_pptx(
829
829
  for item in list_items[:max_bullets]:
830
830
  item = item.strip()
831
831
  if len(item) > 20:
832
- bullets.append(item[:140])
832
+ # Truncate at word boundary, not mid-word
833
+ if len(item) > 140:
834
+ item = item[:140].rsplit(' ', 1)[0] + '...'
835
+ bullets.append(item)
833
836
  if bullets:
834
837
  return bullets
835
838
  # Fallback: first N sentences that are short enough to be readable
@@ -970,37 +973,65 @@ def export_lesson_pptx(
970
973
 
971
974
  _add_footer(slide, slide_num[0])
972
975
 
973
- # ── Source excerpt slides (if quoted passages detected) ─────
974
- # Try multiple quote patterns: curly quotes, straight quotes, markdown bold
975
- source_quotes: list[tuple[str, str]] = []
976
- for pattern in [
977
- r'\u201c([^\u201d]{30,500})\u201d[^\n]*?(?:[-\u2014]\s*(.+?)(?:\n|$))',
978
- r'"([^"]{30,500})"[^\n]*?(?:[-\u2014]\s*(.+?)(?:\n|$))',
979
- r"['\u2018]([^'\u2019]{30,500})['\u2019][^\n]*?(?:[-\u2014]\s*(.+?)(?:\n|$))",
980
- ]:
981
- source_quotes = re.findall(pattern, di_text)
982
- if source_quotes:
983
- break
984
-
985
- source_img_idx = 3 # Start from entity_3 for source slides
986
- for quote_text, attribution in source_quotes[:3]:
976
+ # ── Primary Source slides (from structured lesson data) ─────
977
+ # Use lesson.primary_sources (PrimarySourceDocument objects)
978
+ # instead of regex-extracting fragments from DI prose.
979
+ source_img_idx = 3
980
+ primary_sources = getattr(lesson, "primary_sources", []) or []
981
+ for ps in primary_sources[:3]:
982
+ # Get source fields — handle both dict and object
983
+ if isinstance(ps, dict):
984
+ ps_title = ps.get("title", "") or ps.get("document_label", "")
985
+ ps_author = ps.get("author", "")
986
+ ps_date = ps.get("date", "")
987
+ ps_context = ps.get("context", "")
988
+ ps_text = ps.get("full_text", "") or ps.get("text", "")
989
+ ps_questions = ps.get("analysis_questions", [])
990
+ else:
991
+ ps_title = getattr(ps, "title", "") or getattr(ps, "document_label", "")
992
+ ps_author = getattr(ps, "author", "")
993
+ ps_date = getattr(ps, "date", "")
994
+ ps_context = getattr(ps, "context", "")
995
+ ps_text = getattr(ps, "full_text", "")
996
+ ps_questions = getattr(ps, "analysis_questions", [])
997
+
998
+ if not ps_text and not ps_title:
999
+ continue
1000
+
987
1001
  slide = _next_slide()
988
1002
  _tinted_bg(slide, theme["accent"])
989
1003
 
990
- # "Source Excerpt" badge
1004
+ # "Primary Source" badge with title
1005
+ badge_text = ps_title or "Primary Source"
991
1006
  badge = _rounded_card(
992
1007
  slide,
993
- Inches(0.8), Inches(0.6),
994
- Inches(3.5), Inches(0.7),
1008
+ Inches(0.8), Inches(0.5),
1009
+ Inches(5.0), Inches(0.7),
995
1010
  theme["primary"],
996
1011
  )
997
1012
  badge_tf = badge.text_frame
998
1013
  badge_tf.paragraphs[0].alignment = PP_ALIGN.CENTER
999
1014
  run = badge_tf.paragraphs[0].add_run()
1000
- run.text = "Source Excerpt"
1001
- _set_text_props(run, 22, theme["text_light"], bold=True)
1015
+ run.text = _clean_slide_text(badge_text)[:80]
1016
+ _set_text_props(run, 20, theme["text_light"], bold=True)
1017
+
1018
+ # Attribution line (author + date)
1019
+ attribution = ""
1020
+ if ps_author:
1021
+ attribution = ps_author
1022
+ if ps_date:
1023
+ attribution += f", {ps_date}" if attribution else ps_date
1024
+ if attribution:
1025
+ tb_attr = slide.shapes.add_textbox(
1026
+ Inches(0.8), Inches(1.3), Inches(8.0), Inches(0.4),
1027
+ )
1028
+ p_attr = tb_attr.text_frame.paragraphs[0]
1029
+ run_attr = p_attr.add_run()
1030
+ run_attr.text = f"— {_clean_slide_text(attribution)}"
1031
+ _set_text_props(run_attr, 14, theme["text_dim"])
1032
+ run_attr.font.italic = True
1002
1033
 
1003
- # Add image if available (use remaining entity images)
1034
+ # Source text the actual excerpt
1004
1035
  src_img = images.get(f"entity_{source_img_idx}")
1005
1036
  text_width = slide_w - Inches(2.0)
1006
1037
  if src_img:
@@ -1008,40 +1039,43 @@ def export_lesson_pptx(
1008
1039
  _add_sidebar_image(slide, src_img)
1009
1040
  source_img_idx += 1
1010
1041
 
1011
- # Quoted text — adaptive font, clean text, truncate if needed
1012
- clean_quote = _clean_slide_text(quote_text.strip())
1013
- # Truncate very long quotes to keep readable on slide
1014
- if len(clean_quote) > 350:
1015
- clean_quote = clean_quote[:347].rsplit(" ", 1)[0] + "…"
1042
+ clean_text = _clean_slide_text(ps_text.strip())
1043
+ if len(clean_text) > 400:
1044
+ clean_text = clean_text[:397].rsplit(" ", 1)[0] + "…"
1016
1045
 
1017
- quote_font = 20 if len(clean_quote) < 180 else 17 if len(clean_quote) < 300 else 15
1046
+ text_font = 18 if len(clean_text) < 200 else 15 if len(clean_text) < 350 else 13
1018
1047
 
1048
+ y_start = Inches(1.8) if attribution else Inches(1.5)
1019
1049
  tb = slide.shapes.add_textbox(
1020
- Inches(1.5), Inches(1.9), text_width, Inches(3.8),
1050
+ Inches(1.0), y_start, text_width, Inches(3.5),
1021
1051
  )
1022
1052
  tf = tb.text_frame
1023
1053
  tf.word_wrap = True
1024
1054
  p = tf.paragraphs[0]
1025
1055
  p.alignment = PP_ALIGN.LEFT
1026
- p.line_spacing = Pt(quote_font + 8)
1056
+ p.line_spacing = Pt(text_font + 7)
1027
1057
  run = p.add_run()
1028
- run.text = f"\u201c{clean_quote}\u201d"
1029
- _set_text_props(run, quote_font, theme["text_dark"])
1058
+ run.text = f"\u201c{clean_text}\u201d" if clean_text else "(Source text not available)"
1059
+ _set_text_props(run, text_font, theme["text_dark"])
1030
1060
  run.font.italic = True
1031
1061
 
1032
- # Attribution right-aligned, smaller, just above footer
1033
- if attribution and attribution.strip():
1034
- attr_clean = _clean_slide_text(attribution.strip())
1035
- if len(attr_clean) > 100:
1036
- attr_clean = attr_clean[:97].rsplit(" ", 1)[0] + "…"
1037
- tb_attr = slide.shapes.add_textbox(
1038
- Inches(1.5), slide_h - Inches(1.6), text_width, Inches(0.7),
1062
+ # Analysis questions (if any)
1063
+ if ps_questions:
1064
+ q_y = slide_h - Inches(2.0)
1065
+ tb_q = slide.shapes.add_textbox(
1066
+ Inches(1.0), q_y, slide_w - Inches(2.0), Inches(1.2),
1039
1067
  )
1040
- p_attr = tb_attr.text_frame.paragraphs[0]
1041
- p_attr.alignment = PP_ALIGN.RIGHT
1042
- run_attr = p_attr.add_run()
1043
- run_attr.text = f"\u2014 {attr_clean}"
1044
- _set_text_props(run_attr, 14, "666666")
1068
+ tf_q = tb_q.text_frame
1069
+ tf_q.word_wrap = True
1070
+ for qi, question in enumerate(ps_questions[:2]):
1071
+ p_q = tf_q.paragraphs[0] if qi == 0 else tf_q.add_paragraph()
1072
+ p_q.space_before = Pt(4)
1073
+ run_q = p_q.add_run()
1074
+ q_text = _clean_slide_text(question)
1075
+ if len(q_text) > 100:
1076
+ q_text = q_text[:97].rsplit(" ", 1)[0] + "…"
1077
+ run_q.text = f"Q{qi+1}: {q_text}"
1078
+ _set_text_props(run_q, 14, theme["text_dark"], bold=True)
1045
1079
 
1046
1080
  _add_footer(slide, slide_num[0])
1047
1081
 
@@ -0,0 +1,253 @@
1
+ """Post-generation output review agent.
2
+
3
+ Checks every generated output (PPTX, DOCX, HTML game) for quality
4
+ issues before delivery to the teacher. Catches:
5
+ - Truncated/nonsensical text
6
+ - Images not matching slide content
7
+ - Answer keys visible in student materials
8
+ - Overcrowded slides
9
+ - Empty or incomplete sections
10
+ - Text starting mid-sentence
11
+
12
+ If issues are found, returns a structured report. The caller decides
13
+ whether to regenerate or deliver with warnings.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ import re
20
+ from pathlib import Path
21
+ from typing import Any
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class OutputReview:
27
+ """Quality review results for a generated output."""
28
+
29
+ def __init__(self) -> None:
30
+ self.issues: list[dict[str, Any]] = []
31
+ self.score: float = 10.0 # Start perfect, deduct for issues
32
+
33
+ def add_issue(
34
+ self, severity: str, location: str, description: str
35
+ ) -> None:
36
+ """Add a quality issue. Severity: critical, major, minor."""
37
+ deductions = {"critical": 3.0, "major": 1.5, "minor": 0.5}
38
+ self.issues.append({
39
+ "severity": severity,
40
+ "location": location,
41
+ "description": description,
42
+ })
43
+ self.score = max(0.0, self.score - deductions.get(severity, 0.5))
44
+
45
+ @property
46
+ def passed(self) -> bool:
47
+ """Output passes review if score >= 6.0 and no critical issues."""
48
+ has_critical = any(i["severity"] == "critical" for i in self.issues)
49
+ return self.score >= 6.0 and not has_critical
50
+
51
+ def summary(self) -> str:
52
+ """Human-readable summary."""
53
+ if not self.issues:
54
+ return f"PASSED ({self.score:.1f}/10) — no issues found"
55
+ lines = [f"{'PASSED' if self.passed else 'FAILED'} ({self.score:.1f}/10)"]
56
+ for issue in self.issues:
57
+ icon = {"critical": "X", "major": "!", "minor": "~"}
58
+ lines.append(
59
+ f" [{icon.get(issue['severity'], '?')}] "
60
+ f"{issue['location']}: {issue['description']}"
61
+ )
62
+ return "\n".join(lines)
63
+
64
+
65
+ def review_pptx(pptx_path: Path) -> OutputReview:
66
+ """Review a generated PPTX for quality issues."""
67
+ from pptx import Presentation
68
+
69
+ review = OutputReview()
70
+
71
+ try:
72
+ prs = Presentation(str(pptx_path))
73
+ except Exception as e:
74
+ review.add_issue("critical", "file", f"Cannot open PPTX: {e}")
75
+ return review
76
+
77
+ for i, slide in enumerate(prs.slides):
78
+ slide_num = i + 1
79
+ loc = f"Slide {slide_num}"
80
+
81
+ # Collect all text from the slide
82
+ texts = []
83
+ has_image = False
84
+ for shape in slide.shapes:
85
+ if shape.has_text_frame:
86
+ for para in shape.text_frame.paragraphs:
87
+ t = para.text.strip()
88
+ if t:
89
+ texts.append(t)
90
+ if shape.shape_type == 13:
91
+ has_image = True
92
+
93
+ all_text = " ".join(texts)
94
+
95
+ # Check: empty slide (no text at all)
96
+ if not texts or (len(texts) == 1 and len(texts[0]) < 5):
97
+ review.add_issue("major", loc, "Slide is empty or has minimal content")
98
+
99
+ # Check: text starts mid-sentence (fragment)
100
+ for t in texts:
101
+ if t and len(t) > 10:
102
+ first_char = t[0]
103
+ if first_char.islower() or first_char in ("'", "\u2019"):
104
+ review.add_issue(
105
+ "major", loc,
106
+ f"Text starts mid-sentence: '{t[:50]}...'"
107
+ )
108
+ break
109
+
110
+ # Check: truncated text (ends with ... or ellipsis mid-word)
111
+ for t in texts:
112
+ if t.endswith("...") or t.endswith("\u2026"):
113
+ # Check if truncation is mid-word
114
+ before_ellipsis = t.rstrip(".\u2026 ")
115
+ if before_ellipsis and before_ellipsis[-1].isalpha():
116
+ review.add_issue(
117
+ "minor", loc,
118
+ f"Text truncated mid-word: '...{t[-40:]}'"
119
+ )
120
+
121
+ # Check: too much text on one slide (>500 chars)
122
+ if len(all_text) > 600:
123
+ review.add_issue(
124
+ "major", loc,
125
+ f"Overcrowded slide ({len(all_text)} chars) — "
126
+ f"should be split across multiple slides"
127
+ )
128
+
129
+ # Check: answer keys visible
130
+ answer_patterns = [
131
+ r"\(Answer:\s*[^)]+\)",
132
+ r"\(answer:\s*[^)]+\)",
133
+ r"ANSWER KEY",
134
+ r"Answer:\s+\w+",
135
+ ]
136
+ for pattern in answer_patterns:
137
+ if re.search(pattern, all_text):
138
+ review.add_issue(
139
+ "critical", loc,
140
+ "Answer key visible in student-facing slide"
141
+ )
142
+ break
143
+
144
+ return review
145
+
146
+
147
+ def review_docx(docx_path: Path) -> OutputReview:
148
+ """Review a generated DOCX handout for quality issues."""
149
+ from docx import Document
150
+
151
+ review = OutputReview()
152
+
153
+ try:
154
+ doc = Document(str(docx_path))
155
+ except Exception as e:
156
+ review.add_issue("critical", "file", f"Cannot open DOCX: {e}")
157
+ return review
158
+
159
+ all_text = ""
160
+ for para in doc.paragraphs:
161
+ text = para.text.strip()
162
+ all_text += text + " "
163
+
164
+ # Check: answer keys visible
165
+ if re.search(
166
+ r"\(Answer:\s*[^)]+\)|\(answer:\s*[^)]+\)|ANSWER KEY",
167
+ text,
168
+ ):
169
+ review.add_issue(
170
+ "critical",
171
+ "content",
172
+ f"Answer key visible: '{text[:60]}...'"
173
+ )
174
+
175
+ # Check: document too short
176
+ if len(all_text) < 500:
177
+ review.add_issue(
178
+ "major", "content", "Document is very short — may be incomplete"
179
+ )
180
+
181
+ # Check: missing key sections
182
+ sections_expected = ["do now", "aim", "exit ticket"]
183
+ text_lower = all_text.lower()
184
+ for section in sections_expected:
185
+ if section not in text_lower:
186
+ review.add_issue(
187
+ "minor", "structure",
188
+ f"Missing expected section: '{section}'"
189
+ )
190
+
191
+ return review
192
+
193
+
194
+ def review_game_html(html_path: Path) -> OutputReview:
195
+ """Review a generated HTML game for quality issues."""
196
+ review = OutputReview()
197
+
198
+ try:
199
+ html = html_path.read_text(encoding="utf-8")
200
+ except Exception as e:
201
+ review.add_issue("critical", "file", f"Cannot read HTML: {e}")
202
+ return review
203
+
204
+ # Check: has proper HTML structure
205
+ if "<script>" not in html and "<script " not in html:
206
+ review.add_issue("critical", "structure", "No <script> tag — game won't run")
207
+
208
+ if "<style>" not in html and "<style " not in html:
209
+ review.add_issue("critical", "structure", "No <style> tag — game won't display")
210
+
211
+ if "<body>" not in html and "<body " not in html:
212
+ review.add_issue("major", "structure", "No <body> tag")
213
+
214
+ # Check: has game elements
215
+ game_indicators = [
216
+ "addEventListener", "onclick", "function", "score",
217
+ "querySelector", "getElementById",
218
+ ]
219
+ found = sum(1 for g in game_indicators if g in html)
220
+ if found < 3:
221
+ review.add_issue(
222
+ "critical", "content",
223
+ f"Only {found}/6 game indicators found — may not be interactive"
224
+ )
225
+
226
+ # Check: file size (too small = broken, too large = bloated)
227
+ size_kb = len(html) / 1024
228
+ if size_kb < 5:
229
+ review.add_issue("critical", "content", f"Game is only {size_kb:.0f}KB — likely broken")
230
+ elif size_kb > 200:
231
+ review.add_issue("minor", "size", f"Game is {size_kb:.0f}KB — may load slowly")
232
+
233
+ # Check: duplicate DOCTYPE
234
+ if html.count("<!DOCTYPE") > 1:
235
+ review.add_issue("major", "structure", "Duplicate <!DOCTYPE> tags")
236
+
237
+ return review
238
+
239
+
240
+ def review_all_outputs(output_dir: Path) -> dict[str, OutputReview]:
241
+ """Review all generated outputs in a directory."""
242
+ reviews: dict[str, OutputReview] = {}
243
+
244
+ for pptx in output_dir.glob("*.pptx"):
245
+ reviews[pptx.name] = review_pptx(pptx)
246
+
247
+ for docx in output_dir.glob("*handout*.docx"):
248
+ reviews[docx.name] = review_docx(docx)
249
+
250
+ for html in output_dir.glob("game_*.html"):
251
+ reviews[html.name] = review_game_html(html)
252
+
253
+ return reviews
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "clawed"
7
- version = "2.5.2"
7
+ version = "2.5.3"
8
8
  description = "Free AI lesson planner for teachers. Learns your teaching voice from your curriculum files and generates complete lesson packages. Open source, runs locally, no subscription."
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes