dirpush 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.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 +66 -2
- 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 +76 -4
- 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 +741 -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/db/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
3
|
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
|
4
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import * as schema from "./schema";
|
|
8
|
+
|
|
9
|
+
/** Where the package lives: registry, migrations, examples, scripts and the skill. Read-only once installed. */
|
|
10
|
+
export const PKG = join(import.meta.dir, "../..");
|
|
11
|
+
/**
|
|
12
|
+
* Where your launch lives: data/ (database, Chrome profile, evidence) and workspace/ (products, identity).
|
|
13
|
+
* A git checkout keeps it in the repo, as before; an installed package uses ~/.dirpush. LAUNCH_HOME overrides both.
|
|
14
|
+
*/
|
|
15
|
+
export const HOME = process.env.LAUNCH_HOME ?? (existsSync(join(PKG, ".git")) ? PKG : join(homedir(), ".dirpush"));
|
|
16
|
+
|
|
17
|
+
export function openDb(path = process.env.LAUNCH_DB ?? join(HOME, "data/launch.db")) {
|
|
18
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
const sqlite = new Database(path, { create: true, strict: true });
|
|
20
|
+
sqlite.run("PRAGMA journal_mode = WAL");
|
|
21
|
+
sqlite.run("PRAGMA busy_timeout = 5000");
|
|
22
|
+
sqlite.run("PRAGMA foreign_keys = ON");
|
|
23
|
+
const db = drizzle(sqlite, { schema });
|
|
24
|
+
migrate(db, { migrationsFolder: join(PKG, "drizzle") });
|
|
25
|
+
return db;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type Db = ReturnType<typeof openDb>;
|
|
29
|
+
export { schema };
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { integer, primaryKey, real, sqliteTable, text, uniqueIndex, index } from "drizzle-orm/sqlite-core";
|
|
3
|
+
|
|
4
|
+
const now = sql`(unixepoch() * 1000)`;
|
|
5
|
+
const json = <T>(name: string) => text(name, { mode: "json" }).$type<T>();
|
|
6
|
+
|
|
7
|
+
export const CLASSIFICATIONS = [
|
|
8
|
+
"UNVERIFIED",
|
|
9
|
+
"VERIFIED_FREE",
|
|
10
|
+
"FREEMIUM",
|
|
11
|
+
"CONDITIONAL_FREE",
|
|
12
|
+
"PAID",
|
|
13
|
+
"DEAD",
|
|
14
|
+
"NOT_RELEVANT",
|
|
15
|
+
] as const;
|
|
16
|
+
export type Classification = (typeof CLASSIFICATIONS)[number];
|
|
17
|
+
|
|
18
|
+
export type Provenance = { source: string; name?: string; url: string; claims?: Record<string, unknown> };
|
|
19
|
+
|
|
20
|
+
/** Unverified research queue. Only verified entries become registry/directories/*.yaml. */
|
|
21
|
+
export const candidates = sqliteTable("candidates", {
|
|
22
|
+
domain: text("domain").primaryKey(),
|
|
23
|
+
name: text("name"),
|
|
24
|
+
url: text("url").notNull(),
|
|
25
|
+
classification: text("classification").$type<Classification>().notNull().default("UNVERIFIED"),
|
|
26
|
+
provenance: json<Provenance[]>("provenance").notNull().default([]),
|
|
27
|
+
notes: text("notes"),
|
|
28
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
29
|
+
updatedAt: integer("updated_at").notNull().default(now),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const JOB_STATUSES = ["queued", "running", "blocked", "retry", "unknown", "failed", "done"] as const;
|
|
33
|
+
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
34
|
+
export type JobKind = "research" | "submission";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One row per canonical unit of work: a research job per candidate domain, a
|
|
38
|
+
* submission job per (product, directory). `status` is coarse and enforced;
|
|
39
|
+
* `step` is the agent's free-form checkpoint inside a running job.
|
|
40
|
+
*/
|
|
41
|
+
export const jobs = sqliteTable(
|
|
42
|
+
"jobs",
|
|
43
|
+
{
|
|
44
|
+
id: text("id").primaryKey(),
|
|
45
|
+
kind: text("kind").$type<JobKind>().notNull(),
|
|
46
|
+
subject: text("subject").notNull(), // candidate domain or directory id
|
|
47
|
+
productId: text("product_id").notNull().default(""),
|
|
48
|
+
status: text("status").$type<JobStatus>().notNull().default("queued"),
|
|
49
|
+
step: text("step").notNull().default("queued"),
|
|
50
|
+
worker: text("worker"),
|
|
51
|
+
leaseExpiresAt: integer("lease_expires_at"),
|
|
52
|
+
notBefore: integer("not_before"),
|
|
53
|
+
attempts: integer("attempts").notNull().default(0),
|
|
54
|
+
blockType: text("block_type"),
|
|
55
|
+
blockMessage: text("block_message"),
|
|
56
|
+
lastError: text("last_error"),
|
|
57
|
+
outcome: text("outcome"), // research: classification; submission: submitted|pending|live|rejected
|
|
58
|
+
listingUrl: text("listing_url"),
|
|
59
|
+
data: json<Record<string, unknown>>("data").notNull().default({}),
|
|
60
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
61
|
+
updatedAt: integer("updated_at").notNull().default(now),
|
|
62
|
+
},
|
|
63
|
+
(t) => [uniqueIndex("jobs_unique_subject").on(t.kind, t.productId, t.subject), index("jobs_status").on(t.status)],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export const events = sqliteTable(
|
|
67
|
+
"events",
|
|
68
|
+
{
|
|
69
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
70
|
+
jobId: text("job_id"),
|
|
71
|
+
type: text("type").notNull(),
|
|
72
|
+
worker: text("worker"),
|
|
73
|
+
data: json<Record<string, unknown>>("data").notNull().default({}),
|
|
74
|
+
at: integer("at").notNull().default(now),
|
|
75
|
+
},
|
|
76
|
+
(t) => [index("events_job").on(t.jobId)],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
export type GalleryItem = { id: string; path: string; description: string };
|
|
80
|
+
|
|
81
|
+
export const galleries = sqliteTable("galleries", {
|
|
82
|
+
revision: integer("revision").primaryKey({ autoIncrement: true }),
|
|
83
|
+
productId: text("product_id").notNull(),
|
|
84
|
+
items: json<GalleryItem[]>("items").notNull(),
|
|
85
|
+
status: text("status").$type<"proposed" | "approved" | "rejected">().notNull().default("proposed"),
|
|
86
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
/** Product-wide approvals (such as permission-gated asset capture), independent of directory jobs. */
|
|
90
|
+
export const productApprovals = sqliteTable("product_approvals", {
|
|
91
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
92
|
+
productId: text("product_id").notNull(),
|
|
93
|
+
action: text("action").notNull(),
|
|
94
|
+
message: text("message").notNull(),
|
|
95
|
+
status: text("status").$type<"pending" | "approved" | "rejected" | "used">().notNull().default("pending"),
|
|
96
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
97
|
+
resolvedAt: integer("resolved_at"),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const registryChanges = sqliteTable("registry_changes", {
|
|
101
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
102
|
+
directoryId: text("directory_id").notNull(),
|
|
103
|
+
observation: json<Record<string, unknown>>("observation").notNull(),
|
|
104
|
+
status: text("status").$type<"proposed" | "accepted" | "rejected">().notNull().default("proposed"),
|
|
105
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
/** Things only the human can do that don't hold up a job (verify an email, check a listing). */
|
|
109
|
+
export const todos = sqliteTable("todos", {
|
|
110
|
+
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
111
|
+
productId: text("product_id").notNull().default(""),
|
|
112
|
+
jobId: text("job_id"),
|
|
113
|
+
message: text("message").notNull(),
|
|
114
|
+
url: text("url"),
|
|
115
|
+
status: text("status").$type<"open" | "done">().notNull().default("open"),
|
|
116
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One row per directory account (per product unless shared). Written whenever a job checkpoints an `account`, so
|
|
121
|
+
* "do we already have a login here, and how do we sign in?" has one answer. Never holds a secret: passwords stay in
|
|
122
|
+
* the agent-browser vault, referenced by `vault`.
|
|
123
|
+
*/
|
|
124
|
+
export const ACCOUNT_METHODS = ["password", "google", "github", "magic_link", "other"] as const;
|
|
125
|
+
export const accounts = sqliteTable(
|
|
126
|
+
"accounts",
|
|
127
|
+
{
|
|
128
|
+
directoryId: text("directory_id").notNull(),
|
|
129
|
+
productId: text("product_id").notNull().default(""), // "" = one account shared by every product
|
|
130
|
+
method: text("method", { enum: ACCOUNT_METHODS }).notNull(),
|
|
131
|
+
username: text("username"),
|
|
132
|
+
vault: text("vault"),
|
|
133
|
+
jobId: text("job_id"),
|
|
134
|
+
createdAt: integer("created_at").notNull().default(now),
|
|
135
|
+
lastLoginAt: integer("last_login_at"),
|
|
136
|
+
},
|
|
137
|
+
(t) => [primaryKey({ columns: [t.directoryId, t.productId] })],
|
|
138
|
+
);
|
|
139
|
+
export type Account = typeof accounts.$inferSelect;
|
|
140
|
+
|
|
141
|
+
/** Append-only Ahrefs Domain Rating readings (optional, needs AHREFS_API_KEY). Latest = highest measuredAt. */
|
|
142
|
+
export const domainRatings = sqliteTable(
|
|
143
|
+
"domain_ratings",
|
|
144
|
+
{
|
|
145
|
+
domain: text("domain").notNull(),
|
|
146
|
+
measuredAt: integer("measured_at").notNull(),
|
|
147
|
+
rating: real("rating").notNull(),
|
|
148
|
+
},
|
|
149
|
+
(t) => [primaryKey({ columns: [t.domain, t.measuredAt] })],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
export type Job = typeof jobs.$inferSelect;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const DIRECTORY_KINDS = {
|
|
2
|
+
listing: "Software directory",
|
|
3
|
+
launch_board: "Launch site",
|
|
4
|
+
mcp_registry: "MCP registry",
|
|
5
|
+
github_list: "GitHub list",
|
|
6
|
+
plugin_marketplace: "Plugin marketplace",
|
|
7
|
+
api_catalog: "API catalog",
|
|
8
|
+
llms_directory: "llms.txt directory",
|
|
9
|
+
campaign: "Campaign",
|
|
10
|
+
lead_gen: "Lead generation",
|
|
11
|
+
marketplace: "Marketplace",
|
|
12
|
+
} as const;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { and, eq, gt, inArray, like } from "drizzle-orm";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Db } from "./db";
|
|
5
|
+
import { events, jobs, type Job } from "./db/schema";
|
|
6
|
+
import * as J from "./jobs";
|
|
7
|
+
import { recordRating } from "./ratings";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* One-off import of the retired dirpush.com app's D1 export (see `launch import hosted`).
|
|
11
|
+
* The hosted app ran the same launch through MCP from Oct 11–14, so its rows are often newer than ours.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** A tracked_targets row as exported by `wrangler d1 execute --json`. */
|
|
15
|
+
export type HostedTarget = {
|
|
16
|
+
product_id: string; directory_id: string; state: string; detail: string | null; listing_url: string | null; updated_at: number;
|
|
17
|
+
job_id: string | null; block_type: string | null; fields: string | null; badge: number; decision: string | null;
|
|
18
|
+
answer: string | null; reason: string | null; action: string | null; diagnostics: string | null; account_hint: string | null;
|
|
19
|
+
};
|
|
20
|
+
export type HostedEvent = { id: number; product_id: string; directory_id: string; at: number; kind: string; data: string | null };
|
|
21
|
+
export type HostedRating = { domain: string; measured_at: number; rating: number };
|
|
22
|
+
|
|
23
|
+
const parse = (s: string | null) => {
|
|
24
|
+
if (s == null) return undefined;
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(s) as unknown;
|
|
27
|
+
} catch {
|
|
28
|
+
return s;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** The local job fields a hosted state stands for; null = a hosted-only recommendation that was never queued. */
|
|
33
|
+
export function jobPatch(t: HostedTarget): Partial<Job> | null {
|
|
34
|
+
const clear = { worker: null, leaseExpiresAt: null, notBefore: null, blockType: null, blockMessage: null, lastError: null };
|
|
35
|
+
switch (t.state) {
|
|
36
|
+
case "live":
|
|
37
|
+
case "pending":
|
|
38
|
+
case "needs_badge":
|
|
39
|
+
case "rejected":
|
|
40
|
+
return { ...clear, status: "done", step: "submitted", outcome: t.state, ...(t.listing_url && { listingUrl: t.listing_url }) };
|
|
41
|
+
case "blocked":
|
|
42
|
+
return { ...clear, status: "blocked", blockType: t.block_type ?? "manual", blockMessage: t.detail ?? "blocked on the hosted dashboard" };
|
|
43
|
+
case "paused":
|
|
44
|
+
return { ...clear, status: "blocked", blockType: "hold", blockMessage: t.detail ?? "paused on the hosted dashboard" };
|
|
45
|
+
case "failed":
|
|
46
|
+
return { ...clear, status: "failed", lastError: [t.reason, t.detail].filter(Boolean).join(": ") || "failed on the hosted dashboard" };
|
|
47
|
+
case "todo":
|
|
48
|
+
if (t.badge && t.decision === "declined") return { ...clear, status: "failed", lastError: "badge: free only with a badge, declined" };
|
|
49
|
+
// The old sync pushed unqueued recommendations as "recommended, not queued"; anything else was worked on hosted.
|
|
50
|
+
return t.job_id || (t.detail && t.detail !== "recommended, not queued") ? { ...clear, status: "queued", step: "queued" } : null;
|
|
51
|
+
case "in_progress":
|
|
52
|
+
return { ...clear, status: "queued", step: "queued" };
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Hosted-only context the dashboard recorded (customer replies, required fields, agent diagnostics). */
|
|
59
|
+
function hostedData(t: HostedTarget) {
|
|
60
|
+
const entries = Object.entries({
|
|
61
|
+
action: parse(t.action), fields: parse(t.fields), answer: parse(t.answer), diagnostics: parse(t.diagnostics),
|
|
62
|
+
reason: t.reason, decision: t.decision, accountHint: t.account_hint,
|
|
63
|
+
}).filter(([, v]) => v != null);
|
|
64
|
+
return entries.length ? { hosted: Object.fromEntries(entries) } : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function importHosted(
|
|
68
|
+
db: Db,
|
|
69
|
+
input: { targets: HostedTarget[]; events: HostedEvent[]; ratings: HostedRating[] },
|
|
70
|
+
products: string[],
|
|
71
|
+
) {
|
|
72
|
+
const summary = { applied: 0, created: 0, keptLocal: 0, recommendationsSkipped: 0, events: 0, ratings: 0, skippedProducts: new Set<string>() };
|
|
73
|
+
const known = new Set(products);
|
|
74
|
+
const jobFor = new Map<string, string>(); // product/directory -> local job id, for events
|
|
75
|
+
|
|
76
|
+
for (const t of input.targets) {
|
|
77
|
+
if (!known.has(t.product_id)) {
|
|
78
|
+
summary.skippedProducts.add(t.product_id);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const local =
|
|
82
|
+
(t.job_id && db.select().from(jobs).where(eq(jobs.id, t.job_id)).get()) ||
|
|
83
|
+
db.select().from(jobs).where(and(eq(jobs.kind, "submission"), eq(jobs.productId, t.product_id), eq(jobs.subject, t.directory_id))).get();
|
|
84
|
+
if (local) jobFor.set(`${t.product_id}/${t.directory_id}`, local.id);
|
|
85
|
+
const patch = jobPatch(t);
|
|
86
|
+
if (!patch) {
|
|
87
|
+
summary.recommendationsSkipped++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (local && local.updatedAt >= t.updated_at) {
|
|
91
|
+
summary.keptLocal++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const job = local ?? J.enqueue(db, "submission", t.directory_id, t.product_id, t.updated_at);
|
|
95
|
+
if (!local) summary.created++;
|
|
96
|
+
jobFor.set(`${t.product_id}/${t.directory_id}`, job.id);
|
|
97
|
+
db.transaction((tx) => {
|
|
98
|
+
tx.update(jobs).set({ ...patch, data: { ...job.data, ...hostedData(t) }, updatedAt: t.updated_at }).where(eq(jobs.id, job.id)).run();
|
|
99
|
+
tx.insert(events).values({ jobId: job.id, type: "hosted_import", worker: "hosted", data: { from: { status: job.status, outcome: job.outcome }, to: t.state }, at: Date.now() }).run();
|
|
100
|
+
});
|
|
101
|
+
summary.applied++;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Events are append-only history; the hosted id makes re-running the import a no-op.
|
|
105
|
+
const seen = new Set(
|
|
106
|
+
db.select({ data: events.data }).from(events).where(like(events.type, "hosted.%")).all().map((e) => (e.data as { hostedEventId?: number }).hostedEventId),
|
|
107
|
+
);
|
|
108
|
+
for (const e of input.events) {
|
|
109
|
+
if (!known.has(e.product_id) || seen.has(e.id)) continue;
|
|
110
|
+
const data = parse(e.data);
|
|
111
|
+
db.insert(events).values({
|
|
112
|
+
jobId: jobFor.get(`${e.product_id}/${e.directory_id}`) ?? null,
|
|
113
|
+
type: `hosted.${e.kind}`,
|
|
114
|
+
worker: "hosted",
|
|
115
|
+
data: { hostedEventId: e.id, directoryId: e.directory_id, ...(data && typeof data === "object" ? data : { value: data }) },
|
|
116
|
+
at: e.at,
|
|
117
|
+
}).run();
|
|
118
|
+
summary.events++;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The import's own events would otherwise sort as "today" in a job's timeline: date them to the hosted history.
|
|
122
|
+
const firstHosted = new Map<string, number>();
|
|
123
|
+
for (const e of input.events) {
|
|
124
|
+
const k = `${e.product_id}/${e.directory_id}`;
|
|
125
|
+
firstHosted.set(k, Math.min(firstHosted.get(k) ?? e.at, e.at));
|
|
126
|
+
}
|
|
127
|
+
for (const t of input.targets) {
|
|
128
|
+
const jobId = jobFor.get(`${t.product_id}/${t.directory_id}`);
|
|
129
|
+
if (!jobId) continue;
|
|
130
|
+
const first = Math.min(firstHosted.get(`${t.product_id}/${t.directory_id}`) ?? t.updated_at, t.updated_at);
|
|
131
|
+
db.update(events).set({ at: t.updated_at }).where(and(eq(events.jobId, jobId), eq(events.type, "hosted_import"))).run();
|
|
132
|
+
db.update(events).set({ at: first - 1 }).where(and(eq(events.jobId, jobId), inArray(events.type, ["job_created"]), gt(events.at, t.updated_at))).run();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const r of input.ratings) {
|
|
136
|
+
recordRating(db, r.domain, r.rating, r.measured_at);
|
|
137
|
+
summary.ratings++;
|
|
138
|
+
}
|
|
139
|
+
return { ...summary, skippedProducts: [...summary.skippedProducts] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Reads the `wrangler d1 execute --json` files written to `dir` (one per table). */
|
|
143
|
+
export async function readHostedExport(dir: string) {
|
|
144
|
+
const table = async <T>(name: string): Promise<T[]> => {
|
|
145
|
+
const file = join(dir, `${name}.json`);
|
|
146
|
+
if (!existsSync(file)) throw new Error(`missing ${file}; export it first (see 'dirpush import hosted' in docs/operator.md)`);
|
|
147
|
+
const parsed = JSON.parse(await Bun.file(file).text()) as { results?: T[]; error?: unknown }[] | { error: unknown };
|
|
148
|
+
if (!Array.isArray(parsed) || !parsed[0]?.results) throw new Error(`${file} is not a successful wrangler --json export`);
|
|
149
|
+
return parsed[0].results;
|
|
150
|
+
};
|
|
151
|
+
return {
|
|
152
|
+
targets: await table<HostedTarget>("tracked_targets"),
|
|
153
|
+
events: await table<HostedEvent>("target_events"),
|
|
154
|
+
ratings: await table<HostedRating>("domain_rating_history"),
|
|
155
|
+
};
|
|
156
|
+
}
|
package/src/jobs.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { and, asc, eq, inArray, isNull, lte, or } from "drizzle-orm";
|
|
2
|
+
import type { Db } from "./db";
|
|
3
|
+
import { events, jobs, type Job, type JobKind } from "./db/schema";
|
|
4
|
+
import { recordAccount } from "./accounts";
|
|
5
|
+
|
|
6
|
+
export const LEASE_MS = 15 * 60_000;
|
|
7
|
+
export const MAX_ATTEMPTS = 5;
|
|
8
|
+
/** The one step with an external side effect we cannot see from here. */
|
|
9
|
+
export const SUBMITTING = "submitting";
|
|
10
|
+
|
|
11
|
+
export class JobError extends Error {}
|
|
12
|
+
|
|
13
|
+
type Tx = Parameters<Parameters<Db["transaction"]>[0]>[0];
|
|
14
|
+
|
|
15
|
+
function logEvent(tx: Tx | Db, jobId: string | null, type: string, worker: string | null, data: Record<string, unknown> = {}) {
|
|
16
|
+
tx.insert(events).values({ jobId, type, worker, data }).run();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function get(tx: Tx, id: string): Job {
|
|
20
|
+
const job = tx.select().from(jobs).where(eq(jobs.id, id)).get();
|
|
21
|
+
if (!job) throw new JobError(`no such job: ${id}`);
|
|
22
|
+
return job;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function set(tx: Tx, id: string, patch: Partial<Job>, now: number) {
|
|
26
|
+
return tx.update(jobs).set({ ...patch, updatedAt: now }).where(eq(jobs.id, id)).returning().get()!;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function owned(job: Job, worker: string, now: number, statuses: Job["status"][] = ["running"]) {
|
|
30
|
+
if (!statuses.includes(job.status)) throw new JobError(`job ${job.id} is ${job.status}, expected ${statuses.join("|")}`);
|
|
31
|
+
if (job.worker !== worker) throw new JobError(`job ${job.id} is leased to ${job.worker}, not ${worker}`);
|
|
32
|
+
if ((job.leaseExpiresAt ?? 0) < now) throw new JobError(`lease on ${job.id} expired; claim it again`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Leaving a job mid-submit means we no longer know whether the directory got it. */
|
|
36
|
+
const released = (job: Job) => (job.step === SUBMITTING ? ("unknown" as const) : null);
|
|
37
|
+
|
|
38
|
+
const tx = <T>(db: Db, fn: (tx: Tx) => T) => db.transaction(fn, { behavior: "immediate" });
|
|
39
|
+
|
|
40
|
+
export function enqueue(db: Db, kind: JobKind, subject: string, productId = "", now = Date.now()): Job {
|
|
41
|
+
return tx(db, (t) => {
|
|
42
|
+
const existing = t
|
|
43
|
+
.select()
|
|
44
|
+
.from(jobs)
|
|
45
|
+
.where(and(eq(jobs.kind, kind), eq(jobs.subject, subject), eq(jobs.productId, productId)))
|
|
46
|
+
.get();
|
|
47
|
+
if (existing) return existing;
|
|
48
|
+
const id = `${kind === "research" ? "r" : "s"}_${crypto.randomUUID().slice(0, 8)}`;
|
|
49
|
+
const job = t.insert(jobs).values({ id, kind, subject, productId, createdAt: now, updatedAt: now }).returning().get();
|
|
50
|
+
logEvent(t, id, "job_created", null, { kind, subject, productId });
|
|
51
|
+
return job;
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function claimableWhere(now: number, kind?: JobKind) {
|
|
56
|
+
const expired = or(isNull(jobs.leaseExpiresAt), lte(jobs.leaseExpiresAt, now));
|
|
57
|
+
const ready = or(
|
|
58
|
+
eq(jobs.status, "queued"),
|
|
59
|
+
and(eq(jobs.status, "retry"), or(isNull(jobs.notBefore), lte(jobs.notBefore, now))),
|
|
60
|
+
and(inArray(jobs.status, ["running", "unknown"]), expired),
|
|
61
|
+
);
|
|
62
|
+
return kind ? and(eq(jobs.kind, kind), ready) : ready;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Unknown first (they block retries), then oldest. */
|
|
66
|
+
export function next(db: Db, kind?: JobKind, now = Date.now()): Job | undefined {
|
|
67
|
+
const rows = db.select().from(jobs).where(claimableWhere(now, kind)).orderBy(asc(jobs.createdAt)).all();
|
|
68
|
+
return rows.find((j) => j.status === "unknown") ?? rows[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function claim(db: Db, id: string, worker: string, now = Date.now()): Job {
|
|
72
|
+
return tx(db, (t) => {
|
|
73
|
+
const job = get(t, id);
|
|
74
|
+
const ok = t.select({ id: jobs.id }).from(jobs).where(and(eq(jobs.id, id), claimableWhere(now))).get();
|
|
75
|
+
if (!ok) throw new JobError(`job ${id} is not claimable (${job.status}, worker ${job.worker ?? "-"})`);
|
|
76
|
+
let status = job.status;
|
|
77
|
+
if (status === "running") {
|
|
78
|
+
logEvent(t, id, "lease_expired", job.worker, { step: job.step });
|
|
79
|
+
if (job.step === SUBMITTING) {
|
|
80
|
+
status = "unknown";
|
|
81
|
+
logEvent(t, id, "submission_state_unknown", job.worker, { reason: "lease expired during submit" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const fresh = status === "queued" || status === "retry";
|
|
85
|
+
const updated = set(
|
|
86
|
+
t,
|
|
87
|
+
id,
|
|
88
|
+
{
|
|
89
|
+
status: status === "unknown" ? "unknown" : "running",
|
|
90
|
+
worker,
|
|
91
|
+
leaseExpiresAt: now + LEASE_MS,
|
|
92
|
+
attempts: fresh ? job.attempts + 1 : job.attempts,
|
|
93
|
+
notBefore: null,
|
|
94
|
+
},
|
|
95
|
+
now,
|
|
96
|
+
);
|
|
97
|
+
logEvent(t, id, "claimed", worker, { status: updated.status, step: updated.step, attempt: updated.attempts });
|
|
98
|
+
return updated;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function heartbeat(db: Db, id: string, worker: string, now = Date.now()) {
|
|
103
|
+
return tx(db, (t) => {
|
|
104
|
+
owned(get(t, id), worker, now, ["running", "unknown"]);
|
|
105
|
+
return set(t, id, { leaseExpiresAt: now + LEASE_MS }, now);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function checkpoint(db: Db, id: string, worker: string, step: string, data: Record<string, unknown> = {}, now = Date.now()) {
|
|
110
|
+
if (step === SUBMITTING) throw new JobError(`use 'work begin-submit' to enter ${SUBMITTING}`);
|
|
111
|
+
return tx(db, (t) => {
|
|
112
|
+
const job = get(t, id);
|
|
113
|
+
owned(job, worker, now);
|
|
114
|
+
if (job.step === SUBMITTING) throw new JobError(`job ${id} is mid-submit; record 'work submitted' or fail it`);
|
|
115
|
+
logEvent(t, id, "checkpoint", worker, { step, ...data });
|
|
116
|
+
recordAccount(t, job, data, now); // a checkpoint carrying {"account":…} keeps the accounts table current
|
|
117
|
+
return set(t, id, { step, data: { ...job.data, ...data }, leaseExpiresAt: now + LEASE_MS }, now);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Call immediately before clicking the directory's final submit button. */
|
|
122
|
+
export function beginSubmit(db: Db, id: string, worker: string, now = Date.now()) {
|
|
123
|
+
return tx(db, (t) => {
|
|
124
|
+
const job = get(t, id);
|
|
125
|
+
owned(job, worker, now);
|
|
126
|
+
if (job.kind !== "submission") throw new JobError(`job ${id} is not a submission`);
|
|
127
|
+
logEvent(t, id, "submit_attempted", worker);
|
|
128
|
+
return set(t, id, { step: SUBMITTING, leaseExpiresAt: now + LEASE_MS }, now);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export type Outcome = "submitted" | "pending" | "live" | "rejected" | "needs_badge"; // needs_badge: accepted but only goes/stays live with their badge on our site (we never add one)
|
|
133
|
+
|
|
134
|
+
/** Record that the directory has the listing. Valid mid-submit or while verifying an unknown job. */
|
|
135
|
+
export function recordSubmitted(db: Db, id: string, worker: string, outcome: Outcome = "submitted", listingUrl?: string, now = Date.now()) {
|
|
136
|
+
return tx(db, (t) => {
|
|
137
|
+
const job = get(t, id);
|
|
138
|
+
owned(job, worker, now, ["running", "unknown"]);
|
|
139
|
+
if (job.status === "running" && job.step !== SUBMITTING) throw new JobError(`job ${id} never began submitting`);
|
|
140
|
+
logEvent(t, id, "submission_recorded", worker, { outcome, listingUrl });
|
|
141
|
+
return set(t, id, { status: "done", step: "submitted", outcome, listingUrl: listingUrl ?? job.listingUrl, worker: null, leaseExpiresAt: null }, now);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Verification of an unknown job found no listing: safe to submit again. */
|
|
146
|
+
export function recordNotSubmitted(db: Db, id: string, worker: string, evidence: string, now = Date.now()) {
|
|
147
|
+
return tx(db, (t) => {
|
|
148
|
+
owned(get(t, id), worker, now, ["unknown"]);
|
|
149
|
+
logEvent(t, id, "verified_not_submitted", worker, { evidence });
|
|
150
|
+
return set(t, id, { status: "running", step: "verified_not_submitted", leaseExpiresAt: now + LEASE_MS }, now);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function complete(db: Db, id: string, worker: string, outcome: string, data: Record<string, unknown> = {}, now = Date.now()) {
|
|
155
|
+
return tx(db, (t) => {
|
|
156
|
+
const job = get(t, id);
|
|
157
|
+
owned(job, worker, now);
|
|
158
|
+
if (job.step === SUBMITTING) throw new JobError(`job ${id} is mid-submit; use 'work submitted'`);
|
|
159
|
+
logEvent(t, id, "completed", worker, { outcome });
|
|
160
|
+
return set(t, id, { status: "done", step: "done", outcome, data: { ...job.data, ...data }, worker: null, leaseExpiresAt: null }, now);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function block(db: Db, id: string, worker: string, type: string, message: string, now = Date.now()) {
|
|
165
|
+
return tx(db, (t) => {
|
|
166
|
+
const job = get(t, id);
|
|
167
|
+
owned(job, worker, now, ["running", "unknown"]);
|
|
168
|
+
logEvent(t, id, "blocked", worker, { type, message, step: job.step });
|
|
169
|
+
return set(t, id, { status: "blocked", blockType: type, blockMessage: message, worker: null, leaseExpiresAt: null }, now);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Human resolved the block. `approved` = explicit go-ahead on a permission block (dashboard Approve only). */
|
|
174
|
+
export function unblock(db: Db, id: string, note = "", now = Date.now(), approved = false) {
|
|
175
|
+
return tx(db, (t) => {
|
|
176
|
+
const job = get(t, id);
|
|
177
|
+
if (job.status !== "blocked") throw new JobError(`job ${id} is ${job.status}, not blocked`);
|
|
178
|
+
logEvent(t, id, "unblocked", "human", { type: job.blockType, note, ...(approved && job.blockType === "permission" && { approved }) });
|
|
179
|
+
return set(t, id, { status: released(job) ?? "queued", blockType: null, blockMessage: null }, now);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function fail(db: Db, id: string, worker: string, reason: string, retryable: boolean, now = Date.now()) {
|
|
184
|
+
return tx(db, (t) => {
|
|
185
|
+
const job = get(t, id);
|
|
186
|
+
owned(job, worker, now, ["running", "unknown"]);
|
|
187
|
+
const base = { lastError: reason, worker: null, leaseExpiresAt: null };
|
|
188
|
+
if (released(job) || job.status === "unknown") {
|
|
189
|
+
logEvent(t, id, "submission_state_unknown", worker, { reason });
|
|
190
|
+
return set(t, id, { ...base, status: "unknown" }, now);
|
|
191
|
+
}
|
|
192
|
+
if (retryable && job.attempts < MAX_ATTEMPTS) {
|
|
193
|
+
const notBefore = now + 2 ** job.attempts * 60_000;
|
|
194
|
+
logEvent(t, id, "failed_retryable", worker, { reason, notBefore });
|
|
195
|
+
return set(t, id, { ...base, status: "retry", notBefore }, now);
|
|
196
|
+
}
|
|
197
|
+
logEvent(t, id, "failed", worker, { reason, retryable });
|
|
198
|
+
return set(t, id, { ...base, status: "failed" }, now);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Graceful stop: hand the job back without penalty. */
|
|
203
|
+
export function release(db: Db, id: string, worker: string, now = Date.now()) {
|
|
204
|
+
return tx(db, (t) => {
|
|
205
|
+
const job = get(t, id);
|
|
206
|
+
owned(job, worker, now, ["running", "unknown"]);
|
|
207
|
+
logEvent(t, id, "released", worker, { step: job.step });
|
|
208
|
+
const status = job.status === "unknown" ? "unknown" : (released(job) ?? "queued");
|
|
209
|
+
return set(t, id, { status, worker: null, leaseExpiresAt: null }, now);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Did the human press Approve on a permission block of this job? */
|
|
214
|
+
export function approved(db: Db, id: string) {
|
|
215
|
+
return db.select().from(events).where(and(eq(events.jobId, id), eq(events.type, "unblocked"))).all().some((e) => e.data.approved === true);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Human: give a failed job another go. */
|
|
219
|
+
export function requeue(db: Db, id: string, now = Date.now()) {
|
|
220
|
+
return tx(db, (t) => {
|
|
221
|
+
const job = get(t, id);
|
|
222
|
+
if (job.status !== "failed") throw new JobError(`job ${id} is ${job.status}, not failed`);
|
|
223
|
+
logEvent(t, id, "requeued", "human");
|
|
224
|
+
return set(t, id, { status: "queued", step: "queued", attempts: 0, lastError: null }, now);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Human: redo a finished research job (verdict looked wrong). */
|
|
229
|
+
export function reopen(db: Db, id: string, note = "", now = Date.now()) {
|
|
230
|
+
return tx(db, (t) => {
|
|
231
|
+
const job = get(t, id);
|
|
232
|
+
// A queued job can be reopened again to replace its note.
|
|
233
|
+
if (job.kind !== "research" || (job.status !== "done" && job.status !== "queued")) throw new JobError(`job ${id} is not a finished or queued research job`);
|
|
234
|
+
logEvent(t, id, "reopened", "human", { previous: job.outcome, note });
|
|
235
|
+
return set(t, id, { status: "queued", step: "queued", attempts: 0, outcome: null, data: { ...job.data, reopenNote: note } }, now);
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The human listed the product themselves (or found it already listed). Any job that isn't mid-run becomes a
|
|
241
|
+
* finished submission, so no agent submits there again; `submissions check` then tracks it like any other.
|
|
242
|
+
*/
|
|
243
|
+
export function recordManual(db: Db, id: string, outcome: Outcome, listingUrl?: string, now = Date.now()) {
|
|
244
|
+
return tx(db, (t) => {
|
|
245
|
+
const job = get(t, id);
|
|
246
|
+
if (job.kind !== "submission") throw new JobError(`job ${id} is not a submission`);
|
|
247
|
+
if ((job.status === "running" || job.status === "unknown") && (job.leaseExpiresAt ?? 0) > now)
|
|
248
|
+
throw new JobError(`an agent (${job.worker}) is working on ${job.subject} right now; wait for it to stop`);
|
|
249
|
+
logEvent(t, id, "added_manually", "human", { outcome, listingUrl, from: job.status });
|
|
250
|
+
return set(t, id, {
|
|
251
|
+
status: "done", step: "submitted", outcome, listingUrl: listingUrl ?? job.listingUrl,
|
|
252
|
+
worker: null, leaseExpiresAt: null, blockType: null, blockMessage: null, lastError: null,
|
|
253
|
+
}, now);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Moderation tracking after submit: pending -> live/rejected. */
|
|
258
|
+
export function setOutcome(db: Db, id: string, outcome: Outcome, listingUrl?: string, now = Date.now()) {
|
|
259
|
+
return tx(db, (t) => {
|
|
260
|
+
const job = get(t, id);
|
|
261
|
+
if (job.status !== "done" || job.kind !== "submission") throw new JobError(`job ${id} is not a finished submission`);
|
|
262
|
+
logEvent(t, id, "outcome_changed", null, { from: job.outcome, to: outcome, listingUrl });
|
|
263
|
+
return set(t, id, { outcome, listingUrl: listingUrl ?? job.listingUrl }, now);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export const jobEvents = (db: Db, id: string) =>
|
|
268
|
+
db.select().from(events).where(eq(events.jobId, id)).orderBy(asc(events.at), asc(events.id)).all();
|
|
269
|
+
|
|
270
|
+
export { logEvent };
|