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

opctl

v0.1.11

Published

Conservative local CLI bridge for OpenProject API v3

Readme

opctl

npm version

opctl is a small local Node.js + TypeScript CLI bridge for OpenProject API v3. It uses the current user's personal API token and is read-only by default.

Install

Published package:

Install globally:

npm install -g opctl
opctl --help

Or run without a global install:

npx opctl --help

Configuration

Export variables in your shell, load a local file with --env <path>, or save non-write defaults in a profile. By default, opctl also reads .env from the current working directory when present; pass --no-env to disable that. Required:

  • OPENPROJECT_URL: OpenProject instance URL, optionally including an instance path prefix. Server-returned API hrefs are resolved against this URL; when an instance path prefix is repeated in a server href it is stripped, so a prefixed instance URL behaves the same as a bare one.
  • OPENPROJECT_TOKEN: personal OpenProject API token.

Optional:

  • OPENPROJECT_AUTH_MODE: bearer (default) or basic. Basic auth uses username apikey and the token as password.
  • OPENPROJECT_DEFAULT_PROJECT: project identifier/id used by wp search when --project is omitted.
  • OPENPROJECT_ALLOW_WRITE: must be exactly 1 to allow write-capable commands.

Profile commands:

opctl profile set navlin-qa --url https://openproject.example.com --auth-mode bearer --default-project qa --token ...
opctl profile use navlin-qa
opctl --profile navlin-qa me --json
opctl profile show navlin-qa
opctl profile list

Profiles are stored under ${XDG_CONFIG_HOME:-~/.config}/opctl/profiles.json. The file is written with restrictive permissions where supported, tokens may be stored there, and profile display commands redact tokens. OPENPROJECT_ALLOW_WRITE is never loaded from .env files or profiles; writes still require the real process environment variable.

Usage

Show the authenticated OpenProject user:

opctl me
opctl me --json

Inspect API root links:

opctl api-root
opctl api-root --json

List projects:

opctl projects --page-size 20
opctl projects --json

Read work packages:

opctl wp get 123
opctl wp get 123 --json
opctl wp get 123 --raw-json
opctl wp get 123 124 --table
opctl wp get --ids 123,124 --fields id,subject,status,assignee --table
opctl wp get --ids 123,124 --jsonl
opctl wp get --ids 123,124 --raw-json

--raw-json emits a single raw OpenProject object for one ID and a JSON array for multiple IDs. wp view and wp show are aliases of wp get.

Field selection supports id,subject,status,type,assignee,responsible,project,href,browserUrl,updatedAt,description,shortDescription,attachmentsCount,lockVersion,priority; aliases: title=subject, url=href.

Search work packages:

opctl wp search --project my-project --subject "pump"
opctl wp search --project my-project --assignee-me --status open
opctl wp search --assignee [email protected] --open
opctl wp search --responsible-me --open
opctl wp search --responsible 18 --not-status "In Client Review"
opctl wp search --filter responsible=me --filter status=o --sort updated_at:desc
opctl wp search --open --subject "pump" --compact
opctl wp search --subject "pump" --fields id,subject,status --json

If --project is omitted, opctl wp search uses OPENPROJECT_DEFAULT_PROJECT when set. Without either, it searches the instance-wide work package endpoint.

Look up an exact user email, work package types, statuses, and priorities:

opctl users --email [email protected]
opctl users --email [email protected] --json
opctl types --project my-project
opctl types --json
opctl statuses
opctl priorities

User references accepted by work package search, create, and update are me, a numeric user id, a /api/v3/users/<id> href, or an exact email address. Exact-email resolution fails clearly when no unique visible user matches.

List work packages assigned to the authenticated user:

opctl wp mine
opctl wp mine --open --table
opctl wp mine --project my-project --page-size 50 --fields id,subject,status,updatedAt

List open work packages accountable/responsible to the authenticated user:

opctl wp accountable
opctl wp accountable --table --fields id,subject,status,responsible,updatedAt

Triage a known list:

opctl wp check 123 124
opctl wp check --ids 123,124 --fields id,title,status,assignee,shortDescription,attachmentsCount --table

Inspect and download work package attachments:

opctl wp attachments 123
opctl wp attachments 123 --json
opctl wp download-attachments 123 --dir /tmp/op-attachments
opctl wp download-attachments 123 --output-dir /tmp/op-attachments
opctl wp download-attachments 123 --dir /tmp/op-attachments --overwrite --json

Upload files directly to a work package:

OPENPROJECT_ALLOW_WRITE=1 opctl wp upload-attachments 123 report.pdf screenshot.png --dry-run --json
OPENPROJECT_ALLOW_WRITE=1 opctl wp upload-attachments 123 report.pdf screenshot.png --description "Agent evidence" --jsonl

wp upload-attachments reads and validates every local file before the first write, rejects duplicate paths, then sends one multipart request per file to /api/v3/work_packages/<id>/attachments. Use --json for one document or --jsonl for one result per file. A network failure after a write is reported as indeterminate, because the server may have accepted the upload.

Show redacted diagnostics:

opctl doctor
opctl doctor --json

HTTP errors include the request method and safe API path, with focused messages for authentication, visibility, stale lockVersion, and validation failures. Network errors include the attempt count and timeout or underlying cause without exposing query strings or tokens. Only GET requests are retried (network failures and HTTP 429/502/503/504); write requests are never retried automatically.

Pull the OpenAPI spec (defaults to the public community instance):

opctl spec pull
opctl spec pull --output openapi/my-spec.json
opctl spec pull --url https://openproject.example.com

Write-capable commands:

# Comment on a work package
OPENPROJECT_ALLOW_WRITE=1 opctl wp comment 123 --dry-run "Investigating"
OPENPROJECT_ALLOW_WRITE=1 opctl wp comment 123 "Investigating"

# Create a work package (dry-run validates through /api/v3/work_packages/form)
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "Improve Ask NAVLIN Explore messaging experience" --description-file ticket.md --parent 120 --assignee me --responsible [email protected] --dry-run
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "S" --json < ticket.md

# Update common fields; clear relationship/text fields explicitly
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --subject "Revised subject" --status "In progress" --priority High --assignee [email protected] --dry-run --json
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --clear-description --clear-parent --clear-assignee --clear-responsible

# Replace one Markdown section of the description, leaving the rest intact
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 24067 --replace-description-section "Backend integration" --description-file api.md

# Fail unless the description already contains a section (assertion)
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 24067 --require-description-section "Uploaded screenshots" --status "In Client Review"

# Re-read after writing and confirm the change landed
OPENPROJECT_ALLOW_WRITE=1 opctl wp update 123 --subject "Revised subject" --verify

# Use a user-story template
opctl wp create --template user-story > ticket.md
OPENPROJECT_ALLOW_WRITE=1 opctl wp create --project alspc --type Feature --subject "New feature" --template user-story --dry-run

wp comment, wp create, wp update, attachment uploads, and bulk mutations require OPENPROJECT_ALLOW_WRITE=1. Write-capable commands support --dry-run and avoid mutation in dry-run mode. Dry-run output includes the resolved request url, built by the same code path execution uses, so the preview always matches the actual request. Nested request details are JSON-formatted in text output instead of being collapsed to [object Object].

wp create supports parent, assignee, and responsible links in addition to the common type/status/priority fields. It validates through /api/v3/work_packages/form before creating. Description can come from --description <text>, --description-file <path> (use - for stdin), piped stdin, or --template user-story.

wp update supports subject, description, type, status, priority, parent, assignee, and responsible. Use --clear-description, --clear-parent, --clear-assignee, or --clear-responsible instead of relying on ambiguous empty values. It reads the current work package, validates through its form endpoint, and includes the current lockVersion in the PATCH; --dry-run emits that validated request without sending it. --replace-description-section <heading> rewrites only the body of the named Markdown heading (matched case-insensitively) and preserves every other section; the new body comes from --description or --description-file. --require-description-section <heading> aborts with a validation error when the current description lacks that heading. --verify re-reads the work package after a successful write and fails if the returned subject/description do not match what was sent.

Type, status, and priority references accept a case-insensitive exact name, numeric id, or full /api/v3/... href. Parent references accept an id or href. User references accept me, id, href, or exact email.

Bulk work package mutations

Bulk create and update consume JSONL from a file or stdin and emit JSONL by default:

OPENPROJECT_ALLOW_WRITE=1 opctl wp bulk create --file creates.jsonl --dry-run
OPENPROJECT_ALLOW_WRITE=1 opctl wp bulk update --file -

Each create record requires project, type, and subject; it may also contain description, status, priority, parent, assignee, responsible, and an optional unique caller-defined key that is echoed in the result:

{"key":"feature-1","project":"alspc","type":"Feature","subject":"Agent-created feature","assignee":"me","responsible":"[email protected]"}

Each update record requires a positive id plus at least one update field. Set description, parent, assignee, or responsible to null to clear it:

{"key":"feature-1","id":123,"status":"In progress","assignee":"[email protected]"}
{"id":124,"description":null,"parent":null,"responsible":null}

The entire manifest is parsed and server-preflighted before execution. Any local or server preflight error prevents all writes. After a successful preflight, mutations run sequentially and continue after individual failures; every line receives a result, and network failures during writes are marked indeterminate because their server-side outcome is unknown. --dry-run reports one concise record per line (method, path, and resolved url only, no payload) plus a N validated, 0 conflicts, 0 written summary on stderr, so large manifests do not flood the terminal.

OpenAPI

The repository commits openapi/openproject.json and generated types in src/generated/openproject.ts. The committed spec is an auditable public OpenProject baseline.

npm run openapi:pull and opctl spec pull default to the official public spec at https://community.openproject.org. They do not read OPENPROJECT_URL or OPENPROJECT_TOKEN, so running tests or pulling the spec never sends credentials to a private instance.

# Refresh from the public community spec (safe, no credentials needed)
npm run openapi:update

# Pull from a specific private instance (explicit opt-in)
OPENPROJECT_SPEC_URL=https://openproject.example.com \
OPENPROJECT_SPEC_TOKEN=... \
  npm run openapi:pull

# Or via the CLI
opctl spec pull --url https://openproject.example.com

Private-instance pulls use dedicated OPENPROJECT_SPEC_URL / OPENPROJECT_SPEC_TOKEN / OPENPROJECT_SPEC_AUTH_MODE variables. Normal OPENPROJECT_URL and OPENPROJECT_TOKEN are never used for spec pulling.

Build and verification

npm run typecheck
npm run test
npm run build
node dist/cli.js --help
node dist/cli.js wp --help

Safety model

  • No token or Authorization header is printed by normal errors, JSON output, spec pulling, or tests.
  • Spec pulling defaults to the public community spec and ignores OPENPROJECT_URL / OPENPROJECT_TOKEN; private-instance pulls require explicit --url or OPENPROJECT_SPEC_URL.
  • Local .env files are loaded for read configuration by default; --no-env disables that, and .env cannot enable writes.
  • OpenProject writes are blocked unless the real process environment contains OPENPROJECT_ALLOW_WRITE=1 exactly.
  • Every write-capable command supports --dry-run and avoids mutation in dry-run mode.
  • wp update and bulk update read the current lockVersion and include it in the PATCH, so a concurrent edit between read and write is rejected by OpenProject rather than silently overwritten (optimistic locking).
  • No delete, close, archive, or move commands are implemented. Bulk create/update is available, but only after whole-manifest preflight and with per-record results.