{"_id":"@bysir/herdr-web","_rev":"11-d062b44dc10954c85db23e19099c1362","name":"@bysir/herdr-web","dist-tags":{"latest":"0.6.1"},"versions":{"0.1.0":{"name":"@bysir/herdr-web","version":"0.1.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.1.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"3ae42b80765beb4fa895302401ebe520d6e95cf7","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.1.0.tgz","fileCount":3,"integrity":"sha512-nGYZsWycGXM0dd7pwiuSh101KOHLgsusgtg1QONjZDHXrNjCshTZKW0gQczxawNaWMya+P0qYF6PQdef2id/pQ==","signatures":[{"sig":"MEUCIEUm3OmxOD+bvrtNRIXN/LDBjBVDVC/ZmVkZnWpUNpbkAiEA7q4vhqKFQm32N8sQdKHW3pSdaHrxWarzPWVmLhze8ok=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":73122},"engines":{"node":">=18"},"gitHead":"9a86a55ff1806106b8cb21d624ebfc4736c3cafc","_npmUser":{"name":"bysir","email":"zbysir@qq.com"},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.1.0","@bysir/herdr-web-darwin-x64":"0.1.0","@bysir/herdr-web-linux-arm64":"0.1.0","@bysir/herdr-web-darwin-arm64":"0.1.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.1.0_1787310800106_0.5876479787537239","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@bysir/herdr-web","version":"0.2.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.2.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"f8b73c6ddd6a57b760b69feebe0d677b43f8d61f","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.2.0.tgz","fileCount":3,"integrity":"sha512-a6oGS0ZvJMQkF0yCOzrAdp9MroAKbs0ihcUMWHBtLUfUAJS9m8q1seE2i3Cxtx2pMdxoVrp7RUXClwaagdlQrA==","signatures":[{"sig":"MEYCIQCM6EjXf4QFoOtx7YnCbOLL+gA7fy86QdSAAz623ez+MQIhAIwxtSWyFYVeg+SCgv2KNu8Km+A6lSPRQB4itSn4w2Jg","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":84203},"engines":{"node":">=18"},"gitHead":"5dd0eca9a55c34553dacc045e50ddbdac0cff8f3","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.2.0","@bysir/herdr-web-darwin-x64":"0.2.0","@bysir/herdr-web-linux-arm64":"0.2.0","@bysir/herdr-web-darwin-arm64":"0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.2.0_1787319834210_0.190537062818682","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@bysir/herdr-web","version":"0.3.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.3.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"b7eac43705d279f44bca9c36bde7931b5a3aba60","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.3.0.tgz","fileCount":3,"integrity":"sha512-28d2uR0SkhD1PR1HcPUgCf4RLzhMsNucgKKUK61/3rK31Yh0JKpkUQaeQXicAPoEDdADVonhxrfD6DzgxP4+kQ==","signatures":[{"sig":"MEQCIHrernad7YS3DcVxYttmSM7x/VZr+VBo5LOUU0nuQR4qAiAURCR1Wkj+EZcsZGeqUNVaGEnn/cdOI9iagPQnTZYXkg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":117497},"engines":{"node":">=18"},"gitHead":"8dbbb1b126f0d7787af4686fbf0931a93f4a1c15","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.3.0","@bysir/herdr-web-darwin-x64":"0.3.0","@bysir/herdr-web-linux-arm64":"0.3.0","@bysir/herdr-web-darwin-arm64":"0.3.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.3.0_1787366689912_0.30768715280489123","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@bysir/herdr-web","version":"0.4.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.4.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"d62727f8fe52afd08e71b5c0d70afc451bf9380c","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.4.0.tgz","fileCount":3,"integrity":"sha512-nzSsaXvdQroVVN3YegDaD5/OjK59fzHO1nk3Q/6JhlR8CjpDULiEYPdys29BBop+8pXehc03y/kFwNyw+KkiqA==","signatures":[{"sig":"MEMCHw9oc3/dEz3drNOa7JBReSaSm1ex8BCJReOxWijHewMCIDhprK1Pxm07s5CEPfqlzH9/2AVSRTq6aj2scwRD2qVM","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.4.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":44026},"engines":{"node":">=18"},"gitHead":"986e5debf8d03e6f59f5f8a92c4a8340474ebb6e","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.4.0","@bysir/herdr-web-darwin-x64":"0.4.0","@bysir/herdr-web-linux-arm64":"0.4.0","@bysir/herdr-web-darwin-arm64":"0.4.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.4.0_1787454834607_0.4940453323723537","host":"s3://npm-registry-packages-npm-production"}},"0.5.0":{"name":"@bysir/herdr-web","version":"0.5.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.5.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"09a0a772769fbe6d7e884acfae8e13504ad8d804","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.5.0.tgz","fileCount":3,"integrity":"sha512-wftcpE8HY72LjEsFt09BGU3+mhtywMxi/1yDHz8t1wicLJEhpfn6nNkK3M61bjTdzxiCoEUKtAysRVvxJ6eOvQ==","signatures":[{"sig":"MEUCICYVSwkmKkD6y9AfhoAe9nh2ljPfnzx0+mnul4PAgWMrAiEA4+8aPrFESezcEe8vX0/5ciJc8DE2/yFsXsM0cN/Dj+4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.5.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":45741},"engines":{"node":">=18"},"gitHead":"8bdd6de942e1216bb86d2dab9fe56fffe3c5f625","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.5.0","@bysir/herdr-web-darwin-x64":"0.5.0","@bysir/herdr-web-linux-arm64":"0.5.0","@bysir/herdr-web-darwin-arm64":"0.5.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.5.0_1787564655864_0.7991671533701075","host":"s3://npm-registry-packages-npm-production"}},"0.5.1":{"name":"@bysir/herdr-web","version":"0.5.1","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.5.1","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"377477b1cbf95523245a513e028fb381717c8185","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.5.1.tgz","fileCount":3,"integrity":"sha512-5jZcNv7hZXTJUA4pVUlhaCZI99iuYisFKQLCiFOI0oFVf18/6HH1Nn4kdHulXMlHjWomgRCb0RTx105Z3eEsQQ==","signatures":[{"sig":"MEUCIHbrYmmGfkd5rDObmDJ1D5f8lUCC4Xp05XmKgWXtxUuDAiEA702g/mo10ZI8/LlmNsuOgVD7q9OGxJVWcMWBr5GUMxg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.5.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":45741},"engines":{"node":">=18"},"gitHead":"fc61a18d73ba982cbdd882e6d2d9d7de2b638802","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.5.1","@bysir/herdr-web-darwin-x64":"0.5.1","@bysir/herdr-web-linux-arm64":"0.5.1","@bysir/herdr-web-darwin-arm64":"0.5.1"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.5.1_1787566015819_0.2518836039013186","host":"s3://npm-registry-packages-npm-production"}},"0.5.2":{"name":"@bysir/herdr-web","version":"0.5.2","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.5.2","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"a07dd3c7f2e89da42e4ac44522b371913911f155","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.5.2.tgz","fileCount":3,"integrity":"sha512-QEeX/DZhjqjt7PvW68ywG6bk/Kf8GmfXSDVYB0SgyBOXVWz7YEG/F0MF1VRcG17MQoM7f1o0GAsfCdHZLXdEfg==","signatures":[{"sig":"MEUCIQCHrjrGO11mLwAzUZHgC5M4KVKkYQWAyT0rwLj0r0u5TQIgNOXJcnJqAeKfDdRK5+oX4f9XlEPFNDhspZeMFq6eS5Y=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.5.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":45741},"engines":{"node":">=18"},"gitHead":"63e350a4f45c50a52cfa5f612a98b39ff4563d3a","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.5.2","@bysir/herdr-web-darwin-x64":"0.5.2","@bysir/herdr-web-linux-arm64":"0.5.2","@bysir/herdr-web-darwin-arm64":"0.5.2"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.5.2_1787627455121_0.8363870634836759","host":"s3://npm-registry-packages-npm-production"}},"0.5.3":{"name":"@bysir/herdr-web","version":"0.5.3","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.5.3","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"6cc7daf6ff6f3baa902bd6c077f479535749a94f","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.5.3.tgz","fileCount":3,"integrity":"sha512-XCzYwC6MQmWxb70UBCo/VAMZDD3eHCiWHuge9kYbbbG5fR6BW4skAxCD1+WSvduOLQTnc053/aDHyV3ugItw6g==","signatures":[{"sig":"MEYCIQDOcHdp7wPNQqpK3cxE5NJAZMAQx+aUgwpnqpvcKEZa8wIhALdWcwUF4+c/2FPmxS5RxZjP+x4kuPlZ1wzS3hds+xsJ","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.5.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":48210},"engines":{"node":">=18"},"gitHead":"43582fa6ec8854097ddff68d1c3b0d8b865b1b09","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.5.3","@bysir/herdr-web-darwin-x64":"0.5.3","@bysir/herdr-web-linux-arm64":"0.5.3","@bysir/herdr-web-darwin-arm64":"0.5.3"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.5.3_1787911441332_0.7322175997916014","host":"s3://npm-registry-packages-npm-production"}},"0.5.4":{"name":"@bysir/herdr-web","version":"0.5.4","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.5.4","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"401db5bee8d1922b35594b9964aabfda33b70e89","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.5.4.tgz","fileCount":3,"integrity":"sha512-90ddzhG2fHBKJ1LGtXEnBvA9Ly3dDzbeZtoZbp7pK6e15vHiZUREJFA3Jiks9rubV/MtqIniUEXX2bRK2hyAIQ==","signatures":[{"sig":"MEUCIQD9y2kJ6f3lqPG5Nqb3UnfDHwNc5ntAuFNgrv3rvLGBEQIgCGA5jI50CIl31e0/KqI9dwZz2dYKOUyBYiWilQwNZqs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.5.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":48210},"engines":{"node":">=18"},"gitHead":"d023f10c820db82f26e2d043c7ee86c48fc3bc59","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.5.4","@bysir/herdr-web-darwin-x64":"0.5.4","@bysir/herdr-web-linux-arm64":"0.5.4","@bysir/herdr-web-darwin-arm64":"0.5.4"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.5.4_1787914517609_0.45175779372907465","host":"s3://npm-registry-packages-npm-production"}},"0.6.0":{"name":"@bysir/herdr-web","version":"0.6.0","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"license":"MIT","_id":"@bysir/herdr-web@0.6.0","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"homepage":"https://github.com/zbysir/herdr-web#readme","bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"bin":{"herdr-web":"bin/herdr-web.js"},"dist":{"shasum":"552165cdc0bde1a66227b0e03a9b807b4d8d3e95","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.6.0.tgz","fileCount":3,"integrity":"sha512-uCqFsqv5V67bhhZgLl8bu1My79jF18zfvJwoSxk51rC9UIdubC6XuOZ5Elqll+kHHTDxbXINeTjfYTmhhngCgQ==","signatures":[{"sig":"MEUCIH4zn8C1LCqyH37skvGY+hWaJRhS7YidiPUb1JZXPVz0AiEA2LNqppghO75mD+OTu9B3tP1+iD3TArr+eR1QO397CKg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.6.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":49743},"engines":{"node":">=18"},"gitHead":"dcc71b988b3d6e9020b3e54f58bafd297dce4ae7","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"_nodeVersion":"24.19.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.6.0","@bysir/herdr-web-darwin-x64":"0.6.0","@bysir/herdr-web-linux-arm64":"0.6.0","@bysir/herdr-web-darwin-arm64":"0.6.0"},"_npmOperationalInternal":{"tmp":"tmp/herdr-web_0.6.0_1788438595142_0.5552353881612695","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"_id":"@bysir/herdr-web@0.6.1","bin":{"herdr-web":"bin/herdr-web.js"},"bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"dist":{"shasum":"fa3ae027a43e6540d0722cea1032e7d26186d9c0","tarball":"https://registry.npmjs.org/@bysir/herdr-web/-/herdr-web-0.6.1.tgz","fileCount":3,"integrity":"sha512-Zq7JKh5IZO+9WlSb1pPj0ILGRD4ehj80oUQ0tUKA6i39Rx9ORwnUwnQX9bK+7n+FLjC6ijQQkM186EMwD703bg==","signatures":[{"sig":"MEUCIB081O1jdFn0OgakcA4+eewv3oHTB/qorP/rYik3tcvSAiEAmNhDVV3h2My1JPrIN4DVIcP1/PV+XdswNto8dv2/+Gk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCQnwK9WdWLw7jaIUd5QwXyaRymckQkfxilM9Za3WEQAwIhAKbUHofwaSBjmrYuT2YGSNydv2HXNj1Gm9AQsiancDTI"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@bysir%2fherdr-web@0.6.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":49743},"name":"@bysir/herdr-web","engines":{"node":">=18"},"gitHead":"a4aa4509dad8db7065bc3e9f89b82e39d3213fa8","license":"MIT","version":"0.6.1","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:61293257-a324-4628-a332-b594bdeb63e6"}},"homepage":"https://github.com/zbysir/herdr-web#readme","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"_npmVersion":"11.18.0","description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","directories":{},"maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"_nodeVersion":"24.20.0","_hasShrinkwrap":false,"optionalDependencies":{"@bysir/herdr-web-linux-x64":"0.6.1","@bysir/herdr-web-darwin-x64":"0.6.1","@bysir/herdr-web-linux-arm64":"0.6.1","@bysir/herdr-web-darwin-arm64":"0.6.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/herdr-web_0.6.1_1789535287140_0.4348506254996818"}}},"time":{"created":"2026-08-21T11:13:19.837Z","modified":"2026-09-16T05:08:07.565Z","0.1.0":"2026-08-21T11:13:20.246Z","0.2.0":"2026-08-21T13:43:54.362Z","0.3.0":"2026-08-22T02:44:50.098Z","0.4.0":"2026-08-23T03:13:54.738Z","0.5.0":"2026-08-24T09:44:15.984Z","0.5.1":"2026-08-24T10:06:56.001Z","0.5.2":"2026-08-25T03:10:55.273Z","0.5.3":"2026-08-28T10:04:01.454Z","0.5.4":"2026-08-28T10:55:17.726Z","0.6.0":"2026-09-03T12:29:55.277Z","0.6.1":"2026-09-16T05:08:07.229Z"},"bugs":{"url":"https://github.com/zbysir/herdr-web/issues"},"license":"MIT","homepage":"https://github.com/zbysir/herdr-web#readme","keywords":["herdr","terminal","web-terminal","coding-agent","claude-code"],"repository":{"url":"git+https://github.com/zbysir/herdr-web.git","type":"git"},"description":"浏览器里的 herdr 终端 + 语音投稿。一个 Go 二进制，前端嵌在里面。","maintainers":[{"name":"bysir","email":"zbysir@qq.com"}],"readme":"# herdr-web\n\n<p align=\"center\">\n  <img src=\"assets/logo.png\" alt=\"herdr-web\" width=\"96\" />\n</p>\n\n<p align=\"center\">\n  <b>English</b> · <a href=\"README.zh-CN.md\">简体中文</a>\n</p>\n\nA terminal in your browser, built for running [`herdr`](https://github.com/zbysir/herdr).\nOne Go binary with the frontend baked in. Works on phones.\n\n**Voice compose** is the point of this project: dictate on a tablet, select the words that came out\nwrong and say them again, then hand the whole paragraph to an agent's input line. A phone is enough\nto get by; a tablet in landscape gives you 211 columns — that is a workstation.\n\nThis document covers **installing, using and configuring** it. Why each thing works the way it does,\nand what it cost to learn, lives in the [documents listed at the end](#documents) — those are the\nreal substance of this project. They are in Chinese.\n\n## Install\n\n```bash\nnpm install -g @bysir/herdr-web       # easiest if you have node; upgrades come free\nherdr-web                             # listens on 127.0.0.1 only\n```\n\nNo node (common on servers):\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/zbysir/herdr-web/master/install.sh | sh\n```\n\nInstalls into `~/.local/bin`. To install somewhere else, the variable has to go to `sh`, **not** to `curl`:\n\n```bash\ncurl -fsSL …/install.sh | HERDR_WEB_INSTALL_DIR=/opt/bin sh    # right\nHERDR_WEB_INSTALL_DIR=/opt/bin curl -fsSL …/install.sh | sh    # wrong — curl gets it, the script never sees it\n```\n\nThe wrong one **does not fail**; it quietly installs to the default directory. Same shape for `HERDR_WEB_INSTALL_VER=v0.1.0` to pin a version.\n\nThe installer **always verifies sha256** and refuses to install if neither `sha256sum` nor `shasum` exists — there is a login shell behind this thing.\n\nOther ways in:\n\n```bash\nmake build && ./herdr-web             # from source (frontend → internal/webui/dist → go build)\nHERDR_WEB_HOST=0.0.0.0 ./herdr-web    # listen on the LAN, and print a QR code for your phone\n```\n\n`go install github.com/zbysir/herdr-web/cmd/herdr-web@latest` works too, but **what you get has no frontend**: the web assets are produced by `make build` and embedded, and they are not in the repo, so `go install` can't see them. That binary is only useful with `--web <dir>` pointing at a frontend you built yourself, or for the CLI subcommands. Use one of the three above if you want the page.\n\n**No native Windows build** — install it inside WSL. Not laziness: the terminal in the browser needs a real PTY (Go side uses `creack/pty`, whose Windows implementation is a `return nil, ErrUnsupported` stub) and herdr itself speaks over a unix socket. Inside WSL it is simply the Linux build, fully functional; the browser end was always cross-platform, so `http://localhost:7788/` on Windows works fine. On win32 the npm package prints that explanation instead of installing something that cannot run.\n\nTo run it as a service that starts at boot, see [Daemon](#daemon). To upgrade, see [Updating](#updating).\n\n**Environment variables are the only source of configuration** (no config file; the only flag is `--web`). The full list, how to set it, and a few common setups are under [Configuration](#configuration). Subcommands: `herdr-web --help`.\n\nOn startup it prints the addresses you can reach it at. When listening on `0.0.0.0` it scores the interfaces and marks the one your phone can actually reach with `← use this one from your phone` (the pile of OrbStack / VPN virtual interfaces gets pushed to the bottom); that is the address encoded in the QR code.\n\n**Pair each device once.** The startup banner carries a one-time pairing code (5 minutes, single use) and its QR code — scan it from your phone and you are in, zero typing. After that your bookmark holds no secret (the credential lives in an `HttpOnly` cookie), and changing Wi-Fi, changing subnets or rebooting costs you nothing. To pair another device, run `herdr-web pair` on the machine.\n\nTwo ways to scan:\n\n- **Your camera app** (works everywhere): the code is just a link with `?pair=`; scanning opens it and you land already paired.\n- **\"Scan with camera\" inside the pairing page**: opens the rear camera, points at the code on the host screen, pairs on recognition without navigating. This button **only appears when it can work** — it needs `BarcodeDetector` (the system decoder, which saves tens of KB of JS; macOS uses Vision, Android uses ML Kit, and **iOS Safari and Chrome on Linux do not have it**) plus a camera (only granted in a secure context — plain http on a LAN gets nothing). If either is missing the button is not rendered at all, rather than left there to fail on click.\n\nIf neither is convenient, type the 8-digit code into the pairing page; it submits itself once you have typed 8 characters. What the in-page scanner reads is reduced to the `pair=` part and goes through the same `POST /auth/pair`, so the security model is unchanged (only someone at the machine can produce a code).\n\n```bash\nherdr-web pair          # print a fresh one-time pairing code + QR\nherdr-web devices       # list paired devices (label / last seen / last IP / expiry)\nherdr-web revoke <id>   # kick one (all = everything); the next request gets 401\nherdr-web unlock        # clear the global \"too many failures\" circuit breaker\n```\n\nThe ⚙ at the right end of the top bar is the **settings panel**; its \"Devices\" page shows who has paired, lets you **sign this device out**, and kick others. Kicking one and kicking everyone both take two clicks. **The web UI never issues a pairing code** (not even to an already-paired device) — see [Security](#security) for why.\n\n**Out of codes? Go back to the machine and run `herdr-web pair`.** That is not laziness either — see below.\n\nThe old never-expiring `~/.herdr-web/token` is demoted to **bootstrap only**: an old bookmark exchanges it for a device credential on first open and scrubs the token out of the URL, after which you should `rm ~/.herdr-web/token`. Details and reasoning in [SECURITY.md](docs/dev/SECURITY.md) (Chinese).\n\nOnce connected it **types `herdr` for you**. To type something else, or nothing: `HERDR_WEB_ONCONNECT` (set it to an empty string to stay in the shell). Adding a path segment to the URL (`/work`) gives you **a different herdr session** — see [First run](#first-run). The old \"run herdr\" button in the top bar is gone: with autotyping it earns its place less than once a day, and the shortcut bar ships a preset for it if you want one.\n\n**The admin page is at `http://127.0.0.1:<port+1>/`** (also in the startup banner): certificate status, one-click issue/renew, generated DNS `.env` snippets, pairing codes, device kicking. It is **bound to loopback and does not exist on the public internet**, so it needs no login — anything that can reach it already has your shell. Why not \"an authenticated page on the main server\": authentication is a control that can fail, unreachability is a property; and the admin page must not depend on the very certificate it exists to fix (a broken certificate would lock you out of the page that repairs it).\n\n## First run\n\nOn startup it prints the addresses you can reach it at. When listening on `0.0.0.0` it scores the\ninterfaces and marks the one your phone can actually reach with `← use this one from your phone`;\nthat is the address encoded in the QR code.\n\n**Pair each device once.** The startup banner carries a one-time pairing code (5 minutes, single use)\nand its QR code — scan it from your phone and you are in, zero typing. After that your bookmark holds\nno secret (the credential lives in an `HttpOnly` cookie), and changing Wi-Fi, changing subnets or\nrebooting costs you nothing. Three ways to scan: your camera app (the code is a link with `?pair=`),\n\"scan with camera\" inside the pairing page (only shown when it can work — it needs `BarcodeDetector`\nand a camera, which requires a secure context), or typing the 8-digit code into the pairing page.\n\n```bash\nherdr-web pair          # print a fresh one-time pairing code + QR\nherdr-web devices       # list paired devices (label / last seen / last IP / expiry)\nherdr-web revoke <id>   # kick one (all = everything); the next request gets 401\nherdr-web unlock        # clear the global \"too many failures\" circuit breaker\n```\n\n**The web UI never issues a pairing code** (not even to an already-paired device) — see\n[Security](#security). Out of codes? Go back to the machine and run `herdr-web pair`.\n\nOnce connected it **types `herdr` for you**. To type something else, or nothing:\n`HERDR_WEB_ONCONNECT` (an empty string means stay in the shell).\n\n**A path segment in the URL is a different herdr session**: `/work` types\n`herdr --session work` and creates it if needed; `/scratch` is another one. Two bookmarks are two\nworking contexts that survive closing the browser. Names are `[A-Za-z0-9._-]`, 40 characters max;\nan invalid one is an error rather than a silent fallback to the default session — using the wrong\nsocket would **silently deliver your words into another herdr**.\n\n**The admin page is at `http://127.0.0.1:<port+1>/`**: certificate status, one-click issue/renew,\ngenerated DNS `.env` snippets, pairing codes, device kicking. It is bound to loopback and does not\nexist on the public internet, so it needs no login — anything that can reach it already has your shell.\n\n**Local shell only.** To reach another machine, ssh from inside herdr — herdr does that itself, so\nthis layer implements no host management and no key storage, and the \"the browser can touch your\nprivate keys\" attack surface never exists.\n\n## What you get\n\n### Outbox (voice compose)\n\nThe strip with a textarea at the bottom of the page is the outbox; the ✎ in the top bar toggles it\nand it is **on by default**. You dictate or type in there, fix what came out wrong, then hand the\nwhole paragraph to one of herdr's panes.\n\n| Control | What it does |\n|---|---|\n| **Target** | Defaults to \"follow herdr's current pane\" — nothing to pick, it goes to whatever you have focused in herdr. You can also pin one from the dropdown |\n| **Post** `⌘↵` / `Ctrl↵` | Clears the remote input line first, then submits the whole thing. `Enter` inserts a newline and does not submit |\n| **Pull back** | Grabs what is already in the remote input line into the textarea for editing (useful when the remote side has been Tab-completing) |\n| **Auto pull** | Every 500ms by default. Switching panes swaps in the new pane's content; **never overwrites a local draft**, it just says so in the status line |\n| **Two-way** | Local edits get pushed back into the remote input line (without Enter). Off by default — see the caveats below |\n| **Image** | Upload an image; the path is inserted **at the cursor**. On a phone it offers camera / library; on a desktop just `⌘V` a screenshot into the box, or drop a file. You do not need the outbox open for this — bind `act:img` on the shortcut bar, or paste anywhere on the page |\n| `↑` | With an empty box, recalls the last thing you posted (30 kept locally) |\n| `Esc` | **Forwarded to the terminal.** Esc means nothing inside a plain textarea, while the agent needs it constantly (overlays like `/usage` close with it); focus does not move, so you can press it repeatedly |\n\nUploading does not need the outbox open: bind `act:img` on the shortcut bar, or **paste anywhere on\nthe page** (an image in the clipboard is uploaded directly). Where the path lands depends on whether\nthe outbox is open — appended to your draft, or typed straight into the terminal.\n\n→ Why a separate box at all, how images actually work, the two-way caveats, measured polling\nlatency: [OUTBOX.md](docs/dev/OUTBOX.md)\n\n### Shortcut bar\n\nPhones have no Ctrl key, and herdr's `ctrl+b` prefix depends on one. The keys live **on the server**\n(`~/.herdr-web/softkeys.json`), so phone / tablet / desktop share one set of definitions, edited in\nSettings → Shortcut keys.\n\n- The \"Keys\" field takes a **key spec**; space-separated entries fire in sequence — `ctrl+b c` is the\n  prefix plus c, one tap.\n- Supports `ctrl+x` `alt+x` `shift+tab`, named keys (`esc tab enter space bs del ins up down left\n  right home end pgup pgdn f1-f12`) and literal text (`text:/new`; quote it if it has spaces).\n- `sticky:ctrl` / `sticky:alt` are **sticky** modifiers: tap once to light it up, then a letter sends\n  the combination.\n- `act:` actions run in the browser and send no bytes: `act:kbd` (system keyboard), `act:img`\n  (upload), `act:panes` (pane list), `act:files` (file browsing), `act:clip` / `act:paste`\n  ([copy and paste on a phone](docs/dev/MOBILE.md#手机上怎么复制--粘贴)).\n- Every key has a **\"double-tap\"** checkbox; close pane / close tab / `/clear` ship with it on —\n  keys sit close together and a misfire cannot be undone.\n- \"Load presets\" pours sixty-odd keys into \"My keys\", after which every one of them is yours to edit.\n\nKey specs are parsed into bytes **on the server**, so a typo is reported at save time — telling you\nwhich key and where it stopped making sense — rather than shipped as a key that does nothing.\n\n### Pane list · notices\n\nThe ▦ in the top bar (or `act:panes` on the shortcut bar; on a phone you can also tap herdr's own\n`switch`) opens a list of panes, one per row — **tap one and you are there, zoomed full screen**.\nYou can filter (tab / title / path / pane id) and show only panes running an agent. The list\nrefreshes itself every 4 seconds.\n\nWhen an agent stops to wait for you (or has just finished), **a card appears in the top right\ncarrying what it said**, and a badge lights up on the ▦. Tapping the card jumps there. Opening the\npane list is what marks them read.\n\nIt is an index, not a second interface: after the tap you are looking at the same herdr terminal, and\nevery keyboard habit is unchanged.\n\n→ Sort order, the \"3 minutes ago\" column, when a notice fires, how the badge counts, system\nnotifications: [MOBILE.md](docs/dev/MOBILE.md)\n　How that text is scraped off the screen: [COMPOSER.md](docs/dev/COMPOSER.md)\n\n### File browsing\n\nThe agent says \"the plot is at `/tmp/plot-3.png`\" — **tap that path and look at it**. Absolute paths\nopen directly; `./out/a.png` resolves against that pane's cwd. The 📁 in the top bar (or `act:files`)\nis the fallback: it starts from every pane's cwd + the upload directory + home + temp, `..` walks all\nthe way to `/`, and you can paste an absolute path to open it.\n\nImages (png / jpg / gif / webp, identified by magic number) are shown, text is shown as-is, anything\nelse downloads. From the viewer you can **hand the file to the agent** in one tap (its absolute path\ngoes into the outbox).\n\n**There is no boundary by default** — anyone who can open this page already has a login shell, so an\nallowlist would not stop them and would only get in the way daily. If you want one, set\n`HERDR_WEB_FILE_ROOTS` (that is a real jail); to remove the feature, `HERDR_WEB_FILES=0`.\n\n→ The short-lived link route and the four hard rules on it (never `text/html`, why SVG is safe to\nrender): [SECURITY.md](docs/dev/SECURITY.md)\n\n### Reading a diff\n\n`git diff` is close to unreadable in a phone terminal: long lines are either cut off or scroll\nsideways, a wall of red against a wall of green does not show you *which word* changed, and paging\nmeans driving a pager with arrow keys. The \"改动\" button in the top bar (or `act:diff`) opens a\nseparate layer:\n\n- **a file list first**: what changed, `+n −m` per file, which parts are already `git add`ed;\n- tapping one opens a patch that **wraps long lines** (on by default; the button in its header\n  toggles it, and the choice is remembered in this layout profile), with **word-level highlighting**\n  on lines that pair up — only the part that actually changed gets the darker background;\n- **every file of the change is one continuous stream**: keep scrolling past a and you are in b, no\n  going back to the list. The header tracks where you are (`3 / 19 · filename`), and tapping the band\n  between two files folds one away. Files you have not reached yet hold their place and are fetched\n  as you approach them, one at a time — no fanning out a dozen `git diff`s on the machine your agents\n  are working on;\n- three views: **working tree vs the last commit** (including new files) / **staged** / **the last\n  commit itself**;\n- you never pick the repository: it is discovered from every pane's cwd, deduplicated by repo root,\n  with the focused pane first;\n- the top-bar button carries a **green dot** when there are changes **you have not looked at yet** —\n  not merely \"there are changes\", which is permanently true in a repo an agent is working in. Opening\n  the panel counts as looking; the dot comes back when the agent touches something again. It follows\n  the \"panel dot\" switch in settings — turned off, it does not even poll.\n\n**Read-only.** No add / commit / checkout here, and none planned — anything that changes the\nrepository belongs in the terminal, where you have all of git and can see its output. The boundary\nis the same one as file browsing (`HERDR_WEB_FILES=0` turns this off too, and `HERDR_WEB_FILE_ROOTS`\nstill jails the repo root); if this machine has no `git`, the button is not drawn.\n\n### Phones and tablets\n\nWhen a program has mouse reporting on (herdr does), touch gestures are taken over entirely:\n\n| Gesture | Behaviour |\n|---|---|\n| One-finger vertical swipe | Converted to SGR wheel reports by line height — `CSI < 64/65 ; col ; row M` — and sent to the program; with mouse reporting off it scrolls the local scrollback |\n| Tap | With mouse reporting, sends `CSI < 0 ; col ; row M/m` (clicking panes and tabs both work) and **does not pop the system keyboard**; without it, focuses the hidden textarea (a tap there does mean \"I want to type\"). It goes out immediately, with no delay |\n| Long press (≈380ms) | **Grab**: press the left button and hold, plus `CSI < 32` motion reports, so moving afterwards is a drag — this is how you resize herdr's pane borders on a phone. Releasing sends the matching `m` |\n\n**There is no double tap.** It used to be the \"show / hide the system keyboard\" gesture, and it is gone — one gesture was costing the feel of every other tap. To tell \"this is a tap\" from \"this is the first half of a double tap\", every tap has to sit out the double-tap window (320ms) before it can be sent, so clicking panes and clicking things inside Claude all lag a beat; and without the wait, that first tap **leaks into the program in the pane** — Claude Code has its own clickable UI (expanding a block, **picking an option**), and a leaked tap picks the option for you. Paying \"every click is imprecise and might answer for you\" for one keyboard shortcut is not worth it.\n\nThe keyboard now comes from **buttons** only: the ⌨ shortcut key (`act:kbd` — the first key in the factory set, and the shortcut bar is on by default on phones) and the \"system keyboard\" button in the top bar (drag it on under Settings → Top bar). A button press means you want the keyboard — nothing to guess, and no delay.\n\n**The outbox and the shortcut bar are one dock**: drag either side edge to change its width (when an\nIME covers half the screen, shrink the whole dock into what is left), and the three handles on the\ntop edge of the key area set the height and the boundaries; double-tap any handle to reset. A phone\nin portrait (< 440px) switches to another tier: no handles, full width, one horizontally-scrolling\nrow of keys. **Landscape and portrait keep separate sets**, swapped on rotation.\n\n**The top bar is yours to arrange**, and **layouts are stored per kind of device**: the six keys you\narranged on a phone do not follow you to the desktop, while the definitions stay shared.\n\n→ Why the gestures are split this way, how the keyboard is handled, copy and paste on a phone, the\ndetails of the dock and the top bar: [MOBILE.md](docs/dev/MOBILE.md)\n\n### Settings panel\n\nThe ⚙ at the right end of the top bar, in four pages: **Terminal** (font size / light-dark, kitty\nprotocol / Option as Meta / copy on select / synchronized output, herdr's switch opening our pane\nlist, the badge on the panel icon), **Top bar**, **Shortcut keys**, **Devices**. Above the tabs there is\none more row: which layout profile this device uses. The three overlays (pane list / files /\nsettings) are mutually exclusive.\n\n### Keyboard\n\nherdr's shortcuts are almost all `ctrl+b` plus an ordinary key, which legacy encoding can express.\nThe kitty protocol covers what legacy cannot and is on by default (Settings → Terminal):\n`Ctrl+Shift+letter`, `Ctrl+digit`, `Ctrl+Enter` / `Shift+Enter` / `Ctrl+Tab`.\n\nKeys the browser keeps for itself: on macOS `⌘W` `⌘T` `⌘N` `Ctrl+Tab`; on Windows/Linux also\n`Ctrl+W` `Ctrl+T` `Ctrl+N` `Ctrl+Shift+I/J/C`. Installing as a PWA gets some of them back.\n\nCopy `⌘C` (or `Ctrl+Shift+C`) · paste `⌘V` · clear `⌘K` · `Option` is Meta by default. Copy and\npaste on a phone is a different story — herdr copies to the clipboard of **the machine running\nherdr** — see [MOBILE.md](docs/dev/MOBILE.md#手机上怎么复制--粘贴).\n\n## Configuration\n\n**Environment variables are the only source of configuration.** There is no config file, and the only flag is `--web` (point at a frontend directory during development). It is funnelled through [viper](https://github.com/spf13/viper) in `internal/config/` (`SetEnvPrefix(\"HERDR_WEB\")` + `AutomaticEnv()`), so settings and variable names map one to one.\n\nNot reading a config file is deliberate: there is a login shell behind this port, so \"which configuration is actually in effect\" has to be visible at a glance — environment variables are right there in `ps`, in the systemd unit, in the launchd plist. Add \"there might also be a yaml in some directory\" and the first half day of any incident goes into finding out which one won. Same reason there is no \"flags override environment\": one setting with two entry points means having to specify precedence.\n\n### How to set it\n\n```bash\n# Try something: prefix the command, applies to this run only\nHERDR_WEB_PORT=8000 HERDR_WEB_ONCONNECT= ./herdr-web\n\n# Permanent: in ~/.zshrc (when you start it by hand in a terminal)\nexport HERDR_WEB_HOST=0.0.0.0\nexport HERDR_WEB_TLS=auto\n\n# Permanent: launchd (macOS) in the plist's EnvironmentVariables;\n# systemd in the unit's Environment= / EnvironmentFile=\n```\n\nThree rules, all about not guessing:\n\n- **An explicit empty string counts.** `HERDR_WEB_ONCONNECT=` means \"type nothing on connect\"; it does not fall back to the default `herdr`. Every switch with a default depends on this to be turnable off.\n- **A malformed integer is treated as unset** (falls back to the default) rather than silently becoming 0; below-minimum values are clamped. `HERDR_WEB_DEVICE_TTL_DAYS=9O` (letter O) will not turn device credentials into \"never expires\".\n- **Booleans accept `1` / `true`** (any case); anything else is off.\n\nChanges take effect on restart — configuration is read once at startup. To confirm what was read, look at the startup banner: shell, data directory, herdr socket, TLS tier and paired device count are all printed there.\n\n### Basics\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `HERDR_WEB_PORT` | `7788` | The main port. **It only serves the local network**: a connection whose peer is not loopback / private / link-local / CGNAT gets 403 and nothing else. Public access is a separate, explicit port — see `HERDR_WEB_PUBLIC_PORT` |\n| `HERDR_WEB_HOST` | `127.0.0.1` | Listen address; `0.0.0.0` opens it to the LAN |\n| `HERDR_WEB_TOKEN` | reads `~/.herdr-web/token` | **Legacy**; only good for bootstrapping once (exchanged for a device credential). Not generated on new installs |\n| `HERDR_WEB_SHELL` | `$SHELL` | The shell run inside the PTY |\n| `HERDR_WEB_ONCONNECT` | `herdr` | Typed into the PTY on connect (Enter included). **Set it to an empty string to type nothing.** **Session URLs ignore this** (`/work` always types `herdr --session work`, see [First run](#first-run)) — to always land in a session, bookmark the URL rather than setting this |\n| `HERDR_WEB_ONCONNECT_MS` | `250` | How long to wait before typing that line. The wait starts **after the shell's first output** — an rc file touching `stty`, or a completion plugin initialising, **silently swallows** characters typed too early. If the auto-typed line does not land, raise it |\n| `HERDR_WEB_DIR` | `~/.herdr-web` | Data directory, in two layers: configuration and files (`softkeys.json` / `tls/` / `uploads/`) at the root, **internal data** (device credentials, passkey public keys) under `data/` — those two are not meant to be hand-edited, and tampering is reported in the terminal. **Keep the path short**: a unix socket (`ctl.sock`) is opened inside it, and beyond ~100 bytes it cannot bind, which breaks the subcommands |\n| `HERDR_WEB_FILES` | on | `=0` turns file browsing off: `/api/files/*` and `/_f/` all 404, and the 📁 in the top bar is not drawn (an entry point that opens onto a wall of 404s is worse than no entry point) |\n| `HERDR_WEB_GIT` | on | `=0` turns the diff panel off: `/api/git/*` all 404 and the top-bar button is not drawn. **It also sits under `HERDR_WEB_FILES`** — a diff *is* file content, so being able to read diffs while file browsing is off would make that switch a lie. Same when this machine has no `git` |\n| `HERDR_WEB_FILE_ROOTS` | empty | Comma-separated directories. Set, this is **a real allowlist** (a jail) and only those trees are visible. **Empty means no boundary** — the reasoning is in [File browsing](#file-browsing). `~` is expanded; non-absolute entries are discarded (relative to what? keeping them only makes the prefix check pass somewhere surprising) |\n\n### Outbox / talking to herdr\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `HERDR_WEB_SOCKET` | `$HERDR_SOCKET_PATH` or `~/.config/herdr/herdr.sock` | The herdr socket the outbox connects to. **Do not rely on `HERDR_SOCKET_PATH`**: `dropEnv` strips `HERDR_*`, and this process may not have been started from a herdr pane at all |\n| `HERDR_WEB_POLL_MS` | `500` | How often the outbox checks \"where is focus, what is in the input line\". Minimum 200 |\n| `HERDR_WEB_PUSH_MS` | `700` | With \"two-way\" on, how long after you stop typing the draft is pushed. Minimum 100 |\n| `HERDR_WEB_NOTICE_MS` | `4000` | How often notices (the cards and the unread badge) ask \"anything new\". **`0` turns the whole notice feature off** and the frontend stops polling. Anything under 1000 is treated as 1000 — this tick only reads memory on the server (it does not touch the herdr socket), but a notice is inherently 2.5 seconds behind the state change (debounce), so polling harder cannot beat that |\n| `HERDR_WEB_SETTLE_MS` | `120` | How long to wait between two `pane.read` calls (to defeat the one-frame snapshot lag). **Never 0**: herdr sometimes answers in 1-2ms, both reads land on the same frame, and the clear loop misreads that as \"cannot be cleared\". The clear path has its own 120ms floor |\n\n### Exposure / TLS / credentials\n\nDetails in [SECURITY.md](docs/dev/SECURITY.md) (Chinese).\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `HERDR_WEB_PUBLIC_PORT` | off | **The port to expose.** Opens a second listener on `0.0.0.0:<port>` sharing the same handler, and that is where a tunnel / port forward / reverse proxy should point — never at the main port, which serves the local network only. Requests arriving here are treated as public: loopback-without-pairing and the legacy token's `loopback` tier do not apply (the source address of a tunnelled request is 127.0.0.1 too, so the only trustworthy signal is *which listener it landed on*), the rate limiter's \"never block localhost\" exemption is off, and TLS becomes mandatory. Why a separate port instead of a switch on the main port: a switch is a *declaration*, and declarations get forgotten — the person (or agent) writing code on this machine sees `127.0.0.1:7788` and has no way to know a tunnel is forwarding it, so every decision made under \"only my machine can reach this\" becomes a hole. With a separate port, forgetting to configure it means the tunnel gets connection refused |\n| `HERDR_WEB_EXPOSED` | off | **Legacy; prefer `HERDR_WEB_PUBLIC_PORT`.** `=1` declares that *the main port itself* is reachable from the internet (frp / port forwarding / tunnels) — it cannot be detected, only declared. Once declared: TLS is mandatory, loopback-without-pairing is turned off, and the main port's \"local network only\" gate is lifted (you said it is public). Kept for machines already configured this way |\n| `HERDR_WEB_TLS_CERT` / `_KEY` | empty | Use the certificate you supply. If you own a domain and got a real certificate via DNS-01, take this route — zero browser warnings, no profiles to install, least friction |\n| `HERDR_WEB_ACME_DNS` | empty | Let herdr-web **get its own certificate**; the value is the DNS provider: `cloudflare` / `alidns` / `tencentcloud` / `route53` / `digitalocean` / `huaweicloud`. It uses DNS-01, so nothing has to reach you from outside — behind NAT, or with the domain pointed at a LAN address, it still works. **Where to get each provider's token and what scope it needs: [DNS.md](DNS.md)** (Chinese) |\n| `HERDR_WEB_ACME_EMAIL` | empty | ACME account email. Can be empty, but then you get no expiry reminders either |\n| `HERDR_WEB_ACME_STAGING` | off | `=1` uses Let's Encrypt staging. **Turn it on while debugging**: production allows 5 certificates per domain set per week, and a few attempts lock you out for a week |\n| `HERDR_WEB_TLS` | see notes | `auto` self-signed (local CA + 397-day leaf, re-issued automatically when the IP changes) / `off` plaintext / `proxy` something in front already terminated TLS. Default: exposed or listening on the LAN → `auto`, purely local → `off` |\n| `HERDR_WEB_LAN_PORT` | off | Opens a **second listener** on `0.0.0.0:<port>` with a self-signed certificate whose SANs track your current LAN addresses, so a page loaded through a tunnel can probe for a direct LAN route and switch to it — two public hops per keystroke become one switch hop. One manual step per device that cannot be skipped: **open it once and click through the certificate warning**; until then the probe fails at the TLS handshake and the page quietly stays on the tunnel. It has to be TLS — an https page's fetch to an `http://` target is active mixed content and is blocked unconditionally, so a plaintext LAN port cannot be probed at all. Not needed when the main port already serves self-signed TLS on the LAN. **The direct origin holds its own credential** (cookies are host-only), so the same tablet shows up twice in the device panel — the switch carries a one-time pairing code across for you — and **passkeys do not work there**, because a WebAuthn RP ID has to be a domain and a bare IP is not one (installing the CA does not change that). Details: [DEPLOY.md](DEPLOY.md) |\n| `HERDR_WEB_HOSTNAME` | empty | Domains allowed in the `Host` header, comma separated. **IPs always pass, domains must be listed** — this is the only defence against DNS rebinding, and anything else gets a 421 |\n| `HERDR_WEB_PUBLIC_URL` | empty | The address you **actually visit** (`https://herdr.example.com:17788`). With frp the public port is often not the local one, and without this the QR code in the banner is useless. The domain in it is allowlisted automatically |\n| `HERDR_WEB_DEVICE_TTL_DAYS` | `90` | How long a device credential survives without use (renewed on every use). `0` = **never expires** |\n| `HERDR_WEB_RPID` | derived | The domain a passkey is bound to. Defaults to the first `HERDR_WEB_HOSTNAME`, or `localhost` when purely local. **A bare IP is not a valid value** — such deployments cannot use passkeys |\n| `HERDR_WEB_REAUTH_HOURS` | `24` | Once a passkey is registered, how long a session credential remains valid after the last biometric check. `0` = no re-verification (passkeys serve only as the login / new-device path). **Does nothing at all while no passkey is registered** |\n| `HERDR_WEB_LEGACY_TOKEN` | `on` | `on` / `loopback` (the old token only works locally) / `off`. Once migrated, just delete the token file |\n| `HERDR_WEB_TRUST_LOOPBACK` | off | `=1` exempts requests from 127.0.0.1 from pairing. **Never turn this on behind frp or a reverse proxy** — there, public requests also arrive from 127.0.0.1, i.e. everyone is \"local\". When on, it additionally requires `Host` to be a loopback literal |\n| `HERDR_WEB_TRUST_PROXY` | off | `=1` is required to read `X-Forwarded-For`. With no trusted proxy in front, leaving it on lets an attacker forge the source IP with a header and walk around per-IP rate limiting |\n| `HERDR_WEB_INSECURE` | off | `=1` permits \"exposed but no TLS\". No legitimate use beyond temporary debugging |\n| `HERDR_WEB_UPDATE_CHECK` | on | `=0` disables automatic update checks. With it off the process makes **no outbound requests at all** — a hard requirement in the kind of environment where an internal machine must not dial out. Only the automatic check is disabled; `herdr-web update --check` still works |\n\n### Troubleshooting\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `HERDR_WEB_DEBUG_INPUT` | off | `=1` logs every batch of bytes written into the PTY as hex (including the auto-typed line, prefixed `onconnect`). The only way to answer \"what exactly did that key send\" — guessing does not work |\n\n### Read but not prefixed with `HERDR_WEB_`\n\n| Variable | When it matters |\n|---|---|\n| `SHELL` | The shell run inside the PTY when `HERDR_WEB_SHELL` is unset (falling back to `/bin/zsh`) |\n| `HERDR_SOCKET_PATH` | Fallback herdr socket when `HERDR_WEB_SOCKET` is unset. **Do not count on it being there**: `dropEnv` strips `HERDR_*` from child processes (to prevent nesting), and this process may not have started from a herdr pane |\n\n### A few common setups\n\n```bash\n# 1. Purely local (default): plain http, since loopback is a secure context anyway\n./herdr-web\n\n# 2. Phone / tablet on the LAN: self-signed TLS, pair by scanning the banner QR\nHERDR_WEB_HOST=0.0.0.0 ./herdr-web\n\n# 3. Exposed through frp / a tunnel: point the tunnel at PUBLIC_PORT, never at the\n#    main port — the main port only serves the local network, and every default on\n#    it assumes the internet cannot reach it. PUBLIC_URL decides which address the\n#    QR code encodes\nHERDR_WEB_PUBLIC_PORT=17788 HERDR_WEB_TLS=proxy \\\nHERDR_WEB_PUBLIC_URL=https://herdr.example.com \\\nHERDR_WEB_HOSTNAME=herdr.example.com ./herdr-web\n\n# 4. Your own domain + a real certificate (zero browser warnings, least friction)\nHERDR_WEB_HOST=0.0.0.0 HERDR_WEB_HOSTNAME=herdr.example.com \\\nHERDR_WEB_TLS_CERT=/etc/ssl/herdr/fullchain.pem \\\nHERDR_WEB_TLS_KEY=/etc/ssl/herdr/privkey.pem ./herdr-web\n\n# 5. Do not drop into herdr on connect (stay in the shell)\nHERDR_WEB_ONCONNECT= ./herdr-web\n```\n\n## Daemon\n\nInstall it as a user-level service that starts on boot:\n\n```bash\nherdr-web service install     # macOS → launchd LaunchAgent; Linux → systemd user unit\nherdr-web service status      # installed? running? PID? where are the logs?\nherdr-web service logs        # tail -f the log\nherdr-web service restart     # needed after replacing the binary\nherdr-web service uninstall   # stop and remove (data and logs untouched)\n```\n\n**Configuration is copied out of the current shell at install time**, so the order is \"get the environment right, then install\". **Changing it later takes the same route** — there is no \"edit one setting\" subcommand; changing configuration means installing again from a different environment (idempotent — it overwrites and restarts):\n\n```bash\nHERDR_WEB_PUBLIC_PORT=9000 herdr-web service install   # change one, the rest comes from this shell\nherdr-web service install --env-file .env              # or let one file be the single source\n```\n\nWhat gets copied is every `HERDR_WEB_*`, plus `PATH` / `SHELL` / `HOME` / `USER` / `LOGNAME` / `LANG` / `LC_ALL` / `TERM` / `HERDR_SOCKET_PATH`. `install` prints the whole list — from then on, \"which configuration is this machine's service actually using\" can only be answered by the plist / unit, so it is cheapest to read it at install time. To check later, read that file directly (`service status` only answers installed / running, never configuration):\n\n```bash\nplutil -p ~/Library/LaunchAgents/io.github.zbysir.herdr-web.plist   # macOS\nsystemctl --user cat herdr-web.service                              # Linux\n```\n\nIt is **plaintext**, credentials included — do not paste that output into a pane with an agent in it.\n\n**`install` redoes the whole snapshot; it does not edit one key.** What lands in the file is whatever that shell has at that moment, so **anything installed last time but absent from this shell disappears silently**: open a fresh terminal, run `install` with a single inline prefix, and the port does change — along with the DNS credentials (next paragraph) going missing, which only blows up at the next certificate renewal. So keep configuration either in your shell rc (every new shell carries it) or in an `--env-file` that is the single source; the list `install` prints is the only chance to notice **on the spot** that something dropped out.\n\n**`service restart` does not re-read configuration.** It only kills and restarts the process (that is what you want after replacing the binary); the snapshot inside the plist / unit is untouched. Changing configuration means running `install` again.\n\n**DNS provider credentials carry the `HERDR_WEB_` prefix too** (`HERDR_WEB_CLOUDFLARE_DNS_API_TOKEN`, `HERDR_WEB_ALICLOUD_ACCESS_KEY` and friends), so the rule above already copies them — exporting them in your shell is enough, no `--env-file` required. The prefix is not cosmetic: a bare `CLOUDFLARE_DNS_API_TOKEN` matches neither the prefix nor the allowlist, so it is not copied, and that failure only surfaces at the first issuance (or three months later, at the first renewal). lego still reads the bare names, but those can only reach the service through `--env-file`; when both are set, the prefixed one wins. Per-provider variable names are in [DNS.md](DNS.md).\n\nKeys in `--env-file` go in **wholesale** (and override the current environment). The file is read at `install` time only and never touched again. In the list `install` prints, credentials show up as asterisks and a length — that output often lands in a pane with an agent in it.\n\nThe plist / unit is **0600** — its contents are exactly that environment in plaintext.\n\n**Copying `PATH` is mandatory, and it is the most common failure after installing as a service**: launchd's default `PATH` is only `/usr/bin:/bin:/usr/sbin:/sbin`, so `HERDR_WEB_ONCONNECT=herdr` turns into `herdr: command not found` while the page just shows an empty shell with no clue why.\n\nWhy user-level rather than system-level: this process opens **your** shell. Running it as a root system service means the terminal in the browser is root's, permissions jump straight to maximum, and `~/.herdr-web` and `~/.config/herdr/herdr.sock` all point at somebody else's home.\n\nPlatform-specific traps:\n\n| | File | Note |\n|---|---|---|\n| macOS | `~/Library/LaunchAgents/io.github.zbysir.herdr-web.plist` | A LaunchAgent starts **at login**, not at boot. On a machine with automatic login the two are equivalent; otherwise you have to log in once. \"Start with nobody logged in\" would require a system-level daemon in `/Library/LaunchDaemons`, which makes the shell root's — this project does not do that. |\n| Linux | `~/.config/systemd/user/herdr-web.service` | `install` also runs `loginctl enable-linger`. **Without linger the service is stopped when you log out of ssh** — for a machine you want to reach at any time, that is the same as not running at all. If it fails it tells you to run `sudo loginctl enable-linger $USER`. |\n\nLogs are at `~/.herdr-web/logs/herdr-web.log` on both platforms (deliberately identical, so the docs and `service logs` have one answer). On Linux `journalctl --user -u herdr-web` works as well.\n\n`service status` reporting \"installed but not running\" means **it crashes on start**, and the reason is only in the log — launchd and systemd both keep retrying with a few seconds of backoff, so without looking you would assume it is running.\n\nWindows, and Linux without systemd (containers, WSL1), are told clearly that this cannot work and what to do instead, rather than being given something that will not run. On WSL2, add `[boot] systemd=true` to `/etc/wsl.conf` and `wsl --shutdown` to restart, and it works.\n\n## Updating\n\n```bash\nherdr-web update            # check and upgrade\nherdr-web update --check    # check only, change nothing\nherdr-web update --restart  # upgrade, then restart the service\nherdr-web version           # current version + how it was installed\n```\n\n**How it upgrades depends on how it was installed**, and `update` works that out itself (from the executable's path, resolving symlinks first):\n\n| Installed via | Upgrade action |\n|---|---|\n| npm | runs `npm install -g @bysir/herdr-web@latest` |\n| homebrew | runs `brew upgrade herdr-web` |\n| `go install` | runs `go install …@latest` |\n| release archive / install.sh | **does it itself**: download → verify sha256 → write a temp file in the same directory → atomic `rename` |\n\nPackage-manager installs are not touched directly because editing things inside `node_modules` / `Cellar` gets overwritten the next time that package manager runs — wasted effort.\n\nThree things about the self-managed path are deliberate: **verify before landing** (a `checksums.txt` mismatch aborts everything), **the temp file must be in the same directory** (a cross-directory `rename` gives EXDEV), and **the old file is not deleted** (on unix, renaming over a running executable is allowed, the old inode is still held by the process, so the current process runs safely until it exits).\n\n**Replacing the file is not the same as replacing the running process.** Only a restart takes effect, and a restart kills every terminal session in use — so it is not done by default, only with `--restart`.\n\nNew-version notices appear in three places:\n\n- the last line of the **startup banner** (from cache, so no request is made on the startup path — on a slow network that would turn into \"startup hangs for ten seconds\");\n- a strip at the top of the **admin page**, with the current version, the command to run and a link to the release notes;\n- while the service is running, a daily background check writes one line to the **log** when a new version appears (once per version, not daily nagging).\n\nChecks go to GitHub Releases' anonymous API, with results cached in `~/.herdr-web/update.json` (on disk, so frequent restarts do not mean checking every time; failures are stamped too, so a machine with no connectivity does not eat a timeout on every start). `HERDR_WEB_UPDATE_CHECK=0` disables the automatic check entirely — with it off, this process makes no outbound requests at all. Local builds (where `version` reports `dev`) neither check nor nag.\n\n## Security\n\n**This thing amounts to a shell over HTTP** (the outbox alone can make an agent run commands, even\nwithout a PTY), so the door is designed on that premise. What is implemented:\n\n- **Pair each device once.** A one-time code is exchanged for a per-device credential in an\n  `HttpOnly; SameSite=Strict` cookie; the server **stores only sha256** — the agents on this machine\n  read untrusted content all day, so the credential file being read by prompt injection is a daily\n  risk, not a theoretical one.\n- **Credentials bind to a device, not an IP.** Changing networks costs nothing; trusting an IP loses\n  both ways.\n- **No secrets in URLs.** `?pair=` is exchanged for a cookie and scrubbed with a 302, so bookmark\n  sync and screenshots stop being leak channels.\n- **Revocable.** `herdr-web revoke`, or Settings → Devices; the next request gets 401.\n- **Only someone at the machine can produce a pairing code** — no path on the web issues one. That\n  terminal is the only out-of-band factor in the system.\n- **Refuses to start when exposed without TLS.** A Host allowlist blocks DNS rebinding; Origin +\n  `SameSite=Strict` + a custom header make three layers against CSRF; guessing a pairing code gets\n  exponential backoff, per-IP lockout and a global breaker.\n- **Passkeys are the second factor** (the server stores only the public key). With one registered,\n  moving to a new device does not require going back to the machine, and session credential lifetime\n  can drop from three months to one day.\n\n→ Threat model, the reasoning behind each choice, what is not built yet: [SECURITY.md](docs/dev/SECURITY.md)\n　Reaching it from the internet (frp / tunnels) and the four TLS tiers: [DEPLOY.md](DEPLOY.md)\n\n## Documents\n\nEverything below is in Chinese — that is where the \"why\" lives.\n\n**This file, plus DEPLOY / DNS, is the user documentation.** The first five below live in [`docs/dev/`](docs/dev/README.md) — that layer is *why it is built this way*: design rationale, hand-verified semantics, and the traps that fail silently.\n\n| What you want | Where |\n|---|---|\n| Outbox: why a separate box, how images work, measured polling latency | [OUTBOX.md](docs/dev/OUTBOX.md) |\n| Reading the screen: scraping the input line, scraping what the agent said | [COMPOSER.md](docs/dev/COMPOSER.md) |\n| herdr socket API semantics, verified by hand | [HERDR-API.md](docs/dev/HERDR-API.md) |\n| The whole phone / tablet layer (gestures, keyboard, dock, top bar, notices, clipboard) | [MOBILE.md](docs/dev/MOBILE.md) |\n| Security design and threat model; the rules on the file-serving route | [SECURITY.md](docs/dev/SECURITY.md) |\n| Where to run it, public access, TLS tiers | [DEPLOY.md](DEPLOY.md) |\n| Getting a DNS token from each provider and the scope it needs | [DNS.md](DNS.md) |\n| Read before changing code (layout, releasing, colours, the silent traps) | [CLAUDE.md](CLAUDE.md) |\n\nMIT.\n","readmeFilename":"README.md"}