@zentao-agent/sdk
v0.17.0
Published
TypeScript SDK for ZenTao 20.x — HTTP client with token auto-refresh, composite operations, plus credentials/keychain helpers shared by @zentao-agent/cli.
Readme
@zentao-agent/sdk
TypeScript SDK for ZenTao 20.x (open source edition). It wraps the /api.php/v1 REST API with a token-refreshing HTTP client, a typed resource surface (client.bugs, client.tasks, ...), composite operations that paper over ZenTao's inconsistent filters, and the credentials / keychain helpers shared with @zentao-agent/cli.
- Zero runtime dependencies — built on the global
fetchin Node 18+. - ESM only, ships type declarations.
- Password → token exchange and automatic re-login when a token expires mid-flight.
This is the library half of zentao-agent. If you want the
ztcommand line, the agent integrations, and the built-in MCP server, install@zentao-agent/cliinstead.
Install
npm install @zentao-agent/sdkQuick start
import { ZentaoClient } from "@zentao-agent/sdk";
// Username + password: the client logs in on the first request and
// re-logins once when a request comes back 401.
const client = new ZentaoClient({
baseUrl: "https://zentao.example.com",
username: "alice",
password: process.env.ZENTAO_PASSWORD,
});
// Or bring your own token (no auto-refresh — a token-only client cannot
// re-login when the token expires).
const readonly = new ZentaoClient({
baseUrl: "https://zentao.example.com",
token: process.env.ZENTAO_TOKEN,
});
const bug = await client.bugs.get(9634);
const page = await client.bugs.listByProduct(27, { limit: 20, page: 1 });ZentaoConfig accepts baseUrl (required), username, password, token, timeoutMs (default 15000), and onTokenRefreshed(token) — a hook called after every successful (re-)login so you can persist the fresh token. Errors thrown inside the hook are swallowed; caching a token must never break the request that triggered it.
Transient failures (network errors, HTTP 429, HTTP 5xx) are retried with exponential backoff plus jitter, honouring Retry-After when the server sends one. retries (default 2) and retryBaseDelayMs (default 300) tune it; set retries: 0 to disable. Only replayable methods — GET, HEAD, DELETE — are retried, and a request that hit its own timeout is never replayed.
Reusing zt login credentials
If the machine already ran zt login, loadZentaoConfig() resolves everything from the standard sources — environment variables first, then the credentials file, then the OS keychain for the password:
import { createClientFromConfig, loadZentaoConfig } from "@zentao-agent/sdk";
const cfg = loadZentaoConfig(); // throws with an actionable message if nothing is configured
const client = createClientFromConfig(cfg); // wires onTokenRefreshed back to the profile it came fromloadZentaoConfig({ env, credentialsPath, profile }) lets you override each source; createClientFromConfig(cfg, overrides) takes a Partial<ZentaoConfig> for anything you want to change (a different timeout, a different refresh sink).
Environment variables
| Variable | Meaning |
| ---------------------------- | ----------------------------------------------------------------------------- |
| ZENTAO_URL | Site base URL, without the /api.php/v1 suffix |
| ZENTAO_TOKEN | Pre-issued token; wins over the credentials file and disables file write-back |
| ZENTAO_USERNAME | Account used to log in, and to match "assigned to me" in composite operations |
| ZENTAO_PASSWORD | Password used for login and for auto-refresh when the token expires |
| ZENTAO_PROFILE | Which profile to pick from the credentials file |
| ZENTAO_TIMEOUT_MS | Per-request timeout in ms (positive number; default 15000) |
| ZENTAO_AGENT_CREDENTIALS | Override the credentials file path (default ~/zentao-agent/credentials.json) |
Environment variables take priority over the credentials file, so CI setups stay explicit.
Resource surface
Every resource is eagerly constructed on the client and maps 1:1 onto REST endpoints. Query objects are passed through as the query string ({ limit, page, ... }).
| Accessor | Methods |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| client.bugs | listByProduct listByExecution get create update resolve close confirm activate assignTo delete getActions getHistory |
| client.stories | listByProduct get create update change review close activate assignTo delete |
| client.tasks | get listByExecution create update start finish pause activate close cancel assignTo delete |
| client.products | list get create update delete listBranches listModules listPlans listExecutions listTestSuites listTestTasks |
| client.executions | list get create update start close delete listTasks listStories listTeam |
| client.projects | list get create update delete listExecutions listProducts |
| client.programs | list get create delete |
| client.users | list get listDepartments listByDepartment |
| client.todos | list get create update finish delete |
| client.efforts | create list update delete |
| client.builds | listByProduct listByExecution get create delete |
| client.releases | listByProduct get create delete |
| client.cases | listByProduct get create update delete createResult listResults |
| client.testreports| listByProduct get create delete |
| client.issues | listByProject get create update resolve close delete |
| client.risks | listByProject get create update delete |
| client.docs | listLibs listByLib get create update delete |
| client.repos | list get listBranches listCommits |
For endpoints without a wrapper, call the client directly — client.get(path, query), client.post(path, body, query), client.put(...), client.delete(...). Other client members:
client.baseUrl— the site base (no/api.php/v1), for resolving attachment URLs that live outside the REST namespace.client.getAccount()— the authenticated account, from the configured username orGET /user.client.downloadToFile(urlOrPath, destPath)— authenticated binary download; accepts an absolute URL or a site-relative path.ZentaoClient.fetchToken(baseUrl, account, password, timeoutMs?)— static, one-shot password → token exchange that mutates nothing.
Composite operations
Endpoint-level calls are not always enough: ZenTao 20.x silently ignores some v1 query filters, and history lives at a different path depending on the build. These helpers take the client as their first argument.
import {
downloadBugAttachments,
getBugHistory,
listMyBugs,
listMyTasks,
} from "@zentao-agent/sdk";
// Pages through the product and filters locally, because ?assignedTo= is
// unreliable. Returns { bugs, total, scanned, truncated, account, totalInProduct }.
const { bugs } = await listMyBugs(client, { productId: 27, status: "active", limit: 20 });
// Same shape for one execution: { tasks, total, scanned, truncated, account, totalInExec }.
const { tasks } = await listMyTasks(client, { executionId: 155, status: "doing" });
// Tries /bugs/{id}/actions, then /bugs/{id}/history, then whatever is embedded
// in the bug itself. `source` says which one answered.
const { source, data } = await getBugHistory(client, 9634);
// Uploads from bug.files plus, by default, inline <img> in the reproduction
// steps. Per-file failures land in `errors` instead of aborting the batch.
const result = await downloadBugAttachments(client, {
bugId: 9634,
destDir: "/abs/path/bugs/9634/attachments",
includeInline: true,
});Both list helpers accept limit (default 20) and maxScan (default 1000, the paging budget) and report truncated when the scan stopped early.
The payload-shaping helpers behind them are exported too: extractList and extractTotal (ZenTao envelopes vary between {bugs: []}, {data: []}, and bare arrays), summarize(obj, keys) with the ready-made BUG_SUMMARY_KEYS / TASK_SUMMARY_KEYS, pickDefined, normalizeFileList, and sanitizeFilename.
Credentials and keychain
The same helpers the CLI uses, for tools that want to read or write the shared login state:
resolveCredentialsPath(explicit?)— the credentials file path, honouring$ZENTAO_AGENT_CREDENTIALSand the pre-rename~/zentao-hublocations.readStore/readStoreSync/writeStore/deleteStore— theCredentialStorefile (schemaVersion: 2, named profiles; v1 files are migrated on read).pickProfile(store, requested?)andresolveProfileName(explicit, credsPath)— profile selection: explicit request, then the single-profile shortcut, thenstore.default(DEFAULT_PROFILE_NAME).updateStoredToken(profile, token, credentialsPath?)— atomic write-back after a refresh; returnsfalseinstead of throwing.detectBackend()/tryGetSecret(baseUrl, username)/accountKey(baseUrl, username)— OS keychain access (macOSsecurity, Linuxsecret-tool, Windows DPAPI via PowerShell). Passwords are never written to the credentials file.
Errors
Failed requests throw ZentaoApiError with status (0 for network/timeout failures) and the parsed body when the server sent JSON:
import { ZentaoApiError } from "@zentao-agent/sdk";
try {
await client.bugs.get(1);
} catch (err) {
if (err instanceof ZentaoApiError && err.status === 404) return null;
throw err;
}License
MIT © Deven Liu
