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

@introspection-sdk/coding-agent

v0.22.0

Published

Opt-in OTEL capture of Claude Code and Codex sessions for the Introspection plugin — normalizes host transcripts and exports GenAI spans

Readme

@introspection-sdk/coding-agent

Opt-in OpenTelemetry capture of Claude Code and Codex sessions for the Introspection plugin.

Both hosts use one Introspection-owned, per-session activation protocol. The existing plugin reference loader silently writes a session-scoped request; the next host completion hook binds it to that hook's authoritative session/transcript and native turn boundary. Capture without the resulting rollout-bound marker returns not-activated and never defaults to byte zero. Claude Code requires 2.1.136 or newer; Codex requires CLI/Desktop 0.146.0 or newer, and plugins are not available in the Codex IDE extension.

It turns a host's own session transcript into GenAI spans under service.name = "introspection-plugin", authenticated with the Introspection CLI's existing login — so plugin activity correlates with the org, project, and member captured during onboarding, without anyone pasting a second API key.

Why a transcript reader rather than a tracing SDK

The obvious approach — wrap the agent and trace it in-process — does not apply here. Claude Code and Codex invoke a hook as a fresh subprocess per event, so there is no long-lived process to hold a tracer, and no in-memory state to correlate a tool call with its result.

Both hosts do write an append-only JSONL transcript, and both hand the hook its path. So capture reads the transcript instead: on each turn it consumes the bytes appended since the last checkpoint, converts them to spans, exports, and advances the checkpoint.

Parsing is delegated to @letta-ai/trajectory, which normalizes Claude Code and Codex transcripts (among others) into one record contract. That choice matters beyond not writing two parsers: it is the same normalization the eval harness reaches through Harbor's ATIF export, so a production onboarding session and an eval trajectory describe the same run the same way.

Consent

Capture is off unless a recorded opt-in enables it. A missing, unreadable, or malformed config resolves to disabled, as does a config written by a schema version this build does not recognize.

Consent is recorded at ~/.introspection/telemetry.json:

{
  "version": 1,
  "enabled": true,
  "content": "on",
  "targets": ["claude-code", "codex"],
  "granted_at": "2026-08-04T10:00:00.000Z"
}

content is the privacy dial, separate from the on/off decision. The three names are identical in the config and the --telemetry flag:

| Level | What leaves the machine | | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | off | Nothing. Records an explicit decline distinctly from "never asked". | | on | Span structure, timings, models, tool names, turn counts. No prompts, completions, tool arguments, tool output, working directory, or Git branch. | | full | The above plus message content, tool payloads, working directory, and Git branch. Needed to judge a trajectory the way the eval harness does. |

The gate is applied at span construction, and at on the normalizer is additionally told to drop tool results outright — so unconsented payloads never enter the process, rather than being filtered on the way out.

Override

INTROSPECTION_PLUGIN_TELEMETRY can narrow the stored choice for one session:

INTROSPECTION_PLUGIN_TELEMETRY=off    # disable without editing the file
INTROSPECTION_PLUGIN_TELEMETRY=on     # omit content from a stored full grant

An enabled stored grant is always required. The override can disable capture or narrow full to on; it cannot enable capture or widen on to full. An unrecognized value is ignored rather than read as "on", so a typo cannot silently enable capture. metadata is still accepted as on's former name.

Authentication

The CLI's device-authorization login writes ~/.introspection/credentials.json (mode 0600). This package reads the project-scoped access_token from it and presents it as the OTLP bearer.

It is strictly read-only: it never refreshes, never writes, and never logs a token. Refresh belongs to the CLI, which owns the rotating refresh token. An expired token means capture declines the run without advancing the checkpoint, so the turn simply lands after the next CLI invocation refreshes it.

Tenancy is not sent as span attributes. The processor stamps org_id, project_id, and member_id onto every record from the bearer's own claims; attributes claiming a tenant would be both redundant and untrusted.

Span shape

invoke_agent <host>              one turn — the root
├── chat <model>                 one per assistant message
└── execute_tool <name>          one per tool call, closed by its result

Every span carries the record's own timestamp, not the moment of capture. Capture runs at the end of a turn, so wall-clock-at-export would collapse a multi-minute turn into milliseconds and make every duration a lie.

Correlation attributes, on the resource:

| Attribute | Example | | -------------------------------------- | ------------------------------------------------------------- | | service.name | introspection-plugin (fixed — the platform's discriminator) | | introspection.plugin.host | claude-code, codex | | introspection.plugin.host_version | 2.1.221 | | introspection.plugin.host_entrypoint | cli, remote |

The host version is read back out of the raw transcript, because the normalized record contract is deliberately host-neutral and drops it. Reading it from the transcript also beats shelling out to claude --version: it is the version that produced these records, and it costs no subprocess on a latency-sensitive path.

Failure behavior

A hook runs inside an interactive coding session, so every failure mode is fail-open. Unparseable input, an unknown event, a missing transcript, an expired token, a dead collector, and an outright crash all resolve to "do nothing, exit 0". The binary always exits 0 — a non-zero exit is a signal to the host, and "telemetry had a bad day" is not something a coding session should be told.

Two properties bound the damage:

  • The checkpoint advances only after a confirmed flush. A crash or failed export costs a re-sent turn, never a lost one.
  • A 5s deadline caps a run. Past it the work is abandoned and lands on the next turn, so capture is never the reason a turn feels slow.

Usage

As a host hook:

introspection-capture --host claude-code            # reads the hook event on stdin
introspection-capture --host codex --dry-run --debug

--dry-run reports what would be captured without exporting — worth having, because a silent no-op is otherwise the most confusing thing about a telemetry integration.

Programmatically:

import { capture } from "@introspection-sdk/coding-agent";

const result = await capture({
  host: "claude-code",
  sessionId: "…",
  transcriptPath: "~/.claude/projects/…/session.jsonl",
});
// result.outcome: "exported" | "no-consent" | "host-not-covered"
//               | "not-logged-in" | "no-new-records" | …

Outcomes are enumerated rather than free-text so a caller can explain a no-op.

Tests

pnpm --filter introspection-tests test -- observability/test-coding-agent-capture.test.ts