@byokit/discover

中文文档
0.8.7发布于 2026/10/25Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 3814

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

原始使用文档
文档目录16 节
  1. Install
  2. iOS setup (Expo)
  3. Quickstart
  4. API at a glance
  5. Routes
  6. Recommend
  7. Multiple direct listeners
  8. Phone route evidence (React Native and Node)
  9. Reachability and firewall
  10. Tailscale rules
  11. mDNS
  12. Advertise (Node)
  13. Browse (React Native)
  14. Tests
  15. Links
  16. License

@byokit/discover

npm CI Apache 2.0 Node | React Native

The addresses a phone can dial the home computer on.
For @byokit/pair's offer({ urls }): a private tunnel served to the person's tailnet, a direct encrypted link, another private overlay network or the LAN. On Node it can also advertise the computer over mDNS; on a phone (React Native) it browses the mDNS services around it.

Formerly @byokit/reach, which stays published as a deprecated shim re-exporting this package through 0.7.x and is removed in 0.8.0.

Install

npm install @byokit/discover

npm · Latest release · All releases

iOS setup (Expo)

iOS only returns local-network mDNS results when the app declares the Bonjour service types it browses and a usage description. Add the config plugin and list the types you pass to browse():

{
  "expo": {
    "plugins": [["@byokit/discover", { "services": ["_byokit._tcp"] }]]
  }
}

services is required and must be non-empty; an empty list fails prebuild by name instead of writing an empty Bonjour list. localNetwork overrides the usage string shown in the iOS prompt (a sensible default is used when omitted). Prebuild writes NSBonjourServices and NSLocalNetworkUsageDescription into Info.plist; rebuild the native binary after adding the plugin. Android needs no plugin: react-native-zeroconf's own manifest already merges CHANGE_WIFI_MULTICAST_STATE.

Quickstart

npm install @byokit/discover

Work out the addresses for a server on port 8792, listen where reach says, and put the addresses in the pairing offer. Persist ingress and pass it back as previous next time.

import { createServer } from 'node:http';
import { reach } from '@byokit/discover';

const { urls, bind, ingress, pendingCleanup } = await reach({ port: 8792, previous: saved.ingress });
// One listener per address reach names, never a wildcard: loopback behind Serve, the selected tailnet IP for
// direct Tailscale, the classified LAN (or overlay) addresses for those routes.
const servers = bind.map((address) => createServer(handler).listen(8792, address));
saved.ingress = ingress;
if (pendingCleanup) saved.pendingCleanup = pendingCleanup; // retry unserve() later
const { text } = host.offer({ role: 'control', urls });     // show `text` as a QR

The LAN route with a made-up interface list, so it never touches Tailscale or the network. Docker's bridge is skipped; the NetBird-style wt0 address is a private route, not LAN:

import type { NetworkInterfaceInfo } from 'node:os';
import { reach, routes } from '@byokit/discover';

const nic = (address: string): NetworkInterfaceInfo =>
  ({ address, netmask: '255.255.255.0', family: 'IPv4', mac: '02:00:00:00:00:01', internal: false, cidr: `${address}/24` });
const interfaces = { en0: [nic('192.168.1.20')], docker0: [nic('172.17.0.1')], wt0: [nic('100.90.1.2')] };

console.log(routes(interfaces));
console.log(await reach({ port: 8792, via: 'lan', interfaces }));
{
  lan: [ '192.168.1.20' ],
  private: [ { address: '100.90.1.2', interface: 'wt0' } ],
  tailscale: []
}
{ urls: [ 'ws://192.168.1.20:8792' ], bind: [ '192.168.1.20' ] }

API at a glance

ExportWhat it does
reach({ port, via?, previous?, tailscale?, interfaces?, address? })The dial urls, the bind addresses to listen on (one per listener, never a wildcard), and the Serve ingress to persist (plus pendingCleanup when a removal failed)
directRoutes({ port, listen?, interfaces?, tailnetIPs?, hosts?, path? })Multiple direct listener hosts and pairing urls, with explicit loopback/tailnet/LAN scopes
phoneNetwork({ nativeModule? })Phone Wi-Fi/cellular transports and explicit VPN state (yes, no, unknown)
nativeAddresses({ nativeModule? })IPv4 interface evidence including actual prefix lengths; Node reads its interfaces by default
routeOf(url)Address hint: home, tailscale, relay, loopback or unknown
observe({ urls, priorEvidence?, nativeModule?, addresses?, probe? })Current prefix evidence and a bounded probe, retaining the host’s prior evidence
probe(url, { timeout?, fetch? })Typed answers, refused, timeout or unknown observation
routes(interfaces?, tailnetIPs?)This computer's IPv4 routes: lan addresses, private overlay addresses and tailscale addresses
recommend({ port?, interfaces?, tailscale?, state?, serve?, lan?, private?, current? })Every route in everyday words, recommended first: { via, recommended, sentence, needs, disabledReason? } per route
advertise({ type, port, name?, txt?, addresses? })Publishes _<type>._tcp over mDNS (Node), using only selected addresses; returns { stop }
tailscaleState({ bin?, timeoutMs? })Non-throwing installation, backend, sign-in, key expiry, peer and address diagnostics
firewall({ port, platform?, run?, timeoutMs? })Whether the local firewall blocks inbound on a port: { blocked, how, nextStep }, read without administrator access
needsSignin(status), isPeer(status, ip)Pure helpers for raw tailscale status --json: explicit login state and peer IP membership
tailscaleStatus, tailscaleName, magicDnsNameRead the local Tailscale state and this machine's MagicDNS name
inspectServe, serveRootProxyCheck who holds the Serve root: free, ours, occupied, funnel, disabled or inconclusive
serve, unserveMake or remove the app's Serve mapping, with ownership checks
SERVE_OWNED_ERROR, FUNNEL_ERRORThe error messages for a taken root and a root with Funnel on
browse({ type }), scan({ type, ms })React Native entry only: discover mDNS services, streamed or time-boxed
TypesVia, Reach, RecommendVia, RecommendEntry, RecommendOptions, PrivateRoute, ServeIngress, ServeRoot, TailscaleOptions, TailscaleState, TailscalePeer, Bonjour, BonjourRecord; on React Native BrowseService, BrowseHandle, BrowseOptions, BrowseEvents, BrowseEventName, ZeroconfLike

Routes

via picks the route:

viaAddressServer binds
auto (default)Tailscale Serve if Tailscale is installed, otherwise LANas below
tailscalewss://<MagicDNS name> through Tailscale Serve127.0.0.1
tailscale-directws://<tailnet IP>:<port>the selected tailnet IP
privateanother overlay network (NetBird, WireGuard, ZeroTier, …)each overlay address
lanprivate IPv4 addresses on physical-looking interfaces (no known Docker, VM or VPN bridges)each LAN address

bind is a list: one address per listener, so the phone reaches the server on the address it was offered. A LAN or overlay route binds each classified address it names and never a wildcard — a wildcard would open the port on every interface of this computer, including the ones the phone was never offered.

Direct Tailscale binds the first CGNAT IPv4 address in Self.TailscaleIPs, keeping the LAN closed. Pass address with via: 'tailscale-direct' to select another IPv4 address from that same list; foreign addresses and wildcard binds are refused before removing an existing Serve mapping.

routes(interfaces, tailnetIPs) includes named Tailscale interfaces in tailscale; pass the snapshot's ips to identify Tailscale addresses on macOS utun interfaces too. Other overlays, including CGNAT addresses without Tailscale evidence, stay in private. The function runs no CLI.

IPv6-only networks: not yet.

Tailscale is transport only. Link's handshake still checks every device key.

Recommend

recommend() lists one entry per concrete route (tailscale, tailscale-direct, private, lan) with the recommended one first, so the recommendation order and the everyday copy live in one place. sentence and needs come from @byokit/ui's routeChoices(); only availability is added here. auto is not a route: it picks the recommended entry. Pass state, serve, lan and private fakes to decide without touching Tailscale or the network; with no options it probes this computer (tailscaleState, inspectServe on port, routes).

import { recommend } from '@byokit/discover';

const entries = await recommend({
  state: { installed: true, backendState: 'Running', needsSignin: false, dnsName: 'dev.tailnet.ts.net', ips: ['100.64.0.1'] },
  serve: { status: 'free' },
  lan: ['192.168.1.20'],
  private: [],
});
console.log(entries.map((e) => `${e.recommended ? '●' : '○'} ${e.via}: ${e.sentence}`));
● tailscale: Your phone reaches this computer from anywhere.
○ tailscale-direct: Same as above without Tailscale Serve. Pick this if Serve is already used on this computer for something else.
○ private: Pick this if this computer and phone are already on one.
○ lan: Easiest. Works while your phone is on the same Wi-Fi as this computer. Nothing else to install.

The order is the current healthy route (pass it as current), then Tailscale Serve — direct Tailscale when the Serve root is taken, disabled, funnelled, or nameless — then a private overlay, then Same Wi-Fi. Tailscale installed but signed out stays selectable, with sign-in in needs, while Same Wi-Fi is recommended. Nothing ready means no entry is recommended and every entry carries its disabledReason.

Multiple direct listeners

directRoutes is synchronous and runs no CLI, Serve or listener. By default it selects loopback plus classified tailnet addresses. A supplied listen enables only its true scopes. LAN binds the individual LAN addresses, so enabling LAN does not implicitly open unrelated overlays or disabled scopes. reach() follows the same rule: its bind list names one address per listener, and a LAN or overlay route never asks for a wildcard. Pass tailscaleState().ips as tailnetIPs for unnamed interfaces. Addresses must exist in the interface snapshot; CLI evidence alone does not create a listener. Refresh the snapshot and reconcile listeners when the network or pairing-window policy changes.

import type { NetworkInterfaceInfo } from 'node:os';
import { directRoutes } from '@byokit/discover';

const nic = (address: string): NetworkInterfaceInfo => ({ address, netmask: '255.255.255.0',
  family: 'IPv4', mac: '02:00:00:00:00:01', internal: false, cidr: `${address}/24` });
const umer = directRoutes({
  port: 8792, path: '/link',
  interfaces: { en0: [nic('192.168.1.20')], utun3: [nic('100.101.2.3')] },
  tailnetIPs: ['100.101.2.3'],
  listen: { loopback: true, tailnet: true, lan: true },
});
console.log(umer.hosts); // ['127.0.0.1', '100.101.2.3', '192.168.1.20']
console.log(umer.urls);  // ['ws://192.168.1.20:8792/link', 'ws://100.101.2.3:8792/link']
// Create a server for each host; give urls to link's offer only after those servers listen.

hosts overrides scope selection with explicit pinned listeners (pass an array rather than a comma-separated string). An explicitly pinned 0.0.0.0 deliberately opens every IPv4 interface; its dial URLs expand only to classified LAN/tailnet addresses. An empty hosts array returns no listeners or URLs. Dial URLs put LAN before tailnet, remove duplicates, and include loopback only when there is no remote candidate.

Phone route evidence (React Native and Node)

Use @byokit/discover/react-native explicitly or the root react-native export condition. Both expose nativeAddresses, routeOf, observe, probe and phoneNetwork with the same types as Node. These functions import no Node modules. Expo apps use the bundled ByokitReach native module on Android/iOS by default. Rebuild the native binary after installing @byokit/discover. Android reads interface prefixes and NetworkCapabilities; iOS reads interface netmasks and NWPath transports. Bare React Native apps can supply their native address module through nativeModule on each call. This supports existing modules and fakes without a global singleton. A reader implements addresses(): Promise<NativeAddress[]>, returning { address, prefixLength?, interface? } for each IPv4 interface. Obtain prefixLength from the platform’s interface prefix/netmask, never a guessed /24. A legacy string-only reader needs an adapter and cannot supply prefix evidence until its native implementation exposes it.

This example runs without native networking (use the Node root import to run it under Node):

import { nativeAddresses, routeOf, observe, probe } from '@byokit/discover/react-native';
import type { NativeAddressesModule } from '@byokit/discover/react-native';

const umer: NativeAddressesModule = {
  addresses: async () => [{ address: '192.168.1.2', prefixLength: 23, interface: 'wlan0' }],
};
const fakeFetch: typeof fetch = async () => new Response('', { status: 503 });
console.log(await nativeAddresses({ nativeModule: umer }));
console.log(routeOf('ws://100.64.0.3:8792/link')); // 'tailscale'
console.log(await probe('ws://192.168.0.20:8792/link', { timeout: 4000, fetch: fakeFetch }));
const facts = await observe({
  urls: ['ws://192.168.0.20:8792/link', 'ws://100.64.0.3:8792/link'],
  nativeModule: umer,
  priorEvidence: { anywhere: 'anywhere', peer: true, reached: { tailscale: 123 } },
  probe: { timeout: 4000, fetch: fakeFetch },
});
console.log(facts.home, facts.target, facts.knock?.state); // true, LAN URL, 'answers'

For the VPN/wrong-network banner, phoneNetwork() returns a fresh active network snapshot:

import { phoneNetwork } from '@byokit/discover/react-native';

// Expo: const network = await phoneNetwork();
// Runnable fake for Umer's phone (Node can use the root import):
const network = await phoneNetwork({ nativeModule: {
  phoneNetwork: async () => ({ onWifi: true, cellular: false, vpnActive: 'yes' }),
} });
console.log(network); // { onWifi: true, cellular: false, vpnActive: 'yes' }

Android uses the active network's TRANSPORT_WIFI, TRANSPORT_CELLULAR and TRANSPORT_VPN capabilities; multiple transport flags can be true. iOS uses NWPath for Wi-Fi/cellular and always returns vpnActive: 'unknown'. No native reader, a failed read or unavailable path yields { onWifi: false, cellular: false, vpnActive: 'unknown' }; false transport flags in that fallback do not prove a network is absent. Node returns that fallback unless a reader is injected. This snapshot never establishes that a particular peer can be reached. The package's native manifest supplies Android's normal ACCESS_NETWORK_STATE permission; no runtime permission dialog is needed. Expo autolinking discovers the bundled module; install expo-modules-core in a native app if it is not already present. JS-only environments can inject readers; all native loading is deferred until used.

observe compares the host’s home IPv4 address against the phone’s actual interface prefix. It prefers a matching home URL, otherwise a tailnet URL when the snapshot includes a CGNAT address. home is unknown (undefined) without usable prefix evidence; vpn is unknown without addresses. tailnet means a candidate URL exists; vpn means a local CGNAT address exists. CGNAT and .ts.net classification are route hints, not proof of Tailscale, a shared peer, Wi-Fi association or reachability. routeOf identifies relay URLs by /link/v1/ and returns unknown for malformed/unsupported or other public URLs. IPv6 evidence is not yet supported. priorEvidence retains what the authenticated host previously said; it is not refreshed by this observation. addresses can supply a snapshot directly; Node defaults to its own IPv4 interfaces. An unavailable/failed native module gives an empty snapshot and never invents network evidence.

probe maps WebSocket URLs to HTTP(S) and preserves their path. Every HTTP status means answers, not an authenticated link. An explicit ECONNREFUSED means refused; generic native fetch errors (including possible DNS/TLS failures) mean unknown. timeout bounds the operation even when fetch ignores abort. The default bound is 4000 ms. A probe is an active request only to the URL the caller passes (or the candidate observe selects); it never runs the Tailscale CLI, signs in or changes Serve. Inject fetch for offline tests. Apps decide their own offline wording and still authenticate with link.

Reachability and firewall

Two checks run before setup prints its tick, in this order:

  1. Can the phone reach these addresses? That is @byokit/pair's check(urls): it probes the offer's URLs with LINK_PROBE without spending the one-time ticket, so a checked offer still pairs afterwards. Setup self-checks its own advertised URLs; the phone re-checks the scanned offer before consent. This package does not re-export it — import it from @byokit/pair, where the host side that answers the probe lives.
  2. Is this computer's firewall blocking the port? That is this package's firewall({ port }) (Node only). It reads only what a normal user may read — ufw status, then nft list ruleset, then iptables -S on Linux; the application firewall state on macOS; netsh advfirewall on Windows — and never asks for administrator access. Anything unreadable is 'unknown', which is an honest answer: treat it as "check another way", never as blocked or clear.
import { firewall } from '@byokit/discover';

const verdict = await firewall({ port: 8792 });
console.log(verdict.blocked, verdict.how);  // true | false | 'unknown', plus the firewall that said so
if (verdict.blocked !== false) console.log(verdict.nextStep);  // plain words, never a sudo command

blocked: true names the refusing rule; false means the firewall is off or lets the port through. 'unknown' covers an active firewall with no rule naming the port (including macOS, which decides per app rather than per port), a state that needs administrator access to read, and platforms this helper does not check. Pass platform and run only in tests, to replay stubbed command output.

Tailscale rules

These are the rules from muxr's decision 0004.

  • Never Funnel. Setup uses tailscale serve --yes --bg --https=443 http://127.0.0.1:<port>, which is visible only inside the tailnet. A root already enabled for Funnel is refused, including when its proxy matches the recorded mapping; turn Funnel off before changing routes or DNS names.
  • The server stays on loopback behind Serve (bind: ['127.0.0.1']).
  • The machine's own Self.DNSName is used. A missing or invalid MagicDNS name is an error. A logged-out or broken Tailscale is an error too. Neither one falls back to the LAN without being asked.
  • An unrecorded root handler is refused. If / on <name>:443 already has any handler, reach throws unless previous records the matching app-created mapping. Pick tailscale-direct or remove an unrelated mapping yourself.
  • Ownership fingerprint. Persist the returned ingress ({ kind, port, dnsName, proxy }) and pass it as previous. unserve and reach({ previous }) remove only / while it still points at that proxy; sibling paths remain. On a DNS rename, removal of the recorded old root is attempted before the new one is served; a failed cleanup returns its fingerprint in pendingCleanup, except that Funnel on the old root stops the transition. After setup or removal, the root is inspected again; an occupied root after setup is reported without further changes, while failed cleanup on a route switch retains pendingCleanup. The Tailscale CLI has no compare-and-set: a change between inspection and a write can still be overwritten. Verify-after-write detects a conflicting final state but cannot eliminate that race.
  • Direct fallback and rollback. If Serve is disabled on the tailnet (the error includes the admin link), times out, or is taken, use via: 'tailscale-direct'. With previous, that also removes the mapping this package made. If cleanup cannot be verified, the requested route is still returned with pendingCleanup: previous; persist that fingerprint and retry unserve(pendingCleanup) later. auto also retains previous when Tailscale disappears and it falls back to LAN. An explicit route switch checks for addresses before removing a working Serve mapping. A successful cleanup omits pendingCleanup. If a Serve write succeeds but its status cannot be verified, the thrown error also carries error.pendingCleanup; persist it before reporting the error, then inspect or retry cleanup later.

The CLI is tailscale on PATH, then the macOS app. Pass tailscale: { bin, timeoutMs } to choose another. The lower-level steps are exported too: tailscaleStatus, tailscaleName, inspectServe, serve, unserve, and routes for the interface list.

For a connection picker, await tailscaleState() returns { installed, backendState, needsSignin, reason, dnsName, ips, keyExpiry?, Peer? } without throwing. backendState, reason and the normalized dnsName can be absent when unknown. Missing CLI, timeouts, malformed JSON and daemon failures are diagnostic results; they never set up Serve or sign in. needsSignin is true only for NeedsLogin; NeedsMachineAuth instead asks for admin approval, and Stopped does not mean logged out. The existing tailscaleStatus() still throws on CLI failure.

keyExpiry preserves a valid Self.KeyExpiry date string; the host chooses how to label an expired key. Peer is a validated map of { TailscaleIPs, DNSName?, Online? }, absent when unavailable and empty when the CLI reports an empty map. Extra fields and malformed entries are discarded.

The pure helpers take raw status JSON; isPeer also accepts the snapshot's typed Peer map. isPeer(status, ip) checks Peer[*].TailscaleIPs, accepts an IPv4-mapped socket address (::ffff:100.64.0.2), and excludes Self. Offline peers still count as peers. Peer membership is a discovery hint: link's handshake must authenticate the device.

mDNS

Advertise (Node)

advertise({ type, port, name?, txt?, addresses? }) publishes _<type>._tcp with bonjour-service and returns { stop }. Put the dial URL in txt. The kit filters bonjour-service's A/AAAA records to addresses, defaulting to routes().lan, so Docker, VPN and tailnet addresses are excluded by default. An explicit list can select interface addresses including IPv6; only addresses bonjour-service generates can be published. An empty list publishes no A/AAAA records. The selected list is snapshotted when advertising starts, and the same filter applies to goodbye records on stop. Discovery metadata (PTR, SRV and TXT) is preserved. A device that finds the wrong computer fails link's handshake, because the host key is pinned.

import { advertise } from '@byokit/discover';

const ad = await advertise({ type: 'muxr', port: 8792, txt: { url: 'ws://192.168.1.20:8792' } });
// later
await ad.stop();

Browse (React Native)

Under React Native (the package's react-native export condition), @byokit/discover also browses the LAN so an app can find services without typing an address:

import { browse, scan } from '@byokit/discover';

// Stream discovery: start, then stop when done.
const found = browse({ type: 'muxr' });                  // protocol 'tcp', domain 'local.' by default
found.on('found', (s) => console.log(s.name, s.addresses, s.port, s.txt));   // first resolve of a name
found.on('updated', (s) => console.log('refreshed', s.name));                // a known name resolved again
found.on('lost', (name) => console.log(name, 'left'));
found.on('error', (error) => console.log(error.message));
found.on('stopped', ({ reason }) => console.log(reason)); // 'preempted' if another browse starts
found.stop();                                            // idempotent

// Time-boxed: collect for N ms, then stop. Later resolves replace the earlier entry, first-seen order.
const hosts = await scan({ type: 'ssh', ms: 8000 });     // rejects on error or preemption

A BrowseService carries name, host, addresses, port (0 when the platform gave none) and txt (string values only). The pinned react-native-zeroconf dependency (0.17.4) is supplied by @byokit/discover, not the app.

  • One browse at a time. One native browser runs one browse at a time. Starting another browse or scan preempts the current handle, including when both request the same type: it receives one stopped event with { reason: 'preempted' }, loses its known services, and does not resume. A preempted scan rejects; callers needing continuous discovery must start a fresh browse after their other scan finishes.
  • Event matching. Resolved events must identify the active service type in fullName; removals apply only to names that browse has found. Native errors are ignored for one second after preemption. The native module supplies no scan ID: an untyped late removal for a name reused by the new browse, or an error after that second, can still be attributed to the new scan; a real error during the quiet second is also ignored.
  • Entries. The default Node entry does not import the native module; use the react-native export condition for browsing. Expo apps must rebuild their native binary after adding @byokit/discover.
  • Android emulator. mDNS multicast does not work on the emulator; test discovery on a real device.

Tests

test/journey-reach.test.ts drives the built package as a host app does: reach, serve/unserve, recommend, advertise, routes/directRoutes and tailscaleState against a fake tailscale CLI that logs every call. The real binary never runs, and no packet goes out: mDNS advertise uses a fake publisher, and the React Native browse API (test/browse.test.ts) uses a fake zeroconf module.

License

Apache-2.0. See LICENSE and NOTICE.

版本与发布记录

@byokit/discover 共发布 11 个版本,首次发布于 2026/10/06;过去 12 个月发布了 11 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.7latest2026/10/2514 天前
0.8.62026/10/2316 天前
0.8.52026/10/2317 天前
0.8.42026/10/1822 天前
0.8.32026/10/1722 天前
0.8.22026/10/1623 天前

关于 @byokit/discover 的常见问题

@byokit/discover 是什么?

TypeScript kits that let people sign in to your app with the AI plans they already pay for, on web, phone and desktop.

如何安装 @byokit/discover?

在项目目录运行 npm install @byokit/discover,或使用 pnpm add / yarn add / bun add。

@byokit/discover 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@byokit/discover 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@byokit/discover 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.18。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@byokit/discover 可以商用吗?

@byokit/discover 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@byokit/discover 的下载量有多少?

根据 npm 官方下载统计,@byokit/discover 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,814 次,日均约 127 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@byokit/discover 还在维护吗?

最新版本 0.8.7 发布于 2026/10/25(14 天前);至今共发布 11 个版本,其中过去 12 个月发布了 11 个;首次发布于 2026/10/06。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。