atcute is a monorepository, a collection of lightweight and high-quality TypeScript libraries for AT Protocol (the protocol powering Bluesky.)
the packages are organized into categories under packages/:
clients/: API client implementationsservers/: XRPC server framework and runtime adaptersoauth/: OAuth implementationslexicons/: lexicon schema toolingdefinitions/: generated validation and type definitions for various AT Protocol servicesidentity/: DID document and handle resolutions, and did:plc validationutilities/: DASL codecs, data encoding and atproto primitivesmisc/: general-purpose utilitiesbluesky/: Bluesky-specific helpersinternal/: non-public development packages
- tools like Node.js, Bun and pnpm are managed by mise
- Node.js can run TypeScript files directly (
node index.ts, strip types is unflagged) - check
pnpm view <package>before adding a new dependency
- format via
pnpm run fmt(oxfmt) - lint via
pnpm run lint(oxlint)
run these inside the package directory, e.g. cd packages/utilities/cbor; pnpm run ...
- build via
pnpm run build(tsc) - test via
pnpm run test(vitest)
- name new files in kebab-case
- indent with tabs (spaces allowed for diagrams in comments)
- use single quotes for strings; reserve template literals for localization (user-facing strings, error messages)
- add trailing commas
- order list-like constructs (arrays, object keys, union/intersection members, enum variants, imports, etc.) alphabetically. reserve other orderings for cases where order carries meaning — semantic precedence, an external spec, or similar. if you encounter an unordered list while editing nearby code, reorder it as part of the change; avoid drive-by reorders of unrelated lists
- use braces for control statements, even single-line bodies
- group related code and limit variable scope with bare blocks
{ } - use
switchoverif/else ifchains when branching on a single discriminant value - use
ifover ternaries for complex statements - delimit sections of larger files with
// #region <name>and// #endregion - import directly from source; barrel files (index modules that re-export) are out
- prefer arrow functions; use method shorthand for object/class methods
- make a parameter optional only when callers genuinely split between passing a value and relying on the default. drop unused defaults; promote always-passed params to required; split into a separate function when presence/absence flips behavior
- prefer an options object when the function takes a boolean flag (split into two functions if the flag selects between distinct behaviors), when two same-typed params could be swapped at the call site, or when there are already 2+ optional/defaulted params. positional is fine at any count when each param has a distinct type and a clear semantic order
- write code that satisfies the type system naturally; reach for
as Typeoras constonly when TypeScript errors and no cleaner solution exists
- treat function arguments as immutable; callers expect their inputs to come back unchanged.
in-place operations like
array.sort()orObject.assign(target, ...)are fine on values the function owns — locals, clones, freshly constructed objects — but copy first (array.toSorted(),{ ...obj, ...patch }) before touching anything reachable through a parameter. the exception is a function whose documented purpose is to mutate its argument; the name and JSDoc should make that intent obvious
we use conventional commits with these rules:
- a commit represents one logical work
- accepted types:
feat,fix,refactor,docs,chore- feat
- new additions to public API surface
- docs
- Markdown document changes (README.md and similar)
- chore
- build/tooling/dependency changes
- tests, code comments, or JSDoc changes
- mass-autofixes from linters and formatters
- feat
- commit type describes the substance of the change as a whole, not a category to split it by. tests
written for a feature ship in the
featcommit;choreapplies when test, comment, or JSDoc work is the entire change - optional scope is the package name, e.g.
refactor(package-a): - omit the scope when the change does not involve any specific package, or when it touches most/all packages
- never list multiple packages in the scope (e.g.
refactor(package-a,package-b)is forbidden) - append
!after the type/scope to mark breaking changes, e.g.feat(package-a)!:orrefactor!:
scope selection when multiple packages are involved:
- if the change primarily involves
package-aoverpackage-b, pickpackage-a - if changes in
package-aandpackage-bhinge onpackage-c(even ifpackage-citself was not modified), pickpackage-c
"documentation" here means READMEs, code comments, and commit messages.
- write in lowercase, except for proper nouns, acronyms, and 'I'. public-facing interfaces (web UI) are exempt
- comment non-trivial code only, focusing on why rather than what
- add JSDoc to new publicly exported functions, methods, classes, fields, and enums:
@paramfor parameters (no dashes after param names)@returnsfor return values@throwsfor exceptions when applicable- describe what, not why, unless the rationale affects how callers use the API (e.g. a constraint). put other why explanations in regular code comments next to the implementation
- keep descriptions concise but informative
.research/in the project root is an ephemeral workspace for experiments, analysis, and planning materials, and may contain cloned repositories or other reference materials that inform implementation decisions. create if not present. its contents are local to a single working copy and may be wiped or absent in any future session, so committed source code (including comments, docstrings, commit messages, READMEs, or other docs) must not reference paths under.research/or rely on its contents existing- this document is intentionally incomplete; discover everything else by exploring the repo
- explore the code first when unsure about plans, requirements, or existing behavior. ask for clarification when exploration leaves the question unresolved
- when debugging, isolate the root cause before attempting fixes: add logging, reproduce the issue, narrow down the scope, and confirm the exact source of the problem
- subagent/subtask exploration results may be inaccurate; verify findings as needed
- read files directly rather than using subagents/subtasks as file I/O proxies