npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vforsh/argus-client

v0.5.7

Published

Node-only client for the Argus watcher HTTP API: logs, network, eval, page interaction, and capture.

Readme

@vforsh/argus-client

Node-only client for the Argus watcher HTTP API: logs, network, eval, page interaction, and capture.

Built for programmatic drivers such as verification runners. The watcher is already a persistent HTTP server, so this talks to it directly — no process spawn per call, no stdout parsing.

Install

npm install @vforsh/argus-client

Usage

import { createArgusClient } from '@vforsh/argus-client'

const client = createArgusClient()

const list = await client.list()
const logs = await client.logs('app', { mode: 'preview', since: '10m', levels: ['error'] })

const baseline = await client.beginLogEpoch('app')
// perform the action under test
const actionErrors = await client.logs('app', { sinceEpoch: baseline.epoch, levels: ['error', 'exception'] })

Bind the watcher id once and drop it from every call site:

const page = createArgusClient().watcher('app')

await page.reload({ ignoreCache: true })
await page.evalUntil('window.app?.ready')

const rows = await page.evalValue<number>('document.querySelectorAll("tr").length')
await page.domClick({ selector: '[data-testid="submit"]' })
await page.record({ durationMs: 5_000, selector: 'canvas', outFile: 'demo.mp4' })

API

createArgusClient(options?)

type ArgusClientOptions = {
	registryPath?: string
	ttlMs?: number
	timeoutMs?: number
}
  • registryPath: override registry path instead of ARGUS_REGISTRY_PATH / default.
  • ttlMs: staleness threshold for locally hiding expired watchers (default DEFAULT_TTL_MS).
  • timeoutMs: default HTTP timeout; applied as 2s for status and 5s for logs.

client.list(options?)

type ListOptions = {
	byCwd?: string
}

type ListResult = {
	watcher: WatcherRecord
	reachable: boolean
	status?: StatusResponse
	error?: string
}
  • Refreshes the client’s lock-free registry snapshot, hides expired entries locally, then pings /status for each watcher.
  • Failed probes return reachable: false and error; connection refusal conditionally removes the same failed registration, while timeouts keep it.
  • byCwd filters watchers by cwd substring (empty/whitespace treated as unset).

client.logs(watcherId, options?)

type LogsOptions = {
	mode?: 'preview' | 'full'
	levels?: string | LogLevel[]
	match?: string | string[]
	matchCase?: 'sensitive' | 'insensitive'
	source?: string
	after?: LogEpoch
	sinceEpoch?: LogEpoch
	limit?: number
	since?: string | number
}

type LogsResult = {
	events: LogEvent[]
	nextCursor: LogEpoch
}
  • mode: 'preview' (default) returns events with args bounded via previewValue.
  • mode: 'full' returns raw events from the watcher.
  • levels accepts comma-separated string or array; maps to watcher /logs query.
  • match accepts regex patterns (string or array); multiple patterns use OR semantics.
  • matchCase controls regex case-sensitivity (insensitive by default server-side).
  • source filters by LogEvent.source substring.
  • since accepts a duration string (e.g. "10m", "2h", "30s") or a duration in ms.

client.beginLogEpoch(watcherId)

Returns { epoch } from /logs/epoch without downloading buffered events. Pass it as sinceEpoch (or after) for a deterministic action-scoped log read. The marker is tied to the live watcher session, not the page, so browser reloads do not reset it.

client.logCursor(watcherId) is the same opaque marker under the /logs/cursor name.

Eval

client.eval(watcherId, options) // raw envelope: { result, type, exception }
client.evalValue<T>(watcherId, expression, options?) // the value itself; throws on page exceptions
client.evalUntil(watcherId, expression, options?) // poll until a predicate holds
  • eval is unchanged: page exceptions are reported in exception, never thrown.
  • evalValue returns the value directly and throws ArgusEvalError on a page-side exception, preferring the page's error description over CDP's bare "Uncaught".
  • evalUntil options: intervalMs (250), totalTimeoutMs (30000), count, predicate, signal. Rejects on timeout, exhaustion, abort, or a page exception. Resolves { value, iteration, elapsedMs }.

jsonValue (on by default for evalValue/evalUntil). Transports disagree about raw returnByValue results: the extension relay — Chrome's chrome.debugger serialization — sorts object keys alphabetically at every nesting level, while a direct CDP watcher preserves insertion order. Values are identical, but structural comparisons of the same page state produce different bytes per transport, which silently breaks snapshot assertions. jsonValue has the page serialize the result so the JSON string crosses the transport opaquely, normalizing both to insertion order and making Date round-trip as an ISO string instead of {}.

Evaluation semantics are untouched — statement lists, top-level await, and REPL redeclaration behave exactly as without it. Pass { jsonValue: false } for the raw transport-native value.

A watcher started before this flag existed ignores it; the client detects that and raises an actionable error rather than silently returning transport-dependent key order. Restart the watcher to pick up the current build.

Navigation

client.navigate(watcherId, { url?, param?, params?, wait?, timeoutMs? })
client.back(watcherId, { steps?, wait?, timeoutMs? })
client.forward(watcherId, { steps?, wait?, timeoutMs? })
client.url(watcherId) // -> { url, title, attached }

url is resolved by the watcher, so relative forms work against the page's current URL: /settings, ?tab=2, #top. A scheme-less localhost:3000 gets http://. Omit url and pass param/params to rewrite the query string in place.

wait is 'load' (default), 'domcontentloaded', or 'none'; timeoutMs (default 30000) bounds the wait only. A navigate/back/forward result carries epoch — the log epoch opened just before the navigation — so client.logs(watcherId, { sinceEpoch: epoch }) reads only the new page's output without racing it.

Failures throw: navigation_failed (Chrome refused the URL), navigation_timeout (the load did not finish in time; the page may still be loading), no_history (already at the first/last entry).

Page interaction

client.domClick(watcherId, { selector | ref | x, y, all?, button?, text?, wait?, waitNav?, navTimeoutMs? })
client.visibility(watcherId, { action: 'show' | 'hide', policy?, activate? })
client.visibilityStatus(watcherId) // -> { attached, state, policy }
client.reload(watcherId, { ignoreCache? })
client.netClear(watcherId)

visibility locks the page shown+focused so backgrounded windows do not throttle rAF/timers. The lock and policy are sticky across detach/reattach. foreground may activate the tab/window when applying the lock; background keeps focus emulation enabled without activating Chrome, even when activate: true is supplied. Omit policy to preserve the current policy. activate: false suppresses a one-shot foreground activation, which is useful while restoring a saved state.

visibilityStatus is read-only: it does not activate the tab, change focus emulation, or mutate the lock. It reports the desired state while the watcher is detached, before that state is re-applied on reattach. State lasts for the watcher process: explicit extension detach closes its native host, and the next attachment starts a new watcher with default/foreground. Reacquire background mode for that new watcher.

Writes with policy or activate: false probe watcher support before mutation and reject older watchers with restart guidance. Legacy action-only calls retain foreground semantics on older watchers. Stop active recordings before entering background mode.

Save and restore visibility state in a single-owner automation session:

const saved = await page.visibilityStatus()
try {
	await page.visibility({ action: 'show', policy: 'background' })
	// run the automation
} finally {
	await page.visibility({ action: saved.state === 'shown' ? 'show' : 'hide', policy: saved.policy, activate: false })
}

The status read and restore write are separate operations, so this snapshot pattern is not atomic and does not provide ownership against overlapping callers.

waitNav waits for a top-frame navigation caused by the click and reports it as navigation: { navigated, url, epoch }. Clicking something that does not navigate is not an error — it resolves with navigated: false once navTimeoutMs (default 10000) elapses.

Capture

client.screenshot(watcherId, { outFile?, selector?, clip?, format? })
client.record(watcherId, { durationMs, outFile?, selector?, clip?, fps?, format? })
client.recordStart(watcherId, options?) // -> { recordId, ... }
client.recordStop(watcherId, { recordId?, outFile? })
client.traceStart(watcherId, options?)
client.traceStop(watcherId, options?)

All write to disk on the watcher host. selector and clip are mutually exclusive. Recording is silent video, mp4 or webm, 1–60 fps.

client.watcher(watcherId)

Returns the same API with watcherId pre-bound, minus list.

Errors

  • Throws on invalid inputs (since, after, limit) and on missing/conflicting options, before any request.
  • Throws if the watcher id is not in the registry.
  • Connection refusal removes only the same owner/start/endpoint that failed. Timeouts keep the entry; a lost response does not prove that a mutation was not executed. Failed requests are never replayed.
  • If the watcher answers with an error status, the registry entry is kept: the watcher is demonstrably alive, and evicting it would break every later call in the process over one bad selector or a page mid-navigation.
  • A cursor from another watcher session, a restarted watcher, or an evicted ring-buffer range is rejected; capture a new epoch.

Notes

  • Discovery caches a live snapshot per client for at most 250ms, capped by heartbeat TTL. Transport failures invalidate it; the next call can use a replacement endpoint. Ordinary reads never lock or write the registry. argus watcher prune performs physical cleanup.
  • This package is Node-only and uses the Argus registry on disk.

Log ingestion assigns IDs at arrival. Default reads return final enriched records in that order. client.logs(id, { raw: true }) returns immutable arrival-time previews/generated locations immediately; use one view consistently with a cursor. The watcher bounds enrichment and finalizes generated fallbacks on timeout, overload or navigation; see log guarantees.