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

@letitcurl/harness-ingestor

v0.1.17

Published

Resumable Claude Code and Codex transcript importer for Measurement Platform, with adaptive uploads and performance reporting

Readme

Harness Ingestor

Import the current OS user's Claude Code and Codex JSONL transcripts into Measurement Platform, one JSON record per event, ready for PostgreSQL/Metabase queries. CLI usage comes first. App sessions are included only when they share the same supported local files. Antigravity and separate app database integrations are deferred.

Requires Node.js 24+ on macOS, Windows or Linux. Git is optional; absent Git/project configuration is recorded as unavailable. This is a one-shot command, not a watcher.

Quick start (published package)

npx @letitcurl/harness-ingestor@latest --help
npx @letitcurl/harness-ingestor@latest --days 7 --dry-run
npx @letitcurl/harness-ingestor@latest --days 7

The last command prompts for a hidden Platform token and asks for confirmation. Full private transcript content is uploaded after consent; review the destination and categories first. No npm publishing token is needed to run the public package.

Run locally

Quick help: npx @letitcurl/harness-ingestor@latest --help. Complete offline manual: npx @letitcurl/harness-ingestor@latest --manual or read MANUAL.md. From this checkout, use npm run local-test -- --help or npm run local-test -- --manual.

Unix man page

The build uses remark-man to generate a section-one page from MANUAL.md, bundled at dist/man/man1/harness-ingestor.1. From the checkout, run npm run build, then:

MANPATH="$PWD/dist/man:" man harness-ingestor

After a published release, a global installation can use:

npm install --global @letitcurl/harness-ingestor@latest
MANPATH="$(npm root --global)/@letitcurl/harness-ingestor/dist/man:" man harness-ingestor

Add that directory to your shell's MANPATH to use bare man harness-ingestor. The trailing colon preserves the system's default manual search path. Older npm versions may register the package's man field automatically; npm 12 no longer does, so the explicit path works independently of registration. No installer hook modifies shell configuration or system directories. Native Windows shells generally lack Unix man; use --manual or run man in WSL. Running via npx alone does not install a persistent system man page.

Options

--time-anchor now|latest-record (or --time-anchor=value) controls where the lookback window ends. now is the default. latest-record requires --days and uses each provider's newest valid effective timestamp at or before invocation time.

npm run local-test -- --days 7 --time-anchor=latest-record --dry-run

Claude Code and Codex get independent historical windows, so older Claude history can be selected alongside newer Codex history. Windows are inclusive and fixed from captured snapshots. Timestamp fallback remains record → session → file mtime; filesystem creation time is not used. Future or invalid/oversized records cannot establish anchors. A provider with no eligible records selects nothing.

Confirmed records still determine historical anchors, but are skipped for uploading. Repeating an unchanged run sends no transcripts; new history advances the window and only unconfirmed records within it upload. Late arrivals inside the window are included. Wall-clock time alone does not move a historical anchor. Performance reports may still be sent after consent. Changing anchor mode does not reset resume.

The preview shows provider windows, counts and earliest pending timestamps. Dry-run does not read authenticated resume state: confirmed/pending counts are not checked. Historical mode adds a validation pass over snapshots; it is not a faster scan. Performance selection metadata includes time_anchor, invoked_at, and provider_windows; shared since/until are null in historical mode.

| Option | Meaning | | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | --days N, --days=N | Positive whole number of rolling 24-hour days; omitted means all time | | --time-anchor now, --time-anchor=latest-record | End at invocation time (default), or each provider's latest eligible record; historical mode requires days | | --blazingly-fast | Remove the default adaptive ceilings of 20 requests/second and 8 concurrent requests | | --dry-run | Read/validate/count supported local records without authentication or network | | --theme spaceship, --theme=default | Spaceship by default; original dashboard remains available | | --ascii | Use ASCII decorations | | --help | Print inline command, environment and exit-code reference | | --manual | Print the bundled offline manual | | --version | Print installed version |

Informational flags must be used alone. Duplicate or unknown options are errors. --blazingly-fast keeps gradual ramp-up and error/offline backoff while removing the request-rate and concurrency ceilings. Each window of at least 10 seconds and 20 verified transcript successes checks p95 latency: at 1,000 ms or above, the target rate drops to 80% of the lower of its current target and achieved throughput, and concurrency drops by 20% (rounded down, minimum one). Continued slow windows reduce again; increases require 30 seconds after the last reduction and a fresh window below 1,000 ms. Rate increases are limited by 125% of achieved throughput, preventing the target from growing beyond delivered performance. This control uses the exact window p95; the dashboard shows a cumulative histogram estimate. --fast and --plain belong only to the demo; redirected production output is plain automatically. Days remain a command-line option; tokens/categories/state remain environment settings.

From this checkout, build and run the current code without publishing:

npm ci --ignore-scripts
npm run local-test -- --dry-run
npm run local-test

To select the last seven days:

npx @letitcurl/harness-ingestor@latest --days 7
npm run local-test -- --days 7

Omitting --days imports all time. --days N requires a positive whole number and selects individual records in a rolling window of N × 24 hours ending when the command starts unless --time-anchor=latest-record is selected. Both endpoints are inclusive, and the preview shows the exact UTC window. The filter uses the record timestamp, then the session timestamp, then file modification time when timestamps are absent (timestamp_source distinguishes transcript timestamps from file-time fallback). Future-dated records are excluded from a day window. Record bodies and idempotency keys stay unchanged. Combine with --dry-run to preview counts without uploading. Discovery still reads the supported transcript files because old files can contain recent records.

npm run local-test runs npm run build followed by node dist/cli.js. It uses your local transcripts and the real destination after confirmation. For the offline two-panel terminal UI demo, run:

npm run demo
npm run demo -- --theme default
npm run demo -- --theme spaceship
npm run demo -- --theme spaceship --ascii
npm run demo -- --fast --plain

The roughly 25-second offline design preview shows scanning, review, upload progress, separate HTTP activity and Adaptive decisions panels, rate ramp-up, HTTP 429, cooldown, a temporary connection outage, recovery, and performance-report delivery. Each panel retains ten entries and shrinks on smaller terminals. Resize the terminal during the demo; Ctrl-C cancels and restores the screen. NO_COLOR=1 disables colors. Spaceship is used when no theme is given. The original appearance is preserved as --theme default. spaceship adds bordered telemetry/control panels, cyan/violet accents, provider bars, status labels, and a relative target-rate sparkline. --ascii replaces its Unicode decoration for terminals/fonts that do not display it correctly. Themes affect only the interactive demo; plain logs and simulation scenarios remain unchanged. The real CLI also supports --theme spaceship|default and --ascii, with spaceship selected by default. --plain produces chronological logs; --fast runs the scenario without presentation delays.

All HTTP outcomes, timing, and adaptive decisions are scripted simulations, not a production controller or benchmark. The restart scene reloads a real temporary checkpoint journal and skips 18 synthetic records without mock requests. It does not prove crash-safe production persistence. No real transcripts, credentials, or network are accessed. Temporary fixtures and state are removed on normal exit or handled cancellation. A forced kill can leave the synthetic temporary directory behind. The demo needs no build and does not modify the production CLI. The real CLI implements its own scheduler, durable state, performance reports, and dual-panel renderer; the demo remains a scripted preview, not a load test.

Normal execution reads HARNESS_INGESTOR_TOKEN if set and nonblank, otherwise asks for a hidden Platform User API token, captures and validates source files, displays counts by harness, and asks once before uploading. The token stays in memory; there is no token argument or saved credential file. --dry-run never prompts for a token or makes network requests. --help and --version do not scan local files. An environment token skips the token question only; upload confirmation is still required. Supply it through your shell or secret manager; do not commit it or put its value in CLI arguments.

Terminal scanning uses cli-progress; uploads use a coordinated, at-most-10-FPS dashboard. The scan display stops before the summary and confirmation prompt. Uploads show overall record progress and a bounded window of transcript files with queued/uploading/retrying/done/failed states. Two rolling panels show HTTP outcomes and adaptive decisions (up to ten lines each, reduced for small terminals). Target/actual rates, in-flight slots, provider progress, local skips, and p95 are visible. Retry delays are visible. Redirected output uses plain labeled lines without cursor controls. Summary output is printed after restoring the terminal. Interactive CLI bars are cyan, completed uploads green, retries yellow, failures red, and queued files gray. NO_COLOR=1 or FORCE_COLOR=0 disables colors; FORCE_COLOR=1 enables them in an interactive terminal even when NO_COLOR is set.

All records          [===================>                    ]  50% 50/100 uploading
codex rollout-demo   [========================================] 100% 30/30 done
claude-code demo     [==========>                             ]  28% 20/70 retrying

The published distribution command is:

npx @letitcurl/harness-ingestor@latest

npm may retain its cache. There is no self-deletion.

Fixed destination and content scope

POST https://platform.hub.odt.co.th/api/v1/events
category_key: harness-transcript

The default category is harness-transcript. To target another category that already exists, is published and unarchived for the token's company, set HARNESS_INGESTOR_CATEGORY_KEY when running the CLI. The selected category is shown in the pre-upload summary.

HARNESS_INGESTOR_CATEGORY_KEY=my-category npx @letitcurl/harness-ingestor@latest

The token determines the submitting account and company. The category must already exist, be published and unarchived. This client does not create accounts/categories or change Platform. There is no read-only token-introspection endpoint, so authentication is checked by the first confirmed upload, not during discovery.

Full source record content is uploaded without redaction or summarization, including tool output, code, and any secrets/private text embedded in it. Original files are preserved.

| Harness | Default roots under the current OS home | Supported files | | ----------- | ----------------------------------------------- | ---------------------------------------------------------- | | Claude Code | .claude/projects/ | Project-level .jsonl and nested subagents/ transcripts | | Codex | .codex/sessions/, .codex/archived_sessions/ | Recursive rollout-*.jsonl |

CLAUDE_CONFIG_DIR replaces .claude; CODEX_HOME replaces .codex. These environment variables are read, never changed. Apps using these trees are included automatically; cloud-only sessions and app-specific databases are not searched. There is no whole-disk, other-user, or Windows-to-WSL-profile scan. Run inside each environment where required.

Missing provider directories are normal. Unreadable directories/files and symbolic links are reported. Symlinks inside transcript roots are not followed. “All” means all accessible supported files found at discovery, not deleted history or files created after that scan. History indexes, auth files, skills, paste caches, browser exports and tool-result directories are excluded.

References: Claude directory layout, Codex rollout source.

Event shape and identity

context.body is the native source JSON object: message, metadata, tool event, or other JSONL record. Arrays and nested objects stay queryable. Numbers use a lossless parser and serializer rather than JavaScript's floating-point conversion. No conversation is flattened.

An abbreviated synthetic example:

{
  "event": {
    "category_key": "harness-transcript",
    "idempotency_key": "claude-code:<source-hash>:record:12:<content-hash>",
    "occurred_at": "2026-01-01T00:00:00.000Z",
    "context": {
      "format_version": 1,
      "harness_type": "claude-code",
      "source_id": "<source-hash>",
      "session_id": "<source-session-id>",
      "line_number": 12,
      "content_sha256": "<content-hash>",
      "body": { "type": "user", "message": { "role": "user", "content": "Synthetic example" } },
      "raw_record": "{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"Synthetic example\"}}\n",
      "identity": {
        "local_username": "example",
        "git_user_name": "Example Developer",
        "git_user_email": "[email protected]",
        "git_status": "available"
      }
    }
  }
}

The envelope also carries observed_at, timestamp_source, source_interface and parent_session_id. raw_record preserves original text and line ending for accepted records; it costs additional storage, while body is the query surface. Blank lines have no event.

Local username comes from the OS. Git name/email are the effective settings of the source project, including repository overrides, observed at import time. They are not proof of historical authorship or a GitLab login. Missing fields are null. Missing projects never fall back to an unrelated global Git identity. The first source project directory found in a file is used for its identity lookup. Source content may itself contain absolute paths.

Per-record timestamps are preferred, then a timestamp found elsewhere in the same transcript, then file modification time. timestamp_source distinguishes transcript vs file fallback. CLI/app designation is retained only when the source supplies a string designation; otherwise source_interface is unknown. Parent relationships are preserved when available.

Hashes, retries and appending files

Keys use hashes, not UUIDs:

source_id = SHA-256(harness + NUL + logical source path)
content_sha256 = SHA-256(record JSON with recursively sorted object keys)
idempotency_key = <harness>:<source_id>:record:<physical-line-number>:<content_sha256>

Canonical content hashes ignore object-key order and whitespace, preserving array order and numeric spelling. Logical paths are relative to Claude's projects root, or the Codex rollout basename so moving a rollout into archives does not change its identity. A repeated record at two different positions remains two events. Transcript upload identities remain content-based. Performance runs and private temporary filenames use random identifiers.

Appending messages leaves earlier keys unchanged. A private local receipt journal skips confirmed records without HTTP on restart. Receipts are fsynced only after validated server acknowledgements. Lost acknowledgements, a fresh ledger, or older imports require server replay to establish local confirmation. Accepted events retain original metadata. Changing existing records produces new content-hash revisions; inserting/deleting earlier lines also changes later position-based identities. No history is overwritten.

Metabase / PostgreSQL examples

Use your authorized Platform database connection/reporting surface. These examples target the existing events_items and events_categories tables and explicitly scope by company; they do not provision access, views, or dashboards. Configure company_id as a required numeric Metabase variable, not a client-controlled tenant boundary for embedded dashboards.

Browse record types and message bodies:

SELECT
  e.occurred_at,
  e.context ->> 'harness_type' AS harness,
  e.context ->> 'session_id' AS session_id,
  (e.context ->> 'line_number')::bigint AS line_number,
  e.context #>> '{identity,local_username}' AS local_username,
  e.context #>> '{identity,git_user_name}' AS git_name,
  e.context #>> '{identity,git_user_email}' AS git_email,
  e.context #>> '{body,type}' AS record_type,
  e.context #>> '{body,payload,type}' AS codex_payload_type,
  e.context #> '{body,message}' AS claude_message,
  e.context #> '{body,payload}' AS codex_payload
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
  AND c.key = 'harness-transcript'
  AND c.archived_at IS NULL
ORDER BY e.occurred_at, e.context ->> 'source_id', (e.context ->> 'line_number')::bigint;

Count source records by harness and Git email (this is not a count of human messages):

SELECT e.context ->> 'harness_type' AS harness,
       e.context #>> '{identity,git_user_email}' AS git_email,
       count(*) AS records
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
  AND c.key = 'harness-transcript' AND c.archived_at IS NULL
GROUP BY 1, 2;

Records retain native provider shapes; do not blindly add cumulative token counters or assume every record is a message. Deduplicate historical revisions by source/line when your analysis requires latest state; the importer keeps all accepted facts immutable.

Throttling and failures

  • Shared adaptive budget for transcripts, retries, and reports: starts at 2 requests/s and two in flight, ceilings 20 requests/s and eight in flight, 30-second request timeout.
  • Healthy transcript windows (at least 10 seconds and 20 successes) increase the target by 25% and concurrency by one. Reports alone cannot cause ramp-up.
  • Blazingly fast mode additionally uses the 1,000 ms p95 latency wall and achieved-throughput limit described above; successful but slow windows reduce pace instead of increasing it.
  • HTTP 429 immediately halves the target/concurrency. Three other transient failures in ten seconds also reduce them, at most once per window. Minimum 0.1 requests/s and one slot; increasing after a reduction requires 30 healthy seconds.
  • Five retries after the first attempt for HTTP 408/429 and 500/502/503/504.
  • Three consecutive transport failures open the offline circuit. New records pause; only one pending-request probe runs at a time, with jittered exponential waits from approximately five seconds up to sixty seconds. Waits continue until recovery or Ctrl-C; offline probes do not exhaust the normal retry budget. Recovery resumes conservatively.
  • Exponential backoff starts at one second with jitter; client delays cap at 60 seconds.
  • Valid Retry-After seconds or HTTP dates set a minimum wait, even above 60 seconds.
  • HTTP 200/201 counts as delivered only if category, key and returned source body match.
  • Authentication/category failures, invalid successful responses and exhausted transient retries stop the run. Redirects are not followed with the bearer token.
  • HTTP 400/409/413 and field-validation 422 errors mark the record failed and continue. Category-level validation stops. Error response bodies are never printed.
  • Malformed UTF-8/JSON, duplicate object keys, non-object records, incomplete final JSON, records above 8 MiB, or requests above 16 MiB are reported without upload. They are never truncated, split into opaque fragments or silently counted as complete. Blank lines are ignored.

The live server request-size limit is unverified; a server 413 can reject records below the client limit. Other server JSON/JSONB restrictions may also reject records. Precision changes in a successful server response are detected; raw_record preserves the original numeric text.

Exit codes: 0 complete/dry-run; 1 failed or incomplete ingestion; 2 invalid arguments; 130 cancellation. Saved/replayed/failed/incomplete totals count records. Unreadable counts source issues. Cancellation prevents further scheduled requests; an in-flight request may already have been accepted, and a rerun safely replays it.

Durable state and performance reports

HARNESS_INGESTOR_STATE_DIR overrides the default user state directory: macOS ~/Library/Application Support/harness-ingestor, Linux $XDG_STATE_HOME/harness-ingestor (fallback ~/.local/state/harness-ingestor), Windows %LOCALAPPDATA%/harness-ingestor. Use a private local filesystem, not a shared/network drive. Windows permissions depend on the user directory's inherited ACL. Namespace identity uses the endpoint, transcript category, and an HMAC of the token with a local random salt; the token is never saved. Token rotation conservatively establishes a new ledger through server replay.

A namespace lock prevents concurrent importers. A proven dead process lock can be recovered; an unknown/malformed owner or reused PID fails closed. Never remove a lock while an importer may still be running. Incomplete trailing receipt writes are discarded; corrupt complete receipts and checkpoint write failures stop ingestion. State stores hashed record keys, receipt outcomes, statistics with attribution, and pending reports/logs—not transcript content. Keep it to enable resume.

After confirmation, the CLI automatically submits statistics to harness-client-performance, overridable with HARNESS_INGESTOR_PERFORMANCE_CATEGORY_KEY. The transcript, performance, and harness-client-logs categories must already be usable for the token. Reports are sent at upload start, every 60 seconds, and on exit. No reports are sent for dry-run, declined confirmation, or an empty selection.

Each event has a stable harness-performance:<run-id>:<sequence> key and a queryable context.body: schema/client/controller versions, run status, UTC timestamps, selection, record counts, request/status/retry counts, payload bytes, durations, throughput, adaptation history, and provider breakdowns. Reports contain no transcript text, tokens, raw server responses, or local source paths. Their own HTTP measurements are excluded from transcript metrics, but they share the rate limit and can trigger protective slowdown.

Request latency covers the HTTP attempt and response handling, excluding scheduler/backoff waits. Successful, failed, and all-attempt histograms use versioned fixed upper bounds; median/p95/p99 are bucket estimates, not exact samples. Merge histogram counts to aggregate percentiles; never average p95 values. active_upload excludes offline waits but still includes pacing/backoff. task_throttle_wait and task_backoff_wait sum waits across concurrent tasks; they overlap and must NOT be added to wall-clock upload duration.

Histogram version 2 has 16 numeric upper_bounds_ms and 17 counts: the final counter is the overflow bucket, explicitly described by overflow_above_ms: 60000. There is no trailing null bound, which Rails would remove from incoming arrays. Version 1 reports may have 16 stored bounds and 17 counters for the same reason; their final counter also represents values above 60,000 ms.

Legacy queued reports retain their original payloads and idempotency keys. Only the performance sender tolerates removal of the known version-1 histogram's trailing null when checking a saved/replayed response. All other content must still match; transcript validation is unchanged. A response mismatch is reported explicitly and remains queued. To recover old reports, rerun the updated client on the original machine with the same token, categories and state directory, select a nonempty transcript window, and confirm. Already confirmed transcripts are skipped. Dry-run and declined consent never drain the outbox.

Statistics snapshots persist every five seconds. Hard-killed runs are marked interrupted on a later confirmed invocation, with potentially incomplete metrics. Missing heartbeats alone mean stale/unknown. Reports retry independently through a durable outbox. Unattempted progress reports may be coalesced; attempted payloads remain frozen. Graceful exit gives final delivery one shared five-second network grace period for performance reports and client logs, then leaves remaining events queued. Report failures do not undo receipt checkpoints or cause confirmed transcripts to be resent.

Performance reports include the same context.identity fields as selected transcripts: local_username, git_user_name, git_user_email, and git_status. These are local/Git attribution, not a verified Platform account identity. If selected transcripts have different identities, context.identity is null and context.identities contains the distinct objects; the CLI previews this before confirmation. Missing Git fields remain null. Recovery and retries preserve the original attribution; older queued reports retain their original shape. These identity fields are stored in the private performance outbox, never in client logs.

For Metabase, query the latest report per context.body.run_id, ordered by numeric report_sequence. The reports are cumulative: do not sum every heartbeat's counts. Scope queries through your authorized company-scoped reporting surface. A reporting outage is shown as queued/deferred; a successful import does not imply all telemetry was delivered.

Latest cumulative performance snapshot per run (same company-scoping requirements as above):

SELECT DISTINCT ON (e.context #>> '{body,run_id}')
  e.context #>> '{body,run_id}' AS run_id,
  e.context #>> '{body,status}' AS status,
  e.context #>> '{body,reported_at}' AS reported_at,
  e.context #> '{body,records}' AS records,
  e.context #> '{body,duration_ms}' AS duration_ms,
  e.context #> '{body,requests,success_latency_ms}' AS successful_request_latency,
  e.context #> '{body,adaptation}' AS adaptation
FROM events_items e
JOIN events_categories c ON c.id = e.event_category_id AND c.company_id = e.company_id
WHERE e.company_id = {{company_id}}
  AND c.key = 'harness-client-performance'
ORDER BY e.context #>> '{body,run_id}',
  (e.context #>> '{body,report_sequence}')::bigint DESC;

Temporary storage and verification

Files are copied to private temporary snapshots before confirmation to avoid upload races. Unix permissions are 0700/0600; Windows uses the user's inherited temp-directory ACL. Allow temporary disk space for the corpus, including duplicate copies. Capture checks file identity, size and timestamps and retries twice if they change. Continuously changing files are reported incomplete. These checks are not a defense against hostile timestamp manipulation.

Normal exit/cancellation removes importer snapshots. Forced termination or cleanup errors can leave harness-ingestor-* directories in the OS temp folder; remove only a confirmed inactive importer directory. Original files and npm's cache are never deleted.

npm ci --ignore-scripts
npm run check
git diff --check

For local readability fixes, run npm run lint:fix followed by npm run format. The same scripts work with bun run lint:fix and bun run format. ESLint enforces braces, strict equality, separate variable declarations, sorted imports/exports, type-only imports, and unused-binding checks. ESLint also rejects debugger statements, eval, string-based timers and Function constructors. Prettier provides two-space indentation, single quotes, LF line endings and a 100-column target (not a hard line-length limit). npm run format:check and npm run lint are read-only and both run in npm run check. Generated output and the generated npm lockfile are excluded from formatting.

One-command local CI

Install Bun (tested with the version in .bun-version), Node.js 24+, npm and Git, then run:

bin/ci
# Equivalent; also use these on Windows:
bun run ci
npm run ci

The Bun-powered runner prints timed stages and stops on the first failure: runtime checks, npm ci --ignore-scripts, formatting, lint, type-checking, Node tests, Bun coverage, build, packed-package smoke, and working-tree/staged whitespace checks. It resolves this repository from the script path, so bin/ci also works when invoked by absolute path from another directory. It permits pre-existing local edits but fails if checks change package.json or package-lock.json; it never restores or discards your changes.

Use bun run ci --skip-install for an already-installed working copy, or bun run ci --offline to install using a warm npm cache without network access. bin/ci --help performs no checks. The default install may use the network and replaces node_modules using the npm lockfile. No formatting fixes, publication, push, or live transcript import occurs in this runner.

Bun is the runner, not a replacement for the published CLI's Node runtime or npm packaging. Bun's built-in bun ci only installs dependencies; it is not the same as this project's bun run ci. The existing GitLab checks continue to use npm run check, so this local helper does not require changing runner infrastructure. Local execution was verified on macOS; Windows/Linux execution remains to be verified.

If mise warns about resolving .bun-version while offline, first check bun --version. When the pinned version is already installed under ~/.bun, register that installation without downloading it: mise link [email protected] ~/.bun. Do not label a different Bun version as 1.3.14. The demo itself only needs Node; its full-screen dashboard is cleared on exit, leaving the summary. Use npm run demo -- --plain to retain the activity log.

Test coverage

Run npm run test:coverage with Bun 1.3.14 (also pinned in the GitLab coverage job). This uses Bun's built-in coverage, not c8 or an additional instrumentation dependency. It runs the same test files with Bun, prints function/line coverage and uncovered lines, and writes coverage/lcov.info. The LCOV file can be opened in an editor coverage viewer. Bun does not produce an HTML report here. Generated coverage is ignored by Git and excluded from the npm package.

scripts/check-coverage.mjs requires at least 90% aggregate lines and functions from Bun's LCOV report, failing on missing/malformed reports. Bun's built-in threshold is per-file and is not used for this aggregate gate. Test files, generated output and ESLint configuration are excluded. This is loaded-module coverage, not proof that every repository file is exercised: subprocess-only CLI, demo and packaging entry points are not represented by the parent's coverage. Node tests and package smoke checks remain necessary for the actual shipped runtime. No branch-coverage or per-file threshold is claimed.

bin/ci runs both Node tests and Bun coverage. npm run check remains the Node-only verification command. GitLab runs a separate required coverage verification job, extracts the line percentage and retains LCOV for 14 days, including failed runs when a report was generated. This is a downloadable artifact, not GitLab inline Cobertura annotations. Publication waits for the verification stage to succeed.

Important regression coverage includes the 7-day → 8-day → repeated 8-day import, both providers, local checkpoint skipping, performance reports on each run, immutable report retries, offline recovery, cancellation, state isolation, corrupt journals, checkpoint failures, terminal cleanup and invalid/duplicate flags. Passing percentages do not validate live server limits, native Windows/Linux behavior, power-loss durability or production performance. Those require separate verification.

Checks cover formatting, ESLint, TypeScript, synthetic files, mocked HTTP/clocks, progress rendering, compilation and installation/execution of the packed npm artifact. The smoke install prefers the local npm cache but can fetch missing runtime dependencies on a clean CI runner. No real transcripts or live tokens are fixtures. GitLab's default job uses Node 24 on Linux; opt-in native shell jobs need Node 24/Git, tags harness-windows and harness-macos, and HARNESS_NATIVE_CI=true. This repository does not provision runners or settings. Windows/Linux runtime verification and actual remote CI execution must be recorded separately when available.

private: true prevents accidental publication in the checkout. The protected GitLab release pipeline creates a temporary scoped manifest and publishes only the tested archive. The checks job writes release/ metadata and a SHA-256 checksum; the publish job verifies both before any registry operation. This self-hosted GitLab requires a protected, masked/hidden granular NPM_TOKEN with publish access to the package. npm Trusted Publishing currently supports GitLab.com shared runners, not this GitLab host. Do not revoke the publishing token on the assumption that an OIDC variable makes authentication work. See npm Trusted Publishing. This publishing credential is separate from the user's HARNESS_INGESTOR_TOKEN used for importing into Platform.

The release job runs only for protected main pushes in the intended GitLab project. It publishes an immutable 0.1.<CI_PIPELINE_IID> version under the ci tag and advances latest only when the registry state is newer. Remote CI execution, npm publication, live imports, Platform changes, category creation, app-specific integration, other-user collection and background services remain separate operational actions.

Debugging an interrupted import

Each non-dry run writes a private JSONL diagnostic file under diagnostics/ in the local state directory (HARNESS_INGESTOR_STATE_DIR overrides that directory). The final output prints its path and repeats any stop reason after the progress display closes. Share this diagnostic file when investigating a failed import.

After confirmation, safe diagnostic events also upload to the existing harness-client-logs category. Start, selection, progress every 60 seconds, errors/retries, stop reasons, and final counts share the performance report's context.body.run_id. Each JSONL line becomes one event, with parsed metadata at context.body and the original line at context.raw_record. Keys use harness-client-log:<run-id>:<sequence> and remain unchanged on retry. Order activity by numeric context.body.sequence; stop and summary events are prioritized over queued details.

Diagnostics contain the package version, failed/retried HTTP attempt metadata, safe error codes, scan-local file numbers and line numbers, progress and final counts. They exclude tokens, identities, transcript paths/content, request/response bodies, and raw exception messages/stacks. HTTP status 0 means no response; -1 means the attempt was aborted. Detailed events are capped at 2,000 per run, with the stop and summary retained. Files use owner-only permissions on systems supporting Unix permissions. Local JSONL files remain until manually deleted. New remote events are persisted approximately every second to a separate private outbox before delivery; verified receipts remove them from that outbox. Failed deliveries retry on a later confirmed run with the same token, transcript category, and state directory. No client logs upload for dry runs, declined confirmation, empty selections, or failures before confirmation. Dry runs create no local logs. Hard termination can lose details not yet persisted to the outbox; existing queued events remain retryable. Local file or delivery failures do not stop ingestion.

The CLI previews the log category and privacy details, shows delivery activity while uploading, and prints delivered/queued totals after closing progress. Queued events are not counted as delivered. A missing/unavailable log category or invalid token leaves events queued. The log sender shares request pacing without adding its own attempts to transcript statistics or logs.

incomplete combines invalid/oversized records with valid records left unfinished when a run stops. The final output now shows these counts separately. failed counts explicit upload failures; saved and replayed count verified receipts.

For an offline check of a local checkout, run npm run check. This includes mocked HTTP upload-failure tests and npm run smoke, which installs the built package in a temporary directory and checks CLI startup with isolated transcript roots. These checks do not send test traffic to Platform. Run npm run build before invoking npm run smoke on its own.