minimal-agent 0.9.0 → 0.10.0

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 (467) hide show
  1. package/README.md +11 -8
  2. package/package.json +34 -1
  3. package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
  4. package/plugins/cron-runner/commands/cron.md +26 -0
  5. package/plugins/cron-runner/commands/crons.md +7 -0
  6. package/plugins/cron-runner/plugin.js +34 -0
  7. package/plugins/cron-runner/src/commands.js +348 -0
  8. package/plugins/cron-runner/src/cronTime.js +148 -0
  9. package/plugins/cron-runner/src/lock.js +68 -0
  10. package/plugins/cron-runner/src/runs.js +89 -0
  11. package/plugins/cron-runner/src/scheduleEngine.js +72 -0
  12. package/plugins/cron-runner/src/scheduler.js +355 -0
  13. package/plugins/cron-runner/src/schema.js +140 -0
  14. package/plugins/cron-runner/src/service.js +167 -0
  15. package/plugins/cron-runner/src/store.js +298 -0
  16. package/plugins/cron-runner/src/types.js +1 -0
  17. package/plugins/workflow-runner/commands/workflow.md +4 -4
  18. package/plugins/workflow-runner/commands/workflows.md +1 -1
  19. package/plugins/workflow-runner/plugin.js +2 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +172 -9
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +150 -13
  23. package/plugins/workflow-runner/src/index.js +39 -8
  24. package/plugins/workflow-runner/src/loader.js +216 -64
  25. package/plugins/workflow-runner/src/multimodal.js +37 -0
  26. package/plugins/workflow-runner/src/recovery.js +26 -24
  27. package/plugins/workflow-runner/src/runner.js +693 -287
  28. package/plugins/workflow-runner/src/stepExecutors/agent.js +126 -37
  29. package/plugins/workflow-runner/src/stepExecutors/assert.js +3 -2
  30. package/plugins/workflow-runner/src/stepExecutors/llm.js +38 -30
  31. package/plugins/workflow-runner/src/stepExecutors/skill.js +27 -38
  32. package/plugins/workflow-runner/src/stepExecutors/tool.js +56 -3
  33. package/plugins/workflow-runner/src/stepExecutors/verify.js +19 -10
  34. package/plugins/workflow-runner/src/taskIdentity.js +185 -30
  35. package/plugins/workflow-runner/src/types.js +18 -7
  36. package/plugins/workflow-runner/src/workflowState.js +355 -55
  37. package/skills/ai-image/SKILL.md +89 -0
  38. package/skills/ai-image/scripts/generate.mjs +497 -0
  39. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  40. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  41. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  42. package/skills/browser-automation/SKILL.md +47 -0
  43. package/skills/docx/SKILL.md +25 -3
  44. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  45. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  46. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/opc_check.py +253 -0
  53. package/skills/image-gen-openrouter/SKILL.md +10 -2
  54. package/skills/image-stats/SKILL.md +65 -0
  55. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  56. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  57. package/skills/image-stats/scripts/image_stats.py +368 -0
  58. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  59. package/skills/pptx/SKILL.md +123 -95
  60. package/skills/pptx/design.md +82 -0
  61. package/skills/pptx/examples/README.md +29 -0
  62. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  63. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  64. package/skills/pptx/pptxgenjs.md +41 -0
  65. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  66. package/skills/pptx/scripts/layout_lint.py +293 -0
  67. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  68. package/skills/pptx/scripts/opc_check.py +339 -0
  69. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  70. package/skills/pptx/svg.md +67 -0
  71. package/skills/tts-zh/SKILL.md +19 -2
  72. package/skills/ui-design/SKILL.md +158 -0
  73. package/skills/ui-design/claude_design.md +309 -0
  74. package/skills/ui-design/evals/evals.json +254 -0
  75. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  76. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  77. package/skills/ui-design/references/design-context.md +53 -0
  78. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  79. package/skills/ui-design/references/export-pptx.md +124 -0
  80. package/skills/ui-design/references/layout-composition.md +654 -0
  81. package/skills/ui-design/references/preview-feedback.md +37 -0
  82. package/skills/ui-design/references/source-image-layout.md +189 -0
  83. package/skills/ui-design/references/starter-components.md +78 -0
  84. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  85. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  86. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  87. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  88. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  89. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  90. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  91. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  92. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  93. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  94. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  95. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  96. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  97. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  98. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  99. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  100. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/assemble_material.py +217 -0
  140. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  141. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  142. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  143. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  144. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  145. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  146. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  147. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  148. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  149. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  150. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  151. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  152. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  153. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  154. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  155. package/skills/ui-design/scripts/design_style_check.py +397 -0
  156. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  157. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  158. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  159. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  160. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  161. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  162. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  163. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  164. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  165. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  166. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  167. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  168. package/skills/ui-design/scripts/layout_balance.py +881 -0
  169. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  170. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  171. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  172. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  173. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  174. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  175. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  176. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  177. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  178. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  179. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  180. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  181. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  182. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  183. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  184. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  185. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  186. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  187. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  188. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  195. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  196. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  205. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  206. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  221. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  222. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  225. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  226. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  239. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  240. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  248. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  249. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  261. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  262. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  275. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  292. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  293. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  316. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  324. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  325. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  328. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  329. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  337. package/skills/video-compose/SKILL.md +8 -1
  338. package/skills/web-crawler/SKILL.md +70 -0
  339. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  340. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  341. package/skills/xlsx/SKILL.md +12 -0
  342. package/skills/xlsx/scripts/opc_check.py +253 -0
  343. package/src/advisorState.js +22 -0
  344. package/src/askBridge.js +87 -3
  345. package/src/bootstrap/workingDir.js +12 -1
  346. package/src/cli/args.js +17 -1
  347. package/src/cli/configMenu.js +236 -0
  348. package/src/cli/configWizard.js +7 -5
  349. package/src/cli/print.js +37 -4
  350. package/src/cli/streamJson.js +77 -24
  351. package/src/cli/wizards/advisorWizard.js +147 -0
  352. package/src/cli/wizards/fieldForm.js +149 -0
  353. package/src/cli/wizards/imageWizard.js +60 -0
  354. package/src/cli/wizards/openRouterWizard.js +23 -0
  355. package/src/cli/wizards/providerPresets.js +95 -0
  356. package/src/cli/wizards/providerWizard.js +219 -0
  357. package/src/cli/wizards/searchWizard.js +23 -0
  358. package/src/cli/wizards/ttsWizard.js +34 -0
  359. package/src/config/configFile.js +441 -83
  360. package/src/config/configService.js +446 -0
  361. package/src/config.js +24 -64
  362. package/src/context/agentHome.js +9 -0
  363. package/src/context/archive.js +3 -2
  364. package/src/context/compact.js +8 -5
  365. package/src/context/contextLock.js +81 -0
  366. package/src/context/persistContext.js +30 -11
  367. package/src/context/reactiveCompact.js +4 -2
  368. package/src/context/recentDirs.js +21 -61
  369. package/src/context/sessionPath.js +97 -23
  370. package/src/context/sessionRegistry.js +303 -24
  371. package/src/context/tokenCounter.js +18 -0
  372. package/src/context/workspaceIndex.js +731 -0
  373. package/src/infra/verification.js +3 -2
  374. package/src/llm/client.js +293 -206
  375. package/src/llm/providers/anthropicFormat.js +241 -0
  376. package/src/llm/providers/openaiFormat.js +190 -0
  377. package/src/loop.js +211 -27
  378. package/src/main.js +61 -11
  379. package/src/messageQueue.js +97 -0
  380. package/src/multimodal.js +203 -0
  381. package/src/plugin-sdk.js +15 -0
  382. package/src/plugins/commandRouter.js +3 -0
  383. package/src/plugins/hookEngine.js +5 -1
  384. package/src/plugins/pluginLifecycle.js +28 -0
  385. package/src/plugins/pluginRunner.js +40 -0
  386. package/src/plugins/transcript.js +4 -3
  387. package/src/prompts/pluginList.js +2 -2
  388. package/src/prompts/projectInstructions.js +3 -3
  389. package/src/prompts/promptAssembly.js +133 -109
  390. package/src/prompts/skillList.js +12 -22
  391. package/src/session/runAgentTurn.js +70 -0
  392. package/src/session/runTurn.js +41 -6
  393. package/src/toolGate.js +17 -0
  394. package/src/tools/advisor/advisor.js +379 -0
  395. package/src/tools/agent/agent.js +555 -0
  396. package/src/tools/agent/agentTypes.js +59 -0
  397. package/src/tools/agent/coordinator.js +93 -0
  398. package/src/tools/agent/worktree.js +270 -0
  399. package/src/tools/ask/ask.js +7 -2
  400. package/src/tools/bash/bash.js +112 -18
  401. package/src/tools/download/download.js +144 -0
  402. package/src/tools/edit/edit.js +1 -0
  403. package/src/tools/edit/multi-edit.js +1 -0
  404. package/src/tools/genimage/genimage.js +242 -0
  405. package/src/tools/genspeech/genspeech.js +67 -0
  406. package/src/tools/glob/glob.js +1 -0
  407. package/src/tools/grep/grep.js +3 -0
  408. package/src/tools/grep/rgPath.js +2 -0
  409. package/src/tools/index.js +25 -2
  410. package/src/tools/read/read.js +1 -0
  411. package/src/tools/shared/killTree.js +52 -0
  412. package/src/tools/toolProgressBridge.js +48 -0
  413. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  414. package/src/tools/webbrowser/browser.js +59 -42
  415. package/src/tools/webbrowser/browserControl.js +118 -0
  416. package/src/tools/webbrowser/browserDaemon.js +97 -0
  417. package/src/tools/webbrowser/browserService.js +27 -0
  418. package/src/tools/webbrowser/browserStream.js +119 -0
  419. package/src/tools/webbrowser/webbrowser.js +699 -130
  420. package/src/tools/webfetch/webfetch.js +1 -0
  421. package/src/tools/websearch/websearch.js +134 -51
  422. package/src/tools/write/write.js +1 -0
  423. package/src/ui/App.js +249 -22
  424. package/src/ui/AskQuestion.js +19 -17
  425. package/src/ui/InputBox.js +34 -9
  426. package/src/ui/LiveArea.js +112 -19
  427. package/src/ui/MessageList.js +100 -14
  428. package/src/ui/ModelPicker.js +56 -0
  429. package/src/ui/QueuedMessagesBanner.js +14 -0
  430. package/src/ui/Root.js +18 -10
  431. package/src/ui/SessionPicker.js +102 -0
  432. package/src/ui/StatusLine.js +13 -4
  433. package/src/ui/WorkspacePicker.js +55 -0
  434. package/src/ui/figures.js +44 -0
  435. package/src/ui/hooks/askSlot.js +82 -0
  436. package/src/ui/hooks/useChat.js +345 -64
  437. package/src/ui/hooks/usePasteHandler.js +11 -1
  438. package/src/ui/hooks/useTextBuffer.js +1 -3
  439. package/src/ui/liveViewport.js +5 -1
  440. package/src/ui/markdown.js +206 -0
  441. package/src/ui/textBuffer.js +0 -7
  442. package/src/ui/theme.js +32 -0
  443. package/src/utils/debugLog.js +0 -4
  444. package/src/utils/resourcePaths.js +2 -2
  445. package/src/utils/stripThink.js +50 -0
  446. package/src/web/browserBridge.js +447 -0
  447. package/src/web/protocol.js +19 -0
  448. package/workflows/adaptive-research.yaml +9 -2
  449. package/workflows/book-review-short.yaml +31 -7
  450. package/workflows/e2e-write-greet.yaml +3 -0
  451. package/workflows/illustrate.yaml +227 -0
  452. package/workflows/plan.yaml +437 -207
  453. package/workflows/ppt.yaml +1659 -0
  454. package/workflows/ralph-loop.yaml +142 -112
  455. package/workflows/schema.json +81 -23
  456. package/workflows/webppt.yaml +496 -0
  457. package/workflows/word.yaml +710 -0
  458. package/workflows/youtube-shorts.yaml +38 -10
  459. package/MINIMAL-TRACE.md +0 -77
  460. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  461. package/skills/config/SKILL.md +0 -123
  462. package/skills/frontend-design/LICENSE.txt +0 -177
  463. package/skills/frontend-design/SKILL.md +0 -42
  464. package/skills/pptx/scripts/office/__pycache__/soffice.cpython-314.pyc +0 -0
  465. package/src/ui/ProgressPanel.js +0 -54
  466. package/src/ui/StreamingBlock.js +0 -11
  467. package/src/ui/ToolStatus.js +0 -11
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ image_stats.py —— 图像信息统计:确定性算出图片的客观量化指标。
5
+
6
+ 为什么需要这个脚本?
7
+ ====================
8
+ minimal-agent 内核已支持多模态读图,模型能「看懂」图片;但「看懂」≠「量化」。
9
+ 宽高 / 分辨率 / 平均颜色 / 色调 / 直方图这类**具体数字**靠模型目测既不准也不稳——
10
+ 它会凭印象说「偏暖、大概 1920×1080」,却说不出平均 RGB、主色占比、亮度方差。
11
+ 这些都是确定性计算,应交脚本(与 image_plan_lint.py / layout_balance.py /
12
+ deck_pages_check.py 同哲学:机械活收归脚本,不烧 LLM、零幻觉)。
13
+
14
+ 算什么?
15
+ ========
16
+ 对每张图片:
17
+ - 基本:format / mode / 文件大小 / 是否带 alpha
18
+ - 尺寸:width×height / 宽高比 / 朝向(landscape/portrait/square) / 百万像素
19
+ - 分辨率:DPI(无元数据则 null)
20
+ - 平均颜色:平均 RGB + hex + 逐通道
21
+ - 亮度/对比:感知亮度均值 / 标准差(≈对比度) / min / max
22
+ - 色调:HSV 的饱和度加权环形平均 hue + 平均 sat/val + warm/cool/neutral 分类 + 是否灰阶
23
+ - 主色:quantize 取 top-N 主色 + 各自 hex 与占比
24
+ - 直方图:R/G/B 三通道 + 亮度,默认 16 bin 分箱(--bins 256 出全直方图)
25
+
26
+ 只依赖 Pillow + 标准库(json/argparse/os/sys/math)——**不引入 numpy**
27
+ (全仓库未用;PIL 自带 histogram()/ImageStat/quantize()/convert('HSV') 已够)。
28
+
29
+ 诚实边界
30
+ ========
31
+ - 逐像素统计(色调/主色/直方图)前把分析副本缩略到最长边 ≤512px 提速;
32
+ 尺寸/DPI/format 仍报**原图**真实值,不受缩略影响。
33
+ - 平均色/色调基于 RGB 通道,**忽略 alpha**(透明像素的 RGB 也计入);带 alpha 的图另有 has_alpha 标注。
34
+ - 色调环形平均按饱和度加权(避免大片灰色噪声拉偏 hue);饱和度极低时判 grayscale、hue 记 null。
35
+ - warm/cool/neutral 是色环的粗分类启发式(见 classify_tone),不是色彩学严格定义。
36
+
37
+ 退出码:0 = 正常(至少统计到 1 张,或 --scan 未发现图片);1 = 指定了图片但一张都读不了;2 = 参数错。
38
+ 单张图片读失败 → 打到 stderr 跳过、不影响其余。
39
+
40
+ 用法
41
+ ====
42
+ python image_stats.py a.png b.jpg # 文本报告
43
+ python image_stats.py ./shots --scan # 扫目录下所有受支持图片(非递归)
44
+ python image_stats.py a.png --json # 机读 JSON(工作流/宿主消费,stdout 只有 JSON)
45
+ python image_stats.py a.png --bins 256 --top 8 # 全直方图 + 取 8 个主色
46
+ """
47
+
48
+ import argparse
49
+ import json
50
+ import math
51
+ import os
52
+ import sys
53
+
54
+ from PIL import Image, ImageStat
55
+
56
+ # --scan 扫目录时识别的图片后缀;直接传文件路径则不受此限、能开就统计。
57
+ SUPPORTED_EXT = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tif", ".tiff"}
58
+
59
+ # 逐像素统计前缩略到的最长边像素(仅为提速,不改原图、不影响尺寸/DPI 报告)。
60
+ ANALYSIS_MAX_SIDE = 512
61
+
62
+ # 朝向阈值:与 extract_source_assets.py 保持一致,避免两套脚本对同一图判断打架。
63
+ LANDSCAPE_MIN = 1.25
64
+ PORTRAIT_MAX = 0.80
65
+
66
+ # 判定灰阶的平均饱和度上限(0~1)。低于此视为基本无彩色。
67
+ GRAYSCALE_SAT_MAX = 0.08
68
+
69
+
70
+ def human_size(n: int) -> str:
71
+ """字节数 → 人话(B/KB/MB)。"""
72
+ f = float(n)
73
+ for unit in ("B", "KB", "MB", "GB"):
74
+ if f < 1024 or unit == "GB":
75
+ return f"{f:.0f} {unit}" if unit == "B" else f"{f:.1f} {unit}"
76
+ f /= 1024
77
+ return f"{f:.1f} GB"
78
+
79
+
80
+ def to_hex(rgb) -> str:
81
+ r, g, b = (int(round(c)) for c in rgb[:3])
82
+ return f"#{r:02X}{g:02X}{b:02X}"
83
+
84
+
85
+ def classify_orientation(ar: float) -> str:
86
+ if ar >= LANDSCAPE_MIN:
87
+ return "landscape"
88
+ if ar <= PORTRAIT_MAX:
89
+ return "portrait"
90
+ return "square"
91
+
92
+
93
+ def classify_tone(hue_deg, is_grayscale: bool) -> str:
94
+ """色环粗分类(启发式):grayscale / warm / cool / neutral。"""
95
+ if is_grayscale or hue_deg is None:
96
+ return "grayscale"
97
+ h = hue_deg % 360
98
+ if h < 45 or h >= 315:
99
+ return "warm" # 红/橙 与 品红→红
100
+ if 90 <= h < 270:
101
+ return "cool" # 绿/青/蓝/紫
102
+ return "neutral" # 黄(45~90) 与 紫红(270~315) 过渡带
103
+
104
+
105
+ def bin_histogram(counts, bins: int):
106
+ """把 256 桶的单通道直方图重分箱到 bins 个桶。"""
107
+ if bins >= 256:
108
+ return list(counts)
109
+ out = [0] * bins
110
+ width = 256.0 / bins
111
+ for i, c in enumerate(counts):
112
+ out[min(bins - 1, int(i / width))] += c
113
+ return out
114
+
115
+
116
+ def compute_tone(hsv: Image.Image):
117
+ """对 HSV 缩略图算「饱和度加权环形平均 hue」+ 平均 sat/val。
118
+
119
+ 线性平均 hue 会因为红色在 0°/360° 处回绕而出错;这里用 sin/cos 向量和做环形
120
+ 平均,并以饱和度为权重(灰色像素 hue 无意义、不该参与)。
121
+ """
122
+ sin_sum = 0.0
123
+ cos_sum = 0.0
124
+ sat_weight = 0.0
125
+ sat_total = 0.0
126
+ val_total = 0.0
127
+ n = 0
128
+ for h, s, v in iter_pixels(hsv): # PIL HSV:每通道 0~255
129
+ n += 1
130
+ sat_total += s
131
+ val_total += v
132
+ if s > 0:
133
+ ang = (h / 256.0) * 2 * math.pi
134
+ sin_sum += math.sin(ang) * s
135
+ cos_sum += math.cos(ang) * s
136
+ sat_weight += s
137
+ if n == 0:
138
+ return {"hue_deg": None, "saturation": 0.0, "value": 0.0}
139
+ sat_mean = (sat_total / n) / 255.0
140
+ val_mean = (val_total / n) / 255.0
141
+ if sat_weight > 0 and (sin_sum or cos_sum):
142
+ hue_deg = math.degrees(math.atan2(sin_sum, cos_sum)) % 360.0
143
+ else:
144
+ hue_deg = None
145
+ return {
146
+ "hue_deg": None if hue_deg is None else round(hue_deg, 1),
147
+ "saturation": round(sat_mean, 4),
148
+ "value": round(val_mean, 4),
149
+ }
150
+
151
+
152
+ def iter_pixels(image: Image.Image):
153
+ """兼容 Pillow 新旧版本的像素迭代。
154
+
155
+ Pillow 13 起 Image.getdata() 弃用;新版提供 get_flattened_data()。这里优先用
156
+ 新 API,旧环境 fallback,避免为了一个像素迭代引入 numpy。
157
+ """
158
+ getter = getattr(image, "get_flattened_data", None)
159
+ if callable(getter):
160
+ return getter()
161
+ return image.getdata()
162
+
163
+
164
+ def dominant_colors(rgb: Image.Image, top: int):
165
+ """quantize 取 top-N 主色(含占比)。颜色数不足时自然返回更少。"""
166
+ q = rgb.quantize(colors=max(1, top), method=Image.Quantize.MEDIANCUT)
167
+ palette = q.getpalette() or []
168
+ counts = q.getcolors() or [] # [(count, palette_index), ...]
169
+ total = sum(c for c, _ in counts) or 1
170
+ counts.sort(reverse=True)
171
+ out = []
172
+ for count, idx in counts[:top]:
173
+ r, g, b = palette[idx * 3 : idx * 3 + 3]
174
+ out.append(
175
+ {"hex": to_hex((r, g, b)), "rgb": [r, g, b], "ratio": round(count / total, 4)}
176
+ )
177
+ return out
178
+
179
+
180
+ def analyze(path: str, bins: int, top: int) -> dict:
181
+ """统计单张图片,返回机读 dict。读失败抛异常由调用方捕获。"""
182
+ file_size = os.path.getsize(path)
183
+ with Image.open(path) as img:
184
+ fmt = img.format
185
+ mode = img.mode
186
+ width, height = img.size
187
+ dpi = img.info.get("dpi")
188
+ has_alpha = (
189
+ mode in ("RGBA", "LA", "PA")
190
+ or (mode == "P" and "transparency" in img.info)
191
+ or "transparency" in img.info
192
+ )
193
+
194
+ # 逐像素统计用的缩略 RGB 副本(thumbnail 只缩不放、保宽高比)。
195
+ rgb = img.convert("RGB")
196
+
197
+ analysis = rgb.copy()
198
+ analysis.thumbnail((ANALYSIS_MAX_SIDE, ANALYSIS_MAX_SIDE))
199
+
200
+ # 平均颜色 + 逐通道
201
+ rstat = ImageStat.Stat(analysis)
202
+ avg = rstat.mean # [r, g, b]
203
+
204
+ # 感知亮度(PIL 的 'L' 用 ITU-R 601-2:0.299R+0.587G+0.114B,正是感知亮度)
205
+ lum = analysis.convert("L")
206
+ lstat = ImageStat.Stat(lum)
207
+ lo, hi = lstat.extrema[0]
208
+
209
+ # 色调
210
+ tone = compute_tone(analysis.convert("HSV"))
211
+ is_gray = mode in ("L", "1", "I", "F") or tone["saturation"] < GRAYSCALE_SAT_MAX
212
+ category = classify_tone(tone["hue_deg"], is_gray)
213
+
214
+ # 直方图(分箱)
215
+ rgb_hist = analysis.histogram() # 768:R[0:256] G[256:512] B[512:768]
216
+ hist = {
217
+ "bins": bins if bins < 256 else 256,
218
+ "r": bin_histogram(rgb_hist[0:256], bins),
219
+ "g": bin_histogram(rgb_hist[256:512], bins),
220
+ "b": bin_histogram(rgb_hist[512:768], bins),
221
+ "luminance": bin_histogram(lum.histogram(), bins),
222
+ }
223
+
224
+ ar = width / height if height else 0.0
225
+ return {
226
+ "path": path,
227
+ "format": fmt,
228
+ "mode": mode,
229
+ "file_size_bytes": file_size,
230
+ "file_size_human": human_size(file_size),
231
+ "has_alpha": bool(has_alpha),
232
+ "width": width,
233
+ "height": height,
234
+ "aspect_ratio": round(ar, 4),
235
+ "orientation": classify_orientation(ar),
236
+ "megapixels": round(width * height / 1_000_000, 2),
237
+ "dpi": list(dpi) if dpi else None,
238
+ "average_color": {
239
+ "rgb": [round(c, 1) for c in avg],
240
+ "hex": to_hex(avg),
241
+ },
242
+ "brightness": {
243
+ "mean": round(lstat.mean[0], 1),
244
+ "stddev": round(lstat.stddev[0], 1),
245
+ "min": lo,
246
+ "max": hi,
247
+ },
248
+ "tone": {
249
+ "hue_deg": tone["hue_deg"],
250
+ "saturation": tone["saturation"],
251
+ "value": tone["value"],
252
+ "category": category,
253
+ "is_grayscale": bool(is_gray),
254
+ },
255
+ "dominant_colors": dominant_colors(analysis, top),
256
+ "histogram": hist,
257
+ }
258
+
259
+
260
+ def _spark(counts) -> str:
261
+ """把分箱直方图渲染成一行迷你火花线(仅文本报告用)。"""
262
+ blocks = "▁▂▃▄▅▆▇█"
263
+ peak = max(counts) if counts else 0
264
+ if peak == 0:
265
+ return " " * len(counts)
266
+ return "".join(blocks[min(7, int(c / peak * 7))] for c in counts)
267
+
268
+
269
+ def render_text(s: dict) -> str:
270
+ """单张图片的人话报告。"""
271
+ L = []
272
+ L.append(f"📷 {s['path']}")
273
+ L.append(
274
+ f" 格式 {s['format']} / {s['mode']}"
275
+ f"{' / 含透明' if s['has_alpha'] else ''} 大小 {s['file_size_human']}"
276
+ )
277
+ dpi = f" DPI {s['dpi'][0]:g}×{s['dpi'][1]:g}" if s["dpi"] else ""
278
+ L.append(
279
+ f" 尺寸 {s['width']}×{s['height']}px"
280
+ f"({s['orientation']},比例 {s['aspect_ratio']},{s['megapixels']}MP){dpi}"
281
+ )
282
+ ac = s["average_color"]
283
+ L.append(f" 平均色 {ac['hex']} rgb{tuple(ac['rgb'])}")
284
+ b = s["brightness"]
285
+ L.append(
286
+ f" 亮度 均值 {b['mean']}/255 对比(σ) {b['stddev']} 范围 {b['min']}~{b['max']}"
287
+ )
288
+ t = s["tone"]
289
+ hue = "n/a" if t["hue_deg"] is None else f"{t['hue_deg']}°"
290
+ L.append(
291
+ f" 色调 {t['category']} hue {hue} 饱和 {t['saturation']} 明度 {t['value']}"
292
+ )
293
+ doms = " ".join(f"{d['hex']}({d['ratio'] * 100:.0f}%)" for d in s["dominant_colors"])
294
+ L.append(f" 主色 {doms}")
295
+ h = s["histogram"]
296
+ L.append(f" 直方图({h['bins']}bin) L {_spark(h['luminance'])}")
297
+ L.append(
298
+ f" R {_spark(h['r'])} G {_spark(h['g'])} B {_spark(h['b'])}"
299
+ )
300
+ return "\n".join(L)
301
+
302
+
303
+ def collect_targets(args) -> list:
304
+ """把位置参数解析成图片路径列表(--scan 时把目录展开为受支持图片)。"""
305
+ targets = []
306
+ for t in args.targets:
307
+ if args.scan and os.path.isdir(t):
308
+ for name in sorted(os.listdir(t)):
309
+ if os.path.splitext(name)[1].lower() in SUPPORTED_EXT:
310
+ targets.append(os.path.join(t, name))
311
+ else:
312
+ targets.append(t)
313
+ return targets
314
+
315
+
316
+ def main() -> int:
317
+ # 强制 UTF-8 输出:脚本含中文,Windows 默认 GBK 控制台/管道会乱码或崩溃,
318
+ # 宿主(minimal-agent Bash 工具已 chcp 65001)按 UTF-8 捕获才稳。
319
+ for stream in (sys.stdout, sys.stderr):
320
+ try:
321
+ stream.reconfigure(encoding="utf-8")
322
+ except Exception:
323
+ pass
324
+
325
+ parser = argparse.ArgumentParser(
326
+ description="图像信息统计(确定性):宽高/分辨率/平均色/色调/主色/直方图"
327
+ )
328
+ parser.add_argument("targets", nargs="+", help="图片路径(可多个);配 --scan 则为目录")
329
+ parser.add_argument("--scan", action="store_true", help="把目录参数展开为其中所有受支持图片(非递归)")
330
+ parser.add_argument("--json", action="store_true", help="输出机读 JSON 数组(stdout 只有 JSON)")
331
+ parser.add_argument("--bins", type=int, default=16, help="直方图分箱数(默认 16;256 = 全直方图)")
332
+ parser.add_argument("--top", type=int, default=5, help="主色个数(默认 5)")
333
+ parser.add_argument("-v", "--verbose", action="store_true", help="把每张图进度打到 stderr")
334
+ args = parser.parse_args()
335
+
336
+ if args.bins < 1 or args.bins > 256:
337
+ print("参数错误:--bins 必须在 1~256 之间", file=sys.stderr)
338
+ return 2
339
+ if args.top < 1:
340
+ print("参数错误:--top 必须 >= 1", file=sys.stderr)
341
+ return 2
342
+
343
+ targets = collect_targets(args)
344
+ results = []
345
+ for path in targets:
346
+ if args.verbose:
347
+ print(f"[image-stats] 统计 {path}", file=sys.stderr)
348
+ try:
349
+ results.append(analyze(path, args.bins, args.top))
350
+ except Exception as e: # 单张失败不影响其余
351
+ print(f"[image-stats] 跳过 {path}:{e}", file=sys.stderr)
352
+
353
+ marker = f"IMAGE_STATS: 统计 {len(results)} 张图片(共 {len(targets)} 个目标)"
354
+ if args.json:
355
+ print(json.dumps(results, ensure_ascii=False, indent=2))
356
+ print(marker, file=sys.stderr) # JSON 模式 stdout 保持纯净,marker 走 stderr
357
+ else:
358
+ print("\n\n".join(render_text(s) for s in results) if results else "(无可统计图片)")
359
+ print("\n" + marker)
360
+
361
+ # 指定了目标却一张都没读出来 → 视为失败,让宿主/调用者能察觉。
362
+ if targets and not results:
363
+ return 1
364
+ return 0
365
+
366
+
367
+ if __name__ == "__main__":
368
+ sys.exit(main())
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ image_stats_test.py —— image_stats.py 的单测(PIL 合成图,自包含)。
5
+
6
+ 锁定:
7
+ - 尺寸/朝向:1600×900 横图 → landscape + aspect>1.5;600×1000 竖图 → portrait + aspect<0.8。
8
+ - 平均色:纯红 → #FF0000、rgb≈(255,0,0)。
9
+ - 色调:纯红 → warm hue≈0;纯蓝 → cool hue≈240;灰图 → grayscale + hue=None。
10
+ - 亮度:纯白 → mean=255 / σ=0;半黑半白 → σ 高(对比大)。
11
+ - alpha:RGBA 图 → has_alpha=True。
12
+ - 主色:纯红 → 主色第一名 #FF0000 占比≈1。
13
+ - 直方图:bins=8 → 各通道 8 桶;bins=256 → 256 桶。
14
+ - CLI:--json 的 stdout 是纯 JSON 数组且 bin 数=指定值;读不出任何图 → 退出码 1。
15
+ 依赖:Pillow(已就绪)。
16
+ """
17
+
18
+ import json
19
+ import os
20
+ import subprocess
21
+ import sys
22
+ import tempfile
23
+ import unittest
24
+
25
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
26
+ import image_stats as im # noqa: E402
27
+
28
+ from PIL import Image # noqa: E402
29
+
30
+ SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image_stats.py")
31
+
32
+
33
+ def _png(path, w, h, color, mode="RGB"):
34
+ Image.new(mode, (w, h), color).save(path)
35
+ return path
36
+
37
+
38
+ class TestDimensions(unittest.TestCase):
39
+ def setUp(self):
40
+ self.tmp = tempfile.mkdtemp(prefix="imgstats-dim-")
41
+
42
+ def test_landscape(self):
43
+ p = _png(os.path.join(self.tmp, "l.png"), 1600, 900, (80, 120, 200))
44
+ s = im.analyze(p, bins=16, top=5)
45
+ self.assertEqual((s["width"], s["height"]), (1600, 900))
46
+ self.assertEqual(s["orientation"], "landscape")
47
+ self.assertGreater(s["aspect_ratio"], 1.5)
48
+ self.assertAlmostEqual(s["megapixels"], 1.44, places=2)
49
+ self.assertEqual(s["format"], "PNG")
50
+
51
+ def test_portrait(self):
52
+ p = _png(os.path.join(self.tmp, "p.png"), 600, 1000, (30, 30, 30))
53
+ s = im.analyze(p, bins=16, top=5)
54
+ self.assertEqual(s["orientation"], "portrait")
55
+ self.assertLess(s["aspect_ratio"], 0.8)
56
+
57
+
58
+ class TestColorAndTone(unittest.TestCase):
59
+ def setUp(self):
60
+ self.tmp = tempfile.mkdtemp(prefix="imgstats-color-")
61
+
62
+ def test_pure_red_warm(self):
63
+ p = _png(os.path.join(self.tmp, "red.png"), 64, 64, (255, 0, 0))
64
+ s = im.analyze(p, bins=16, top=5)
65
+ self.assertEqual(s["average_color"]["hex"], "#FF0000")
66
+ self.assertEqual([round(c) for c in s["average_color"]["rgb"]], [255, 0, 0])
67
+ self.assertEqual(s["tone"]["category"], "warm")
68
+ self.assertFalse(s["tone"]["is_grayscale"])
69
+ # hue 接近 0°(红)
70
+ self.assertTrue(s["tone"]["hue_deg"] < 15 or s["tone"]["hue_deg"] > 345)
71
+
72
+ def test_pure_blue_cool(self):
73
+ p = _png(os.path.join(self.tmp, "blue.png"), 64, 64, (0, 0, 255))
74
+ s = im.analyze(p, bins=16, top=5)
75
+ self.assertEqual(s["tone"]["category"], "cool")
76
+ self.assertAlmostEqual(s["tone"]["hue_deg"], 240, delta=15)
77
+
78
+ def test_gray_is_grayscale(self):
79
+ p = _png(os.path.join(self.tmp, "gray.png"), 64, 64, (128, 128, 128))
80
+ s = im.analyze(p, bins=16, top=5)
81
+ self.assertTrue(s["tone"]["is_grayscale"])
82
+ self.assertEqual(s["tone"]["category"], "grayscale")
83
+ self.assertIsNone(s["tone"]["hue_deg"])
84
+
85
+ def test_dominant_color(self):
86
+ p = _png(os.path.join(self.tmp, "red.png"), 64, 64, (255, 0, 0))
87
+ s = im.analyze(p, bins=16, top=5)
88
+ self.assertEqual(s["dominant_colors"][0]["hex"], "#FF0000")
89
+ self.assertAlmostEqual(s["dominant_colors"][0]["ratio"], 1.0, places=2)
90
+
91
+
92
+ class TestBrightnessAlpha(unittest.TestCase):
93
+ def setUp(self):
94
+ self.tmp = tempfile.mkdtemp(prefix="imgstats-bri-")
95
+
96
+ def test_white_brightness(self):
97
+ p = _png(os.path.join(self.tmp, "w.png"), 32, 32, (255, 255, 255))
98
+ s = im.analyze(p, bins=16, top=5)
99
+ self.assertEqual(s["brightness"]["mean"], 255)
100
+ self.assertEqual(s["brightness"]["stddev"], 0)
101
+ self.assertEqual((s["brightness"]["min"], s["brightness"]["max"]), (255, 255))
102
+
103
+ def test_high_contrast(self):
104
+ # 左半黑右半白 → 亮度标准差很大
105
+ img = Image.new("RGB", (64, 32), (0, 0, 0))
106
+ for x in range(32, 64):
107
+ for y in range(32):
108
+ img.putpixel((x, y), (255, 255, 255))
109
+ p = os.path.join(self.tmp, "split.png")
110
+ img.save(p)
111
+ s = im.analyze(p, bins=16, top=5)
112
+ self.assertGreater(s["brightness"]["stddev"], 100)
113
+
114
+ def test_rgba_has_alpha(self):
115
+ p = _png(os.path.join(self.tmp, "a.png"), 16, 16, (255, 0, 0, 128), mode="RGBA")
116
+ s = im.analyze(p, bins=16, top=5)
117
+ self.assertTrue(s["has_alpha"])
118
+ self.assertEqual(s["mode"], "RGBA")
119
+
120
+
121
+ class TestHistogram(unittest.TestCase):
122
+ def setUp(self):
123
+ self.tmp = tempfile.mkdtemp(prefix="imgstats-hist-")
124
+
125
+ def test_bins(self):
126
+ p = _png(os.path.join(self.tmp, "g.png"), 64, 64, (10, 200, 90))
127
+ s8 = im.analyze(p, bins=8, top=5)
128
+ self.assertEqual(s8["histogram"]["bins"], 8)
129
+ for ch in ("r", "g", "b", "luminance"):
130
+ self.assertEqual(len(s8["histogram"][ch]), 8)
131
+ s256 = im.analyze(p, bins=256, top=5)
132
+ self.assertEqual(s256["histogram"]["bins"], 256)
133
+ self.assertEqual(len(s256["histogram"]["r"]), 256)
134
+
135
+
136
+ class TestCli(unittest.TestCase):
137
+ def setUp(self):
138
+ self.tmp = tempfile.mkdtemp(prefix="imgstats-cli-")
139
+
140
+ def test_json_stdout_is_pure_json(self):
141
+ p = _png(os.path.join(self.tmp, "x.png"), 100, 50, (200, 50, 50))
142
+ r = subprocess.run(
143
+ [sys.executable, SCRIPT, p, "--json", "--bins", "8"],
144
+ capture_output=True, text=True, encoding="utf-8",
145
+ )
146
+ self.assertEqual(r.returncode, 0)
147
+ data = json.loads(r.stdout) # stdout 必须是纯 JSON
148
+ self.assertEqual(len(data), 1)
149
+ self.assertEqual(data[0]["width"], 100)
150
+ self.assertEqual(len(data[0]["histogram"]["r"]), 8)
151
+ self.assertIn("IMAGE_STATS:", r.stderr) # marker 走 stderr 不污染 JSON
152
+
153
+ def test_text_mode_marker(self):
154
+ p = _png(os.path.join(self.tmp, "x.png"), 20, 20, (10, 10, 10))
155
+ r = subprocess.run([sys.executable, SCRIPT, p], capture_output=True, text=True, encoding="utf-8")
156
+ self.assertEqual(r.returncode, 0)
157
+ self.assertIn("IMAGE_STATS:", r.stdout)
158
+
159
+ def test_unreadable_target_exit_1(self):
160
+ bogus = os.path.join(self.tmp, "not-an-image.txt")
161
+ with open(bogus, "w") as f:
162
+ f.write("hello")
163
+ r = subprocess.run([sys.executable, SCRIPT, bogus], capture_output=True, text=True, encoding="utf-8")
164
+ self.assertEqual(r.returncode, 1) # 给了目标但一张都读不出来
165
+
166
+ def test_scan_dir(self):
167
+ _png(os.path.join(self.tmp, "a.png"), 40, 40, (1, 2, 3))
168
+ _png(os.path.join(self.tmp, "b.jpg"), 50, 30, (4, 5, 6))
169
+ with open(os.path.join(self.tmp, "note.txt"), "w") as f:
170
+ f.write("ignored")
171
+ r = subprocess.run(
172
+ [sys.executable, SCRIPT, self.tmp, "--scan", "--json"],
173
+ capture_output=True, text=True, encoding="utf-8",
174
+ )
175
+ self.assertEqual(r.returncode, 0)
176
+ data = json.loads(r.stdout)
177
+ self.assertEqual(len(data), 2) # 只统计 png/jpg,忽略 txt
178
+
179
+
180
+ if __name__ == "__main__":
181
+ unittest.main(verbosity=2)