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

@quire-io/api-client

v1.0.0

Published

TypeScript client for the Quire API — typed endpoints, OAuth helpers, error formatting.

Readme

@quire-io/api-client

A TypeScript client for the Quire REST API.

Typed wrappers around fetch, OAuth helpers, and error-formatting utilities for the Quire API. Used by the Quire CLI and other Node-based Quire integrations.

Status: v0.1.x is the initial extraction. Expect minor breaking changes before 1.0.

Install

npm install @quire-io/api-client

Requires Node.js 20+.

What's in the box

| Module | Exports | |---|---| | QuireClient | Authenticated client covering tasks, projects, organizations, comments, chats, documents, insights, dashboards, reminders, partners, sublists, statuses, tags, custom fields, attachments, timelogs, approvals, undo. Auto-refreshes tokens via a caller-supplied callback. See COVERAGE.md for the full per-endpoint table. | | exchangeCode / refreshTokens | Parametrized OAuth helpers. Supports both confidential clients (pass clientSecret) and public PKCE clients (pass codeVerifier). | | formatQuireError | Maps an HTTP error response into a short, user-readable string. Knows about Quire's quota / rate-limit / paid-plan signals. | | QuireAuthRevokedError, QuireTokenRefreshError | Typed errors thrown by QuireClient and the OAuth helpers. | | parseQuireUrl | Parses a quire.io URL into a { kind, ... } descriptor. | | looksLikeOid | Distinguishes a 24-char Quire OID from a slug / numeric id. | | resolveColor, COLOR_TABLE, NAMED_COLORS | Quire's fixed icon-color palette + friendly name lookup. | | Type definitions | QuireTask, QuireProject, QuireOrganization, QuireUser, QuireRecurrence, etc. |

Usage

Register an OAuth client and look up scopes / endpoints in the Quire API docs.

Confidential client (e.g. server-side OAuth)

import {
  QuireClient,
  exchangeCode,
  refreshTokens,
} from "@quire-io/api-client";

const tokens = await exchangeCode({
  apiServer: "https://quire.io",
  clientId: process.env.QUIRE_CLIENT_ID!,
  clientSecret: process.env.QUIRE_CLIENT_SECRET!,
  code,
  redirectUri: "https://your.app/callback",
});

const client = new QuireClient({
  tokens,
  apiServer: "https://quire.io",
  refreshTokens: (refreshToken) =>
    refreshTokens({
      apiServer: "https://quire.io",
      clientId: process.env.QUIRE_CLIENT_ID!,
      clientSecret: process.env.QUIRE_CLIENT_SECRET!,
      refreshToken,
    }),
  onTokenRefresh: async (newTokens) => {
    await db.saveTokens(userId, newTokens);
  },
});

const me = await client.getMe();

Public PKCE client (e.g. CLI / installed app)

import {
  QuireClient,
  exchangeCode,
  refreshTokens,
} from "@quire-io/api-client";

// During login: PKCE code_verifier is generated alongside code_challenge
// and stored in memory until the redirect comes back.
const tokens = await exchangeCode({
  apiServer: "https://quire.io",
  clientId: "your-cli-public-client-id",
  code,
  redirectUri: "http://127.0.0.1:54321/callback",
  codeVerifier,
});

const client = new QuireClient({
  tokens,
  apiServer: "https://quire.io",
  refreshTokens: (refreshToken) =>
    refreshTokens({
      apiServer: "https://quire.io",
      clientId: "your-cli-public-client-id",
      refreshToken,
    }),
  onTokenRefresh: async (newTokens) => {
    await fs.writeFile(credPath, JSON.stringify(newTokens), { mode: 0o600 });
  },
});

Logger

QuireClient is silent by default. Pass a logger to surface API errors:

new QuireClient({
  tokens,
  apiServer: "https://quire.io",
  logger: {
    error: (msg, ctx) => console.error(msg, ctx),
    info: (msg, ctx) => console.log(msg, ctx),
  },
});

The logger interface is { error, info, debug?, warn? } — any structural match works.

Search filters

searchTasks (project), searchTasksInOrganization, and searchTasksInFolder all accept the same QuireTaskSearchParams shape. See the interface in src/client.ts for the full field list and per-field JSDoc; this section covers the grammar shared across many fields.

Boolean grammar (user / tag / priority / type / createdBy / recurring)

assignee, assignor, follower, createdBy, tag, priority, type, recurring all share the same grammar:

| Token | Meaning | |---|---| | , | AND | | \| | OR | | ! | NOT |

Values pass through verbatim — the server parses. Quote names containing spaces or special characters: tag: '"In Progress"'.

Date columns

created, edited, archived, unarchived, toggled, start, due accept three operand styles:

| Style | Example | Notes | |---|---|---| | Keyword | due: "today" | past, yesterday, today, tomorrow, upcoming, last7d, next7d, lastWeek, thisWeek, nextWeek — timezone is the caller's. | | op:value | created: "ge:2026-01-01T00:00:00Z" | Ops: ge, gt, le, lt, eq, ne, between, notBetween. Operand is ISO 8601; between / notBetween are inclusive on both ends. | | Null | archived: "isNull" | isNull / isNotNull, nullable fields only. |

start and due additionally accept a date-only operand (YYYY-MM-DD) that expands to a whole-day window in the caller's timezone.

Numeric custom fields (May 27 2026)

Number / Money / Duration custom fields accept the same op:value grammar as date columns — ge: / gt: / le: / lt: / eq: / ne: / between:v1,v2 (inclusive) / notBetween:v1,v2 / isNull / isNotNull (case-insensitive). A bare value is exact match (equivalent to eq:). Duration operands use the same 8h / 30m shape as the modified interval.

await client.searchTasks(projectOid, {
  customFields: {
    Cost: "ge:100",                 // Money: ≥ 100
    Score: "between:50,150",         // Number: 50 ≤ x ≤ 150
    Effort: "between:8h,40h",        // Duration: 8h ≤ x ≤ 40h
  },
});

Other custom-field types are exact match — Email / Hyperlink also accept ~ / ~* prefix for regex; Text custom fields aren't searchable here (use the top-level text parameter for full-text search).

Scope restrictions

sublist and customFields are project-scope only — the org and folder endpoints reject them with Unsupported query parameter.

Pagination

Pair limit (integer, or "no" for unlimited; free-plan cap is 30) with cursor. The last item of each page carries a cursor field; pass it as the next request's cursor (with the same limit and filters) to fetch the next page. The absence of cursor on the final item signals end of stream. cursor cannot combine with sublist (400).

Examples

Mixed filters — boolean grammar, date range, pagination cap:

const tasks = await client.searchTasks(projectOid, {
  status: "active",
  tag: '"In Progress",urgent',         // (In Progress) AND urgent
  assignee: `${me.oid}|${teammate.oid}`, // me OR teammate
  due: "between:2026-05-01,2026-05-31",
  priority: "high|urgent",
  limit: 50,
});

Recently modified (any edit, comment, status flip, etc.) — use the modified interval, not a date column:

const recent = await client.searchTasks(projectOid, {
  modified: "7d",        // "24h" / "30m" also valid; default is "7d"
  status: "active",
  limit: 50,
});

modified defaults to "7d" if omitted; pass modified: false to search the full history. The response isn't sorted by modified time — sort client-side on QuireTask.modified if you need most-recent-first.

Followers

Follower fields share one value grammar everywhere they appear: a user OID, ID, or email, plus these special values:

| Value | Meaning | |---|---| | "me" | The authenticated user | | "app" | The application itself (receives hook notifications) | | "app\|team", "app\|team\|channel" | App follower routed to a team / channel | | "app\|/path" | Appended to the registered hook URL — hook https://super.app/hooks/standard + "app\|/soc1/33456/a7" → https://super.app/hooks/standard/soc1/33456/a7 | | "inherit" | Tasks only. On add: pull in the parent task's followers. On remove: drop the ones inherited from the parent. |

Three field shapes, all optional and independent:

  • followers — replaces the entire list.
  • addFollowers / removeFollowers — edit in place, leaving the rest alone.

| Method | followers | addFollowers / removeFollowers | |---|---|---| | createTask / createSubtask / createTaskRelative | ✅ | — (create has no list to diff) | | updateTask | ✅ | ✅ | | bulkCreateTasks / bulkCreateSubtasks / bulkUpdateTasks | ✅ | ✅ (update only — items are pass-through Record<string, unknown>) | | updateProject | ✅ | ✅ | | updateOrganization | ✅ | ✅ | | createChat / createDocument | ✅ | — | | updateChat / updateDocument | ✅ | ✅ |

// Follow a task as the current user, and route notifications to the app.
await client.updateTask(taskOid, { addFollowers: ["me", "app|eng|alerts"] });

// Stop following, but leave everyone else alone.
await client.updateTask(taskOid, { removeFollowers: ["me"] });

// Pin the list to exactly these two.
await client.updateTask(taskOid, { followers: [aliceOid, "[email protected]"] });

Notes:

  • The creating user is added as a follower automatically by createTask — passing followers at creation adds to that, it doesn't replace it.
  • Responses carry followers back as objects ({ oid, id, name }[]) on QuireTask.followers, QuireProject.followers, QuireOrganization.followers, QuireChat.followers, QuireDocument.followers — not as the string refs you sent.
  • Documents are followable only when project-owned; sending follower fields for other owner types returns 400.
  • searchTasks has a separate read-side follower filter — see Search filters.

Record visibility (members)

Sublists, documents, chat channels, insights, dashboards and reminders take an optional members list at creation, which decides who can see the record. It is a tri-state, and all three states come back explicitly so they can be told apart:

| Sent | Meaning | Returned as | |---|---|---| | omitted, or null | Every member of the owner (the default) | null | | [] | The owner's admins only | [] | | ["me", "[email protected]"] | Only those users | list of user objects |

const private = await client.createSublist("project", projectOid, {
  name: "Q3 planning",
  members: ["me"],          // only you
});

Rules worth knowing before you send it:

  • Create-only. There is no way to change members afterwards — recreate the record.
  • Every listed user must already be a member of the owner, and a non-empty list must include you. A non-member is rejected with 400 rather than being dropped silently the way a task's assignees are.
  • members cannot be combined with partner — that already shares the record with the whole project and the external team. The pair is 400.
  • Responses return members as user objects ({ oid, id, name }), matching followers and mutes. Before the Sep 21 2026 server release it was a list of OID strings.

Reminders

A reminder notifies the people it names, either before a task's start/due date or at a time of its own. Unlike every other work endpoint, ownerType is always required — there is no implied project.

// Standalone reminder on a project
await client.createReminder("project", projectOid, {
  when: "2027-01-15T09:00:00.000Z",
  leads: [{ minutes: 30 }, { days: 1, at: "09:00" }],
});

// Personal reminder in your own inbox
await client.createReminder("project", "-", { when: "2027-01-15T09:00:00.000Z" });

// On a task that already has a start or due date — send `leads` alone
await client.createReminder("task", taskOid, { leads: [{ days: 1 }] });

What it fires from depends on the task:

  • Task with a start or due date → it fires from that date. Sending when or recurrence is rejected with 400, because they would be ignored.
  • No task, or a task with neither date → when is required.

leads says when to notify ahead of the fire time, one notification each. Each entry names exactly one of minutes (an absolute offset) or days (calendar days, so it keeps its wall-clock time across a daylight-saving change); at ("HH:mm") is only valid with days and is read in the creating member's timezone. Omitting leads means a single notification at the fire time ([{ minutes: 0 }]). Max 30 leads, none negative or beyond 10,000 days.

Listing is by owner, and a project's list includes the reminders on its tasks — narrow it by passing "task":

await client.listReminders("project", projectOid);        // project + its tasks'
await client.listReminders("task", taskOid);              // just this task's
await client.listReminders("project", projectOid, { limit: 100 });

Reminders are ordered by oid — stable, but not chronological, so sort client-side on the field you care about. With a numeric limit, the last item of a page carries a cursor when more remain.

deleteReminder is permanent: a reminder is not moved to the trash and cannot be restored, so there is no undo-remove for it.

Errors

| Class | Thrown when | |---|---| | QuireAuthRevokedError | The user's grant was revoked or expired past the refresh window. The caller should clear stored tokens and prompt the user to re-authorize. | | QuireTokenRefreshError | A token refresh failed with a specific HTTP status. 4xx → grant is dead (the client converts these to QuireAuthRevokedError automatically); 5xx → transient, callers may retry. | | Error (with formatted message) | Any other Quire API failure — body is run through formatQuireError so it stays compact and consumer-friendly. |

Since the Sep 21 2026 server release the HTTP status carries the category and the JSON body's code the specific cause. formatQuireError maps them to distinct messages, because the recovery differs:

| Status + body code | Meaning | What helps | |---|---|---| | 402 + 469 | Plan limit or quota (tasks, custom fields, dashboard, insight, sublist, chat, doc) | Upgrading. Retrying never does. | | 402 + 470 | Subscription expired | Resubscribing — no in-plan upgrade clears it | | 403 + 471 | Account temporarily blocked | Waiting, or contacting Quire | | 429 | The per-minute / per-hour API rate limit, and only this | Backing off; always carries Retry-After | | 400 + 400 | Validation (missing name, duplicate or reserved id) | Fixing the request |

Before that release, plan limits and expired subscriptions both came back as 429, which invited callers to back off and retry a condition that only upgrading could clear. The body-code checks are status-independent, so a client pointed at an older server still gets the right message.

Development

npm install
npm test            # offline / mocked unit tests (CI-default)
npm run typecheck   # tsc --noEmit
npm run build       # tsc → dist/
npm run gen-coverage  # regenerate COVERAGE.md from src/client.ts

Live API tests

A separate, opt-in suite under tests/live/ exercises QuireClient against the real Quire API. It's gated on a configured env file (see tests/live/README.md for setup) and never runs during npm test.

npm run test:live:prepare   # one-time OAuth bootstrap → writes tokens to ~/.config/quire/test-api.env
npm run test:live           # run the full live-API suite

License

MIT © Potix Corporation