Skip to main content
The ticket-rs npm package provides native TypeScript bindings to ticket-rs, giving you full access to issue management and graph analytics from Node.js and Bun. Every visible tk subcommand has a typed TypeScript wrapper, generated from the same clap::Command tree the binary parses against — so the TypeScript surface stays in lockstep with the CLI by construction. Calls go through an in-process napi FFI; no subprocess is spawned.

Installation

The postinstall step downloads (or builds) the prebuilt native module for your platform. No Rust toolchain required for the supported triples below; if your platform isn’t listed, install Rust and the package will compile from source.

Quick Start

import * as tk from "ticket-rs";

// Every visible `tk` subcommand is a top-level function.
// Pass an options object the same way you'd pass flags on the CLI.

// Initialise a tickets directory
tk.init({ dir: "/path/to/your/project" });

// Get issues ready to work on (no open blockers)
const ready = tk.ready({
  dir: "/path/to/your/project",
  format: "json",
}) as Array<{ id: string; title: string }>;

for (const issue of ready) {
  console.log(`${issue.id}: ${issue.title}`);
}

// Get repository statistics
const stats = tk.stats({
  dir: "/path/to/your/project",
  format: "json",
}) as { open: number; closed: number; ready: number };

console.log(`Open: ${stats.open}  Closed: ${stats.closed}  Ready: ${stats.ready}`);

API Reference

ticket-rs exposes three categories of API:
  1. 76 typed wrappers — one per visible tk subcommand. Type signatures come from clap.
  2. napiDispatch(argv) — raw escape hatch for any command, including hidden ones.
  3. commandSpec() — introspect the CLI surface as a nested JSON spec.

Typed wrappers

Each wrapper takes the command’s flags as properties of a single options object. The munging rules:
  • kebab-case becomes camelCase: --dry-rundryRun
  • nested commands are namespaced: tk dep adddepAdd
  • the JS reserved word delete gets a trailing underscore: tk deletedelete_
  • global flags dir, format, json, and strict are accepted on every wrapper
Return value depends on format:
  • format: "json" → parsed JSON (unknown; narrow at the call site)
  • omitted or any other value → the raw stdout string
import * as tk from "ticket-rs";

// Core issue management
tk.create({ title: "Fix the bug", priority: 1, format: "json" });
const issue = tk.show({ id: "tk-abc123", format: "json" });
tk.update({ id: "tk-abc123", status: "in_progress" });
tk.close({ id: "tk-abc123", reason: "fixed" });
const issues = tk.list({ format: "json" });

// Dependencies (nested commands become camelCase)
tk.depAdd({ blocked: "tk-1", blocker: "tk-2" });
tk.depTree({ id: "tk-1" });

// AI/analytics
const triage = tk.triage({ format: "json" });
const priority = tk.priority({ format: "json" });
const plan = tk.plan({ format: "json" });
const nextPick = tk.next({ format: "json" });

// Reserved word `delete` becomes `delete_`
tk.delete_({ id: "tk-abc123" });

// kebab-case flags become camelCase
tk.depAdd({ blocked: "tk-1", blocker: "tk-2", dryRun: true });
The full surface includes every visible tk command — linearSync, githubSync, claudeSync, worktree, stacks, insights, search, similar, validate, lint, and more. Discover them at runtime:
import { TYPED_API_FUNCTIONS } from "ticket-rs";
console.log([...TYPED_API_FUNCTIONS].sort());

napiDispatch(argv)

Raw escape hatch for any command, including hidden ones. Same in-process napi path; you build the argv array yourself.
import { napiDispatch } from "ticket-rs";

// Equivalent to `tk list --format json -C /path/to/repo`
const out = napiDispatch(["list", "--format", "json", "-C", "/path/to/repo"]);

// Returns the raw stdout string. Parse it yourself.
const issues = JSON.parse(out);

commandSpec()

Introspect the CLI surface as a nested JSON spec — useful for building meta-tools, generating other typed bindings, or discovering commands and their flags programmatically.
import { commandSpec } from "ticket-rs";

const spec = JSON.parse(commandSpec());
// spec = { name: "tk", subcommands: [...], global_args: [...] }

// List every visible top-level command
for (const cmd of spec.subcommands) {
  if (!cmd.hidden) {
    console.log(cmd.name, "—", cmd.about ?? "");
  }
}

Error handling

All errors surface as plain Error instances — not TypeError. The split between argv-parse failures and runtime failures is carried on err.code:
err.codeCause
"InvalidArg"argv didn’t parse (unknown subcommand, bad flag, missing required positional). Fix the call.
"GenericFailure"argv parsed fine but the command itself failed (issue not found, parse error in a ticket file, etc.). The system refused.
napi-derive wraps every Err(_) from Rust in a plain JsError (which presents as Error in JS), not a TypeError. Don’t write catch (err) { if (err instanceof TypeError) ... } — that branch will never fire. Use err.code to discriminate.
import * as tk from "ticket-rs";

try {
  tk.show({ id: "tk-doesnotexist" });
} catch (err) {
  // All errors are plain Error instances. Discriminate via .code:
  const code = (err as { code?: string }).code;
  if (code === "GenericFailure") {
    // The command ran but failed — issue doesn't exist
    console.error(`command failed: ${(err as Error).message}`);
  } else if (code === "InvalidArg") {
    // You mis-typed the call — bad flag, missing arg, etc.
    console.error(`bad call: ${(err as Error).message}`);
  } else {
    throw err;
  }
}

Return shapes

When you pass format: "json", wrappers return parsed JSON. The shape mirrors what tk <command> --format json emits on the CLI. The TypeScript type is unknown — narrow with a type guard or zod in your code. (The clap surface doesn’t carry per-command output schemas; pinning them in TS would be a separate source of truth that could drift.)
// list({ format: "json" }) → array of issues
[
  { id: "tk-abc", title: "Fix bug", status: "open", priority: 1 },
]

// triage({ format: "json" }) → dict with nested structure
{
  quick_ref: { total: 42, open: 18, ready: 5 },
  recommendations: [{ id: "tk-abc", score: 87.5, reason: "..." }],
  quick_wins: [],
  blockers_to_clear: [],
}

// stats({ format: "json" }) → flat counts
{ total: 42, open: 18, in_progress: 3, blocked: 2, closed: 19, ready: 5 }
For the exact shape of each command’s JSON, run tk <command> --format json once and inspect.

Migration from the hand-written client

The previous createClient / Client API has been removed — src/client.ts, src/models.ts, and src/batch.ts are gone. Replace client.method(...) calls with the corresponding typed wrapper:
BeforeAfter
createClient().listIssues()tk.list({ format: "json" })
client.readyIssues()tk.ready({ format: "json" })
client.computePriorities()tk.priority({ format: "json" })
client.computeTriage()tk.triage({ format: "json" })
client.generatePlan()tk.plan({ format: "json" })
client.getVersion()tk.version({})
Anything elsenapiDispatch(["<cmd>", ...args])
The migration is mechanical — every old client method maps either to a typed wrapper named after its tk subcommand, or to a raw napiDispatch call.

Examples

Find high-priority ready issues

import * as tk from "ticket-rs";

const ready = tk.ready({ format: "json" }) as Array<{
  id: string;
  title: string;
  priority: number;
}>;

const highPriority = ready.filter((i) => i.priority <= 1);
for (const issue of highPriority) {
  console.log(`[P${issue.priority}] ${issue.id}: ${issue.title}`);
}

Batch create issues

import * as tk from "ticket-rs";

const tasks: Array<[string, string, number]> = [
  ["Set up CI/CD", "task", 1],
  ["Write documentation", "task", 2],
  ["Add unit tests", "task", 2],
];

for (const [title, issueType, priority] of tasks) {
  const out = tk.create({
    title,
    issueType,
    priority,
    format: "json",
  }) as { id: string };
  console.log(`Created: ${out.id}`);
}

Export issues to JSON

import { writeFileSync } from "node:fs";
import * as tk from "ticket-rs";

const issues = tk.list({ format: "json" }) as Array<{
  id: string;
  title: string;
  status: string;
  priority: number;
}>;

const data = issues.map((i) => ({
  id: i.id,
  title: i.title,
  status: i.status,
  priority: i.priority,
}));

writeFileSync("issues.json", JSON.stringify(data, null, 2));

Run a hidden command via the escape hatch

import { napiDispatch } from "ticket-rs";

// Hidden commands aren't surfaced as typed wrappers, but `napiDispatch`
// reaches them by argv. Useful for the MCP server and internal tools.
const out = napiDispatch(["migrate-beads", "--dry-run"]);
console.log(out);

Platform Support

Pre-built native modules are shipped for:
PlatformArchitecturesNode.js Versions
Linuxx86_64, aarch6418+
macOSx86_64 (Intel), arm64 (Apple Silicon)18+
Windowsx86_6418+
If no pre-built module exists for your platform, the postinstall step compiles from source (requires Rust toolchain).

Troubleshooting

The native bindings weren’t installed. If you cloned the repo, run:
cd npm && bun run build
If you installed from npm and the postinstall step failed, reinstall with verbose logging:
bun install --verbose ticket-rs
The hand-written createClient / Client API was removed when the typed_api surface shipped. The src/client.ts, src/models.ts, and src/batch.ts files are gone.
// Before (removed)
import { createClient } from "ticket-rs";
const client = createClient();
client.listIssues();

// After
import * as tk from "ticket-rs";
tk.list({ format: "json" });
See the Migration table above, or use napiDispatch(["list", "-f", "json"]) as the raw escape hatch.
By design. napi-derive wraps every Rust Err(_) in a plain JsError that presents as Error (not TypeError) in JS. Discriminate via err.code:
catch (err) {
  const code = (err as { code?: string }).code;
  if (code === "InvalidArg") {
    // argv parse failure
  } else if (code === "GenericFailure") {
    // runtime failure (e.g. issue not found)
  }
}
See Error handling for the full table.
The package contains a native Node addon. If no pre-built native module exists for your platform, you need:
  1. Rust toolchain: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. Node.js 18+ with headers
Then re-run install:
bun install ticket-rs

Next Steps

MCP Server

Use ticket-mcp for AI agent integration.

CLI Reference

Full command documentation.