@fangorn-network/westmarch 0.0.20-dev → 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/README.md +35 -5
- package/docs/app-to-agent.md +113 -620
- package/package.json +18 -18
- package/site/dist/assets/__vite-browser-external-D2U8RZpp.js +1 -0
- package/site/dist/assets/embed.worker-CmS57Zti.js +1 -0
- package/site/dist/assets/index-CkP1WhkV.js +8 -0
- package/site/dist/assets/roles-BDPDWrwo.js +1 -0
- package/site/dist/assets/rolldown-runtime-COiEfhsA.js +1 -0
- package/site/dist/assets/transformers.web-CUZUFjAK.js +49 -0
- package/site/dist/assets/transformers.web-CwR_IYQV.js +40 -0
- package/site/dist/index.html +4 -5
- package/site/feed.js +16 -1
- package/site/no-model.js +4 -0
- package/site/worker.js +382 -59
- package/src/agent/agent-card.js +51 -37
- package/src/agent/mcp.js +173 -500
- package/src/agent/tools.js +3 -2
- package/src/agent/ui.js +1 -1
- package/src/agent/view-tools.js +11 -30
- package/src/agent/x402.js +194 -85
- package/src/core/embed.js +56 -53
- package/src/core/embed.worker.js +1 -1
- package/src/core/rank.js +1 -1
- package/src/core/roles.js +7 -7
- package/src/core/shard.js +12 -8
- package/src/discover/apps.js +103 -67
- package/src/discover/corpora.js +1 -1
- package/src/discover/directory.js +3 -3
- package/src/market/cohort.js +1 -1
- package/src/market/demand.js +1 -1
- package/src/market/reactions.js +24 -18
- package/src/publish/devnet.js +146 -0
- package/src/publish/graph.js +1 -1
- package/src/publish/init.js +74 -0
- package/src/publish/lint.js +3 -2
- package/src/publish/ship.js +433 -287
- package/src/publish/view-cli.js +14 -10
- package/src/publish/view.js +485 -255
- package/src/taste/steam.js +1 -1
- package/src/taste/taste-doc.js +4 -3
- package/src/taste/taste.js +3 -3
- package/src/taste/vault.js +4 -2
- package/template/README.md +48 -0
- package/template/app.json +27 -0
- package/template/eval/goals.md +7 -0
- package/template/eval/golden.jsonl +0 -0
- package/template/gitignore +3 -0
- package/template/package.json +13 -0
- package/template/wrangler.json +12 -0
- package/plugin/scripts/fx.mjs +0 -268
- package/site/dist/assets/AccountNotFoundScreen-C4njXc69-BOfmiSH1.js +0 -1
- package/site/dist/assets/AddFundsSelectionScreen-RAoCn7ej-D_EsIeJu.js +0 -31
- package/site/dist/assets/Address-H8kD1d9O-C85sJ79B.js +0 -17
- package/site/dist/assets/AffirmativeConsentScreen-BG0MJxku-DdEPFOww.js +0 -6
- package/site/dist/assets/AllowlistRejectionScreen-Cae6e1Od-DhVy9iGN.js +0 -1
- package/site/dist/assets/ApiController-CYdiyu9B.js +0 -5
- package/site/dist/assets/ArrowsRightLeftIcon-cp60yobq.js +0 -1
- package/site/dist/assets/AuthenticateWithWalletScreen-CU6hIcFT-8rbYiYYE.js +0 -1
- package/site/dist/assets/AwaitingEvmToSolBridgingScreen-D0e_sZMS-2pkNfCno.js +0 -1
- package/site/dist/assets/AwaitingExternalEthereumTransferScreen-ur5HL4R7-7AQt25_k.js +0 -1
- package/site/dist/assets/AwaitingPasswordlessCodeScreen-DRHz76mM-BZY30TGs.js +0 -103
- package/site/dist/assets/AwaitingSolToEvmBridgingScreen-CdLRrGD2-BILOOLi_.js +0 -1
- package/site/dist/assets/BridgeNetworkSelectionView-CFGjB2nO-CvsbQcFv.js +0 -68
- package/site/dist/assets/CaptchaScreen-DbpvkIGx-D8eDcAkc.js +0 -1
- package/site/dist/assets/CheckCircleIcon-BM409mGI.js +0 -1
- package/site/dist/assets/CheckCircleIcon-NCOQc0Gs.js +0 -1
- package/site/dist/assets/Checkbox-D1EDeo41-rHlvsZzP.js +0 -52
- package/site/dist/assets/ChevronDownIcon-CW-4MSpE.js +0 -1
- package/site/dist/assets/Chip-CZKIKt9K-CutJG010.js +0 -26
- package/site/dist/assets/CoinbaseOnrampStatusScreen-BbyCJDXL-DLs9zX-Q.js +0 -16
- package/site/dist/assets/ConnectEmailForm-B-VRE0MN-N9J6T_YL.js +0 -3
- package/site/dist/assets/ConnectLedgerScreen-DWHABGot-h0uYIUi6.js +0 -10
- package/site/dist/assets/ConnectOnlyLandingScreen-CZYx9Bmp-LeuDMN1E.js +0 -1
- package/site/dist/assets/ConnectOnlyStatusScreen-DCRzjxHx-DaRa59tZ.js +0 -1
- package/site/dist/assets/ConnectOrCreateScreen-CUUYUO07-crHzh-TL.js +0 -1
- package/site/dist/assets/ConnectPhoneForm-Dek5xn4C-BEA4FmhI.js +0 -95
- package/site/dist/assets/ConnectWalletView-DqI-PTvX-CMQIad63.js +0 -74
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-BeWLchcf.js +0 -7
- package/site/dist/assets/ConnectionStatusScreen-fUfROm9m-wu3a3kw4.js +0 -1
- package/site/dist/assets/ConnectorUtil-Cq4lWnfe.js +0 -21
- package/site/dist/assets/ConstantsUtil-DmLsRHAl.js +0 -198
- package/site/dist/assets/CopyToClipboard-i_OQSBJr-DujUHNzW.js +0 -39
- package/site/dist/assets/CopyableText-CQapvaMr-DMIdpDaU.js +0 -28
- package/site/dist/assets/CrossAppAuthScreen-CLw7ymZq-Csju14vF.js +0 -1
- package/site/dist/assets/CustomLandingScreenView-Cm_MBSyX-CBOrbNLo.js +0 -22
- package/site/dist/assets/DelegatedActionsConsentScreen-DYF2U4y5-BL5hzNUI.js +0 -1
- package/site/dist/assets/DelegatedActionsRevokeScreen-bPp6Uwyh-fcO51q5H.js +0 -1
- package/site/dist/assets/DepositAddressAddressStepView-4vdkb55L-B3vY888G.js +0 -187
- package/site/dist/assets/DepositAddressScreen-DBLRs7aI-DmdQaFI2.js +0 -8
- package/site/dist/assets/DepositAddressV3Screen-CDqIEMQw-BoupRyfV.js +0 -1
- package/site/dist/assets/EmailInputForm-Cqo1mda8-B2NG3K5V.js +0 -124
- package/site/dist/assets/EmbeddedWalletConnectingScreen-D4T7DUzs-D_AVgO4s.js +0 -1
- package/site/dist/assets/EmbeddedWalletCreatedScreen-D7WhW5ve-CLsJbPYQ.js +0 -1
- package/site/dist/assets/EmbeddedWalletKeyExportScreen-CqV4NYQ3-C9sAExzE.js +0 -23
- package/site/dist/assets/EmbeddedWalletOnAccountCreateScreen-BoKb3Q2S-DxaWAAKa.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateScreen-D3rMlqS7-CxrfLr5q.js +0 -1
- package/site/dist/assets/EmbeddedWalletPasswordUpdateSplashScreen-BPFkWJeE-DHZRGoTN.js +0 -1
- package/site/dist/assets/EnrollTotp-Dibew4dp-SA2pvNEc.js +0 -66
- package/site/dist/assets/EnvelopeIcon-Dh2vSVj4.js +0 -1
- package/site/dist/assets/ErrorBanner-BcpGRt0h-BKOYTyCi.js +0 -21
- package/site/dist/assets/ErrorMessage-D8VaAP5m-CrZOnUyg.js +0 -8
- package/site/dist/assets/ErrorScreen-DipdTWzE-DcxVjMO4.js +0 -1
- package/site/dist/assets/ErrorScreen-DipdTWzE-c8g8En21.js +0 -4
- package/site/dist/assets/ExclamationCircleIcon-DUA2wMfO.js +0 -1
- package/site/dist/assets/ExclamationTriangleIcon-eZfaqcFq.js +0 -1
- package/site/dist/assets/FarcasterConnectStatusScreen-51tjUxqq-s_s5M2CZ.js +0 -75
- package/site/dist/assets/FarcasterSignerStatusScreen-6x-Uh95G-BHBR4k8H.js +0 -28
- package/site/dist/assets/FiatOnrampScreen-BAhYeiTI-BH_sU6EV.js +0 -491
- package/site/dist/assets/FingerPrintIcon-Dm6kxmg7.js +0 -1
- package/site/dist/assets/FundSolWalletWithExternalSolanaWallet-WtLwYaOl-B8TRV36j.js +0 -1
- package/site/dist/assets/FundWalletMethodHeader-BWNkokCT-BQdbJ5Bb.js +0 -1
- package/site/dist/assets/FundWithBankDepositScreen-DajkHxBD-9rPtyA3d.js +0 -58
- package/site/dist/assets/FundingEditAmountScreen-CpvpiO4M-GaXigGI9.js +0 -1
- package/site/dist/assets/FundingMethodSelectionScreen-Db7vhRlE-UsmtTqv4.js +0 -1
- package/site/dist/assets/GlobeAltIcon-vqrVV5ho.js +0 -1
- package/site/dist/assets/GooglePay-B53WnudL-BX5_WtCx.js +0 -1
- package/site/dist/assets/HCaptchaWrapper-CUi9LJsp-D-I8j7NO.js +0 -4
- package/site/dist/assets/InAppBrowserLoginNotPossible-BMR9br1T-BY3j50El.js +0 -1
- package/site/dist/assets/InfoBanner-Cb3p1z12-DLBl_Ywy.js +0 -22
- package/site/dist/assets/InjectedWalletIcon-DLcYOGDj-DLSV_NjP.js +0 -1
- package/site/dist/assets/InstallWalletScreen-DTA8dwLv-CxreyXoD.js +0 -1
- package/site/dist/assets/JsonTree-BHzNC-ic-DcNcbSoM.js +0 -34
- package/site/dist/assets/LabelXs-oqZNqbm_-iafrj25N.js +0 -6
- package/site/dist/assets/LandingScreen-CGWUoSUG-DK1EY-6v.js +0 -1
- package/site/dist/assets/Layouts-BMRfo5hw-CeCOyCLK.js +0 -77
- package/site/dist/assets/Link-BdDilT2T-BEDEdHuS.js +0 -40
- package/site/dist/assets/LinkConflictScreen-C3ZEPsgl-ClWoqvEL.js +0 -77
- package/site/dist/assets/LinkEmailScreen-dKo9frwc-C3v7glH0.js +0 -1
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-D72Gb0LW.js +0 -117
- package/site/dist/assets/LinkPasskeyScreen-zbrl_AI_-DYIl5Kr2.js +0 -1
- package/site/dist/assets/LinkPhoneScreen-oIUpfj0w-C0DuD14j.js +0 -1
- package/site/dist/assets/Loader-UAZqxsnw-Cy7hEE2f.js +0 -62
- package/site/dist/assets/LoadingSkeleton-BMsgO5PV-CiEyS5PZ.js +0 -17
- package/site/dist/assets/LockClosedIcon-BgY_5v_Z.js +0 -1
- package/site/dist/assets/LoginFailedScreen-CN0dJV6U-DhUMiJZJ.js +0 -1
- package/site/dist/assets/ManualTransferScreen-CHqcuqbs-tXiZDHmN.js +0 -1
- package/site/dist/assets/MfaAuthEnrollmentFlowScreen-Dj7gPTRC-CaW7xeCC.js +0 -1
- package/site/dist/assets/MfaAuthVerifyFlowScreen-DbFpyWMy-CeRqvx8k.js +0 -1
- package/site/dist/assets/MfaEnrollmentFlowScreen-DhLnGgnr-BkN1k4lh.js +0 -1
- package/site/dist/assets/MfaVerifyFlowScreen-C_XPjMRP-CXSqLVbE.js +0 -1
- package/site/dist/assets/ModalFooter-BCuEK_nz-c-khvG4Y.js +0 -403
- package/site/dist/assets/MoonpayStatusScreen-CzTUtokc-BRFN2eXr.js +0 -16
- package/site/dist/assets/OAuthStatusScreen-ufbOa-da-CeiGcSia.js +0 -1
- package/site/dist/assets/OpenLink-CUpJ1mOr-DfAvJRyK.js +0 -39
- package/site/dist/assets/PasskeySelectSignupOrLogin-xh2ScC_l-CIf5I7wU.js +0 -1
- package/site/dist/assets/PasskeyStatusScreen-Bla8MaOk-CLoL7QC-.js +0 -6
- package/site/dist/assets/PasswordRecoveryScreen-awGXZkQk-B9F67IYV.js +0 -20
- package/site/dist/assets/PhArrowCircleDown-D97G-BOA.js +0 -14
- package/site/dist/assets/PhArrowClockwise-DVgyFuez.js +0 -14
- package/site/dist/assets/PhArrowDown-87b1BizM.js +0 -14
- package/site/dist/assets/PhArrowLeft-Da0MfnbK.js +0 -14
- package/site/dist/assets/PhArrowRight-AdrIL4DW.js +0 -14
- package/site/dist/assets/PhArrowSquareOut-Bk2CMG3I.js +0 -14
- package/site/dist/assets/PhArrowUp-4sgyKPGx.js +0 -14
- package/site/dist/assets/PhArrowUpRight-BNghD2zq.js +0 -14
- package/site/dist/assets/PhArrowsClockwise-BXRn1GEl.js +0 -14
- package/site/dist/assets/PhArrowsDownUp-DM09Jo_3.js +0 -14
- package/site/dist/assets/PhArrowsLeftRight-MuKBLUoF.js +0 -14
- package/site/dist/assets/PhBank-fHbgCqqZ.js +0 -14
- package/site/dist/assets/PhBrowser-2-9m-AkM.js +0 -14
- package/site/dist/assets/PhCaretDown-BDNabFuL.js +0 -14
- package/site/dist/assets/PhCaretLeft-CI460R-A.js +0 -14
- package/site/dist/assets/PhCaretRight-D7ZS3Ls8.js +0 -14
- package/site/dist/assets/PhCaretUp-C-iIfyvO.js +0 -14
- package/site/dist/assets/PhCheck-DxQXn57r.js +0 -14
- package/site/dist/assets/PhCircleHalf-MJZeRBGF.js +0 -14
- package/site/dist/assets/PhClock-x3MS3Mwi.js +0 -14
- package/site/dist/assets/PhCompass-hYn3HhGN.js +0 -14
- package/site/dist/assets/PhCopy-CWNYcABI.js +0 -14
- package/site/dist/assets/PhCreditCard-Fy4pzwtt.js +0 -14
- package/site/dist/assets/PhCurrencyDollar-BgiaenCi.js +0 -14
- package/site/dist/assets/PhDesktop-CxrzCSv5.js +0 -14
- package/site/dist/assets/PhDeviceMobile-CsyA5Goi.js +0 -14
- package/site/dist/assets/PhDotsThree-D-k1BiYg.js +0 -14
- package/site/dist/assets/PhEnvelope-D8kzeNox.js +0 -14
- package/site/dist/assets/PhFunnelSimple-CfQs5DAW.js +0 -14
- package/site/dist/assets/PhGlobe-DvmSc9_A.js +0 -14
- package/site/dist/assets/PhIdentificationCard-CfkZm-5K.js +0 -14
- package/site/dist/assets/PhImage-Cf0J2ZfJ.js +0 -14
- package/site/dist/assets/PhInfo-Bwi51-2X.js +0 -14
- package/site/dist/assets/PhLightbulb-C4FZV7Es.js +0 -14
- package/site/dist/assets/PhMagnifyingGlass-C9u8jSCJ.js +0 -14
- package/site/dist/assets/PhPaperPlaneRight-zxQVvPLX.js +0 -14
- package/site/dist/assets/PhPlus-C4UoW-2n.js +0 -14
- package/site/dist/assets/PhPower-CFyWDmgD.js +0 -14
- package/site/dist/assets/PhPuzzlePiece-BjoNVfST.js +0 -14
- package/site/dist/assets/PhQrCode-CP3tFauY.js +0 -14
- package/site/dist/assets/PhQuestion-wOWJBd3G.js +0 -14
- package/site/dist/assets/PhQuestionMark-CV0k9C0Y.js +0 -14
- package/site/dist/assets/PhSealCheck-vIKzREuN.js +0 -14
- package/site/dist/assets/PhSignOut-CxELV-i0.js +0 -14
- package/site/dist/assets/PhSpinner-CGDmbzUd.js +0 -14
- package/site/dist/assets/PhTrash-C9PD0SAt.js +0 -14
- package/site/dist/assets/PhUser-0sWNXQeZ.js +0 -14
- package/site/dist/assets/PhVault-6JTskUcC.js +0 -14
- package/site/dist/assets/PhWarning-CoKs3gz6.js +0 -14
- package/site/dist/assets/PhWarningCircle-BXHw1fCt.js +0 -14
- package/site/dist/assets/PhX-DX0SFIqC.js +0 -14
- package/site/dist/assets/PhoneIcon-CvIiPwYQ.js +0 -1
- package/site/dist/assets/PinInput-DbZ0b1i1-CDXZBdQx.js +0 -247
- package/site/dist/assets/PrivyPluginContext-DA-yS5vO-CtQWBiV2.js +0 -1
- package/site/dist/assets/QrCode-DbnZwlkH-h6Bhoe5U.js +0 -22
- package/site/dist/assets/RecoveryOAuthStatusScreen-CsgtTI2x-D-A5P3PX.js +0 -1
- package/site/dist/assets/RecoveryPasswordCreateScreen-Be-qJ-LU-C8lTSeSr.js +0 -1
- package/site/dist/assets/RecoverySelectionScreen-D1iGtpmi-P5P3plkH.js +0 -7
- package/site/dist/assets/SIWXUtil-TUCxa7aA.js +0 -1
- package/site/dist/assets/Screen-D30meWsY-CCeVNuWs.js +0 -175
- package/site/dist/assets/ScreenHeader-CHmc4-Lu-DjyrH7IK.js +0 -31
- package/site/dist/assets/ScreenLayout-OwaQQcxH-CrQM6kvA.js +0 -1
- package/site/dist/assets/SelectSourceAsset-CWFog6gm-D7yTw87h.js +0 -140
- package/site/dist/assets/SetAutomaticRecoveryScreen-BO_OEqAZ-f0iDIGmw.js +0 -14
- package/site/dist/assets/SetWalletPasswordForm-DwCTVtAh-kq1zvA7d.js +0 -11
- package/site/dist/assets/ShieldCheckIcon-o6RHYev1.js +0 -1
- package/site/dist/assets/SignRequestScreen-BHKv32KR-DDpMYyJ1.js +0 -26
- package/site/dist/assets/SolanaWalletConnector-C7Lqk-Wz-DPwhuzcI.js +0 -1
- package/site/dist/assets/StackedContainer-B2vaEl56-eQCM2cmv.js +0 -24
- package/site/dist/assets/StandardSignAndSendTransactionScreen-CfhIXVtd-BJ-onCjv.js +0 -20
- package/site/dist/assets/Subtitle-CV-2yKE4-zxWw2m1Q.js +0 -13
- package/site/dist/assets/TelegramAuthScreen-UL9zLfO8-NWlMgNRX.js +0 -1
- package/site/dist/assets/Title-BnzYV3Is-B_JpTatT.js +0 -7
- package/site/dist/assets/TodoList-DnyULl18-C8Eyy28X.js +0 -36
- package/site/dist/assets/TransactionDetails-e9nesl30-C4MpIHR-.js +0 -109
- package/site/dist/assets/TransactionErrorView-BDBNqA_f-p1T9WVMx.js +0 -178
- package/site/dist/assets/TransferFromWalletScreen-Qvn1Yuno-88IQcRnY.js +0 -31
- package/site/dist/assets/TransferOrBridgeLoadingScreen-Dl1I_iIz-1fDAIxAf.js +0 -6
- package/site/dist/assets/TurnstileWrapper-B7sNddys-Jwy1tJtm.js +0 -1
- package/site/dist/assets/UpdateEmailScreen-DVOhPV0H-Jj2DQW8D.js +0 -1
- package/site/dist/assets/UpdatePhoneScreen-CXRswTNZ-DwYIUBqi.js +0 -1
- package/site/dist/assets/UserLimitReachedScreen-5we2DHn8-D7m70C4b.js +0 -1
- package/site/dist/assets/Value-DTgR824E-BRuIGiec.js +0 -27
- package/site/dist/assets/WalletCards-DH1rqayz-Bgp9Fsgs.js +0 -1
- package/site/dist/assets/WalletIcon-gKf0gTw6.js +0 -1
- package/site/dist/assets/WalletInfoCard-sq_2wgFg-DpNSAGte.js +0 -29
- package/site/dist/assets/WalletInterstitialScreen-Sh7LfsAh-DG-RuO-F.js +0 -1
- package/site/dist/assets/WalletLink-DK5LB5Ha-DHaL350n.js +0 -20
- package/site/dist/assets/WalletOverflowButton-bfCnp-9T-CUyIJFzt.js +0 -1
- package/site/dist/assets/WarningBanner-ZZqCEtZK-2eU4Qo74.js +0 -21
- package/site/dist/assets/__vite-browser-external-CitKSmYk.js +0 -1
- package/site/dist/assets/_esm-B26FZg5Y.js +0 -7
- package/site/dist/assets/_esm-BdeM6pxJ.js +0 -1
- package/site/dist/assets/accounts-D96UKWkC.js +0 -1
- package/site/dist/assets/accounts-DiZ-0VmN.js +0 -1
- package/site/dist/assets/analytics-mkkvFRju-BRBcadka.js +0 -1
- package/site/dist/assets/baseSepolia-DX-qGsik.js +0 -1
- package/site/dist/assets/basic-oIwwSyu4.js +0 -1575
- package/site/dist/assets/capitalizeFirstLetter-DmLYqXsO-DtOXx-2n.js +0 -1
- package/site/dist/assets/ccip-65sIfSPR.js +0 -1
- package/site/dist/assets/ccip-BZvRTL6g.js +0 -7
- package/site/dist/assets/ccip-DA_zG341.js +0 -1
- package/site/dist/assets/chain-DM3oH6Px.js +0 -8
- package/site/dist/assets/check-BPgcwEPP.js +0 -1
- package/site/dist/assets/chevron-down-BSsbXkK-.js +0 -1
- package/site/dist/assets/circle-alert-DWaeLZNj.js +0 -1
- package/site/dist/assets/circle-check-big-Chb1ivw9.js +0 -1
- package/site/dist/assets/circle-x-AV9Cet0R.js +0 -1
- package/site/dist/assets/client-CZG0ndth.js +0 -2
- package/site/dist/assets/client-Dsa7_3V3.js +0 -9
- package/site/dist/assets/coinbase-DCPC2L_R-D3aIgpyd.js +0 -3
- package/site/dist/assets/copy-Ckr0AkL6.js +0 -1
- package/site/dist/assets/core-snws5xwv.js +0 -12
- package/site/dist/assets/createLucideIcon-IPW50ckY.js +0 -1
- package/site/dist/assets/defineChain-CiuCXUZW.js +0 -1
- package/site/dist/assets/dijkstra-Drfl93V4.js +0 -1
- package/site/dist/assets/dist-Cm7pNAtL.js +0 -1
- package/site/dist/assets/dist-CoS6E-3x.js +0 -1
- package/site/dist/assets/dist-Dl07uGq1.js +0 -1
- package/site/dist/assets/dist-DnGANz51.js +0 -16
- package/site/dist/assets/embed.worker-ugGhEqCV.js +0 -1
- package/site/dist/assets/errors-BQ3N-RVt-BLqjTPNR.js +0 -1
- package/site/dist/assets/esm-B5cbIQaz.js +0 -37
- package/site/dist/assets/esm-ByALO6mA.js +0 -1
- package/site/dist/assets/esm-C3DslVJx.js +0 -1
- package/site/dist/assets/esm-Ci1ogKuK.js +0 -1
- package/site/dist/assets/esm-n2lWo7kz.js +0 -1
- package/site/dist/assets/ethers-zTjbodk5-wHhL_BQ1.js +0 -1
- package/site/dist/assets/eventemitter3-CL76jPsW.js +0 -1
- package/site/dist/assets/external-link-D-uehwM2.js +0 -1
- package/site/dist/assets/farcaster-DPlSjvF5-BqTvDXBR.js +0 -1
- package/site/dist/assets/features-DQ0UYy6K.js +0 -2
- package/site/dist/assets/fingerprint-pattern-D8ytA16Q.js +0 -1
- package/site/dist/assets/floating-ui.react-dom-DKWOL9UG.js +0 -1
- package/site/dist/assets/floating-ui.react-xRkl9O3u.js +0 -1
- package/site/dist/assets/formatErc20TokenAmount-BuPk9xcy-BVqOKQif.js +0 -1
- package/site/dist/assets/formatter-CVbp4SlJ.js +0 -1
- package/site/dist/assets/get-entropy-details-for-user-dHRH5Gpi-Dukf7_kJ.js +0 -1
- package/site/dist/assets/getChainName-DjpPdUSc-DUyKDrnp.js +0 -1
- package/site/dist/assets/getErc20Balance-jXNpFf0N-Bb9WM31M.js +0 -1
- package/site/dist/assets/getErc20TokenInfo-BNTrzLMP-KPrdn04K.js +0 -1
- package/site/dist/assets/getFormattedUsdFromLamports-De3U9GlO-Gx4_wxON.js +0 -1
- package/site/dist/assets/getUsdcMintAddress-BVgm9GRo-r2UnDeEy.js +0 -1
- package/site/dist/assets/guards-BIW-GiMJ-CHfXo8XI.js +0 -1
- package/site/dist/assets/hooks.module-BYZGfwDl.js +0 -1
- package/site/dist/assets/hourglass-CPG1HZmf.js +0 -1
- package/site/dist/assets/id-UqEwL2BI-BkKm533U.js +0 -1
- package/site/dist/assets/index-1HYFYA-W.js +0 -8
- package/site/dist/assets/index-BdEw5wyl-ChSJvPZk.js +0 -1
- package/site/dist/assets/index-CWARkn2w-Cezl_uBr.js +0 -61
- package/site/dist/assets/index-DvMddJcd-BatvgC-S.js +0 -12
- package/site/dist/assets/index-YiCw0Y_x-CX5i3Z87.js +0 -437
- package/site/dist/assets/index.browser-B-i-Lj3J.js +0 -1
- package/site/dist/assets/index.browser-BMmUcykv.js +0 -1
- package/site/dist/assets/index.es-BmqyoBEM.js +0 -2
- package/site/dist/assets/index.es-C1v1or--.js +0 -3
- package/site/dist/assets/isPaymentRequestAvailable-Bq1cemEn-By6r0dVB.js +0 -1
- package/site/dist/assets/jsx-runtime-BbDfbRii.js +0 -1
- package/site/dist/assets/landmark-CH6LV6F0.js +0 -1
- package/site/dist/assets/lib-BsS8z8Lj.js +0 -1
- package/site/dist/assets/localBatchGatewayRequest-3kLrX_c7.js +0 -32
- package/site/dist/assets/lock-DcskI4Bb.js +0 -1
- package/site/dist/assets/logger-CsXm3oIv-B0UYpNFI.js +0 -1
- package/site/dist/assets/mail-Bs0r5yOi.js +0 -1
- package/site/dist/assets/modal-context-CwmSeFcv-DruX9J1E.js +0 -1
- package/site/dist/assets/native-BKj27wfc.js +0 -1
- package/site/dist/assets/ofetch.CWycOUEr-CHp-crOQ.js +0 -1
- package/site/dist/assets/parseAbi-Csg-8t59.js +0 -2
- package/site/dist/assets/parseAbiParameters-BWbIMYe4.js +0 -1
- package/site/dist/assets/parseEther-VjhVo3dd.js +0 -1
- package/site/dist/assets/pkce-DXF0yKjP-ChQhE0st.js +0 -1
- package/site/dist/assets/preload-helper-uBIymjUX.js +0 -1
- package/site/dist/assets/privy-context-81KmsBw2-CjWGzyg7.js +0 -2
- package/site/dist/assets/property-DuZ9DLP4.js +0 -3
- package/site/dist/assets/react-DCXkXSrv.js +0 -1
- package/site/dist/assets/react-DQnMl2Xv.js +0 -1
- package/site/dist/assets/react-DrJ0MmMO.js +0 -1
- package/site/dist/assets/react-auth-sHnipzbS.js +0 -1
- package/site/dist/assets/react-dom-5dW7aKoP.js +0 -1
- package/site/dist/assets/reservoir-x-nGuZkT-BMX127i6.js +0 -1
- package/site/dist/assets/roles-32M1l9jI.js +0 -1
- package/site/dist/assets/rolldown-runtime-BcKkbAw3.js +0 -1
- package/site/dist/assets/safe-url-D7SRPu33-CkMedGro.js +0 -1
- package/site/dist/assets/secp256k1-C4nzcPo-.js +0 -1
- package/site/dist/assets/secp256k1-D92aGNnU.js +0 -1
- package/site/dist/assets/secp256k1-DmbwyFau.js +0 -1
- package/site/dist/assets/secp256k1-_MleGpqL.js +0 -1
- package/site/dist/assets/secp256k1-xl2og-wO.js +0 -1
- package/site/dist/assets/serializeAuthorizationList-CX8hGL56.js +0 -1
- package/site/dist/assets/serializeSignature-DfxkPnu-.js +0 -2
- package/site/dist/assets/sha2-BU_QyRTx.js +0 -1
- package/site/dist/assets/shared-DRZUmnL5-xqAUARWo.js +0 -193
- package/site/dist/assets/shared-FM0rljBt--Avg6q2R.js +0 -21
- package/site/dist/assets/shouldProceedtoEmbeddedWalletCreationFlow-B3vyavrh-BK_FBkaE.js +0 -1
- package/site/dist/assets/simulateTransaction-CfuOrae4-64lWYJAL.js +0 -1
- package/site/dist/assets/smartphone-CcEZ-XAy.js +0 -1
- package/site/dist/assets/src-BBjkfi-Q.js +0 -1
- package/site/dist/assets/storage-Ufl800Ne-D0bZBw-H.js +0 -9
- package/site/dist/assets/stringify-BlDm7tuc.js +0 -21
- package/site/dist/assets/stripe.esm-CZRDpc-N.js +0 -2
- package/site/dist/assets/styles-BsotlekN-CXEB4Ud9.js +0 -9
- package/site/dist/assets/styles-Cm9H63WO-Br4QtzUL.js +0 -92
- package/site/dist/assets/styles-DVyDvTdj-BTABn2K8.js +0 -74
- package/site/dist/assets/styles-hslXy6xG-D-49b1rk.js +0 -164
- package/site/dist/assets/telegram-B-JqnkqZ-RBoY3PLx.js +0 -1
- package/site/dist/assets/to-ui-error-DWe-qwS3-Cx2bk1kZ.js +0 -4
- package/site/dist/assets/toViemAccount-B5iDGq1b-Ba8L7zed.js +0 -2
- package/site/dist/assets/transaction-BNTP-bFm-M5tJx8vY.js +0 -1
- package/site/dist/assets/transfer-6YztDh-t-BVQkTvG-.js +0 -1
- package/site/dist/assets/transformers.web-CVP3pJOS.js +0 -40
- package/site/dist/assets/transformers.web-D8W1u_I2.js +0 -49
- package/site/dist/assets/triangle-alert-CWjYH75H.js +0 -1
- package/site/dist/assets/twitch-5IOe4sIQ-lDtgk0os.js +0 -1
- package/site/dist/assets/types-4CuOD23i.js +0 -1
- package/site/dist/assets/use-headless-crypto-deposit-Yn-QRLB3-LUIVFBSd.js +0 -1
- package/site/dist/assets/use-unlink-wallet-BKK4_5Ur-CC2pq27z.js +0 -1
- package/site/dist/assets/useActiveWallet-B-yZYGc6-D7Ik-LxJ.js +0 -123
- package/site/dist/assets/useGetSolPrice-x7gfUIHJ-D1hmQfpA.js +0 -1
- package/site/dist/assets/useGetTokenPrice-CaoUkb79-ZnbuJ-vP.js +0 -1
- package/site/dist/assets/useI18n-C-5n_noF-BcqC76yj.js +0 -1
- package/site/dist/assets/useWalletBalance-CZnP5f_f-Dw2GIJ4g.js +0 -1
- package/site/dist/assets/useWallets-X1ndvJ_U-DRjz7uYD.js +0 -1
- package/site/dist/assets/utils-BkR7EQjg.js +0 -1
- package/site/dist/assets/utils-DJhBNOZA.js +0 -1
- package/site/dist/assets/utils-DdckVfQr.js +0 -1
- package/site/dist/assets/w3m-modal-BeVrBOIf.js +0 -1174
- package/site/dist/assets/wallet-Bn85Sg2W.js +0 -1
- package/site/dist/assets/wallet-Bnzeiqed.js +0 -2
- package/site/dist/assets/wallet-connect-BvJtUsDt-Y03hhqFN.js +0 -1
- package/site/dist/assets/walletconnect-registry-xxG8XLCb-D1SSFvMp.js +0 -1
- package/site/dist/assets/walletconnect-v2-3frQFwbg-CrtLXofr.js +0 -12
- package/site/dist/assets/wui-ux-by-reown-D34ILNLS.js +0 -1147
- package/site/dist/assets/x-rlSp9U7W.js +0 -1
- package/src/discover/explore.js +0 -271
- package/src/market/settle.js +0 -165
- package/src/market/terms.js +0 -200
- package/src/publish/cli.js +0 -336
- package/src/publish/envelope.js +0 -185
- package/src/publish/manifest.js +0 -142
package/src/publish/ship.js
CHANGED
|
@@ -1,42 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// westmarch-ship: one config, one command, a live Fangorn app.
|
|
2
|
+
// westmarch-ship: one config, one command, a live Fangorn app on the free tier.
|
|
3
3
|
//
|
|
4
4
|
// westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run]
|
|
5
5
|
// westmarch-ship app.json --local <out> [--crawl] a view from staged records, no chain (see buildLocal)
|
|
6
|
-
// westmarch-ship app.json --register the app alone:
|
|
6
|
+
// westmarch-ship app.json --register the app alone: its repo, empty site, card bound
|
|
7
|
+
// westmarch-ship app.json --devnet [--only …] the whole app on this machine (see devnetShip)
|
|
8
|
+
// westmarch-ship app.json --index the deployed app's indexer, once, here (see indexHere)
|
|
7
9
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
+
// The app is one Cloudflare Worker (site/worker.js): its indexer (a cron that runs the
|
|
11
|
+
// embedding lens over everything the app presents, into R2), its view, its MCP endpoint
|
|
12
|
+
// (/mcp) and its site. Nothing is embedded here. Every step checks the chain (or
|
|
13
|
+
// Cloudflare) first and only acts on a difference, so re-running is how you update.
|
|
10
14
|
//
|
|
11
|
-
// 1.
|
|
12
|
-
// 2.
|
|
13
|
-
// 3.
|
|
14
|
-
// 4.
|
|
15
|
-
// 5.
|
|
16
|
-
// 6. the stock page, the card and _headers → site/
|
|
17
|
-
// 7. deploy to Cloudflare Pages
|
|
18
|
-
// 8. register the ERC-8004 agent and bind the card, if the chain points elsewhere
|
|
15
|
+
// 1. the app's repo, `<your wallet> : app` (first run only; that block becomes the card's fromBlock)
|
|
16
|
+
// 2. run each source, which crawls and publishes straight into the app
|
|
17
|
+
// 3. the stock page, the card and _headers → .ship/site
|
|
18
|
+
// 4. deploy the Worker: its R2 bucket, Workers AI, the cron, CONFIG (app.json's schema rides in it)
|
|
19
|
+
// 5. register the ERC-8004 agent and point the repo's @card at the card, if it points elsewhere
|
|
19
20
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// The Worker picks up what 2 published on its next cron run (every few minutes),
|
|
22
|
+
// so the view follows the chain with nobody running anything.
|
|
23
|
+
//
|
|
24
|
+
// --register is the fast path, run before there is any data: 1, 3, 4 and 5 pointing at the card
|
|
25
|
+
// without minting. The mint copies the card's skills into its IPFS registration file and is
|
|
26
|
+
// never redone, so it waits for the first full ship, when the card lists what the app does.
|
|
24
27
|
//
|
|
25
28
|
// Chain writes go through the `fangorn` CLI (its configured wallet signs, and
|
|
26
29
|
// `fangorn wallet` shows which). Reads go through the SDK. State lives in `.ship/` next
|
|
27
|
-
// to app.json: the site, the
|
|
30
|
+
// to app.json: the site, the Worker's config and a small state.json.
|
|
28
31
|
|
|
29
|
-
import { spawnSync } from "node:child_process";
|
|
32
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
30
33
|
import { createHash } from "node:crypto";
|
|
31
34
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
32
35
|
import { createRequire } from "node:module";
|
|
33
|
-
import { dirname, join, resolve } from "node:path";
|
|
36
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
34
37
|
import { homedir } from "node:os";
|
|
35
38
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
36
39
|
|
|
37
40
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
38
|
-
const USDC = { "arbitrum-sepolia": "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d", arbitrum: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
|
|
39
|
-
"base-sepolia": "0x036CbD53842c5426634e7929541eC2318f3dCF7e", base: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" };
|
|
40
41
|
const STOCK = join(HERE, "../../site/dist");
|
|
41
42
|
const log = (s) => console.error(`[ship] ${s}`);
|
|
42
43
|
|
|
@@ -44,27 +45,41 @@ const log = (s) => console.error(`[ship] ${s}`);
|
|
|
44
45
|
//
|
|
45
46
|
// {
|
|
46
47
|
// "app": "quorum", "name": "Quorum", "description": "…", "tags": ["civic"],
|
|
47
|
-
// "site": { "project": "quorum",
|
|
48
|
+
// "site": { "project": "quorum", // the Worker's name (and its R2 bucket's: <project>-view)
|
|
49
|
+
// "account": "<cloudflare account id>",
|
|
50
|
+
// "url": "https://quorum.example", // optional: a custom domain routed to the Worker
|
|
51
|
+
// "cron": "*/5 * * * *", // optional: how often the indexer runs
|
|
48
52
|
// "pages": "pages", // optional: the app's own files, copied over the stock page
|
|
49
53
|
// // (pages/intro.html, a fragment, replaces the description on the front page)
|
|
50
54
|
// "nav": [{ "href": "coverage.html", "label": "Coverage" }], // optional: header links to them
|
|
55
|
+
// "examples": ["…"], // optional: questions to start from, on the front page
|
|
56
|
+
// "plugin": {"marketplace": "owner/repo", "name": "…"}, // optional: its Claude Code plugin (hooks), named in its card
|
|
57
|
+
// "skill": "skill/SKILL.md", // optional: the app's agent skill, served at skill/<app>/SKILL.md and named
|
|
58
|
+
// // in the card with its sha256; fangorn-index offers it to whoever opens the app
|
|
51
59
|
// "agent": "app/agent.js" }, // optional: the module whose registerAgent the card lists, when
|
|
52
60
|
// // `pages` replaces the stock index.html with a page of its own
|
|
53
61
|
// "types": { "<tag>": { "description": "…", "role_map": {…}, "presentation": {…} } },
|
|
54
62
|
// "relations": [{ "rel": "…", "from": "<tag>", "to": "<tag>" }],
|
|
55
63
|
// "sources": [{ "namespace": "…", "cwd": "…", "command": ["python", "-m", "…", …] }],
|
|
56
|
-
// "
|
|
57
|
-
//
|
|
64
|
+
// "live": ["readings"], // optional: namespaces members push to from their own wallets (devices,
|
|
65
|
+
// // contributors), presented beside the sources'; true = every repo they push.
|
|
66
|
+
// // Without sources the app already presents every member's repo.
|
|
67
|
+
// "network": "baseSepolia", // required: the SDK's network name
|
|
68
|
+
// "owner": "0x…", // the wallet that publishes the app's repo; only `westmarch build` (the
|
|
69
|
+
// // deploy button) needs it, westmarch-ship asks the CLI
|
|
70
|
+
// "fangorn": "fangorn", // the CLI that signs; e.g. "node ~/fangorn/fangorn/lib/cli/cli.js",
|
|
71
|
+
// // or "env HOME=<dir> node …/cli.js" to sign with <dir>/.fangorn/config.json
|
|
72
|
+
// "paid": true, // optional: the app quotes its records' prices over x402
|
|
73
|
+
// "facilitator": "https://…" // with paid: the x402 facilitator its quotes settle through (Barliman)
|
|
58
74
|
// }
|
|
59
75
|
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
76
|
+
// An app sells nothing itself: publishers seal and price their own records (`fangorn commit
|
|
77
|
+
// --paid body --price 0.01`), and the app presents them. With `paid`, the Worker answers
|
|
78
|
+
// GET /paid/<record id> with an x402 402 built from the record's signed terms, paid through
|
|
79
|
+
// Barliman (CONFIG.facilitator); the buyer then settles and gets the key from the key server.
|
|
64
80
|
//
|
|
65
|
-
// A source is any command that publishes into `--namespace` when given
|
|
66
|
-
//
|
|
67
|
-
// with FANGORN_APP_ID set, so it publishes into this app whatever the CLI's saved app is.
|
|
81
|
+
// A source is any command that publishes into the repo `--namespace` names when given
|
|
82
|
+
// `--publish`.
|
|
68
83
|
|
|
69
84
|
/** `{field}`, `{field|slug}` and `{field|lower}` from a row; an argv item that is exactly
|
|
70
85
|
* `{args...}` splices the row's own `args` array (for flags only some rows need). */
|
|
@@ -93,20 +108,41 @@ export function loadConfig(path) {
|
|
|
93
108
|
c.sources = expandSources(c.sources ?? [], dirname(resolve(path)));
|
|
94
109
|
for (const k of ["app", "name", "description", "site"]) // types and sources can come after --register
|
|
95
110
|
if (!c[k]) throw new Error(`${path}: missing "${k}"`);
|
|
96
|
-
if (!c.site.project) throw new Error(`${path}: site.project names the Cloudflare
|
|
111
|
+
if (!c.site.project) throw new Error(`${path}: site.project names the Cloudflare Worker`);
|
|
97
112
|
for (const s of c.sources) if (!s.namespace || !Array.isArray(s.command))
|
|
98
113
|
throw new Error(`${path}: each source needs a namespace and a command (an argv array)`);
|
|
114
|
+
if (c.live != null && c.live !== true && !(Array.isArray(c.live) && c.live.every((n) => typeof n === "string")))
|
|
115
|
+
throw new Error(`${path}: live is true (every repo members push) or a list of the namespaces they push to`);
|
|
99
116
|
return { relations: [], tags: [], types: {}, ...c };
|
|
100
117
|
}
|
|
101
118
|
|
|
102
|
-
/** The
|
|
103
|
-
*
|
|
104
|
-
export function
|
|
105
|
-
|
|
119
|
+
/** The repo names the view presents: the sources' namespaces plus `live`'s, the ones members push
|
|
120
|
+
* to themselves (devices, contributors). [] means every repo the app presents: no sources, or live: true. */
|
|
121
|
+
export function presentedRepos(cfg) {
|
|
122
|
+
if (cfg.live === true) return [];
|
|
123
|
+
return [...new Set([...cfg.sources.map((s) => s.namespace), ...(cfg.live ?? [])])];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The Worker's wrangler config: the stock worker, the site as its assets, its bucket, the AI
|
|
127
|
+
* binding, the cron and CONFIG. `worker` is site/worker.js unless a test points elsewhere. */
|
|
128
|
+
export function wranglerConfig({ name, site, account, cron = "*/5 * * * *", cpuMs, config, url, ipfsGateway, worker = join(HERE, "../../site/worker.js") }) {
|
|
106
129
|
return {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
name, main: worker, compatibility_date: "2025-09-15", compatibility_flags: ["nodejs_compat"],
|
|
131
|
+
...(account ? { account_id: account } : {}),
|
|
132
|
+
// site.cpu_ms: the indexer's CPU per run. Reading one big commit (50k records) outlasts the
|
|
133
|
+
// 30 s Workers Paid default; up to 300000. The Free plan refuses the deploy with it set.
|
|
134
|
+
...(cpuMs ? { limits: { cpu_ms: cpuMs } } : {}),
|
|
135
|
+
assets: { directory: site, binding: "ASSETS" },
|
|
136
|
+
r2_buckets: [{ binding: "VIEW", bucket_name: `${name}-view` }],
|
|
137
|
+
ai: { binding: "AI" },
|
|
138
|
+
triggers: { crons: [cron] },
|
|
139
|
+
// ipfs.io answers a Worker's reads with 429s; the owner's gateway serves the owner's pins.
|
|
140
|
+
// ponytail: a dedicated Pinata gateway serves only its account's pins unless opened up;
|
|
141
|
+
// an app presenting other publishers' repos needs an open gateway here.
|
|
142
|
+
vars: { CONFIG: config, ...(ipfsGateway ? { IPFS_GATEWAY: ipfsGateway } : {}) },
|
|
143
|
+
// The Worker embeds with its AI binding; the in-process model is ~1.5 MB of bundle it never runs.
|
|
144
|
+
alias: { "@huggingface/transformers": join(HERE, "../../site/no-model.js") },
|
|
145
|
+
...(url ? { routes: [{ pattern: new URL(url).hostname, custom_domain: true }] } : {}),
|
|
110
146
|
};
|
|
111
147
|
}
|
|
112
148
|
|
|
@@ -115,38 +151,35 @@ export function schemaGraph(types, relations = []) {
|
|
|
115
151
|
const stable = (v) => Array.isArray(v) ? v.map(stable)
|
|
116
152
|
: v && typeof v === "object" ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, stable(v[k])])) : v;
|
|
117
153
|
|
|
118
|
-
/** Same shape parseSchema returns, so a config and the chain compare directly. */
|
|
119
|
-
const canonical = (types, relations = []) => JSON.stringify(stable({
|
|
120
|
-
types: Object.fromEntries(Object.entries(types).sort(([a], [b]) => a.localeCompare(b))
|
|
121
|
-
.map(([tag, t]) => [tag, { description: t.description ?? "", role_map: t.role_map ?? {}, presentation: t.presentation ?? {} }])),
|
|
122
|
-
relations: [...relations].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))),
|
|
123
|
-
}));
|
|
124
|
-
|
|
125
154
|
// ── processes ─────────────────────────────────────────────────────────────────
|
|
126
155
|
|
|
127
156
|
/** `${VAR}` and `${VAR:-default}` from the environment, so one app.json runs on a laptop
|
|
128
157
|
* and in CI (e.g. `"${PYTHON:-python3}"`). */
|
|
129
158
|
export const expandEnv = (s) => s.replace(/\$\{(\w+)(?::-([^}]*))?\}/g, (_, k, d) => process.env[k] ?? d ?? "");
|
|
130
159
|
|
|
131
|
-
function run(cmd, args, { cwd, env, capture = false, dry = false } = {}) {
|
|
160
|
+
function run(cmd, args, { cwd, env, capture = false, dry = false, input } = {}) {
|
|
132
161
|
cmd = expandEnv(cmd); args = args.map(expandEnv);
|
|
133
162
|
log(`$ ${[cmd, ...args].join(" ")}${cwd ? ` (in ${cwd})` : ""}`);
|
|
134
163
|
if (dry) return "";
|
|
135
|
-
|
|
136
|
-
|
|
164
|
+
// `input`: what the command reads on stdin (a secret: never on the command line, never logged).
|
|
165
|
+
const r = spawnSync(cmd, args, { cwd, env: { ...process.env, ...env }, encoding: "utf8", input,
|
|
166
|
+
stdio: [input != null ? "pipe" : capture ? "ignore" : "inherit", capture ? "pipe" : "inherit", "inherit"] });
|
|
137
167
|
if (r.status !== 0) throw new Error(`${cmd} ${args[0]} failed (exit ${r.status})`);
|
|
138
168
|
return r.stdout ?? "";
|
|
139
169
|
}
|
|
140
170
|
const expand = (p) => p.replace(/^~(?=\/|$)/, homedir());
|
|
141
171
|
const cliArgv = (cfg) => expandEnv(process.env.FANGORN_CLI ?? cfg.fangorn ?? "fangorn").split(/\s+/).filter(Boolean).map(expand);
|
|
142
172
|
let CLI = ["fangorn"];
|
|
143
|
-
const fangornCli = (
|
|
173
|
+
const fangornCli = (args, opts = {}) => run(CLI[0], [...CLI.slice(1), ...args], { capture: true, ...opts });
|
|
144
174
|
|
|
145
175
|
/** The CLI must be the SDK version westmarch reads with. A different one signs against
|
|
146
|
-
* another deployment's contracts: its
|
|
147
|
-
* and every read says the app is unclaimed. (It happened: a global 2026.08.18-dev CLI
|
|
176
|
+
* another deployment's contracts: its commits land where nothing here looks. (It happened: a global 2026.08.18-dev CLI
|
|
148
177
|
* claimed and published onto the previous registries.) */
|
|
149
178
|
function assertSameDeployment(cli) {
|
|
179
|
+
// "env HOME=… node cli.js": a CLI with its own config (another wallet); find the script past the env.
|
|
180
|
+
let i = cli[0] === "env" ? 1 : 0;
|
|
181
|
+
while (cli[i]?.includes("=")) i++;
|
|
182
|
+
cli = cli.slice(i);
|
|
150
183
|
const which = (c) => spawnSync("sh", ["-c", `command -v ${c}`], { encoding: "utf8" }).stdout.trim();
|
|
151
184
|
const script = cli.length > 1 && /(^|\/)node$/.test(cli[0]) ? cli[1] : which(cli[0]);
|
|
152
185
|
if (!script) throw new Error(`fangorn CLI not found: ${cli.join(" ")}`);
|
|
@@ -160,12 +193,58 @@ function assertSameDeployment(cli) {
|
|
|
160
193
|
"they talk to different contracts. Point app.json's \"fangorn\" at a matching CLI.");
|
|
161
194
|
}
|
|
162
195
|
|
|
196
|
+
/** The template's app.json as `app`'s: its name, its type tag `<app>.item.v1` and the Worker. */
|
|
197
|
+
export const TEMPLATE_APP = "my-app";
|
|
198
|
+
export function nameApp(cfg, app) {
|
|
199
|
+
const from = `${TEMPLATE_APP}.item.v1`, tag = `${app}.item.v1`;
|
|
200
|
+
const types = Object.fromEntries(Object.entries(cfg.types ?? {}).map(([k, t]) =>
|
|
201
|
+
k === from ? [tag, JSON.parse(JSON.stringify(t).replaceAll(from, tag))] : [k, t]));
|
|
202
|
+
return { ...cfg, app, name: cfg.name === TEMPLATE_APP ? app : cfg.name, types,
|
|
203
|
+
site: { ...cfg.site, project: app.replace(/[._]/g, "-") } }; // a Worker name: letters, digits, dashes
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* `westmarch build`: what Workers Builds runs under the deploy button. The site, with a card
|
|
208
|
+
* that names ORIGIN for its own URL (the Worker fills it in), and CONFIG, into the app's own
|
|
209
|
+
* wrangler.json, which `wrangler deploy` then ships with the bucket, AI and cron it declares.
|
|
210
|
+
* No chain writes and no Cloudflare calls: the owner makes the repo and registers its agent
|
|
211
|
+
* from fangorn.network. While app.json still names the template's app, the app is the Worker's name.
|
|
212
|
+
*/
|
|
213
|
+
export async function buildForDeploy(configPath, wranglerPath = join(dirname(resolve(configPath)), "wrangler.json")) {
|
|
214
|
+
const w = JSON.parse(readFileSync(wranglerPath, "utf8"));
|
|
215
|
+
let cfg = loadConfig(configPath);
|
|
216
|
+
if (cfg.app === TEMPLATE_APP) cfg = nameApp(cfg, w.name);
|
|
217
|
+
if (!cfg.network) throw new Error(`${configPath}: "network" is required`);
|
|
218
|
+
if (!/^0x[0-9a-fA-F]{40}$/.test(cfg.owner ?? "")) throw new Error(`${configPath}: "owner" is required: the wallet that publishes the app's repo`);
|
|
219
|
+
if (!w.r2_buckets?.[0]?.bucket_name?.includes(w.name))
|
|
220
|
+
log(`the R2 bucket ${w.r2_buckets?.[0]?.bucket_name} doesn't name the Worker; two apps sharing one overwrite each other's view`);
|
|
221
|
+
|
|
222
|
+
process.env.FANGORN_NETWORK = process.env.CHAIN_NAME = cfg.network;
|
|
223
|
+
const view = await import("./view.js");
|
|
224
|
+
const FangornConfig = await view.network(cfg.network);
|
|
225
|
+
const fromBlock = String(cfg.fromBlock ?? FangornConfig.fromBlock);
|
|
226
|
+
|
|
227
|
+
const base = dirname(resolve(configPath));
|
|
228
|
+
const { site, writeCard } = await buildSite(cfg, base);
|
|
229
|
+
writeCard(view.ORIGIN, { owner: cfg.owner, fromBlock, repos: [] });
|
|
230
|
+
const gateway = process.env.FANGORN_IPFS_GATEWAY;
|
|
231
|
+
w.assets = { ...w.assets, directory: relative(dirname(resolve(wranglerPath)), site) };
|
|
232
|
+
w.vars = { ...w.vars, CONFIG: { app: cfg.app, name: cfg.name, description: cfg.description, fromBlock, network: cfg.network,
|
|
233
|
+
schema: { types: cfg.types, relations: cfg.relations }, repos: [], paid: false },
|
|
234
|
+
...(gateway ? { IPFS_GATEWAY: gateway } : {}) };
|
|
235
|
+
writeFileSync(wranglerPath, JSON.stringify(w, null, 1) + "\n");
|
|
236
|
+
log(`built ${cfg.app} (fromBlock ${fromBlock}) into ${site}; wrangler deploy ships it`);
|
|
237
|
+
}
|
|
238
|
+
|
|
163
239
|
// ── the steps ─────────────────────────────────────────────────────────────────
|
|
164
240
|
|
|
165
|
-
export async function ship(configPath, { crawl = true, deploy = true, dry = false, replace = false, only = null, register = false
|
|
241
|
+
export async function ship(configPath, { crawl = true, deploy = true, dry = false, replace = false, only = null, register = false } = {}) {
|
|
166
242
|
const cfg = loadConfig(configPath);
|
|
167
|
-
|
|
168
|
-
|
|
243
|
+
// No sources is fine: the app then presents every repo on chain (repos: [] = all of them), as the explorer does.
|
|
244
|
+
if (!register && cfg.sources.length && !Object.keys(cfg.types).length)
|
|
245
|
+
throw new Error(`${configPath}: no types yet, so nothing to build; --register sets up the app alone`);
|
|
246
|
+
if (!cfg.network) throw new Error(`${configPath}: "network" is required, e.g. "baseSepolia"`);
|
|
247
|
+
if (cfg.paid && !cfg.facilitator) throw new Error(`${configPath}: "paid" needs "facilitator", the URL of the x402 facilitator (Barliman) its quotes settle through`);
|
|
169
248
|
CLI = cliArgv(cfg);
|
|
170
249
|
assertSameDeployment(CLI);
|
|
171
250
|
const base = dirname(resolve(configPath));
|
|
@@ -174,169 +253,94 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
|
|
|
174
253
|
mkdirSync(dir, { recursive: true });
|
|
175
254
|
const state = existsSync(statePath) ? JSON.parse(readFileSync(statePath, "utf8")) : {};
|
|
176
255
|
const save = () => !dry && writeFileSync(statePath, JSON.stringify(state, null, 1));
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
]);
|
|
182
|
-
env.cacheDir = `${homedir()}/.cache/fangorn-mcp/models`;
|
|
256
|
+
// The SDK's network name: view.js reads with it, and the CLI and the sources spawned below sign on it.
|
|
257
|
+
process.env.FANGORN_NETWORK = process.env.CHAIN_NAME = cfg.network;
|
|
258
|
+
const [{ createPublicClient, http }, view] = await Promise.all([import("viem"), import("./view.js")]);
|
|
259
|
+
const FangornConfig = await view.network(cfg.network);
|
|
183
260
|
const fangorn = await view.readOnlyFangorn();
|
|
184
|
-
|
|
185
|
-
const appId = fangorn.getAppId();
|
|
186
|
-
const registry = fangorn.getAppRegistry();
|
|
261
|
+
const cf = cfg.site.account ? { CLOUDFLARE_ACCOUNT_ID: cfg.site.account } : {};
|
|
187
262
|
|
|
188
|
-
// 1. claim
|
|
189
|
-
const me = (fangornCli(
|
|
263
|
+
// 1. the app's repo: this wallet's `cfg.app`. Repo names are per publisher, so there is nothing to claim.
|
|
264
|
+
const me = (fangornCli(["wallet"]).match(/Address:\s+(0x[0-9a-fA-F]{40})/) ?? [])[1];
|
|
190
265
|
if (!me) throw new Error("`fangorn wallet` shows no address; run `fangorn init`");
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
const chain = createPublicClient({ transport: http(FangornConfig.rpcUrl) });
|
|
267
|
+
const repoDir = join(dir, "repo");
|
|
268
|
+
if (!(await fangorn.onChainTip(me, cfg.app))) {
|
|
269
|
+
// Its own records start now; an app presenting every repo (the explorer) reads from the deployment's start.
|
|
270
|
+
if (presentedRepos(cfg).length) state.fromBlock = String(await chain.getBlockNumber());
|
|
271
|
+
log(`creating the app's repo ${me}:${cfg.app}`);
|
|
272
|
+
mkdirSync(repoDir, { recursive: true });
|
|
273
|
+
fangornCli(["repo", "init", cfg.app], { cwd: repoDir, dry });
|
|
198
274
|
save();
|
|
199
|
-
} else if (owner !== me) {
|
|
200
|
-
throw new Error(`${cfg.app} belongs to ${owner}, not this wallet (${me}); pick another name`);
|
|
201
|
-
}
|
|
202
|
-
state.fromBlock ??= String(cfg.fromBlock ?? 0);
|
|
203
|
-
log(`app ${cfg.app} = ${appId}, owner ${owner}, fromBlock ${state.fromBlock}`);
|
|
204
|
-
|
|
205
|
-
// 2. join
|
|
206
|
-
// Both registries: claiming joins the owner to the app, but commits also need the
|
|
207
|
-
// wallet's global DataRegistry standing, or every push reverts NotRegistered().
|
|
208
|
-
const info = fangornCli(cfg.app, ["app", "info"]);
|
|
209
|
-
if (!/DataRegistry:\s+registered/.test(info) || !/This app:\s+joined/.test(info)) fangornCli(cfg.app, ["register"], { dry });
|
|
210
|
-
|
|
211
|
-
// 3. schema
|
|
212
|
-
let onChain = null;
|
|
213
|
-
if (!register) {
|
|
214
|
-
try { onChain = view.parseSchema((await fangorn.readNamespace(owner, view.SCHEMA_NAMESPACE)).contents); } catch { /* none yet */ }
|
|
215
|
-
if (!onChain || canonical(onChain.types, onChain.relations) !== canonical(cfg.types, cfg.relations)) {
|
|
216
|
-
const sdir = join(dir, "schema");
|
|
217
|
-
mkdirSync(sdir, { recursive: true });
|
|
218
|
-
writeFileSync(join(sdir, "graph.json"), JSON.stringify(schemaGraph(cfg.types, cfg.relations), null, 1));
|
|
219
|
-
log(`schema ${onChain ? "changed" : "is new"}: committing ${Object.keys(cfg.types).join(", ")}`);
|
|
220
|
-
fangornCli(cfg.app, ["repo", "init", view.SCHEMA_NAMESPACE], { cwd: sdir, dry });
|
|
221
|
-
fangornCli(cfg.app, ["commit", "graph.json", "-m", "schema", "--replace"], { cwd: sdir, dry });
|
|
222
|
-
fangornCli(cfg.app, ["push"], { cwd: sdir, dry });
|
|
223
|
-
} else log("schema unchanged");
|
|
224
275
|
}
|
|
276
|
+
state.fromBlock ??= String(cfg.fromBlock ?? FangornConfig.fromBlock);
|
|
277
|
+
log(`app ${me}:${cfg.app}, fromBlock ${state.fromBlock}`);
|
|
225
278
|
|
|
226
|
-
//
|
|
279
|
+
// 2. sources: crawl + publish straight into the app. One source failing (a site that
|
|
227
280
|
// blocks us, a feed that is down) keeps its last published data and does not stop the
|
|
228
281
|
// others; the ship still exits non-zero at the end, naming it.
|
|
229
282
|
const failed = [];
|
|
230
283
|
if (crawl && !register) for (const s of cfg.sources.filter((s) => !only || only.includes(s.namespace))) {
|
|
231
284
|
const cwd = s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base;
|
|
232
|
-
const paidArgs = cfg.paid && s.paid_dir ? ["--paid-dir", resolve(cwd, s.paid_dir)] : [];
|
|
233
285
|
try {
|
|
234
|
-
run(s.command[0], [...s.command.slice(1), ...
|
|
235
|
-
"--publish", "--namespace", s.namespace, "--fangorn-bin", CLI.join(" ")],
|
|
236
|
-
{ cwd, env: { FANGORN_APP_ID: cfg.app }, dry });
|
|
286
|
+
run(s.command[0], [...s.command.slice(1), ...(replace ? ["--replace"] : []),
|
|
287
|
+
"--publish", "--namespace", s.namespace, "--fangorn-bin", CLI.join(" ")], { cwd, dry });
|
|
237
288
|
} catch (e) {
|
|
238
289
|
log(`✗ ${s.namespace}: ${e.message}; its last published records stay live`);
|
|
239
290
|
failed.push(s.namespace);
|
|
240
291
|
}
|
|
241
292
|
}
|
|
242
293
|
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
294
|
+
// 3. the page, the card, the headers
|
|
295
|
+
const { site, writeCard } = await buildSite(cfg, base);
|
|
296
|
+
// site.url: the app's own domain, routed to the Worker. Otherwise its workers.dev URL,
|
|
297
|
+
// which is only known once it is deployed: the first deploy learns it and the card follows.
|
|
298
|
+
let url = cfg.site.url?.replace(/\/$/, "") ?? state.url ?? null;
|
|
299
|
+
const repos = presentedRepos(cfg);
|
|
300
|
+
const writeSite = (u) => writeCard(u, { owner: me, fromBlock: state.fromBlock, repos, paid: !!cfg.paid });
|
|
301
|
+
writeSite(url ?? `https://${cfg.site.project}.workers.dev`);
|
|
251
302
|
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const live = view.liveFields(join(site, "view"));
|
|
264
|
-
if (cfg.paid) {
|
|
265
|
-
const network = cfg.paid.network ?? "arbitrum-sepolia";
|
|
266
|
-
const asset = cfg.paid.asset ?? USDC[network];
|
|
267
|
-
if (!asset) throw new Error(`paid.network ${network}: no default USDC address; set paid.asset`);
|
|
268
|
-
const decimals = cfg.paid.decimals ?? 6;
|
|
269
|
-
const [whole, frac = ""] = String(cfg.paid.price).split(".");
|
|
270
|
-
const price = (BigInt(whole) * 10n ** BigInt(decimals) + BigInt((frac + "0".repeat(decimals)).slice(0, decimals) || "0")).toString();
|
|
271
|
-
paid = { url: `${url}/paid/{id}`, price, asset, network, decimals, symbol: cfg.paid.symbol ?? "USDC", description: cfg.paid.description ?? "" };
|
|
272
|
-
// Only records a live row vouches for (its committed paid_sha256): one left over from
|
|
273
|
-
// an item since removed or re-extracted is not for sale, whatever is still on disk.
|
|
274
|
-
rmSync(join(site, "_paid"), { recursive: true, force: true });
|
|
275
|
-
mkdirSync(join(site, "_paid"), { recursive: true });
|
|
276
|
-
const vouched = new Set(live.map((f) => f.paid_sha256).filter(Boolean));
|
|
277
|
-
let n = 0;
|
|
278
|
-
for (const s of cfg.sources) {
|
|
279
|
-
const d = s.paid_dir && resolve(s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base, s.paid_dir);
|
|
280
|
-
if (d && existsSync(d)) for (const f of readdirSync(d)) {
|
|
281
|
-
const bytes = readFileSync(join(d, f));
|
|
282
|
-
if (!vouched.has(createHash("sha256").update(bytes).digest("hex"))) continue;
|
|
283
|
-
writeFileSync(join(site, "_paid", f), bytes); n++;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
sale = { price, network, asset, payTo: cfg.paid.payTo ?? owner, facilitator: cfg.paid.facilitator ?? "https://facilitator.payai.network",
|
|
287
|
-
name: cfg.paid.tokenName ?? "USD Coin", version: cfg.paid.tokenVersion ?? "2", description: paid.description };
|
|
288
|
-
log(`paid: ${n} record(s) at ${cfg.paid.price} ${paid.symbol} on ${network}, to ${sale.payTo}`);
|
|
289
|
-
}
|
|
290
|
-
// The worker: paid records if any, and each record's source document shown in the page.
|
|
291
|
-
// Only the hosts the app's own records link to (their media role) are proxied.
|
|
292
|
-
const media = Object.values(cfg.types).flatMap((t) => [t.role_map?.media ?? []].flat());
|
|
293
|
-
const docHosts = [...new Set(live.flatMap((f) => media.map((m) => f[m])).filter((u) => typeof u === "string" && u.startsWith("https://"))
|
|
294
|
-
.map((u) => { try { return new URL(u).hostname; } catch { return null; } }).filter(Boolean))].sort();
|
|
295
|
-
writeFileSync(join(site, "_worker.js"), readFileSync(join(HERE, "../../site/worker.js"), "utf8")
|
|
296
|
-
.replace(/\/\*CONFIG\*\/[\s\S]*?\/\*END\*\//, JSON.stringify({ docHosts, paid: sale })));
|
|
297
|
-
log(`documents: shown inline from ${docHosts.length} source host(s)`);
|
|
298
|
-
// The card lists the tools the page at `url` registers: the stock page's, or the app's own.
|
|
299
|
-
const { registerAgent } = await import(cfg.site.agent ? pathToFileURL(resolve(base, cfg.site.agent)).href : "../../site/agent.js");
|
|
300
|
-
const agentCard = card.agentCard({
|
|
301
|
-
name: cfg.name, description: cfg.description, url: `${url}/`, version: new Date().toISOString().slice(0, 10),
|
|
302
|
-
tools: await card.captureTools(() => registerAgent({})), tags: cfg.tags,
|
|
303
|
-
fangorn: { app: cfg.app, fromBlock: state.fromBlock, namespaces, views: [`${url}/view`], ...(paid ? { paid } : {}) },
|
|
304
|
-
});
|
|
305
|
-
mkdirSync(join(site, ".well-known"), { recursive: true });
|
|
306
|
-
writeFileSync(join(site, ".well-known/agent-card.json"), JSON.stringify(agentCard, null, 1));
|
|
307
|
-
const immutable = " Cache-Control: public, max-age=31536000, immutable\n";
|
|
308
|
-
const domains = existsSync(join(site, "view/cdn/domains")) ? readdirSync(join(site, "view/cdn/domains")) : [];
|
|
309
|
-
writeFileSync(join(site, "_headers"),
|
|
310
|
-
"/.well-known/agent-card.json\n Access-Control-Allow-Origin: *\n"
|
|
311
|
-
+ "/view/*\n Access-Control-Allow-Origin: *\n"
|
|
312
|
-
+ `/assets/*\n${immutable}`
|
|
313
|
-
// A shard's name is its content hash, so a browser may keep it for good: a change or a
|
|
314
|
-
// retraction writes new names and the manifest stops pointing at the old ones. `private`
|
|
315
|
-
// keeps it out of shared caches, so a retracted record's bytes stop being served the
|
|
316
|
-
// moment the deploy drops the file (see view.js). An hour made readers on a phone fetch
|
|
317
|
-
// the whole catalog again after every hour away.
|
|
318
|
-
+ domains.map((d) => `/view/cdn/domains/${d}/shards/*\n Cache-Control: private, max-age=31536000, immutable\n`).join(""));
|
|
319
|
-
|
|
320
|
-
// 7. deploy
|
|
303
|
+
// 4. the Worker
|
|
304
|
+
const workerConfig = (u) => {
|
|
305
|
+
const path = join(dir, "wrangler.json");
|
|
306
|
+
writeFileSync(path, JSON.stringify(wranglerConfig({
|
|
307
|
+
name: cfg.site.project, site, account: cfg.site.account, cron: cfg.site.cron, cpuMs: cfg.site.cpu_ms, url: cfg.site.url, ipfsGateway: view.ipfsGateway(),
|
|
308
|
+
config: { app: cfg.app, name: cfg.name, description: cfg.description, fromBlock: state.fromBlock, network: cfg.network,
|
|
309
|
+
schema: { types: cfg.types, relations: cfg.relations }, repos: register ? [] : repos, paid: !!cfg.paid,
|
|
310
|
+
...(cfg.facilitator ? { facilitator: cfg.facilitator } : {}) },
|
|
311
|
+
}), null, 1));
|
|
312
|
+
return path;
|
|
313
|
+
};
|
|
321
314
|
if (deploy) {
|
|
322
|
-
|
|
323
|
-
|
|
315
|
+
const bucket = `${cfg.site.project}-view`;
|
|
316
|
+
const buckets = run("npx", ["wrangler", "r2", "bucket", "list"], { env: cf, capture: true, dry });
|
|
317
|
+
if (!dry && !buckets.split("\n").some((l) => l.trim() === `name: ${bucket}` || l.includes(` ${bucket}`)))
|
|
318
|
+
run("npx", ["wrangler", "r2", "bucket", "create", bucket], { env: cf });
|
|
319
|
+
const out = run("npx", ["wrangler", "deploy", "--config", workerConfig(url ?? "https://pending.invalid")], { env: cf, capture: true, dry });
|
|
320
|
+
const deployed = (out.match(/https:\/\/[a-z0-9.-]+\.workers\.dev/) ?? [])[0];
|
|
321
|
+
if (!url) {
|
|
322
|
+
if (!deployed && !dry) throw new Error("wrangler deploy printed no workers.dev URL; set site.url");
|
|
323
|
+
url = deployed ?? `https://${cfg.site.project}.workers.dev`;
|
|
324
|
+
// The card and CONFIG name the URL, which the first deploy only now learned.
|
|
325
|
+
writeSite(url);
|
|
326
|
+
run("npx", ["wrangler", "deploy", "--config", workerConfig(url)], { env: cf, capture: true, dry });
|
|
327
|
+
}
|
|
324
328
|
state.url = url;
|
|
325
329
|
save();
|
|
326
|
-
}
|
|
330
|
+
} else workerConfig(url ?? `https://${cfg.site.project}.workers.dev`);
|
|
327
331
|
|
|
328
|
-
//
|
|
332
|
+
// 5. ERC-8004 + the repo's @card
|
|
329
333
|
const cardUrl = `${url}/.well-known/agent-card.json`;
|
|
330
|
-
const bound = await
|
|
334
|
+
const bound = (await fangorn.cardOf(me, cfg.app).catch(() => undefined)) ?? "";
|
|
331
335
|
const mint = () => {
|
|
332
|
-
const out = dry ? "" : fangornCli(
|
|
336
|
+
const out = dry ? "" : fangornCli(["card", cardUrl, "--repo", cfg.app]);
|
|
333
337
|
state.agentId = (out.match(/Agent ID:\s+(\S+)/) ?? [])[1] ?? state.agentId;
|
|
334
338
|
delete state.agentPending;
|
|
335
339
|
save();
|
|
336
340
|
};
|
|
337
341
|
if (deploy && bound !== cardUrl && register) {
|
|
338
342
|
log(`binding ${cardUrl} (on chain: ${bound || "nothing"}); the agent is minted on the first full ship`);
|
|
339
|
-
fangornCli(
|
|
343
|
+
fangornCli(["card", cardUrl, "--repo", cfg.app, "--skip-register"], { dry });
|
|
340
344
|
state.agentPending = true;
|
|
341
345
|
save();
|
|
342
346
|
} else if (deploy && bound !== cardUrl) {
|
|
@@ -347,39 +351,71 @@ export async function ship(configPath, { crawl = true, deploy = true, dry = fals
|
|
|
347
351
|
mint();
|
|
348
352
|
} else if (deploy) log("card already bound");
|
|
349
353
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
354
|
+
// A new app has no view until the cron runs, which may be hours off: build the first one now.
|
|
355
|
+
if (deploy && !dry && !register && (cfg.sources.length || cfg.live) && !await fetch(`${url}/view/cdn/catalog`).then((r) => r.ok, () => false)) {
|
|
356
|
+
log("no view yet: indexing it here rather than waiting for the cron");
|
|
357
|
+
await indexHere(configPath).catch((e) => log(`✗ first index: ${e.message}; the cron will build it`));
|
|
353
358
|
}
|
|
354
|
-
|
|
355
|
-
log(`done: ${url} · card ${cardUrl} ·
|
|
359
|
+
|
|
360
|
+
log(`done: ${url} · card ${cardUrl} · MCP ${url}/mcp · the view fills on the Worker's next cron run`);
|
|
356
361
|
if (failed.length) throw new Error(`deployed, but these sources failed and kept their old records: ${failed.join(", ")}`);
|
|
357
|
-
return { url, cardUrl, agentId: state.agentId
|
|
362
|
+
return { url, cardUrl, agentId: state.agentId };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The stock page with the app's own files over it, in .ship/site; `writeCard(url, …)` adds the card for where it is served. */
|
|
366
|
+
export async function buildSite(cfg, base) {
|
|
367
|
+
if (!existsSync(join(STOCK, "index.html"))) throw new Error(`no stock page at ${STOCK}; run \`npx vite build\` in westmarch/site`);
|
|
368
|
+
const site = join(base, ".ship/site");
|
|
369
|
+
rmSync(site, { recursive: true, force: true });
|
|
370
|
+
mkdirSync(site, { recursive: true });
|
|
371
|
+
cpSync(STOCK, site, { recursive: true });
|
|
372
|
+
if (cfg.site.pages) cpSync(resolve(base, cfg.site.pages), site, { recursive: true });
|
|
373
|
+
writeFileSync(join(site, "nav.json"), JSON.stringify(cfg.site.nav ?? []));
|
|
374
|
+
writeFileSync(join(site, "examples.json"), JSON.stringify(cfg.site.examples ?? []));
|
|
375
|
+
// site.skill: the app's agent skill (a SKILL.md), served at skill/<app>/SKILL.md and named in the
|
|
376
|
+
// card with its hash, so fangorn-index can offer it to whoever opens the app.
|
|
377
|
+
let skill = null;
|
|
378
|
+
if (cfg.site.skill) {
|
|
379
|
+
const bytes = readFileSync(resolve(base, cfg.site.skill));
|
|
380
|
+
mkdirSync(join(site, "skill", cfg.app), { recursive: true });
|
|
381
|
+
writeFileSync(join(site, "skill", cfg.app, "SKILL.md"), bytes);
|
|
382
|
+
skill = { path: `skill/${cfg.app}/SKILL.md`, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
383
|
+
}
|
|
384
|
+
// The card lists the tools the page at `url` registers: the stock page's, or the app's own.
|
|
385
|
+
const card = await import("../agent/agent-card.js");
|
|
386
|
+
const { registerAgent } = await import(cfg.site.agent ? pathToFileURL(resolve(base, cfg.site.agent)).href : "../../site/agent.js");
|
|
387
|
+
const tools = await card.captureTools(() => registerAgent({}));
|
|
388
|
+
const config = await (await import("./view.js")).network(cfg.network);
|
|
389
|
+
const writeCard = (u, { owner, fromBlock, repos, paid = false }) => {
|
|
390
|
+
const agentCard = card.agentCard({
|
|
391
|
+
name: cfg.name, description: cfg.description, url: `${u}/`, version: new Date().toISOString().slice(0, 10), tools, tags: cfg.tags,
|
|
392
|
+
// What is sold, as a skill tagged x402: ERC-8004 registration reads x402Support off the tags.
|
|
393
|
+
skills: paid ? [{ id: "records", name: "Paid records", tags: [...(cfg.tags ?? []), "x402"],
|
|
394
|
+
description: `Records whose publishers sell fields (rows with a \`sale\`): GET ${u}/paid/{publisher}/{repo}/{id} quotes one over x402 (scheme "fangorn").` }] : [],
|
|
395
|
+
fangorn: { app: cfg.app, owner, fromBlock, repos, views: [`${u}/view`], mcp: `${u}/mcp`, config, ...(paid ? { paid: { url: `${u}/paid/{publisher}/{repo}/{id}` } } : {}),
|
|
396
|
+
...(skill ? { skill: { url: `${u}/${skill.path}`, sha256: skill.sha256 } } : {}),
|
|
397
|
+
...(cfg.site.plugin ? { plugin: cfg.site.plugin } : {}) },
|
|
398
|
+
});
|
|
399
|
+
mkdirSync(join(site, ".well-known"), { recursive: true });
|
|
400
|
+
writeFileSync(join(site, ".well-known/agent-card.json"), JSON.stringify(agentCard, null, 1));
|
|
401
|
+
writeFileSync(join(site, "_headers"), "/.well-known/agent-card.json\n Access-Control-Allow-Origin: *\n"
|
|
402
|
+
+ "/openapi.json\n Access-Control-Allow-Origin: *\n"
|
|
403
|
+
+ "/.well-known/x402\n Access-Control-Allow-Origin: *\n Content-Type: application/json\n"
|
|
404
|
+
+ "/assets/*\n Cache-Control: public, max-age=31536000, immutable\n");
|
|
405
|
+
};
|
|
406
|
+
return { site, writeCard };
|
|
358
407
|
}
|
|
359
408
|
|
|
360
409
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* The sources run without --publish, or not at all with crawl=false. Every build is whole
|
|
364
|
-
* (a changed `text` role must reach every row, and publishView re-embeds only new CIDs);
|
|
365
|
-
* `.ship/vectors.ndjson` keeps it cheap, so two recipes over the same data embed only the
|
|
366
|
-
* text that differs.
|
|
410
|
+
* What each source staged in its --output-dir, as records: `{namespace: [{id, type, payload}]}`.
|
|
411
|
+
* The sources run first (without --publish) with crawl=true.
|
|
367
412
|
*
|
|
368
413
|
* ponytail: reads every `volume_<n>_*.json` in a source's --output-dir; the
|
|
369
414
|
* harness publishes only the stems its source declares, so a stale file left by an older
|
|
370
|
-
* shape of a source is
|
|
415
|
+
* shape of a source is read here and not on chain. Clear the stage dir if that bites.
|
|
371
416
|
*/
|
|
372
|
-
export
|
|
373
|
-
const
|
|
374
|
-
const base = dirname(resolve(configPath));
|
|
375
|
-
const [{ env }, view] = await Promise.all([import("@huggingface/transformers"), import("./view.js")]);
|
|
376
|
-
env.cacheDir = `${homedir()}/.cache/fangorn-mcp/models`;
|
|
377
|
-
// Not the app's real id or owner: nothing here is compared with the chain.
|
|
378
|
-
const appId = `0x${createHash("sha256").update(cfg.app).digest("hex")}`, owner = `0x${"10ca1".padStart(40, "0")}`;
|
|
379
|
-
const g = schemaGraph(cfg.types, cfg.relations);
|
|
380
|
-
const chain = { [`${owner}/${view.SCHEMA_NAMESPACE}`]: {
|
|
381
|
-
vertices: g.vertices.map((v) => ({ cid: v.id, schemaId: v.tag, payload: v.payload })),
|
|
382
|
-
edges: g.edges.map((e) => ({ relation: e.rel, sourceCid: e.from, targetCid: e.to })) } };
|
|
417
|
+
export function readStage(cfg, base, { crawl = false, only = null, log: say = log } = {}) {
|
|
418
|
+
const out = {};
|
|
383
419
|
for (const s of cfg.sources.filter((s) => !only || only.includes(s.namespace))) {
|
|
384
420
|
const cwd = s.cwd ? resolve(base, s.cwd.replace(/^~/, homedir())) : base;
|
|
385
421
|
if (crawl) run(s.command[0], s.command.slice(1), { cwd, env: { FANGORN_APP_ID: cfg.app } });
|
|
@@ -387,122 +423,232 @@ export async function buildLocal(configPath, out, { crawl = false, only = null,
|
|
|
387
423
|
if (at < 0) { say(`${s.namespace}: no --output-dir in its command, so nothing staged to read`); continue; }
|
|
388
424
|
const dir = resolve(cwd, expandEnv(s.command[at + 1]));
|
|
389
425
|
const volume = s.command.includes("--volume") ? s.command[s.command.indexOf("--volume") + 1] : "1";
|
|
390
|
-
const
|
|
391
|
-
const
|
|
426
|
+
const records = (out[s.namespace] ??= []);
|
|
427
|
+
const byName = new Map();
|
|
392
428
|
for (const f of existsSync(dir) ? readdirSync(dir).sort() : []) {
|
|
393
429
|
const m = new RegExp(`^volume_${volume}_(.+)\\.json$`).exec(f);
|
|
394
430
|
if (!m || m[1] === "edges") continue;
|
|
395
|
-
// The harness
|
|
396
|
-
// addresses it by the payload's hash, and so does this.
|
|
431
|
+
// The harness stages {name, fields}: the record's id and its payload.
|
|
397
432
|
for (const n of JSON.parse(readFileSync(join(dir, f), "utf8"))) {
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
433
|
+
const r = { id: n.name, type: n.fields?.entityType ?? m[1][0].toUpperCase() + m[1].slice(1), payload: { ...n.fields } };
|
|
434
|
+
byName.set(n.name, r);
|
|
435
|
+
records.push(r);
|
|
401
436
|
}
|
|
402
437
|
}
|
|
403
|
-
// Edges name
|
|
438
|
+
// Edges name records by the harness's ids: each becomes a reference on its source.
|
|
404
439
|
const edges = join(dir, `volume_${volume}_edges.json`);
|
|
405
440
|
for (const e of existsSync(edges) ? JSON.parse(readFileSync(edges, "utf8")) : [])
|
|
406
|
-
if (
|
|
441
|
+
if (byName.has(e.from) && byName.has(e.to)) byName.get(e.from).payload[e.rel] = { local: e.to };
|
|
407
442
|
}
|
|
408
443
|
// An empty stage is a crawl that did not happen (or a cache that was not restored), not
|
|
409
444
|
// an app with no records; a view of it would grade as a failure for the wrong reason.
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
mkdirSync(join(base, ".ship"), { recursive: true });
|
|
414
|
-
const embed = view.cachedEmbed(join(base, ".ship/vectors.ndjson"));
|
|
415
|
-
const namespaces = [...new Set(cfg.sources.map((s) => s.namespace))].filter((n) => !only || only.includes(n));
|
|
416
|
-
const report = await view.publishView({ fangorn: view.localFangorn(chain, { appId, owner }), app: cfg.app,
|
|
417
|
-
namespaces, out, embed, log: say });
|
|
418
|
-
const rows = Object.values(report).reduce((n, r) => n + r.added, 0);
|
|
419
|
-
say(`local view: ${out} · ${rows} records, ${embed.misses} embedded, ${rows - embed.misses} from the vector cache`);
|
|
420
|
-
return report;
|
|
445
|
+
if (!Object.values(out).some((rs) => rs.length))
|
|
446
|
+
throw new Error("no staged records under any source's --output-dir: run the sources (--crawl, or a ship) or restore .ship/stage first");
|
|
447
|
+
return out;
|
|
421
448
|
}
|
|
422
449
|
|
|
423
|
-
/**
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
450
|
+
/**
|
|
451
|
+
* The whole app on this machine (devnet.js): a local chain and IPFS, what the sources staged published there, and its Worker under
|
|
452
|
+
* `wrangler dev` with a local R2 and an encoder on localhost (or Workers AI with ai=true).
|
|
453
|
+
* Runs the indexer once, says where everything is, and stays up until interrupted.
|
|
454
|
+
*/
|
|
455
|
+
export async function devnetShip(configPath, { crawl = false, only = null, port = 8787, ai = false } = {}) {
|
|
456
|
+
const cfg = loadConfig(configPath);
|
|
457
|
+
const base = dirname(resolve(configPath));
|
|
458
|
+
const { devnet } = await import("./devnet.js");
|
|
459
|
+
const staged = readStage(cfg, base, { crawl, only });
|
|
460
|
+
const net = await devnet({ log });
|
|
461
|
+
// A paid app's services, as a deployment runs them: the key server publishers deposit with,
|
|
462
|
+
// and Barliman, which its quotes settle through.
|
|
463
|
+
const services = cfg.paid ? await Promise.all([net.keyServer(), net.relay()]).then(([keys, r]) => ({ keys, relay: r.url })) : null;
|
|
464
|
+
const owner = await net.client();
|
|
465
|
+
// The owner publishes the sources, as `fangorn` does in a ship.
|
|
466
|
+
for (const [ns, records] of Object.entries(staged)) {
|
|
467
|
+
if (!records.length) continue;
|
|
468
|
+
await owner.initRepo(ns);
|
|
469
|
+
await owner.uploadBatch(ns, records);
|
|
470
|
+
log(`published ${ns}: ${records.length} records`);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const url = `http://127.0.0.1:${port}`;
|
|
474
|
+
const repos = Object.keys(staged);
|
|
475
|
+
const { site, writeCard } = await buildSite(cfg, base);
|
|
476
|
+
writeCard(url, { owner: owner.getAddress(), fromBlock: "0", repos, paid: !!services });
|
|
477
|
+
const wcfg = wranglerConfig({ name: cfg.site.project, site, config: { app: cfg.app, name: cfg.name, description: cfg.description,
|
|
478
|
+
fromBlock: "0", schema: { types: cfg.types, relations: cfg.relations }, repos, paid: !!services, deployment: net.deployment,
|
|
479
|
+
...(services ? { facilitator: services.relay } : {}) } });
|
|
480
|
+
// Its own R2 and state, apart from a deployed app's.
|
|
481
|
+
wcfg.vars.RPC_URL = net.rpcUrl;
|
|
482
|
+
wcfg.vars.IPFS_GATEWAY = net.gatewayUrl;
|
|
483
|
+
if (!ai) { wcfg.vars.EMBED_URL = await net.encoder(); delete wcfg.ai; } // no binding: wrangler would reach Cloudflare for it
|
|
484
|
+
delete wcfg.routes;
|
|
485
|
+
const wpath = join(base, ".ship/wrangler.devnet.json");
|
|
486
|
+
writeFileSync(wpath, JSON.stringify(wcfg, null, 1));
|
|
487
|
+
rmSync(join(base, ".ship/devnet-state"), { recursive: true, force: true }); // a fresh chain: last run's view is of another one
|
|
488
|
+
const wrangler = spawn("npx", ["wrangler", "dev", "--config", wpath, "--port", String(port), "--ip", "127.0.0.1", "--test-scheduled",
|
|
489
|
+
"--persist-to", join(base, ".ship/devnet-state")], { cwd: join(HERE, "../.."), stdio: ["ignore", "inherit", "inherit"],
|
|
490
|
+
env: { ...process.env, ...(ai && cfg.site.account ? { CLOUDFLARE_ACCOUNT_ID: cfg.site.account } : {}) } });
|
|
491
|
+
net.kids.push(wrangler);
|
|
492
|
+
const up = async () => { for (;;) { if (wrangler.exitCode !== null) throw new Error("wrangler dev exited");
|
|
493
|
+
if (await fetch(url).then((r) => r.ok, () => false)) return; await new Promise((r) => setTimeout(r, 1000)); } };
|
|
494
|
+
await up();
|
|
495
|
+
log(`indexing (the cron, once): ${repos.length} repos`);
|
|
496
|
+
await fetch(`${url}/__scheduled?cron=${encodeURIComponent(wcfg.triggers.crons[0])}`);
|
|
497
|
+
const want = Object.values(staged).filter((rs) => rs.length).length;
|
|
498
|
+
let catalog = null;
|
|
499
|
+
while ((catalog?.domains?.length ?? 0) < want) {
|
|
500
|
+
if (wrangler.exitCode !== null) throw new Error("wrangler dev exited");
|
|
501
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
502
|
+
catalog = await fetch(`${url}/view/cdn/catalog`).then((r) => r.ok ? r.json() : null, () => null);
|
|
503
|
+
}
|
|
504
|
+
log(`done: ${catalog?.domains?.length ?? 0} domains, ${(catalog?.domains ?? []).reduce((n, d) => n + d.count, 0)} rows`);
|
|
505
|
+
log(`site ${url} · MCP ${url}/mcp · view ${url}/view · reindex: curl '${url}/__scheduled' · Ctrl-C stops it all`);
|
|
506
|
+
log(`grade it: npx westmarch-eval ${url}/view --golden eval/golden.jsonl`);
|
|
507
|
+
// ponytail: the devnet's IPFS is in this process's memory, so records are sold from here (sealRecords),
|
|
508
|
+
// not from a separate `fangorn` CLI; scripts/e2e-paid.mjs does exactly that.
|
|
509
|
+
if (services) log(`paid: key server ${services.keys} · Barliman ${services.relay}`);
|
|
510
|
+
await new Promise((r) => wrangler.on("exit", r));
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The deployed app's indexer, run once on this machine: its Worker under `wrangler dev` with
|
|
515
|
+
* the app's own R2 bucket and Workers AI (remote bindings) and its CONFIG. The first view right after a
|
|
516
|
+
* deploy, without waiting for the cron; and a catalog too big for one cron run's limits, since
|
|
517
|
+
* nothing here is capped. Writes the same view the cron would.
|
|
518
|
+
* ponytail: Workers AI still embeds; an encoder on this machine (EMBED_URL) would record another
|
|
519
|
+
* embedder, and the deployed Worker's queries would not match it.
|
|
520
|
+
*/
|
|
521
|
+
export async function indexHere(configPath, { port = 8791 } = {}) {
|
|
522
|
+
const cfg = loadConfig(configPath);
|
|
523
|
+
const base = dirname(resolve(configPath)), dir = join(base, ".ship");
|
|
524
|
+
if (!existsSync(join(dir, "wrangler.json"))) throw new Error(`${dir}/wrangler.json: ship the app first`);
|
|
525
|
+
const w = JSON.parse(readFileSync(join(dir, "wrangler.json"), "utf8"));
|
|
526
|
+
w.r2_buckets = w.r2_buckets.map((b) => ({ ...b, remote: true }));
|
|
527
|
+
w.ai = { ...w.ai, remote: true };
|
|
528
|
+
delete w.routes;
|
|
529
|
+
const wpath = join(dir, "wrangler.index.json");
|
|
530
|
+
writeFileSync(wpath, JSON.stringify(w, null, 1));
|
|
531
|
+
const url = `http://127.0.0.1:${port}`, start = Math.floor(Date.now() / 1000);
|
|
532
|
+
const wrangler = spawn("npx", ["wrangler", "dev", "--config", wpath, "--port", String(port), "--ip", "127.0.0.1", "--test-scheduled"],
|
|
533
|
+
{ cwd: join(HERE, "../.."), stdio: ["ignore", "pipe", "inherit"],
|
|
534
|
+
env: { ...process.env, ...(cfg.site.account ? { CLOUDFLARE_ACCOUNT_ID: cfg.site.account } : {}) } });
|
|
535
|
+
// A run already holds the lock (the deployed cron's): this one skips, and the catalog would never move.
|
|
536
|
+
let skipped = false;
|
|
537
|
+
wrangler.stdout.on("data", (b) => { process.stdout.write(b); if (String(b).includes("index: a run is in progress; skipped")) skipped = true; });
|
|
538
|
+
try {
|
|
539
|
+
for (;;) {
|
|
540
|
+
if (wrangler.exitCode !== null) throw new Error("wrangler dev exited");
|
|
541
|
+
if (await fetch(`${url}/.well-known/agent-card.json`).then((r) => r.ok, () => false)) break;
|
|
542
|
+
await new Promise((ok) => setTimeout(ok, 1000));
|
|
439
543
|
}
|
|
544
|
+
log(`indexing ${cfg.app} here, into ${w.r2_buckets[0].bucket_name}`);
|
|
545
|
+
await fetch(`${url}/__scheduled?cron=${encodeURIComponent(w.triggers.crons[0])}`);
|
|
546
|
+
let catalog = null;
|
|
547
|
+
while (!(catalog?.generated_at >= start)) {
|
|
548
|
+
if (wrangler.exitCode !== null) throw new Error("wrangler dev exited");
|
|
549
|
+
if (skipped) throw new Error("another indexer run holds the lock (the deployed cron's, most likely); it frees within 2 minutes of that run dying. Try again then.");
|
|
550
|
+
await new Promise((ok) => setTimeout(ok, 5000));
|
|
551
|
+
catalog = await fetch(`${url}/view/cdn/catalog`).then((r) => (r.ok ? r.json() : null), () => null);
|
|
552
|
+
}
|
|
553
|
+
log(`indexed: ${catalog.domains.length} domain(s), ${catalog.domains.reduce((n, d) => n + d.count, 0)} rows`);
|
|
554
|
+
return catalog;
|
|
555
|
+
} finally {
|
|
556
|
+
wrangler.kill();
|
|
440
557
|
}
|
|
441
|
-
writeFileSync(join(dir, "cdn/catalog"), JSON.stringify(catalog, null, 1));
|
|
442
|
-
log(`started from the deployed view: ${catalog.domains?.length ?? 0} domain(s), ${files} shard(s)`);
|
|
443
558
|
}
|
|
444
559
|
|
|
445
|
-
/**
|
|
446
|
-
*
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
560
|
+
/**
|
|
561
|
+
* A view from what the sources staged, with app.json's schema, touching no chain and no
|
|
562
|
+
* site: what a recipe change would ship, built so it can be graded first (westmarch-eval).
|
|
563
|
+
* The sources run without --publish, or not at all with crawl=false. Every build is whole
|
|
564
|
+
* (a changed `text` role must reach every row, and publishView re-embeds only new CIDs);
|
|
565
|
+
* `.ship/vectors.ndjson` keeps it cheap, so two recipes over the same data embed only the
|
|
566
|
+
* text that differs.
|
|
567
|
+
*/
|
|
568
|
+
export async function buildLocal(configPath, out, { crawl = false, only = null, log: say = log } = {}) {
|
|
569
|
+
const cfg = loadConfig(configPath);
|
|
570
|
+
const base = dirname(resolve(configPath));
|
|
571
|
+
const [{ env }, view] = await Promise.all([import("@huggingface/transformers"), import("./view.js")]);
|
|
572
|
+
env.cacheDir = `${homedir()}/.cache/fangorn-mcp/models`;
|
|
573
|
+
// Not the app's real owner: nothing here is compared with the chain.
|
|
574
|
+
const owner = `0x${"10ca1".padStart(40, "0")}`;
|
|
575
|
+
const chain = {};
|
|
576
|
+
// Storage addresses a record by its content, and so does this.
|
|
577
|
+
if (cfg.sources.length) for (const [ns, records] of Object.entries(readStage(cfg, base, { crawl, only, log: say })))
|
|
578
|
+
chain[`${owner}/${ns}`] = { records: records.map((r) => ({ ...r, cid: createHash("sha256").update(JSON.stringify(stable(r.payload))).digest("hex") })) };
|
|
579
|
+
// What members already pushed to the live namespaces, read from the chain (reads only, no gas),
|
|
580
|
+
// so a recipe is graded on contributed data too, not just on what the sources staged.
|
|
581
|
+
if (cfg.live) {
|
|
582
|
+
if (cfg.network) process.env.FANGORN_NETWORK = process.env.CHAIN_NAME = cfg.network;
|
|
583
|
+
const fangorn = await view.readOnlyFangorn();
|
|
584
|
+
const statePath = join(base, ".ship/state.json");
|
|
585
|
+
const fromBlock = (existsSync(statePath) ? JSON.parse(readFileSync(statePath, "utf8")).fromBlock : null) ?? cfg.fromBlock;
|
|
586
|
+
const staged = new Set(cfg.sources.map((s) => s.namespace));
|
|
587
|
+
for (const t of await fangorn.repos(fromBlock == null ? {} : { fromBlock: BigInt(fromBlock) })) {
|
|
588
|
+
if (t.repo === "fangorn.schema" || staged.has(t.repo) || (only && !only.includes(t.repo))
|
|
589
|
+
|| (cfg.live !== true && !cfg.live.includes(t.repo))) continue;
|
|
590
|
+
const { contents } = await fangorn.readRepo(t.owner, t.repo);
|
|
591
|
+
chain[`${t.owner.toLowerCase()}/${t.repo}`] = { records: contents.records };
|
|
592
|
+
say(`live ${t.owner}/${t.repo}: ${contents.records.length} records, from the chain`);
|
|
593
|
+
}
|
|
459
594
|
}
|
|
460
|
-
if (!
|
|
461
|
-
|
|
595
|
+
if (!Object.keys(chain).length) throw new Error("nothing to build: no sources staged anything and no publisher has pushed to a live namespace");
|
|
596
|
+
rmSync(out, { recursive: true, force: true });
|
|
597
|
+
mkdirSync(join(base, ".ship"), { recursive: true });
|
|
598
|
+
const embed = view.cachedEmbed(join(base, ".ship/vectors.ndjson"));
|
|
599
|
+
const repos = presentedRepos(cfg).filter((n) => !only || only.includes(n));
|
|
600
|
+
const report = await view.publishView({ fangorn: view.localFangorn(chain), app: cfg.app,
|
|
601
|
+
schema: { types: cfg.types, relations: cfg.relations }, repos, out, embed, log: say });
|
|
602
|
+
const rows = Object.values(report).reduce((n, r) => n + r.added, 0);
|
|
603
|
+
say(`local view: ${out} · ${rows} records, ${embed.misses} embedded, ${rows - embed.misses} from the vector cache`);
|
|
604
|
+
return report;
|
|
462
605
|
}
|
|
463
606
|
|
|
464
607
|
// ── cli ───────────────────────────────────────────────────────────────────────
|
|
465
608
|
if (process.argv[1]?.endsWith("ship.js") || process.argv[1]?.endsWith("westmarch-ship")) {
|
|
466
609
|
if (process.argv[2] === "--selfcheck") {
|
|
467
|
-
// The schema as committed, read back the way the chain returns it, must compare
|
|
468
|
-
// equal to the config, or every run re-commits an unchanged schema.
|
|
469
|
-
const { parseSchema } = await import("./view.js");
|
|
470
|
-
const types = { "t.v1": { description: "d", role_map: { title: "name", text: ["body"] }, presentation: { externalUrl: { "t.v1": "{url}" } } },
|
|
471
|
-
"u.v1": { description: "e", role_map: { title: "label" } } };
|
|
472
|
-
const relations = [{ rel: "cites", from: "t.v1", to: "u.v1" }];
|
|
473
|
-
const g = schemaGraph(types, relations);
|
|
474
|
-
// Keys reversed at every depth, as a canonical (sorted) encoding hands them back.
|
|
475
|
-
const reverse = (v) => Array.isArray(v) ? v.map(reverse)
|
|
476
|
-
: v && typeof v === "object" ? Object.fromEntries(Object.keys(v).reverse().map((k) => [k, reverse(v[k])])) : v;
|
|
477
|
-
const readBack = parseSchema({
|
|
478
|
-
vertices: g.vertices.map((v) => ({ cid: `cid-${v.id}`, schemaId: v.tag, payload: reverse(v.payload) })),
|
|
479
|
-
edges: g.edges.map((e) => ({ relation: e.rel, sourceCid: `cid-${e.from}`, targetCid: `cid-${e.to}` })),
|
|
480
|
-
});
|
|
481
|
-
if (canonical(readBack.types, readBack.relations) !== canonical(types, relations)) throw new Error("schema does not round-trip");
|
|
482
|
-
if (canonical(readBack.types, readBack.relations) === canonical({ ...types, "v.v1": {} }, relations)) throw new Error("a new type must count as a change");
|
|
483
610
|
const row = { city: "Wisconsin Rapids", state: "WI", site: "https://x", args: ["--archive-page", "/569/A"] };
|
|
484
611
|
const cmd = fill(["py", "--site", "{site}", "--city", "{city}", "{args...}", "--ns", "us-{state|lower}-{city|slug}"], row);
|
|
485
612
|
if (JSON.stringify(cmd) !== JSON.stringify(["py", "--site", "https://x", "--city", "Wisconsin Rapids", "--archive-page", "/569/A", "--ns", "us-wi-wisconsin-rapids"]))
|
|
486
613
|
throw new Error(`template fill: ${JSON.stringify(cmd)}`);
|
|
487
|
-
|
|
614
|
+
const w = wranglerConfig({ name: "quorum", site: "/s", config: { app: "quorum" }, url: "https://quorum.example" });
|
|
615
|
+
if (w.r2_buckets[0].bucket_name !== "quorum-view" || !w.ai || !w.triggers.crons.length || w.routes[0].pattern !== "quorum.example"
|
|
616
|
+
|| !w.compatibility_flags.includes("nodejs_compat") || !existsSync(w.main) || !existsSync(w.alias["@huggingface/transformers"]))
|
|
617
|
+
throw new Error(`wrangler config: ${JSON.stringify(w)}`);
|
|
618
|
+
const src = [{ namespace: "towns", command: ["py"] }];
|
|
619
|
+
if (presentedRepos({ sources: src }).join() !== "towns" || presentedRepos({ sources: src, live: ["readings", "towns"] }).join() !== "towns,readings"
|
|
620
|
+
|| presentedRepos({ sources: src, live: true }).length || presentedRepos({ sources: [] }).length)
|
|
621
|
+
throw new Error("presentedRepos: sources plus live namespaces; [] (every repo) for live: true or no sources");
|
|
622
|
+
console.log("ship.js self-check ok — a source template fills per row, the Worker config binds its bucket, AI and cron, and presentedRepos merges sources and live namespaces");
|
|
488
623
|
process.exit(0);
|
|
489
624
|
}
|
|
490
625
|
const [path, ...flags] = process.argv.slice(2);
|
|
491
626
|
if (!path || flags.includes("--help")) {
|
|
492
|
-
console.error("usage: westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run] [--replace] [--
|
|
493
|
-
" westmarch-ship app.json --register [--dry-run] (
|
|
494
|
-
" westmarch-ship app.json --local <out> [--crawl] [--only ns1,ns2] (no chain, no deploy)"
|
|
627
|
+
console.error("usage: westmarch-ship app.json [--no-crawl] [--no-deploy] [--dry-run] [--replace] [--only ns1,ns2]\n" +
|
|
628
|
+
" westmarch-ship app.json --register [--dry-run] (the app's repo, empty site, card bound: no data)\n" +
|
|
629
|
+
" westmarch-ship app.json --local <out> [--crawl] [--only ns1,ns2] (no chain, no deploy)\n" +
|
|
630
|
+
" westmarch-ship app.json --index (the deployed app's indexer, once, on this machine)\n" +
|
|
631
|
+
" westmarch-ship app.json --devnet [--crawl] [--only ns1,ns2] [--port 8787] [--ai] (the whole app on this machine)");
|
|
495
632
|
process.exit(path ? 0 : 2);
|
|
496
633
|
}
|
|
497
634
|
const only = flags.includes("--only") ? flags[flags.indexOf("--only") + 1].split(",") : null;
|
|
635
|
+
if (flags.includes("--devnet")) {
|
|
636
|
+
await devnetShip(path, { crawl: flags.includes("--crawl"), only, ai: flags.includes("--ai"),
|
|
637
|
+
port: flags.includes("--port") ? Number(flags[flags.indexOf("--port") + 1]) : 8787 })
|
|
638
|
+
.catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
|
|
639
|
+
process.exit(0);
|
|
640
|
+
}
|
|
641
|
+
if (flags.includes("--index")) {
|
|
642
|
+
await indexHere(path).catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
|
|
643
|
+
process.exit(0);
|
|
644
|
+
}
|
|
498
645
|
if (flags.includes("--local")) {
|
|
499
646
|
await buildLocal(path, resolve(flags[flags.indexOf("--local") + 1]), { crawl: flags.includes("--crawl"), only })
|
|
500
647
|
.catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
|
|
501
648
|
process.exit(0);
|
|
502
649
|
}
|
|
503
650
|
await ship(path, { crawl: !flags.includes("--no-crawl"), deploy: !flags.includes("--no-deploy"), dry: flags.includes("--dry-run"),
|
|
504
|
-
replace: flags.includes("--replace"), register: flags.includes("--register"),
|
|
505
|
-
only })
|
|
651
|
+
replace: flags.includes("--replace"), register: flags.includes("--register"), only })
|
|
506
652
|
.catch((e) => { console.error(`[ship] ✗ ${e.message}`); process.exit(1); });
|
|
507
653
|
process.exit(0); // a CUDA embedder keeps the process alive (and can abort in its teardown)
|
|
508
654
|
}
|