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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +0 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/routes-manifest.json +0 -6
- 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.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/reach/route.js +13 -13
- package/.next/server/app/api/reach/route.js.nft.json +1 -1
- package/.next/server/app/api/routine/route.js.nft.json +1 -1
- package/.next/server/app/api/signins/route.js +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.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js +14 -15
- 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 +4 -4
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +0 -1
- package/.next/server/chunks/[root-of-the-server]__014ydvq._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0209ah4._.js +7 -5
- package/.next/server/chunks/[root-of-the-server]__04r4h-2._.js +4 -2
- package/.next/server/chunks/[root-of-the-server]__0542v_d._.js +3 -0
- package/.next/server/chunks/[root-of-the-server]__0gewdg9._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__0mrk2it._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1-swlkw._.js +2 -2
- package/.next/server/chunks/[root-of-the-server]__12iddfq._.js +1 -1
- package/.next/server/chunks/[root-of-the-server]__1ai5lme._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1cjxw0-._.js +2 -2
- package/.next/server/chunks/{[root-of-the-server]__1fy2g82._.js → [root-of-the-server]__1k3j52n._.js} +1 -1
- package/.next/server/chunks/[root-of-the-server]__1kyjt5m._.js +3 -3
- package/.next/server/chunks/[root-of-the-server]__1ygplx1._.js +1 -1
- package/.next/server/chunks/_05v8k21._.js +3 -1
- package/.next/server/chunks/_0awtznl._.js +1 -1
- package/.next/server/chunks/_0na2fj1._.js +1 -1
- package/.next/server/chunks/_0x-ej3f._.js +17 -17
- package/.next/server/chunks/_12-amdu._.js +2 -2
- package/.next/server/chunks/_15740dw._.js +60 -0
- package/.next/server/chunks/_1dc_d1q._.js +3 -3
- package/.next/server/chunks/_1i350l5._.js +3 -1
- package/.next/server/chunks/_1jc5g4p._.js +2 -2
- package/.next/server/chunks/_1jn-aep._.js +4 -4
- package/.next/server/chunks/_1m88lkh._.js +11 -11
- package/.next/server/chunks/_1ogqovh._.js +4 -4
- package/.next/server/chunks/_1uuucy-._.js +1 -1
- package/.next/server/chunks/_1zxp-p6._.js +1 -1
- package/.next/server/chunks/features_0rfvely._.js +5 -3
- package/.next/server/chunks/ssr/{[root-of-the-server]__1se25x0._.js → [root-of-the-server]__0hpp3nw._.js} +29 -13
- package/.next/server/chunks/ssr/[root-of-the-server]__0m7u9j-._.js +3 -0
- package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +24 -60
- package/.next/server/chunks/ssr/[root-of-the-server]__1cz2rz5._.js +3 -0
- package/.next/server/chunks/ssr/{[root-of-the-server]__0tvavy9._.js → [root-of-the-server]__1iwyp1e._.js} +1 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__1w9hqq8._.js +1 -1
- package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
- package/.next/server/chunks/ssr/{_1tmu8z-._.js → _0btzccu._.js} +2 -2
- package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
- package/.next/server/chunks/ssr/_0hjzb5o._.js +3 -0
- package/.next/server/chunks/ssr/_0ld9_qq._.js +1 -1
- package/.next/server/chunks/ssr/{_0urj3mc._.js → _0ouzu9f._.js} +2 -2
- package/.next/server/chunks/ssr/{_1-zyvdb._.js → _0u7ko71._.js} +2 -2
- package/.next/server/chunks/ssr/{_0ltz2q5._.js → _0wd_o8-._.js} +2 -2
- package/.next/server/chunks/ssr/_0ya0cb0._.js +3 -0
- package/.next/server/chunks/ssr/_10yyqtk._.js +67 -0
- package/.next/server/chunks/ssr/{_1uy0xom._.js → _12eyown._.js} +3 -3
- package/.next/server/chunks/ssr/_12mijol._.js +11 -11
- package/.next/server/chunks/ssr/_132qkfz._.js +1 -1
- package/.next/server/chunks/ssr/_184327j._.js +1 -1
- package/.next/server/chunks/ssr/{_1z74dmq._.js → _19ps4p3._.js} +7 -7
- package/.next/server/chunks/ssr/{_1flbs77._.js → _1bhy4yg._.js} +2 -2
- package/.next/server/chunks/ssr/_1d423aj._.js +7 -5
- package/.next/server/chunks/ssr/{_1a-nuxx._.js → _1kd843u._.js} +2 -2
- package/.next/server/chunks/ssr/{_0ital-2._.js → _1kq2ye_._.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/_1p8xorg._.js +3 -0
- package/.next/server/chunks/ssr/{_1x-kojb._.js → _1q683w8._.js} +3 -3
- package/.next/server/chunks/ssr/_1tawxu2._.js +3 -0
- package/.next/server/chunks/ssr/{_0scf7xy._.js → _1w2oba6._.js} +2 -2
- package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
- package/.next/server/chunks/ssr/{_0kjj5ts._.js → _1ybo1hd._.js} +3 -3
- package/.next/server/chunks/ssr/features_bot_0s11m4r._.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/features_workspace_00jz2hu._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_1af3w91._.js → node_modules_00pocul._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0th357h._.js → node_modules_02zt4az._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0jljel9._.js → node_modules_05ojmcl._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0wnlddw._.js → node_modules_06i6bli._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_06bb4q8._.js → node_modules_09dj429._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_068ws5v._.js → node_modules_0auzgvx._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1ogczzz._.js → node_modules_0evjwh1._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_13kwejm._.js → node_modules_0gmdur8._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1s-gb-1._.js → node_modules_0j3te-1._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1bucjlk._.js → node_modules_0rbaymq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1wbkzl9._.js → node_modules_0tf8_1e._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_12et8d5._.js → node_modules_0yqy-0b._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1lkix61._.js → node_modules_1-53ebx._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_03d0rfz._.js → node_modules_1-y9aho._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0x630an._.js → node_modules_11bltzp._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0uyh_vb._.js → node_modules_18jn2qo._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0dzm4um._.js → node_modules_1_te3bu._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_07rs_p2._.js → node_modules_1c0pg3k._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0sgy-dc._.js → node_modules_1en2jx5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ubdtk-._.js → node_modules_1p-c8cr._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1z1lrbk._.js → node_modules_1qe-oxg._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_04_sbbc._.js → node_modules_1rjt833._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yvz9e0._.js → node_modules_1tzw5sn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1uwg-gz._.js → node_modules_1x6uwvn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0t_ez6-._.js → node_modules_1zqddke._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_05ybmta._.js → node_modules_211zgim._.js} +1 -1
- 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
- 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
- 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/{13ixzheqw7o-w.js → 03-724sj0nk_z.js} +2 -2
- package/.next/static/chunks/03bhu4mk8en2q.js +1 -0
- package/.next/static/chunks/{3h2e-xv1zxh_f.js → 04imzjhvzw65d.js} +2 -2
- package/.next/static/chunks/{27tvv1i-smrck.js → 0gljzv9fz1g7d.js} +1 -1
- package/.next/static/chunks/{37j27b3e1gt7r.js → 0h0lythv1ql93.js} +1 -1
- package/.next/static/chunks/{2o4dndk0d68do.js → 0hzbj7f48cmx0.js} +1 -1
- package/.next/static/chunks/0lwunwbl7u7pt.js +1 -0
- package/.next/static/chunks/{0u9ae9k_2g7k3.js → 0r6nwxg4dv83t.js} +1 -1
- package/.next/static/chunks/0rn9nbg6aif9f.js +17 -0
- package/.next/static/chunks/{3bbzsitgiltnt.js → 0x_w4shtvrrfg.js} +1 -1
- package/.next/static/chunks/{2sdwbfs1x5skp.js → 0ympuji_ufypd.js} +1 -1
- package/.next/static/chunks/{3t9huugh-5_ps.js → 0zmrz4zfzm4dt.js} +1 -1
- package/.next/static/chunks/{0qmd8n52hp-6v.js → 11tlxkr7a_rpc.js} +1 -1
- package/.next/static/chunks/{2c64drdevdla3.js → 12scesro428m7.js} +2 -2
- package/.next/static/chunks/16s2fx3890_jd.js +65 -0
- package/.next/static/chunks/{0oyz95sujd2c7.js → 1qzfgplyo18tc.js} +1 -1
- package/.next/static/chunks/{04e-e--5lk26x.js → 1r4zajz14jp9-.js} +1 -1
- package/.next/static/chunks/1s6m4cmbn7htf.js +1 -0
- package/.next/static/chunks/1wl9t1-3i-q8p.css +1 -0
- package/.next/static/chunks/2-asot3p1j3yg.js +1 -0
- package/.next/static/chunks/{0u3ktztrzu1fa.js → 2-jn4xscbm-i2.js} +1 -1
- package/.next/static/chunks/22h9gvclqtx_2.js +1 -0
- package/.next/static/chunks/{337g9g_ux2gj-.js → 26-eblxwnykck.js} +1 -1
- package/.next/static/chunks/{43q9cgbuw8krx.js → 2kzwhbuobpkw3.js} +2 -2
- package/.next/static/chunks/2m6jd3586ijb-.js +1 -0
- package/.next/static/chunks/{0_awn8tucznxc.js → 304q9p54tcgm1.js} +1 -1
- package/.next/static/chunks/{1u5i-jti87jbo.js → 35wdx3wt7cg4a.js} +1 -1
- package/.next/static/chunks/37unh9460im7v.js +18 -0
- package/.next/static/chunks/{01xjer0i_e5z5.js → 39gj5vxz1zpm_.js} +1 -1
- package/.next/static/chunks/{427qsc6kahxyo.js → 39vrqy1r_00jc.js} +1 -1
- package/.next/static/chunks/{245hb85nsy6j4.js → 3dipmu0mptqum.js} +1 -1
- package/.next/static/chunks/3gki-squw28yu.js +1 -0
- package/.next/static/chunks/{3obf9n3cxj99k.js → 3jvldk2wvwbge.js} +1 -1
- package/.next/static/chunks/{38301ji-85i30.js → 3nnzdl_te20hv.js} +1 -1
- package/.next/static/chunks/{2m267diam_vib.js → 3tz4dr0_8pq1-.js} +1 -1
- package/.next/static/chunks/{2gocl3dq0_h1s.js → 3x6pggb-2kyrk.js} +1 -1
- package/.next/static/chunks/{025bmrgeukcb2.js → 4440mws9jz3mt.js} +1 -1
- package/README.md +4 -0
- package/app/api/thursday/tool-call/route.ts +1 -15
- package/components/ui/radio-group.tsx +2 -2
- package/docs/images/demo-still.jpg +0 -0
- package/docs/live-calls.md +19 -11
- package/features/ai/prompts/live.prompt.ts +51 -26
- package/features/ai/prompts/prompt-helper.ts +1 -9
- package/features/ai/prompts/thursday.prompt.ts +8 -2
- package/features/bot/bot.run.ts +9 -3
- package/features/bot/bot.seed.ts +35 -91
- package/features/bot/components/bot-room.tsx +0 -6
- package/features/bot/components/bot-setting.tsx +4 -4
- package/features/bot/components/room-pill.tsx +4 -2
- package/features/bot/seed-bots.ts +2 -2
- package/features/intro/components/intro.tsx +2 -3
- package/features/reach/reach.ts +2 -2
- package/features/routine/components/routine-setting.tsx +1 -1
- package/features/signins/signins.query.ts +66 -1
- package/features/thursday/components/side-captions.tsx +31 -8
- package/features/thursday/components/thursday-setting.tsx +5 -5
- package/features/thursday/components/thursday.tsx +97 -39
- package/features/thursday/components/write-line.tsx +28 -17
- package/features/thursday/thursday.action.ts +4 -20
- package/features/thursday/thursday.schema.ts +0 -9
- package/features/thursday/thursday.store.ts +11 -15
- package/features/thursday/thursday.text.ts +10 -8
- package/features/thursday/tool-line.ts +17 -6
- package/features/thursday/use-call-ring.ts +1 -5
- package/features/thursday/use-text-call.ts +37 -21
- package/features/thursday/use-thursday.ts +14 -44
- package/features/workspace/workspace.ts +4 -2
- package/guide/bots.md +13 -3
- package/guide/calls.md +7 -5
- package/knip.json +1 -0
- package/lib/live/live.session.ts +0 -39
- package/package.json +1 -1
- package/scripts/live-session.test.mts +19 -26
- package/seed-skills/analyst/README.md +18 -0
- package/seed-skills/analyst/data-report/SKILL.md +60 -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/fetch.mjs +521 -0
- package/seed-skills/insta/instagram/SKILL.md +36 -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 +333 -0
- package/seed-skills/insta/instagram/scripts/inbox.mjs +163 -0
- package/seed-skills/insta/instagram/scripts/lib.mjs +33 -0
- package/seed-skills/insta/instagram/scripts/media.mjs +104 -0
- package/seed-skills/insta/instagram/scripts/profile.mjs +143 -0
- package/seed-skills/insta/instagram/scripts/read.mjs +244 -0
- package/seed-skills/insta/instagram/scripts/search.mjs +137 -0
- package/seed-skills/insta/instagram/scripts/selectors.mjs +56 -0
- package/seed-skills/insta/instagram/scripts/sheet.mjs +43 -0
- package/seed-skills/insta/instagram/templates/carousel.html +62 -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/seed-skills/tutor/picture-book/SKILL.md +16 -0
- package/seed-skills/tutor/picture-book/book/book.css +181 -0
- package/seed-skills/tutor/picture-book/book/book.html +33 -0
- package/seed-skills/tutor/picture-book/book/book.js +60 -0
- package/seed-skills/tutor/picture-book/references/picture-book.md +104 -0
- package/seed-skills/tutor/picture-book/scripts/book-video.mjs +177 -0
- package/seed-skills/tutor/picture-book/scripts/book.mjs +98 -0
- package/skills/browser/SKILL.md +13 -4
- package/skills/browser/references/scripts.md +55 -0
- package/skills/browser/scripts/image-size.mjs +30 -0
- package/skills/browser/scripts/render.mjs +126 -0
- package/skills/browser/scripts/session.mjs +105 -0
- package/skills/browser/scripts/sheet.mjs +97 -0
- package/skills/browser/scripts/webimage.mjs +105 -0
- package/skills/interactive-page/SKILL.md +4 -3
- package/skills/interactive-page/scripts/chart.mjs +679 -0
- package/skills/interactive-page/scripts/page.mjs +1 -0
- package/.next/server/app/api/probe/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/probe/route/build-manifest.json +0 -12
- package/.next/server/app/api/probe/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/probe/route.js +0 -10
- package/.next/server/app/api/probe/route.js.map +0 -5
- package/.next/server/app/api/probe/route.js.nft.json +0 -1
- package/.next/server/app/api/probe/route_client-reference-manifest.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__012s2o8._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__0z0hc1f._.js +0 -4
- package/.next/server/chunks/[root-of-the-server]__1v7-zke._.js +0 -5
- package/.next/server/chunks/_0whlpyi._.js +0 -4
- package/.next/server/chunks/_next-internal_server_app_api_probe_route_actions_0cqd30j.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__0oo-wlv._.js +0 -103
- package/.next/server/chunks/ssr/[root-of-the-server]__0zuns7e._.js +0 -3
- package/.next/server/chunks/ssr/_070nv1_._.js +0 -3
- package/.next/server/chunks/ssr/_0dshsh4._.js +0 -3
- package/.next/server/chunks/ssr/_0l_mcfz._.js +0 -3
- package/.next/server/chunks/ssr/_0u5xv6g._.js +0 -3
- package/.next/server/chunks/ssr/_157yfrk._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1nr94ks._.js +0 -3
- package/.next/static/chunks/06nt9tatr-dug.js +0 -1
- package/.next/static/chunks/0ge-hx0ed83a1.js +0 -1
- package/.next/static/chunks/0pda_vf-abwvx.js +0 -1
- package/.next/static/chunks/0rowp9rx3qbxu.js +0 -18
- package/.next/static/chunks/1grkt5gr8pbg1.js +0 -17
- package/.next/static/chunks/1yugpvvi8qrbu.js +0 -101
- package/.next/static/chunks/27lkanxvtdn09.js +0 -1
- package/.next/static/chunks/3fec-aeteddr-.js +0 -1
- package/.next/static/chunks/3go3hwp1z2_h2.css +0 -1
- package/.next/static/chunks/3h2bun3hcggg8.js +0 -1
- package/.next/static/chunks/3j1y335o95mj7.js +0 -1
- package/lib/probe.server.ts +0 -17
- package/lib/probe.ts +0 -55
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_buildManifest.js +0 -0
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → En1vUPazxcYjPjnnahcN5}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* How one account posts: its size, its latest posts (type, slides, ratio, how they
|
|
4
|
+
* did, caption shape), and the pattern across them — read from the data the
|
|
5
|
+
* profile page itself fetches, not from the screen.
|
|
6
|
+
*
|
|
7
|
+
* node profile.mjs <handle | profile url> [--posts 12] [--out posts.json] [--sheet covers.png]
|
|
8
|
+
*/
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { shipped } from "./lib.mjs";
|
|
11
|
+
import { media, patterns, postLine, short } from "./media.mjs";
|
|
12
|
+
|
|
13
|
+
const { fail, inPage, orFail, parseArgs } = await shipped(
|
|
14
|
+
"browser/scripts/session.mjs",
|
|
15
|
+
);
|
|
16
|
+
const { makeSheet } = await shipped("browser/scripts/sheet.mjs");
|
|
17
|
+
|
|
18
|
+
const opts = parseArgs();
|
|
19
|
+
const handle = String(opts._[0] ?? "")
|
|
20
|
+
.replace(/^https?:\/\/(www\.)?instagram\.com\//, "")
|
|
21
|
+
.replace(/^@/, "")
|
|
22
|
+
.split(/[/?]/)[0];
|
|
23
|
+
if (!handle)
|
|
24
|
+
fail(
|
|
25
|
+
"usage: node profile.mjs <handle> [--posts 12] [--out posts.json] [--sheet covers.png]",
|
|
26
|
+
);
|
|
27
|
+
const want = Number(opts.posts ?? 12);
|
|
28
|
+
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
const got = orFail(
|
|
31
|
+
await inPage(
|
|
32
|
+
async (page, { handle, want }, { media }) => {
|
|
33
|
+
const posts = [];
|
|
34
|
+
let profile = null;
|
|
35
|
+
let more = true;
|
|
36
|
+
const first = (text) => JSON.parse(text.split("\n")[0]);
|
|
37
|
+
const listen = async (res) => {
|
|
38
|
+
if (!/\/graphql/.test(res.url())) return;
|
|
39
|
+
let text;
|
|
40
|
+
try {
|
|
41
|
+
text = await res.text();
|
|
42
|
+
} catch {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
if (
|
|
47
|
+
text.includes("xdt_api__v1__feed__user_timeline_graphql_connection")
|
|
48
|
+
) {
|
|
49
|
+
const c =
|
|
50
|
+
first(text).data
|
|
51
|
+
?.xdt_api__v1__feed__user_timeline_graphql_connection;
|
|
52
|
+
for (const e of c?.edges ?? []) posts.push(media(e.node));
|
|
53
|
+
more = Boolean(c?.page_info?.has_next_page);
|
|
54
|
+
} else if (!profile && text.includes('"follower_count"')) {
|
|
55
|
+
const u = first(text).data?.user;
|
|
56
|
+
if (u?.username?.toLowerCase() === handle.toLowerCase())
|
|
57
|
+
profile = {
|
|
58
|
+
handle: u.username,
|
|
59
|
+
name: u.full_name,
|
|
60
|
+
followers: u.follower_count,
|
|
61
|
+
following: u.following_count ?? null,
|
|
62
|
+
posts: u.media_count ?? null,
|
|
63
|
+
verified: u.is_verified,
|
|
64
|
+
private: u.is_private,
|
|
65
|
+
category: u.category ?? null,
|
|
66
|
+
bio: u.biography ?? "",
|
|
67
|
+
link: u.bio_links?.[0]?.url ?? u.external_url ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
};
|
|
72
|
+
page.on("response", listen);
|
|
73
|
+
try {
|
|
74
|
+
await page.goto(`https://www.instagram.com/${handle}/`, {
|
|
75
|
+
waitUntil: "domcontentloaded",
|
|
76
|
+
});
|
|
77
|
+
for (let i = 0; i < 40 && !(profile && posts.length); i++)
|
|
78
|
+
await page.waitForTimeout(250);
|
|
79
|
+
for (
|
|
80
|
+
let tries = 0;
|
|
81
|
+
posts.length < want && more && tries < 12;
|
|
82
|
+
tries++
|
|
83
|
+
) {
|
|
84
|
+
const had = posts.length;
|
|
85
|
+
await page.evaluate(() =>
|
|
86
|
+
window.scrollTo(0, document.body.scrollHeight),
|
|
87
|
+
);
|
|
88
|
+
for (let i = 0; i < 24 && posts.length === had; i++)
|
|
89
|
+
await page.waitForTimeout(250);
|
|
90
|
+
if (posts.length === had) break;
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
page.off("response", listen);
|
|
94
|
+
}
|
|
95
|
+
const path = page.url().replace(/^https?:\/\/[^/]+/, "");
|
|
96
|
+
if (/\/accounts\/login|\/challenge\//.test(path))
|
|
97
|
+
return {
|
|
98
|
+
error: `Not signed in (${path}). Borrow the kept sign-in first.`,
|
|
99
|
+
};
|
|
100
|
+
if (!profile && !posts.length)
|
|
101
|
+
return {
|
|
102
|
+
error: `Nothing came back for @${handle}: no such account, a private one, or the page changed (it no longer fetches xdt_api__v1__feed__user_timeline_graphql_connection). Open it and look.`,
|
|
103
|
+
};
|
|
104
|
+
const seen = new Set();
|
|
105
|
+
return {
|
|
106
|
+
profile,
|
|
107
|
+
// Newest first, the pinned ones after: the grid puts old pinned posts on top
|
|
108
|
+
posts: posts
|
|
109
|
+
.filter((p) => !seen.has(p.url) && seen.add(p.url))
|
|
110
|
+
.sort((a, b) => a.pinned - b.pinned || b.at - a.at)
|
|
111
|
+
.slice(0, want),
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
{ handle, want },
|
|
115
|
+
{ media },
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const { profile, posts } = got;
|
|
120
|
+
if (opts.out)
|
|
121
|
+
writeFileSync(opts.out, JSON.stringify({ profile, posts }, null, 1));
|
|
122
|
+
if (profile)
|
|
123
|
+
console.log(
|
|
124
|
+
`@${profile.handle} (${profile.name}) — ${short(profile.followers)} followers, ${profile.posts ?? "?"} posts${profile.verified ? ", verified" : ""}${profile.category ? `, ${profile.category}` : ""}\n` +
|
|
125
|
+
`bio: ${profile.bio.replace(/\s+/g, " ").slice(0, 160)}`,
|
|
126
|
+
);
|
|
127
|
+
posts.forEach((p, i) => {
|
|
128
|
+
console.log(postLine(p, i));
|
|
129
|
+
});
|
|
130
|
+
for (const line of patterns(posts)) console.log(`pattern: ${line}`);
|
|
131
|
+
if (opts.sheet) {
|
|
132
|
+
const made = await makeSheet(
|
|
133
|
+
posts.map((p, i) => ({
|
|
134
|
+
src: p.cover,
|
|
135
|
+
label: `#${i + 1} ${p.type}${p.type === "carousel" ? `×${p.slides}` : ""} ${p.ratio ?? ""} likes ${short(p.likes)}`,
|
|
136
|
+
})),
|
|
137
|
+
opts.sheet,
|
|
138
|
+
);
|
|
139
|
+
console.log(`covers: ${made.out}`);
|
|
140
|
+
}
|
|
141
|
+
console.log(
|
|
142
|
+
`${posts.length} posts in ${((Date.now() - started) / 1000).toFixed(1)}s${opts.out ? ` → ${opts.out} (full captions, every slide's picture url)` : ""}`,
|
|
143
|
+
);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reads one DM thread into a JSONL file, oldest first, by walking its message pane
|
|
4
|
+
* up from the newest message. Opening a thread marks it seen.
|
|
5
|
+
*
|
|
6
|
+
* node read.mjs <thread id | thread url> --out <file.jsonl>
|
|
7
|
+
* [--since 2026-09-01] [--max 200] [--tail 10]
|
|
8
|
+
*
|
|
9
|
+
* Stops at whichever comes first: --max messages, a message older than --since,
|
|
10
|
+
* the top of the thread, or two rounds that brought nothing new.
|
|
11
|
+
*/
|
|
12
|
+
import { writeFileSync } from "node:fs";
|
|
13
|
+
import { oneLine, shipped, stamp } from "./lib.mjs";
|
|
14
|
+
import { SEL } from "./selectors.mjs";
|
|
15
|
+
|
|
16
|
+
const { fail, inPage, orFail, parseArgs } = await shipped(
|
|
17
|
+
"browser/scripts/session.mjs",
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
const opts = parseArgs();
|
|
21
|
+
const target = opts._[0];
|
|
22
|
+
if (!target || !opts.out)
|
|
23
|
+
fail(
|
|
24
|
+
"usage: node read.mjs <thread id | url> --out <file.jsonl> [--since YYYY-MM-DD] [--max 200] [--tail 10]",
|
|
25
|
+
);
|
|
26
|
+
const id = String(target).match(/(\d{6,})/)?.[1];
|
|
27
|
+
if (!id) fail(`Not a thread id or url: ${target}`);
|
|
28
|
+
const url = `https://www.instagram.com/direct/t/${id}/`;
|
|
29
|
+
const max = Number(opts.max ?? 200);
|
|
30
|
+
const since = opts.since ? new Date(opts.since).getTime() : null;
|
|
31
|
+
if (opts.since && Number.isNaN(since))
|
|
32
|
+
fail(`--since is not a date: ${opts.since}`);
|
|
33
|
+
const tail = Number(opts.tail ?? 10);
|
|
34
|
+
/** Each round is one browser call of a few screens; this caps a runaway thread. */
|
|
35
|
+
const ROUNDS = 60;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One round: a few screens of the pane, each read and then moved up by most of a
|
|
39
|
+
* screen. The pane unmounts rows that leave the screen, so it is walked, never
|
|
40
|
+
* jumped to the top: a jump skips everything in between.
|
|
41
|
+
*/
|
|
42
|
+
async function round(page, { SEL, url, first, steps }) {
|
|
43
|
+
const extract = (SEL) => {
|
|
44
|
+
if (/\/accounts\/login|\/challenge\//.test(location.pathname))
|
|
45
|
+
return { error: `Not signed in (${location.pathname}).` };
|
|
46
|
+
const pane = [...document.querySelectorAll("main *")].find(
|
|
47
|
+
(e) =>
|
|
48
|
+
e.clientHeight > 200 &&
|
|
49
|
+
!e.closest(SEL.inboxList) &&
|
|
50
|
+
/(auto|scroll)/.test(getComputedStyle(e).overflowY) &&
|
|
51
|
+
e.querySelector(SEL.messageRow),
|
|
52
|
+
);
|
|
53
|
+
if (!pane)
|
|
54
|
+
return {
|
|
55
|
+
error: document.querySelector("main")
|
|
56
|
+
? "Selectors changed: no message pane (SEL.messageRow). Snapshot the thread once and fix selectors.mjs."
|
|
57
|
+
: "No thread on the page: something covers it (a dialog, a daily time-limit screen). Snapshot, close it, run again.",
|
|
58
|
+
};
|
|
59
|
+
pane.setAttribute("data-read-pane", "");
|
|
60
|
+
const fiberOf = (el) =>
|
|
61
|
+
el[Object.keys(el).find((k) => k.startsWith("__reactFiber"))];
|
|
62
|
+
const findProp = (el, name) => {
|
|
63
|
+
for (let f = fiberOf(el), d = 0; f && d < 40; f = f.return, d++) {
|
|
64
|
+
const p = f.memoizedProps;
|
|
65
|
+
if (p && typeof p === "object" && p[name] != null) return p[name];
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
};
|
|
69
|
+
const paneBox = pane.getBoundingClientRect();
|
|
70
|
+
const messages = [];
|
|
71
|
+
for (const row of pane.querySelectorAll(SEL.messageRow)) {
|
|
72
|
+
const body = row.querySelector(SEL.messageBody);
|
|
73
|
+
if (!body) continue; // a date line, a "seen" line
|
|
74
|
+
const ref = findProp(row, SEL.messageRefProp);
|
|
75
|
+
const deep =
|
|
76
|
+
body.querySelector("[dir=auto], img, video, a") ??
|
|
77
|
+
body.firstElementChild ??
|
|
78
|
+
body;
|
|
79
|
+
let outgoing = findProp(deep, SEL.outgoingProp);
|
|
80
|
+
if (typeof outgoing !== "boolean") {
|
|
81
|
+
// Heuristic: no flag on the row, so a bubble nearer the right edge is the account's own
|
|
82
|
+
const box = body.getBoundingClientRect();
|
|
83
|
+
outgoing = box.left - paneBox.left > paneBox.right - box.right;
|
|
84
|
+
}
|
|
85
|
+
messages.push({
|
|
86
|
+
id: ref?.id ?? null,
|
|
87
|
+
ts: ref?.timestamp_ms ? Number(ref.timestamp_ms) : null,
|
|
88
|
+
from: outgoing ? "me" : "them",
|
|
89
|
+
sender: ref?.sender_fbid ?? null,
|
|
90
|
+
text: body.innerText.trim(),
|
|
91
|
+
images: [...body.querySelectorAll("img")]
|
|
92
|
+
.filter((i) => i.getBoundingClientRect().width >= SEL.minImagePx)
|
|
93
|
+
.map((i) => i.currentSrc || i.src),
|
|
94
|
+
video: body.querySelector("video") !== null,
|
|
95
|
+
links: [
|
|
96
|
+
...new Set(
|
|
97
|
+
[...body.querySelectorAll("a[href]")]
|
|
98
|
+
.map((a) => a.href)
|
|
99
|
+
.filter((h) => h.startsWith("http")),
|
|
100
|
+
),
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// A column-reverse pane counts scrollTop from the bottom, as zero and below
|
|
105
|
+
const reversed = getComputedStyle(pane).flexDirection === "column-reverse";
|
|
106
|
+
const atTop = reversed
|
|
107
|
+
? Math.abs(pane.scrollTop) + pane.clientHeight >= pane.scrollHeight - 2
|
|
108
|
+
: pane.scrollTop <= 1;
|
|
109
|
+
return { messages, atTop, height: pane.scrollHeight };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
if (first) {
|
|
113
|
+
if (!page.url().startsWith(url)) {
|
|
114
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
115
|
+
await page
|
|
116
|
+
.waitForFunction(
|
|
117
|
+
(SEL) => document.querySelector(`main ${SEL.messageBody}`),
|
|
118
|
+
SEL,
|
|
119
|
+
{ timeout: 20000 },
|
|
120
|
+
)
|
|
121
|
+
.catch(() => {});
|
|
122
|
+
}
|
|
123
|
+
await page.waitForTimeout(800);
|
|
124
|
+
const found = await page.evaluate(extract, SEL);
|
|
125
|
+
if (found.error) return found;
|
|
126
|
+
// Start from the newest message, wherever an earlier look left the pane
|
|
127
|
+
await page.evaluate(() => {
|
|
128
|
+
const pane = document.querySelector("[data-read-pane]");
|
|
129
|
+
pane.scrollTop =
|
|
130
|
+
getComputedStyle(pane).flexDirection === "column-reverse"
|
|
131
|
+
? 0
|
|
132
|
+
: pane.scrollHeight;
|
|
133
|
+
});
|
|
134
|
+
await page.waitForTimeout(500);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const messages = [];
|
|
138
|
+
let moved = Number.POSITIVE_INFINITY;
|
|
139
|
+
for (let s = 0; s < steps; s++) {
|
|
140
|
+
const seen = await page.evaluate(extract, SEL);
|
|
141
|
+
if (seen.error) return seen;
|
|
142
|
+
messages.push(...seen.messages);
|
|
143
|
+
// At the top, or held in place while older rows load: wait for the pane to grow
|
|
144
|
+
if (seen.atTop || moved < 10) {
|
|
145
|
+
const grew = await page
|
|
146
|
+
.waitForFunction(
|
|
147
|
+
(height) =>
|
|
148
|
+
(document.querySelector("[data-read-pane]")?.scrollHeight ?? 0) >
|
|
149
|
+
height,
|
|
150
|
+
seen.height,
|
|
151
|
+
{ timeout: 5000 },
|
|
152
|
+
)
|
|
153
|
+
.then(() => true)
|
|
154
|
+
.catch(() => false);
|
|
155
|
+
if (!grew && seen.atTop) return { messages, top: true };
|
|
156
|
+
await page.waitForTimeout(300);
|
|
157
|
+
}
|
|
158
|
+
moved = await page.evaluate(() => {
|
|
159
|
+
const pane = document.querySelector("[data-read-pane]");
|
|
160
|
+
const before = pane.scrollTop;
|
|
161
|
+
pane.scrollTop -= pane.clientHeight * 0.8;
|
|
162
|
+
return Math.abs(pane.scrollTop - before);
|
|
163
|
+
});
|
|
164
|
+
await page.waitForTimeout(350);
|
|
165
|
+
}
|
|
166
|
+
return { messages, top: false };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const started = Date.now();
|
|
170
|
+
const byKey = new Map();
|
|
171
|
+
let fallbackKeys = 0;
|
|
172
|
+
let stop = `the round limit (${ROUNDS})`;
|
|
173
|
+
let quiet = 0;
|
|
174
|
+
for (let i = 0; i < ROUNDS; i++) {
|
|
175
|
+
const r = orFail(await inPage(round, { SEL, url, first: i === 0, steps: 5 }));
|
|
176
|
+
if (i === 0 && r.messages.length === 0)
|
|
177
|
+
fail(
|
|
178
|
+
"Selectors changed: the pane has rows but no message was read (SEL.messageBody). Fix selectors.mjs.",
|
|
179
|
+
);
|
|
180
|
+
let fresh = 0;
|
|
181
|
+
for (const m of r.messages) {
|
|
182
|
+
// Heuristic when the row carries no message id: side + time + text stand in for one
|
|
183
|
+
const key =
|
|
184
|
+
m.id ?? `${m.from}|${m.ts}|${m.text.slice(0, 120)}|${m.images[0] ?? ""}`;
|
|
185
|
+
const known = byKey.get(key);
|
|
186
|
+
if (known) {
|
|
187
|
+
// A picture that had not loaded on the first sighting may have by now
|
|
188
|
+
known.images = [...new Set([...known.images, ...m.images])];
|
|
189
|
+
known.links = [...new Set([...known.links, ...m.links])];
|
|
190
|
+
if (m.text.length > known.text.length) known.text = m.text;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!m.id) fallbackKeys++;
|
|
194
|
+
byKey.set(key, m);
|
|
195
|
+
fresh++;
|
|
196
|
+
}
|
|
197
|
+
quiet = fresh ? 0 : quiet + 1;
|
|
198
|
+
const oldest = Math.min(
|
|
199
|
+
...[...byKey.values()].map((m) => m.ts ?? Number.POSITIVE_INFINITY),
|
|
200
|
+
);
|
|
201
|
+
if (byKey.size >= max) {
|
|
202
|
+
stop = `--max ${max}`;
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
if (since && oldest < since) {
|
|
206
|
+
stop = `--since ${opts.since}`;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
if (r.top) {
|
|
210
|
+
stop = "the top of the thread";
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
if (quiet >= 2) {
|
|
214
|
+
stop = "two rounds with nothing new";
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let messages = [...byKey.values()].sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
220
|
+
if (since) messages = messages.filter((m) => m.ts == null || m.ts >= since);
|
|
221
|
+
messages = messages.slice(-max);
|
|
222
|
+
writeFileSync(
|
|
223
|
+
opts.out,
|
|
224
|
+
messages
|
|
225
|
+
.map((m) => JSON.stringify({ ...m, at: m.ts ? stamp(m.ts) : null }))
|
|
226
|
+
.join("\n") + (messages.length ? "\n" : ""),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const count = (f) => messages.filter(f).length;
|
|
230
|
+
console.log(
|
|
231
|
+
`${messages.length} messages (${count((m) => m.from === "me")} from me, ${count((m) => m.from === "them")} from them), ` +
|
|
232
|
+
`${messages.length ? `${stamp(messages[0].ts)} to ${stamp(messages.at(-1).ts)}` : "none"}; ` +
|
|
233
|
+
`${count((m) => m.images.length)} with pictures, ${count((m) => m.links.length)} with links. ` +
|
|
234
|
+
`Stopped at ${stop}. ${((Date.now() - started) / 1000).toFixed(1)}s → ${opts.out}`,
|
|
235
|
+
);
|
|
236
|
+
if (fallbackKeys)
|
|
237
|
+
console.log(
|
|
238
|
+
`${fallbackKeys} rows had no message id; they were told apart by side, time and text.`,
|
|
239
|
+
);
|
|
240
|
+
for (const m of tail > 0 ? messages.slice(-tail) : [])
|
|
241
|
+
console.log(
|
|
242
|
+
`${stamp(m.ts)} ${m.from}: ${oneLine(m.text, 160) || "(no text)"}` +
|
|
243
|
+
`${m.images.length ? ` [${m.images.length} picture]` : ""}${m.links.length ? ` [${m.links[0]}]` : ""}`,
|
|
244
|
+
);
|
|
@@ -0,0 +1,137 @@
|
|
|
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 { shipped } from "./lib.mjs";
|
|
11
|
+
import { media, postLine, short } from "./media.mjs";
|
|
12
|
+
|
|
13
|
+
const { fail, inPage, orFail, parseArgs } = await shipped(
|
|
14
|
+
"browser/scripts/session.mjs",
|
|
15
|
+
);
|
|
16
|
+
const { makeSheet } = await shipped("browser/scripts/sheet.mjs");
|
|
17
|
+
|
|
18
|
+
const opts = parseArgs();
|
|
19
|
+
const topic = opts._[0];
|
|
20
|
+
if (!topic)
|
|
21
|
+
fail(
|
|
22
|
+
'usage: node search.mjs "<topic>" [--type reel|carousel|image] [--max 24] [--out found.json] [--sheet covers.png]',
|
|
23
|
+
);
|
|
24
|
+
const max = Number(opts.max ?? 24);
|
|
25
|
+
const type = opts.type ?? null;
|
|
26
|
+
if (type && !["reel", "carousel", "image"].includes(type))
|
|
27
|
+
fail("--type is reel, carousel or image");
|
|
28
|
+
|
|
29
|
+
const started = Date.now();
|
|
30
|
+
const got = orFail(
|
|
31
|
+
await inPage(
|
|
32
|
+
async (page, { topic, max, type }, { media }) => {
|
|
33
|
+
const found = [];
|
|
34
|
+
let more = true;
|
|
35
|
+
let pages = 0;
|
|
36
|
+
const listen = async (res) => {
|
|
37
|
+
if (!/\/graphql/.test(res.url())) return;
|
|
38
|
+
let text;
|
|
39
|
+
try {
|
|
40
|
+
text = await res.text();
|
|
41
|
+
} catch {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!text.includes("xdt_fbsearch__top_serp_graphql")) return;
|
|
45
|
+
try {
|
|
46
|
+
const c = JSON.parse(text.split("\n")[0]).data
|
|
47
|
+
?.xdt_fbsearch__top_serp_graphql;
|
|
48
|
+
pages++;
|
|
49
|
+
more = Boolean(c?.page_info?.has_next_page);
|
|
50
|
+
const walk = (o) => {
|
|
51
|
+
if (!o || typeof o !== "object") return;
|
|
52
|
+
if (typeof o.code === "string" && "media_type" in o) {
|
|
53
|
+
found.push(media(o));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const v of Object.values(o)) walk(v);
|
|
57
|
+
};
|
|
58
|
+
walk(c?.edges);
|
|
59
|
+
} catch {}
|
|
60
|
+
};
|
|
61
|
+
const kept = () => found.filter((p) => !type || p.type === type).length;
|
|
62
|
+
page.on("response", listen);
|
|
63
|
+
try {
|
|
64
|
+
await page.goto(
|
|
65
|
+
`https://www.instagram.com/explore/search/keyword/?q=${encodeURIComponent(topic)}`,
|
|
66
|
+
{ waitUntil: "domcontentloaded" },
|
|
67
|
+
);
|
|
68
|
+
for (let i = 0; i < 60 && !pages; i++) await page.waitForTimeout(250);
|
|
69
|
+
for (let tries = 0; kept() < max && more && tries < 8; tries++) {
|
|
70
|
+
const had = pages;
|
|
71
|
+
await page.evaluate(() =>
|
|
72
|
+
window.scrollTo(0, document.body.scrollHeight),
|
|
73
|
+
);
|
|
74
|
+
for (let i = 0; i < 32 && pages === had; i++)
|
|
75
|
+
await page.waitForTimeout(250);
|
|
76
|
+
if (pages === had) break;
|
|
77
|
+
}
|
|
78
|
+
} finally {
|
|
79
|
+
page.off("response", listen);
|
|
80
|
+
}
|
|
81
|
+
const path = page.url().replace(/^https?:\/\/[^/]+/, "");
|
|
82
|
+
if (/\/accounts\/login|\/challenge\//.test(path))
|
|
83
|
+
return {
|
|
84
|
+
error: `Not signed in (${path}). Borrow the kept sign-in first.`,
|
|
85
|
+
};
|
|
86
|
+
if (!pages)
|
|
87
|
+
return {
|
|
88
|
+
error:
|
|
89
|
+
"The search page fetched nothing this script knows (xdt_fbsearch__top_serp_graphql): the page changed, or something covers it. Open it and look.",
|
|
90
|
+
};
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
return {
|
|
93
|
+
posts: found
|
|
94
|
+
.filter((p) => !seen.has(p.url) && seen.add(p.url))
|
|
95
|
+
.filter((p) => !type || p.type === type)
|
|
96
|
+
.slice(0, max),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
{ topic, max, type },
|
|
100
|
+
{ media },
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const { posts } = got;
|
|
105
|
+
if (opts.out)
|
|
106
|
+
writeFileSync(opts.out, JSON.stringify({ topic, posts }, null, 1));
|
|
107
|
+
posts.forEach((p, i) => {
|
|
108
|
+
console.log(postLine(p, i));
|
|
109
|
+
});
|
|
110
|
+
// The accounts behind the results, by the likes they drew here
|
|
111
|
+
const accounts = Object.values(
|
|
112
|
+
posts.reduce((all, p) => {
|
|
113
|
+
const a = (all[p.owner] ??= { owner: p.owner, posts: 0, likes: 0 });
|
|
114
|
+
a.posts++;
|
|
115
|
+
a.likes += p.likes ?? 0;
|
|
116
|
+
return all;
|
|
117
|
+
}, {}),
|
|
118
|
+
).sort((a, b) => b.likes - a.likes);
|
|
119
|
+
console.log(
|
|
120
|
+
`accounts: ${accounts
|
|
121
|
+
.slice(0, 10)
|
|
122
|
+
.map((a) => `@${a.owner} (${a.posts}, likes ${short(a.likes)})`)
|
|
123
|
+
.join(", ")}`,
|
|
124
|
+
);
|
|
125
|
+
if (opts.sheet) {
|
|
126
|
+
const made = await makeSheet(
|
|
127
|
+
posts.map((p, i) => ({
|
|
128
|
+
src: p.cover,
|
|
129
|
+
label: `#${i + 1} ${p.type}${p.type === "carousel" ? `×${p.slides}` : ""} likes ${short(p.likes)} @${p.owner}`,
|
|
130
|
+
})),
|
|
131
|
+
opts.sheet,
|
|
132
|
+
);
|
|
133
|
+
console.log(`covers: ${made.out}`);
|
|
134
|
+
}
|
|
135
|
+
console.log(
|
|
136
|
+
`${posts.length} results for "${topic}"${type ? ` (${type})` : ""} in ${((Date.now() - started) / 1000).toFixed(1)}s${opts.out ? ` → ${opts.out}` : ""}`,
|
|
137
|
+
);
|
|
@@ -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,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A feed as one image, each picture with a label under it: the covers of every post
|
|
4
|
+
* `profile.mjs` or `search.mjs` saved, or every slide of one of them. Any other
|
|
5
|
+
* pictures go straight to the browser skill's sheet.
|
|
6
|
+
*
|
|
7
|
+
* node sheet.mjs --out sheet.png <posts.json> covers of every post
|
|
8
|
+
* node sheet.mjs --out sheet.png <posts.json> --post 3 every slide of post 3
|
|
9
|
+
* node sheet.mjs --out sheet.png <image url | file>... any pictures
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { shipped } from "./lib.mjs";
|
|
13
|
+
import { short } from "./media.mjs";
|
|
14
|
+
|
|
15
|
+
const { fail, parseArgs } = await shipped("browser/scripts/session.mjs");
|
|
16
|
+
const { makeSheet, pictures } = await shipped("browser/scripts/sheet.mjs");
|
|
17
|
+
|
|
18
|
+
const opts = parseArgs();
|
|
19
|
+
if (!opts.out || !opts._.length)
|
|
20
|
+
fail(
|
|
21
|
+
"usage: node sheet.mjs --out sheet.png <posts.json> [--post N] | <image url | file>...",
|
|
22
|
+
);
|
|
23
|
+
let items;
|
|
24
|
+
if (opts._[0].endsWith(".json")) {
|
|
25
|
+
const saved = JSON.parse(readFileSync(opts._[0], "utf8"));
|
|
26
|
+
const posts = saved.posts ?? saved;
|
|
27
|
+
if (opts.post) {
|
|
28
|
+
const p = posts[Number(opts.post) - 1];
|
|
29
|
+
if (!p) fail(`No post ${opts.post}; there are ${posts.length}.`);
|
|
30
|
+
items = p.images.map((src, i) => ({
|
|
31
|
+
src,
|
|
32
|
+
label: `${i + 1}/${p.images.length} ${p.ratio ?? ""}`,
|
|
33
|
+
}));
|
|
34
|
+
} else
|
|
35
|
+
items = posts.map((p, i) => ({
|
|
36
|
+
src: p.cover,
|
|
37
|
+
label: `#${i + 1} ${p.type}${p.type === "carousel" ? `×${p.slides}` : ""} ${p.ratio ?? ""} likes ${short(p.likes)}${p.owner ? ` @${p.owner}` : ""}`,
|
|
38
|
+
}));
|
|
39
|
+
} else items = pictures(opts._);
|
|
40
|
+
const made = await makeSheet(items, opts.out, Number(opts.cols ?? 4));
|
|
41
|
+
console.log(
|
|
42
|
+
`${made.out} — ${items.length} pictures${made.missing ? `, ${made.missing} did not load` : ""}`,
|
|
43
|
+
);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
A carousel: every <section data-slide> is one PNG (the browser skill's 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>
|