Package Exports
- @run402/sdk
- @run402/sdk/node
Readme
@run402/sdk
Typed TypeScript client for the Run402 API. The kernel shared by run402-mcp, the run402 CLI, and (eventually) user-deployed functions. Every operation is a method on a resource namespace — r.projects.provision(), r.blobs.put(), r.deploy.apply(), r.functions.deploy(), …
npm install @run402/sdkTwo entry points
| Import | Use when |
|---|---|
@run402/sdk/node |
Running in Node 22 with the local keystore + allowance. Auto-loads ~/.config/run402/projects.json and signs x402 payments from ~/.config/run402/allowance.json. Includes r.sites.deployDir(dir), fileSetFromDir(dir), loadDeployManifest(path), and normalizeDeployManifest(input). |
@run402/sdk |
Isomorphic — works in Node, Deno, Bun, V8 isolates. No filesystem access. Bring your own CredentialsProvider (a session-token shim, a remote vault, anything that resolves project keys + auth headers). |
Quick start (Node)
import { run402 } from "@run402/sdk/node";
const r = run402();
const project = await r.projects.provision({ tier: "prototype" });
await r.blobs.put(project.project_id, "hello.txt", { content: "hi" });That's it — credentials are read, x402 payments are signed, results are typed.
Project-scoped sub-client
If you're working on a single project for the duration of a script, bind it once and skip the id arg on every call:
const p = await r.useProject(projectId); // persists active project + returns scoped handle
await p.blobs.put("hello.txt", { content: "hi" }); // no projectId arg
await p.functions.list();
await p.deploy.apply({ site: { replace: files({ "index.html": "<h1>hi</h1>" }) } });r.useProject(id) writes the active project to the keystore (shared with concurrent CLI runs). For transient in-script scoping that does NOT mutate that state, use r.project(id) (or r.project() with no arg to resolve from whatever the keystore currently considers active).
Quick start (isomorphic)
import { Run402 } from "@run402/sdk";
const r = new Run402({
apiBase: "https://api.run402.com",
credentials: {
async getAuth() { return { Authorization: `Bearer ${session.token}` }; },
async getProject(id) { return session.projects[id] ?? null; },
},
});The CredentialsProvider interface has two required methods (getAuth, getProject) plus optional ones (saveProject, removeProject, setActiveProject, readAllowance, saveAllowance, …) for hosts that want full sticky-default behavior.
Namespaces (20)
| Namespace | Highlights |
|---|---|
projects |
provision, delete, list, sql, rest, validateExpose, applyExpose, getExpose, getUsage, getSchema, info, keys, use, active, pin, getQuote |
deploy |
The unified deploy primitive (v1.34+). apply / start / resume / status / list / events / resolve / getRelease / getActiveRelease / diff / plan / upload / commit |
ci |
GitHub Actions OIDC federation over /ci/v1/*: createBinding, listBindings, getBinding, revokeBinding, exchangeToken; plus canonical delegation helpers |
sites |
deployDir — Node entry only (@run402/sdk/node); thin wrapper over r.deploy.apply |
blobs |
put (returns AssetRef with cdnUrl / sri / etag / cacheKind and scriptTag()/linkTag()/imgTag() emitters), get, ls, rm, sign, diagnoseUrl, waitFresh |
functions |
deploy, invoke, logs, update, list, delete |
secrets |
set, list, delete |
subdomains |
claim, list, delete (most agents declare subdomains in r.deploy.apply({ subdomains: { set: [...] } }) instead) |
domains |
add, list, status, remove |
email |
createMailbox, getMailbox, deleteMailbox, send, list, get, getRaw, webhooks.* |
senderDomain |
register, status, remove, enableInbound, disableInbound |
auth |
requestMagicLink, verifyMagicLink, createUser, inviteUser, setUserPassword, settings, passkey registration/login/list/delete helpers, providers, promote, demote |
apps |
browse, getApp, fork, publish, listVersions, updateVersion, deleteVersion, bundleDeploy (legacy shim → routes through deploy) |
tier |
set, status (tier pricing lives on r.projects.getQuote()) |
billing |
createEmailAccount, linkWallet, tierCheckout, buyEmailPack, setAutoRecharge, checkBalance, getAccount, getHistory, balance, history, createCheckout |
contracts |
provisionWallet, getWallet, listWallets, setRecovery, setLowBalanceAlert, call, read, callStatus, drain, deleteWallet |
ai |
translate, moderate, usage, generateImage |
allowance |
status, create, export, faucet |
service |
status, health (no auth, no setup — works on a fresh install) |
admin |
Operator/admin endpoints: messages/contact, per-project finance (getProjectFinance) |
CLI-style aliases are available for agent ergonomics: r.image aliases r.ai,
and common command names such as r.billing.balance, r.auth.magicLink,
r.projects.schema, r.email.create, and r.contracts.setAlert point at the
canonical camelCase methods.
Casing in returned shapes
Two casings coexist by design — agents reading the type surface should classify a field by the SHAPE it belongs to:
- Raw API result shapes preserve the gateway's snake_case fields. Examples:
ProvisionResult.project_id,ProvisionResult.anon_key,ProvisionResult.service_key,ProvisionResult.schema_slot,ProjectInfo.project_id,ProjectSummary.lease_expires_at,UsageReport.api_calls,SchemaReport.schema. These mirror the HTTP response bodies one-to-one. - SDK-specific helper shapes use camelCase. Examples:
AssetRef.cdnUrl/AssetRef.cacheKind/AssetRef.contentSha256,Run402DeployError.safeToRetry/operationId/mutationState, everyDeployEventvariant's discriminator (type, plus per-variant fields likereleaseId,urls).
This split is intentional and stays through 1.x. Doc examples in this
README and in llms-sdk.txt use the exact field names the types export —
copy them verbatim. CI fails any TypeScript-fenced example that accesses a
field that does not exist on the actual type.
Reference tables (in
llms-sdk.txt) use plain code fences, nottsfences. They document the type surface in compact form for visual scanning — they are not runnable programs and are exempt from CI type-checking. Runnable example snippets still use```tsand are CI-gated against the published types.
Patterns
Paste-and-go assets — content-addressed URLs with SRI
r.blobs.put returns an AssetRef. The cdnUrl is content-addressed (pr-<public_id>.run402.com/_blob/<key>-<8hex>.<ext>), served through CloudFront, and never needs cache invalidation. The browser refuses execution on byte mismatch via SRI:
const logo = await r.blobs.put(projectId, "logo.png", { bytes });
// logo.cdnUrl → drop into <img src="…">
// logo.sri → "sha256-…" for <script integrity="…">
// logo.etag → strong "sha256-<hex>"
// logo.cacheKind → "immutable" | "mutable" | "private"immutable: true is the default since v1.45 — pass false only when you specifically want to skip the SHA-256 pass on a very large upload.
For custom resumable upload UX, use the low-level session primitives:
r.blobs.initUploadSession(...), r.blobs.getUploadSession(...), and
r.blobs.completeUploadSession(...). Bytes still go directly to the presigned
part URLs; the Run402 gateway sees only session metadata.
Expose manifest validation
Validate the auth/expose manifest used by manifest.json, database.expose, and apply_expose before mutating a project:
const manifest = { version: "1" as const, tables: [] };
const result = await r.projects.validateExpose(manifest, {
project: projectId, // optional live-schema context
migrationSql: "create table items (id bigint primary key);",
});
if (result.hasErrors) console.log(result.errors);migrationSql is reference context only; it is not executed as a PostgreSQL dry run. This method validates authorization manifests, not deploy manifests.
Unified deploy (v1.34+) — r.deploy.apply
The canonical primitive for any deploy (database + migrations + manifest + value-free secret declarations + functions + site + subdomain). Three layers:
// One-shot — most agents use this.
const result = await r.deploy.apply(spec);
// Long-running with progress events. Events are a discriminated union on `type`.
const op = await r.deploy.start(spec);
for await (const ev of op.events()) console.log(ev.type);
const final = await op.result();
// Resume a previously-started deploy by id.
const resumed = await r.deploy.resume(operationId);All bytes ride through CAS. The plan request body never carries inline bytes — only
ContentRefobjects. When the spec exceeds 5 MB JSON, the SDK uploads the manifest itself as a CAS object (manifest_refescape hatch).Per-resource semantics on the spec.
site.replace= "this is the whole site" (files absent are removed).site.patch.put/patch.deleteare surgical updates.functions.replace/functions.patch.set/functions.patch.deletemirror that. Secrets are value-free: set values first withr.secrets.set(project, key, value), then deploy withsecrets.requireand/orsecrets.delete.subdomains.set/subdomains.add/subdomains.removeuse their own shape. Top-level absence = leave untouched.Same-origin web routes.
routesisundefined | null | { replace: RouteSpec[] }. Omit it or passnullto carry forward base routes, pass{ replace: [] }to clear routes, or pass route entries to replace the table. Function targets use{ type: "function", name }; exact static route targets use{ type: "static", file }with methods["GET"]or["GET","HEAD"], no wildcard pattern, and a relative deployed file path with no leading slash. Use exact/adminplus final-wildcard/admin/*for a dynamic section root; ordinary static files do not need route entries. Routed browser ingress invokes Node 22 Fetch Request -> Response handlers;req.urlis the full public URL on managed subdomains, deployment hosts, and verified custom domains. Direct/functions/v1/:nameinvocation remains API-key protected. Runtime route failure codes includeROUTE_MANIFEST_LOAD_FAILED,ROUTED_INVOKE_WORKER_SECRET_MISSING,ROUTED_INVOKE_AUTH_FAILED,ROUTED_ROUTE_STALE,ROUTE_METHOD_NOT_ALLOWED, andROUTED_RESPONSE_TOO_LARGE.Strict spec validation happens before network calls. Raw
ReleaseSpecobjects reject unknown fields (for exampleproject_idorsubdomain) instead of silently dropping them during normalization, and project/base-only or empty nested specs fail withRun402DeployError.code === "MANIFEST_EMPTY". Use the Node manifest helpers when starting from CLI/MCP-style JSON.Warnings are structured.
DeployResult.warningscontainsWarningEntry[](code,severity,requires_confirmation,message, optionalaffected/details/confidence); the type preserves legacy low/medium/high plan warnings and modern deploy-observability info/warn/high warnings.apply()emitsplan.warningsand stops before upload/commit on confirmation-required warnings unlessallowWarningsis set. ForMISSING_REQUIRED_SECRET, set the affected keys withr.secrets.set, then retry.Safe release-race retries are SDK-owned.
deploy.apply()automatically re-plans and retries omitted/current-base specs when the gateway returnsBASE_RELEASE_CONFLICTwithsafe_to_retry: true. The default budget is two retries after the initial attempt; pass{ maxRetries: 0 }to opt out. Each retry emitsdeploy.retry; exhausted retries keep the lastRun402DeployErrorand addattempts,maxRetries, andlastRetryCode.Planning supports dry-runs.
r.deploy.plan(spec, { dryRun: true })calls the server-authoritative dry-run route and returns the normalized v2 plan envelope without uploading bytes or creating plan/operation rows (plan_idandoperation_idarenull).Release observability is typed. Use
r.deploy.getRelease({ project, releaseId, siteLimit? }),r.deploy.getActiveRelease({ project, siteLimit? }), andr.deploy.diff({ project, from, to, limit? })to inspect release inventory and release-to-release diffs. Inventories includerelease_generation,static_manifest_sha256, and nullablestatic_manifest_metadata(file_count,total_bytes,cache_classes,cache_class_sources,spa_fallback);nullmeans unavailable, not zero.diffreturnsReleaseToReleaseDiffwithmigrations.applied_between_releases; secret diffs expose keys only;static_assetsexposes unchanged/changed/added/removed files, CAS byte reuse, eliminated deployment-copy bytes, and immutable/CAS warning counts.Server-authoritative manifest digest — no byte-for-byte canonicalize requirement on the client.
The Node entry adds
fileSetFromDir(path)for filesystem byte sources:import { run402, fileSetFromDir } from "@run402/sdk/node"; const r = run402(); await r.deploy.apply({ project: projectId, site: { replace: await fileSetFromDir("./dist") }, subdomains: { set: ["my-app"] }, });
Route manifests are ordinary deploy specs:
import { run402, type RouteSpec, type ReleaseSpec } from "@run402/sdk/node"; const r = run402(); const routes: RouteSpec[] = [ { pattern: "/api/*", methods: ["GET", "POST", "OPTIONS"], target: { type: "function", name: "api" } }, { pattern: "/admin", target: { type: "function", name: "admin" } }, { pattern: "/admin/*", target: { type: "function", name: "admin" } }, { pattern: "/login", methods: ["POST"], target: { type: "function", name: "auth" } }, { pattern: "/events", methods: ["GET", "HEAD"], target: { type: "static", file: "events.html" } }, ]; const spec: ReleaseSpec = { project: projectId, functions: { replace: { api: { source: "export default async function handler(req) { const url = new URL(req.url); return Response.json({ ok: true, path: url.pathname }); }" }, admin: { source: "export default async () => new Response('admin')" }, auth: { source: "export default async () => new Response('login')" }, }, }, site: { replace: { "index.html": "<!doctype html><main id='app'></main>", "events.html": "<!doctype html><h1>Events</h1>", } }, routes: { replace: routes }, }; await r.deploy.apply(spec);
Matching is exact or final
/*prefix only./admin/*does not match/admin; deploy both/adminand/admin/*when the section root is dynamic. Static route targets are exact file targets, not rewrites or redirects; avoid routing every static file, wildcard static targets, leading-slash files, directory shorthand, broad method lists by default, and one-static-route-target-per-page route-table exhaustion. Query strings are ignored for matching and preserved in the handler's full publicreq.url. Exact beats prefix, longest prefix wins, and method-compatible dynamic routes beat static files. A method-specificPOST /loginroute lets staticGET /loginserve HTML. Unsafe method mismatch returns405; matched dynamic route failures do not fall back to static assets.Routed functions use Node 22 Fetch Request -> Response.
req.urlis the full public URL on managed subdomains, deployment hosts, and verified custom domains. The rawrun402.routed_http.v1envelope is internal; direct/functions/v1/:nameremains API-key protected.URL-first public diagnostics:
import { buildDeployResolveSummary, normalizeDeployResolveRequest, run402, type DeployResolveResponse, } from "@run402/sdk/node"; const r = run402(); const request = normalizeDeployResolveRequest({ project: projectId, url: "https://example.com/events?utm=x#hero", method: "GET", }); const resolution: DeployResolveResponse = await r.deploy.resolve(request); const summary = buildDeployResolveSummary(resolution, request); console.log(summary.would_serve, summary.match, request.ignored);
r.deploy.resolve({ project, url, method })and scopedp.deploy.resolve({ url, method })also accept lower-level{ project, host, path?, method? }. URL query strings/fragments are ignored for lookup and surfaced inrequest.ignored. Current knownmatchliterals arehost_missing,manifest_missing,path_error,none,static_exact,static_index,spa_fallback, andspa_fallback_missing; preserve unknown future strings. Today resolve is authoritative for host/static/SPAfallback diagnostics, not complete route introspection unless future gateways return route context.resultis diagnostic body status, not SDK HTTP transport status, so host misses can be successful calls withwould_serve: false. Do not use resolve as a fetch, cache purge, or cache-policy oracle, and do not hard-codecache_policystrings; branch oncache_classand preserve unknown cache classes.Route warning recovery:
Code Why it matters Recovery PUBLIC_ROUTED_FUNCTIONFunction becomes public same-origin browser ingress. Review app auth, CSRF, CORS/ OPTIONS, and cookies; direct/functions/v1/:nameremains API-key protected. Retry withallowWarningsonly after review.ROUTE_TARGET_CARRIED_FORWARDCarried-forward route still targets a base-release function. Inspect active routes and deploy routes.replaceif the target should change.ROUTE_SHADOWS_STATIC_PATH/WILDCARD_ROUTE_SHADOWS_STATIC_PATHSDynamic route shadows static content. Inspect warning details and active routes; confirm only when intentional. METHOD_SPECIFIC_ROUTE_ALLOWS_GET_STATIC_FALLBACKUnmatched methods can serve static content. Confirm fallback is intended or add method coverage. WILDCARD_ROUTE_EXCLUDES_MUTATION_METHODSWildcard function route only allows GET/HEAD.Add mutation methods such as POST, omit methods for an API prefix, or confirm it is read-only.ROUTE_TABLE_NEAR_LIMITRoute table is near a limit. Consolidate or remove routes. ROUTES_NOT_ENABLEDRoutes are disabled for the project/environment. Deploy without routesor request enablement; direct function invoke is not a browser-route substitute.STATIC_ALIAS_SHADOWS_STATIC_PATH/STATIC_ALIAS_RELATIVE_ASSET_RISKStatic route target conflicts with a real static path or has relative-asset risk. Prefer canonical static files, fix relative asset paths, and confirm only when intentional. STATIC_ALIAS_DUPLICATE_CANONICAL_URL/STATIC_ALIAS_EXTENSIONLESS_NON_HTMLStatic route target may duplicate canonical URLs or expose extensionless non-HTML. Use one canonical URL per page and reserve exact static route targets for intentional HTML-like public paths. STATIC_ALIAS_TABLE_NEAR_LIMITStatic route targets are near route-table limits. Avoid one-static-route-target-per-page tables; consolidate. The Node entry also has the typed manifest adapter shared by CLI/MCP:
import { loadDeployManifest, run402 } from "@run402/sdk/node"; const r = run402(); const { spec, idempotencyKey } = await loadDeployManifest("./run402.deploy.json"); await r.deploy.apply(spec, { idempotencyKey });
loadDeployManifest(path)parses JSON relative to the manifest file, maps agent-friendlyproject_idintoReleaseSpec.project, decodes base64 file entries, turns{ path }entries into lazyFsFileSourcevalues, and reads migrationsql_path/sql_file. It rejects unknown manifest fields before they can become partial deploys. UsenormalizeDeployManifest(input)when the manifest object is already in memory.
GitHub Actions OIDC — CI credentials drive deploy
The v1 CI path keeps the deploy primitive simple: link a GitHub repository once, then call the existing r.deploy.apply with CI-marked credentials. There is no separate r.ci.deployApply method and no public ci: true deploy option.
The CLI is the easiest setup path (run402 ci link github), but the SDK exposes the building blocks:
import {
CI_GITHUB_ACTIONS_PROVIDER,
V1_CI_ALLOWED_ACTIONS,
V1_CI_ALLOWED_EVENTS_DEFAULT,
run402,
signCiDelegation,
} from "@run402/sdk/node";
const values = {
project_id: projectId,
subject_match: "repo:owner/name:ref:refs/heads/main",
allowed_actions: V1_CI_ALLOWED_ACTIONS,
allowed_events: V1_CI_ALLOWED_EVENTS_DEFAULT,
// Optional: omit or [] for no CI route authority.
// Use exact paths and/or final wildcard prefixes for route declarations.
route_scopes: ["/admin", "/api/*"],
github_repository_id: "123456789",
expires_at: null,
nonce: "0123456789abcdef0123456789abcdef",
};
const r = run402({ disablePaidFetch: true });
const signed_delegation = signCiDelegation(values);
await r.ci.createBinding({
...values,
provider: CI_GITHUB_ACTIONS_PROVIDER,
signed_delegation,
});Inside GitHub Actions, use githubActionsCredentials. It reads GitHub's OIDC environment, exchanges the subject token through r.ci.exchangeToken, caches the Run402 session until expires_in - refreshBeforeSeconds, and marks the credentials so deploy uses CI Bearer auth:
import { githubActionsCredentials, run402, type ReleaseSpec } from "@run402/sdk/node";
const r = run402({
credentials: githubActionsCredentials({ projectId }),
disablePaidFetch: true,
});
const ciSpec: ReleaseSpec = {
project: projectId,
base: { release: "current" },
site: { patch: { put: { "index.html": "<h1>ship</h1>" } } },
};
await r.deploy.apply(ciSpec);CI deploys intentionally allow only project, database, functions, site, absent/current base, and routes authorized by the binding's route_scopes. Omitted or empty route_scopes preserves the original no-routes CI posture. The SDK normalizes scopes, sends route_scopes only when non-empty, and still rejects secrets, subdomains, checks, unknown future top-level fields, non-current base, and specs large enough to require manifest_ref before upload/plan. Gateway planning enforces route diffs and can return CI_ROUTE_SCOPE_DENIED; re-link with covering exact scopes like /admin or final-wildcard scopes like /api/*, or deploy locally. Use the canonical builders (buildCiDelegationStatement, buildCiDelegationResourceUri) instead of hand-rolling SIWX text; gateway tests pin those strings as golden vectors.
Errors
All failures throw subclasses of Run402Error. Every subclass carries a stable
kind discriminator string and an isRun402Error brand:
| Class | kind |
When | Notable fields |
|---|---|---|---|
PaymentRequired |
"payment_required" |
HTTP 402 | x402 payment requirements in body |
ProjectNotFound |
"project_not_found" |
Project ID not in the credential provider | projectId |
Unauthorized |
"unauthorized" |
HTTP 401 / 403 | — |
ApiError |
"api_error" |
Other non-2xx responses | status, body |
NetworkError |
"network_error" |
Fetch rejected with no HTTP response | cause |
LocalError |
"local_error" |
Local-host issues (filesystem, signing) | cause |
Run402DeployError |
"deploy_error" |
Structured envelope from the deploy state machine (v1.34+) | code, phase, operationId, safeToRetry, mutationState, nextActions |
Branch with type guards, not instanceof. instanceof X is an identity
check on the class object — it fails silently when the consumer's runtime
holds a different copy of the SDK (duplicate npm installs, bundler chunk
splits, ESM/CJS interop, V8-isolate realms). The exported guards
(isPaymentRequired, isDeployError, …) check isRun402Error + kind,
which is identity-free and survives all of those scenarios. instanceof
continues to work for back-compat in the simple single-copy case.
import {
run402,
isPaymentRequired,
isDeployError,
type ReleaseSpec,
} from "@run402/sdk/node";
declare const spec: ReleaseSpec;
const r = run402();
try {
await r.deploy.apply(spec);
} catch (e) {
if (isPaymentRequired(e)) {
// e is narrowed to PaymentRequired
// present payment requirements to the user — read e.body, e.context, etc.
} else if (isDeployError(e)) {
// e is narrowed to Run402DeployError.
// deploy.apply auto-retries safe BASE_RELEASE_CONFLICT races for current-base specs.
// Log the structured envelope for policy errors, exhausted retries, or caller-owned recovery.
} else throw e;
}Run402Error.toJSON() returns a canonical envelope, so JSON.stringify(e)
produces a populated structured object instead of the empty "{}" plain
Error produces. Use this for telemetry, MCP tool results, CLI JSON output,
and any inter-process boundary where the error needs to survive serialization.
Retry idempotent operations with withRetry
withRetry(fn, opts?) wraps any async call with exponential backoff. It uses
isRetryableRun402Error (the canonical "should I retry this?" policy: 408 /
425 / 429 / 5xx / NetworkError / gateway-flagged retryable or
safeToRetry) by default. Pair it with the SDK method's own
idempotencyKey so retried mutations dedup server-side:
For r.deploy.apply(), safe BASE_RELEASE_CONFLICT release races are already
handled by the deploy namespace with a fresh plan and visible deploy.retry
events. Use withRetry for caller-owned retry policies around other operations,
or pass maxRetries: 0 to deploy.apply when you want to handle deploy races
yourself.
import {
run402,
withRetry,
isPaymentRequired,
isDeployError,
type ReleaseSpec,
} from "@run402/sdk/node";
declare const spec: ReleaseSpec;
const r = run402();
try {
const release = await withRetry(
() => r.deploy.apply(spec, { idempotencyKey: "deploy-2026-05-01" }),
{
attempts: 3,
onRetry: (e, attempt, delayMs) =>
process.stderr.write(`retry ${attempt} in ${delayMs}ms\n`),
},
);
console.log(release.urls);
} catch (e) {
if (isPaymentRequired(e)) {
// ... present payment
} else if (isDeployError(e)) {
// log structured envelope for triage
process.stderr.write(JSON.stringify(e) + "\n");
} else throw e;
}Defaults: 3 attempts (1 initial + 2 retries), 250 ms base delay, 5 s cap. Pass
a custom retryIf to override the default policy (e.g., retry on
PaymentRequired if your sandbox auto-funds). After exhausting attempts
withRetry throws the LAST error — your catch handler sees the original
structured envelope, not a wrapper.
The SDK never calls process.exit. Each interface (MCP tools, CLI, your code) wraps with its own error behavior.
Stability
This package is on the 1.x line. The CLI (run402), MCP server (run402-mcp), SDK (@run402/sdk), and @run402/functions release in lockstep at the same version. Pin an exact version in production dependencies.
Other interfaces
@run402/sdk is the kernel that powers four sibling packages:
run402— CLI (terminal / scripts / CI)run402-mcp— MCP server (Claude Desktop / Cursor / Cline / Claude Code)@run402/functions— in-function helper imported inside deployed functions- OpenClaw skill — script-based skill for OpenClaw agents
All five release in lockstep.
Links
- HTTP API reference: https://run402.com/llms.txt
- CLI reference: https://run402.com/llms-cli.txt
- Run402: https://run402.com
License
MIT