@dichovsky/testrail-api-client
v9.0.0
Published
Type-safe ESM TestRail API client and CLI for Node.js
Maintainers
Readme
TestRail API Client
Type-safe TypeScript client and testrail CLI for the TestRail REST API, with a single runtime dependency: Zod. ESM only.
Compatibility is tracked through
TestRail 10.7.0 (Default 1021),
including the cumulative API additions from 10.4–10.7 and the 10.7 repeated
refs[] filters for case and BDD lists. The compatibility surface also covers
BDD/case-title/version discovery, result editing, dynamic filters, label
management, and current run/plan/test filters and scheduling fields. Older
scalar refs queries remain supported.
See the TestRail 10.7.0 compatibility audit for the primary-source corpus, endpoint counts, corrected mismatches, and remaining upstream documentation ambiguities.
Install
npm install @dichovsky/testrail-api-clientRequires Node.js 24+. CI verifies Node 24 and 26 across Linux, Windows, and macOS. Other Node majors satisfy the engine range but are not currently in the CI matrix.
Published declarations are smoke-tested with TypeScript 6 and 7. The repository build and primary type-check use native TypeScript 7; compiler-API-based generators and typed lint tooling use the TypeScript 6 compatibility compiler until the native compiler exposes that API.
Repository verification
After npm ci, run npm run verify to build, run both compiler checks,
lint/format/generated-document checks, execute the coverage suite, and smoke-test
the packed package. npm test runs only Vitest. The repository disables
implicit npm lifecycle hooks, so verification is an explicit command.
Additional release fuzz and registry audit gates are listed in
the release guide.
30-second example
import { TestRailClient } from '@dichovsky/testrail-api-client';
const client = new TestRailClient({
baseUrl: process.env.TESTRAIL_BASE_URL!, // https://your-domain.testrail.io
email: process.env.TESTRAIL_EMAIL!,
apiKey: process.env.TESTRAIL_API_KEY!,
});
try {
const project = await client.projects.getProject(1);
console.log(project.name);
} finally {
client.destroy(); // release timers, clear cache, zero the credential
}The client surfaces the supported TestRail REST API endpoints. See docs/API-MAPPING.md for the endpoint-to-method matrix and CODEMAP.md for exact signatures and file:line locations of every symbol.
Write example
With a live client instance, write payloads are typed from the Zod payload schemas (compile-time only — the SDK does not re-validate them at runtime; the CLI does):
const run = await client.runs.addRun(5, {
suite_id: 12,
name: 'CI build',
include_all: false,
case_ids: [42, 43, 44],
});
await client.results.addResultForCase(run.id, 42, { status_id: 1, comment: 'Passed' });status_id is optional on a new result: it needs at least one of status_id,
comment or assignedto_id, so a comment-only result is valid. Only the CLI
enforces that rule; through the SDK a typed {} compiles and is sent to
TestRail as is.
CLI quick tour
The package ships a testrail binary. Authenticate with environment variables, then read, write, or delete:
export TESTRAIL_BASE_URL="https://your-domain.testrail.io"
export TESTRAIL_EMAIL="[email protected]"
export TESTRAIL_API_KEY="…" # never pass the key on argv
npx testrail project list # read (JSON to stdout)
npx testrail run add 5 --data '{"name":"CI build","include_all":true}' # write (Zod-validated)
# Destructive: needs BOTH the per-invocation --yes flag AND the process-wide env unlock
TESTRAIL_ALLOW_DESTRUCTIVE=1 npx testrail run close 100 --yesPrefer TESTRAIL_API_KEY. If an environment variable is not an option, pipe the key with echo "$KEY" | npx testrail ... --api-key-stdin. That flag consumes stdin, so write bodies must come from --data or --data-file.
--dry-run previews any write or delete client-side with no API call. Output format is selectable with --format <json|table|yaml|csv>. npx testrail <resource> --help (for example npx testrail case --help) lists one resource's commands; npx testrail --help lists them all. See skill/SKILL.md and the command and recipe references beside it for the complete command surface and recipes.
String options require their own value: --filename --dry-run is rejected before
the command runs, including when a later occurrence supplies a valid filename.
For a literal value beginning with --, use the inline form, such as
--filter=--all. Boolean options take no value: pass --dry-run, not
--dry-run=true.
Attachment and BDD downloads accept regular files as --out destinations.
--force permits replacing a regular file; it rejects symlinks, FIFOs, and
devices, including /dev/null. To discard a download, send its payload to
stdout and let the shell discard it:
testrail attachment get 42 --out - > /dev/nullThe JSON acknowledgement still goes to stderr. Use the same --out - pattern
with testrail bdd get <case-id>; do not use --out /dev/null --force.
Features
| Capability | What it does | Documented in |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Response caching | GET-only in-process LRU cache with TTL; any write invalidates it | docs/ARCHITECTURE.md §2.3 |
| Bounded pagination | Preserve page metadata or collect every page with explicit safety limits | Pagination |
| Rate limiting | Sliding-window limiter (default 100 req/60s); rejects over-limit before fetch | docs/ARCHITECTURE.md §2.2 |
| Retry with backoff | Exponential backoff with Retry-After; GET retries 5xx/429/network, JSON writes only 429, multipart uploads never retry | docs/ARCHITECTURE.md §2.4 |
| SSRF guard | Validated DNS addresses bound to each connection, private-host blocking, manual-redirect rejection | docs/ARCHITECTURE.md §2.5 |
| Response-body caps | Byte ceiling + wall-clock deadline on every body read | docs/ARCHITECTURE.md §2.2 |
| Streaming uploads | Attachment uploads stream from disk, so large files don't buffer in heap | docs/ARCHITECTURE.md §2.4 |
| CLI | testrail binary: read / write / destructive actions, four output formats | skill/SKILL.md |
| AI-agent skill | Bundled Claude Code skill; install it with npx testrail install-skill | skill/SKILL.md |
For a project-scoped Claude Code installation, run npx testrail install-skill. Add --global to install it under ~/.claude/skills/. It installs SKILL.md and the reference/ files the body links to; after upgrading the package, re-run it with --force to replace an earlier install.
Note on rate-limit headers. A live-instance check found that TestRail Cloud does not emit rate-limit headers (
Retry-After,X-RateLimit-*) under normal serial load — a burst of requests all returned200with no such headers. The client'sRetry-Afterhandling is therefore dormant in practice and only engages if the server starts sending the header (e.g. under heavy throttling or on a future TestRail version); it is fully covered by synthetic tests. The effective throttle is the client's own sliding-window limiter (rateLimiter, default 100 req/60s), which rejects over-limit requests before they leave the process — tune it to your instance's quota.
Configuration
All options except baseUrl / email / apiKey are optional:
const client = new TestRailClient({
baseUrl: 'https://your-domain.testrail.io',
email: '[email protected]',
apiKey: 'your-api-key',
timeout: 30000, // request timeout (ms)
maxRetries: 3, // retry attempts for retryable failures
enableCache: true, // cache GET responses
cacheTtl: 300000, // cache TTL (ms)
rateLimiter: { maxRequests: 100, windowMs: 60000 },
});| Option | Type | Default | Description |
| ------------------------- | ------------------- | ------------------ | ------------------------------------------------------ |
| baseUrl | string | required | HTTPS TestRail URL; HTTP requires allowInsecure |
| email | string | required | TestRail user email (validated format) |
| apiKey | string | required | TestRail API key |
| timeout | number | 30000 | Per-attempt timeout in ms (max 5 min); covers DNS |
| maxRetries | number | 3 | Max retry attempts for failed requests; integer 0-10 |
| enableCache | boolean | true | Enable caching for GET requests |
| cacheTtl | number | 300000 | Cache time-to-live in milliseconds |
| cacheCleanupInterval | number | 60000 | Integer 0–2,147,483,647 ms; 0 disables cleanup |
| maxCacheSize | number | 1000 | Maximum number of entries in cache |
| rateLimiter | RateLimiterConfig | 100 / 60s | { maxRequests, windowMs } sliding window |
| allowInsecure | boolean | false | Permit cleartext HTTP (credentials sent in Base64) |
| allowPrivateHosts | boolean | false | Permit private/loopback/link-local hosts |
| maxJsonResponseBytes | number | 10485760 | JSON/text response body cap (10 MiB; ceiling 1 GiB) |
| maxBinaryResponseBytes | number | 104857600 | Binary response body cap (100 MiB; ceiling 1 GiB) |
| bodyTimeout | number | = timeout | Wall-clock deadline for the body read (0 disables) |
| registerProcessHandlers | boolean | false | Install exit/SIGINT/SIGTERM handlers (opt-in) |
| fetch | typeof fetch | globalThis.fetch | Trusted transport; honor the supplied dispatcher |
| dnsLookup | function | system DNS | Custom resolver for SSRF checks and connection pinning |
| onSchemaMismatch | function | none (silent) | Notified when a response does not match its schema |
Library consumers should leave registerProcessHandlers off and call client.destroy() from their own shutdown hook. The testrail CLI opts in on your behalf.
To retain a concurrency slot or staged upload until driver work has actually
finished, use trackOperation:
const operation = client.trackOperation(() => client.projects.getAllProjects({ maxDurationMs: 1000 }));
try {
const projects = await operation.result;
console.log(projects.length);
} finally {
await operation.settled;
// Release the operation's slot or staged files here.
}result preserves the normal method value, error, and deadline. settled
always resolves, after the callback and every started or joined driver task
finish, including DNS, fetch, response reads and cancellation, upload streams,
retries, and shared requests. A deadline can reject result while settled
is still pending. An abort request alone does not release ownership; custom
transports or cancellation that never complete keep it pending. Nested
operations are included. Return the promise for your complete callback workflow;
unawaited application timers are outside the driver's accounting.
trackOperation does not add cancellation, and destroy() does not abort
in-flight work or settle its handles. Settlement tracking switches on with the
first trackOperation call and stays on. Multipart uploads also enter a separate
async context to identify their native transport requests, even without
trackOperation. A process that neither tracks operations nor uploads avoids
the library's context-propagation cost. A request already in flight at the first
trackOperation call can be joined for its result but not for its post-result
cleanup.
Multipart cleanup requests abort and then requires transport evidence before
closing active upload streams normally. The pinned dispatcher or native request
diagnostics can provide that evidence. When an injected or globally replaced
fetch supplies neither, cleanup errors the source instead, preventing an ignored
abort from sending a truncated file with a valid multipart closing boundary.
Pending source reads and cancellation still delay settled on every path.
A cache hit is isolated, not free. Entries are deep-copied with
structuredClone on both write and read, so a cached caller can mutate what it
receives without corrupting the entry the next caller will get. The cost scales
with payload size: a hit on a 250-case getCases() page copies that whole
payload again. This is the right trade for correctness, but it means
maxCacheSize bounds entry count, not memory — size it against the responses
you actually cache, and lower maxJsonResponseBytes (default 10 MiB) if a
bulk-export endpoint would otherwise pin large bodies for the full TTL.
reports.runReport() and reports.runCrossProjectReport() generate a new
report for each call. Although the API routes use GET, these methods bypass
cache reads, writes, and request coalescing. A 5xx or network failure is never
retried — the report may already have been generated and the template may have
sent email, so re-running an uncertain outcome must be your own explicit new
invocation. A 429 is retried (honoring Retry-After), because the rate limiter
rejects the request before execution.
By default, the host guard rejects private, loopback, link-local, CGNAT,
benchmarking (198.18.0.0/15), multicast, and reserved IPv4 addresses,
including their IPv4-mapped IPv6 spellings, plus IPv6 transition ranges
such as 6to4 and the well-known and local-use NAT64 prefixes. Literal URLs and
DNS answers use the same address classifier. The default transport connects directly to
the validated addresses without a second DNS lookup, preserving the original
hostname for TLS certificate checks and SNI. Injected fetch implementations
are trusted: they must honor the supplied Node dispatcher or enforce equivalent
destination checks, abort signals, and manual redirects. On-premise SDK deployments that
need these addresses must explicitly set allowPrivateHosts: true; this also
disables DNS host validation and connection pinning. See the host guard details
for the exact ranges.
Concurrent GETs share an in-flight request only when their effective header and body timeouts match. Timeout views still share completed cached responses.
Custom DNS resolvers
Each dnsLookup answer must contain a valid IP literal and its matching numeric
family: 4 for IPv4 or 6 for IPv6. Missing families, 0, and mismatches now
fail before any request is sent, even when the address itself is public. Use
the system resolver's complete answer objects, or supply the correct family
when adapting another resolver:
import { lookup } from 'node:dns/promises';
const client = new TestRailClient({
...config,
dnsLookup: (hostname) => lookup(hostname, { all: true }),
});
// A fixed IPv4 answer has this shape:
// dnsLookup: async () => [{ address: '203.0.113.10', family: 4 }]
// IPv6 answers must use family: 6.Proxies and custom certificate authorities
With allowPrivateHosts: false (the default), the client now supplies a direct
dispatcher on each request. This is a breaking change for applications that
previously relied on Undici's setGlobalDispatcher() with a ProxyAgent,
EnvHttpProxyAgent, or custom TLS settings. It also bypasses
NODE_USE_ENV_PROXY=1 / --use-env-proxy; the private HTTP/HTTPS agents do not
inherit http.globalAgent or https.globalAgent customization. Node documents
the separate per-request dispatcher
and global proxy configuration.
For additional CA trust on direct connections, set
NODE_EXTRA_CA_CERTS
to a PEM bundle before starting Node. This preserves certificate and hostname
verification while retaining DNS pinning; a custom global agent's ca setting
is not copied into the client's agents.
For a required proxy, inject an application-owned fetch adapter that explicitly selects that proxy and preserves the supplied abort signal and manual-redirect policy. Replacing the dispatcher also replaces connection pinning: the client's local DNS check cannot verify a second lookup performed by the proxy. Before using this migration, configure the trusted proxy to permit only the approved TestRail origin and enforce the same private-address restrictions on its actual connection destination. An origin allowlist alone does not prevent DNS rebinding.
For example, after establishing that proxy policy, an application that already uses Undici's ProxyAgent can select it explicitly:
import { ProxyAgent } from 'undici'; // Application dependency; not bundled by this client.
const approvedOrigin = 'https://your-domain.testrail.io';
const proxy = new ProxyAgent('https://approved-proxy.example:8443');
const proxyFetch: typeof globalThis.fetch = (input, init) => {
const url = new URL(input instanceof Request ? input.url : input);
if (url.origin !== approvedOrigin) throw new Error('Unexpected TestRail origin');
return globalThis.fetch(input, { ...init, dispatcher: proxy });
};
const client = new TestRailClient({
...config,
baseUrl: approvedOrigin,
allowPrivateHosts: false,
fetch: proxyFetch,
});
try {
console.log(await client.projects.getProjects());
} finally {
client.destroy();
await proxy.close();
}The injected adapter and proxy now own destination enforcement. If that policy
cannot be established, this package has no safe automatic proxy fallback.
allowPrivateHosts: true restores the configured fetch's connection handling
but disables both DNS validation and pinning; it is not a proxy fix. Reserve it
for intentionally trusted on-premise destinations with separate network controls.
Pagination
The 24 endpoints in the pagination registry expose three projections. Existing
methods keep their backward-compatible behavior: get*() performs one request
and returns that response's item array. get*Page() performs one request and
returns a discriminated Page<T> with the server's offset, limit, size,
and _links when an envelope was returned. getAll*() follows every response
continuation and returns one concatenated array:
const firstResponse = await client.runs.getRuns(5); // Run[]; one response
const page = await client.runs.getRunsPage(5, { limit: 25, offset: 50 }); // Page<Run>
const all = await client.runs.getAllRuns(5, { pageSize: 100, maxItems: 10_000 }); // Run[]An envelope's _links.next decides whether another request is required. The
client extracts only a validated offset and optional limit. Each descriptor
declares its allowed operation names; its adapter supplies one selected
operation, validated numeric path parameters, and normalized filters. The
shared executor rejects undeclared operations and rebuilds the known TestRail
endpoint itself, so it never follows the continuation's host or path. A legacy
bare-array response is necessarily one terminal page. All-page reads bypass GET
cache reads, writes, and request coalescing so one aggregate cannot combine
pages captured at different times. They also do not enumerate wider runtime
option objects: only declared filters and public aggregate controls are read,
while injected limit/offset, internal collector fields, and unrelated
properties are ignored. A page read uses normal GET caching in a separate
validated namespace, so a permissive legacy one-response wrapper cannot poison
the stricter Page<T> projection.
Aggregation is fail-closed and never returns partial results. Defaults are a
page size of 250, start offset 0, 100 pages, 25,000 items, five minutes, and
100 MiB of UTF-8 serialized items. Page size is capped at 250, duration at five
minutes, and the byte bound at 1 GiB. A safety, continuation, or page-structure
failure throws TestRailPaginationError with reason, pagesFetched, and
itemsFetched. Reasons are max_pages, max_items, max_duration,
max_bytes, invalid_page, invalid_continuation, and non_progress.
Registry scope is deliberately finite: cases, case history, and project BDDs;
projects, suites, sections, plans, runs, tests, and milestones; the three result
lists; labels; shared-step lists and history; case/run/plan attachment lists;
datasets, variables, roles, groups, and case statuses. Shared-step history,
datasets, variables, roles, groups, and case statuses expose response-driven
pagination but no caller-controlled page size or start offset. Test
attachments, plan-entry attachments, users, and ordinary
metadata/configuration/report lists remain one-response-only; some legacy
methods may still accept limit/offset, but that is not a
get*Page()/getAll*() guarantee.
The CLI mirrors the projections. Its default output remains an item array;
--page emits the normalized page object, and --all emits the complete
array. --all uses --page-size, --start-offset, --max-pages,
--max-items, --max-duration-ms, and --max-bytes. It cannot be combined
with --page, --limit, or --offset; aggregate controls require --all.
Response-driven endpoints reject caller-controlled size/offset flags.
Response types are a description, not a guarantee
Since 6.0.0, Zod response validation is advisory. When entity fields do not match their schema, the client normally returns the raw body unchanged and notifies onSchemaMismatch; the Zod mismatch alone does not throw. With the default or another non-throwing hook, domain methods still enforce list/page outer structure so a malformed collection cannot masquerade as a successful empty result. A caller-supplied hook that throws takes precedence over downstream structural decoding.
In non-throwing advisory mode, the hard protocol exceptions are structural
rather than entity-field drift. A malformed default list response throws
TestRailApiError; explicit page/all reads throw TestRailPaginationError. An
unrecognized successful response from cases.addCases() or
cases.updateCases() also throws TestRailApiError with an explicit “write
outcome is indeterminate” message. Those non-idempotent writes may already have
changed server state, so reporting an empty result could prompt a duplicate
retry.
The CLI makes advisory mismatches visible without copying the response into
logs. By default it writes at most 10 unique, deduplicated warnings to stderr,
then a safe suppressed-count summary. Warnings contain only the request method,
CLI resource/action, Zod issue codes, and shape-only paths whose segments are
all masked as *; they never include the endpoint, field/record keys, issue
messages, or raw response. Pass
--strict-responses or set TESTRAIL_STRICT_RESPONSES=1 to stop at the first
mismatch with exit code 1. Read mismatches use TestRailValidationError; a
successful mutating request with a mismatched response uses a privacy-safe
TestRailApiError that says the write outcome is indeterminate, and must not be
retried blindly. One-shot commands emit no value for the mismatched response,
and bounded aggregates emit no partial array. A streaming run watch may
already have emitted completed earlier polls before a later mismatch; those
events cannot be retracted. The environment variable accepts only 1, 0, an
empty value, or unset; any other value is rejected before authentication or a
network request. The flag is boolean-only, so forms such as
--strict-responses=true are rejected instead of guessed. --quiet suppresses
advisory warnings as well as normal output.
This is deliberate. TestRail's published API documentation is not a reliable description of what the API actually sends: it documents a {step_history} wrapper for an endpoint that returns a bare array, a boolean mfa_required that arrives as integer 0, and an is_untested field on the wrong endpoint entirely. Because list endpoints validate a whole page at once, a single unmodelled row used to discard up to 250 valid ones — so strict validation reliably converted a working response into an outage, and never once caught a server-side regression.
Two consequences worth knowing:
- Exported response types state the expected shape, not a runtime guarantee. A field typed
numbercan hold whatever TestRail sent. Fields are widened to match reality as wire evidence arrives, in any release — pin an exact version if you need frozen types. - Caller-supplied input still fails closed. Client configuration and CLI write payloads are validated on a separate path and reject invalid input as before.
- The hook must be synchronous, and what it logs can contain personal data. An
asynchook cannot restore fail-closed validation and is rejected withTestRailValidationError; see the comment in the example below before loggingendpointordata.
Public response types are derived from the declared Zod response shapes, so a
runtime .passthrough() no longer leaks a broad [key: string]: unknown into
every entity. Flat response custom fields are modeled only where TestRail emits
them: Case, Test, and Result support bracket access such as
test['custom_browser'], typed unknown; narrow before use. Their older
custom_fields container remains deprecated for compatibility. Stable fields
added in 6.0 include Test.refs_data/case_title,
Result.case_title/case_refs,
CaseField.is_indexed/is_system, and ResultField.is_system; milestone
children are now recursively typed.
// Observe drift without changing behavior.
// Never log `endpoint`, `error`, or `data` wholesale. Endpoint path/query values
// and the raw body can contain personal data, while Zod paths can contain
// response-controlled record/catchall keys. Keep only the operation token,
// issue code, and path depth; mask every path segment.
const client = new TestRailClient({
...config,
onSchemaMismatch: ({ method, endpoint, error }) =>
log.warn({
method,
operation: endpoint.replace(/[\/&].*$/, ''),
issues: error.issues.map(({ code, path }) => ({
code,
path: path.length === 0 ? '$' : `$.${path.map(() => '*').join('.')}`,
})),
}),
});
// Or restore strict, fail-closed validation — useful in CI.
// `handleZodError` reproduces the exact TestRailValidationError older versions
// threw, so existing `instanceof` handlers keep matching; throw `error` as-is
// if you would rather have the raw Zod issue tree.
import { handleZodError } from '@dichovsky/testrail-api-client';
const strict = new TestRailClient({
...config,
onSchemaMismatch: ({ error }) => {
throw handleZodError(error);
},
});Error handling
The client exposes three primary error classes:
import { TestRailApiError, TestRailPaginationError, TestRailValidationError } from '@dichovsky/testrail-api-client';
try {
await client.projects.getProject(999);
} catch (error) {
if (error instanceof TestRailApiError) {
// HTTP/network/protocol failure, including a malformed successful default-list response
console.error(error.status, error.statusText, error.response);
} else if (error instanceof TestRailPaginationError) {
// Bounded aggregation or structural page/continuation failure
console.error(error.reason, error.pagesFetched, error.itemsFetched);
} else if (error instanceof TestRailValidationError) {
// Bad config, invalid ID, or invalid params
console.error(error.message);
}
}TestRailApiError carries status, statusText, and response (the raw body
lives only in response, never in message). In non-throwing advisory mode, it
also represents an unrecognized successful outer structure from a default list
read or non-idempotent bulk case write. CLI strict mode uses the same class for
any successful mutating request whose response mismatches, with an indeterminate
outcome message and no raw response attached. TestRailPaginationError
extends TestRailValidationError; catch it first when its structured reason
matters. Ordinary entity-field schema mismatches remain advisory. Explicit
page/all projections require complete envelope metadata, valid links, and a
safe continuation. A throwing SDK mismatch hook takes precedence over these
downstream decoders; never treat its error as proof that a write did not happen.
Other TestRailValidationErrors signal caller mistakes — bad config, an invalid
ID, or an invalid parameter. Calling any method after destroy() throws a plain
Error.
For list filters that carry numeric IDs, validation also happens before any request is sent. Arrays such as createdBy, statusId, and milestoneId must contain positive integers; invalid values fail locally with TestRailValidationError instead of reaching the API.
For CLI error details, opt into a private diagnostic file on the original invocation:
testrail case-field add --data-file field.json --diagnostic-file ./field-error.jsonFor real invocations, --diagnostic-file reserves a new regular file before the command handler runs and rejects existing
paths, including symlinks, and destinations shared with --out. Its directory must
already exist. The file has mode 0600. The flag currently rejects Windows before
stdin or authentication work because the client cannot establish an equivalent private ACL there.
--dry-run ignores the diagnostic destination on every platform: previews create no
diagnostic file and do not validate or modify an existing destination.
On macOS, the client clears inherited ACLs on a private staging directory before
creating the file, then reserves the requested destination with an exclusive link
to that private file. Bounded calls to the system chmod secure the staging
directory and remove ACLs added to the file before writing diagnostics. The
destination directory's ACL is preserved. If securing the file fails, the client
rejects the destination or omits the diagnostic record.
Normal completion removes an unused reservation; process exit also attempts cleanup,
including exits caused by SIGINT/SIGTERM. A permission or I/O failure can leave an empty
reservation; the command's exit status still describes the operation, and --quiet
suppresses cleanup warnings. File existence alone does not establish API failure.
Failures after reservation produce a
version-1 JSON record with kind, HTTP status when available, an explicit
operationOutcome, and server containing state,
messages, and truncated. Early argument/auth failures have no diagnostic artifact.
operationOutcome is "not_dispatched" for a known failure before invoking the
command handler, such as invalid client configuration. Once the handler starts it
is conservatively "failed_or_indeterminate"; the error's class alone cannot prove
whether an API write happened.
Only recognized structured validation details are included. Known credentials and
common encoded variants are redacted; sensitive nested keys are omitted. Raw bodies,
headers, request payloads, and stacks are excluded. messages contains nonblank redacted
string values in encounter order, without object keys or field-to-message mappings.
Input processing is capped at
64 KiB and the record at 16 KiB; malformed/non-JSON or oversized bodies yield a safe
omission state. When extracted messages exceed the record limit, the first complete
redacted messages are retained with truncated: true. Safe validation text preserves
the server's original escaping. Diagnostic failures preserve the command's exit status and report
that the file could not be saved or cleaned up. --quiet also suppresses these
warnings. Default output and request/retry behavior are unchanged. Never replay a
write just to obtain diagnostics.
After successful custom-field creation, inventory visibility can lag. See the GET-only readiness guide for bounded, cancellable polling that retains the creation result and verifies scope/options before dependent case writes.
Links
- CHANGELOG.md — release notes and migration guidance
- docs/RELEASING.md — maintainer release and post-release checklist
- docs/API-MAPPING.md — endpoint ↔ client method ↔ CLI command ↔ skill recipe matrix
- CODEMAP.md — every symbol with exact
file:linelinks - docs/ARCHITECTURE.md — how the layers are organized and why
- AGENTS.md — vendor-neutral guidance for AI coding agents
License
MIT — see LICENSE. If this saved you time, you can buy me a coffee.
