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

@znt/sdk-nodejs

v1.0.1

Published

Node.js-only client SDK for the znt-core JSON-RPC IPC API

Readme

znt-sdk-nodejs

Dependency-free, Node.js-only client SDK for the znt-core JSON-RPC 2.0 IPC API. The SDK uses one reusable Unix socket or Windows named-pipe connection, supports concurrent requests, preserves structured RPC errors, and includes TypeScript declarations.

Requirements: Node.js 18 or newer. The SDK owns the znt-core daemon lifecycle: it reuses a running daemon or starts the installed binary when requested.

This package runs only in a trusted Node.js environment. It imports Node's node:net module and cannot run directly in browsers, Web Workers, or Vue/React client bundles. For a web application, use it in a local Node.js backend or an Electron main/preload process and expose only the required operations to the frontend.

Vue/browser -> local Node.js host -> znt-sdk-nodejs -> znt-core

| Runtime | Supported | |---|---| | Node.js 18+ backend or local host | Yes | | Electron main/preload process | Yes | | Browser or Vue/React renderer | No | | Web Worker | No | | Deno or Bun | Not officially supported |

Install

For local development in this repository:

cd znt-sdk-nodejs
npm install
npm run build

From another local package:

npm install ../znt-sdk-nodejs

Quick start

Start or reuse the daemon from Node.js:

import { ZntClient } from "znt-sdk-nodejs";

const znt = new ZntClient();

try {
  await znt.startCore();

  const info = await znt.info();
  console.log(info.languages);

  const status = await znt.status();
  console.log(status);

  const results = await znt.semanticSearch({
    query: "SemanticService.SearchWithOptions",
    mode: "lexical",
    limit: 10,
    compact: true,
  });
  console.log(results);
} finally {
  znt.disconnect();
}

semanticSearch() normalizes the server's valid null no-results response to an empty array.

startCore() is concurrency-safe per client instance. If no daemon answers at the configured endpoint, it checks the managed installation and starts znt-core daemon. It never downloads a binary automatically.

Managed core installation

The SDK checks a per-user installation directory, so application startup does not require administrator privileges:

| Platform | Default directory | |---|---| | Linux | $XDG_DATA_HOME/znt/bin, or ~/.local/share/znt/bin | | macOS | ~/Library/Application Support/Znt/bin | | Windows | %LOCALAPPDATA%\\Znt\\bin |

The executable is named znt-core on Linux/macOS and znt-core.exe on Windows. ZNT_CORE_HOME overrides the managed directory; ZNT_CORE_BINARY overrides the complete executable path for development.

Daemon options can also be provided directly to ZntClient:

const znt = new ZntClient({
  configPath: "/absolute/path/to/config.yaml",
  daemonStartTimeoutMs: 10_000,
});

Downloading is always a separate, explicit operation:

await znt.downloadCore(); // Latest release for the current OS and CPU architecture.

The SDK reads the release manifest from znt-app/core, selects the binary for the current OS and CPU architecture, verifies its SHA-256 checksum, makes it executable on Unix, and moves it into the managed installation path. Downloading the binary does not create or overwrite config.yaml; the MCP setup flow creates the working YAML next to the managed binary. To install a specific release, pass { version: "1.0" }. The release source is fixed to the official znt-app/core GitHub repository and cannot be overridden by a URL or environment variable. Call startCore() separately after installation.

The complete lifecycle is explicit:

if (!(await znt.isCoreInstalled("1.0"))) {
  await znt.downloadCore({ version: "1.0" });
}

const defaults = await znt.getDefaultConfig(); // YAML from znt-core.
// A setup UI writes the selected YAML to znt.resolveManagedConfigPath().
await znt.validateConfig(znt.resolveManagedConfigPath(), { checkProvider: true });
await znt.startCore();
// Use the RPC API.
await znt.stopCore();
await znt.removeCore(); // Optional uninstall; does not stop a running daemon.

isCoreInstalled() checks the executable independently from configuration. isCoreConfigured() checks ZNT_CONFIG or the adjacent managed config.yaml. When an expected version is supplied, isCoreInstalled() also compares the version stored by downloadCore() in the adjacent znt-core.metadata.json file. A manually copied executable without version metadata does not satisfy a versioned check. removeCore() removes both the executable and its metadata and returns whether an executable was present. Stop the daemon before removing it.

Configuration and setup

The SDK provides methods to inspect and configure znt-core managed configuration, store API keys securely in the OS keyring via znt-core config secret set, and validate the provider setup:

// Inspect configuration status
const status = await znt.getSetupStatus();
console.log(status.status); // "ready" | "unconfigured"
console.log(status.config_managed); // true if managed by SDK, false if explicit external path
console.log(status.model); // model name or "not_required (fast mode)"
console.log(status.recommendations);

// Configure managed config with selected provider and options
const result = await znt.setupManagedConfig({
  mode: "openapi", // "openapi" | "ollama" | "bm25"
  provider_url: "https://openrouter.ai/api/v1",
  api_key: "sk-or-...",
  model: "qwen/qwen-2.5-coder-32b-instruct",
  embed_model: "bge-m3",
  semantic_mode: "llm", // "llm" | "fast"
  description_language: "ru",
  exclude: ["**/dist/**"],
  languages: {
    go: { exclude: ["**/*_test.go"] },
  },
  check_provider: true,
});

console.log(result.status); // "ready"

The setup helper:

  1. Loads default YAML template from znt-core config defaults or merges with existing configuration.
  2. Applies optimized concurrency, throttling, retry, and payload bounds (retry_delays_seconds: [60, 120, 300], max_bytes: 25000, max_embed_bytes: 63768, semantic_workers: 6, delay_ms: 10).
  3. Writes the configuration atomically to config.yaml and validates it with znt-core config validate.
  4. Saves API keys directly to the OS keyring using znt-core config secret set over stdin (no plain text secret in the YAML file).
  5. Validates provider connectivity when check_provider: true.
  6. When semantic_mode: "fast", chat LLM generation is not required (AST heuristics are used); status.model explicitly reports "not_required (fast mode)" if no chat model is configured.

Endpoint selection

const znt = new ZntClient({
  endpoint: "/custom/path/znt.sock",
  connectTimeoutMs: 5_000,
  requestTimeoutMs: 30_000,
});

Resolution order when endpoint is omitted:

  1. ZNT_ENDPOINT;
  2. ~/.znt/znt.sock on Linux/macOS;
  3. \\.\pipe\znt-core on Windows.

ZNT_RUNTIME_DIR changes znt-core process files such as znt.log, but does not change the default endpoint. Set ZNT_ENDPOINT on both processes when a custom endpoint is required.

Scanning

scan() returns immediately with a scan_id:

const started = await znt.scan({
  file_path: "/absolute/path/to/workspace",
  language: "auto",
});
console.log(started.scan_id);

Set restart: true to remove only the workspace .znt index and rebuild it from scratch:

await znt.scanAndWait({
  file_path: "/absolute/path/to/workspace",
  language: "auto",
  restart: true,
});

info().config contains the absolute path of the configuration file loaded by the active daemon. Pass configPath to ZntClient or set ZNT_CONFIG before calling startCore(). The SDK forwards the path when it launches core but does not own or modify the configuration file.

Use scanAndWait() when the caller must wait for semantic indexing and HNSW:

const completed = await znt.scanAndWait(
  { file_path: "/absolute/path/to/workspace", language: "auto" },
  { timeoutMs: 10 * 60_000, pollIntervalMs: 250 },
);
console.log(completed.phase); // completed

scan_status describes the daemon's current or most recent global scan. The SDK verifies that its scan_id remains current while scanAndWait() polls.

API

| SDK method | JSON-RPC method | |---|---| | startCore() | Reuse or start the installed znt-core process | | stopCore() | Request daemon shutdown and disconnect the SDK client | | downloadCore(options) | Explicitly download and install a core artifact | | isCoreInstalled(expectedVersion?) | Check executable presence and optionally its installed version | | isCoreConfigured() | Check whether the selected YAML configuration exists | | resolveManagedConfigPath() | Return the adjacent managed config.yaml path | | getDefaultConfig() | Return canonical default YAML emitted by core | | validateConfig(path, options) | Validate YAML and optionally check the provider | | storeSecret(reference, secret) | Store a credential through core stdin without process arguments | | removeCore() | Remove the installed executable and version metadata | | info() | info | | status() | status | | logs() | logs | | watchStatus(options) | Repeated status polling | | watchScanStatus(options) | Repeated scan_status polling | | watchLogs(options) | Repeated logs polling | | scan(params) | scan | | scanStatus() | scan_status | | semanticSearch(params) | znatok_semantic_search | | findSimilar(params) | znatok_find_similar | | getSubgraph(params) | znatok_get_subgraph | | fileOutline(params) | znatok_file_outline | | shutdown() | shutdown | | call(method, params) | Any current or future JSON-RPC method |

Parameter names intentionally match the wire specification (file_path, include_code, edge_types, and so on), while SDK method names follow normal JavaScript camelCase conventions.

Logs

const snapshot = await znt.logs();
for (const entry of snapshot.entries) {
  console.log(`${entry.time} [${entry.level}] ${entry.message}`);
}

const delta = await znt.logs({
  stream_id: snapshot.stream_id,
  after_id: snapshot.next_cursor,
});

The first logs() call returns the current in-memory snapshot. Subsequent calls must pass both stream_id and after_id and return only newer entries. A changed stream_id or an expired cursor returns the complete retained buffer with truncated: true. The buffer contains up to 100 entries and is reset when the daemon restarts; the persistent znt.log file is not read by this method.

Polling streams

The SDK can expose status, scan progress, and log snapshots as async iterables. These helpers use repeated JSON-RPC calls over the existing persistent IPC connection; they do not require WebSocket support in znt-core.

const controller = new AbortController();

for await (const status of znt.watchStatus({
  intervalMs: 1_000,
  signal: controller.signal,
})) {
  console.log(status.status, status.files);
}

The same pattern is available through watchScanStatus() and watchLogs():

for await (const batch of znt.watchLogs({ intervalMs: 500 })) {
  if (batch.truncated) {
    replaceActivityLog(batch.entries);
  } else {
    appendActivityLog(batch.entries);
  }
}

watchLogs() manages stream_id and after_id automatically. Its first value is the current snapshot; later values contain only new entries. With the default distinct: true, empty unchanged polling responses are not yielded. Polling options are:

  • intervalMs: delay between requests; defaults to 1 second for status/logs and 250 ms for scan progress;
  • emitInitial: when true (default), request the first snapshot immediately;
  • distinct: when true (default), skip snapshots identical to the last one;
  • timeoutMs: timeout of each individual JSON-RPC request;
  • signal: stops polling with ZntAbortError when aborted.

Leaving a for await loop stops that iterator cleanly. For prompt external cancellation while it is sleeping or waiting for a response, use an AbortController. A transport, protocol, RPC, or request-timeout error ends the iterator and is propagated to the consumer.

Each watchLogs() value retains the server metadata (stream_id, next_cursor, and truncated) so consumers can replace their local state when continuity was lost.

Similar implementations

const similar = await znt.findSimilar({
  target: "pkg/semantic/service.go::SemanticService.SearchWithOptions",
  edge_types: "contains,call",
  include_code: true,
});

Subgraph

const graph = await znt.getSubgraph({
  from: "SemanticService.SearchWithOptions",
  depth: 2,
  edge_types: "call,contains",
  format: "mermaid",
});

if (graph.format === "mermaid") {
  console.log(graph.mermaid);
}

The TypeScript result is a discriminated union: text has only text, json has nodes and edges, and mermaid has only mermaid.

File outline

const outline = await znt.fileOutline({
  file_path: "internal/engine/watcher.go",
  include_code: true,
});
console.log(outline.formatted_text);

Errors, timeout, and cancellation

import { ZntRpcError, ZntTimeoutError } from "znt-sdk-nodejs";

try {
  await znt.semanticSearch({ query: "Search", mode: "bogus" });
} catch (error) {
  if (error instanceof ZntRpcError) {
    console.error(error.code, error.message, error.data);
    if (error.retryable) {
      // Only -32001 Busy is marked retryable.
    }
  } else if (error instanceof ZntTimeoutError) {
    console.error(error.timeoutMs);
  }
}

Every request accepts timeoutMs and AbortSignal:

const controller = new AbortController();
const request = znt.status({ timeoutMs: 2_000, signal: controller.signal });
controller.abort();
await request;

Error classes:

  • ZntRpcError: server error with code, data, method, and requestId;
  • ZntTransportError: connection or socket failure;
  • ZntProtocolError: malformed response or oversized request;
  • ZntTimeoutError: client-side request or scan timeout;
  • ZntAbortError: cancellation through AbortSignal;
  • ZntScanError: terminal scan failure with the complete progress DTO.

Low-level connection

ZntConnection is exported for clients that only need raw typed calls:

import { ZntConnection } from "znt-sdk-nodejs";

const connection = new ZntConnection();
const info = await connection.call("info", {});
connection.disconnect();

Tests

npm test
npm run test:live
npm pack --dry-run

test:live uses the endpoint selected by ZNT_ENDPOINT or the platform default and never calls scan or shutdown.

The authoritative wire contract is ../znt-core/docs/sdk-specification.md.