@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-clientUsage
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 ofARGUS_REGISTRY_PATH/ default.ttlMs: staleness threshold for locally hiding expired watchers (defaultDEFAULT_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
/statusfor each watcher. - Failed probes return
reachable: falseanderror; connection refusal conditionally removes the same failed registration, while timeouts keep it. byCwdfilters watchers bycwdsubstring (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 withargsbounded viapreviewValue.mode: 'full'returns raw events from the watcher.levelsaccepts comma-separated string or array; maps to watcher/logsquery.matchaccepts regex patterns (string or array); multiple patterns use OR semantics.matchCasecontrols regex case-sensitivity (insensitiveby default server-side).sourcefilters byLogEvent.sourcesubstring.sinceaccepts 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 holdsevalis unchanged: page exceptions are reported inexception, never thrown.evalValuereturns the value directly and throwsArgusEvalErroron a page-side exception, preferring the page's error description over CDP's bare"Uncaught".evalUntiloptions: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 pruneperforms 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.
