@devtune/client
v0.4.0
Published
Project-scoped JavaScript and TypeScript client for the DevTune public API
Readme
@devtune/client
A project-scoped JavaScript and TypeScript client for DevTune's public API. Every method and request/response type comes from the committed public OpenAPI. The runtime uses fetch and has no dependencies.
Installation
npm install @devtune/clientUse ESM in Node 20+ or a modern fetch-capable runtime. Keep API keys on the server,
outside browser bundles. This is an API consumer client; @devtune/ai-traffic is
the separate traffic ingestion package.
import { createClient } from '@devtune/client';
const apiKey = process.env.DEVTUNE_API_KEY;
const projectId = process.env.DEVTUNE_PROJECT_ID;
if (!apiKey || !projectId) throw new Error('Set DEVTUNE_API_KEY and DEVTUNE_PROJECT_ID');
const client = createClient({ apiKey, projectId });
const citations = await client.getCitationStats({ query: { windowDays: 30 } });
console.log(citations.data?.coverage, citations.data?.stats);Create the Bearer project API key in API Keys in the account sidebar. Its
project must match projectId. Select the read/write scopes needed for your
operations. Existing keys do not automatically gain scopes added later.
OAuth access tokens and async token providers require SDK 0.2.0 or later and are limited to the CLI-supported OAuth endpoints. SDK 0.1.x supports project API keys. The SDK does not perform browser login or persist credentials.
Operations and types
Method names are OpenAPI operationIds, such as listActions, getTrafficSummary,
getManagedAgentRun, and listOutcomesLedger. Your editor lists every operation.
Pass resource IDs in path, filters in query, and JSON in body.
The configured project cannot be overridden per call.
import type { OperationInput, OperationResponse } from '@devtune/client';
const input: OperationInput<'getManagedAgentRun'> = { path: { runId: 'RUN_UUID' } };
const run: OperationResponse<'getManagedAgentRun'> = await client.getManagedAgentRun(input);
console.log(run.data.status);OperationId, ProjectClient, ClientOptions, RequestOptions, and the raw
generated operations, paths, components types are also exported. Types retain
the OpenAPI's required and optional properties. The client does not validate every
schema at runtime. The API validates UUIDs, ranges and cross-field constraints.
Responses are returned as documented, usually { data, meta }. The client does
not unwrap data, rename fields, fill in missing metadata, or replace nulls.
A success with no documented response schema, such as changeRetrievalIntent,
returns unknown. Narrow its value before using it. Explicitly bodyless successes
return void, including HTTP 204/205 or an explicitly empty content map. An
unspecified response may contain JSON or be empty. Declared JSON content requires
valid nonempty JSON, even when its schema is unspecified. Unexpected success
statuses, malformed nonempty JSON and bodies on explicitly bodyless successes
raise DevTuneProtocolError.
Response history
For retained answers beyond the latest 20 successful executions, use
listVisibilityResponseHistory and getVisibilityResponse. History pages all
lifecycle statuses in a fixed 30/90-day started-at window, with exact prompt,
platform and status filters. Preserve data.window, data.population, nulls and
continuation. Export row counts are not visibility denominators. The
CLI adds a bounded responses export workflow using this SDK, with NDJSON
completion and page replay recovery.
Timeouts and errors
import { DevTuneHttpError, DevTuneNetworkError, DevTuneAbortError } from '@devtune/client';
try {
await client.getProjectUsage(undefined, { signal: AbortSignal.timeout(5000) });
} catch (error) {
if (error instanceof DevTuneHttpError) {
console.error(error.status, error.body?.error, error.body?.reason, error.retryAfterMs);
} else if (error instanceof DevTuneNetworkError || error instanceof DevTuneAbortError) {
console.error(error.message);
} else {
throw error;
}
}The default total call timeout is 30 seconds, including response body reading,
retries and waits. Set timeoutMs on the client or a call, up to 300000 ms.
Cancellation throws DevTuneAbortError with reason cancelled or timeout.
Invalid success JSON or a response contradicting the documented success contract
throws DevTuneProtocolError. An HTTP error exposes its
status, operation ID, attempts, sanitized documented error body, retry delay and
rate limit values. rateLimit.resetAt is Unix seconds. Raw URLs, request headers,
network causes and arbitrary response fields are not retained. The SDK never logs.
GET requests retry network failures and HTTP 408, 429, 500, 502, 503 and 504 up to
twice. Set maxRetries from 0 to 5 to change that read budget. Backoff starts at
250 ms and doubles to a maximum of 4 seconds. Retry-After seconds or HTTP dates
take precedence over X-RateLimit-Reset Unix seconds, and a retry never happens
before that wait. If the required wait exceeds the remaining timeout, the HTTP
error is returned immediately so you can schedule a later read.
POST, PATCH, PUT and DELETE are never automatically retried, even with an
idempotency key. A lost response or timeout does not prove a write failed. For a
managed-agent launch, reuse the saved requestIdempotencyKey with the same agent
and inputs if you explicitly retry. Another key can create another charge.
Use baseUrl to override the API root, including /api/v2, and fetch to inject
a compatible implementation. Redirects are refused. No authentication discovery,
account discovery, write retry opt-in is added.
Recipes
The package includes TypeScript recipes in recipes/. Copy the directory into
an ESM project with this package installed, then run with a TypeScript runner:
npx tsx recipes/metrics.tsAlternatively, install TypeScript and @types/node, compile with
npx tsc --module nodenext --target es2022 --outDir build recipes/*.ts,
and run node build/metrics.js.
All recipes require DEVTUNE_API_KEY and DEVTUNE_PROJECT_ID. Read recipes make
authenticated reads when run; they are not executed against production by tests.
| Recipe | Inputs and behavior |
| --- | --- |
| actions-report.ts | Reads up to 10 pages of Actions with evidence context. Throws if the report is incomplete. Requires actions.read. |
| metrics.ts | Reads 30-day citation stats and AI Traffic, retaining coverage, window and sensor metadata. Requires citations.read and traffic.read. |
| managed-agent.ts | Requires DEVTUNE_APPROVED_METERED_RUN=true, DEVTUNE_AGENT_KEY, DEVTUNE_AGENT_INSTRUCTIONS, and a persisted UUID in DEVTUNE_RUN_IDEMPOTENCY_KEY. Launches once and polls at most 20 times within two minutes. Requires agents.run and agents.read, an eligible key creator with project manage permission, available evidence and credits. |
| action-results.ts | Requires an existing measurement's DEVTUNE_INTERVENTION_ID. Reads 90-day Results and completion-based impact windows. Requires outcomes.read. |
The managed-agent recipe can spend credits. Run it only for an explicitly intended
task. It prints the task ID before waiting. If waiting ends or is cancelled, resume
getManagedAgentRun with that ID. Cancelling a client request does not stop the
server run. Draft output still needs human approval before publishing.
Windows and coverage
Most measurement operations accept fixed 30 or 90 day rolling windows. Results
ledger windows also support 7 and 365 days, while individual impact comparisons
have their own returned windows. Use each operation's typed windowDays and the
server's returned dates. Do not invent date filters or calculate rates from the
latest-20 visibility response list.
stats: null with unavailable citation coverage means no measurement. Zero means
a measured zero only when returned coverage supports it. AI Traffic sensor state
is project-wide even when totals are filtered to a domain or path. A quiet healthy
sensor is no_recent_traffic; inactive needs attention. Snippet coverage does
not establish server-side answer fetch or 404 coverage. Keep sensor state and
returned evidence with your totals.
Results show observed before/after movement, not proof that an action caused it.
Keep unmeasured, still_measuring, null deltas, confidence and evidence fields.
Do not substitute zero for a missing conversion or an unfinished window.
Pagination
Every paged list takes cursor and returns data.pagination.hasMore and
data.pagination.nextCursor. Page size is limit or pageSize; the two are the
same setting. Pass nextCursor back as query.cursor with the other parameters
unchanged. A cursor sent with different filters returns 400. Where a list reads a
published generation or snapshot, the cursor carries it, so a later page never
mixes two publications.
paginate follows the cursor for you. It needs a bound, and it reports whether
it read the whole list:
import { paginate } from '@devtune/client';
const report = await paginate(
client,
'listActions',
{ query: { status: 'suggested', pageSize: 100 } },
{ maxPages: 10, items: (page) => page.data.actions ?? [] },
);
report.items; // rows from every page read
report.complete; // false when maxPages or maxItems stopped the read
report.nextCursor; // pass as query.cursor to continue an incomplete readSet maxPages, or maxItems with an items selector; reading stops after the
page that reaches either bound. paginate accepts only operations that take a
cursor. It throws DevTunePaginationError when a page has no pagination, reports
more rows without a cursor, or repeats a cursor, instead of returning a list it
cannot vouch for.
offset, page, generation and snapshotId still work on the lists that had
them, and are deprecated. Sending one beside cursor returns 400. The visibility
response listing returns only the latest 20 successful executions; it has no
cursor and cannot retrieve complete history.
Maintainer commands
From the repository root:
pnpm --filter @devtune/client generate
pnpm --filter @devtune/client generate:check
pnpm --filter @devtune/client test
pnpm --filter @devtune/client test:types
pnpm --filter @devtune/client build
pnpm --filter @devtune/client pack --pack-destination /tmp/devtune-client-pack
pnpm --filter @devtune/client test:packGeneration reads apps/docs/openapi.json locally. openapi-typescript generates
schemas, parameters and responses; a small package-owned script generates runtime
method/path bindings. Both files include the source hash and use the pinned repo
formatter. generate:check fails on any source drift, including documentation
changes. Unsupported serialization changes fail generation instead of silently
producing unusable methods. The package pins its build-time TypeScript 5 compiler
API through an npm alias, independently of the repository's TypeScript 7.
Neither compiler ships at runtime.
test:pack packs, checks the file allowlist, installs offline in a temporary
directory, runs a JavaScript import, and compiles valid/invalid TypeScript consumers
and all recipes against the packed exports. No API credentials are used.
The package is prepared for npm with an explicit file allowlist, ESM exports, declarations and MIT license. Publishing is a separate maintainer action.
Versions and updates
The SDK and CLI have separate npm versions. Check published versions with
npm view @devtune/client version. Pin a version in repeatable scripts.
See CHANGELOG.md for release changes and the
customer documentation.
