CLAUDE.md#
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Commands#
npm run dev -- <args> # Run CLI in development (use this, not ./tangled)
npm run build # Compile TypeScript to dist/
npm test # Run all tests once
npm run test:watch # Run tests in watch mode
npm run typecheck # Type-check without building (prefer over npx tsc --noEmit)
npm run lint # Check with Biome
npm run lint:fix # Auto-fix lint/format issues
Run a single test file:
npx vitest run tests/commands/issue.test.ts
Architecture#
src/index.ts is the entry point — it registers all commands and parses process.argv.
Layer structure#
src/commands/— Commander.js command factories (e.g.createIssueCommand()). Each command: resumes session, gets repo context, calls lib functions, outputs results.src/lib/— Business logic with no Commander dependency:api-client.ts—TangledApiClientwrapsAtpAgent;isAuthenticated()is synchronoussession.ts— OS keychain storage via@napi-rs/keyring; throwsKeychainAccessErrorif keychain is inaccessible (not just missing)context.ts— Infers repo fromgit remoteURLs; resolvesRepositoryContextwith owner DID/handle and repo nameissues-api.ts— All issue CRUD; exportsIssueData(canonical JSON shape),getCompleteIssueData,resolveSequentialNumber
src/utils/— Stateless helpers:auth-helpers.ts—requireAuth(client)throws if unauthenticated (use in lib functions);ensureAuthenticated(client)for commands (callsresumeSession, exits on failure)validation.ts— All validation logic lives here (Zod schemas + boolean helpers)formatting.ts—outputJson<T extends object>(data, fields?),formatDate,formatIssueStateat-uri.ts— Parse/build AT-URIs and repo AT-URIsbody-input.ts— Reads--body/--body-file/ stdin (-F -)
src/lexicon/— Auto-generated AT Protocol type definitions; regenerate withnpm run codegen
Key patterns#
Issue numbering — Sequential numbers are not stored; they are computed by sorting all issues for a repo by createdAt ascending. The 1-based index is the display number.
Issue state — Stored as separate sh.tangled.repo.issue.state records. The latest record wins; default is 'open' if no record exists.
JSON output — All issue sub-commands use IssueCommand extends Command (in issue.ts) to share a --json [fields] option. The canonical field set is: number, title, body, state, author, createdAt, uri, cid. Use getCompleteIssueData() to populate all fields.
Auth flow — Commands call client.resumeSession() directly, then proceed. Lib functions call requireAuth(client). KeychainAccessError from session.ts propagates through resumeSession() without clearing metadata.
Tests#
Tests mirror src/ under tests/. Command tests mock the entire issuesApi module:
vi.mock('../../src/lib/issues-api.js');
// Use importOriginal to preserve exported classes/errors if needed
isAuthenticated() is synchronous — mock as vi.fn(() => true), not vi.fn(async () => true).
Version Control#
- Use small meaningful commits with clear messages.
- Break work down into logical steps, committing each step separately.
- Use feature branches when addressing issues. Include the issue number and a description of changes in the branch name (e.g.,
issue-123-fix-auth). - When work is complete push commits to remote and open a pull request for review. Include the issue number and a description of changes in the PR title (e.g.,
Fix authentication flow for issue #123).