Contributing to dario

September 7, 2026 · View on GitHub

PRs welcome. The codebase is ~12,650 lines across 27 TypeScript files and stays dependency-free at runtime.

Setup

git clone https://github.com/askalf/dario
cd dario
npm install
npm run dev   # runs with tsx, no build needed

Structure

FilePurpose
src/proxy.tsHTTP proxy server, request dispatch, rate governor, billing tag, account-pool routing (the one credential path since v5.0 — pool-as-primitive), OpenAI-compat backend routing, SSE streaming forwarder
src/cc-template.tsCC request template engine, forward tool mapping (translateArgs), reverse tool mapping (translateBack), reverseMapResponse for non-streaming responses, createStreamingReverseMapper for SSE streaming tool_use blocks, framework/orchestration scrubbing
src/cc-template-data.jsonCC request template data (25 tools, 25KB system prompt)
src/cc-oauth-detect.tsAuto-detect OAuth config from the installed Claude Code binary (v3.4.3+), anchored on BASE_API_URL:"https://api.anthropic.com"
src/oauth.tsdario login token storage, refresh, credential detection, macOS keychain fallback (v3.7.0+); since v5.0 the login credential is back-filled into the pool under the reserved login alias
src/accounts.tsPer-account credential storage at ~/.dario/accounts/<alias>.json (v3.5.0+) — the backing store for the account pool, the one credential model since v5.0
src/pool.tsAccount pool, headroom-aware selection, failover-target selection, request queueing (v3.5.0+); always constructed since v5.0 — a plain dario login is a pool of one
src/analytics.tsRolling request history, per-account / per-model stats, burn-rate, exhaustion predictions (v3.5.0+)
src/openai-backend.tsOpenAI-compat backend credential storage and request forwarder (v3.6.0+)
src/cli.tsCLI entry point, command routing (login, proxy, accounts, backend, status, refresh, logout), Bun auto-relaunch
src/index.tsLibrary exports
test/issue-29-tool-translation.mjsIn-process regression test for the tool-use reverse translation layer (28 assertions, no OAuth or live proxy required)
test/compat.mjsLive-proxy end-to-end compat suite (tool use, streaming, OpenAI compat). Requires a running dario proxy and authenticated Claude credentials.
test/e2e.mjsLive-proxy end-to-end smoke suite
test/stealth-test.mjsLive-proxy stealth suite (billing classification, thinking stripping, field scrubbing)
test/oauth-detector.mjsEnd-to-end test for the OAuth detector against a real CC binary

Before submitting

  1. npm run build — must compile clean.
  2. npm test — in-process regression test for the tool-use reverse translation layer (no OAuth or upstream calls required; runs anywhere).
  3. npm audit --production --audit-level=high — no high-severity vulnerabilities.
  4. For changes that touch proxy.ts, cc-template.ts, or streaming behavior: test manually against a live proxy with dario proxy --verbose and then run node test/compat.mjs (requires valid credentials). The live suites aren't wired into npm test because they require credentials and consume real subscription usage.
  5. No new runtime dependencies — dario's zero-runtime-deps posture is load-bearing for its audit story.
  6. Keep it simple — this project's value is that it's small enough to audit.

Security issues

Do not open a public issue. Email security@askalf.org instead. See SECURITY.md.

Review policy

Every PR goes through at least one review round. The bar for merge is:

  • Functional. Tests pass locally and on CI (the full npm test suite, not just the change's new tests).
  • Necessary. The change solves a stated problem (linked issue, user report, or review-feedback entry). "While I was in there" changes get split into a separate PR.
  • Non-breaking by default. Any change that removes or alters a @stable API per STABILITY.md requires a deprecation cycle — no breaking changes slip through on patch or minor releases.
  • Test coverage for behavior changes. New flags, new code paths, new exit conditions all get at least one assertion in test/. Pure refactors are exempt.
  • Zero new runtime deps. This is non-negotiable — dario's audit story depends on it. Dev-only deps (TypeScript, tsx, @types/node) are fine.

PRs that don't meet the bar get comments explaining why, not a silent block. If the bar seems arbitrary in a specific case, argue it in the PR — every bar item has been negotiated before.

Linking an issue you didn't file

Write Addresses #N, not Fixes #N. A closing keyword ends the thread the moment the PR merges, and the reporter — who may still be mid-question — reads the close as the answer rather than as a keyword firing. That is what happened on #1244: it closed under the reporter while he was asking follow-ups, and had to be reopened and explained. The person who filed an issue is the one who closes it, once the fix reads right to them.

Two things enforce it. scripts/check-issue-close-keywords.mjs fails CI on a PR whose title, body, or any commit message on the branch would auto-close somebody else's issue — a squash merge carries the branch's commit messages as the commit body, and GitHub scans a commit message raw, backticks and all, so a keyword quoted in code is still a keyword (#1255 closed #1244 that way on its own first merge). Quoting one on purpose? Keep the word and the number apart — Fixes … #1244 — or drop the #. It and .github/workflows/keyword-close-notice.yml comments on one that got closed anyway, naming what closed it and inviting the reporter to reopen. The close stands — reopening every keyword-closed report would also reopen the ones that really are fixed. What was missing on #1244 was the explanation, not the open state. Your own issues and the drift watchers' bot-filed alerts are exempt — Fixes #N those freely.

Release cadence

For the actual release mechanics — the inline auto-release chain in cc-drift-auto-release.yml (build → smoke → GHCR push → GitHub release with attested artifacts → tokenless npm publish via OIDC trusted publishing), the pre-merge checklist, and the post-publish smoke against the installed npm bin added after dario#143 (that's npm's bin stub, unrelated to the dario shim transport removed in v5.0) — see RELEASING.md.

See STABILITY.md for the full policy. Summary:

  • Patch (5.0.x) — bug fixes, review-feedback, drift patches. Often multiple per day during active cycles.
  • Minor (5.1.0) — new flags, new exports, new endpoints. Ships when accumulated new surface justifies it.
  • Major (5.0.0) — removes @deprecated APIs, changes @stable behavior. v5.0 made the account pool the one credential model (a plain dario login is a pool of one) and deleted the deprecated shim transport. Every major carries a migration guide — see MIGRATION.md.

New features default to @experimental for at least one minor before being promoted to @stable. The promotion is a CHANGELOG entry, not a code change — the JSDoc tag moves and the CHANGELOG.md notes the graduation.

Semver commitments

  • @stable API: never breaks without a major bump + a minor-long deprecation cycle
  • @experimental API: can change in any minor, but breaks are called out in CHANGELOG
  • @deprecated API: removed at the next major, stays working with a one-shot warning until then
  • Internal (unmarked) APIs: free to change without notice — don't depend on them

If your PR touches something at the @stable boundary in a way that requires a shape change, flag it in the PR title: [STABILITY] and propose whether it's a deprecation cycle or a new-minor addition that leaves the old surface intact.