dirpush 0.0.0 → 0.1.1
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/.agents/skills/dirpush/SKILL.md +92 -0
- package/.agents/skills/dirpush/references/assets.md +48 -0
- package/.agents/skills/dirpush/references/authentication.md +59 -0
- package/.agents/skills/dirpush/references/recovery.md +34 -0
- package/.agents/skills/dirpush/references/submissions.md +43 -0
- package/LICENSE +21 -0
- package/README.md +64 -5
- package/THIRD_PARTY_NOTICES.md +10 -0
- package/bin/dirpush.cjs +13 -0
- package/drizzle/0000_init.sql +59 -0
- package/drizzle/0001_jazzy_masked_marvel.sql +9 -0
- package/drizzle/0002_loose_may_parker.sql +9 -0
- package/drizzle/0003_modern_lilandra.sql +6 -0
- package/drizzle/0004_deep_master_chief.sql +11 -0
- package/drizzle/meta/0000_snapshot.json +404 -0
- package/drizzle/meta/0001_snapshot.json +466 -0
- package/drizzle/meta/0002_snapshot.json +527 -0
- package/drizzle/meta/0003_snapshot.json +566 -0
- package/drizzle/meta/0004_snapshot.json +642 -0
- package/drizzle/meta/_journal.json +41 -0
- package/examples/workspace/identity.yaml +6 -0
- package/examples/workspace/permissions.yaml +29 -0
- package/examples/workspace/product.yaml +29 -0
- package/package.json +61 -6
- package/registry/LICENSE +396 -0
- package/registry/directories/1000-tools/directory.yaml +55 -0
- package/registry/directories/1000-tools/playbook.md +5 -0
- package/registry/directories/10015-io/directory.yaml +66 -0
- package/registry/directories/10015-io/playbook.md +9 -0
- package/registry/directories/10words/directory.yaml +53 -0
- package/registry/directories/10words/playbook.md +8 -0
- package/registry/directories/600-tools/directory.yaml +48 -0
- package/registry/directories/600-tools/playbook.md +1 -0
- package/registry/directories/a1bookmarks/directory.yaml +42 -0
- package/registry/directories/aback-launch/directory.yaml +54 -0
- package/registry/directories/aback-launch/playbook.md +1 -0
- package/registry/directories/acid-tools/directory.yaml +51 -0
- package/registry/directories/acid-tools/playbook.md +8 -0
- package/registry/directories/agenthunter/directory.yaml +53 -0
- package/registry/directories/ai-agents-directory/directory.yaml +49 -0
- package/registry/directories/ai-agents-directory/playbook.md +6 -0
- package/registry/directories/ai-agents-list/directory.yaml +46 -0
- package/registry/directories/ai-agents-list/playbook.md +1 -0
- package/registry/directories/ai-agents-listing/directory.yaml +76 -0
- package/registry/directories/ai-directories/directory.yaml +45 -0
- package/registry/directories/ai-directories/playbook.md +7 -0
- package/registry/directories/ai-dust/directory.yaml +68 -0
- package/registry/directories/ai-dust/playbook.md +1 -0
- package/registry/directories/ai-nav/directory.yaml +54 -0
- package/registry/directories/ai-nav/playbook.md +1 -0
- package/registry/directories/ai-respo/directory.yaml +43 -0
- package/registry/directories/ai-respo/playbook.md +1 -0
- package/registry/directories/ai-superhub/directory.yaml +65 -0
- package/registry/directories/ai-tool-list/directory.yaml +44 -0
- package/registry/directories/ai-tool-list/playbook.md +1 -0
- package/registry/directories/ai-tool-saver/directory.yaml +52 -0
- package/registry/directories/ai-tool-saver/playbook.md +1 -0
- package/registry/directories/ai-toolex/directory.yaml +46 -0
- package/registry/directories/ai-toolex/playbook.md +1 -0
- package/registry/directories/ai-toolz-dir/directory.yaml +60 -0
- package/registry/directories/ai-website-directory/directory.yaml +66 -0
- package/registry/directories/ai-website-directory/playbook.md +1 -0
- package/registry/directories/ai-x-collection/directory.yaml +45 -0
- package/registry/directories/ai-x-collection/playbook.md +1 -0
- package/registry/directories/aiagentstore/directory.yaml +52 -0
- package/registry/directories/aiagentstore/playbook.md +1 -0
- package/registry/directories/aidirs-best/directory.yaml +46 -0
- package/registry/directories/aifindy/directory.yaml +44 -0
- package/registry/directories/aifindy/playbook.md +1 -0
- package/registry/directories/aihubs/directory.yaml +44 -0
- package/registry/directories/aihubs/playbook.md +5 -0
- package/registry/directories/aihuntlist/directory.yaml +43 -0
- package/registry/directories/aihuntlist/playbook.md +6 -0
- package/registry/directories/aihustle/directory.yaml +51 -0
- package/registry/directories/aihustle/playbook.md +5 -0
- package/registry/directories/ailaunch-space/directory.yaml +44 -0
- package/registry/directories/ailaunch-space/playbook.md +6 -0
- package/registry/directories/ailib/directory.yaml +64 -0
- package/registry/directories/ailib/playbook.md +1 -0
- package/registry/directories/aimatch-pro/directory.yaml +42 -0
- package/registry/directories/aimatch-pro/playbook.md +1 -0
- package/registry/directories/ainave/directory.yaml +71 -0
- package/registry/directories/ainave/playbook.md +10 -0
- package/registry/directories/aitechviral/directory.yaml +47 -0
- package/registry/directories/aitechviral/playbook.md +10 -0
- package/registry/directories/aitogrow/directory.yaml +52 -0
- package/registry/directories/aitogrow/playbook.md +6 -0
- package/registry/directories/aitoolhunt/directory.yaml +43 -0
- package/registry/directories/aitoolhunt/playbook.md +5 -0
- package/registry/directories/aitools-fyi/directory.yaml +52 -0
- package/registry/directories/aitools-inc/directory.yaml +48 -0
- package/registry/directories/aitools-inc/playbook.md +1 -0
- package/registry/directories/aitoolsdirectory/directory.yaml +73 -0
- package/registry/directories/aitoolsdirectory/playbook.md +8 -0
- package/registry/directories/aitoolshunt/directory.yaml +64 -0
- package/registry/directories/aitoolshunt/playbook.md +6 -0
- package/registry/directories/aitoolslist/directory.yaml +59 -0
- package/registry/directories/aitoolslist/playbook.md +6 -0
- package/registry/directories/aitoolsmagazine/directory.yaml +46 -0
- package/registry/directories/aitoolsmagazine/playbook.md +5 -0
- package/registry/directories/aitoolsmarketer/directory.yaml +56 -0
- package/registry/directories/aitoolsmarketer/playbook.md +7 -0
- package/registry/directories/aitooltrek/directory.yaml +47 -0
- package/registry/directories/aitooltrek/playbook.md +6 -0
- package/registry/directories/aitop10-tools/directory.yaml +44 -0
- package/registry/directories/aitop10-tools/playbook.md +5 -0
- package/registry/directories/aitoptools/directory.yaml +74 -0
- package/registry/directories/aitoptools/playbook.md +1 -0
- package/registry/directories/aiwithme/directory.yaml +54 -0
- package/registry/directories/aiwithme/playbook.md +10 -0
- package/registry/directories/all-in-ai-tools/directory.yaml +51 -0
- package/registry/directories/all-in-ai-tools/playbook.md +1 -0
- package/registry/directories/allmcps/directory.yaml +55 -0
- package/registry/directories/allmcps/playbook.md +7 -0
- package/registry/directories/alltopstartups/directory.yaml +43 -0
- package/registry/directories/altern-ai/directory.yaml +50 -0
- package/registry/directories/altern-ai/playbook.md +6 -0
- package/registry/directories/alternative-tools/directory.yaml +46 -0
- package/registry/directories/alternative-tools/playbook.md +4 -0
- package/registry/directories/alternativeto/directory.yaml +48 -0
- package/registry/directories/alternativeto/playbook.md +8 -0
- package/registry/directories/ant-directory/directory.yaml +52 -0
- package/registry/directories/ant-directory/playbook.md +1 -0
- package/registry/directories/app-scout/directory.yaml +60 -0
- package/registry/directories/app-scout/playbook.md +5 -0
- package/registry/directories/app-stack-builder/directory.yaml +71 -0
- package/registry/directories/appalist/directory.yaml +46 -0
- package/registry/directories/appalist/playbook.md +4 -0
- package/registry/directories/apprater/directory.yaml +65 -0
- package/registry/directories/apprater/playbook.md +1 -0
- package/registry/directories/appsumo/directory.yaml +45 -0
- package/registry/directories/appsumo/playbook.md +1 -0
- package/registry/directories/ask-ai-for-it/directory.yaml +45 -0
- package/registry/directories/ask-ai-for-it/playbook.md +1 -0
- package/registry/directories/auraplusplus/directory.yaml +56 -0
- package/registry/directories/auraplusplus/playbook.md +6 -0
- package/registry/directories/awesome-mcp-list/directory.yaml +45 -0
- package/registry/directories/awesome-mcp-list/playbook.md +7 -0
- package/registry/directories/awesome-mcp-lists/directory.yaml +44 -0
- package/registry/directories/awesome-mcp-lists/playbook.md +6 -0
- package/registry/directories/awesome-remote-mcp-servers/directory.yaml +46 -0
- package/registry/directories/awesome-remote-mcp-servers/playbook.md +8 -0
- package/registry/directories/awesome-tools/directory.yaml +55 -0
- package/registry/directories/awesome-tools/playbook.md +8 -0
- package/registry/directories/awesomeindie/directory.yaml +51 -0
- package/registry/directories/awesomeindie/playbook.md +7 -0
- package/registry/directories/axis-launch/directory.yaml +64 -0
- package/registry/directories/b2b-saas-market/directory.yaml +63 -0
- package/registry/directories/bai-tools/directory.yaml +53 -0
- package/registry/directories/bai-tools/playbook.md +7 -0
- package/registry/directories/basedtools/directory.yaml +59 -0
- package/registry/directories/basedtools/playbook.md +1 -0
- package/registry/directories/best-ai-org/directory.yaml +53 -0
- package/registry/directories/best-ai-org/playbook.md +1 -0
- package/registry/directories/best-of-ai/directory.yaml +57 -0
- package/registry/directories/best-of-ai/playbook.md +1 -0
- package/registry/directories/bestaito/directory.yaml +47 -0
- package/registry/directories/bestaito/playbook.md +6 -0
- package/registry/directories/bestofai/directory.yaml +45 -0
- package/registry/directories/bestofai/playbook.md +7 -0
- package/registry/directories/bestsky-tools/directory.yaml +44 -0
- package/registry/directories/bestsky-tools/playbook.md +5 -0
- package/registry/directories/betabound/directory.yaml +63 -0
- package/registry/directories/betabound/playbook.md +7 -0
- package/registry/directories/betalist/directory.yaml +52 -0
- package/registry/directories/betalist/playbook.md +11 -0
- package/registry/directories/betterbuys/directory.yaml +55 -0
- package/registry/directories/betterbuys/playbook.md +7 -0
- package/registry/directories/betterlaunch/directory.yaml +53 -0
- package/registry/directories/beyond-ai-tools/directory.yaml +43 -0
- package/registry/directories/beyond-ai-tools/playbook.md +1 -0
- package/registry/directories/billion-dollar-pitch-decks/directory.yaml +64 -0
- package/registry/directories/billion-dollar-pitch-decks/playbook.md +4 -0
- package/registry/directories/browserextension-dev/directory.yaml +55 -0
- package/registry/directories/browserextension-dev/playbook.md +7 -0
- package/registry/directories/bsdirectory/directory.yaml +68 -0
- package/registry/directories/bsdirectory/playbook.md +5 -0
- package/registry/directories/buildvoyage/directory.yaml +64 -0
- package/registry/directories/buildvoyage/playbook.md +1 -0
- package/registry/directories/built-here/directory.yaml +50 -0
- package/registry/directories/built-here/playbook.md +6 -0
- package/registry/directories/builtbyme/directory.yaml +55 -0
- package/registry/directories/builtbyme/playbook.md +7 -0
- package/registry/directories/builtineu/directory.yaml +70 -0
- package/registry/directories/builtineu/playbook.md +7 -0
- package/registry/directories/businesssoftware/directory.yaml +42 -0
- package/registry/directories/buy-sell-startups/directory.yaml +52 -0
- package/registry/directories/buy-sell-startups/playbook.md +5 -0
- package/registry/directories/claude-mcp-directory/directory.yaml +45 -0
- package/registry/directories/claude-mcp-directory/playbook.md +7 -0
- package/registry/directories/claude-plugin-directory/directory.yaml +48 -0
- package/registry/directories/claude-plugin-directory/playbook.md +7 -0
- package/registry/directories/cocoon-ai/directory.yaml +57 -0
- package/registry/directories/codehype/directory.yaml +68 -0
- package/registry/directories/codehype/playbook.md +7 -0
- package/registry/directories/collectai/directory.yaml +43 -0
- package/registry/directories/collectai/playbook.md +5 -0
- package/registry/directories/commune-launchpad/directory.yaml +51 -0
- package/registry/directories/confettisaas/directory.yaml +51 -0
- package/registry/directories/confettisaas/playbook.md +1 -0
- package/registry/directories/crowdstax/directory.yaml +54 -0
- package/registry/directories/crowdstax/playbook.md +6 -0
- package/registry/directories/crozdesk/directory.yaml +72 -0
- package/registry/directories/crozdesk/playbook.md +8 -0
- package/registry/directories/crypto-tools-directory/directory.yaml +60 -0
- package/registry/directories/crypto-tools-directory/playbook.md +6 -0
- package/registry/directories/ctrlaltship/directory.yaml +92 -0
- package/registry/directories/ctrlaltship/playbook.md +6 -0
- package/registry/directories/dachstartups/directory.yaml +49 -0
- package/registry/directories/dachstartups/playbook.md +4 -0
- package/registry/directories/dailypings/directory.yaml +51 -0
- package/registry/directories/dailypings/playbook.md +6 -0
- package/registry/directories/dang-ai/directory.yaml +44 -0
- package/registry/directories/dang-ai/playbook.md +1 -0
- package/registry/directories/daniellaunches/directory.yaml +47 -0
- package/registry/directories/daniellaunches/playbook.md +6 -0
- package/registry/directories/datalook/directory.yaml +60 -0
- package/registry/directories/dayslaunch/directory.yaml +46 -0
- package/registry/directories/dayslaunch/playbook.md +5 -0
- package/registry/directories/deeplaunch/directory.yaml +46 -0
- package/registry/directories/deeplaunch/playbook.md +6 -0
- package/registry/directories/desifounder-spotlight/directory.yaml +49 -0
- package/registry/directories/desifounder-spotlight/playbook.md +1 -0
- package/registry/directories/developers-digest-mcp/directory.yaml +46 -0
- package/registry/directories/developers-digest-mcp/playbook.md +7 -0
- package/registry/directories/devhunt/directory.yaml +53 -0
- package/registry/directories/devhunt/playbook.md +8 -0
- package/registry/directories/devpages/directory.yaml +62 -0
- package/registry/directories/devpages/playbook.md +7 -0
- package/registry/directories/devresourc-es/directory.yaml +49 -0
- package/registry/directories/devresourc-es/playbook.md +9 -0
- package/registry/directories/devtools-directory/directory.yaml +63 -0
- package/registry/directories/devtools-directory/playbook.md +7 -0
- package/registry/directories/dir-hub/directory.yaml +62 -0
- package/registry/directories/dir-hub/playbook.md +6 -0
- package/registry/directories/direct2app/directory.yaml +47 -0
- package/registry/directories/direct2app/playbook.md +5 -0
- package/registry/directories/directree/directory.yaml +56 -0
- package/registry/directories/directree/playbook.md +7 -0
- package/registry/directories/dirs-cc/directory.yaml +49 -0
- package/registry/directories/dirs-cc/playbook.md +1 -0
- package/registry/directories/dododirectory/directory.yaml +46 -0
- package/registry/directories/dododirectory/playbook.md +6 -0
- package/registry/directories/dofollowtools/directory.yaml +75 -0
- package/registry/directories/dofollowtools/playbook.md +7 -0
- package/registry/directories/dropyourai/directory.yaml +73 -0
- package/registry/directories/dynamite-ai/directory.yaml +59 -0
- package/registry/directories/dynamite-ai/playbook.md +1 -0
- package/registry/directories/earlyhunt/directory.yaml +44 -0
- package/registry/directories/earlyhunt/playbook.md +6 -0
- package/registry/directories/ebool/directory.yaml +72 -0
- package/registry/directories/ebool/playbook.md +8 -0
- package/registry/directories/eliteai-tools/directory.yaml +45 -0
- package/registry/directories/eliteai-tools/playbook.md +1 -0
- package/registry/directories/emoji-launch/directory.yaml +60 -0
- package/registry/directories/emoji-launch/playbook.md +1 -0
- package/registry/directories/enlisted/directory.yaml +48 -0
- package/registry/directories/enlisted/playbook.md +7 -0
- package/registry/directories/eu-software-alternatives/directory.yaml +87 -0
- package/registry/directories/eu-software-alternatives/playbook.md +5 -0
- package/registry/directories/eu-vetted/directory.yaml +64 -0
- package/registry/directories/eu-vetted/playbook.md +1 -0
- package/registry/directories/eubuilt/directory.yaml +77 -0
- package/registry/directories/eubuilt/playbook.md +5 -0
- package/registry/directories/euroalternative/directory.yaml +48 -0
- package/registry/directories/euroalternative/playbook.md +1 -0
- package/registry/directories/european-tech/directory.yaml +55 -0
- package/registry/directories/european-tech/playbook.md +5 -0
- package/registry/directories/europeanapps/directory.yaml +52 -0
- package/registry/directories/europeanapps/playbook.md +5 -0
- package/registry/directories/europeanstack/directory.yaml +66 -0
- package/registry/directories/europeanstack/playbook.md +7 -0
- package/registry/directories/eurotoolsindex/directory.yaml +68 -0
- package/registry/directories/eurotoolsindex/playbook.md +7 -0
- package/registry/directories/everydev-ai/directory.yaml +50 -0
- package/registry/directories/everydev-ai/playbook.md +6 -0
- package/registry/directories/faceless-directory/directory.yaml +52 -0
- package/registry/directories/fastd/directory.yaml +45 -0
- package/registry/directories/fastd/playbook.md +1 -0
- package/registry/directories/fastlaunch/directory.yaml +53 -0
- package/registry/directories/fastlaunch/playbook.md +10 -0
- package/registry/directories/favors/directory.yaml +72 -0
- package/registry/directories/favors/playbook.md +7 -0
- package/registry/directories/fazier/directory.yaml +44 -0
- package/registry/directories/fazier/playbook.md +8 -0
- package/registry/directories/featuredtool/directory.yaml +51 -0
- package/registry/directories/featuredtool/playbook.md +1 -0
- package/registry/directories/find-seo-tools/directory.yaml +72 -0
- package/registry/directories/find-seo-tools/playbook.md +1 -0
- package/registry/directories/findly-tools/directory.yaml +45 -0
- package/registry/directories/findly-tools/playbook.md +7 -0
- package/registry/directories/findstack/directory.yaml +46 -0
- package/registry/directories/findstack/playbook.md +1 -0
- package/registry/directories/findyoursaas/directory.yaml +47 -0
- package/registry/directories/findyoursaas/playbook.md +7 -0
- package/registry/directories/firsto/directory.yaml +54 -0
- package/registry/directories/firsto/playbook.md +10 -0
- package/registry/directories/fosspace/directory.yaml +49 -0
- package/registry/directories/founders-today/directory.yaml +52 -0
- package/registry/directories/francesaas/directory.yaml +49 -0
- package/registry/directories/francesaas/playbook.md +5 -0
- package/registry/directories/free-startup-directories/directory.yaml +70 -0
- package/registry/directories/free-startup-listing/directory.yaml +59 -0
- package/registry/directories/free-startup-listing/playbook.md +6 -0
- package/registry/directories/free-tools-directory/directory.yaml +55 -0
- package/registry/directories/free-tools-directory/playbook.md +7 -0
- package/registry/directories/freeaitools-net/directory.yaml +57 -0
- package/registry/directories/from-europe-with-love/directory.yaml +63 -0
- package/registry/directories/from-europe-with-love/playbook.md +5 -0
- package/registry/directories/fstlaunch/directory.yaml +57 -0
- package/registry/directories/fstlaunch/playbook.md +6 -0
- package/registry/directories/futuretools/directory.yaml +67 -0
- package/registry/directories/futuretools/playbook.md +10 -0
- package/registry/directories/glama/directory.yaml +46 -0
- package/registry/directories/glama/playbook.md +7 -0
- package/registry/directories/goodaitools/directory.yaml +46 -0
- package/registry/directories/goodaitools/playbook.md +7 -0
- package/registry/directories/gptbot/directory.yaml +56 -0
- package/registry/directories/gptdemo/directory.yaml +56 -0
- package/registry/directories/gptdemo/playbook.md +7 -0
- package/registry/directories/hicyou/directory.yaml +51 -0
- package/registry/directories/hicyou/playbook.md +1 -0
- package/registry/directories/hightechattic/directory.yaml +49 -0
- package/registry/directories/hightechattic/playbook.md +6 -0
- package/registry/directories/howtobuysaas/directory.yaml +59 -0
- package/registry/directories/howtobuysaas/playbook.md +1 -0
- package/registry/directories/hunt0/directory.yaml +81 -0
- package/registry/directories/hunt0/playbook.md +1 -0
- package/registry/directories/huntscreens/directory.yaml +56 -0
- package/registry/directories/huntscreens/playbook.md +11 -0
- package/registry/directories/huntstartup/directory.yaml +53 -0
- package/registry/directories/huntstartup/playbook.md +8 -0
- package/registry/directories/huzzler/directory.yaml +49 -0
- package/registry/directories/huzzler/playbook.md +7 -0
- package/registry/directories/hype-star/directory.yaml +57 -0
- package/registry/directories/hype-star/playbook.md +6 -0
- package/registry/directories/idea-kiln/directory.yaml +50 -0
- package/registry/directories/idea-kiln/playbook.md +6 -0
- package/registry/directories/ignlab-launch/directory.yaml +82 -0
- package/registry/directories/ignlab-launch/playbook.md +7 -0
- package/registry/directories/indie-deals/directory.yaml +48 -0
- package/registry/directories/indie-deals/playbook.md +8 -0
- package/registry/directories/indie-hustles/directory.yaml +61 -0
- package/registry/directories/indie-hustles/playbook.md +1 -0
- package/registry/directories/indie-makers-hub/directory.yaml +50 -0
- package/registry/directories/indie-makers-hub/playbook.md +5 -0
- package/registry/directories/indie-page/directory.yaml +47 -0
- package/registry/directories/indie-page/playbook.md +1 -0
- package/registry/directories/indiefounders/directory.yaml +48 -0
- package/registry/directories/indiefounders/playbook.md +5 -0
- package/registry/directories/indiehackers/directory.yaml +47 -0
- package/registry/directories/indiehackers/playbook.md +1 -0
- package/registry/directories/indiehackerstacks/directory.yaml +45 -0
- package/registry/directories/indiehackerstacks/playbook.md +5 -0
- package/registry/directories/indiehub/directory.yaml +45 -0
- package/registry/directories/indiehub/playbook.md +8 -0
- package/registry/directories/indiehunt/directory.yaml +45 -0
- package/registry/directories/indiehunt/playbook.md +6 -0
- package/registry/directories/indieneed/directory.yaml +78 -0
- package/registry/directories/indieneed/playbook.md +9 -0
- package/registry/directories/indieproducts/directory.yaml +47 -0
- package/registry/directories/indieproducts/playbook.md +4 -0
- package/registry/directories/indiesignal/directory.yaml +47 -0
- package/registry/directories/indietool/directory.yaml +44 -0
- package/registry/directories/indietool/playbook.md +1 -0
- package/registry/directories/indietools/directory.yaml +69 -0
- package/registry/directories/insidr-ai/directory.yaml +49 -0
- package/registry/directories/insidr-ai/playbook.md +5 -0
- package/registry/directories/intelligent-tools/directory.yaml +59 -0
- package/registry/directories/intelligent-tools/playbook.md +1 -0
- package/registry/directories/inventlist/directory.yaml +71 -0
- package/registry/directories/inventlist/playbook.md +5 -0
- package/registry/directories/joinly/directory.yaml +68 -0
- package/registry/directories/joinly/playbook.md +5 -0
- package/registry/directories/junkstartups/directory.yaml +72 -0
- package/registry/directories/junkstartups/playbook.md +1 -0
- package/registry/directories/justhunt/directory.yaml +55 -0
- package/registry/directories/justhunt/playbook.md +7 -0
- package/registry/directories/kittylaunch/directory.yaml +70 -0
- package/registry/directories/kittylaunch/playbook.md +7 -0
- package/registry/directories/lachief/directory.yaml +81 -0
- package/registry/directories/lachief/playbook.md +1 -0
- package/registry/directories/landbook/directory.yaml +48 -0
- package/registry/directories/landbook/playbook.md +1 -0
- package/registry/directories/launch-community/directory.yaml +73 -0
- package/registry/directories/launch-community/playbook.md +1 -0
- package/registry/directories/launch-list/directory.yaml +52 -0
- package/registry/directories/launch-list/playbook.md +6 -0
- package/registry/directories/launchaf/directory.yaml +51 -0
- package/registry/directories/launchbase-india/directory.yaml +53 -0
- package/registry/directories/launchboard/directory.yaml +50 -0
- package/registry/directories/launchboard/playbook.md +6 -0
- package/registry/directories/launchclash/directory.yaml +47 -0
- package/registry/directories/launchclash/playbook.md +6 -0
- package/registry/directories/launched-tools/directory.yaml +48 -0
- package/registry/directories/launchfree/directory.yaml +58 -0
- package/registry/directories/launchfree/playbook.md +1 -0
- package/registry/directories/launchigniter/directory.yaml +60 -0
- package/registry/directories/launchigniter/playbook.md +9 -0
- package/registry/directories/launchit/directory.yaml +48 -0
- package/registry/directories/launchit/playbook.md +1 -0
- package/registry/directories/launchllama/directory.yaml +53 -0
- package/registry/directories/launchllama/playbook.md +7 -0
- package/registry/directories/launchonit/directory.yaml +74 -0
- package/registry/directories/launchonit/playbook.md +7 -0
- package/registry/directories/launchpedia-tools/directory.yaml +43 -0
- package/registry/directories/launchpedia-tools/playbook.md +5 -0
- package/registry/directories/launchscaler/directory.yaml +53 -0
- package/registry/directories/launchtory/directory.yaml +47 -0
- package/registry/directories/launchtory/playbook.md +5 -0
- package/registry/directories/launchvault/directory.yaml +52 -0
- package/registry/directories/launchvault/playbook.md +5 -0
- package/registry/directories/launchvibe/directory.yaml +45 -0
- package/registry/directories/launchvibe/playbook.md +1 -0
- package/registry/directories/launchvoid/directory.yaml +45 -0
- package/registry/directories/launchvoid/playbook.md +6 -0
- package/registry/directories/letslaunch/directory.yaml +53 -0
- package/registry/directories/letslaunch/playbook.md +1 -0
- package/registry/directories/libhunt/directory.yaml +50 -0
- package/registry/directories/libhunt/playbook.md +1 -0
- package/registry/directories/linezine/directory.yaml +44 -0
- package/registry/directories/linezine/playbook.md +1 -0
- package/registry/directories/linkrena/directory.yaml +58 -0
- package/registry/directories/linkrena/playbook.md +6 -0
- package/registry/directories/list-maro/directory.yaml +74 -0
- package/registry/directories/list-your-tool/directory.yaml +76 -0
- package/registry/directories/list-your-tool/playbook.md +1 -0
- package/registry/directories/listbulb/directory.yaml +48 -0
- package/registry/directories/listbulb/playbook.md +1 -0
- package/registry/directories/listing-sh/directory.yaml +70 -0
- package/registry/directories/llm-relevance/directory.yaml +67 -0
- package/registry/directories/llm-relevance/playbook.md +1 -0
- package/registry/directories/llms-txt-hub/directory.yaml +47 -0
- package/registry/directories/llms-txt-hub/playbook.md +6 -0
- package/registry/directories/llms-txt-site/directory.yaml +53 -0
- package/registry/directories/llms-txt-site/playbook.md +7 -0
- package/registry/directories/macnative/directory.yaml +47 -0
- package/registry/directories/macnative/playbook.md +6 -0
- package/registry/directories/made-with-bolt/directory.yaml +58 -0
- package/registry/directories/made-with-bolt/playbook.md +1 -0
- package/registry/directories/made-with-lovable/directory.yaml +64 -0
- package/registry/directories/made-with-lovable/playbook.md +1 -0
- package/registry/directories/magicbox/directory.yaml +51 -0
- package/registry/directories/magicbox/playbook.md +16 -0
- package/registry/directories/maidensail/directory.yaml +54 -0
- package/registry/directories/makeitlast/directory.yaml +63 -0
- package/registry/directories/makeitlast/playbook.md +8 -0
- package/registry/directories/makerhunt/directory.yaml +56 -0
- package/registry/directories/makerhunt/playbook.md +1 -0
- package/registry/directories/makerlist/directory.yaml +49 -0
- package/registry/directories/makerlist/playbook.md +7 -0
- package/registry/directories/mapbid/directory.yaml +46 -0
- package/registry/directories/marketingdb/directory.yaml +50 -0
- package/registry/directories/marketingdb/playbook.md +8 -0
- package/registry/directories/marketraa/directory.yaml +63 -0
- package/registry/directories/marketraa/playbook.md +1 -0
- package/registry/directories/mcp-market/directory.yaml +49 -0
- package/registry/directories/mcp-market/playbook.md +7 -0
- package/registry/directories/mcp-repository/directory.yaml +45 -0
- package/registry/directories/mcp-repository/playbook.md +6 -0
- package/registry/directories/mcp-so/directory.yaml +45 -0
- package/registry/directories/mcp-so/playbook.md +6 -0
- package/registry/directories/mcpservers/directory.yaml +53 -0
- package/registry/directories/mcpservers/playbook.md +7 -0
- package/registry/directories/mf8/directory.yaml +53 -0
- package/registry/directories/mf8/playbook.md +5 -0
- package/registry/directories/microlaunch/directory.yaml +48 -0
- package/registry/directories/microlaunch/playbook.md +1 -0
- package/registry/directories/microsaasexamples/directory.yaml +50 -0
- package/registry/directories/must-tools/directory.yaml +68 -0
- package/registry/directories/must-tools/playbook.md +6 -0
- package/registry/directories/my-launch-stash/directory.yaml +47 -0
- package/registry/directories/my-launch-stash/playbook.md +8 -0
- package/registry/directories/nachonacho/directory.yaml +47 -0
- package/registry/directories/navfolders/directory.yaml +48 -0
- package/registry/directories/navfolders/playbook.md +5 -0
- package/registry/directories/navs/directory.yaml +66 -0
- package/registry/directories/navs/playbook.md +6 -0
- package/registry/directories/navtools-ai/directory.yaml +51 -0
- package/registry/directories/navtools-ai/playbook.md +1 -0
- package/registry/directories/neeed-directory/directory.yaml +60 -0
- package/registry/directories/neeed-directory/playbook.md +10 -0
- package/registry/directories/neonrev/directory.yaml +43 -0
- package/registry/directories/neonrev/playbook.md +1 -0
- package/registry/directories/newaiforyou/directory.yaml +50 -0
- package/registry/directories/newaiforyou/playbook.md +1 -0
- package/registry/directories/newtool/directory.yaml +51 -0
- package/registry/directories/newtool/playbook.md +1 -0
- package/registry/directories/nextbigwhat/directory.yaml +48 -0
- package/registry/directories/nextbigwhat/playbook.md +5 -0
- package/registry/directories/nextlaunch/directory.yaml +50 -0
- package/registry/directories/nextlaunch/playbook.md +8 -0
- package/registry/directories/nicklaunches/directory.yaml +70 -0
- package/registry/directories/nicklaunches/playbook.md +1 -0
- package/registry/directories/nocodefinder/directory.yaml +45 -0
- package/registry/directories/nocodefinder/playbook.md +6 -0
- package/registry/directories/nocodelist/directory.yaml +48 -0
- package/registry/directories/nocodelist/playbook.md +7 -0
- package/registry/directories/noonlaunch/directory.yaml +51 -0
- package/registry/directories/noonlaunch/playbook.md +6 -0
- package/registry/directories/nxgn-tools/directory.yaml +49 -0
- package/registry/directories/nxgn-tools/playbook.md +6 -0
- package/registry/directories/once-tools/directory.yaml +58 -0
- package/registry/directories/once-tools/playbook.md +1 -0
- package/registry/directories/open-launch/directory.yaml +46 -0
- package/registry/directories/open-launch/playbook.md +1 -0
- package/registry/directories/openalternative/directory.yaml +62 -0
- package/registry/directories/openalternative/playbook.md +8 -0
- package/registry/directories/openhunts/directory.yaml +52 -0
- package/registry/directories/openhunts/playbook.md +1 -0
- package/registry/directories/openresources/directory.yaml +59 -0
- package/registry/directories/openresources/playbook.md +6 -0
- package/registry/directories/opensaas-directory/directory.yaml +47 -0
- package/registry/directories/opensourcealternative-to/directory.yaml +63 -0
- package/registry/directories/opensourcealternative-to/playbook.md +6 -0
- package/registry/directories/opentools/directory.yaml +42 -0
- package/registry/directories/opentools/playbook.md +4 -0
- package/registry/directories/osssoftware/directory.yaml +66 -0
- package/registry/directories/osssoftware/playbook.md +5 -0
- package/registry/directories/oursoftware-eu/directory.yaml +69 -0
- package/registry/directories/oursoftware-eu/playbook.md +1 -0
- package/registry/directories/peerpush/directory.yaml +48 -0
- package/registry/directories/peerpush/playbook.md +8 -0
- package/registry/directories/phyco/directory.yaml +54 -0
- package/registry/directories/phygital-ai-library/directory.yaml +47 -0
- package/registry/directories/phygital-ai-library/playbook.md +6 -0
- package/registry/directories/pitchground/directory.yaml +47 -0
- package/registry/directories/pitchground/playbook.md +5 -0
- package/registry/directories/pitchwall/directory.yaml +47 -0
- package/registry/directories/pitchwall/playbook.md +1 -0
- package/registry/directories/postmake/directory.yaml +56 -0
- package/registry/directories/postmake/playbook.md +1 -0
- package/registry/directories/poweredbyai/directory.yaml +57 -0
- package/registry/directories/poweredbyai/playbook.md +1 -0
- package/registry/directories/pro-launch/directory.yaml +51 -0
- package/registry/directories/pro-launch/playbook.md +9 -0
- package/registry/directories/product-hunt/directory.yaml +54 -0
- package/registry/directories/product-hunt/playbook.md +1 -0
- package/registry/directories/product-rally/directory.yaml +50 -0
- package/registry/directories/product-rally/playbook.md +6 -0
- package/registry/directories/productivity-directory/directory.yaml +47 -0
- package/registry/directories/productivity-directory/playbook.md +7 -0
- package/registry/directories/productwatch/directory.yaml +51 -0
- package/registry/directories/productwatch/playbook.md +7 -0
- package/registry/directories/proofstories/directory.yaml +47 -0
- package/registry/directories/proofstories/playbook.md +1 -0
- package/registry/directories/prouct/directory.yaml +93 -0
- package/registry/directories/prouct/playbook.md +7 -0
- package/registry/directories/public-apis/directory.yaml +47 -0
- package/registry/directories/public-apis/playbook.md +6 -0
- package/registry/directories/publishyoursaas/directory.yaml +53 -0
- package/registry/directories/publishyoursaas/playbook.md +7 -0
- package/registry/directories/rankinpublic/directory.yaml +50 -0
- package/registry/directories/rankinpublic/playbook.md +1 -0
- package/registry/directories/rankyourai/directory.yaml +50 -0
- package/registry/directories/rankyourai/playbook.md +1 -0
- package/registry/directories/resource-fyi/directory.yaml +47 -0
- package/registry/directories/resource-fyi/playbook.md +5 -0
- package/registry/directories/roozna/directory.yaml +68 -0
- package/registry/directories/roozna/playbook.md +1 -0
- package/registry/directories/saas-fyi/directory.yaml +73 -0
- package/registry/directories/saas-fyi/playbook.md +7 -0
- package/registry/directories/saas-italia/directory.yaml +58 -0
- package/registry/directories/saas-launches/directory.yaml +47 -0
- package/registry/directories/saas-launches/playbook.md +6 -0
- package/registry/directories/saasbison/directory.yaml +44 -0
- package/registry/directories/saasbison/playbook.md +8 -0
- package/registry/directories/saascity/directory.yaml +50 -0
- package/registry/directories/saascubes/directory.yaml +49 -0
- package/registry/directories/saascubes/playbook.md +6 -0
- package/registry/directories/saasfame/directory.yaml +47 -0
- package/registry/directories/saasfame/playbook.md +6 -0
- package/registry/directories/saasgenius/directory.yaml +50 -0
- package/registry/directories/saasgenius/playbook.md +8 -0
- package/registry/directories/saasgrave-launches/directory.yaml +53 -0
- package/registry/directories/saasgrave-launches/playbook.md +7 -0
- package/registry/directories/saashammer/directory.yaml +53 -0
- package/registry/directories/saashammer/playbook.md +6 -0
- package/registry/directories/saashub/directory.yaml +51 -0
- package/registry/directories/saashub/playbook.md +9 -0
- package/registry/directories/saashunt/directory.yaml +43 -0
- package/registry/directories/saaslineup/directory.yaml +68 -0
- package/registry/directories/saaslineup/playbook.md +1 -0
- package/registry/directories/saaslist/directory.yaml +55 -0
- package/registry/directories/saaslist/playbook.md +5 -0
- package/registry/directories/saasmag-saas1000/directory.yaml +71 -0
- package/registry/directories/saaspage/directory.yaml +48 -0
- package/registry/directories/saasplace/directory.yaml +66 -0
- package/registry/directories/saasplace/playbook.md +1 -0
- package/registry/directories/saasrow/directory.yaml +51 -0
- package/registry/directories/saasrow/playbook.md +1 -0
- package/registry/directories/saassy-board/directory.yaml +54 -0
- package/registry/directories/saassy-board/playbook.md +1 -0
- package/registry/directories/saastore/directory.yaml +56 -0
- package/registry/directories/saastore/playbook.md +6 -0
- package/registry/directories/saastr-community-agents/directory.yaml +65 -0
- package/registry/directories/saastr-community-agents/playbook.md +8 -0
- package/registry/directories/saasvault/directory.yaml +48 -0
- package/registry/directories/saasvault/playbook.md +6 -0
- package/registry/directories/scrolllaunch/directory.yaml +54 -0
- package/registry/directories/scrolllaunch/playbook.md +8 -0
- package/registry/directories/seewhatnewai/directory.yaml +53 -0
- package/registry/directories/sellwithboost/directory.yaml +54 -0
- package/registry/directories/sellwithboost/playbook.md +5 -0
- package/registry/directories/serchen/directory.yaml +48 -0
- package/registry/directories/serchen/playbook.md +8 -0
- package/registry/directories/shinylaunch/directory.yaml +48 -0
- package/registry/directories/shinylaunch/playbook.md +6 -0
- package/registry/directories/shipyard-hq/directory.yaml +58 -0
- package/registry/directories/shipyard-hq/playbook.md +1 -0
- package/registry/directories/shipybara/directory.yaml +48 -0
- package/registry/directories/shipybara/playbook.md +8 -0
- package/registry/directories/shipycorn/directory.yaml +47 -0
- package/registry/directories/shipycorn/playbook.md +6 -0
- package/registry/directories/showmebest-ai/directory.yaml +50 -0
- package/registry/directories/showmebest-ai/playbook.md +1 -0
- package/registry/directories/showmysites/directory.yaml +52 -0
- package/registry/directories/showmysites/playbook.md +7 -0
- package/registry/directories/sidehunt/directory.yaml +56 -0
- package/registry/directories/sidehunt/playbook.md +1 -0
- package/registry/directories/sideprojectors/directory.yaml +55 -0
- package/registry/directories/sideprojectors/playbook.md +1 -0
- package/registry/directories/sideprojects/directory.yaml +52 -0
- package/registry/directories/sideprojects/playbook.md +1 -0
- package/registry/directories/sitepatent/directory.yaml +58 -0
- package/registry/directories/sitepatent/playbook.md +1 -0
- package/registry/directories/small-business-marketing-tools/directory.yaml +67 -0
- package/registry/directories/small-business-marketing-tools/playbook.md +1 -0
- package/registry/directories/smol-launch/directory.yaml +44 -0
- package/registry/directories/smol-launch/playbook.md +9 -0
- package/registry/directories/software-como-servicio/directory.yaml +64 -0
- package/registry/directories/software-como-servicio/playbook.md +6 -0
- package/registry/directories/software-finder/directory.yaml +47 -0
- package/registry/directories/software-finder/playbook.md +6 -0
- package/registry/directories/softwaresuggest/directory.yaml +68 -0
- package/registry/directories/softwaresuggest/playbook.md +6 -0
- package/registry/directories/solo-launches/directory.yaml +48 -0
- package/registry/directories/solo-launches/playbook.md +6 -0
- package/registry/directories/sololaunch/directory.yaml +69 -0
- package/registry/directories/sourceforge/directory.yaml +75 -0
- package/registry/directories/sourceforge/playbook.md +9 -0
- package/registry/directories/spacerrapps/directory.yaml +53 -0
- package/registry/directories/spacerrapps/playbook.md +6 -0
- package/registry/directories/speartip/directory.yaml +48 -0
- package/registry/directories/speartip/playbook.md +1 -0
- package/registry/directories/spiff-store/directory.yaml +46 -0
- package/registry/directories/spiff-store/playbook.md +6 -0
- package/registry/directories/spot-startups/directory.yaml +48 -0
- package/registry/directories/spotsaas/directory.yaml +45 -0
- package/registry/directories/squidir/directory.yaml +60 -0
- package/registry/directories/squidir/playbook.md +5 -0
- package/registry/directories/stackboo/directory.yaml +49 -0
- package/registry/directories/stackboo/playbook.md +6 -0
- package/registry/directories/stackledge/directory.yaml +84 -0
- package/registry/directories/stackledge/playbook.md +8 -0
- package/registry/directories/startup-alternatives/directory.yaml +68 -0
- package/registry/directories/startup-alternatives/playbook.md +5 -0
- package/registry/directories/startup-benchmarks/directory.yaml +59 -0
- package/registry/directories/startup-benchmarks/playbook.md +1 -0
- package/registry/directories/startup-collections/directory.yaml +56 -0
- package/registry/directories/startup-collections/playbook.md +5 -0
- package/registry/directories/startup-europe/directory.yaml +52 -0
- package/registry/directories/startup-europe/playbook.md +1 -0
- package/registry/directories/startup-fame/directory.yaml +51 -0
- package/registry/directories/startup-fame/playbook.md +8 -0
- package/registry/directories/startup-fast/directory.yaml +52 -0
- package/registry/directories/startup-fast/playbook.md +9 -0
- package/registry/directories/startup-list/directory.yaml +81 -0
- package/registry/directories/startup-list/playbook.md +8 -0
- package/registry/directories/startup-listing/directory.yaml +77 -0
- package/registry/directories/startup-listing/playbook.md +1 -0
- package/registry/directories/startup-og/directory.yaml +52 -0
- package/registry/directories/startup-ranking/directory.yaml +51 -0
- package/registry/directories/startup-ranking/playbook.md +1 -0
- package/registry/directories/startup-spotlight/directory.yaml +50 -0
- package/registry/directories/startup-spotlight/playbook.md +5 -0
- package/registry/directories/startup88/directory.yaml +46 -0
- package/registry/directories/startup88/playbook.md +1 -0
- package/registry/directories/startupaccelerator/directory.yaml +76 -0
- package/registry/directories/startupaccelerator/playbook.md +7 -0
- package/registry/directories/startupbase/directory.yaml +44 -0
- package/registry/directories/startupbase/playbook.md +1 -0
- package/registry/directories/startupbeats/directory.yaml +48 -0
- package/registry/directories/startupbeats/playbook.md +6 -0
- package/registry/directories/startupdirectory/directory.yaml +50 -0
- package/registry/directories/startupdirectory/playbook.md +1 -0
- package/registry/directories/startupinspire/directory.yaml +52 -0
- package/registry/directories/startupinspire/playbook.md +8 -0
- package/registry/directories/startuplibrary/directory.yaml +46 -0
- package/registry/directories/startuplibrary/playbook.md +1 -0
- package/registry/directories/startuplist-africa/directory.yaml +45 -0
- package/registry/directories/startuplist-africa/playbook.md +6 -0
- package/registry/directories/startups-gallery/directory.yaml +61 -0
- package/registry/directories/startups-gallery/playbook.md +5 -0
- package/registry/directories/startupsfm/directory.yaml +64 -0
- package/registry/directories/startupsfm/playbook.md +9 -0
- package/registry/directories/startupsfyi/directory.yaml +51 -0
- package/registry/directories/startupsfyi/playbook.md +4 -0
- package/registry/directories/startupslab/directory.yaml +61 -0
- package/registry/directories/startupslab/playbook.md +8 -0
- package/registry/directories/startuptile/directory.yaml +51 -0
- package/registry/directories/startuptile/playbook.md +5 -0
- package/registry/directories/startuptrusted/directory.yaml +46 -0
- package/registry/directories/startuptrusted/playbook.md +1 -0
- package/registry/directories/startuups/directory.yaml +50 -0
- package/registry/directories/steemhunt/directory.yaml +46 -0
- package/registry/directories/steemhunt/playbook.md +5 -0
- package/registry/directories/stellar-launch/directory.yaml +55 -0
- package/registry/directories/stellar-launch/playbook.md +1 -0
- package/registry/directories/stork-ai/directory.yaml +49 -0
- package/registry/directories/stork-ai/playbook.md +3 -0
- package/registry/directories/stork-mcp/directory.yaml +52 -0
- package/registry/directories/stork-mcp/playbook.md +3 -0
- package/registry/directories/submitaitools/directory.yaml +57 -0
- package/registry/directories/submitaitools/playbook.md +19 -0
- package/registry/directories/submithunt/directory.yaml +49 -0
- package/registry/directories/submithunt/playbook.md +9 -0
- package/registry/directories/submitmysaas/directory.yaml +51 -0
- package/registry/directories/submitmysaas/playbook.md +6 -0
- package/registry/directories/submitstartup/directory.yaml +73 -0
- package/registry/directories/sumodir/directory.yaml +47 -0
- package/registry/directories/sumodir/playbook.md +7 -0
- package/registry/directories/superlaunch/directory.yaml +64 -0
- package/registry/directories/superlaunch/playbook.md +7 -0
- package/registry/directories/techboard-australia/directory.yaml +54 -0
- package/registry/directories/techboard-australia/playbook.md +5 -0
- package/registry/directories/technologyadvice/directory.yaml +65 -0
- package/registry/directories/technologyadvice/playbook.md +8 -0
- package/registry/directories/tekpon/directory.yaml +44 -0
- package/registry/directories/tensorblock-mcp/directory.yaml +46 -0
- package/registry/directories/tensorblock-mcp/playbook.md +7 -0
- package/registry/directories/thankjohn/directory.yaml +42 -0
- package/registry/directories/thankjohn/playbook.md +1 -0
- package/registry/directories/that-ai-collection/directory.yaml +52 -0
- package/registry/directories/the-ai-library/directory.yaml +46 -0
- package/registry/directories/the-ai-library/playbook.md +1 -0
- package/registry/directories/the-next-ai/directory.yaml +62 -0
- package/registry/directories/the-popular-apps/directory.yaml +43 -0
- package/registry/directories/the-popular-apps/playbook.md +1 -0
- package/registry/directories/the-startup-inc/directory.yaml +90 -0
- package/registry/directories/the-startup-inc/playbook.md +4 -0
- package/registry/directories/the-startup-project/directory.yaml +67 -0
- package/registry/directories/the-startup-project/playbook.md +5 -0
- package/registry/directories/the-tool-directory/directory.yaml +75 -0
- package/registry/directories/thedevtoolsdir/directory.yaml +57 -0
- package/registry/directories/thedevtoolsdir/playbook.md +6 -0
- package/registry/directories/theres-an-ai/directory.yaml +44 -0
- package/registry/directories/theres-an-ai/playbook.md +1 -0
- package/registry/directories/theresanaiforthat/directory.yaml +51 -0
- package/registry/directories/theresanaiforthat/playbook.md +1 -0
- package/registry/directories/thesaasdir/directory.yaml +44 -0
- package/registry/directories/thesaasdir/playbook.md +6 -0
- package/registry/directories/tinylaunch/directory.yaml +50 -0
- package/registry/directories/tinylaunch/playbook.md +8 -0
- package/registry/directories/tinystartups/directory.yaml +61 -0
- package/registry/directories/tinystartups/playbook.md +6 -0
- package/registry/directories/tipseason/directory.yaml +68 -0
- package/registry/directories/tolodora/directory.yaml +59 -0
- package/registry/directories/tool-commons/directory.yaml +46 -0
- package/registry/directories/tool-commons/playbook.md +1 -0
- package/registry/directories/tool-directory-ai/directory.yaml +64 -0
- package/registry/directories/tool-directory-ai/playbook.md +5 -0
- package/registry/directories/tool-finder/directory.yaml +64 -0
- package/registry/directories/tool-finder/playbook.md +6 -0
- package/registry/directories/tooldirs/directory.yaml +64 -0
- package/registry/directories/tooldirs/playbook.md +7 -0
- package/registry/directories/toolfame/directory.yaml +47 -0
- package/registry/directories/toolfame/playbook.md +6 -0
- package/registry/directories/toolfio/directory.yaml +48 -0
- package/registry/directories/toolfio/playbook.md +7 -0
- package/registry/directories/toolfolio/directory.yaml +54 -0
- package/registry/directories/toolfolio/playbook.md +1 -0
- package/registry/directories/toolfound/directory.yaml +66 -0
- package/registry/directories/toolify/directory.yaml +52 -0
- package/registry/directories/toolify/playbook.md +8 -0
- package/registry/directories/toolkitly/directory.yaml +46 -0
- package/registry/directories/toolkitly/playbook.md +1 -0
- package/registry/directories/toollist-ai/directory.yaml +60 -0
- package/registry/directories/toollist-ai/playbook.md +1 -0
- package/registry/directories/toolomio/directory.yaml +55 -0
- package/registry/directories/toolomio/playbook.md +5 -0
- package/registry/directories/toolpasta/directory.yaml +46 -0
- package/registry/directories/toolpasta/playbook.md +1 -0
- package/registry/directories/toolpilot-ai/directory.yaml +77 -0
- package/registry/directories/toolpilot-ai/playbook.md +8 -0
- package/registry/directories/toolpromote/directory.yaml +81 -0
- package/registry/directories/toolrain/directory.yaml +48 -0
- package/registry/directories/toolrain/playbook.md +1 -0
- package/registry/directories/tools-ai-online/directory.yaml +65 -0
- package/registry/directories/tools-ai-online/playbook.md +7 -0
- package/registry/directories/tools-cafe/directory.yaml +51 -0
- package/registry/directories/tools-cafe/playbook.md +6 -0
- package/registry/directories/toolscout/directory.yaml +45 -0
- package/registry/directories/toolscout/playbook.md +5 -0
- package/registry/directories/toolsfine/directory.yaml +58 -0
- package/registry/directories/toolsfine/playbook.md +9 -0
- package/registry/directories/toolsnocode/directory.yaml +48 -0
- package/registry/directories/toolsnocode/playbook.md +6 -0
- package/registry/directories/tooltrove/directory.yaml +71 -0
- package/registry/directories/tooltrove/playbook.md +9 -0
- package/registry/directories/toolveyor/directory.yaml +44 -0
- package/registry/directories/toolveyor/playbook.md +1 -0
- package/registry/directories/toolwise/directory.yaml +67 -0
- package/registry/directories/toolwise/playbook.md +7 -0
- package/registry/directories/toools-design/directory.yaml +64 -0
- package/registry/directories/toools-design/playbook.md +8 -0
- package/registry/directories/topai-tools/directory.yaml +43 -0
- package/registry/directories/topai-tools/playbook.md +1 -0
- package/registry/directories/traffiklist/directory.yaml +55 -0
- package/registry/directories/traffiklist/playbook.md +6 -0
- package/registry/directories/trendy-startups/directory.yaml +49 -0
- package/registry/directories/trendy-startups/playbook.md +1 -0
- package/registry/directories/trillionagent/directory.yaml +57 -0
- package/registry/directories/trillionagent/playbook.md +1 -0
- package/registry/directories/truerev/directory.yaml +83 -0
- package/registry/directories/truerev/playbook.md +12 -0
- package/registry/directories/trustmrr/directory.yaml +43 -0
- package/registry/directories/trustradius/directory.yaml +67 -0
- package/registry/directories/trustradius/playbook.md +9 -0
- package/registry/directories/trylaunch/directory.yaml +51 -0
- package/registry/directories/trylaunch/playbook.md +1 -0
- package/registry/directories/tulimoa/directory.yaml +65 -0
- package/registry/directories/tulimoa/playbook.md +7 -0
- package/registry/directories/turbo0/directory.yaml +49 -0
- package/registry/directories/turbo0/playbook.md +9 -0
- package/registry/directories/twelve-tools/directory.yaml +47 -0
- package/registry/directories/twelve-tools/playbook.md +6 -0
- package/registry/directories/ultimate-free-tools/directory.yaml +49 -0
- package/registry/directories/ultimate-free-tools/playbook.md +8 -0
- package/registry/directories/uneed/directory.yaml +55 -0
- package/registry/directories/uneed/playbook.md +9 -0
- package/registry/directories/unitelist/directory.yaml +49 -0
- package/registry/directories/unitelist/playbook.md +7 -0
- package/registry/directories/uno-directory/directory.yaml +48 -0
- package/registry/directories/uno-directory/playbook.md +1 -0
- package/registry/directories/ventureradar/directory.yaml +47 -0
- package/registry/directories/ventureradar/playbook.md +5 -0
- package/registry/directories/vibe-coding-cloud/directory.yaml +45 -0
- package/registry/directories/vibe-coding-cloud/playbook.md +6 -0
- package/registry/directories/victrays/directory.yaml +55 -0
- package/registry/directories/victrays/playbook.md +8 -0
- package/registry/directories/visalytica/directory.yaml +59 -0
- package/registry/directories/wavel/directory.yaml +51 -0
- package/registry/directories/we-are-founders/directory.yaml +85 -0
- package/registry/directories/we-are-founders/playbook.md +1 -0
- package/registry/directories/web-store-extensions/directory.yaml +45 -0
- package/registry/directories/web-store-extensions/playbook.md +6 -0
- package/registry/directories/webcurate/directory.yaml +50 -0
- package/registry/directories/website-rating/directory.yaml +66 -0
- package/registry/directories/website-rating/playbook.md +9 -0
- package/registry/directories/webspot/directory.yaml +46 -0
- package/registry/directories/webspot/playbook.md +5 -0
- package/registry/directories/weekhack/directory.yaml +56 -0
- package/registry/directories/weekhack/playbook.md +5 -0
- package/registry/directories/weliketools/directory.yaml +50 -0
- package/registry/directories/weliketools/playbook.md +6 -0
- package/registry/directories/what-is-ai-tools/directory.yaml +48 -0
- package/registry/directories/what-is-ai-tools/playbook.md +1 -0
- package/registry/directories/wired-business/directory.yaml +44 -0
- package/registry/directories/wired-business/playbook.md +6 -0
- package/registry/directories/would-you-pay/directory.yaml +49 -0
- package/registry/directories/would-you-pay/playbook.md +6 -0
- package/registry/directories/xai-plugin-marketplace/directory.yaml +46 -0
- package/registry/directories/xai-plugin-marketplace/playbook.md +5 -0
- package/registry/directories/zplatform-ai/directory.yaml +75 -0
- package/registry/directories/zumvu/directory.yaml +50 -0
- package/scripts/ab +40 -0
- package/scripts/check-listings +8 -0
- package/scripts/google-login +13 -0
- package/scripts/show-browser +17 -0
- package/src/accounts.ts +81 -0
- package/src/audit.ts +229 -0
- package/src/browser.ts +123 -0
- package/src/capabilities.ts +62 -0
- package/src/cli.ts +780 -0
- package/src/db/index.ts +29 -0
- package/src/db/schema.ts +152 -0
- package/src/directory-kinds.ts +12 -0
- package/src/import-hosted.ts +156 -0
- package/src/jobs.ts +270 -0
- package/src/listings.ts +64 -0
- package/src/manual.ts +26 -0
- package/src/product-approvals.ts +31 -0
- package/src/public.ts +24 -0
- package/src/ratings.ts +71 -0
- package/src/registry-updates.ts +54 -0
- package/src/registry.ts +101 -0
- package/src/review.ts +58 -0
- package/src/server.ts +186 -0
- package/src/views.ts +71 -0
- package/src/web/app.tsx +1026 -0
- package/src/web/favicon.svg +4 -0
- package/src/web/index.html +14 -0
- package/src/web/style.css +301 -0
- package/src/workspace.ts +97 -0
package/src/listings.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Is a submitted listing publicly live? Used by `submissions check`. */
|
|
2
|
+
|
|
3
|
+
const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130 Safari/537.36";
|
|
4
|
+
// Some directories publish the page before approval ("the approval team will review it").
|
|
5
|
+
const NOT_YET = /will review it|pending (approval|review)|awaiting (approval|review)|under review/i;
|
|
6
|
+
|
|
7
|
+
export const isLive = (body: string, host: string) => body.includes(host) && !NOT_YET.test(body);
|
|
8
|
+
|
|
9
|
+
/** Path segment naming the product: "ledgerline", "ledgerline-com", "ledgerline-1234" — not "ledgerlinex". */
|
|
10
|
+
export const matchesSlug = (url: string, slugs: string[]) => {
|
|
11
|
+
const path = (() => { try { return new URL(url).pathname.toLowerCase(); } catch { return ""; } })();
|
|
12
|
+
return path.split("/").some((seg) => slugs.some((s) => seg === s || seg.startsWith(`${s}-`)));
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const locs = (xml: string) => [...xml.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)].map((m) => m[1]!.replace(/&/g, "&"));
|
|
16
|
+
|
|
17
|
+
const get = (url: string) =>
|
|
18
|
+
fetch(url, { redirect: "follow", headers: { "user-agent": UA }, signal: AbortSignal.timeout(20_000) })
|
|
19
|
+
.then(async (r) => ({ status: r.status, body: r.ok ? await r.text() : "" }))
|
|
20
|
+
.catch((e: Error) => ({ status: 0, body: "", error: e.message }));
|
|
21
|
+
|
|
22
|
+
/** Listing URL from the directory's sitemap(s), for directories with no known URL pattern. */
|
|
23
|
+
export async function findInSitemap(site: string, slugs: string[]) {
|
|
24
|
+
const origin = new URL(site).origin;
|
|
25
|
+
const robots = (await get(`${origin}/robots.txt`)).body;
|
|
26
|
+
const queue = [...robots.matchAll(/^sitemap:\s*(\S+)/gim)].map((m) => m[1]!);
|
|
27
|
+
if (!queue.length) queue.push(`${origin}/sitemap.xml`);
|
|
28
|
+
// ponytail: fetches at most 30 sitemap files per directory; huge directories may hide the listing deeper.
|
|
29
|
+
for (let i = 0; i < queue.length && i < 30; i++) {
|
|
30
|
+
const xml = (await get(queue[i]!)).body;
|
|
31
|
+
const urls = locs(xml);
|
|
32
|
+
if (/<sitemapindex/i.test(xml)) queue.push(...urls);
|
|
33
|
+
else {
|
|
34
|
+
const hit = urls.find((u) => matchesSlug(u, slugs));
|
|
35
|
+
if (hit) return hit;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Rendered page in a throwaway anonymous browser: client-rendered pages and sites that serve fetch a different page. */
|
|
41
|
+
function render(url: string, session: string) {
|
|
42
|
+
const ab = (...args: string[]) => Bun.spawnSync(["agent-browser", "--session", session, ...args], { stderr: "pipe", timeout: 60_000 });
|
|
43
|
+
if (ab("open", url).exitCode) return;
|
|
44
|
+
ab("wait", "2000");
|
|
45
|
+
const out = ab("eval", `JSON.stringify([location.href, document.body.innerText + [...document.querySelectorAll("a[href]")].map(a => a.href).join(" ")])`);
|
|
46
|
+
try {
|
|
47
|
+
let v = JSON.parse(out.stdout.toString());
|
|
48
|
+
if (typeof v === "string") v = JSON.parse(v);
|
|
49
|
+
return { finalUrl: v[0] as string, body: v[1] as string };
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function checkListing(url: string, host: string, slugs: string[], session: string) {
|
|
54
|
+
const res = await get(url);
|
|
55
|
+
if (isLive(res.body, host)) return { live: true, how: "fetch" };
|
|
56
|
+
if (res.status === 404 || res.status === 410) return { live: false, how: `not yet (${res.status})` };
|
|
57
|
+
// Fetch saw no listing (bot wall, JS-only page, soft 404) → look like a person.
|
|
58
|
+
const page = render(url, session);
|
|
59
|
+
// A redirect to the homepage can still show the product in a "recent" list; that's not its listing.
|
|
60
|
+
const live = !!page && matchesSlug(page.finalUrl, slugs) && isLive(page.body, host);
|
|
61
|
+
return { live, how: live ? "browser" : `not yet (${res.status || ("error" in res && res.error)}, browser: ${page ? "no listing" : "failed"})` };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const closeBrowser = (session: string) => Bun.spawnSync(["agent-browser", "--session", session, "close"], { stderr: "ignore" });
|
package/src/manual.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Db } from "./db";
|
|
2
|
+
import * as J from "./jobs";
|
|
3
|
+
import { loadDirectories, normalizeUrl } from "./registry";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Records a listing the human made outside the agents. `target` is a registry id or any URL: a URL that
|
|
7
|
+
* matches a registry directory's site maps to it, anything else is tracked under its domain.
|
|
8
|
+
*/
|
|
9
|
+
export async function addManualSubmission(db: Db, productId: string, target: string, outcome: J.Outcome, listingUrl?: string) {
|
|
10
|
+
if (!["pending", "live", "rejected"].includes(outcome)) throw new Error("outcome must be pending, live or rejected");
|
|
11
|
+
const dirs = await loadDirectories();
|
|
12
|
+
let subject = dirs.find((d) => d.id === target)?.id;
|
|
13
|
+
if (!subject) {
|
|
14
|
+
let domain: string;
|
|
15
|
+
try {
|
|
16
|
+
domain = normalizeUrl(target).domain;
|
|
17
|
+
} catch {
|
|
18
|
+
throw new Error(`${target} is neither a registry directory id nor a URL`);
|
|
19
|
+
}
|
|
20
|
+
const matches = dirs.filter((d) => normalizeUrl(d.website).domain === domain).map((d) => d.id);
|
|
21
|
+
if (matches.length > 1) throw new Error(`${domain} matches several directories (${matches.join(", ")}); pass the id`);
|
|
22
|
+
subject = matches[0] ?? domain;
|
|
23
|
+
}
|
|
24
|
+
const job = J.enqueue(db, "submission", subject, productId);
|
|
25
|
+
return { ...J.recordManual(db, job.id, outcome, listingUrl?.trim() || undefined), inRegistry: dirs.some((d) => d.id === subject) };
|
|
26
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { and, desc, eq, inArray } from "drizzle-orm";
|
|
2
|
+
import type { Db } from "./db";
|
|
3
|
+
import { productApprovals } from "./db/schema";
|
|
4
|
+
|
|
5
|
+
export function requestProductApproval(db: Db, input: { productId: string; action: string; message: string }) {
|
|
6
|
+
const existing = db.select().from(productApprovals)
|
|
7
|
+
.where(and(eq(productApprovals.productId, input.productId), eq(productApprovals.action, input.action), inArray(productApprovals.status, ["pending", "approved"])))
|
|
8
|
+
.orderBy(desc(productApprovals.id)).get();
|
|
9
|
+
if (existing) return existing;
|
|
10
|
+
return db.insert(productApprovals).values({ ...input, status: "pending" }).returning().get();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveProductApproval(db: Db, id: number, status: "approved" | "rejected") {
|
|
14
|
+
const row = db.update(productApprovals).set({ status, resolvedAt: Date.now() })
|
|
15
|
+
.where(and(eq(productApprovals.id, id), eq(productApprovals.status, "pending"))).returning().get();
|
|
16
|
+
if (!row) throw new Error(`product approval ${id} is not pending`);
|
|
17
|
+
return row;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function approvedProductAction(db: Db, productId: string, action: string) {
|
|
21
|
+
return db.select().from(productApprovals)
|
|
22
|
+
.where(and(eq(productApprovals.productId, productId), eq(productApprovals.action, action), eq(productApprovals.status, "approved")))
|
|
23
|
+
.orderBy(desc(productApprovals.id)).get();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function consumeProductApproval(db: Db, id: number) {
|
|
27
|
+
const row = db.update(productApprovals).set({ status: "used", resolvedAt: Date.now() })
|
|
28
|
+
.where(and(eq(productApprovals.id, id), eq(productApprovals.status, "approved"))).returning().get();
|
|
29
|
+
if (!row) throw new Error(`product approval ${id} is not approved`);
|
|
30
|
+
return row;
|
|
31
|
+
}
|
package/src/public.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registry is shared and published: it must say what a directory is like, never what one product or one
|
|
3
|
+
* operator did there. These helpers drop whole sentences that mention a workspace product, an operator, or
|
|
4
|
+
* internal tooling.
|
|
5
|
+
*/
|
|
6
|
+
const INTERNAL = /workspace\/|\bdata\/|permissions?\.yaml|add_backlink|money\.spend|external\.\w|per (your|the user'?s?) instruction|\bvault\b|launch work|bun launch|agent-browser|scripts\/ab|LAUNCH_\w+|@gmail\.com|automatic review (rejected|blocked)|evidence: data/i;
|
|
7
|
+
|
|
8
|
+
export function scrubber(productWords: string[]) {
|
|
9
|
+
const names = productWords.filter((w) => w.length >= 3).map((w) => w.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
10
|
+
const product = names.length ? new RegExp(`\\b(${names.join("|")})\\b`, "i") : null;
|
|
11
|
+
const bad = (s: string) => INTERNAL.test(s) || (product?.test(s) ?? false);
|
|
12
|
+
/** Keeps every sentence (or markdown line) that says nothing about a specific product or operator. */
|
|
13
|
+
const text = (t: string) => (t.split(/(?<=[.!?])\s+|\n/).some(bad) ? clean(t) : t);
|
|
14
|
+
const clean = (t: string) =>
|
|
15
|
+
t
|
|
16
|
+
.split("\n")
|
|
17
|
+
.map((line) => line.split(/(?<=[.!?])\s+/).filter((s) => !bad(s)).join(" "))
|
|
18
|
+
.filter((line, i, all) => line.trim() !== "" || (i > 0 && all[i - 1]!.trim() !== "" && !/^\s*[-*]\s*$/.test(all[i - 1]!)))
|
|
19
|
+
.filter((line) => !/^\s*[-*]\s*$/.test(line))
|
|
20
|
+
.join("\n")
|
|
21
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
22
|
+
.trim();
|
|
23
|
+
return { text, bad };
|
|
24
|
+
}
|
package/src/ratings.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { desc, inArray } from "drizzle-orm";
|
|
2
|
+
import type { Db } from "./db";
|
|
3
|
+
import { domainRatings } from "./db/schema";
|
|
4
|
+
|
|
5
|
+
/** Ahrefs' free Domain Rating endpoint. Bring your own key via AHREFS_API_KEY; never a flag or file. */
|
|
6
|
+
const ENDPOINT = "https://api.ahrefs.com/v3/public/domain-rating-free";
|
|
7
|
+
const BATCH_SIZE = 80;
|
|
8
|
+
export const RATING_TTL = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
export function ratingDomain(website: string): string {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(website.includes("://") ? website : `https://${website}`).hostname.replace(/^www\./, "").toLowerCase();
|
|
13
|
+
} catch {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A list hosted on a platform (a GitHub repo, a Notion page) would inherit the platform's DR, which says nothing about the list.
|
|
19
|
+
const PLATFORM = /^(github\.com|gitlab\.com|[\w-]+\.notion\.site|notion\.so|medium\.com|[\w-]+\.substack\.com|reddit\.com|docs\.google\.com)$/;
|
|
20
|
+
export const platformHosted = (website: string) => PLATFORM.test(ratingDomain(website));
|
|
21
|
+
|
|
22
|
+
/** Every reading per domain, newest first. */
|
|
23
|
+
export function ratingHistory(db: Db, websites: string[]) {
|
|
24
|
+
const domains = [...new Set(websites.map(ratingDomain).filter(Boolean))];
|
|
25
|
+
const rows = domains.length
|
|
26
|
+
? db.select().from(domainRatings).where(inArray(domainRatings.domain, domains)).orderBy(desc(domainRatings.measuredAt)).all()
|
|
27
|
+
: [];
|
|
28
|
+
const byDomain = new Map(domains.map((d) => [d, [] as { measuredAt: number; rating: number }[]]));
|
|
29
|
+
for (const r of rows) byDomain.get(r.domain)!.push({ measuredAt: r.measuredAt, rating: r.rating });
|
|
30
|
+
return byDomain;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function recordRating(db: Db, domain: string, rating: number, measuredAt: number) {
|
|
34
|
+
db.insert(domainRatings).values({ domain, rating, measuredAt }).onConflictDoNothing().run();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type Fetch = (url: string, init: RequestInit) => Promise<Response>;
|
|
38
|
+
|
|
39
|
+
/** Measures every domain not read in the last week (or all with `force`). One POST per batch of 80. */
|
|
40
|
+
export async function refreshRatings(
|
|
41
|
+
db: Db,
|
|
42
|
+
websites: string[],
|
|
43
|
+
{ apiKey, force = false, now = Date.now(), fetcher = fetch as Fetch }: { apiKey: string; force?: boolean; now?: number; fetcher?: Fetch },
|
|
44
|
+
) {
|
|
45
|
+
const history = ratingHistory(db, websites);
|
|
46
|
+
const due = [...history].filter(([, h]) => force || !h[0] || now - h[0].measuredAt >= RATING_TTL).map(([d]) => d);
|
|
47
|
+
const measured: Record<string, number> = {};
|
|
48
|
+
for (let i = 0; i < due.length; i += BATCH_SIZE) {
|
|
49
|
+
const chunk = due.slice(i, i + BATCH_SIZE);
|
|
50
|
+
const res = await fetcher(`${ENDPOINT}?output=json`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json", "Content-Type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ targets: chunk, version: "dr3" }),
|
|
54
|
+
redirect: "manual",
|
|
55
|
+
signal: AbortSignal.timeout(15000),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
await res.body?.cancel();
|
|
59
|
+
throw new Error(res.status === 401 || res.status === 403 ? "Ahrefs rejected AHREFS_API_KEY" : `Ahrefs returned HTTP ${res.status}`);
|
|
60
|
+
}
|
|
61
|
+
const data = (await res.json()) as { domain_rating?: { targets?: { target?: unknown; domain_rating?: unknown }[] } };
|
|
62
|
+
for (const entry of data.domain_rating?.targets ?? []) {
|
|
63
|
+
const domain = typeof entry.target === "string" ? ratingDomain(entry.target) : "";
|
|
64
|
+
const value = entry.domain_rating;
|
|
65
|
+
if (!chunk.includes(domain) || typeof value !== "number" || !(value >= 0 && value <= 100)) continue;
|
|
66
|
+
recordRating(db, domain, value, now);
|
|
67
|
+
measured[domain] = value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { checked: due.length, skipped: history.size - due.length, measured };
|
|
71
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Directory, loadDirectories, loadDirectory, writeDirectory } from "./registry";
|
|
3
|
+
|
|
4
|
+
/** Where `registry updates` looks unless LAUNCH_REGISTRY_URL or --url says otherwise. Built by scripts/build-site.ts and redeployed monthly. */
|
|
5
|
+
export const DEFAULT_REGISTRY_URL = "https://dirpush.com/registry.json";
|
|
6
|
+
export const registrySource = () => process.env.LAUNCH_REGISTRY_URL ?? DEFAULT_REGISTRY_URL;
|
|
7
|
+
|
|
8
|
+
/** The published registry: every verified directory plus its playbook. Built by scripts/build-registry.ts. */
|
|
9
|
+
export const RegistryFile = z.object({
|
|
10
|
+
revision: z.string(),
|
|
11
|
+
generatedAt: z.string(),
|
|
12
|
+
directories: z.array(z.object({ directory: Directory, playbook: z.string().nullable() })),
|
|
13
|
+
});
|
|
14
|
+
export type RegistryFile = z.infer<typeof RegistryFile>;
|
|
15
|
+
|
|
16
|
+
export async function buildRegistryFile(): Promise<RegistryFile> {
|
|
17
|
+
const dirs = (await loadDirectories()).sort((a, b) => a.id.localeCompare(b.id));
|
|
18
|
+
const directories = await Promise.all(dirs.map((d) => loadDirectory(d.id)));
|
|
19
|
+
const digest = new Bun.CryptoHasher("sha256").update(JSON.stringify(directories)).digest("hex");
|
|
20
|
+
return { revision: digest.slice(0, 16), generatedAt: new Date().toISOString(), directories };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** `source`: an https URL or a local path to a registry.json. */
|
|
24
|
+
export async function fetchRegistryFile(source: string) {
|
|
25
|
+
const text = /^https?:\/\//.test(source)
|
|
26
|
+
? await fetch(source, { signal: AbortSignal.timeout(30000) }).then((r) => {
|
|
27
|
+
if (!r.ok) throw new Error(`registry download failed: HTTP ${r.status}`);
|
|
28
|
+
return r.text();
|
|
29
|
+
})
|
|
30
|
+
: await Bun.file(source).text();
|
|
31
|
+
return RegistryFile.parse(JSON.parse(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const same = (a: unknown, b: unknown) => JSON.stringify(a) === JSON.stringify(b);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compares the published registry with the local one. A local entry verified more recently than the
|
|
38
|
+
* published copy is kept: that is a correction you made and haven't reported yet.
|
|
39
|
+
*/
|
|
40
|
+
export async function registryUpdates(remote: RegistryFile, apply = false) {
|
|
41
|
+
const local = new Map((await loadDirectories()).map((d) => [d.id, d]));
|
|
42
|
+
const added: string[] = [], changed: string[] = [], localNewer: string[] = [];
|
|
43
|
+
for (const { directory, playbook } of remote.directories) {
|
|
44
|
+
const mine = local.get(directory.id);
|
|
45
|
+
local.delete(directory.id);
|
|
46
|
+
if (!mine) added.push(directory.id);
|
|
47
|
+
else if (mine.verification.checked_at > directory.verification.checked_at) localNewer.push(directory.id);
|
|
48
|
+
else if (!same(mine, directory) || !same((await loadDirectory(directory.id)).playbook, playbook)) changed.push(directory.id);
|
|
49
|
+
else continue;
|
|
50
|
+
if (apply && !localNewer.includes(directory.id)) await writeDirectory(directory, playbook ?? undefined);
|
|
51
|
+
}
|
|
52
|
+
// Removed upstream: reported, never deleted, so nothing you rely on disappears silently.
|
|
53
|
+
return { revision: remote.revision, generatedAt: remote.generatedAt, applied: apply, added, changed, localNewer, onlyLocal: [...local.keys()].sort() };
|
|
54
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, readdirSync, existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { HOME, PKG } from "./db";
|
|
5
|
+
import { CLASSIFICATIONS } from "./db/schema";
|
|
6
|
+
import { DIRECTORY_KINDS } from "./directory-kinds";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Your copy of the registry. An installed package seeds ~/.dirpush/registry from the one it ships, once; after that
|
|
10
|
+
* `registry updates --apply` keeps it current, and edits never touch the package.
|
|
11
|
+
*/
|
|
12
|
+
export const REGISTRY = process.env.LAUNCH_REGISTRY ?? join(HOME, "registry/directories");
|
|
13
|
+
if (!process.env.LAUNCH_REGISTRY && HOME !== PKG && !existsSync(REGISTRY)) cpSync(join(PKG, "registry/directories"), REGISTRY, { recursive: true });
|
|
14
|
+
|
|
15
|
+
export const Directory = z.object({
|
|
16
|
+
id: z.string().regex(/^[a-z0-9-]+$/),
|
|
17
|
+
name: z.string(),
|
|
18
|
+
website: z.url(),
|
|
19
|
+
submission_url: z.url().nullable(),
|
|
20
|
+
cost: z.object({
|
|
21
|
+
classification: z.enum(CLASSIFICATIONS).exclude(["UNVERIFIED", "DEAD", "NOT_RELEVANT"]),
|
|
22
|
+
notes: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
audience: z.array(z.string()).default([]),
|
|
25
|
+
authentication: z.object({ required: z.boolean(), signup_allowed: z.boolean(), methods: z.array(z.string()).default([]) }),
|
|
26
|
+
submission: z.object({
|
|
27
|
+
url_first: z.boolean(),
|
|
28
|
+
fields: z.array(z.object({ name: z.string(), required: z.boolean(), limit: z.number().optional() })).default([]),
|
|
29
|
+
conditions: z.array(z.string()).default([]), // e.g. "backlink required", "manual review"
|
|
30
|
+
}),
|
|
31
|
+
autofetch: z.object({ supported: z.boolean(), expected: z.array(z.string()).default([]) }),
|
|
32
|
+
media: z.object({
|
|
33
|
+
logo: z.boolean(),
|
|
34
|
+
screenshots: z.object({ supported: z.boolean(), max: z.number().optional() }),
|
|
35
|
+
}),
|
|
36
|
+
// What kind of site this really is. Only "listing" is a plain directory submission; the rest need a human decision or timing.
|
|
37
|
+
kind: z.enum(Object.keys(DIRECTORY_KINDS) as [keyof typeof DIRECTORY_KINDS, ...(keyof typeof DIRECTORY_KINDS)[]]).optional(),
|
|
38
|
+
// Set when the site's bot check rejects the automated browser outright (why/where). Such directories are for the human.
|
|
39
|
+
human_only: z.string().nullable().optional(),
|
|
40
|
+
// Free-tier wait before a listing goes live, as the site states it ("instant", "3-4 months").
|
|
41
|
+
review_time: z.string().optional(),
|
|
42
|
+
// Where a listing appears once live, {slug} = product name lowercased; lets a plain script check pending listings.
|
|
43
|
+
listing_url_pattern: z.string().optional(),
|
|
44
|
+
// Measured by `registry audit` (no AI): what a listing's link is worth, read off real listing pages.
|
|
45
|
+
link: z
|
|
46
|
+
.object({
|
|
47
|
+
rel: z.enum(["dofollow", "nofollow", "ugc", "sponsored", "mixed", "unknown"]),
|
|
48
|
+
method: z.enum(["own_listing", "sample_listing", "platform_rule", "none"]),
|
|
49
|
+
evidence: z.string().nullable(),
|
|
50
|
+
samples: z.number(),
|
|
51
|
+
noindex: z.boolean().nullable(),
|
|
52
|
+
claimed: z.enum(["dofollow", "nofollow"]).nullable(),
|
|
53
|
+
bot_check: z.enum(["recaptcha", "turnstile", "hcaptcha", "math", "none", "unknown"]),
|
|
54
|
+
checked_at: z.string(),
|
|
55
|
+
// Why rel is still "unknown" after a real attempt: shown on the site instead of a bare "not measured".
|
|
56
|
+
unmeasured: z.enum(["no_listings", "no_outbound_link", "bot_wall", "login_required", "site_down"]).optional(),
|
|
57
|
+
})
|
|
58
|
+
.optional(),
|
|
59
|
+
verification: z.object({
|
|
60
|
+
status: z.literal("verified"),
|
|
61
|
+
checked_at: z.string(),
|
|
62
|
+
by: z.string().optional(),
|
|
63
|
+
sources: z.array(z.string()).default([]),
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
export type Directory = z.infer<typeof Directory>;
|
|
67
|
+
|
|
68
|
+
export function normalizeUrl(raw: string) {
|
|
69
|
+
const u = new URL(raw.trim().includes("://") ? raw.trim() : `https://${raw.trim()}`);
|
|
70
|
+
u.hash = "";
|
|
71
|
+
const domain = u.hostname.toLowerCase().replace(/^www\./, "");
|
|
72
|
+
return { domain, url: u.toString() };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function loadDirectories(): Promise<Directory[]> {
|
|
76
|
+
if (!existsSync(REGISTRY)) return [];
|
|
77
|
+
const ids = readdirSync(REGISTRY).filter((d) => existsSync(join(REGISTRY, d, "directory.yaml")));
|
|
78
|
+
return Promise.all(ids.map(async (id) => Directory.parse(Bun.YAML.parse(await Bun.file(join(REGISTRY, id, "directory.yaml")).text()))));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function loadDirectory(id: string) {
|
|
82
|
+
const dir = join(REGISTRY, id);
|
|
83
|
+
if (!existsSync(join(dir, "directory.yaml"))) throw new Error(`no such directory: ${id}`);
|
|
84
|
+
const directory = Directory.parse(Bun.YAML.parse(await Bun.file(join(dir, "directory.yaml")).text()));
|
|
85
|
+
const playbook = existsSync(join(dir, "playbook.md")) ? await Bun.file(join(dir, "playbook.md")).text() : null;
|
|
86
|
+
return { directory, playbook };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function writeDirectory(d: Directory, playbook?: string) {
|
|
90
|
+
const dir = join(REGISTRY, d.id);
|
|
91
|
+
mkdirSync(dir, { recursive: true });
|
|
92
|
+
await Bun.write(join(dir, "directory.yaml"), Bun.YAML.stringify(Directory.parse(d), null, 2));
|
|
93
|
+
if (playbook) await Bun.write(join(dir, "playbook.md"), playbook);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function removeDirectory(id: string) {
|
|
97
|
+
const dir = join(REGISTRY, id);
|
|
98
|
+
if (!/^[a-z0-9-]+$/.test(id) || !existsSync(dir)) return false;
|
|
99
|
+
rmSync(dir, { recursive: true });
|
|
100
|
+
return true;
|
|
101
|
+
}
|
package/src/review.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import type { Db } from "./db";
|
|
3
|
+
import { galleries, jobs, productApprovals, registryChanges, todos } from "./db/schema";
|
|
4
|
+
import { logEvent, unblock } from "./jobs";
|
|
5
|
+
|
|
6
|
+
/** Human decisions, shared by the CLI and the dashboard. */
|
|
7
|
+
export function reviewGallery(db: Db, revision: number, status: "approved" | "rejected") {
|
|
8
|
+
const row = db
|
|
9
|
+
.update(galleries)
|
|
10
|
+
.set({ status })
|
|
11
|
+
.where(and(eq(galleries.revision, revision), eq(galleries.status, "proposed")))
|
|
12
|
+
.returning()
|
|
13
|
+
.get();
|
|
14
|
+
if (!row) throw new Error(`gallery revision ${revision} is not awaiting review`);
|
|
15
|
+
logEvent(db, null, `gallery_${status}`, "human", { revision });
|
|
16
|
+
// Jobs parked on this product's gallery go back to the queue; the agent reads the decision.
|
|
17
|
+
const waiting = db
|
|
18
|
+
.select({ id: jobs.id })
|
|
19
|
+
.from(jobs)
|
|
20
|
+
.where(and(eq(jobs.productId, row.productId), eq(jobs.status, "blocked"), eq(jobs.blockType, "gallery_approval")))
|
|
21
|
+
.all();
|
|
22
|
+
for (const j of waiting) unblock(db, j.id, `gallery revision ${revision} ${status}`);
|
|
23
|
+
return row;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Accepting only marks the proposal; an agent applies it to registry/directories/<id>/directory.yaml. */
|
|
27
|
+
export function reviewRegistryChange(db: Db, id: number, status: "accepted" | "rejected") {
|
|
28
|
+
const row = db
|
|
29
|
+
.update(registryChanges)
|
|
30
|
+
.set({ status })
|
|
31
|
+
.where(and(eq(registryChanges.id, id), eq(registryChanges.status, "proposed")))
|
|
32
|
+
.returning()
|
|
33
|
+
.get();
|
|
34
|
+
if (!row) throw new Error(`registry change ${id} is not awaiting review`);
|
|
35
|
+
logEvent(db, null, `registry_change_${status}`, "human", { changeId: id, directoryId: row.directoryId });
|
|
36
|
+
return row;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function reviewProductApproval(db: Db, id: number, status: "approved" | "rejected") {
|
|
40
|
+
const row = db.update(productApprovals).set({ status, resolvedAt: Date.now() })
|
|
41
|
+
.where(and(eq(productApprovals.id, id), eq(productApprovals.status, "pending"))).returning().get();
|
|
42
|
+
if (!row) throw new Error(`product approval ${id} is not pending`);
|
|
43
|
+
logEvent(db, null, `product_approval_${status}`, "human", { approvalId: row.id, productId: row.productId, action: row.action });
|
|
44
|
+
return row;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function addTodo(db: Db, todo: { productId?: string; jobId?: string; message: string; url?: string }) {
|
|
48
|
+
const row = db.insert(todos).values(todo).returning().get();
|
|
49
|
+
logEvent(db, todo.jobId ?? null, "todo_added", null, { todo: row.id, message: todo.message });
|
|
50
|
+
return row;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function closeTodo(db: Db, id: number) {
|
|
54
|
+
const row = db.update(todos).set({ status: "done" }).where(and(eq(todos.id, id), eq(todos.status, "open"))).returning().get();
|
|
55
|
+
if (!row) throw new Error(`todo ${id} is not open`);
|
|
56
|
+
logEvent(db, row.jobId, "todo_done", "human", { todo: id });
|
|
57
|
+
return row;
|
|
58
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { desc, eq, sql } from "drizzle-orm";
|
|
2
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
3
|
+
import { join, resolve, sep } from "node:path";
|
|
4
|
+
import { HOME, openDb, PKG } from "./db";
|
|
5
|
+
import { events, galleries, jobs, productApprovals, registryChanges, todos } from "./db/schema";
|
|
6
|
+
import * as J from "./jobs";
|
|
7
|
+
import { loadDirectories, loadDirectory } from "./registry";
|
|
8
|
+
import { addManualSubmission } from "./manual";
|
|
9
|
+
import { platformHosted, ratingDomain, ratingHistory, refreshRatings } from "./ratings";
|
|
10
|
+
import { fetchRegistryFile, registrySource, registryUpdates } from "./registry-updates";
|
|
11
|
+
import { closeTodo, reviewGallery, reviewProductApproval, reviewRegistryChange } from "./review";
|
|
12
|
+
import { loadIdentity, loadPermissions, loadProduct, productIds, WORKSPACE } from "./workspace";
|
|
13
|
+
import index from "./web/index.html";
|
|
14
|
+
|
|
15
|
+
/** Local dashboard: observability + human decisions. Agents use the CLI, not this. */
|
|
16
|
+
export function serve(port: number, hostname = "127.0.0.1") {
|
|
17
|
+
const db = openDb();
|
|
18
|
+
const safe = (fn: () => unknown) => async () => {
|
|
19
|
+
try {
|
|
20
|
+
return Response.json(await fn());
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return Response.json({ error: (e as Error).message }, { status: 400 });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const body = async (req: Request) => ((await req.json().catch(() => ({}))) ?? {}) as Record<string, string | undefined>;
|
|
26
|
+
/** Read-only files confined to `root` (screenshots for review and evidence). */
|
|
27
|
+
const files = (root: string, prefix: string) => (req: Request) => {
|
|
28
|
+
const path = resolve(root, decodeURIComponent(new URL(req.url).pathname.slice(prefix.length)));
|
|
29
|
+
if (!path.startsWith(resolve(root) + sep)) return new Response("forbidden", { status: 403 });
|
|
30
|
+
return new Response(Bun.file(path));
|
|
31
|
+
};
|
|
32
|
+
const EVIDENCE = join(HOME, "data/evidence");
|
|
33
|
+
const optional = async <T>(fn: () => Promise<T>) => fn().catch((e: Error) => ({ error: e.message }));
|
|
34
|
+
|
|
35
|
+
const server = Bun.serve({
|
|
36
|
+
port,
|
|
37
|
+
hostname,
|
|
38
|
+
development: process.env.NODE_ENV !== "production",
|
|
39
|
+
routes: {
|
|
40
|
+
"/": index,
|
|
41
|
+
"/api/overview": safe(() => ({
|
|
42
|
+
jobs: db.select({ kind: jobs.kind, status: jobs.status, n: sql<number>`count(*)` }).from(jobs).groupBy(jobs.kind, jobs.status).all(),
|
|
43
|
+
outcomes: db
|
|
44
|
+
.select({ outcome: jobs.outcome, n: sql<number>`count(*)` })
|
|
45
|
+
.from(jobs)
|
|
46
|
+
.where(eq(jobs.kind, "submission"))
|
|
47
|
+
.groupBy(jobs.outcome)
|
|
48
|
+
.all(),
|
|
49
|
+
activity: db.select().from(events).orderBy(desc(events.at)).limit(60).all(),
|
|
50
|
+
})),
|
|
51
|
+
"/api/actions": safe(() => ({
|
|
52
|
+
blocked: db.select().from(jobs).where(eq(jobs.status, "blocked")).all(),
|
|
53
|
+
unknown: db.select().from(jobs).where(eq(jobs.status, "unknown")).all(),
|
|
54
|
+
failed: db.select().from(jobs).where(eq(jobs.status, "failed")).all(),
|
|
55
|
+
galleries: db.select().from(galleries).where(eq(galleries.status, "proposed")).all(),
|
|
56
|
+
productApprovals: db.select().from(productApprovals).where(eq(productApprovals.status, "pending")).all(),
|
|
57
|
+
registryChanges: db.select().from(registryChanges).where(eq(registryChanges.status, "proposed")).all(),
|
|
58
|
+
todos: db.select().from(todos).where(eq(todos.status, "open")).all(),
|
|
59
|
+
})),
|
|
60
|
+
"/api/todos/:id/done": { POST: (req) => safe(() => closeTodo(db, Number(req.params.id)))() },
|
|
61
|
+
"/api/jobs": (req) =>
|
|
62
|
+
safe(() => {
|
|
63
|
+
const kind = new URL(req.url).searchParams.get("kind");
|
|
64
|
+
return db.select().from(jobs).where(eq(jobs.kind, kind === "research" ? "research" : "submission")).orderBy(desc(jobs.updatedAt)).all();
|
|
65
|
+
})(),
|
|
66
|
+
"/api/meta": safe(() => ({ registrySource: registrySource(), ahrefs: Boolean(process.env.AHREFS_API_KEY?.trim()) })),
|
|
67
|
+
// Latest DR per directory domain plus full history per product (newest first).
|
|
68
|
+
"/api/ratings": safe(async () => {
|
|
69
|
+
const products = (await Promise.all(productIds().map((id) => loadProduct(id).then((p) => ({ id, url: p.url })).catch(() => null)))).filter((p) => p !== null);
|
|
70
|
+
const directories = await loadDirectories();
|
|
71
|
+
const all = ratingHistory(db, [...products.map((p) => p.url), ...directories.map((d) => d.website)]);
|
|
72
|
+
return {
|
|
73
|
+
products: Object.fromEntries(products.map((p) => [p.id, all.get(ratingDomain(p.url)) ?? []])),
|
|
74
|
+
directories: Object.fromEntries(directories.map((d) => [d.id, platformHosted(d.website) ? null : all.get(ratingDomain(d.website))?.[0]?.rating ?? null])),
|
|
75
|
+
};
|
|
76
|
+
}),
|
|
77
|
+
"/api/ratings/refresh": {
|
|
78
|
+
POST: () =>
|
|
79
|
+
safe(async () => {
|
|
80
|
+
const apiKey = process.env.AHREFS_API_KEY?.trim();
|
|
81
|
+
if (!apiKey) throw new Error("Set AHREFS_API_KEY in the environment you start the dashboard from.");
|
|
82
|
+
const urls = (await Promise.all(productIds().map((id) => loadProduct(id).then((p) => p.url).catch(() => null)))).filter((u): u is string => !!u);
|
|
83
|
+
return refreshRatings(db, urls, { apiKey });
|
|
84
|
+
})(),
|
|
85
|
+
},
|
|
86
|
+
// Compares the published registry with the local one; { apply: true } writes the updates.
|
|
87
|
+
"/api/registry/updates": {
|
|
88
|
+
POST: async (req) => {
|
|
89
|
+
const { apply } = (await req.json().catch(() => ({}))) as { apply?: boolean };
|
|
90
|
+
return safe(async () => ({ source: registrySource(), checkedAt: Date.now(), ...(await registryUpdates(await fetchRegistryFile(registrySource()), apply === true)) }))();
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
"/api/jobs/:id": (req) =>
|
|
94
|
+
safe(async () => {
|
|
95
|
+
const job = db.select().from(jobs).where(eq(jobs.id, req.params.id)).get();
|
|
96
|
+
if (!job) throw new Error(`no such job: ${req.params.id}`);
|
|
97
|
+
const dir = join(EVIDENCE, job.id);
|
|
98
|
+
return {
|
|
99
|
+
job,
|
|
100
|
+
events: J.jobEvents(db, job.id),
|
|
101
|
+
evidence: /^[\w-]+$/.test(job.id) && existsSync(dir) ? readdirSync(dir).filter((f) => /\.(png|jpe?g|webp)$/i.test(f)).sort() : [],
|
|
102
|
+
directory: job.kind === "submission" ? (await optional(() => loadDirectory(job.subject).then((d) => d.directory))) : null,
|
|
103
|
+
};
|
|
104
|
+
})(),
|
|
105
|
+
"/api/jobs/:id/unblock": {
|
|
106
|
+
POST: async (req) => {
|
|
107
|
+
const { note, approve } = await body(req);
|
|
108
|
+
return safe(() => J.unblock(db, req.params.id, note?.trim() || (approve ? "approved" : "resolved via dashboard"), Date.now(), approve === "yes"))();
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"/api/jobs/:id/outcome": {
|
|
112
|
+
POST: async (req) => {
|
|
113
|
+
const { outcome, listingUrl } = await body(req);
|
|
114
|
+
if (outcome !== "live" && outcome !== "rejected" && outcome !== "pending") return Response.json({ error: "outcome must be live, rejected or pending" }, { status: 400 });
|
|
115
|
+
return safe(() => J.setOutcome(db, req.params.id, outcome, listingUrl?.trim() || undefined))();
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
// Opens plain Chrome on Google's sign-in page (scripts/google-login). Same-origin only: another site
|
|
119
|
+
// in the browser must not be able to pop this open.
|
|
120
|
+
"/api/google-login": {
|
|
121
|
+
POST: (req) => {
|
|
122
|
+
if ((req.headers.get("sec-fetch-site") ?? "same-origin") !== "same-origin") return new Response("forbidden", { status: 403 });
|
|
123
|
+
Bun.spawn([join(PKG, "scripts/google-login")], { cwd: PKG, env: { ...process.env, LAUNCH_HOME: HOME }, stdout: "ignore", stderr: "ignore" }).unref();
|
|
124
|
+
return Response.json({ ok: true });
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
// Raises the agents' Chrome on the worker's tab (scripts/show-browser). Same-origin only, as above.
|
|
128
|
+
"/api/show-browser": {
|
|
129
|
+
POST: async (req) => {
|
|
130
|
+
if ((req.headers.get("sec-fetch-site") ?? "same-origin") !== "same-origin") return new Response("forbidden", { status: 403 });
|
|
131
|
+
const p = Bun.spawn([join(PKG, "scripts/show-browser")], { cwd: PKG, env: { ...process.env, LAUNCH_HOME: HOME }, stdout: "ignore", stderr: "ignore" });
|
|
132
|
+
return (await p.exited) === 0 ? Response.json({ ok: true }) : Response.json({ error: "agent browser is not open" }, { status: 409 });
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
// Runs `submissions check` (plain fetch, no AI) for one job or all pending ones; marks live listings.
|
|
136
|
+
"/api/submissions/check": {
|
|
137
|
+
POST: async (req) => {
|
|
138
|
+
const { job } = (await req.json().catch(() => ({}))) as { job?: string };
|
|
139
|
+
const p = Bun.spawn([process.execPath, join(PKG, "src/cli.ts"), "submissions", "check", ...(job ? [job] : [])], { cwd: PKG, env: { ...process.env, LAUNCH_HOME: HOME }, stdout: "pipe", stderr: "ignore" });
|
|
140
|
+
const out = await new Response(p.stdout).text();
|
|
141
|
+
return Response.json({ results: JSON.parse(out.slice(out.indexOf("["))) });
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
// A listing the human made themselves, from the dashboard: { product, target, outcome, listingUrl }.
|
|
145
|
+
"/api/submissions/add": {
|
|
146
|
+
POST: async (req) => {
|
|
147
|
+
const { product, target, outcome, listingUrl } = await body(req);
|
|
148
|
+
return safe(async () => addManualSubmission(db, (await loadProduct(product)).id, target ?? "", (outcome ?? "pending") as J.Outcome, listingUrl))();
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
// "I submitted it myself" on an existing job.
|
|
152
|
+
"/api/jobs/:id/manual": {
|
|
153
|
+
POST: async (req) => {
|
|
154
|
+
const { outcome, listingUrl } = await body(req);
|
|
155
|
+
return safe(() => J.recordManual(db, req.params.id, (outcome ?? "pending") as J.Outcome, listingUrl?.trim() || undefined))();
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
"/api/jobs/:id/requeue": { POST: (req) => safe(() => J.requeue(db, req.params.id))() },
|
|
159
|
+
"/api/galleries/:rev/:decision": {
|
|
160
|
+
POST: (req) => safe(() => reviewGallery(db, Number(req.params.rev), req.params.decision === "approve" ? "approved" : "rejected"))(),
|
|
161
|
+
},
|
|
162
|
+
"/api/registry-changes/:id/:decision": {
|
|
163
|
+
POST: (req) => safe(() => reviewRegistryChange(db, Number(req.params.id), req.params.decision === "accept" ? "accepted" : "rejected"))(),
|
|
164
|
+
},
|
|
165
|
+
"/api/product-approvals/:id/:decision": {
|
|
166
|
+
POST: (req) => safe(() => reviewProductApproval(db, Number(req.params.id), req.params.decision === "approve" ? "approved" : "rejected"))(),
|
|
167
|
+
},
|
|
168
|
+
"/api/registry": safe(() => loadDirectories()),
|
|
169
|
+
"/api/registry/:id": (req) => safe(() => loadDirectory(req.params.id))(),
|
|
170
|
+
"/api/products": safe(() => Promise.all(productIds().map((id) => optional(() => loadProduct(id)).then((p) => ({ id, ...p }))))),
|
|
171
|
+
// Workspace-wide: who agents sign up as and what they may do.
|
|
172
|
+
"/api/settings": safe(async () => ({ identity: await optional(() => loadIdentity()), permissions: await optional(() => loadPermissions()) })),
|
|
173
|
+
"/api/products/:id": (req) =>
|
|
174
|
+
safe(async () => ({
|
|
175
|
+
product: await optional(() => loadProduct(req.params.id)),
|
|
176
|
+
galleries: db.select().from(galleries).where(eq(galleries.productId, req.params.id)).orderBy(desc(galleries.revision)).all(),
|
|
177
|
+
}))(),
|
|
178
|
+
// Screenshots for gallery review. Confined to the workspace directory.
|
|
179
|
+
"/workspace/*": files(WORKSPACE, "/workspace/"),
|
|
180
|
+
// Agent screenshots per job: data/evidence/<job-id>/*.png
|
|
181
|
+
"/evidence/*": files(EVIDENCE, "/evidence/"),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
console.log(`dirpush dashboard: ${server.url}`);
|
|
185
|
+
return server;
|
|
186
|
+
}
|