CtrlK
BlogDocsLog inGet started
Tessl Logo

tpscloud/flared

Agent skills for Cloudflare, starting with Artifacts: Git repos for agents through the Workers binding, the REST API, and plain git, with type-checked TypeScript and tested Go examples.

100

1.00x
Quality

100%

Does it follow best practices?

Impact

100%

1.00x

Average score across 4 eval scenarios

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

workers-binding.mdskills/cloudflare-artifacts/references/

Workers binding

Source: Workers binding and the Artifacts declarations in @cloudflare/workers-types 5.20261012.1. Regenerate types with wrangler types and trust the generated worker-configuration.d.ts over this file when they differ.

Configuration

Requires Wrangler 4.145.0 or later for the generated Artifacts type and for Blob-returning methods on a remote binding in local development.

// wrangler.jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "repo-broker",
  "main": "src/index.ts",
  "compatibility_date": "2026-10-11",
  "artifacts": [
    { "binding": "ARTIFACTS", "namespace": "agents" },
    // One binding per namespace. This EU-only one also sets remote: true, so
    // wrangler dev uses the real service for it (see below):
    { "binding": "ARTIFACTS_EU", "namespace": "agents-eu", "remote": true }
  ]
}
  • binding and namespace are required. The schema allows no other keys except remote.
  • remote: true makes wrangler dev call the real Artifacts service. That touches real repos and, once billing starts, costs operations.
  • The binding cannot create a namespace or set a jurisdiction. Create the namespace through REST first if it needs one (see rest-api.md).
  • Like other bindings, set artifacts in every named Wrangler environment that needs it.
// examples/cloudflare-artifacts/src/env.d.ts
interface Env {
  ARTIFACTS: Artifacts;
}

Namespace methods (env.ARTIFACTS)

MethodReturnsNotes
create(name, { readOnly?, description?, setDefaultBranch? })ArtifactsCreateRepoResultIncludes an initial token string. Throws ALREADY_EXISTS, INVALID_REPO_NAME.
get(name)ArtifactsRepo (disposable RPC stub)Throws NOT_FOUND, CREATE_IN_PROGRESS, IMPORT_IN_PROGRESS, FORK_IN_PROGRESS.
list({ limit?, cursor? }){ repos, total, cursor? }limit 1 to 200, default 50. Listed repos omit remote. The docs also show a status (ready, importing, forking), but the types don't have it; check readiness with get().
import({ source: { url, branch?, depth? }, target: { name, opts?: { description?, readOnly? } } })ArtifactsCreateRepoResultPublic HTTPS remotes only. Throws INVALID_URL, REMOTE_AUTH_REQUIRED, UPSTREAM_UNAVAILABLE, MEMORY_LIMIT.
delete(name)booleanfalse if not found. Deletes the repo's tokens too. Irreversible.

Repo handle methods (using repo = await env.ARTIFACTS.get(name))

MethodReturnsNotes
info()ArtifactsRepoInfoFresh lookup each call. Metadata is not exposed as properties on the handle.
createToken(scope?, ttl?){ id, plaintext, scope, expiresAt }Scope defaults to "write". TTL in seconds, default 86,400, range 60 to 31,536,000 (INVALID_TTL).
listTokens(){ tokens: { id, scope, state, createdAt, expiresAt }[], total }No plaintext.
revokeToken(tokenOrId)booleanAccepts the plaintext token or its id. false if not found.
fork(name, { description?, readOnly?, defaultBranchOnly? })ArtifactsCreateRepoResultdefaultBranchOnly defaults to true per the type docs. New repo, new token.
log({ ref?, limit?, offset? })ArtifactsCommitMetadata[]First-parent chain, newest first. ref defaults to HEAD, limit 50 (max 1000). Unresolvable ref returns [].
readCommit(hash)ArtifactsCommitMetadata | null
readTree(hash)ArtifactsTreeEntry[] | nullImmediate children only. Recurse yourself.
readBlob(hash)Blob | nullUntyped (blob.type is empty). Lowercase 40-hex SHA-1.
readFile({ ref, path })Blob | nullMIME-typed. null for a missing path or a directory. Throws INVALID_INPUT on empty args.

Read methods can also throw MEMORY_LIMIT for objects too large to buffer.

Result shapes

// examples/cloudflare-artifacts/src/binding-types.ts
interface ArtifactsCreateRepoResult {
  id: string; name: string; description: string | null;
  defaultBranch: string; remote: string;
  token: string; // "art_v2_<letter>_<40 hex>?expires=<unix>", only returned here
}
interface ArtifactsRepoInfo {
  id: string; name: string; description: string | null; defaultBranch: string;
  createdAt: string; updatedAt: string; lastPushAt: string | null;
  source: string | null; // e.g. "github:owner/repo" or "artifacts:ns/repo" for forks and imports
  readOnly: boolean; remote: string;
}
interface ArtifactsCommitMetadata {
  hash: string; treeHash: string; message: string;
  author: { name: string; email: string };
  committer: { name: string; email: string };
  parents: string[]; authoredAt: number; committedAt: number; // unix seconds
}
interface ArtifactsTreeEntry {
  name: string; mode: string; hash: string;
  type: "tree" | "blob" | "symlink" | "gitlink" | "exec";
}

Errors are ArtifactsError with a string code and a numericCode that matches the REST errors[].code values (see limits-pricing-errors.md).

Example: a token broker route

The Worker decides who gets which token. Authenticate the caller first; the docs' examples omit this on purpose and so does nothing in the platform.

// examples/cloudflare-artifacts/src/broker.ts
const noStore = { "cache-control": "no-store" };

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // POST only, so no link, prefetch or cache can mint a token.
    if (request.method !== "POST") return new Response("method not allowed", { status: 405, headers: { allow: "POST" } });
    const caller = await authenticate(request, env); // your auth; reject otherwise
    if (!caller) return new Response("unauthorized", { status: 401 });

    const url = new URL(request.url);
    const name = url.searchParams.get("repo");
    if (!name || !caller.mayAccess(name)) return new Response("forbidden", { status: 403 });

    const scope = caller.mayPush(name) ? "write" : "read";
    try {
      using repo = await env.ARTIFACTS.get(name);
      const { remote } = await repo.info();
      const t = await repo.createToken(scope, 900);
      return Response.json({ remote, token: t.plaintext, scope: t.scope, expiresAt: t.expiresAt }, { headers: noStore });
    } catch (err) {
      const code = (err as Partial<ArtifactsError>).code;
      if (code === "NOT_FOUND") return new Response("no such repo", { status: 404 });
      if (code?.endsWith("_IN_PROGRESS")) return new Response("not ready", { status: 409, headers: { "retry-after": "2" } });
      throw err;
    }
  },
} satisfies ExportedHandler<Env>;

Example: read a file without cloning

// examples/cloudflare-artifacts/src/read-config.ts
async function readConfig(env: Env, name: string, ref = "main") {
  using repo = await env.ARTIFACTS.get(name);
  const file = await repo.readFile({ ref, path: ".app/config.json" });
  return file ? JSON.parse(await file.text()) : null;
}

using and toolchains

The docs use using (explicit resource management) so the stub is released before the request ends. If your TypeScript or bundler setup rejects it, call repo[Symbol.dispose]() in a finally block instead. Do not keep handles in module scope across requests.

README.md

tile.json