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

@adversarylabs/sdk

v0.1.16

Published

Small TypeScript SDK for authoring Adversaries.

Readme

adversary-sdk-typescript

Small TypeScript SDK for building file-based Adversaries.

The SDK owns the runtime boilerplate: read runtime input, discover the source repository path, execute registered rules, collect observations, synthesize findings, normalize and rank the review, and write runtime output.

Install

npm install @adversarylabs/sdk@latest

Requires Node 22 or newer and ESM.

Migrating from 0.1.4

SDK 0.1.4 output is incompatible with the current strict Adversary CLI schema. Version 0.1.5 is the canonical protocol release. Existing adversaries should:

  1. Upgrade the package to @adversarylabs/sdk@^0.1.5.
  2. Set findings.format to adversary.review.v1 in adversary.yaml.
  3. Stop reading result.schemaVersion; protocol selection is expressed by protocolVersion: 1 and the manifest format.
  4. If consuming serialized evidence, read file, line, endLine, message, snippet, and metadata instead of location, label, and data.
  5. If consuming review scores, read the canonical review observation with key score.<score-key>; the complete authored score is preserved in that note's metadata.

There is no compatibility adapter or dual wire format. Output is validated against the strict adversary.review.v1 schema before it is written.

Author an adversary

import { Adversary, Severity, log } from "@adversarylabs/sdk";

const app = new Adversary({
  name: "adversarylabs/comment-sentences"
});

app.defineRule({
  id: "comments.complete-sentence",
  category: "code-style",
  defaultSeverity: Severity.Info,
  groupBy: ["ruleId", "subject"],
  aggregate(observations) {
    return {
      title:
        observations.length === 1
          ? "Comment is a complete sentence"
          : "Comments contain complete sentences",
      confidence: "high",
      summary: `${observations.length} comments are written as complete sentences.`,
      recommendation:
        "Keep complete-sentence comments only when they explain non-obvious intent."
    };
  }
});

app.rule("comments.complete-sentence", async (ctx) => {
  log.debug(`scanning ${ctx.repoPath}`);

  ctx.observe({
    ruleId: "comments.complete-sentence",
    subject: "src/index.ts",
    confidence: "high",
    title: "Comment is a complete sentence",
    location: {
      file: "src/index.ts",
      line: 2
    },
    evidence: "This comment is a complete sentence.",
    recommendation: "Use complete-sentence comments intentionally where they clarify non-obvious code."
  });
});

await app.runFromEnvironment();

API

new Adversary(options)

Creates an adversary app.

const app = new Adversary({
  name: "adversarylabs/example"
});

app.rule(ruleId, handler)

Registers a rule. Rules report through ctx.observe(...), ctx.finding(...), and ctx.review.*.

Rule context exposes:

  • ctx.repoPath
  • ctx.change
  • ctx.summary
  • ctx.cache
  • ctx.relpath(path)
  • ctx.glob(pattern)
  • ctx.rglob(pattern)
  • ctx.model.review(request)
  • ctx.observe(observation)
  • ctx.finding(finding)
  • ctx.review.assessment(assessment)
  • ctx.review.positive(note)
  • ctx.review.observe(note)
  • ctx.review.opinion(opinion)

ctx.model.review(request)

Model-backed adversaries provide their review prompt, bounded structured input, response schema, and output budget. The SDK sends that request to the execution-scoped broker created by the Adversary CLI and validates the structured answer before returning it:

const review = await ctx.model.review<{
  decision: "approve" | "request_changes";
  observations: Array<{ summary: string; evidenceIds: string[] }>;
}>({
  prompt: "Review the engineering quality of this change.",
  input: {
    change: ctx.change,
    evidence: preparedEvidence,
  },
  schema: {
    type: "object",
    additionalProperties: false,
    required: ["decision", "observations"],
    properties: {
      decision: { enum: ["approve", "request_changes"] },
      observations: {
        type: "array",
        items: {
          type: "object",
          additionalProperties: false,
          required: ["summary", "evidenceIds"],
          properties: {
            summary: { type: "string" },
            evidenceIds: { type: "array", items: { type: "string" } },
          },
        },
      },
    },
  },
  budget: {
    maximumOutputTokens: 8_000,
    timeoutMs: 120_000,
  },
});

For repository-scale reviews, add bounded repository tools instead of placing source bodies in input. The SDK asks the model which directories and line ranges it needs, performs those reads inside the rule context's repository boundary, and then makes the final structured review call:

const review = await ctx.model.review<EngineeringReview>({
  prompt: ENGINEERING_REVIEW_PROMPT,
  input: {
    change: ctx.change,
    repositoryPurpose: "Go command-line adversary",
  },
  schema: engineeringReviewSchema,
  tools: {
    repository: {
      include: ["*.ts", "**/*.ts", "*.json"],
      exclude: ["fixtures/**", "generated/**"],
      maxRounds: 6,
      maxToolCalls: 24,
      maxTotalBytes: 256_000,
      maxBytesPerRead: 32_000,
      maxLinesPerRead: 400,
    },
  },
  budget: {
    maximumOutputTokens: 6_000,
    timeoutMs: 300_000,
  },
});

Repository retrieval provides paginated list_directory and line-range read_file operations. Reads are repository-relative, read-only, symlink-safe, glob-constrained, and bounded by call, round, line, and byte budgets. Each successful read creates an immutable citation:

review.citations?.[0];
// {
//   citationId: "repo:read:1",
//   path: "src/index.ts",
//   startLine: 20,
//   endLine: 80,
//   content: "..."
// }

The final model input contains the original value under reviewInput and the prepared tool results under repository.toolResults. Final output schemas should cite the supplied citationId and a line inside its inclusive range. The SDK never exposes arbitrary shell tools, follows repository symlinks, or sends provider credentials into the adversary process.

Declare permissions.model: true in adversary.yaml. The adversary process receives only a short-lived authenticated loopback broker endpoint. Provider credentials, provider selection, network transport, retries, and provider-specific response handling remain owned by the CLI.

app.run({ model }) accepts an injected ReviewModel for deterministic unit tests. Calling ctx.model.review(...) without an injected model or CLI broker throws ModelUnavailableError; the SDK never silently substitutes deterministic heuristics for model judgment.

ctx.change

The requested review scope, normalized from the runtime input's change block. Honor it instead of inferring scope from Git state: the runner has already resolved what should be reviewed.

  • null — review the entire target; the runner provided no change context (for example --all-files with no refs, or a non-Git target).
  • scanMode: "all" — review the entire target; change metadata is still available.
  • scanMode: "changed" — review only the change from baseRef to headRef.

changedFiles lists the repository-relative paths the runner identified as changed. worktree is true when the reviewed head is the uncommitted worktree (headRef is the WORKTREE sentinel, exported as WORKTREE_HEAD_REF).

app.rule("scoped", async (ctx) => {
  if (ctx.change === null || ctx.change.scanMode === "all") {
    // Review every file in ctx.repoPath.
  } else {
    // Review only ctx.change.changedFiles against ctx.change.baseRef.
  }
});

Opinion framing (formatOpinion / formatOpinionAsync)

Do not hardcode "before merging" (or similar decision language) in domain adversaries. The runner already resolved the review posture; the SDK turns a ship decision and domain concern into posture-aware prose:

| Posture | When | Deadline language | | --- | --- | --- | | repository | change === null or scanMode: "all" (for example --all-files) | before shipping | | change | committed range (scanMode: "changed", not worktree) | before merging | | worktree | uncommitted local changes (headRef is WORKTREE) | before committing |

import { formatOpinion, formatOpinionAsync, requireOpinionConcern } from "@adversarylabs/sdk";

// Synchronous: concern must already be a noun phrase.
ctx.review.opinion(
  formatOpinion({
    ship: false,
    concern: "direct process termination below the application boundary",
    change: ctx.change,
  }),
);

// Async: free-form model titles/clauses are rewritten via the CLI model broker.
ctx.review.opinion(
  await formatOpinionAsync({
    ship: false,
    concern: "api get/post/patch/put silently no-op for v1 paths",
    change: ctx.change,
    model: ctx.model,
  }),
);

// Or rewrite only the concern, then format:
const { concern } = await ctx.model.concern({
  text: "Commands discard inherited context, breaking Ctrl+C",
});
// concern === "inherited context in command handlers" (example)

// Validate early when adapting model titles or free-form text:
requireOpinionConcern("forced exit code 124");
// throws: requireOpinionConcern("commands replace inherited context");

Helpers:

  • resolveReviewPosture(change) — map ctx.change to repository | change | worktree
  • isOpinionConcernPhrase(concern) — true when the string is a short noun phrase
  • requireOpinionConcern(concern) — validate/normalize a noun phrase or throw
  • normalizeOpinionConcern(concern) — lenient helper (may wrap clauses as that …); prefer requireOpinionConcern for overall opinions
  • formatOpinion({ ship, concern?, remainingCount?, change?, posture? }) — build { ship, summary }; rejects clause-shaped / headline concerns
  • formatOpinionAsync({ ship, concern?, model, … }) — same framing; rewrites invalid concerns through the model broker (no call when the phrase is already valid)
  • rewriteOpinionConcern(model, { text }) / ctx.model.concern({ text }) — pass-through rewrite only (structured schema + requireOpinionConcern validation, one retry by default)

When an adversary omits ctx.review.opinion(...), the SDK synthesizes an opinion from residual findings using the same posture rules.

Ownership: adversaries supply judgment (ship + concern draft); the SDK validates noun-phrase shape, optionally rewrites invalid concerns via the CLI-owned model broker, and frames posture language. The CLI prints opinion.summary and does not rewrite prose at render time. Provider credentials remain in the CLI broker (permissions.model: true).

app.defineRule(definition)

Registers domain-specific aggregation for a stable rule id. The SDK still owns grouping, deduplication, ranking, suppression, and rendering; the rule definition supplies engineering language for a grouped set of observations.

app.defineRule({
  id: "comments.complete-sentence",
  category: "code-style",
  defaultSeverity: "info",
  defaultConfidence: "high",
  groupBy: ["ruleId", "subject"],
  aggregate(observations) {
    return {
      title:
        observations.length === 1
          ? "Comment is a complete sentence"
          : "Comments contain complete sentences",
      confidence: "high",
      summary: `${observations.length} comments are written as complete sentences.`,
      whyItMatters:
        "Comments are most useful when they explain non-obvious intent rather than restating code.",
      recommendation:
        "Keep complete-sentence comments only when they explain non-obvious intent."
    };
  }
});

category, defaultSeverity, defaultConfidence, and groupBy act as defaults for observations with the same ruleId. If a rule has no aggregate(...), the SDK uses generic synthesis.

Definitions are scoped to one Adversary. Duplicate IDs throw; use app.replaceRule(...) when an intentional replacement is required. The top-level defineRule(...) API remains temporarily available for compatibility but is deprecated.

ctx.observe(input)

Use observations for raw detector output and evidence. Observations are normalized, deduplicated, grouped, synthesized, ranked, and rendered by the SDK. Prefer this path for new adversaries.

Default grouping uses:

ruleId + subject + category

Rule definitions can override this with groupBy. Individual observations can still override the issue boundary with groupKey:

ctx.observe({
  ruleId: "comments.complete-sentence",
  subject: "src/index.ts",
  groupKey: "complete-sentence-comments",
  category: "code-style",
  severity: "info",
  confidence: 0.95,
  title: "Comments contain complete sentences",
  location: { file: "src/index.ts", line: 3 },
  evidence: { comment: "This comment is a complete sentence." },
  recommendation: {
    summary: "Use complete-sentence comments intentionally where they clarify non-obvious code."
  }
});

Set deduplicate: false on a completed finding when separate findings intentionally share an identity. The SDK otherwise merges findings with the same group key or generated ID.

ctx.finding(input)

Use completed findings when the adversary has already synthesized the issue:

ctx.finding({
  title: "Comments contain complete sentences",
  category: "code-style",
  severity: "info",
  confidence: "high",
  summary: "Three comments are written as complete sentences.",
  whyItMatters: "Complete-sentence comments can be useful for intent, but noisy when they restate code.",
  impact: "Reviewers may spend time reading comments that do not add much context.",
  evidence: [
    { location: { file: "src/index.ts", line: 3 }, message: "Explains parser intent." },
    { location: { file: "src/index.ts", line: 11 }, message: "Explains fallback behavior." },
    { location: { file: "src/index.ts", line: 20 }, message: "Explains output formatting." }
  ],
  recommendation: "Keep complete-sentence comments only when they explain non-obvious intent.",
  remediation: { complexity: "trivial" }
});

Completed findings still pass through validation, deduplication, ranking, suppression, and rendering.

remediation.complexity accepts "trivial", "small", "medium", "large", or "architectural". It remains available in structured output but is not rendered in the default terminal review.

Confidence

Confidence accepts "low", "medium", "high", or a number from 0 to 1.

Default numeric thresholds:

  • low: less than 0.60
  • medium: 0.60 through 0.84
  • high: 0.85 and above

Customize thresholds with new Adversary({ review: { confidenceThresholds } }).

Severity

The SDK uses severity as a review calibration signal, not just a detector label.

  • info: interesting observations.
  • low: reasonable engineering improvements.
  • medium: issues likely to create operational problems.
  • high: security, correctness, or reliability risks.
  • critical: immediate production risk.

Override calibration when needed:

new Adversary({
  name: "adversarylabs/example",
  review: {
    severityOverrides: {
      "rule.id": "medium"
    }
  }
});

Suppression and Ranking

Review policy controls human-readable output:

new Adversary({
  name: "adversarylabs/comment-sentences",
  review: {
    minimumConfidence: "medium",
    maximumFindings: 5,
    includeInformational: false
  }
});

By default, low-confidence and informational findings are suppressed from the primary review. Suppressed findings are counted and can be included with run({ includeSuppressed: true }). Raw observations can be included with run({ includeRawObservations: true }).

Ranking is deterministic and considers severity, confidence, affected evidence count, runtime or production tags, and qualitative remediation complexity. It is not severity-only; a high-confidence medium issue can rank above a speculative high-severity issue.

Review Notes

Use review-level APIs for concise summaries that are not findings:

ctx.review.assessment({
  risk: "none",
  summary: "This review only reports complete-sentence comments."
});

ctx.review.positive({
  key: "intentional-comments",
  summary: "Several comments explain intent rather than restating implementation.",
  evidence: [{ location: { file: "src/index.ts", line: 3 } }]
});

ctx.review.observe({
  key: "sentence-style",
  summary: "Some comments are written as complete sentences."
});

ctx.review.opinion(
  formatOpinion({
    ship: true,
    concern: "comment sentence style cleanup",
    change: ctx.change,
  }),
);
// Or set a fully custom summary when domain voice must differ:
// ctx.review.opinion({ ship: true, summary: "Comment sentence style does not block shipping." });

ctx.review.score({
  key: "production-readiness",
  label: "Production readiness",
  score: 8.8,
  max: 10,
  summary: "Ready"
});

Scores are optional and remain available as an authoring API and terminal section. Because the CLI schema has no top-level score collection, the SDK serializes each score as a review observation. The note key is score.<score-key>, its summary is the rendered score, and its metadata preserves the authored key, label, score, max, and summary. No score data is discarded.

Observation-First Authoring

Prefer ctx.observe(...) for new adversaries. The intended flow is:

observe -> group -> synthesize -> rank -> review

Adversaries should describe what they observed, where it happened, and why it matters. The SDK should decide how observations group, which findings survive suppression, how they are ranked, and how they are presented.

Use ctx.finding(...) when the adversary has already done issue synthesis itself.

log

log.debug() and log.info() print only when ADVERSARY_VERBOSE is enabled with 1, true, TRUE, yes, or YES. log.warn() and log.error() always print. Logs go to stderr as:

[adversary] level: message

Review Result

app.run({ input }) is the side-effect-free library API and returns one normalized review object. Container and CLI entry points should call app.runFromEnvironment(), which reads the runtime environment and writes the run envelope to the configured output path.

type ReviewResult = {
  adversary: { name: string; version?: string };
  target: { repository?: string; filesScanned?: number };
  assessment?: { risk: "none" | "low" | "medium" | "high" | "critical"; summary?: string };
  positives: ReviewNote[];
  observations: ReviewNote[];
  findings: ReviewFinding[];
  opinion?: { ship?: boolean; summary: string };
  suppressed: { observations: number; findings: number };
  timing?: { totalMs?: number };
};

Renderers consume this result. The SDK includes TerminalRenderer and JsonRenderer:

const result = await app.run({ input: { source: { path: "/repo" } } });
new TerminalRenderer().render(result);
new JsonRenderer().render(result);

TerminalRenderer uses the same product layout as the adversary CLI text report: header (adversary, shortened repository, files scanned) → overall assessment → finding index → finding detail (evidence capped) → suppressed findings (when requested) → positives → scores → observations → overall opinion → findings footer. Active Findings: N counts only non-suppressed findings; suppressed details use a separate section and footer count. Prep/context notes (keys ending in .analysis or metadata.role: "context") are omitted from the Observations section. JSON output remains the full structured result.

Adversary implementations should not manually format review output.

Canonical wire evidence

The authoring API continues to accept nested location, structured data, and label. Envelope serialization translates those fields without losing their meaning:

| Authoring field | Canonical wire field | | --- | --- | | location.file | file | | location.line | line | | location.endLine | endLine | | data | metadata | | label when message is absent | message |

Wire evidence never contains location, data, or label. Findings likewise never contain SDK diagnostic fields such as synthesisSource.

Comment Sentence Example

adversary.yaml:

name: comment-sentences
version: 0.1.0
description: Reports TypeScript comments that are written as complete sentences.

triggers:
  manual: true
  files_changed:
    - "*.ts"
    - "**/*.ts"

detection:
  files:
    - "*.ts"
    - "**/*.ts"

runtime:
  name: node
  version: "22"
  command:
    - dist/index.js

permissions:
  filesystem:
    read:
      - .
    write:
      - .adversary/results
  network: false
  environment:
    allow: []

findings:
  format: adversary.review.v1

Automatic detection

The SDK owns the canonical adversary.yaml model, parser, validation, and published JSON schema. The CLI uses the parsed optional detection section when deciding which installed adversaries are relevant to adversary auto.

Use declarative file detection when changed repository-relative paths are sufficient:

detection:
  files:
    - Dockerfile
    - "**/Dockerfile"
    - .dockerignore

Use a detector entrypoint when applicability requires lightweight repository or change inspection:

detection:
  entrypoint: dist/detect.js

Both forms may be declared together. The entrypoint is validated as a portable project-relative path, but its build output does not need to exist while the manifest is parsed. Detector execution and matching behavior belong to the CLI/runtime and are not implemented by this SDK feature.

Use parseAdversaryManifest(...) to parse YAML or validateAdversaryManifest(...) to validate an already-decoded value. The resulting AdversaryManifest exposes detection directly, so consumers never need to re-read raw YAML. The schema is published as @adversarylabs/sdk/schemas/adversary.manifest.v1.

src/index.ts:

import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { Adversary } from "@adversarylabs/sdk";

const app = new Adversary({
  name: "adversarylabs/comment-sentences",
  review: {
    minimumConfidence: "medium"
  }
});

app.defineRule({
  id: "comments.complete-sentence",
  category: "code-style",
  defaultSeverity: "info",
  defaultConfidence: "high",
  groupBy: ["ruleId", "subject"],
  aggregate(observations) {
    return {
      title:
        observations.length === 1
          ? "Comment is a complete sentence"
          : "Comments contain complete sentences",
      confidence: "high",
      summary: `${observations.length} comments in ${observations[0]?.subject ?? "the file"} are written as complete sentences.`,
      whyItMatters:
        "Comments are most useful when they explain non-obvious intent rather than restating code.",
      impact: "Repeated prose can make routine code harder to scan during review.",
      evidence: observations.map((observation) => ({
        location: {
          file: observation.location?.file,
          line: observation.location?.line
        },
        message: "complete sentence",
        snippet:
          typeof observation.evidence === "object" && observation.evidence !== null
            ? String(observation.evidence.comment)
            : undefined,
        data:
          typeof observation.evidence === "object" && observation.evidence !== null
            ? observation.evidence
            : undefined
      })),
      recommendation:
        "Keep complete-sentence comments only when they explain non-obvious intent.",
      remediation: {
        complexity: "trivial"
      }
    };
  }
});

app.rule("comments.complete-sentence", async (ctx) => {
  const files = await ctx.rglob("*.ts");
  ctx.summary.files_scanned = files.length;

  for (const file of files) {
    const content = await readFile(join(ctx.repoPath, file), "utf8");
    const lines = content.split(/\r?\n/);

    lines.forEach((line, index) => {
      const match = line.match(/^\s*\/\/\s+(.+)/);
      if (!match) {
        return;
      }

      const comment = match[1] ?? "";
      if (!/^[A-Z][^.!?]*[.!?]$/.test(comment)) {
        return;
      }

      ctx.observe({
        ruleId: "comments.complete-sentence",
        subject: file,
        confidence: "high",
        title: "Comment is a complete sentence",
        location: {
          file,
          line: index + 1
        },
        evidence: {
          comment
        },
        tags: ["style"]
      });
    });
  }

  ctx.review.assessment({
    risk: "none",
    summary: "This review only reports complete-sentence comments."
  });

  ctx.review.opinion({
    ship: true,
    summary: "Comment sentence style does not block shipping."
  });
});

export default app;

Runtime Contract

Input is read from ADVERSARY_INPUT when set, otherwise:

/adversary/input.json

Expected input:

{
  "source": {
    "path": "/repo"
  }
}

Output is written to ADVERSARY_OUTPUT when set, otherwise:

/adversary/output.json

Output shape:

{
  "protocolVersion": 1,
  "result": {
    "adversary": {
      "name": "adversarylabs/example"
    },
    "target": {
      "repository": "/repo",
      "filesScanned": 2
    },
    "positives": [],
    "observations": [],
    "findings": [],
    "suppressed": {
      "observations": 0,
      "findings": 0
    }
  }
}

The package exports the exact CLI schema at @adversarylabs/sdk/schemas/adversary.review.v1. writeOutput(...) and runFromEnvironment(...) validate the complete envelope against that schema before writing.

Development

npm install
npm test
npm run build
npm run lint

CI and Release

Depot CI workflows live in .depot/workflows/.

  • Pull requests run lint, tests, and build.
  • Tags matching v* run lint, tests, build, verify the tag matches package.json, and publish to npm.

Publishing requires an NPM_TOKEN secret in Depot CI. Release tags should match the package version, for example v0.1.0.

With direnv:

direnv allow

The Nix flake provides Node 22 and npm.