thursday-agent 0.8.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.
- package/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/keep-working/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js +1 -1
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/probe/route.js.nft.json +1 -1
- package/.next/server/app/api/reach/route.js +11 -11
- package/.next/server/app/api/reach/route.js.nft.json +1 -1
- package/.next/server/app/api/routine/route.js +6 -6
- package/.next/server/app/api/routine/route.js.nft.json +1 -1
- package/.next/server/app/api/signins/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/text/route.js +11 -11
- package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +6 -6
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +56 -56
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__012s2o8._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__04r4h-2._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__0nmhgeo._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1fy2g82._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1ndi14y._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1nmflqk._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1v7-zke._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1wnzmx9._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__1xv-5cq._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
- package/.next/server/chunks/_05v8k21._.js +1 -1
- package/.next/server/chunks/_0awtznl._.js +4 -4
- package/.next/server/chunks/_0k6qjcf._.js +1 -1
- package/.next/server/chunks/_0na2fj1._.js +5 -5
- package/.next/server/chunks/_0x-ej3f._.js +21 -21
- package/.next/server/chunks/_1dc_d1q._.js +22 -20
- package/.next/server/chunks/_1jc5g4p._.js +2 -2
- package/.next/server/chunks/_1jn-aep._.js +50 -48
- package/.next/server/chunks/{_1u8ygbz._.js → _1m88lkh._.js} +7 -7
- package/.next/server/chunks/_1uuucy-._.js +4 -4
- package/.next/server/chunks/_1v9r2ke._.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__1h4bnrf._.js → [root-of-the-server]__0cxqzfc._.js} +2 -2
- package/.next/server/chunks/ssr/{[root-of-the-server]__1kyi568._.js → [root-of-the-server]__0tvavy9._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1-d2tgh._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__1se25x0._.js +1 -1
- package/.next/server/chunks/ssr/{_1i3ddn7._.js → _05pl_u8._.js} +1 -1
- package/.next/server/chunks/ssr/_05rxyf9._.js +3 -0
- package/.next/server/chunks/ssr/_070nv1_._.js +3 -0
- package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
- package/.next/server/chunks/ssr/{_10y6r1n._.js → _0bp-_fi._.js} +3 -3
- package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
- package/.next/server/chunks/ssr/_0hlxij-._.js +3 -0
- package/.next/server/chunks/ssr/_0ld9_qq._.js +1 -1
- package/.next/server/chunks/ssr/{_0z8jpch._.js → _0p9pmxe._.js} +2 -2
- package/.next/server/chunks/ssr/{_1y-ea28._.js → _0s41irp._.js} +2 -2
- package/.next/server/chunks/ssr/_0tg0vf8._.js +164 -0
- package/.next/server/chunks/ssr/_0u5xv6g._.js +3 -0
- package/.next/server/chunks/ssr/{_1_rqu8x._.js → _12mijol._.js} +7 -7
- package/.next/server/chunks/ssr/_15zqt-t._.js +1 -1
- package/.next/server/chunks/ssr/{_1oijqjw._.js → _173siic._.js} +2 -2
- package/.next/server/chunks/ssr/_17v-33v._.js +20 -0
- package/.next/server/chunks/ssr/_184327j._.js +1 -1
- package/.next/server/chunks/ssr/{_1nod5hh._.js → _19n08g5._.js} +2 -2
- package/.next/server/chunks/ssr/_1alxym6._.js +3 -0
- package/.next/server/chunks/ssr/{_0hzz82u._.js → _1d423aj._.js} +25 -23
- package/.next/server/chunks/ssr/{_0ewory0._.js → _1doidrw._.js} +2 -2
- package/.next/server/chunks/ssr/{_0pm-2p-._.js → _1flsjeo._.js} +2 -2
- package/.next/server/chunks/ssr/_1mqn9mc._.js +1 -1
- package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
- package/.next/server/chunks/ssr/_1ng831c._.js +3 -0
- package/.next/server/chunks/ssr/{_19g570d._.js → _1u2jw0i._.js} +3 -3
- package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
- package/.next/server/chunks/ssr/{_1u1mxu9._.js → _1yk6yak._.js} +2 -2
- package/.next/server/chunks/ssr/features_bot_1s_-2vj._.js +1 -1
- package/.next/server/chunks/ssr/features_bot_1yr-_pm._.js +3 -0
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
- package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_1_8mv4v._.js → node_modules_048wmmj._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1p9rdsy._.js → node_modules_04t200t._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_03ptrl9._.js → node_modules_0511ha-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_07ih6wb._.js → node_modules_08g12f-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_01f8_se._.js → node_modules_0cdu6jq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_111sh9o._.js → node_modules_0eb4rx7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_10gbm2y._.js → node_modules_0f2k5l0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1ix-g49._.js → node_modules_0piws2a._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0t-44lj._.js → node_modules_0pk6i-e._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0lg75qx._.js → node_modules_0s1lwc9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1v9tc1c._.js → node_modules_0wq4-ts._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1m3gb16._.js → node_modules_0zjqwz7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0b8df3s._.js → node_modules_11hbkuj._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_00jv9_3._.js → node_modules_18woe20._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1x825hz._.js → node_modules_1_afpz7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0woscox._.js → node_modules_1_ban4u._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0gt-uw9._.js → node_modules_1cxc8kn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_059vtf2._.js → node_modules_1dcbvfw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1lsyy7k._.js → node_modules_1expacw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1-7qawg._.js → node_modules_1ksh9gy._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_06w645b._.js → node_modules_1m2ho33._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0sz5yg1._.js → node_modules_1n0h9sf._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1teyzfa._.js → node_modules_1qehmwm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yv-re6._.js → node_modules_1xzo2wn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_20xmwtu._.js → node_modules_1zf46lp._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ii1l89._.js → node_modules_1zkpek5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@base-ui_react_0jne69c._.js → node_modules_@base-ui_react_0buz1-j._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_00q1oa3._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0avl8-1._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_1-2_5z7._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_14oslro._.js} +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +57 -57
- package/.next/static/chunks/03kgfyrli_0gz.js +1 -0
- package/.next/static/chunks/{1e4yrzs5evq_r.js → 09120tms63y21.js} +1 -1
- package/.next/static/chunks/{2zhfdbq50ee5i.js → 0o980477kfqez.js} +1 -1
- package/.next/static/chunks/{1ocy4w0v20a3v.js → 0oz3rs47yy3p8.js} +2 -2
- package/.next/static/chunks/{2be08eqxsfc1y.js → 0w20wnnfrscq7.js} +1 -1
- package/.next/static/chunks/{09c13vnqy7al3.js → 19ojn4lomsk0r.js} +2 -2
- package/.next/static/chunks/{2om4lkg86feb4.js → 1mwee7xvitlp9.js} +1 -1
- package/.next/static/chunks/1p764d75_0lhv.js +18 -0
- package/.next/static/chunks/{0oz7a68qanm99.js → 1pjygp7hqib6j.js} +1 -1
- package/.next/static/chunks/22shnca_6zfzs.js +1 -0
- package/.next/static/chunks/{34s52v8f841zo.js → 24dst4mgurbsh.js} +1 -1
- package/.next/static/chunks/25zq8gzhbdsr7.js +1 -0
- package/.next/static/chunks/2d4z3er53ng8r.js +1 -0
- package/.next/static/chunks/{2d3yhjtafgmtw.js → 2ky81z-jgvasc.js} +1 -1
- package/.next/static/chunks/2m6jd3586ijb-.js +1 -0
- package/.next/static/chunks/{0k5tkt_qi52kb.js → 2ndw4w4u2x51r.js} +1 -1
- package/.next/static/chunks/2qod5tqlpz8b7.js +17 -0
- package/.next/static/chunks/{21gf2t_whhurc.js → 30skcjexa57zn.js} +2 -2
- package/.next/static/chunks/{2upc9mkcc3nlq.js → 35kwn237o4wdn.js} +1 -1
- package/.next/static/chunks/{2u9yjkpkoij75.js → 3exjsi8pfdvzj.js} +1 -1
- package/.next/static/chunks/3obf9n3cxj99k.js +1 -0
- package/.next/static/chunks/3qq4o8w2rv_e9.css +1 -0
- package/.next/static/chunks/{0g1p98s2pfx2w.js → 3vynnr71ghvom.js} +2 -2
- package/.next/static/chunks/{44kjmi5h_p8px.js → 42890jqk049q1.js} +1 -1
- package/README.md +35 -14
- package/bin/port.mjs +11 -7
- package/bin/thursday.mjs +1 -1
- package/components/ui/radio-group.tsx +1 -1
- package/components/ui/shiny-text.tsx +3 -22
- package/docs/how-it-works.md +11 -3
- package/docs/images/first-run.png +0 -0
- package/docs/images/keeps-going.png +0 -0
- package/docs/images/two-minds.png +0 -0
- package/features/ai/components/model-browser.tsx +1 -1
- package/features/ai/components/model-picker.tsx +1 -1
- package/features/ai/guide.ts +9 -4
- package/features/ai/prompts/bot.prompt.ts +4 -1
- package/features/ai/tools/routine.tool.ts +28 -16
- package/features/bot/bot.runner.ts +3 -1
- package/features/bot/components/bot-mark.tsx +6 -1
- package/features/bot/components/bot-setting.tsx +4 -6
- package/features/bot/components/bot-tool.tsx +2 -0
- package/features/bot/components/room-list.tsx +12 -3
- package/features/bot/components/room-pill.tsx +26 -19
- package/features/config/components/config-setting.tsx +17 -5
- package/features/config/components/voice-key.tsx +38 -18
- package/features/connectors/mcp.const.ts +90 -59
- package/features/intro/components/intro.tsx +50 -27
- package/features/routine/components/routine-mark.tsx +4 -6
- package/features/routine/components/routine-setting.tsx +332 -143
- package/features/routine/routine.query.ts +24 -3
- package/features/routine/routine.schema.ts +55 -4
- package/features/settings/components/install-app.tsx +1 -4
- package/features/settings/components/setting-ui.tsx +1 -1
- package/features/skills/skills.discover.ts +2 -1
- package/features/skills/skills.query.ts +5 -1
- package/features/skills/skills.schema.ts +2 -0
- package/features/thursday/components/ascii-orb.tsx +39 -10
- package/features/thursday/components/connect-wave.tsx +284 -0
- package/features/thursday/components/side-captions.tsx +29 -17
- package/features/thursday/components/thursday-setting.tsx +3 -3
- package/features/thursday/components/thursday.tsx +130 -58
- package/features/thursday/components/voice-picker.tsx +1 -1
- package/features/thursday/components/write-line.tsx +2 -2
- package/features/thursday/open-work.ts +11 -3
- package/features/thursday/tool-line.ts +94 -12
- package/features/thursday/use-text-call.ts +3 -2
- package/features/thursday/use-thursday.ts +5 -2
- package/features/workspace/components/artifact-view.tsx +129 -7
- package/guide/bots.md +17 -5
- package/guide/calls.md +15 -7
- package/guide/setup.md +10 -2
- package/knip.json +1 -0
- package/lib/desktop-notify.ts +3 -2
- package/package.json +1 -1
- package/scripts/bot-context.test.mts +1 -1
- package/seed-skills/analyst/README.md +19 -0
- package/seed-skills/analyst/data-report/SKILL.md +59 -0
- package/seed-skills/analyst/data-report/references/brief.md +31 -0
- package/seed-skills/analyst/data-report/references/company.md +41 -0
- package/seed-skills/analyst/data-report/references/comparison.md +42 -0
- package/seed-skills/analyst/data-report/references/decision.md +28 -0
- package/seed-skills/analyst/data-report/references/market-size.md +49 -0
- package/seed-skills/analyst/data-report/references/price-check.md +33 -0
- package/seed-skills/analyst/data-report/references/sources.md +51 -0
- package/seed-skills/analyst/data-report/references/trend.md +39 -0
- package/seed-skills/analyst/data-report/scripts/chart.mjs +679 -0
- package/seed-skills/analyst/data-report/scripts/fetch.mjs +521 -0
- package/seed-skills/insta/instagram/SKILL.md +33 -0
- package/seed-skills/insta/instagram/references/dm.md +49 -0
- package/seed-skills/insta/instagram/references/post.md +74 -0
- package/seed-skills/insta/instagram/scripts/compose.mjs +329 -0
- package/seed-skills/insta/instagram/scripts/image-size.mjs +30 -0
- package/seed-skills/insta/instagram/scripts/inbox.mjs +159 -0
- package/seed-skills/insta/instagram/scripts/lib.mjs +91 -0
- package/seed-skills/insta/instagram/scripts/media.mjs +104 -0
- package/seed-skills/insta/instagram/scripts/profile.mjs +139 -0
- package/seed-skills/insta/instagram/scripts/read.mjs +240 -0
- package/seed-skills/insta/instagram/scripts/render.mjs +126 -0
- package/seed-skills/insta/instagram/scripts/search.mjs +133 -0
- package/seed-skills/insta/instagram/scripts/selectors.mjs +56 -0
- package/seed-skills/insta/instagram/scripts/sheet.mjs +100 -0
- package/seed-skills/insta/instagram/scripts/webimage.mjs +105 -0
- package/seed-skills/insta/instagram/templates/carousel.html +62 -0
- package/seed-skills/mail/mail/SKILL.md +37 -0
- package/seed-skills/mail/mail/recipes/gmail.mjs +448 -0
- package/seed-skills/mail/mail/references/recipes.md +55 -0
- package/seed-skills/mail/mail/scripts/mail.mjs +202 -0
- package/seed-skills/marketer/README.md +6 -4
- package/seed-skills/marketer/copywriting/SKILL.md +1 -1
- package/seed-skills/marketer/emails/SKILL.md +1 -1
- package/seed-skills/marketer/launch/SKILL.md +1 -1
- package/seed-skills/marketer/product-marketing/SKILL.md +5 -5
- package/seed-skills/marketer/seo-audit/SKILL.md +1 -1
- package/seed-skills/marketer/social/SKILL.md +1 -1
- package/skills/computer/SKILL.md +1 -0
- package/skills/interactive-page/SKILL.md +4 -0
- package/.next/server/chunks/[root-of-the-server]__0n-l_n_._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0wvo2of._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1e6aut3._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__1ziojdq._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__1srp8l5._.js +0 -3
- package/.next/server/chunks/ssr/_0mes4s0._.js +0 -3
- package/.next/server/chunks/ssr/_0ya0cb0._.js +0 -3
- package/.next/server/chunks/ssr/_0yto5s7._.js +0 -20
- package/.next/server/chunks/ssr/_10dw2nv._.js +0 -3
- package/.next/server/chunks/ssr/_1p0gxeo._.js +0 -164
- package/.next/server/chunks/ssr/_1r41yvh._.js +0 -3
- package/.next/server/chunks/ssr/_1r79ksv._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1ogjt52._.js +0 -3
- package/.next/server/chunks/ssr/node_modules_@base-ui_react_0b3d4m9._.js +0 -3
- package/.next/static/chunks/03omwz_vi2ndg.js +0 -1
- package/.next/static/chunks/0_1bqgd6wt_og.js +0 -1
- package/.next/static/chunks/0abgkzikaznxc.js +0 -1
- package/.next/static/chunks/0bddkpash59-o.js +0 -17
- package/.next/static/chunks/0c2-bzhy3a0ib.js +0 -1
- package/.next/static/chunks/0iilfb1ocevnx.js +0 -1
- package/.next/static/chunks/0imbhx-j830a_.css +0 -1
- package/.next/static/chunks/24h1y9dhy3m_2.js +0 -1
- package/.next/static/chunks/2f_5saocsko_b.js +0 -1
- package/.next/static/chunks/2lwbr5vx0ixaj.js +0 -18
- /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → Ar5V4kMdL4CE1trx8anrH}/_buildManifest.js +0 -0
- /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → Ar5V4kMdL4CE1trx8anrH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{UiOMPYpQ4fqGyu5PzS-7j → Ar5V4kMdL4CE1trx8anrH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* What Instagram's own search shows for a topic — posts and reels, with how each
|
|
4
|
+
* did — and the accounts behind them, read from the data the search page fetches.
|
|
5
|
+
* One word or a #tag finds more than a phrase.
|
|
6
|
+
*
|
|
7
|
+
* node search.mjs "<topic>" [--type reel|carousel|image] [--max 24] [--out found.json] [--sheet covers.png]
|
|
8
|
+
*/
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { fail, inPage, orFail, parseArgs } from "./lib.mjs";
|
|
11
|
+
import { media, postLine, short } from "./media.mjs";
|
|
12
|
+
import { makeSheet } from "./sheet.mjs";
|
|
13
|
+
|
|
14
|
+
const opts = parseArgs();
|
|
15
|
+
const topic = opts._[0];
|
|
16
|
+
if (!topic)
|
|
17
|
+
fail(
|
|
18
|
+
'usage: node search.mjs "<topic>" [--type reel|carousel|image] [--max 24] [--out found.json] [--sheet covers.png]',
|
|
19
|
+
);
|
|
20
|
+
const max = Number(opts.max ?? 24);
|
|
21
|
+
const type = opts.type ?? null;
|
|
22
|
+
if (type && !["reel", "carousel", "image"].includes(type))
|
|
23
|
+
fail("--type is reel, carousel or image");
|
|
24
|
+
|
|
25
|
+
const started = Date.now();
|
|
26
|
+
const got = orFail(
|
|
27
|
+
await inPage(
|
|
28
|
+
async (page, { topic, max, type }, { media }) => {
|
|
29
|
+
const found = [];
|
|
30
|
+
let more = true;
|
|
31
|
+
let pages = 0;
|
|
32
|
+
const listen = async (res) => {
|
|
33
|
+
if (!/\/graphql/.test(res.url())) return;
|
|
34
|
+
let text;
|
|
35
|
+
try {
|
|
36
|
+
text = await res.text();
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!text.includes("xdt_fbsearch__top_serp_graphql")) return;
|
|
41
|
+
try {
|
|
42
|
+
const c = JSON.parse(text.split("\n")[0]).data
|
|
43
|
+
?.xdt_fbsearch__top_serp_graphql;
|
|
44
|
+
pages++;
|
|
45
|
+
more = Boolean(c?.page_info?.has_next_page);
|
|
46
|
+
const walk = (o) => {
|
|
47
|
+
if (!o || typeof o !== "object") return;
|
|
48
|
+
if (typeof o.code === "string" && "media_type" in o) {
|
|
49
|
+
found.push(media(o));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const v of Object.values(o)) walk(v);
|
|
53
|
+
};
|
|
54
|
+
walk(c?.edges);
|
|
55
|
+
} catch {}
|
|
56
|
+
};
|
|
57
|
+
const kept = () => found.filter((p) => !type || p.type === type).length;
|
|
58
|
+
page.on("response", listen);
|
|
59
|
+
try {
|
|
60
|
+
await page.goto(
|
|
61
|
+
`https://www.instagram.com/explore/search/keyword/?q=${encodeURIComponent(topic)}`,
|
|
62
|
+
{ waitUntil: "domcontentloaded" },
|
|
63
|
+
);
|
|
64
|
+
for (let i = 0; i < 60 && !pages; i++) await page.waitForTimeout(250);
|
|
65
|
+
for (let tries = 0; kept() < max && more && tries < 8; tries++) {
|
|
66
|
+
const had = pages;
|
|
67
|
+
await page.evaluate(() =>
|
|
68
|
+
window.scrollTo(0, document.body.scrollHeight),
|
|
69
|
+
);
|
|
70
|
+
for (let i = 0; i < 32 && pages === had; i++)
|
|
71
|
+
await page.waitForTimeout(250);
|
|
72
|
+
if (pages === had) break;
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
page.off("response", listen);
|
|
76
|
+
}
|
|
77
|
+
const path = page.url().replace(/^https?:\/\/[^/]+/, "");
|
|
78
|
+
if (/\/accounts\/login|\/challenge\//.test(path))
|
|
79
|
+
return {
|
|
80
|
+
error: `Not signed in (${path}). Borrow the kept sign-in first.`,
|
|
81
|
+
};
|
|
82
|
+
if (!pages)
|
|
83
|
+
return {
|
|
84
|
+
error:
|
|
85
|
+
"The search page fetched nothing this script knows (xdt_fbsearch__top_serp_graphql): the page changed, or something covers it. Open it and look.",
|
|
86
|
+
};
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
return {
|
|
89
|
+
posts: found
|
|
90
|
+
.filter((p) => !seen.has(p.url) && seen.add(p.url))
|
|
91
|
+
.filter((p) => !type || p.type === type)
|
|
92
|
+
.slice(0, max),
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
{ topic, max, type },
|
|
96
|
+
{ media },
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const { posts } = got;
|
|
101
|
+
if (opts.out)
|
|
102
|
+
writeFileSync(opts.out, JSON.stringify({ topic, posts }, null, 1));
|
|
103
|
+
posts.forEach((p, i) => {
|
|
104
|
+
console.log(postLine(p, i));
|
|
105
|
+
});
|
|
106
|
+
// The accounts behind the results, by the likes they drew here
|
|
107
|
+
const accounts = Object.values(
|
|
108
|
+
posts.reduce((all, p) => {
|
|
109
|
+
const a = (all[p.owner] ??= { owner: p.owner, posts: 0, likes: 0 });
|
|
110
|
+
a.posts++;
|
|
111
|
+
a.likes += p.likes ?? 0;
|
|
112
|
+
return all;
|
|
113
|
+
}, {}),
|
|
114
|
+
).sort((a, b) => b.likes - a.likes);
|
|
115
|
+
console.log(
|
|
116
|
+
`accounts: ${accounts
|
|
117
|
+
.slice(0, 10)
|
|
118
|
+
.map((a) => `@${a.owner} (${a.posts}, likes ${short(a.likes)})`)
|
|
119
|
+
.join(", ")}`,
|
|
120
|
+
);
|
|
121
|
+
if (opts.sheet) {
|
|
122
|
+
const made = await makeSheet(
|
|
123
|
+
posts.map((p, i) => ({
|
|
124
|
+
src: p.cover,
|
|
125
|
+
label: `#${i + 1} ${p.type}${p.type === "carousel" ? `×${p.slides}` : ""} likes ${short(p.likes)} @${p.owner}`,
|
|
126
|
+
})),
|
|
127
|
+
opts.sheet,
|
|
128
|
+
);
|
|
129
|
+
console.log(`covers: ${made.out}`);
|
|
130
|
+
}
|
|
131
|
+
console.log(
|
|
132
|
+
`${posts.length} results for "${topic}"${type ? ` (${type})` : ""} in ${((Date.now() - started) / 1000).toFixed(1)}s${opts.out ? ` → ${opts.out}` : ""}`,
|
|
133
|
+
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything these scripts know about Instagram's page, in one place. Class names
|
|
3
|
+
* are generated and change with every release, so nothing here is a class: roles,
|
|
4
|
+
* tags, and the names of props Instagram's own components pass (read off the React
|
|
5
|
+
* tree, which outlives a restyle). When a script stops with "selectors changed",
|
|
6
|
+
* open the page, take one snapshot, fix the line here, and note the date.
|
|
7
|
+
*/
|
|
8
|
+
export const SEL = {
|
|
9
|
+
// Inbox (last checked 2026-09)
|
|
10
|
+
/** Primary and General share one url and differ by tab (by position: labels are localized). */
|
|
11
|
+
folders: {
|
|
12
|
+
primary: { url: "/direct/inbox/", tab: 0 },
|
|
13
|
+
general: { url: "/direct/inbox/", tab: 1 },
|
|
14
|
+
requests: { url: "/direct/requests/" },
|
|
15
|
+
},
|
|
16
|
+
folderTab: "main [role=tab]",
|
|
17
|
+
/** The thread list. */
|
|
18
|
+
inboxList: "main [role=navigation]",
|
|
19
|
+
/** One thread in it: a pressable row carrying a relative time. */
|
|
20
|
+
inboxRow: "[role=button]:has(abbr)",
|
|
21
|
+
/** An unread row draws its title and preview semibold; a read one is normal. */
|
|
22
|
+
unreadWeight: 600,
|
|
23
|
+
/** Props on the row's components: the id in /direct/t/<id>/, and its last activity. */
|
|
24
|
+
threadIdProp: "threadKeyForSelection",
|
|
25
|
+
lastActivityProp: "last_activity_timestamp_ms",
|
|
26
|
+
|
|
27
|
+
// Thread (last checked 2026-09)
|
|
28
|
+
/** One row of the message list; a message row holds an article, a date line does not. */
|
|
29
|
+
messageRow: "[role=group]",
|
|
30
|
+
messageBody: "[role=article]",
|
|
31
|
+
/** The row's message record: { id, timestamp_ms, sender_fbid }. */
|
|
32
|
+
messageRefProp: "currentMessageRef",
|
|
33
|
+
/** True on a message the signed-in account sent. */
|
|
34
|
+
outgoingProp: "outgoing",
|
|
35
|
+
/** A picture smaller than this on screen is an avatar or an icon, not an attachment. */
|
|
36
|
+
minImagePx: 80,
|
|
37
|
+
|
|
38
|
+
// Composer (last checked 2026-09)
|
|
39
|
+
/** Sidebar entries that open something rather than a page; Create is the second (after Notifications). */
|
|
40
|
+
sidebarAction: 'a[href="#"]',
|
|
41
|
+
createIndex: 1,
|
|
42
|
+
/** Below this width the sidebar becomes a bottom bar with Create elsewhere. */
|
|
43
|
+
wideWidth: 1280,
|
|
44
|
+
/** The step that takes files; the caption step is the one with a textbox. */
|
|
45
|
+
composer: "[role=dialog]",
|
|
46
|
+
fileInput: "input[type=file]",
|
|
47
|
+
caption: "[role=textbox]",
|
|
48
|
+
/** The picture on the crop step, drawn as an image or a background. */
|
|
49
|
+
preview: 'img, [style*="background-image"]',
|
|
50
|
+
aiLabel: "input[role=switch]",
|
|
51
|
+
/** Crop options start with their ratio; the first is Original. */
|
|
52
|
+
cropOptions: ["original", "1:1", "4:5", "16:9"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Instagram's own web client id; its JSON endpoints answer only with it. */
|
|
56
|
+
export const APP_ID = "936619743392459";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Many pictures as one image, each with a label under it, so one look sees a
|
|
4
|
+
* whole feed: covers side by side, or every slide of one post.
|
|
5
|
+
*
|
|
6
|
+
* node sheet.mjs --out sheet.png <posts.json> covers of every post
|
|
7
|
+
* node sheet.mjs --out sheet.png <posts.json> --post 3 every slide of post 3
|
|
8
|
+
* node sheet.mjs --out sheet.png <image url | file>... any pictures
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { extname, resolve } from "node:path";
|
|
12
|
+
import { pathToFileURL } from "node:url";
|
|
13
|
+
import { fail, inPage, orFail, parseArgs } from "./lib.mjs";
|
|
14
|
+
import { short } from "./media.mjs";
|
|
15
|
+
|
|
16
|
+
/** Pictures are fetched by the browser (page.request) and inlined, so the sheet needs no server. */
|
|
17
|
+
export async function makeSheet(items, out, cols = 4) {
|
|
18
|
+
return orFail(
|
|
19
|
+
await inPage(
|
|
20
|
+
async (page, { items, out, cols }) => {
|
|
21
|
+
const cells = [];
|
|
22
|
+
for (const it of items) {
|
|
23
|
+
let src = it.data ?? "";
|
|
24
|
+
if (!src && it.src)
|
|
25
|
+
try {
|
|
26
|
+
const r = await page.request.get(it.src, { timeout: 30000 });
|
|
27
|
+
if (r.ok())
|
|
28
|
+
src = `data:${r.headers()["content-type"] ?? "image/jpeg"};base64,${(await r.body()).toString("base64")}`;
|
|
29
|
+
} catch {}
|
|
30
|
+
cells.push({ src, label: it.label });
|
|
31
|
+
}
|
|
32
|
+
const esc = (s) =>
|
|
33
|
+
String(s).replace(/[&<>"]/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
34
|
+
const html = `<!doctype html><meta charset="utf-8"><style>
|
|
35
|
+
body{margin:0;background:#fff;font:13px/1.35 system-ui,sans-serif;color:#111}
|
|
36
|
+
.grid{display:grid;grid-template-columns:repeat(${cols},240px);gap:10px;padding:10px}
|
|
37
|
+
.cell img,.cell .none{width:240px;height:300px;object-fit:contain;background:#e8e8e8;display:block}
|
|
38
|
+
.cell div{padding:3px 2px;height:36px;overflow:hidden}</style>
|
|
39
|
+
<div class="grid">${cells
|
|
40
|
+
.map(
|
|
41
|
+
(c) =>
|
|
42
|
+
`<div class="cell">${c.src ? `<img src="${c.src}">` : `<div class="none"></div>`}<div>${esc(c.label)}</div></div>`,
|
|
43
|
+
)
|
|
44
|
+
.join("")}</div>`;
|
|
45
|
+
const tab = await page.context().newPage();
|
|
46
|
+
try {
|
|
47
|
+
await tab.setViewportSize({ width: cols * 250 + 10, height: 400 });
|
|
48
|
+
await tab.setContent(html, { waitUntil: "load" });
|
|
49
|
+
await tab.screenshot({ path: out, fullPage: true, scale: "css" });
|
|
50
|
+
} finally {
|
|
51
|
+
await tab.close();
|
|
52
|
+
}
|
|
53
|
+
return { out, missing: cells.filter((c) => !c.src).length };
|
|
54
|
+
},
|
|
55
|
+
{ items, out: resolve(out), cols },
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const MIME = {
|
|
61
|
+
".png": "image/png",
|
|
62
|
+
".webp": "image/webp",
|
|
63
|
+
".gif": "image/gif",
|
|
64
|
+
};
|
|
65
|
+
const inline = (path) =>
|
|
66
|
+
`data:${MIME[extname(path).toLowerCase()] ?? "image/jpeg"};base64,${readFileSync(path).toString("base64")}`;
|
|
67
|
+
|
|
68
|
+
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
69
|
+
const opts = parseArgs();
|
|
70
|
+
if (!opts.out || !opts._.length)
|
|
71
|
+
fail(
|
|
72
|
+
"usage: node sheet.mjs --out sheet.png <posts.json> [--post N] | <image url | file>...",
|
|
73
|
+
);
|
|
74
|
+
let items;
|
|
75
|
+
if (opts._[0].endsWith(".json")) {
|
|
76
|
+
const saved = JSON.parse(readFileSync(opts._[0], "utf8"));
|
|
77
|
+
const posts = saved.posts ?? saved;
|
|
78
|
+
if (opts.post) {
|
|
79
|
+
const p = posts[Number(opts.post) - 1];
|
|
80
|
+
if (!p) fail(`No post ${opts.post}; there are ${posts.length}.`);
|
|
81
|
+
items = p.images.map((src, i) => ({
|
|
82
|
+
src,
|
|
83
|
+
label: `${i + 1}/${p.images.length} ${p.ratio ?? ""}`,
|
|
84
|
+
}));
|
|
85
|
+
} else
|
|
86
|
+
items = posts.map((p, i) => ({
|
|
87
|
+
src: p.cover,
|
|
88
|
+
label: `#${i + 1} ${p.type}${p.type === "carousel" ? `×${p.slides}` : ""} ${p.ratio ?? ""} likes ${short(p.likes)}${p.owner ? ` @${p.owner}` : ""}`,
|
|
89
|
+
}));
|
|
90
|
+
} else
|
|
91
|
+
items = opts._.map((x, i) =>
|
|
92
|
+
/^https?:/.test(x)
|
|
93
|
+
? { src: x, label: `${i + 1}` }
|
|
94
|
+
: { data: inline(x), label: `${i + 1} ${x.split("/").pop()}` },
|
|
95
|
+
);
|
|
96
|
+
const made = await makeSheet(items, opts.out, Number(opts.cols ?? 4));
|
|
97
|
+
console.log(
|
|
98
|
+
`${made.out} — ${items.length} pictures${made.missing ? `, ${made.missing} did not load` : ""}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Saves the picture a web page shares itself with (og:image, else twitter:image),
|
|
4
|
+
* and with --all the large pictures in its body too, through the session's browser
|
|
5
|
+
* so a site that refuses curl still answers. Prints each file with its size and
|
|
6
|
+
* the credit line to put on the slide.
|
|
7
|
+
*
|
|
8
|
+
* node webimage.mjs <page url> --out <dir> [--all] [--min 600]
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { imageSize } from "./image-size.mjs";
|
|
13
|
+
import { fail, inPage, orFail, parseArgs } from "./lib.mjs";
|
|
14
|
+
|
|
15
|
+
const opts = parseArgs();
|
|
16
|
+
const url = opts._[0];
|
|
17
|
+
if (!url || !opts.out)
|
|
18
|
+
fail("usage: node webimage.mjs <page url> --out <dir> [--all] [--min 600]");
|
|
19
|
+
const out = resolve(opts.out);
|
|
20
|
+
mkdirSync(out, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const got = orFail(
|
|
23
|
+
await inPage(
|
|
24
|
+
async (page, { url, all, min }) => {
|
|
25
|
+
const res = await page.request.get(url, { timeout: 30000 });
|
|
26
|
+
const html = await res.text();
|
|
27
|
+
const meta = (key) => {
|
|
28
|
+
const tag = html.match(
|
|
29
|
+
new RegExp(`<meta[^>]+(?:property|name)=["']${key}["'][^>]*>`, "i"),
|
|
30
|
+
)?.[0];
|
|
31
|
+
return tag?.match(/content=["']([^"']+)["']/i)?.[1] ?? null;
|
|
32
|
+
};
|
|
33
|
+
const decode = (s) => s?.replace(/&/g, "&").replace(///g, "/");
|
|
34
|
+
// The VM has no URL class
|
|
35
|
+
const origin = url.match(/^https?:\/\/[^/]+/)[0];
|
|
36
|
+
const absolute = (src) =>
|
|
37
|
+
src.startsWith("//")
|
|
38
|
+
? `https:${src}`
|
|
39
|
+
: src.startsWith("/")
|
|
40
|
+
? origin + src
|
|
41
|
+
: src;
|
|
42
|
+
const site =
|
|
43
|
+
decode(meta("og:site_name")) ?? origin.replace(/^https?:\/\//, "");
|
|
44
|
+
const title =
|
|
45
|
+
decode(meta("og:title")) ?? html.match(/<title>([^<]*)/i)?.[1] ?? "";
|
|
46
|
+
const wanted = [decode(meta("og:image")) ?? decode(meta("twitter:image"))]
|
|
47
|
+
.filter(Boolean)
|
|
48
|
+
.map((src) => ({ src: absolute(src), alt: "og:image" }));
|
|
49
|
+
if (all) {
|
|
50
|
+
const tab = await page.context().newPage();
|
|
51
|
+
try {
|
|
52
|
+
await tab.goto(url, { waitUntil: "load", timeout: 45000 });
|
|
53
|
+
await tab.waitForTimeout(1500);
|
|
54
|
+
const found = await tab.evaluate(
|
|
55
|
+
(min) =>
|
|
56
|
+
[...document.images]
|
|
57
|
+
.filter((i) => i.naturalWidth >= min)
|
|
58
|
+
.map((i) => ({ src: i.currentSrc || i.src, alt: i.alt || "" })),
|
|
59
|
+
min,
|
|
60
|
+
);
|
|
61
|
+
for (const f of found)
|
|
62
|
+
if (!wanted.some((w) => w.src === f.src)) wanted.push(f);
|
|
63
|
+
} finally {
|
|
64
|
+
await tab.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const files = [];
|
|
68
|
+
for (const w of wanted.slice(0, 8)) {
|
|
69
|
+
try {
|
|
70
|
+
const r = await page.request.get(w.src, {
|
|
71
|
+
headers: { referer: url },
|
|
72
|
+
timeout: 30000,
|
|
73
|
+
});
|
|
74
|
+
if (!r.ok()) continue;
|
|
75
|
+
const type = r.headers()["content-type"] ?? "";
|
|
76
|
+
if (!type.startsWith("image/")) continue;
|
|
77
|
+
files.push({ ...w, type, data: (await r.body()).toString("base64") });
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
return { site, title, files, status: res.status() };
|
|
81
|
+
},
|
|
82
|
+
{ url, all: Boolean(opts.all), min: Number(opts.min ?? 600) },
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (!got.files.length)
|
|
87
|
+
fail(
|
|
88
|
+
`No picture came back from ${url} (page answered ${got.status}). Open it in the browser and take one from the page, or try --all.`,
|
|
89
|
+
);
|
|
90
|
+
const ext = (type) =>
|
|
91
|
+
({
|
|
92
|
+
"image/png": "png",
|
|
93
|
+
"image/webp": "webp",
|
|
94
|
+
"image/gif": "gif",
|
|
95
|
+
"image/avif": "avif",
|
|
96
|
+
})[type.split(";")[0]] ?? "jpg";
|
|
97
|
+
got.files.forEach((f, i) => {
|
|
98
|
+
const path = `${out}/web-${String(i + 1).padStart(2, "0")}.${ext(f.type)}`;
|
|
99
|
+
writeFileSync(path, Buffer.from(f.data, "base64"));
|
|
100
|
+
const size = imageSize(path);
|
|
101
|
+
console.log(
|
|
102
|
+
`${path} ${size ? `${size.w}x${size.h}` : f.type} ${f.alt ? `(${f.alt.slice(0, 60)})` : ""}`,
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
console.log(`Credit: ${got.site} — ${got.title.trim().slice(0, 100)}`);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
A carousel: every <section data-slide> is one PNG (scripts/render.mjs).
|
|
4
|
+
Set --w and --h to the size you render at. Pictures sit beside this file.
|
|
5
|
+
Backgrounds: .bg is the picture filling the slide; add .blur, .dark or a
|
|
6
|
+
.shade gradient over it until the text reads. .fit shows a whole picture
|
|
7
|
+
over a blurred copy of itself. .credit names the source of a real picture.
|
|
8
|
+
-->
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;700;900&display=block">
|
|
13
|
+
<style>
|
|
14
|
+
:root { --w: 1080px; --h: 1350px; --ink: #fff; --accent: #ffd400; --paper: #111; }
|
|
15
|
+
* { box-sizing: border-box; margin: 0; }
|
|
16
|
+
body { background: #000; font-family: "Noto Sans KR", "Apple SD Gothic Neo", system-ui, sans-serif; }
|
|
17
|
+
section[data-slide] {
|
|
18
|
+
position: relative; width: var(--w); height: var(--h); overflow: hidden;
|
|
19
|
+
background: var(--paper); color: var(--ink);
|
|
20
|
+
}
|
|
21
|
+
.bg, .fit img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
|
|
22
|
+
.bg.blur { filter: blur(24px) brightness(.7); transform: scale(1.1); }
|
|
23
|
+
.bg.dark { filter: brightness(.55); }
|
|
24
|
+
.shade { position: absolute; inset: 0; background: linear-gradient(180deg, rgba(0,0,0,0) 35%, rgba(0,0,0,.85) 100%); }
|
|
25
|
+
.fit .back { filter: blur(40px) brightness(.6); transform: scale(1.15); }
|
|
26
|
+
.fit .front { object-fit: contain; inset: 12% 6% 30%; width: 88%; height: 58%; }
|
|
27
|
+
.text { position: absolute; left: 72px; right: 72px; bottom: 120px; }
|
|
28
|
+
.kicker { font-size: 34px; font-weight: 700; color: var(--accent); margin-bottom: 20px; }
|
|
29
|
+
h1 { font-size: 92px; line-height: 1.12; font-weight: 900; letter-spacing: -0.02em; word-break: keep-all; }
|
|
30
|
+
p { font-size: 44px; line-height: 1.45; word-break: keep-all; }
|
|
31
|
+
.credit { position: absolute; left: 72px; bottom: 44px; font-size: 24px; opacity: .75; }
|
|
32
|
+
.page { position: absolute; right: 60px; top: 56px; font-size: 28px; opacity: .7; }
|
|
33
|
+
</style>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
|
|
37
|
+
<section data-slide>
|
|
38
|
+
<img class="bg" src="photo.jpg" alt="">
|
|
39
|
+
<div class="shade"></div>
|
|
40
|
+
<div class="text">
|
|
41
|
+
<div class="kicker">NEWS</div>
|
|
42
|
+
<h1>The hook: one line that earns the swipe</h1>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="credit">Photo: Source name</div>
|
|
45
|
+
</section>
|
|
46
|
+
|
|
47
|
+
<section data-slide>
|
|
48
|
+
<div class="fit"><img class="back" src="photo.jpg" alt=""><img class="front" src="photo.jpg" alt=""></div>
|
|
49
|
+
<div class="text"><p>A whole picture over a blurred copy of itself, with a line under it.</p></div>
|
|
50
|
+
<div class="page">2</div>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section data-slide style="--paper: #f4f1ea; --ink: #111">
|
|
54
|
+
<div class="text" style="top: 160px; bottom: auto">
|
|
55
|
+
<h1 style="font-size: 72px">Text alone</h1>
|
|
56
|
+
<p style="margin-top: 40px">One idea a slide, large enough to read on a phone at arm's length.</p>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="page">3</div>
|
|
59
|
+
</section>
|
|
60
|
+
|
|
61
|
+
</body>
|
|
62
|
+
</html>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: mail
|
|
3
|
+
description: "Read and answer web mail by script instead of snapshots: list a search, read a thread into a file, save a draft or an in-thread reply. Gmail today; another site is one more recipe."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Mail by script
|
|
7
|
+
|
|
8
|
+
`scripts/mail.mjs` drives the browser this shell already has, so it rides whatever that browser
|
|
9
|
+
is signed in to. Open it and load the sign-in first (`playwright-cli open`, then `sign_in_use`
|
|
10
|
+
with `google.com`); the script goes to the page itself. Each command is one bash call and prints
|
|
11
|
+
a few lines, never a page.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
M="node <skill dir>/scripts/mail.mjs gmail"
|
|
15
|
+
$M list [query] [--max N] [--json] # Gmail search syntax; default in:inbox, 20, newest first
|
|
16
|
+
$M unread [label] [--json] # how many are unread, and the newest 20
|
|
17
|
+
$M read <id> <file> [--head N] # the whole thread as text into <file>; prints its head
|
|
18
|
+
$M draft --to a@x.com[,b@y.com] [--cc …] --subject "…" --body-file <file>
|
|
19
|
+
$M reply <id> --body-file <file> [--all]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
A list line is `id * date | from <address> | subject (messages) — snippet`, `*` marking unread.
|
|
23
|
+
`read` and `reply` take that id, or any message id from the thread.
|
|
24
|
+
|
|
25
|
+
**What it costs.** `unread` and `read` are plain requests on the session's cookies and draw no
|
|
26
|
+
page: about a second. `list`, `draft` and `reply` drive the Gmail app; the first loads it (5-8 s),
|
|
27
|
+
later ones take 1-2 s. A snapshot of the inbox is about 140,000 characters; `list` of 20 is 7,000.
|
|
28
|
+
|
|
29
|
+
**What marks mail read.** `list` and `unread` never do. `reply` opens the thread, which does.
|
|
30
|
+
`read` on an unread thread may; when the snippet answers the question, leave it unread.
|
|
31
|
+
|
|
32
|
+
**Nothing here sends.** `draft` and `reply` stop once Gmail has saved the draft, with the compose
|
|
33
|
+
left open in this browser; Send is a separate click, and none of these commands make it.
|
|
34
|
+
|
|
35
|
+
**An error is the answer.** `Layout changed: …` means the recipe no longer matches Gmail, never
|
|
36
|
+
that there is no mail: fix it with [references/recipes.md](references/recipes.md). `Signed out`
|
|
37
|
+
means load the sign-in again; if it still says so, the site ended the session.
|