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,448 @@
|
|
|
1
|
+
// Gmail on the web, driven from inside the signed-in browser.
|
|
2
|
+
//
|
|
3
|
+
// Everything that breaks when Gmail changes is in `urls` and `sel`; `seen` is the day they
|
|
4
|
+
// last worked. Each step runs inside the browser (mail.mjs serializes it), so a step uses
|
|
5
|
+
// only its arguments, `r` (this object) and the other steps as `r.steps.<name>`.
|
|
6
|
+
//
|
|
7
|
+
// What costs what: `unread` and `read` are plain requests on the session's cookies and draw
|
|
8
|
+
// no page. `list`, `draft` and `reply` drive the Gmail app itself; the first of them loads it
|
|
9
|
+
// (5-6 s), the ones after it only change the address after `#`.
|
|
10
|
+
export default {
|
|
11
|
+
seen: "2026-09-19",
|
|
12
|
+
defaultQuery: "in:inbox",
|
|
13
|
+
base: "https://mail.google.com/mail/u/0/",
|
|
14
|
+
urls: {
|
|
15
|
+
// Unread mail only, the newest 20, and a count of all of it. Reading it marks nothing.
|
|
16
|
+
feed: "feed/atom/",
|
|
17
|
+
// A whole thread as plain HTML. `th` takes a thread id or the id of any message in it.
|
|
18
|
+
print: "?view=pt&search=all&th=",
|
|
19
|
+
// Full-page compose; the fields are prefilled from the address.
|
|
20
|
+
compose: "?view=cm&fs=1&tf=1",
|
|
21
|
+
search: "#search/",
|
|
22
|
+
thread: "#all/",
|
|
23
|
+
},
|
|
24
|
+
sel: {
|
|
25
|
+
// The pane showing now: Gmail keeps earlier views in the page, hidden.
|
|
26
|
+
main: "div[role=main]",
|
|
27
|
+
row: "tr.zA",
|
|
28
|
+
unreadRow: "zE",
|
|
29
|
+
id: "[data-legacy-thread-id]",
|
|
30
|
+
sender: ".yW [email]",
|
|
31
|
+
count: ".bx0",
|
|
32
|
+
subject: ".bog",
|
|
33
|
+
snippet: ".y2",
|
|
34
|
+
date: ".xW span[title]",
|
|
35
|
+
// "1–50 of 243": first, last, total.
|
|
36
|
+
counter: ".Dj .ts",
|
|
37
|
+
// The "No matches" panel of an empty search.
|
|
38
|
+
empty: ".HBrgMb, td.TC",
|
|
39
|
+
// A free-text search opens sorted by relevance; the list wants newest first.
|
|
40
|
+
// Words on the sort button and in its menu, in the account's display language.
|
|
41
|
+
relevance: "most relevant",
|
|
42
|
+
newest: "Most recent",
|
|
43
|
+
// Print view.
|
|
44
|
+
printMessage: "table.message",
|
|
45
|
+
printSubject: ".maincontent font[size='+1'] b",
|
|
46
|
+
quoted: ".gmail_quote, blockquote[type=cite]",
|
|
47
|
+
// Compose and reply. `draft` holds the draft's id once Gmail has saved it.
|
|
48
|
+
composeBody: "div[role=textbox][contenteditable=true]",
|
|
49
|
+
composeSubject: "input[name=subjectbox]",
|
|
50
|
+
draftId: "input[name=draft]",
|
|
51
|
+
threadOpen: "h2.hP",
|
|
52
|
+
replyButton: ".ams.bkH",
|
|
53
|
+
replyAllButton: ".ams.bkI",
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
steps: {
|
|
57
|
+
/** The Gmail app on screen, signed in; loads it only when another page is showing. */
|
|
58
|
+
async app(page, _args, r) {
|
|
59
|
+
// The app is the bare address plus a `#view`; compose and print pages carry a `?`.
|
|
60
|
+
const inApp = () =>
|
|
61
|
+
page.url().startsWith(r.base) &&
|
|
62
|
+
!page.url().split("#")[0].includes("?");
|
|
63
|
+
if (!inApp()) {
|
|
64
|
+
await page.goto(r.base, { waitUntil: "domcontentloaded" });
|
|
65
|
+
}
|
|
66
|
+
await page
|
|
67
|
+
.waitForFunction(
|
|
68
|
+
(main) =>
|
|
69
|
+
/accounts\.google\.com|workspace\.google\.com/.test(
|
|
70
|
+
location.host,
|
|
71
|
+
) ||
|
|
72
|
+
[...document.querySelectorAll(main)].some((m) => m.offsetParent),
|
|
73
|
+
r.sel.main,
|
|
74
|
+
{ timeout: 30_000 },
|
|
75
|
+
)
|
|
76
|
+
.catch(() => {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Layout changed: no ${r.sel.main} on ${page.url()} after 30 s. Update sel.main in the recipe.`,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
if (!page.url().startsWith(r.base))
|
|
82
|
+
throw new Error(
|
|
83
|
+
"Signed out of Gmail. Load the kept sign-in for google.com, then run this again.",
|
|
84
|
+
);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/** Waits for the pane after `hash` to show, then returns its rows as fields. */
|
|
88
|
+
async show(page, { hash }, r) {
|
|
89
|
+
const sel = r.sel;
|
|
90
|
+
const same = await page.evaluate(
|
|
91
|
+
({ hash, row }) => {
|
|
92
|
+
document.querySelectorAll(row).forEach((x) => {
|
|
93
|
+
x.dataset.mailKitOld = "1";
|
|
94
|
+
});
|
|
95
|
+
if (location.hash === hash) return true;
|
|
96
|
+
location.hash = hash;
|
|
97
|
+
return false;
|
|
98
|
+
},
|
|
99
|
+
{ hash, row: sel.row },
|
|
100
|
+
);
|
|
101
|
+
if (same) {
|
|
102
|
+
await page.evaluate((row) => {
|
|
103
|
+
document.querySelectorAll(row).forEach((x) => {
|
|
104
|
+
delete x.dataset.mailKitOld;
|
|
105
|
+
});
|
|
106
|
+
}, sel.row);
|
|
107
|
+
}
|
|
108
|
+
await page
|
|
109
|
+
.waitForFunction(
|
|
110
|
+
(sel) => {
|
|
111
|
+
const main = [...document.querySelectorAll(sel.main)].find(
|
|
112
|
+
(m) => m.offsetParent,
|
|
113
|
+
);
|
|
114
|
+
if (!main) return false;
|
|
115
|
+
const rows = [...main.querySelectorAll(sel.row)];
|
|
116
|
+
if (rows.length) return rows.every((x) => !x.dataset.mailKitOld);
|
|
117
|
+
return !!main.querySelector(sel.empty);
|
|
118
|
+
},
|
|
119
|
+
sel,
|
|
120
|
+
{ timeout: 20_000 },
|
|
121
|
+
)
|
|
122
|
+
.catch(() => {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Layout changed: after ${hash} neither rows (${sel.row}) nor the empty panel (${sel.empty}) showed in 20 s. Update the recipe.`,
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
return page.evaluate((sel) => {
|
|
128
|
+
const main = [...document.querySelectorAll(sel.main)].find(
|
|
129
|
+
(m) => m.offsetParent,
|
|
130
|
+
);
|
|
131
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
132
|
+
const when = (title) => {
|
|
133
|
+
const d = new Date(title);
|
|
134
|
+
return Number.isNaN(d.getTime())
|
|
135
|
+
? title
|
|
136
|
+
: `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
137
|
+
};
|
|
138
|
+
const rows = [...main.querySelectorAll(sel.row)].map((x) => {
|
|
139
|
+
const senders = [...x.querySelectorAll(sel.sender)];
|
|
140
|
+
const last = senders.at(-1);
|
|
141
|
+
return {
|
|
142
|
+
id: x.querySelector(sel.id)?.getAttribute("data-legacy-thread-id"),
|
|
143
|
+
unread: x.classList.contains(sel.unreadRow),
|
|
144
|
+
date: when(x.querySelector(sel.date)?.getAttribute("title") ?? ""),
|
|
145
|
+
from: [
|
|
146
|
+
...new Set(
|
|
147
|
+
senders.map((s) => s.getAttribute("name") || s.textContent),
|
|
148
|
+
),
|
|
149
|
+
].join(", "),
|
|
150
|
+
email: last?.getAttribute("email") ?? "",
|
|
151
|
+
subject: x.querySelector(sel.subject)?.textContent ?? null,
|
|
152
|
+
count: Number(
|
|
153
|
+
x.querySelector(sel.count)?.textContent.replace(/\D/g, "") || 1,
|
|
154
|
+
),
|
|
155
|
+
snippet: (x.querySelector(sel.snippet)?.textContent ?? "").replace(
|
|
156
|
+
/^\s*[-–]\s*/,
|
|
157
|
+
"",
|
|
158
|
+
),
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
const counter = [...main.querySelectorAll(sel.counter)].map((t) =>
|
|
162
|
+
t.textContent.trim(),
|
|
163
|
+
);
|
|
164
|
+
const missing = rows.length
|
|
165
|
+
? ["id", "subject", "from"].filter((k) => rows.some((x) => !x[k]))
|
|
166
|
+
: [];
|
|
167
|
+
return {
|
|
168
|
+
rows,
|
|
169
|
+
missing,
|
|
170
|
+
total: counter[2] ?? String(rows.length),
|
|
171
|
+
more:
|
|
172
|
+
counter.length === 3 &&
|
|
173
|
+
(!/^\d[\d,.]*$/.test(counter[2]) ||
|
|
174
|
+
Number(counter[1].replace(/\D/g, "")) <
|
|
175
|
+
Number(counter[2].replace(/\D/g, ""))),
|
|
176
|
+
};
|
|
177
|
+
}, sel);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async list(page, { query, max }, r) {
|
|
181
|
+
await r.steps.app(page, {}, r);
|
|
182
|
+
const hash = `${r.urls.search}${encodeURIComponent(query)}`;
|
|
183
|
+
let shown = await r.steps.show(page, { hash }, r);
|
|
184
|
+
const sortButton = page
|
|
185
|
+
.locator(`${r.sel.main} button:visible`)
|
|
186
|
+
.filter({ hasText: r.sel.relevance })
|
|
187
|
+
.first();
|
|
188
|
+
if (shown.rows.length && (await sortButton.count())) {
|
|
189
|
+
await sortButton.click();
|
|
190
|
+
const newest = page
|
|
191
|
+
.locator("[role=menuitemradio]:visible")
|
|
192
|
+
.filter({ hasText: r.sel.newest })
|
|
193
|
+
.first();
|
|
194
|
+
if ((await newest.getAttribute("aria-checked")) === "true") {
|
|
195
|
+
await page.keyboard.press("Escape");
|
|
196
|
+
} else {
|
|
197
|
+
await newest.click();
|
|
198
|
+
await sortButton.waitFor({ state: "detached", timeout: 10_000 });
|
|
199
|
+
// The sorted rows replace the old ones in the same pane.
|
|
200
|
+
await page.waitForTimeout(1500);
|
|
201
|
+
shown = await r.steps.show(page, { hash }, r);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const rows = [...shown.rows];
|
|
205
|
+
for (let p = 2; rows.length < max && shown.more && p <= 10; p++) {
|
|
206
|
+
shown = await r.steps.show(page, { hash: `${hash}/p${p}` }, r);
|
|
207
|
+
rows.push(...shown.rows);
|
|
208
|
+
}
|
|
209
|
+
if (shown.missing.length)
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Layout changed: rows came back without ${shown.missing.join(", ")}. Update sel in the recipe.`,
|
|
212
|
+
);
|
|
213
|
+
return { rows: rows.slice(0, max), total: shown.total };
|
|
214
|
+
},
|
|
215
|
+
|
|
216
|
+
async unread(page, { label }, r) {
|
|
217
|
+
const res = await page
|
|
218
|
+
.context()
|
|
219
|
+
.request.get(`${r.base}${r.urls.feed}${encodeURIComponent(label)}`);
|
|
220
|
+
if (res.status() === 401)
|
|
221
|
+
throw new Error(
|
|
222
|
+
"Signed out of Gmail. Load the kept sign-in for google.com, then run this again.",
|
|
223
|
+
);
|
|
224
|
+
if (!res.ok())
|
|
225
|
+
throw new Error(`The unread feed answered ${res.status()}.`);
|
|
226
|
+
const xml = await res.text();
|
|
227
|
+
return page.evaluate((xml) => {
|
|
228
|
+
// Gmail enforces Trusted Types: a raw string is refused by DOMParser.
|
|
229
|
+
const tt = window.trustedTypes;
|
|
230
|
+
const policy =
|
|
231
|
+
tt &&
|
|
232
|
+
(window.mailKitPolicy ??= tt.createPolicy("mailKit", {
|
|
233
|
+
createHTML: (s) => s,
|
|
234
|
+
}));
|
|
235
|
+
const doc = new DOMParser().parseFromString(
|
|
236
|
+
policy ? policy.createHTML(xml) : xml,
|
|
237
|
+
"text/xml",
|
|
238
|
+
);
|
|
239
|
+
const fullcount = doc.querySelector("fullcount");
|
|
240
|
+
if (!fullcount)
|
|
241
|
+
throw new Error(
|
|
242
|
+
"Layout changed: the unread feed has no <fullcount>. Update the recipe.",
|
|
243
|
+
);
|
|
244
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
245
|
+
const when = (iso) => {
|
|
246
|
+
const d = new Date(iso);
|
|
247
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
248
|
+
};
|
|
249
|
+
const rows = [...doc.querySelectorAll("entry")].map((e) => {
|
|
250
|
+
const text = (s) => e.querySelector(s)?.textContent ?? "";
|
|
251
|
+
const link = e.querySelector("link")?.getAttribute("href") ?? "";
|
|
252
|
+
return {
|
|
253
|
+
id: new URL(link).searchParams.get("message_id") ?? "",
|
|
254
|
+
unread: true,
|
|
255
|
+
date: when(text("issued") || text("modified")),
|
|
256
|
+
from: text("author name"),
|
|
257
|
+
email: text("author email"),
|
|
258
|
+
subject: text("title"),
|
|
259
|
+
count: 1,
|
|
260
|
+
snippet: text("summary"),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
return { total: Number(fullcount.textContent), rows };
|
|
264
|
+
}, xml);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
async read(page, { id }, r) {
|
|
268
|
+
if (!/^[0-9a-f]{10,20}$/i.test(id))
|
|
269
|
+
throw new Error(
|
|
270
|
+
`${id} is not a Gmail id: take the id column from list or unread.`,
|
|
271
|
+
);
|
|
272
|
+
const res = await page
|
|
273
|
+
.context()
|
|
274
|
+
.request.get(`${r.base}${r.urls.print}${id}`);
|
|
275
|
+
if (!res.url().startsWith(r.base))
|
|
276
|
+
throw new Error(
|
|
277
|
+
"Signed out of Gmail. Load the kept sign-in for google.com, then run this again.",
|
|
278
|
+
);
|
|
279
|
+
if (!res.ok())
|
|
280
|
+
throw new Error(`The print view answered ${res.status()} for ${id}.`);
|
|
281
|
+
const html = await res.text();
|
|
282
|
+
return page.evaluate(
|
|
283
|
+
({ html, sel }) => {
|
|
284
|
+
// Gmail enforces Trusted Types: a raw string is refused by DOMParser.
|
|
285
|
+
const tt = window.trustedTypes;
|
|
286
|
+
const policy =
|
|
287
|
+
tt &&
|
|
288
|
+
(window.mailKitPolicy ??= tt.createPolicy("mailKit", {
|
|
289
|
+
createHTML: (s) => s,
|
|
290
|
+
}));
|
|
291
|
+
const doc = new DOMParser().parseFromString(
|
|
292
|
+
policy ? policy.createHTML(html) : html,
|
|
293
|
+
"text/html",
|
|
294
|
+
);
|
|
295
|
+
const tables = [...doc.querySelectorAll(sel.printMessage)];
|
|
296
|
+
if (!tables.length)
|
|
297
|
+
throw new Error(
|
|
298
|
+
`Layout changed or no such thread: the print view has no ${sel.printMessage}. Check the id; if it is right, update the recipe.`,
|
|
299
|
+
);
|
|
300
|
+
const BLOCK =
|
|
301
|
+
/^(P|DIV|BR|TR|LI|H[1-6]|TABLE|BLOCKQUOTE|PRE|UL|OL|HR|SECTION|ARTICLE|HEADER|FOOTER)$/;
|
|
302
|
+
const text = (node) => {
|
|
303
|
+
let out = "";
|
|
304
|
+
const walk = (n) => {
|
|
305
|
+
if (n.nodeType === 3) out += n.textContent.replace(/\s+/g, " ");
|
|
306
|
+
if (n.nodeType !== 1) return;
|
|
307
|
+
if (/^(STYLE|SCRIPT|HEAD|TITLE)$/.test(n.tagName)) return;
|
|
308
|
+
const block = BLOCK.test(n.tagName);
|
|
309
|
+
if (block) out += "\n";
|
|
310
|
+
if (n.tagName === "TD") out += " ";
|
|
311
|
+
for (const c of n.childNodes) walk(c);
|
|
312
|
+
if (block) out += "\n";
|
|
313
|
+
};
|
|
314
|
+
walk(node);
|
|
315
|
+
return out
|
|
316
|
+
.split("\n")
|
|
317
|
+
.map((l) => l.trim())
|
|
318
|
+
.join("\n")
|
|
319
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
320
|
+
.trim();
|
|
321
|
+
};
|
|
322
|
+
const unwrap = (href) => {
|
|
323
|
+
try {
|
|
324
|
+
const u = new URL(href);
|
|
325
|
+
return u.hostname === "www.google.com" && u.pathname === "/url"
|
|
326
|
+
? (u.searchParams.get("q") ?? href)
|
|
327
|
+
: href;
|
|
328
|
+
} catch {
|
|
329
|
+
return href;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
const messages = tables.map((t) => {
|
|
333
|
+
const rows = [...t.querySelectorAll(":scope > tbody > tr")];
|
|
334
|
+
const cells = rows[0]?.querySelectorAll(":scope > td") ?? [];
|
|
335
|
+
const bodyCell = rows.at(-1);
|
|
336
|
+
bodyCell
|
|
337
|
+
?.querySelectorAll(sel.quoted)
|
|
338
|
+
.forEach((q) =>
|
|
339
|
+
q.replaceWith(doc.createTextNode("[quoted text cut]")),
|
|
340
|
+
);
|
|
341
|
+
const links = [
|
|
342
|
+
...new Set(
|
|
343
|
+
[...(bodyCell?.querySelectorAll("a[href]") ?? [])]
|
|
344
|
+
.map((a) => unwrap(a.getAttribute("href")))
|
|
345
|
+
.filter((h) => /^https?:/.test(h)),
|
|
346
|
+
),
|
|
347
|
+
];
|
|
348
|
+
return {
|
|
349
|
+
from: text(cells[0] ?? doc.createElement("i")),
|
|
350
|
+
date: text(cells[1] ?? doc.createElement("i")),
|
|
351
|
+
to: rows.length > 2 ? text(rows[1]) : "",
|
|
352
|
+
body: bodyCell ? text(bodyCell) : "",
|
|
353
|
+
links,
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
return {
|
|
357
|
+
subject: doc.querySelector(sel.printSubject)?.textContent ?? "",
|
|
358
|
+
messages,
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
{ html, sel: r.sel },
|
|
362
|
+
);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/** Waits for the open compose to be saved as a draft, and says what it holds. */
|
|
366
|
+
async saved(page, _args, r) {
|
|
367
|
+
await page
|
|
368
|
+
.waitForFunction(
|
|
369
|
+
(input) =>
|
|
370
|
+
[...document.querySelectorAll(input)].some((i) =>
|
|
371
|
+
/^#?msg-/.test(i.value),
|
|
372
|
+
),
|
|
373
|
+
r.sel.draftId,
|
|
374
|
+
{ timeout: 20_000 },
|
|
375
|
+
)
|
|
376
|
+
.catch(() => {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`The draft was not saved in 20 s (no id in ${r.sel.draftId}). Take a snapshot and look; update the recipe if the compose changed.`,
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
return "Saved in Drafts, not sent. The compose stays open in this browser.";
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
async draft(page, { to, cc, subject, body }, r) {
|
|
385
|
+
// run-code has no URLSearchParams.
|
|
386
|
+
const q = Object.entries({ to, cc, su: subject, body })
|
|
387
|
+
.filter(([, v]) => v)
|
|
388
|
+
.map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
|
|
389
|
+
.join("&");
|
|
390
|
+
await page.goto(`${r.base}${r.urls.compose}&${q}`, {
|
|
391
|
+
waitUntil: "domcontentloaded",
|
|
392
|
+
});
|
|
393
|
+
if (!page.url().startsWith(r.base))
|
|
394
|
+
throw new Error(
|
|
395
|
+
"Signed out of Gmail. Load the kept sign-in for google.com, then run this again.",
|
|
396
|
+
);
|
|
397
|
+
await page
|
|
398
|
+
.locator(r.sel.composeSubject)
|
|
399
|
+
.first()
|
|
400
|
+
.waitFor({ timeout: 30_000 })
|
|
401
|
+
.catch(() => {
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Layout changed: no ${r.sel.composeSubject} in the compose. Update the recipe.`,
|
|
404
|
+
);
|
|
405
|
+
});
|
|
406
|
+
// An untouched prefilled compose is not saved until something is typed into it.
|
|
407
|
+
const box = page.locator(r.sel.composeBody).first();
|
|
408
|
+
await box.click();
|
|
409
|
+
await page.keyboard.press("End");
|
|
410
|
+
await page.keyboard.insertText(" ");
|
|
411
|
+
await page.keyboard.press("Backspace");
|
|
412
|
+
return r.steps.saved(page, {}, r);
|
|
413
|
+
},
|
|
414
|
+
|
|
415
|
+
async reply(page, { id, all, body }, r) {
|
|
416
|
+
await r.steps.app(page, {}, r);
|
|
417
|
+
await page.evaluate((h) => {
|
|
418
|
+
location.hash = h;
|
|
419
|
+
}, `${r.urls.thread}${id}`);
|
|
420
|
+
await page
|
|
421
|
+
.locator(r.sel.threadOpen)
|
|
422
|
+
.first()
|
|
423
|
+
.waitFor({ timeout: 20_000 })
|
|
424
|
+
.catch(() => {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`Layout changed or no such thread: ${r.sel.threadOpen} did not show for ${id}.`,
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
const button = page
|
|
430
|
+
.locator(all ? r.sel.replyAllButton : r.sel.replyButton)
|
|
431
|
+
.last();
|
|
432
|
+
await button.click({ timeout: 10_000 }).catch(() => {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Layout changed: no reply button (${all ? r.sel.replyAllButton : r.sel.replyButton}). Update the recipe.`,
|
|
435
|
+
);
|
|
436
|
+
});
|
|
437
|
+
const box = page.locator(r.sel.composeBody).last();
|
|
438
|
+
await box.waitFor({ timeout: 10_000 });
|
|
439
|
+
await box.click();
|
|
440
|
+
const lines = body.replace(/\r/g, "").split("\n");
|
|
441
|
+
for (const [i, line] of lines.entries()) {
|
|
442
|
+
if (i) await page.keyboard.press("Enter");
|
|
443
|
+
if (line) await page.keyboard.insertText(line);
|
|
444
|
+
}
|
|
445
|
+
return r.steps.saved(page, {}, r);
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
A recipe is one file in `recipes/`, named after the site the first argument of `mail.mjs` names
|
|
4
|
+
(`gmail` → `recipes/gmail.mjs`). It holds everything that breaks when the site changes:
|
|
5
|
+
|
|
6
|
+
- `base` and `urls` — the address shapes: the app, a search, a thread, compose, and any plain
|
|
7
|
+
endpoint that answers without drawing the app (Gmail's unread feed and print view).
|
|
8
|
+
- `sel` — every selector and every word matched on screen.
|
|
9
|
+
- `seen` — the day the recipe last worked.
|
|
10
|
+
- `steps` — `list`, `unread`, `read`, `draft`, `reply`, each `async (page, args, r)`.
|
|
11
|
+
|
|
12
|
+
This copy is in your own folder, so what you change here is what every later job runs.
|
|
13
|
+
|
|
14
|
+
## When a command says `Layout changed`
|
|
15
|
+
|
|
16
|
+
1. Read the error: it names the selector or the step that failed.
|
|
17
|
+
2. Look once. Open the page it was on and read the part in question — `playwright-cli find` with
|
|
18
|
+
words you expect there, or `snapshot <ref>` for one region. `--raw eval` confirms a candidate:
|
|
19
|
+
`playwright-cli --raw eval "document.querySelectorAll('<selector>').length"`.
|
|
20
|
+
3. Change that one entry in `sel` (or `urls`), set `seen` to today, and run the command again.
|
|
21
|
+
4. Still failing after one fix: say what broke in your report, and do the rest of the job by
|
|
22
|
+
snapshot. Never run a broken recipe twice.
|
|
23
|
+
|
|
24
|
+
Class names in Gmail (`tr.zA`, `.bog`) are generated but have held for years; the empty-search
|
|
25
|
+
panel (`.HBrgMb`) and the sort menu's words are the likeliest to move. The sort words follow the
|
|
26
|
+
account's display language.
|
|
27
|
+
|
|
28
|
+
## How a step runs
|
|
29
|
+
|
|
30
|
+
`mail.mjs` turns the recipe into one `playwright-cli --raw run-code` call: every step is
|
|
31
|
+
serialized to text and run inside the browser session, and what the step returns comes back as
|
|
32
|
+
JSON. So a step:
|
|
33
|
+
|
|
34
|
+
- uses only its arguments, `r` and `r.steps.<other>` — nothing from the file around it, no imports;
|
|
35
|
+
- has no `URLSearchParams`, `fetch` or `require` at the top level: build addresses with
|
|
36
|
+
`encodeURIComponent`, fetch with `page.context().request.get(url)` (it carries the session's
|
|
37
|
+
cookies and draws nothing), and do DOM work inside `page.evaluate`;
|
|
38
|
+
- parses fetched HTML inside the page with `DOMParser` — on Gmail through a Trusted Types policy,
|
|
39
|
+
since the page refuses raw strings;
|
|
40
|
+
- throws an `Error` whose message says what to fix, rather than returning an empty list.
|
|
41
|
+
|
|
42
|
+
## Adding a site
|
|
43
|
+
|
|
44
|
+
Copy `gmail.mjs` to `recipes/<site>.mjs` and fill the same five steps, returning the same shapes:
|
|
45
|
+
|
|
46
|
+
- `list`: `{ rows: [{ id, unread, date, from, email, subject, count, snippet }], total }`
|
|
47
|
+
- `unread`: the same, for unread mail only; a site with no cheap way returns `list` of its unread
|
|
48
|
+
search.
|
|
49
|
+
- `read`: `{ subject, messages: [{ from, date, to, body, links }] }`
|
|
50
|
+
- `draft`, `reply`: a sentence saying where the draft was saved.
|
|
51
|
+
|
|
52
|
+
Find the cheap read first: load the site once, run `playwright-cli requests`, and look for a
|
|
53
|
+
feed, a print or "show original" view, or a JSON call carrying the list — `response-body N`
|
|
54
|
+
shows it. A request on the session's cookies beats reading the drawn app. Try every step by
|
|
55
|
+
hand on mail that is already read, and write down in `sel` comments what each selector is.
|