notification manager for bsky
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add add-method skill

Captures the add-one-Bluesky-SDK-method workflow so future me (or Claude) can follow the same path: METHOD_DEFS is single source of truth, record-based ops need dispatch overrides and PDS-level verification, and verification must read from the same layer the write went to.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>

+87
+87
.claude/skills/add-method/SKILL.md
··· 1 + --- 2 + name: add-method 3 + description: add a new Bluesky SDK method to the code-mode surface 4 + argument-hint: e.g. "blockActor" or "app.bsky.graph.blockActor" 5 + --- 6 + 7 + add a new method to the code-mode surface. method: $ARGUMENTS 8 + 9 + ## what this means 10 + 11 + METHOD_DEFS in `src/code-mode.ts` is the single source of truth. adding one entry automatically updates: 12 + - SDK_SURFACE (what Claude sees in the prompt) 13 + - ALLOWED_METHODS (string validator) 14 + - SANDBOX_PREAMBLE (sandbox stub, generated) 15 + - METHOD_DISPATCH (host replay table, generated) 16 + 17 + ## gotcha 1: is this a method or a record operation? 18 + 19 + before writing anything, classify the operation: 20 + 21 + - **direct method** (like `muteActor`, `putActivitySubscription`): `agent.x.y.z(input)` exists as a function on the agent. auto-generated METHOD_DISPATCH works out of the box. 22 + - **record operation** (like `block`, `follow`, `list`): the SDK exposes `.create()`, `.delete()`, `.list()` on a namespace. `agent.app.bsky.graph.block` is not a callable, it's an object with record methods. 23 + 24 + for record operations, METHOD_DEFS still describes the flat shape the model sees (`blockActor(input)`), but METHOD_DISPATCH needs a custom handler that translates: 25 + 26 + ```ts 27 + const METHOD_DISPATCH_OVERRIDES: Record<string, ...> = { 28 + blockActor: async (agent, input) => { 29 + const {actor} = input as {actor: string} 30 + await agent.app.bsky.graph.block.create( 31 + {repo: agent.did}, 32 + {subject: actor, createdAt: new Date().toISOString()}, 33 + ) 34 + }, 35 + unblockActor: async (agent, input) => { 36 + // list records, find matching subject, delete by rkey 37 + const records = await agent.app.bsky.graph.block.list({repo: agent.did}) 38 + const found = records.records.find(r => r.value.subject === actor) 39 + if (!found) return 40 + const rkey = found.uri.split('/').pop() 41 + await agent.app.bsky.graph.block.delete({repo: agent.did, rkey}) 42 + }, 43 + } 44 + ``` 45 + 46 + then merge overrides into METHOD_DISPATCH (overrides win). 47 + 48 + ## gotcha 2: verify at the layer you wrote 49 + 50 + verification compares before/after state to confirm the mutation took effect. the read path must match the write path: 51 + 52 + | write goes to | read from | why | 53 + |---------------|-----------|-----| 54 + | appview endpoint (muteActor, putActivitySubscription) | appview (listMutes, listActivitySubscriptions) | same layer, consistent | 55 + | PDS record (block, follow, list) | **PDS via listRecords**, not appview `getBlocks/getFollows` | appview indexing lag + appview outages race the read | 56 + 57 + for record-based ops, read the records directly from the user's PDS: 58 + 59 + ```ts 60 + const res = await agent.com.atproto.repo.listRecords({ 61 + repo: agent.did!, 62 + collection: 'app.bsky.graph.block', // or app.bsky.graph.follow, etc. 63 + limit: 100, 64 + }) 65 + // each record.value.subject is the target DID 66 + ``` 67 + 68 + this is zero-lag and works even when the appview is flaky. 69 + 70 + ## steps 71 + 72 + 1. **classify**: is this a direct method or a record operation? (see gotcha 1) 73 + 2. **research**: find the SDK shape in `node_modules/@atproto/api`. for record ops, note the record schema. 74 + 3. **add to METHOD_DEFS**: one entry with `fq`, `doc`, `inputType`, `input`, `signature`. follow existing entries as examples. use the flat method shape even for record ops — that's what the model sees. 75 + 4. **for record ops**: add a METHOD_DISPATCH_OVERRIDES entry in `src/code-mode.ts` that translates the flat call to the real record operation. 76 + 5. **prompt guidance**: decide if `SYSTEM_QUEUE_SUGGESTIONS` in `src/recommend-prompt.ts` needs steering (e.g. "prefer X over Y", "never suggest X if already X"). `SYSTEM_HANDLE_USER_REQUEST` usually needs nothing. 77 + 6. **verification** (src/bluesky.ts): 78 + - `ActorState` type — add the new state field 79 + - `getActorStates()` — read the new state (see gotcha 2 for record-based ops — use PDS listRecords) 80 + - `diffActorStates()` — detect changes in the new field 81 + - update `applyResultLines()` in `src/server.ts` — render the change for toast feedback 82 + 7. **management targets**: if the model needs to know the current state to make good recommendations, add the field to `ManagementTarget` in `src/types.ts` and populate it in `buildManagementTargets()`. thread the new `Set<string>` through callers. 83 + 8. **verify**: `bun run check && bun run lint`, then test end-to-end with a throwaway script: 84 + ```bash 85 + bun -e 'import { executeCode } from "./src/code-mode.ts"; ...' 86 + ``` 87 + restart the server and try ask-noti.