WIP: A simple cli for daily tangled use cases and AI integration. This is for my personal use right now, but happy if others get mileage from it! :)

Update Claude guideance

markbennett.ca 168b5a33 4f9e9fb9

verified
+61
+2
.claude/settings.json
··· 6 6 "Bash(npm test:*)", 7 7 "Bash(npm run typecheck:*)", 8 8 "Bash(npm run lint:*)", 9 + "Bash(npm run lint:fix:*)", 10 + "Bash(git checkout:*)", 9 11 "Bash(npm run format:*)" 10 12 ] 11 13 }
+59
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Commands 6 + 7 + ```bash 8 + npm run dev -- <args> # Run CLI in development (use this, not ./tangled) 9 + npm run build # Compile TypeScript to dist/ 10 + npm test # Run all tests once 11 + npm run test:watch # Run tests in watch mode 12 + npm run typecheck # Type-check without building (prefer over npx tsc --noEmit) 13 + npm run lint # Check with Biome 14 + npm run lint:fix # Auto-fix lint/format issues 15 + ``` 16 + 17 + Run a single test file: 18 + ```bash 19 + npx vitest run tests/commands/issue.test.ts 20 + ``` 21 + 22 + ## Architecture 23 + 24 + `src/index.ts` is the entry point — it registers all commands and parses `process.argv`. 25 + 26 + ### Layer structure 27 + 28 + - **`src/commands/`** — Commander.js command factories (e.g. `createIssueCommand()`). Each command: resumes session, gets repo context, calls lib functions, outputs results. 29 + - **`src/lib/`** — Business logic with no Commander dependency: 30 + - `api-client.ts` — `TangledApiClient` wraps `AtpAgent`; `isAuthenticated()` is **synchronous** 31 + - `session.ts` — OS keychain storage via `@napi-rs/keyring`; throws `KeychainAccessError` if keychain is inaccessible (not just missing) 32 + - `context.ts` — Infers repo from `git remote` URLs; resolves `RepositoryContext` with owner DID/handle and repo name 33 + - `issues-api.ts` — All issue CRUD; exports `IssueData` (canonical JSON shape), `getCompleteIssueData`, `resolveSequentialNumber` 34 + - **`src/utils/`** — Stateless helpers: 35 + - `auth-helpers.ts` — `requireAuth(client)` throws if unauthenticated (use in lib functions); `ensureAuthenticated(client)` for commands (calls `resumeSession`, exits on failure) 36 + - `validation.ts` — **All** validation logic lives here (Zod schemas + boolean helpers) 37 + - `formatting.ts` — `outputJson<T extends object>(data, fields?)`, `formatDate`, `formatIssueState` 38 + - `at-uri.ts` — Parse/build AT-URIs and repo AT-URIs 39 + - `body-input.ts` — Reads `--body` / `--body-file` / stdin (`-F -`) 40 + - **`src/lexicon/`** — Auto-generated AT Protocol type definitions; regenerate with `npm run codegen` 41 + 42 + ### Key patterns 43 + 44 + **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. 45 + 46 + **Issue state** — Stored as separate `sh.tangled.repo.issue.state` records. The latest record wins; default is `'open'` if no record exists. 47 + 48 + **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. 49 + 50 + **Auth flow** — Commands call `client.resumeSession()` directly, then proceed. Lib functions call `requireAuth(client)`. `KeychainAccessError` from `session.ts` propagates through `resumeSession()` without clearing metadata. 51 + 52 + ### Tests 53 + 54 + Tests mirror `src/` under `tests/`. Command tests mock the entire `issuesApi` module: 55 + ```typescript 56 + vi.mock('../../src/lib/issues-api.js'); 57 + // Use importOriginal to preserve exported classes/errors if needed 58 + ``` 59 + `isAuthenticated()` is synchronous — mock as `vi.fn(() => true)`, not `vi.fn(async () => true)`.