thursday-agent 0.9.0 → 0.11.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 (320) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +0 -1
  3. package/.next/build-manifest.json +3 -3
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/routes-manifest.json +0 -6
  6. package/.next/server/app/_global-error/page.js.nft.json +1 -1
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found/page.js.nft.json +1 -1
  13. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +2 -2
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  19. package/.next/server/app/api/artifact/route.js.nft.json +1 -1
  20. package/.next/server/app/api/bot/keep-working/route.js.nft.json +1 -1
  21. package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
  22. package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
  23. package/.next/server/app/api/bot/route.js.nft.json +1 -1
  24. package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
  25. package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
  26. package/.next/server/app/api/config/route.js.nft.json +1 -1
  27. package/.next/server/app/api/events/route.js.nft.json +1 -1
  28. package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
  29. package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
  30. package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
  31. package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
  32. package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
  33. package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
  34. package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
  35. package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
  36. package/.next/server/app/api/mcp/route.js.nft.json +1 -1
  37. package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
  38. package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
  39. package/.next/server/app/api/memory/route.js.nft.json +1 -1
  40. package/.next/server/app/api/reach/route.js +13 -13
  41. package/.next/server/app/api/reach/route.js.nft.json +1 -1
  42. package/.next/server/app/api/routine/route.js.nft.json +1 -1
  43. package/.next/server/app/api/signins/route.js +1 -1
  44. package/.next/server/app/api/signins/route.js.nft.json +1 -1
  45. package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
  46. package/.next/server/app/api/skills/route.js.nft.json +1 -1
  47. package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
  48. package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
  49. package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
  50. package/.next/server/app/api/thursday/tool-call/route.js +14 -15
  51. package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
  52. package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
  53. package/.next/server/app/api/workspace/route.js.nft.json +1 -1
  54. package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
  55. package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
  56. package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
  57. package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
  58. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
  59. package/.next/server/app/page/server-reference-manifest.json +56 -56
  60. package/.next/server/app/page.js +4 -4
  61. package/.next/server/app/page.js.nft.json +1 -1
  62. package/.next/server/app/page_client-reference-manifest.js +1 -1
  63. package/.next/server/app-paths-manifest.json +0 -1
  64. package/.next/server/chunks/[root-of-the-server]__014ydvq._.js +2 -2
  65. package/.next/server/chunks/[root-of-the-server]__0209ah4._.js +7 -5
  66. package/.next/server/chunks/[root-of-the-server]__04r4h-2._.js +4 -2
  67. package/.next/server/chunks/[root-of-the-server]__0542v_d._.js +3 -0
  68. package/.next/server/chunks/[root-of-the-server]__0gewdg9._.js +2 -2
  69. package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
  70. package/.next/server/chunks/[root-of-the-server]__1-swlkw._.js +2 -2
  71. package/.next/server/chunks/[root-of-the-server]__12iddfq._.js +1 -1
  72. package/.next/server/chunks/[root-of-the-server]__1ai5lme._.js +3 -3
  73. package/.next/server/chunks/[root-of-the-server]__1cjxw0-._.js +2 -2
  74. package/.next/server/chunks/{[root-of-the-server]__1fy2g82._.js → [root-of-the-server]__1k3j52n._.js} +1 -1
  75. package/.next/server/chunks/[root-of-the-server]__1kyjt5m._.js +3 -3
  76. package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
  77. package/.next/server/chunks/_05v8k21._.js +3 -1
  78. package/.next/server/chunks/_0awtznl._.js +1 -1
  79. package/.next/server/chunks/_0na2fj1._.js +1 -1
  80. package/.next/server/chunks/_0x-ej3f._.js +17 -17
  81. package/.next/server/chunks/_12-amdu._.js +2 -2
  82. package/.next/server/chunks/_15740dw._.js +60 -0
  83. package/.next/server/chunks/_1dc_d1q._.js +3 -3
  84. package/.next/server/chunks/_1i350l5._.js +3 -1
  85. package/.next/server/chunks/_1jc5g4p._.js +2 -2
  86. package/.next/server/chunks/_1jn-aep._.js +4 -4
  87. package/.next/server/chunks/_1m88lkh._.js +11 -11
  88. package/.next/server/chunks/_1ogqovh._.js +4 -4
  89. package/.next/server/chunks/_1uuucy-._.js +1 -1
  90. package/.next/server/chunks/_1zxp-p6._.js +1 -1
  91. package/.next/server/chunks/features_0rfvely._.js +5 -3
  92. package/.next/server/chunks/ssr/{[root-of-the-server]__1se25x0._.js → [root-of-the-server]__0hpp3nw._.js} +29 -13
  93. package/.next/server/chunks/ssr/[root-of-the-server]__0m7u9j-._.js +3 -0
  94. package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +24 -60
  95. package/.next/server/chunks/ssr/[root-of-the-server]__1cz2rz5._.js +3 -0
  96. package/.next/server/chunks/ssr/{[root-of-the-server]__0tvavy9._.js → [root-of-the-server]__1iwyp1e._.js} +1 -1
  97. package/.next/server/chunks/ssr/[root-of-the-server]__1w9hqq8._.js +1 -1
  98. package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
  99. package/.next/server/chunks/ssr/{_1tmu8z-._.js → _0btzccu._.js} +2 -2
  100. package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
  101. package/.next/server/chunks/ssr/_0hjzb5o._.js +3 -0
  102. package/.next/server/chunks/ssr/_0ld9_qq._.js +1 -1
  103. package/.next/server/chunks/ssr/{_0urj3mc._.js → _0ouzu9f._.js} +2 -2
  104. package/.next/server/chunks/ssr/{_1-zyvdb._.js → _0u7ko71._.js} +2 -2
  105. package/.next/server/chunks/ssr/{_0ltz2q5._.js → _0wd_o8-._.js} +2 -2
  106. package/.next/server/chunks/ssr/_0ya0cb0._.js +3 -0
  107. package/.next/server/chunks/ssr/_10yyqtk._.js +67 -0
  108. package/.next/server/chunks/ssr/{_1uy0xom._.js → _12eyown._.js} +3 -3
  109. package/.next/server/chunks/ssr/_12mijol._.js +11 -11
  110. package/.next/server/chunks/ssr/_132qkfz._.js +1 -1
  111. package/.next/server/chunks/ssr/_184327j._.js +1 -1
  112. package/.next/server/chunks/ssr/{_1z74dmq._.js → _19ps4p3._.js} +7 -7
  113. package/.next/server/chunks/ssr/{_1flbs77._.js → _1bhy4yg._.js} +2 -2
  114. package/.next/server/chunks/ssr/_1d423aj._.js +7 -5
  115. package/.next/server/chunks/ssr/{_1a-nuxx._.js → _1kd843u._.js} +2 -2
  116. package/.next/server/chunks/ssr/{_0ital-2._.js → _1kq2ye_._.js} +2 -2
  117. package/.next/server/chunks/ssr/_1mqn9mc._.js +1 -1
  118. package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
  119. package/.next/server/chunks/ssr/_1p8xorg._.js +3 -0
  120. package/.next/server/chunks/ssr/{_1x-kojb._.js → _1q683w8._.js} +3 -3
  121. package/.next/server/chunks/ssr/_1tawxu2._.js +3 -0
  122. package/.next/server/chunks/ssr/{_0scf7xy._.js → _1w2oba6._.js} +2 -2
  123. package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
  124. package/.next/server/chunks/ssr/{_0kjj5ts._.js → _1ybo1hd._.js} +3 -3
  125. package/.next/server/chunks/ssr/features_bot_0s11m4r._.js +3 -0
  126. package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
  127. package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
  128. package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
  129. package/.next/server/chunks/ssr/features_workspace_00jz2hu._.js +1 -1
  130. package/.next/server/chunks/ssr/{node_modules_1af3w91._.js → node_modules_00pocul._.js} +1 -1
  131. package/.next/server/chunks/ssr/{node_modules_0th357h._.js → node_modules_02zt4az._.js} +1 -1
  132. package/.next/server/chunks/ssr/{node_modules_0jljel9._.js → node_modules_05ojmcl._.js} +1 -1
  133. package/.next/server/chunks/ssr/{node_modules_0wnlddw._.js → node_modules_06i6bli._.js} +1 -1
  134. package/.next/server/chunks/ssr/{node_modules_06bb4q8._.js → node_modules_09dj429._.js} +1 -1
  135. package/.next/server/chunks/ssr/{node_modules_068ws5v._.js → node_modules_0auzgvx._.js} +1 -1
  136. package/.next/server/chunks/ssr/{node_modules_1ogczzz._.js → node_modules_0evjwh1._.js} +1 -1
  137. package/.next/server/chunks/ssr/{node_modules_13kwejm._.js → node_modules_0gmdur8._.js} +1 -1
  138. package/.next/server/chunks/ssr/{node_modules_1s-gb-1._.js → node_modules_0j3te-1._.js} +1 -1
  139. package/.next/server/chunks/ssr/{node_modules_1bucjlk._.js → node_modules_0rbaymq._.js} +1 -1
  140. package/.next/server/chunks/ssr/{node_modules_1wbkzl9._.js → node_modules_0tf8_1e._.js} +1 -1
  141. package/.next/server/chunks/ssr/{node_modules_12et8d5._.js → node_modules_0yqy-0b._.js} +1 -1
  142. package/.next/server/chunks/ssr/{node_modules_1lkix61._.js → node_modules_1-53ebx._.js} +1 -1
  143. package/.next/server/chunks/ssr/{node_modules_03d0rfz._.js → node_modules_1-y9aho._.js} +1 -1
  144. package/.next/server/chunks/ssr/{node_modules_0x630an._.js → node_modules_11bltzp._.js} +1 -1
  145. package/.next/server/chunks/ssr/{node_modules_0uyh_vb._.js → node_modules_18jn2qo._.js} +1 -1
  146. package/.next/server/chunks/ssr/{node_modules_0dzm4um._.js → node_modules_1_te3bu._.js} +1 -1
  147. package/.next/server/chunks/ssr/{node_modules_07rs_p2._.js → node_modules_1c0pg3k._.js} +1 -1
  148. package/.next/server/chunks/ssr/{node_modules_0sgy-dc._.js → node_modules_1en2jx5._.js} +1 -1
  149. package/.next/server/chunks/ssr/{node_modules_0ubdtk-._.js → node_modules_1p-c8cr._.js} +1 -1
  150. package/.next/server/chunks/ssr/{node_modules_1z1lrbk._.js → node_modules_1qe-oxg._.js} +1 -1
  151. package/.next/server/chunks/ssr/{node_modules_04_sbbc._.js → node_modules_1rjt833._.js} +1 -1
  152. package/.next/server/chunks/ssr/{node_modules_0yvz9e0._.js → node_modules_1tzw5sn._.js} +1 -1
  153. package/.next/server/chunks/ssr/{node_modules_1uwg-gz._.js → node_modules_1x6uwvn._.js} +1 -1
  154. package/.next/server/chunks/ssr/{node_modules_0t_ez6-._.js → node_modules_1zqddke._.js} +1 -1
  155. package/.next/server/chunks/ssr/{node_modules_05ybmta._.js → node_modules_211zgim._.js} +1 -1
  156. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0r443fh._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0y51bqj._.js} +1 -1
  157. package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_10wnlbk._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1sc114e._.js} +1 -1
  158. package/.next/server/middleware-build-manifest.js +3 -3
  159. package/.next/server/pages/404.html +1 -1
  160. package/.next/server/pages/500.html +1 -1
  161. package/.next/server/server-reference-manifest.js +1 -1
  162. package/.next/server/server-reference-manifest.json +57 -57
  163. package/.next/static/chunks/{13ixzheqw7o-w.js → 03-724sj0nk_z.js} +2 -2
  164. package/.next/static/chunks/03bhu4mk8en2q.js +1 -0
  165. package/.next/static/chunks/{3h2e-xv1zxh_f.js → 04imzjhvzw65d.js} +2 -2
  166. package/.next/static/chunks/{27tvv1i-smrck.js → 0gljzv9fz1g7d.js} +1 -1
  167. package/.next/static/chunks/{37j27b3e1gt7r.js → 0h0lythv1ql93.js} +1 -1
  168. package/.next/static/chunks/{2o4dndk0d68do.js → 0hzbj7f48cmx0.js} +1 -1
  169. package/.next/static/chunks/0lwunwbl7u7pt.js +1 -0
  170. package/.next/static/chunks/{0u9ae9k_2g7k3.js → 0r6nwxg4dv83t.js} +1 -1
  171. package/.next/static/chunks/0rn9nbg6aif9f.js +17 -0
  172. package/.next/static/chunks/{3bbzsitgiltnt.js → 0x_w4shtvrrfg.js} +1 -1
  173. package/.next/static/chunks/{2sdwbfs1x5skp.js → 0ympuji_ufypd.js} +1 -1
  174. package/.next/static/chunks/{3t9huugh-5_ps.js → 0zmrz4zfzm4dt.js} +1 -1
  175. package/.next/static/chunks/{0qmd8n52hp-6v.js → 11tlxkr7a_rpc.js} +1 -1
  176. package/.next/static/chunks/{2c64drdevdla3.js → 12scesro428m7.js} +2 -2
  177. package/.next/static/chunks/16s2fx3890_jd.js +65 -0
  178. package/.next/static/chunks/{0oyz95sujd2c7.js → 1qzfgplyo18tc.js} +1 -1
  179. package/.next/static/chunks/{04e-e--5lk26x.js → 1r4zajz14jp9-.js} +1 -1
  180. package/.next/static/chunks/1s6m4cmbn7htf.js +1 -0
  181. package/.next/static/chunks/1wl9t1-3i-q8p.css +1 -0
  182. package/.next/static/chunks/2-asot3p1j3yg.js +1 -0
  183. package/.next/static/chunks/{0u3ktztrzu1fa.js → 2-jn4xscbm-i2.js} +1 -1
  184. package/.next/static/chunks/22h9gvclqtx_2.js +1 -0
  185. package/.next/static/chunks/{337g9g_ux2gj-.js → 26-eblxwnykck.js} +1 -1
  186. package/.next/static/chunks/{43q9cgbuw8krx.js → 2kzwhbuobpkw3.js} +2 -2
  187. package/.next/static/chunks/2m6jd3586ijb-.js +1 -0
  188. package/.next/static/chunks/{0_awn8tucznxc.js → 304q9p54tcgm1.js} +1 -1
  189. package/.next/static/chunks/{1u5i-jti87jbo.js → 35wdx3wt7cg4a.js} +1 -1
  190. package/.next/static/chunks/37unh9460im7v.js +18 -0
  191. package/.next/static/chunks/{01xjer0i_e5z5.js → 39gj5vxz1zpm_.js} +1 -1
  192. package/.next/static/chunks/{427qsc6kahxyo.js → 39vrqy1r_00jc.js} +1 -1
  193. package/.next/static/chunks/{245hb85nsy6j4.js → 3dipmu0mptqum.js} +1 -1
  194. package/.next/static/chunks/3gki-squw28yu.js +1 -0
  195. package/.next/static/chunks/{3obf9n3cxj99k.js → 3jvldk2wvwbge.js} +1 -1
  196. package/.next/static/chunks/{38301ji-85i30.js → 3nnzdl_te20hv.js} +1 -1
  197. package/.next/static/chunks/{2m267diam_vib.js → 3tz4dr0_8pq1-.js} +1 -1
  198. package/.next/static/chunks/{2gocl3dq0_h1s.js → 3x6pggb-2kyrk.js} +1 -1
  199. package/.next/static/chunks/{025bmrgeukcb2.js → 4440mws9jz3mt.js} +1 -1
  200. package/README.md +4 -0
  201. package/app/api/thursday/tool-call/route.ts +1 -15
  202. package/components/ui/radio-group.tsx +2 -2
  203. package/docs/images/demo-still.jpg +0 -0
  204. package/docs/live-calls.md +19 -11
  205. package/features/ai/prompts/live.prompt.ts +51 -26
  206. package/features/ai/prompts/prompt-helper.ts +1 -9
  207. package/features/ai/prompts/thursday.prompt.ts +8 -2
  208. package/features/bot/bot.run.ts +9 -3
  209. package/features/bot/bot.seed.ts +35 -91
  210. package/features/bot/components/bot-room.tsx +0 -6
  211. package/features/bot/components/bot-setting.tsx +4 -4
  212. package/features/bot/components/room-pill.tsx +4 -2
  213. package/features/bot/seed-bots.ts +2 -2
  214. package/features/intro/components/intro.tsx +2 -3
  215. package/features/reach/reach.ts +2 -2
  216. package/features/routine/components/routine-setting.tsx +1 -1
  217. package/features/signins/signins.query.ts +66 -1
  218. package/features/thursday/components/side-captions.tsx +31 -8
  219. package/features/thursday/components/thursday-setting.tsx +5 -5
  220. package/features/thursday/components/thursday.tsx +97 -39
  221. package/features/thursday/components/write-line.tsx +28 -17
  222. package/features/thursday/thursday.action.ts +4 -20
  223. package/features/thursday/thursday.schema.ts +0 -9
  224. package/features/thursday/thursday.store.ts +11 -15
  225. package/features/thursday/thursday.text.ts +10 -8
  226. package/features/thursday/tool-line.ts +17 -6
  227. package/features/thursday/use-call-ring.ts +1 -5
  228. package/features/thursday/use-text-call.ts +37 -21
  229. package/features/thursday/use-thursday.ts +14 -44
  230. package/features/workspace/workspace.ts +4 -2
  231. package/guide/bots.md +13 -3
  232. package/guide/calls.md +7 -5
  233. package/knip.json +1 -0
  234. package/lib/live/live.session.ts +0 -39
  235. package/package.json +1 -1
  236. package/scripts/live-session.test.mts +19 -26
  237. package/seed-skills/analyst/README.md +18 -0
  238. package/seed-skills/analyst/data-report/SKILL.md +60 -0
  239. package/seed-skills/analyst/data-report/references/brief.md +31 -0
  240. package/seed-skills/analyst/data-report/references/company.md +41 -0
  241. package/seed-skills/analyst/data-report/references/comparison.md +42 -0
  242. package/seed-skills/analyst/data-report/references/decision.md +28 -0
  243. package/seed-skills/analyst/data-report/references/market-size.md +49 -0
  244. package/seed-skills/analyst/data-report/references/price-check.md +33 -0
  245. package/seed-skills/analyst/data-report/references/sources.md +51 -0
  246. package/seed-skills/analyst/data-report/references/trend.md +39 -0
  247. package/seed-skills/analyst/data-report/scripts/fetch.mjs +521 -0
  248. package/seed-skills/insta/instagram/SKILL.md +36 -0
  249. package/seed-skills/insta/instagram/references/dm.md +49 -0
  250. package/seed-skills/insta/instagram/references/post.md +74 -0
  251. package/seed-skills/insta/instagram/scripts/compose.mjs +333 -0
  252. package/seed-skills/insta/instagram/scripts/inbox.mjs +163 -0
  253. package/seed-skills/insta/instagram/scripts/lib.mjs +33 -0
  254. package/seed-skills/insta/instagram/scripts/media.mjs +104 -0
  255. package/seed-skills/insta/instagram/scripts/profile.mjs +143 -0
  256. package/seed-skills/insta/instagram/scripts/read.mjs +244 -0
  257. package/seed-skills/insta/instagram/scripts/search.mjs +137 -0
  258. package/seed-skills/insta/instagram/scripts/selectors.mjs +56 -0
  259. package/seed-skills/insta/instagram/scripts/sheet.mjs +43 -0
  260. package/seed-skills/insta/instagram/templates/carousel.html +62 -0
  261. package/seed-skills/marketer/README.md +6 -4
  262. package/seed-skills/marketer/copywriting/SKILL.md +1 -1
  263. package/seed-skills/marketer/emails/SKILL.md +1 -1
  264. package/seed-skills/marketer/launch/SKILL.md +1 -1
  265. package/seed-skills/marketer/product-marketing/SKILL.md +5 -5
  266. package/seed-skills/marketer/seo-audit/SKILL.md +1 -1
  267. package/seed-skills/marketer/social/SKILL.md +1 -1
  268. package/seed-skills/tutor/picture-book/SKILL.md +16 -0
  269. package/seed-skills/tutor/picture-book/book/book.css +181 -0
  270. package/seed-skills/tutor/picture-book/book/book.html +33 -0
  271. package/seed-skills/tutor/picture-book/book/book.js +60 -0
  272. package/seed-skills/tutor/picture-book/references/picture-book.md +104 -0
  273. package/seed-skills/tutor/picture-book/scripts/book-video.mjs +177 -0
  274. package/seed-skills/tutor/picture-book/scripts/book.mjs +98 -0
  275. package/skills/browser/SKILL.md +13 -4
  276. package/skills/browser/references/scripts.md +55 -0
  277. package/skills/browser/scripts/image-size.mjs +30 -0
  278. package/skills/browser/scripts/render.mjs +126 -0
  279. package/skills/browser/scripts/session.mjs +105 -0
  280. package/skills/browser/scripts/sheet.mjs +97 -0
  281. package/skills/browser/scripts/webimage.mjs +105 -0
  282. package/skills/interactive-page/SKILL.md +4 -3
  283. package/skills/interactive-page/scripts/chart.mjs +679 -0
  284. package/skills/interactive-page/scripts/page.mjs +1 -0
  285. package/.next/server/app/api/probe/route/app-paths-manifest.json +0 -3
  286. package/.next/server/app/api/probe/route/build-manifest.json +0 -12
  287. package/.next/server/app/api/probe/route/server-reference-manifest.json +0 -4
  288. package/.next/server/app/api/probe/route.js +0 -10
  289. package/.next/server/app/api/probe/route.js.map +0 -5
  290. package/.next/server/app/api/probe/route.js.nft.json +0 -1
  291. package/.next/server/app/api/probe/route_client-reference-manifest.js +0 -3
  292. package/.next/server/chunks/[root-of-the-server]__012s2o8._.js +0 -3
  293. package/.next/server/chunks/[root-of-the-server]__0z0hc1f._.js +0 -4
  294. package/.next/server/chunks/[root-of-the-server]__1v7-zke._.js +0 -5
  295. package/.next/server/chunks/_0whlpyi._.js +0 -4
  296. package/.next/server/chunks/_next-internal_server_app_api_probe_route_actions_0cqd30j.js +0 -3
  297. package/.next/server/chunks/ssr/[root-of-the-server]__0oo-wlv._.js +0 -103
  298. package/.next/server/chunks/ssr/[root-of-the-server]__0zuns7e._.js +0 -3
  299. package/.next/server/chunks/ssr/_070nv1_._.js +0 -3
  300. package/.next/server/chunks/ssr/_0dshsh4._.js +0 -3
  301. package/.next/server/chunks/ssr/_0l_mcfz._.js +0 -3
  302. package/.next/server/chunks/ssr/_0u5xv6g._.js +0 -3
  303. package/.next/server/chunks/ssr/_157yfrk._.js +0 -3
  304. package/.next/server/chunks/ssr/features_bot_1nr94ks._.js +0 -3
  305. package/.next/static/chunks/06nt9tatr-dug.js +0 -1
  306. package/.next/static/chunks/0ge-hx0ed83a1.js +0 -1
  307. package/.next/static/chunks/0pda_vf-abwvx.js +0 -1
  308. package/.next/static/chunks/0rowp9rx3qbxu.js +0 -18
  309. package/.next/static/chunks/1grkt5gr8pbg1.js +0 -17
  310. package/.next/static/chunks/1yugpvvi8qrbu.js +0 -101
  311. package/.next/static/chunks/27lkanxvtdn09.js +0 -1
  312. package/.next/static/chunks/3fec-aeteddr-.js +0 -1
  313. package/.next/static/chunks/3go3hwp1z2_h2.css +0 -1
  314. package/.next/static/chunks/3h2bun3hcggg8.js +0 -1
  315. package/.next/static/chunks/3j1y335o95mj7.js +0 -1
  316. package/lib/probe.server.ts +0 -17
  317. package/lib/probe.ts +0 -55
  318. /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_buildManifest.js +0 -0
  319. /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_clientMiddlewareManifest.js +0 -0
  320. /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_ssgManifest.js +0 -0
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env node
2
+ // Draws a CSV into a page as one figure: an inline SVG chart, its source, and the rows
3
+ // behind it with a CSV download. The page is a quick page (`page.mjs quick` beside this
4
+ // script) or any HTML file; the figure takes the place of the element with the given id,
5
+ // or goes before </body> when there is none. Run it again and the same figure is replaced.
6
+ //
7
+ // node chart.mjs <page.html> <id> <data.csv> [options]
8
+ //
9
+ // --kind line|bar line when the first column is dates, bar otherwise
10
+ // --title "<text>" what the chart shows (default: the CSV's `# title:`)
11
+ // --columns a,b which value columns to draw (default: all)
12
+ // --from / --to a date range to draw, YYYY[-MM[-DD]]
13
+ // --index rebase every line to 100 at its first shared date
14
+ // --mark "2025-03=Rate cut" a dated event line; repeat for more
15
+ // --unit "%" --prefix "$" around every number shown
16
+ // --highlight "<label>" the one bar in the accent color; the rest go quiet
17
+ // --keep-order bars in the CSV's order instead of largest first
18
+ // --source "<url or text>" when the CSV has no `# source:` line
19
+ // --note "<text>" one line under the chart: an estimate, a gap, a break in the series
20
+ // --locale ko how numbers and dates are written (a BCP 47 tag)
21
+ //
22
+ // The figure carries no words of its own beyond what it is given, so it reads the same
23
+ // in any language: the site's name, a date, "CSV".
24
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
25
+
26
+ class Stop extends Error {}
27
+
28
+ /** The comment at the top of this file, which is its manual. */
29
+ const usage = () => {
30
+ const lines = readFileSync(new URL(import.meta.url), "utf8")
31
+ .split("\n")
32
+ .slice(1);
33
+ const end = lines.findIndex((line) => !line.startsWith("//"));
34
+ return lines
35
+ .slice(0, end)
36
+ .map((line) => line.replace(/^\/\/ ?/, ""))
37
+ .join("\n");
38
+ };
39
+
40
+ function parseArgs(argv) {
41
+ const positional = [];
42
+ const flags = { mark: [] };
43
+ for (let i = 0; i < argv.length; i++) {
44
+ const arg = argv[i];
45
+ if (!arg.startsWith("--")) {
46
+ positional.push(arg);
47
+ continue;
48
+ }
49
+ const key = arg.slice(2);
50
+ const next = argv[i + 1];
51
+ const value =
52
+ next === undefined || next.startsWith("--") ? true : (i++, next);
53
+ if (key === "mark") flags.mark.push(value);
54
+ else flags[key] = value;
55
+ }
56
+ return { positional, flags };
57
+ }
58
+
59
+ // ── CSV ────────────────────────────────────────────────────────────────────
60
+
61
+ function splitLine(line) {
62
+ const cells = [];
63
+ let cur = "";
64
+ let quoted = false;
65
+ for (let i = 0; i < line.length; i++) {
66
+ const ch = line[i];
67
+ if (quoted) {
68
+ if (ch === '"' && line[i + 1] === '"') (cur += '"'), i++;
69
+ else if (ch === '"') quoted = false;
70
+ else cur += ch;
71
+ } else if (ch === '"') quoted = true;
72
+ else if (ch === ",") cells.push(cur), (cur = "");
73
+ else cur += ch;
74
+ }
75
+ cells.push(cur);
76
+ return cells.map((c) => c.trim());
77
+ }
78
+
79
+ function readCsv(path) {
80
+ if (!existsSync(path)) throw new Stop(`No CSV at ${path}.`);
81
+ const text = readFileSync(path, "utf8").replace(/^\uFEFF/, "");
82
+ const meta = {};
83
+ const lines = [];
84
+ for (const line of text.split(/\r?\n/)) {
85
+ if (!line.trim()) continue;
86
+ const m = line.match(/^#\s*([\w-]+):\s*(.*)$/);
87
+ if (m) meta[m[1].toLowerCase()] = m[2];
88
+ else if (!line.startsWith("#")) lines.push(splitLine(line));
89
+ }
90
+ if (lines.length < 2) throw new Stop(`${path} has no rows under its header.`);
91
+ const [header, ...rows] = lines;
92
+ return { meta, header, rows, text };
93
+ }
94
+
95
+ const toNumber = (cell) => {
96
+ if (cell === undefined || cell === "") return null;
97
+ const n = Number(String(cell).replace(/[,\s%$₩€£¥]/g, ""));
98
+ return Number.isFinite(n) ? n : null;
99
+ };
100
+
101
+ /** YYYY, YYYY-MM, YYYY-MM-DD, YYYY-Qn and YYYYQn as a UTC time; null when it is not a date. */
102
+ function toTime(cell) {
103
+ const s = String(cell).trim();
104
+ let m = s.match(/^(\d{4})(?:-(\d{1,2}))?(?:-(\d{1,2}))?(?:[T ].*)?$/);
105
+ if (m) return Date.UTC(+m[1], m[2] ? +m[2] - 1 : 0, m[3] ? +m[3] : 1);
106
+ m = s.match(/^(\d{4})-?Q([1-4])$/i);
107
+ if (m) return Date.UTC(+m[1], (+m[2] - 1) * 3, 1);
108
+ return null;
109
+ }
110
+
111
+ // ── Numbers ────────────────────────────────────────────────────────────────
112
+
113
+ function formatter(locale, flags) {
114
+ const prefix = typeof flags.prefix === "string" ? flags.prefix : "";
115
+ const unit = typeof flags.unit === "string" ? flags.unit : "";
116
+ const wrap = (s) =>
117
+ `${prefix}${s}${unit && !unit.startsWith(" ") && unit.length > 1 ? " " : ""}${unit}`;
118
+ const compact = (v) =>
119
+ new Intl.NumberFormat(locale, {
120
+ notation: Math.abs(v) >= 10_000 ? "compact" : "standard",
121
+ maximumSignificantDigits: 3,
122
+ }).format(v);
123
+ // As published: the decimals the source gave, only grouped
124
+ const exact = (raw) => {
125
+ const v = toNumber(raw);
126
+ const decimals = (String(raw).split(".")[1] ?? "").replace(
127
+ /\D/g,
128
+ "",
129
+ ).length;
130
+ return wrap(
131
+ new Intl.NumberFormat(locale, {
132
+ maximumFractionDigits: Math.min(decimals, 6),
133
+ }).format(v),
134
+ );
135
+ };
136
+ // A label on a point: up to two decimals below a million, compact past it
137
+ const label = (v) =>
138
+ wrap(
139
+ Math.abs(v) >= 1_000_000
140
+ ? compact(v)
141
+ : new Intl.NumberFormat(locale, {
142
+ maximumFractionDigits: Math.abs(v) >= 1 ? 2 : 4,
143
+ }).format(v),
144
+ );
145
+ return { short: (v) => wrap(compact(v)), exact, label };
146
+ }
147
+
148
+ function niceTicks(min, max, count = 5) {
149
+ if (min === max) {
150
+ const pad = Math.abs(min) * 0.1 || 1;
151
+ min -= pad;
152
+ max += pad;
153
+ }
154
+ const raw = (max - min) / count;
155
+ const mag = 10 ** Math.floor(Math.log10(raw));
156
+ const step = [1, 2, 2.5, 5, 10].map((f) => f * mag).find((s) => s >= raw);
157
+ const lo = Math.floor(min / step) * step;
158
+ const hi = Math.ceil(max / step) * step;
159
+ const ticks = [];
160
+ for (let v = lo; v <= hi + step / 2; v += step)
161
+ ticks.push(+v.toPrecision(12));
162
+ return ticks;
163
+ }
164
+
165
+ function timeTicks(t0, t1, locale, most = 7) {
166
+ const DAY = 86_400_000;
167
+ const years = (t1 - t0) / (365.25 * DAY);
168
+ const ticks = [];
169
+ const d0 = new Date(t0);
170
+ if (years >= 3) {
171
+ const every = [1, 2, 5, 10, 20, 50].find((k) => years / k <= most);
172
+ for (
173
+ let y = Math.ceil(d0.getUTCFullYear() / every) * every;
174
+ Date.UTC(y, 0, 1) <= t1;
175
+ y += every
176
+ )
177
+ if (Date.UTC(y, 0, 1) >= t0)
178
+ ticks.push({ t: Date.UTC(y, 0, 1), label: String(y) });
179
+ return ticks;
180
+ }
181
+ const months = years * 12;
182
+ if (months >= 2) {
183
+ const every = [1, 2, 3, 6].find((k) => months / k <= most) ?? 12;
184
+ const month = new Intl.DateTimeFormat(locale, {
185
+ month: "short",
186
+ timeZone: "UTC",
187
+ });
188
+ let y = d0.getUTCFullYear();
189
+ let m = d0.getUTCMonth() + (d0.getUTCDate() > 1 ? 1 : 0);
190
+ m = Math.ceil(m / every) * every;
191
+ for (let first = true; ; m += every) {
192
+ const t = Date.UTC(y, m, 1);
193
+ if (t > t1) break;
194
+ const date = new Date(t);
195
+ const label =
196
+ first || date.getUTCMonth() === 0
197
+ ? `${month.format(date)} ${date.getUTCFullYear()}`
198
+ : month.format(date);
199
+ ticks.push({ t, label });
200
+ first = false;
201
+ }
202
+ return ticks;
203
+ }
204
+ const dayFmt = new Intl.DateTimeFormat(locale, {
205
+ month: "short",
206
+ day: "numeric",
207
+ timeZone: "UTC",
208
+ });
209
+ const days = (t1 - t0) / DAY;
210
+ const every = [1, 2, 7, 14].find((k) => days / k <= most) ?? 14;
211
+ for (
212
+ let t = Date.UTC(d0.getUTCFullYear(), d0.getUTCMonth(), d0.getUTCDate());
213
+ t <= t1;
214
+ t += every * DAY
215
+ )
216
+ ticks.push({ t, label: dayFmt.format(new Date(t)) });
217
+ return ticks;
218
+ }
219
+
220
+ // ── Drawing ────────────────────────────────────────────────────────────────
221
+
222
+ const esc = (s) =>
223
+ String(s).replace(
224
+ /[&<>"']/g,
225
+ (c) =>
226
+ ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[
227
+ c
228
+ ],
229
+ );
230
+ /** A width for text that is only measured, never shown: about 0.58em a character, 1em for CJK. */
231
+ const textWidth = (s, size) =>
232
+ [...String(s)].reduce(
233
+ (w, ch) =>
234
+ w + (/[\u1100-\u11ff\u3000-\u9fff\uac00-\ud7af]/.test(ch) ? 1 : 0.58),
235
+ 0,
236
+ ) * size;
237
+ const FONT = 13;
238
+ // Every chart is drawn twice and CSS shows one: text in an SVG scales with it, so a
239
+ // wide drawing squeezed onto a phone would read at half size
240
+ const SIZES = {
241
+ wide: { W: 640, ticks: 7, endCap: 170, labelChars: 26, nameChars: 24 },
242
+ narrow: { W: 360, ticks: 4, endCap: 124, labelChars: 14, nameChars: 10 },
243
+ };
244
+ const cut = (s, n) =>
245
+ [...String(s)].length > n
246
+ ? `${[...String(s)].slice(0, n - 1).join("")}…`
247
+ : String(s);
248
+
249
+ function lineChart({ xs, labels, series, fmt, marks, indexed, locale, size }) {
250
+ const { W } = size;
251
+ const H = size.W < 500 ? 260 : 320;
252
+ const all = series.flatMap((s) => s.values).filter((v) => v !== null);
253
+ const yTicks = niceTicks(
254
+ Math.min(...all),
255
+ Math.max(...all),
256
+ size.W < 500 ? 4 : 5,
257
+ );
258
+ const [yMin, yMax] = [yTicks[0], yTicks.at(-1)];
259
+ const lastOf = (s) => {
260
+ for (let i = s.values.length - 1; i >= 0; i--)
261
+ if (s.values[i] !== null) return i;
262
+ return -1;
263
+ };
264
+ const ends = series.map((s) => {
265
+ const i = lastOf(s);
266
+ const text = `${series.length > 1 ? `${cut(s.name, size.nameChars)} ` : ""}${fmt.label(s.values[i])}`;
267
+ return { s, i, text };
268
+ });
269
+ const left =
270
+ Math.max(...yTicks.map((v) => textWidth(fmt.short(v), FONT))) + 14;
271
+ const right = Math.min(
272
+ size.endCap,
273
+ Math.max(...ends.map((e) => textWidth(e.text, FONT))) + 16,
274
+ );
275
+ const top = marks.length ? 34 : 14;
276
+ const bottom = 30;
277
+ const [t0, t1] = [xs[0], xs.at(-1)];
278
+ const px = (t) => left + ((t - t0) / (t1 - t0 || 1)) * (W - left - right);
279
+ const py = (v) => top + (1 - (v - yMin) / (yMax - yMin)) * (H - top - bottom);
280
+
281
+ const parts = [];
282
+ for (const v of yTicks) {
283
+ parts.push(
284
+ `<line class="grid${indexed && v === 100 ? " base" : ""}" x1="${left}" x2="${W - right}" y1="${py(v)}" y2="${py(v)}"/>`,
285
+ );
286
+ parts.push(
287
+ `<text class="tick" x="${left - 8}" y="${py(v) + 4}" text-anchor="end">${esc(fmt.short(v))}</text>`,
288
+ );
289
+ }
290
+ for (const { t, label } of timeTicks(t0, t1, locale, size.ticks))
291
+ parts.push(
292
+ `<text class="tick" x="${px(t)}" y="${H - 8}" text-anchor="middle">${esc(label)}</text>`,
293
+ );
294
+ marks.forEach(({ t, text }, k) => {
295
+ if (t < t0 || t > t1) return;
296
+ const x = px(t);
297
+ const anchor = x > left + (W - left - right) * 0.7 ? "end" : "start";
298
+ parts.push(
299
+ `<line class="mark" x1="${x}" x2="${x}" y1="${top - 6}" y2="${H - bottom}"/>`,
300
+ );
301
+ parts.push(
302
+ `<text class="mark-label" x="${x + (anchor === "start" ? 4 : -4)}" y="${k % 2 ? top - 20 : top - 8}" text-anchor="${anchor}">${esc(cut(text, size.labelChars + 6))}</text>`,
303
+ );
304
+ });
305
+ series.forEach((s, n) => {
306
+ let d = "";
307
+ let pen = false;
308
+ // A series published less often than the others (weekly beside daily) is empty on most
309
+ // rows and joins across them; a denser one breaks its line where a value is missing
310
+ const sparse =
311
+ s.values.filter((v) => v !== null).length < s.values.length / 2;
312
+ s.values.forEach((v, i) => {
313
+ if (v === null) return void (pen = pen && sparse);
314
+ d += `${pen ? "L" : "M"}${px(xs[i]).toFixed(1)},${py(v).toFixed(1)}`;
315
+ pen = true;
316
+ });
317
+ parts.push(`<path class="line c${(n % 6) + 1}" d="${d}"/>`);
318
+ });
319
+ // End labels, pushed apart so two lines ending close together stay readable
320
+ const placed = ends
321
+ .filter((e) => e.i >= 0)
322
+ .map((e) => ({ ...e, y: py(e.s.values[e.i]) }))
323
+ .sort((a, b) => a.y - b.y);
324
+ for (let k = 1; k < placed.length; k++)
325
+ placed[k].y = Math.max(placed[k].y, placed[k - 1].y + 16);
326
+ for (const e of placed) {
327
+ const n = series.indexOf(e.s);
328
+ parts.push(
329
+ `<circle class="dot c${(n % 6) + 1}" cx="${px(xs[e.i])}" cy="${py(e.s.values[e.i])}" r="3.5"/>`,
330
+ );
331
+ parts.push(
332
+ `<text class="end c${(n % 6) + 1}" x="${W - right + 8}" y="${e.y + 4}">${esc(e.text)}</text>`,
333
+ );
334
+ }
335
+ const data = {
336
+ x: xs.map((t) => +px(t).toFixed(1)),
337
+ labels,
338
+ series: series.map((s) => ({
339
+ name: s.name,
340
+ values: s.values.map((v) => (v === null ? null : fmt.label(v))),
341
+ y: s.values.map((v) => (v === null ? null : +py(v).toFixed(1))),
342
+ })),
343
+ top,
344
+ bottom: H - bottom,
345
+ width: W,
346
+ };
347
+ return {
348
+ svg: `<svg class="chart-svg ${size.W < 500 ? "narrow" : "wide"}" viewBox="0 0 ${W} ${H}" role="img" data-chart='${esc(JSON.stringify(data))}'>${parts.join("")}<g class="hover"></g></svg>`,
349
+ axis: indexed ? `100 = ${indexed}` : null,
350
+ };
351
+ }
352
+
353
+ function barChart({ labels, series, fmt, highlight, size }) {
354
+ const { W } = size;
355
+ const groups = labels.length;
356
+ const per = series.length;
357
+ const barH = per > 1 ? 14 : 22;
358
+ const gap = per > 1 ? 14 : 10;
359
+ const legend = per > 1 ? 26 : 0;
360
+ const H = legend + groups * (per * barH + gap) + 8;
361
+ const all = series.flatMap((s) => s.values).filter((v) => v !== null);
362
+ const lo = Math.min(0, ...all);
363
+ const hi = Math.max(0, ...all);
364
+ const shownLabel = (l) => cut(l, size.labelChars);
365
+ const left = Math.min(
366
+ size.W < 500 ? 130 : 220,
367
+ Math.max(...labels.map((l) => textWidth(shownLabel(l), FONT))) + 14,
368
+ );
369
+ const valueRoom =
370
+ Math.max(...all.map((v) => textWidth(fmt.label(v), FONT))) + 12;
371
+ const span = W - left - valueRoom - (lo < 0 ? valueRoom : 0);
372
+ const px = (v) =>
373
+ left + (lo < 0 ? valueRoom : 0) + ((v - lo) / (hi - lo || 1)) * span;
374
+ const parts = [];
375
+ if (per > 1)
376
+ series.forEach((s, n) => {
377
+ const x = (size.W < 500 ? 0 : left) + n * (size.W < 500 ? 110 : 130);
378
+ parts.push(
379
+ `<rect class="bar c${(n % 6) + 1}" x="${x}" y="4" width="12" height="12" rx="2"/>`,
380
+ );
381
+ parts.push(
382
+ `<text class="tick" x="${x + 18}" y="14">${esc(cut(s.name, size.nameChars + 2))}</text>`,
383
+ );
384
+ });
385
+ labels.forEach((label, g) => {
386
+ const y0 = legend + g * (per * barH + gap);
387
+ const quiet = highlight && label !== highlight;
388
+ parts.push(
389
+ `<text class="label${quiet ? " quiet-text" : ""}" x="${left - 10}" y="${y0 + (per * barH) / 2 + 4}" text-anchor="end">${esc(shownLabel(label))}<title>${esc(label)}</title></text>`,
390
+ );
391
+ series.forEach((s, n) => {
392
+ const v = s.values[g];
393
+ if (v === null) return;
394
+ const [a, b] = [px(Math.min(0, v)), px(Math.max(0, v))];
395
+ const y = y0 + n * barH;
396
+ // One series: the highlighted bar takes the accent. Several: it keeps the legend's colors
397
+ const color = quiet
398
+ ? "quiet"
399
+ : highlight && per === 1
400
+ ? "c1"
401
+ : `c${(n % 6) + 1}`;
402
+ parts.push(
403
+ `<rect class="bar ${color}" x="${a}" y="${y + 1}" width="${Math.max(1, b - a)}" height="${barH - 2}" rx="3"><title>${esc(label)}${per > 1 ? ` — ${esc(s.name)}` : ""}: ${esc(fmt.label(v))}</title></rect>`,
404
+ );
405
+ const tx = v < 0 ? a - 6 : b + 6;
406
+ parts.push(
407
+ `<text class="value" x="${tx}" y="${y + barH / 2 + 4}" text-anchor="${v < 0 ? "end" : "start"}">${esc(fmt.label(v))}</text>`,
408
+ );
409
+ });
410
+ });
411
+ if (lo < 0)
412
+ parts.push(
413
+ `<line class="zero" x1="${px(0)}" x2="${px(0)}" y1="${legend}" y2="${H - 8}"/>`,
414
+ );
415
+ return {
416
+ svg: `<svg class="chart-svg ${size.W < 500 ? "narrow" : "wide"}" viewBox="0 0 ${W} ${H}" role="img">${parts.join("")}</svg>`,
417
+ axis: null,
418
+ };
419
+ }
420
+
421
+ // ── The page ───────────────────────────────────────────────────────────────
422
+
423
+ const STYLE = `<style id="chart-style">
424
+ .chart{margin:1.5rem 0 2rem}
425
+ .chart-title{margin:0 0 .15rem;font-weight:600}
426
+ .chart-sub{margin:0 0 .6rem;font-size:.85rem;color:var(--muted,#71717a)}
427
+ .chart-svg{display:block;width:100%;height:auto;overflow:visible;border-radius:0;font:${FONT}px ui-sans-serif,system-ui,-apple-system,"Apple SD Gothic Neo","Noto Sans KR",sans-serif;font-variant-numeric:tabular-nums;touch-action:pan-y}
428
+ .chart-svg .grid{stroke:var(--line,#e4e4e7);stroke-width:1}
429
+ .chart-svg .grid.base,.chart-svg .zero{stroke:var(--muted,#71717a);stroke-dasharray:3 3}
430
+ .chart-svg .tick,.chart-svg .mark-label{fill:var(--muted,#71717a)}
431
+ .chart-svg .label,.chart-svg .value{fill:var(--fg,#18181b)}
432
+ .chart-svg .quiet-text{fill:var(--muted,#71717a)}
433
+ .chart-svg .mark{stroke:var(--muted,#71717a);stroke-dasharray:2 3}
434
+ .chart-svg .mark-label{font-size:11px}
435
+ .chart-svg .line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}
436
+ .chart-svg .end{font-weight:600}
437
+ .chart-svg .c1{--c:#2563eb}.chart-svg .c2{--c:#d97706}.chart-svg .c3{--c:#059669}.chart-svg .c4{--c:#db2777}.chart-svg .c5{--c:#7c3aed}.chart-svg .c6{--c:#64748b}
438
+ .chart-svg .quiet{--c:color-mix(in srgb,var(--muted,#71717a) 45%,transparent)}
439
+ .chart-svg .line{stroke:var(--c)}.chart-svg .dot,.chart-svg .bar{fill:var(--c)}.chart-svg .end{fill:var(--c)}
440
+ .chart-svg .hover line{stroke:var(--muted,#71717a)}
441
+ .chart-svg .hover rect{fill:var(--bg,#fff);stroke:var(--line,#e4e4e7)}
442
+ .chart-svg .hover text{fill:var(--fg,#18181b);font-size:12px}
443
+ .chart-svg.narrow{display:none}
444
+ @media (max-width:560px){.chart-svg.wide{display:none}.chart-svg.narrow{display:block}}
445
+ .chart .source{margin:.4rem 0 .6rem;font-size:.85rem;color:var(--muted,#71717a)}
446
+ .chart details{margin:0;font-size:.9rem}
447
+ @media (prefers-color-scheme:dark){.chart-svg .c1{--c:#60a5fa}.chart-svg .c2{--c:#fbbf24}.chart-svg .c3{--c:#34d399}.chart-svg .c4{--c:#f472b6}.chart-svg .c5{--c:#a78bfa}.chart-svg .c6{--c:#94a3b8}}
448
+ </style>`;
449
+
450
+ // Reads every line chart's own data off its svg: a readout follows the pointer
451
+ const SCRIPT = `<script id="chart-script">
452
+ document.querySelectorAll("svg[data-chart]").forEach(function(svg){
453
+ var d=JSON.parse(svg.getAttribute("data-chart")),g=svg.querySelector(".hover"),NS="http://www.w3.org/2000/svg";
454
+ function el(n,a){var e=document.createElementNS(NS,n);for(var k in a)e.setAttribute(k,a[k]);g.appendChild(e);return e}
455
+ function show(ev){
456
+ var p=svg.createSVGPoint();p.x=ev.clientX;p.y=ev.clientY;var x=p.matrixTransform(svg.getScreenCTM().inverse()).x,i=0;
457
+ for(var k=1;k<d.x.length;k++)if(Math.abs(d.x[k]-x)<Math.abs(d.x[i]-x))i=k;
458
+ g.innerHTML="";el("line",{x1:d.x[i],x2:d.x[i],y1:d.top,y2:d.bottom});
459
+ var lines=[d.labels[i]].concat(d.series.filter(function(s){return s.values[i]!==null}).map(function(s){return(d.series.length>1?s.name+": ":"")+s.values[i]}));
460
+ d.series.forEach(function(s,n){if(s.y[i]!==null)el("circle",{cx:d.x[i],cy:s.y[i],r:4,"class":"dot c"+(n%6+1)})});
461
+ var w=Math.max.apply(null,lines.map(function(l){return l.length}))*7+16,h=lines.length*16+10,bx=d.x[i]+10+w>d.width?d.x[i]-10-w:d.x[i]+10;
462
+ el("rect",{x:bx,y:d.top,width:w,height:h,rx:6});
463
+ lines.forEach(function(l,n){var t=el("text",{x:bx+8,y:d.top+18+n*16});t.textContent=l;if(!n)t.setAttribute("font-weight","600")});
464
+ }
465
+ svg.addEventListener("pointermove",show);svg.addEventListener("pointerleave",function(){g.innerHTML=""});
466
+ });
467
+ </script>`;
468
+
469
+ function both(draw) {
470
+ const wide = draw(SIZES.wide);
471
+ return { svg: `${wide.svg}\n${draw(SIZES.narrow).svg}`, axis: wide.axis };
472
+ }
473
+
474
+ function sourceLinks(text) {
475
+ const hosts = new Set();
476
+ return String(text)
477
+ .split(/\s+/)
478
+ .filter(Boolean)
479
+ .map((part) => {
480
+ if (!/^https?:\/\//.test(part)) return esc(part);
481
+ const host = new URL(part).hostname.replace(/^www\./, "");
482
+ // One link a site: the CSV keeps every address
483
+ if (hosts.has(host)) return "";
484
+ hosts.add(host);
485
+ return `<a href="${esc(part)}">${esc(host)}</a>`;
486
+ })
487
+ .filter(Boolean)
488
+ .join(" ")
489
+ .replace(/(<\/a>) (<a )/g, "$1, $2");
490
+ }
491
+
492
+ function main() {
493
+ const { positional, flags } = parseArgs(process.argv.slice(2));
494
+ const [pagePath, id, csvPath] = positional;
495
+ if (!pagePath || !id || !csvPath) throw new Stop(usage());
496
+ if (!/^[\w-]+$/.test(id))
497
+ throw new Stop(`"${id}" is not an id: letters, numbers, - and _ only.`);
498
+ if (!existsSync(pagePath))
499
+ throw new Stop(
500
+ `No page at ${pagePath}. Start one first: \`page.mjs quick\` beside this script, or any HTML file.`,
501
+ );
502
+ const locale = typeof flags.locale === "string" ? flags.locale : "en";
503
+ const fmt = formatter(locale, flags);
504
+
505
+ const { meta, header, rows, text } = readCsv(csvPath);
506
+ const wanted =
507
+ typeof flags.columns === "string"
508
+ ? flags.columns.split(",").map((c) => c.trim())
509
+ : header.slice(1);
510
+ const cols = wanted.map((name) => {
511
+ const at = header.indexOf(name);
512
+ if (at < 1)
513
+ throw new Stop(
514
+ `No column "${name}" in ${csvPath}; it has ${header.slice(1).join(", ")}.`,
515
+ );
516
+ return at;
517
+ });
518
+ const dated = rows.every((r) => toTime(r[0]) !== null);
519
+ const kind = flags.kind ?? (dated ? "line" : "bar");
520
+ if (kind === "line" && !dated)
521
+ throw new Stop(
522
+ "A line needs dates in the first column (YYYY, YYYY-MM, YYYY-MM-DD or YYYY-Qn); draw categories with --kind bar.",
523
+ );
524
+
525
+ let body = rows;
526
+ if (dated) {
527
+ const lo = flags.from ? toTime(flags.from) : null;
528
+ const hi = flags.to ? toTime(flags.to) : null;
529
+ body = rows
530
+ .map((r) => ({ r, t: toTime(r[0]) }))
531
+ .filter(({ t }) => (lo === null || t >= lo) && (hi === null || t <= hi))
532
+ .sort((a, b) => a.t - b.t)
533
+ .map(({ r }) => r);
534
+ }
535
+ if (!body.length) throw new Stop("No rows left to draw in that range.");
536
+ let series = cols.map((c) => ({
537
+ name: header[c],
538
+ values: body.map((r) => toNumber(r[c])),
539
+ }));
540
+ if (series.every((s) => s.values.every((v) => v === null)))
541
+ throw new Stop(`No numbers in ${wanted.join(", ")}.`);
542
+
543
+ let chart;
544
+ if (kind === "line") {
545
+ let indexed = null;
546
+ if (flags.index) {
547
+ const start = body.findIndex((_, i) =>
548
+ series.every((s) => s.values[i] !== null && s.values[i] !== 0),
549
+ );
550
+ if (start < 0)
551
+ throw new Stop(
552
+ "--index needs one date where every column has a value.",
553
+ );
554
+ indexed = body[start][0];
555
+ series = series.map((s) => ({
556
+ ...s,
557
+ values: s.values.map((v, i) =>
558
+ v === null || i < start ? null : (v / s.values[start]) * 100,
559
+ ),
560
+ }));
561
+ }
562
+ const marks = flags.mark.map((m) => {
563
+ const [date, ...label] = String(m).split("=");
564
+ const t = toTime(date);
565
+ if (t === null)
566
+ throw new Stop(
567
+ `--mark "${m}" needs a date first: --mark "2025-03=Rate cut".`,
568
+ );
569
+ return { t, text: label.join("=") || date };
570
+ });
571
+ const draw = (size) =>
572
+ lineChart({
573
+ xs: body.map((r) => toTime(r[0])),
574
+ labels: body.map((r) => r[0]),
575
+ series,
576
+ fmt: indexed ? formatter(locale, {}) : fmt,
577
+ marks,
578
+ indexed,
579
+ locale,
580
+ size,
581
+ });
582
+ chart = both(draw);
583
+ } else {
584
+ let order = body.map((_, i) => i);
585
+ if (!flags["keep-order"])
586
+ order.sort(
587
+ (a, b) =>
588
+ (series[0].values[b] ?? -Infinity) -
589
+ (series[0].values[a] ?? -Infinity),
590
+ );
591
+ chart = both((size) =>
592
+ barChart({
593
+ labels: order.map((i) => body[i][0]),
594
+ series: series.map((s) => ({
595
+ ...s,
596
+ values: order.map((i) => s.values[i]),
597
+ })),
598
+ fmt,
599
+ highlight: typeof flags.highlight === "string" ? flags.highlight : null,
600
+ size,
601
+ }),
602
+ );
603
+ }
604
+
605
+ const title =
606
+ typeof flags.title === "string"
607
+ ? flags.title
608
+ : (meta.title ?? header.slice(1).join(", "));
609
+ const sub = [
610
+ chart.axis,
611
+ meta.unit && !flags.unit && !flags.prefix && !chart.axis ? meta.unit : null,
612
+ ]
613
+ .filter(Boolean)
614
+ .join(" ");
615
+ const source = typeof flags.source === "string" ? flags.source : meta.source;
616
+ // A note given here replaces the CSV's own, so it can be said in the page's language
617
+ const note = typeof flags.note === "string" ? flags.note : meta.note;
618
+ const said = [
619
+ source
620
+ ? `${sourceLinks(source)}${meta.fetched ? ` (${esc(meta.fetched)})` : ""}`
621
+ : "",
622
+ note ? esc(note) : "",
623
+ ].filter(Boolean);
624
+ const sourceLine = said.length
625
+ ? `<p class="source">${said.join(". ")}</p>`
626
+ : "";
627
+ const table = `<table><thead><tr>${[header[0], ...cols.map((c) => header[c])].map((h, i) => `<th${i ? ' class="num"' : ""}>${esc(h)}</th>`).join("")}</tr></thead><tbody>${body
628
+ .map(
629
+ (r) =>
630
+ `<tr><td>${esc(r[0])}</td>${cols.map((c) => `<td class="num">${toNumber(r[c]) === null ? "—" : esc(fmt.exact(r[c]))}</td>`).join("")}</tr>`,
631
+ )
632
+ .join("")}</tbody></table>`;
633
+ const download = `data:text/csv;charset=utf-8,${encodeURIComponent(text)}`;
634
+ const figure = `<figure id="${id}" class="chart">
635
+ <p class="chart-title">${esc(title)}</p>${sub ? `\n<p class="chart-sub">${esc(sub)}</p>` : ""}
636
+ ${chart.svg}
637
+ ${sourceLine}
638
+ <details><summary>CSV (${body.length})</summary><div class="scroll">${table}</div><p><a download="${esc(id)}.csv" href="${download}">${esc(id)}.csv</a></p></details>
639
+ </figure>`;
640
+
641
+ let html = readFileSync(pagePath, "utf8");
642
+ const existing = new RegExp(
643
+ `<(figure|div|section|p)\\b[^>]*\\bid=["']${id}["'][^>]*>[\\s\\S]*?</\\1>`,
644
+ );
645
+ const selfClosed = new RegExp(
646
+ `<(figure|div|section|p)\\b[^>]*\\bid=["']${id}["'][^>]*/>`,
647
+ );
648
+ if (existing.test(html)) html = html.replace(existing, () => figure);
649
+ else if (selfClosed.test(html)) html = html.replace(selfClosed, () => figure);
650
+ else if (html.includes("</body>"))
651
+ html = html.replace("</body>", () => `${figure}\n</body>`);
652
+ else html += `\n${figure}\n`;
653
+
654
+ html = html.replace(/<style id="chart-style">[\s\S]*?<\/style>\s*/, "");
655
+ html = html.replace(/<script id="chart-script">[\s\S]*?<\/script>\s*/, "");
656
+ html = html.includes("</head>")
657
+ ? html.replace("</head>", () => `${STYLE}\n</head>`)
658
+ : STYLE + html;
659
+ html = html.includes("</body>")
660
+ ? html.replace("</body>", () => `${SCRIPT}\n</body>`)
661
+ : html + SCRIPT;
662
+ writeFileSync(pagePath, html);
663
+
664
+ const drawn = series.map((s) => {
665
+ const vals = s.values.filter((v) => v !== null);
666
+ return `${s.name} ${fmt.label(vals[0])} → ${fmt.label(vals.at(-1))}`;
667
+ });
668
+ console.log(
669
+ `Drew "${title}" (${kind}, ${body.length} rows) into ${pagePath} as #${id}. ${kind === "line" ? `First → last: ${drawn.join("; ")}.` : ""}`.trim(),
670
+ );
671
+ }
672
+
673
+ try {
674
+ main();
675
+ } catch (error) {
676
+ if (!(error instanceof Stop)) throw error;
677
+ console.error(error.message);
678
+ process.exitCode = 1;
679
+ }
@@ -5,6 +5,7 @@
5
5
  // node page.mjs new <name> start a page in the kit
6
6
  // node page.mjs build <name> bundle it into one offline HTML file in the artifacts folder
7
7
  // node page.mjs add <package>... add a library the kit lacks; every page can use it after
8
+ // node page.mjs quick <name> one hand-written HTML file, styled, no kit
8
9
  import { spawnSync } from "node:child_process";
9
10
  import { createHash } from "node:crypto";
10
11
  import {
@@ -1,3 +0,0 @@
1
- {
2
- "/api/probe/route": "app/api/probe/route.js"
3
- }