a tool for shared writing and social publishing

refactor skills

+539
+304
.claude/skills/lexicons/SKILL.md
··· 1 + --- 2 + name: lexicons 3 + description: Reference for the AT Protocol lexicon system. Use when working with lexicons, adding new record types, or modifying AT Protocol schemas. 4 + user-invocable: false 5 + --- 6 + 7 + # Lexicon System 8 + 9 + ## Overview 10 + 11 + Lexicons define the schema for AT Protocol records. This project has two namespaces: 12 + - **`pub.leaflet.*`** - Leaflet-specific lexicons (documents, publications, blocks, etc.) 13 + - **`site.standard.*`** - Standard site lexicons for interoperability 14 + 15 + The lexicons are defined as TypeScript in `lexicons/src/`, built to JSON in `lexicons/pub/leaflet/` and `lexicons/site/standard/`, and TypeScript types are generated in `lexicons/api/`. 16 + 17 + ## Key Files 18 + 19 + - **`lexicons/src/*.ts`** - Source definitions for `pub.leaflet.*` lexicons 20 + - **`lexicons/site/standard/**/*.json`** - JSON definitions for `site.standard.*` lexicons (manually maintained) 21 + - **`lexicons/build.ts`** - Builds TypeScript sources to JSON 22 + - **`lexicons/api/`** - Generated TypeScript types and client 23 + - **`package.json`** - Contains `lexgen` script 24 + 25 + ## Running Lexicon Generation 26 + 27 + ```bash 28 + npm run lexgen 29 + ``` 30 + 31 + This runs: 32 + 1. `tsx ./lexicons/build.ts` - Builds `pub.leaflet.*` JSON from TypeScript 33 + 2. `lex gen-api` - Generates TypeScript types from all JSON lexicons 34 + 3. `tsx ./lexicons/fix-extensions.ts` - Fixes import extensions 35 + 36 + ## Adding a New pub.leaflet Lexicon 37 + 38 + ### 1. Create the Source Definition 39 + 40 + Create a file in `lexicons/src/` (e.g., `lexicons/src/myLexicon.ts`): 41 + 42 + ```typescript 43 + import { LexiconDoc } from "@atproto/lexicon"; 44 + 45 + export const PubLeafletMyLexicon: LexiconDoc = { 46 + lexicon: 1, 47 + id: "pub.leaflet.myLexicon", 48 + defs: { 49 + main: { 50 + type: "record", // or "object" for non-record types 51 + key: "tid", 52 + record: { 53 + type: "object", 54 + required: ["field1"], 55 + properties: { 56 + field1: { type: "string", maxLength: 1000 }, 57 + field2: { type: "integer", minimum: 0 }, 58 + optionalRef: { type: "ref", ref: "other.lexicon#def" }, 59 + }, 60 + }, 61 + }, 62 + // Additional defs for sub-objects 63 + subType: { 64 + type: "object", 65 + properties: { 66 + nested: { type: "string" }, 67 + }, 68 + }, 69 + }, 70 + }; 71 + ``` 72 + 73 + ### 2. Add to Build 74 + 75 + Update `lexicons/build.ts`: 76 + 77 + ```typescript 78 + import { PubLeafletMyLexicon } from "./src/myLexicon"; 79 + 80 + const lexicons = [ 81 + // ... existing lexicons 82 + PubLeafletMyLexicon, 83 + ]; 84 + ``` 85 + 86 + ### 3. Update lexgen Command (if needed) 87 + 88 + If your lexicon is at the top level of `pub/leaflet/` (not in a subdirectory), add it to the `lexgen` script in `package.json`: 89 + 90 + ```json 91 + "lexgen": "tsx ./lexicons/build.ts && lex gen-api ./lexicons/api ./lexicons/pub/leaflet/document.json ./lexicons/pub/leaflet/myLexicon.json ./lexicons/pub/leaflet/*/* ..." 92 + ``` 93 + 94 + Note: Files in subdirectories (`pub/leaflet/*/*`) are automatically included. 95 + 96 + ### 4. Add to authFullPermissions (for record types) 97 + 98 + If your lexicon is a record type that users should be able to create/update/delete, add it to the `authFullPermissions` permission set in `lexicons/src/authFullPermissions.ts`: 99 + 100 + ```typescript 101 + import { PubLeafletMyLexicon } from "./myLexicon"; 102 + 103 + // In the permissions collection array: 104 + collection: [ 105 + // ... existing lexicons 106 + PubLeafletMyLexicon.id, 107 + ], 108 + ``` 109 + 110 + ### 5. Regenerate Types 111 + 112 + ```bash 113 + npm run lexgen 114 + ``` 115 + 116 + ### 6. Use the Generated Types 117 + 118 + ```typescript 119 + import { PubLeafletMyLexicon } from "lexicons/api"; 120 + 121 + // Type for the record 122 + type MyRecord = PubLeafletMyLexicon.Record; 123 + 124 + // Validation 125 + const result = PubLeafletMyLexicon.validateRecord(data); 126 + if (result.success) { 127 + // result.value is typed 128 + } 129 + 130 + // Type guard 131 + if (PubLeafletMyLexicon.isRecord(data)) { 132 + // data is typed as Record 133 + } 134 + ``` 135 + 136 + ## Adding a New site.standard Lexicon 137 + 138 + ### 1. Create the JSON Definition 139 + 140 + Create a file in `lexicons/site/standard/` (e.g., `lexicons/site/standard/myType.json`): 141 + 142 + ```json 143 + { 144 + "lexicon": 1, 145 + "id": "site.standard.myType", 146 + "defs": { 147 + "main": { 148 + "type": "record", 149 + "key": "tid", 150 + "record": { 151 + "type": "object", 152 + "required": ["field1"], 153 + "properties": { 154 + "field1": { 155 + "type": "string", 156 + "maxLength": 1000 157 + } 158 + } 159 + } 160 + } 161 + } 162 + } 163 + ``` 164 + 165 + ### 2. Regenerate Types 166 + 167 + ```bash 168 + npm run lexgen 169 + ``` 170 + 171 + The `site/*/* site/*/*/*` globs in the lexgen command automatically pick up new files. 172 + 173 + ## Common Lexicon Patterns 174 + 175 + ### Referencing Other Lexicons 176 + 177 + ```typescript 178 + // Reference another lexicon's main def 179 + { type: "ref", ref: "pub.leaflet.publication" } 180 + 181 + // Reference a specific def within a lexicon 182 + { type: "ref", ref: "pub.leaflet.publication#theme" } 183 + 184 + // Reference within the same lexicon 185 + { type: "ref", ref: "#myDef" } 186 + ``` 187 + 188 + ### Union Types 189 + 190 + ```typescript 191 + { 192 + type: "union", 193 + refs: [ 194 + "pub.leaflet.pages.linearDocument", 195 + "pub.leaflet.pages.canvas", 196 + ], 197 + } 198 + 199 + // Open union (allows unknown types) 200 + { 201 + type: "union", 202 + closed: false, // default is true 203 + refs: ["pub.leaflet.content"], 204 + } 205 + ``` 206 + 207 + ### Blob Types (for images/files) 208 + 209 + ```typescript 210 + { 211 + type: "blob", 212 + accept: ["image/*"], // or specific types like ["image/png", "image/jpeg"] 213 + maxSize: 1000000, // bytes 214 + } 215 + ``` 216 + 217 + ### Color Types 218 + 219 + The project has color types defined: 220 + - `pub.leaflet.theme.color#rgb` / `#rgba` 221 + - `site.standard.theme.color#rgb` / `#rgba` 222 + 223 + ```typescript 224 + // In lexicons/src/theme.ts 225 + export const ColorUnion = { 226 + type: "union", 227 + refs: [ 228 + "pub.leaflet.theme.color#rgba", 229 + "pub.leaflet.theme.color#rgb", 230 + ], 231 + }; 232 + ``` 233 + 234 + ## Normalization Between Formats 235 + 236 + Use `lexicons/src/normalize.ts` to convert between `pub.leaflet` and `site.standard` formats: 237 + 238 + ```typescript 239 + import { 240 + normalizeDocument, 241 + normalizePublication, 242 + isLeafletDocument, 243 + isStandardDocument, 244 + getDocumentPages, 245 + } from "lexicons/src/normalize"; 246 + 247 + // Normalize a document from either format 248 + const normalized = normalizeDocument(record); 249 + if (normalized) { 250 + // normalized is always in site.standard.document format 251 + console.log(normalized.title, normalized.site); 252 + 253 + // Get pages if content is pub.leaflet.content 254 + const pages = getDocumentPages(normalized); 255 + } 256 + 257 + // Normalize a publication 258 + const pub = normalizePublication(record); 259 + if (pub) { 260 + console.log(pub.name, pub.url); 261 + } 262 + ``` 263 + 264 + ## Handling in Appview (Firehose Consumer) 265 + 266 + When processing records from the firehose in `appview/index.ts`: 267 + 268 + ```typescript 269 + import { ids } from "lexicons/api/lexicons"; 270 + import { PubLeafletMyLexicon } from "lexicons/api"; 271 + 272 + // In filterCollections: 273 + filterCollections: [ 274 + ids.PubLeafletMyLexicon, 275 + // ... 276 + ], 277 + 278 + // In handleEvent: 279 + if (evt.collection === ids.PubLeafletMyLexicon) { 280 + if (evt.event === "create" || evt.event === "update") { 281 + let record = PubLeafletMyLexicon.validateRecord(evt.record); 282 + if (!record.success) return; 283 + 284 + // Store in database 285 + await supabase.from("my_table").upsert({ 286 + uri: evt.uri.toString(), 287 + data: record.value as Json, 288 + }); 289 + } 290 + if (evt.event === "delete") { 291 + await supabase.from("my_table").delete().eq("uri", evt.uri.toString()); 292 + } 293 + } 294 + ``` 295 + 296 + ## Publishing Lexicons 297 + 298 + To publish lexicons to an AT Protocol PDS: 299 + 300 + ```bash 301 + npm run publish-lexicons 302 + ``` 303 + 304 + This runs `lexicons/publish.ts` which publishes lexicons to the configured PDS.
+150
.claude/skills/notifications/SKILL.md
··· 1 + --- 2 + name: notifications 3 + description: Reference for the notification system. Use when adding new notification types or modifying notification handling. 4 + user-invocable: false 5 + --- 6 + 7 + # Notification System 8 + 9 + ## Overview 10 + 11 + Notifications are stored in the database and hydrated with related data before being rendered. The system supports multiple notification types (comments, subscriptions, etc.) that are processed in parallel. 12 + 13 + ## Key Files 14 + 15 + - **`src/notifications.ts`** - Core notification types and hydration logic 16 + - **`app/(home-pages)/notifications/NotificationList.tsx`** - Renders all notification types 17 + - **`app/(home-pages)/notifications/Notification.tsx`** - Base notification component 18 + - Individual notification components (e.g., `CommentNotification.tsx`, `FollowNotification.tsx`) 19 + 20 + ## Adding a New Notification Type 21 + 22 + ### 1. Update Notification Data Types (`src/notifications.ts`) 23 + 24 + Add your type to the `NotificationData` union: 25 + 26 + ```typescript 27 + export type NotificationData = 28 + | { type: "comment"; comment_uri: string; parent_uri?: string } 29 + | { type: "subscribe"; subscription_uri: string } 30 + | { type: "your_type"; your_field: string }; // Add here 31 + ``` 32 + 33 + Add to the `HydratedNotification` union: 34 + 35 + ```typescript 36 + export type HydratedNotification = 37 + | HydratedCommentNotification 38 + | HydratedSubscribeNotification 39 + | HydratedYourNotification; // Add here 40 + ``` 41 + 42 + ### 2. Create Hydration Function (`src/notifications.ts`) 43 + 44 + ```typescript 45 + export type HydratedYourNotification = Awaited< 46 + ReturnType<typeof hydrateYourNotifications> 47 + >[0]; 48 + 49 + async function hydrateYourNotifications(notifications: NotificationRow[]) { 50 + const yourNotifications = notifications.filter( 51 + (n): n is NotificationRow & { data: ExtractNotificationType<"your_type"> } => 52 + (n.data as NotificationData)?.type === "your_type", 53 + ); 54 + 55 + if (yourNotifications.length === 0) return []; 56 + 57 + // Fetch related data with joins 58 + const { data } = await supabaseServerClient 59 + .from("your_table") 60 + .select("*, related_table(*)") 61 + .in("uri", yourNotifications.map((n) => n.data.your_field)); 62 + 63 + return yourNotifications.map((notification) => ({ 64 + id: notification.id, 65 + recipient: notification.recipient, 66 + created_at: notification.created_at, 67 + type: "your_type" as const, 68 + your_field: notification.data.your_field, 69 + yourData: data?.find((d) => d.uri === notification.data.your_field)!, 70 + })); 71 + } 72 + ``` 73 + 74 + Add to `hydrateNotifications` parallel array: 75 + 76 + ```typescript 77 + const [commentNotifications, subscribeNotifications, yourNotifications] = await Promise.all([ 78 + hydrateCommentNotifications(notifications), 79 + hydrateSubscribeNotifications(notifications), 80 + hydrateYourNotifications(notifications), // Add here 81 + ]); 82 + 83 + const allHydrated = [...commentNotifications, ...subscribeNotifications, ...yourNotifications]; 84 + ``` 85 + 86 + ### 3. Trigger the Notification (in your action file) 87 + 88 + ```typescript 89 + import { Notification, pingIdentityToUpdateNotification } from "src/notifications"; 90 + import { v7 } from "uuid"; 91 + 92 + // When the event occurs: 93 + const recipient = /* determine who should receive it */; 94 + if (recipient !== currentUser) { 95 + const notification: Notification = { 96 + id: v7(), 97 + recipient, 98 + data: { 99 + type: "your_type", 100 + your_field: "value", 101 + }, 102 + }; 103 + await supabaseServerClient.from("notifications").insert(notification); 104 + await pingIdentityToUpdateNotification(recipient); 105 + } 106 + ``` 107 + 108 + ### 4. Create Notification Component 109 + 110 + Create a new component (e.g., `YourNotification.tsx`): 111 + 112 + ```typescript 113 + import { HydratedYourNotification } from "src/notifications"; 114 + import { Notification } from "./Notification"; 115 + 116 + export const YourNotification = (props: HydratedYourNotification) => { 117 + // Extract data from props.yourData 118 + 119 + return ( 120 + <Notification 121 + timestamp={props.created_at} 122 + href={/* link to relevant page */} 123 + icon={/* icon or avatar */} 124 + actionText={<>Message to display</>} 125 + content={/* optional additional content */} 126 + /> 127 + ); 128 + }; 129 + ``` 130 + 131 + ### 5. Update NotificationList (`NotificationList.tsx`) 132 + 133 + Import and render your notification type: 134 + 135 + ```typescript 136 + import { YourNotification } from "./YourNotification"; 137 + 138 + // In the map function: 139 + if (n.type === "your_type") { 140 + return <YourNotification key={n.id} {...n} />; 141 + } 142 + ``` 143 + 144 + ## Example: Subscribe Notifications 145 + 146 + See the implementation in: 147 + - `src/notifications.ts:88-125` - Hydration logic 148 + - `app/lish/subscribeToPublication.ts:55-68` - Trigger 149 + - `app/(home-pages)/notifications/FollowNotification.tsx` - Component 150 + - `app/(home-pages)/notifications/NotificationList.tsx:40-42` - Rendering
+85
.claude/skills/spec/SKILL.md
··· 1 + --- 2 + name: spec 3 + description: Draft design documents and specs with research-informed questioning 4 + disable-model-invocation: true 5 + --- 6 + 7 + # Spec Writing 8 + 9 + ## When to Use 10 + 11 + Use this skill when the user explicitly requests a spec, design document, or similar planning artifact. Do not auto-trigger for routine tasks. 12 + 13 + ## Process 14 + 15 + ### 1. Initial Scoping 16 + 17 + When starting a spec: 18 + - Ask the user for a brief description of what they want to build/change 19 + - Reason about which question categories matter for this particular work—derive these from the problem, not a checklist 20 + 21 + ### 2. Interrogation Loop 22 + 23 + Conduct open-ended questioning until the designer signals completion: 24 + 25 + **Research-informed questions**: Before asking about an area, do just-in-time codebase research. Find relevant files, understand current patterns, identify constraints. Reference specific files and functions in questions when it adds clarity. 26 + 27 + **Adaptive scope**: When the designer marks something as out of scope, update your mental model and don't revisit. However, flag when you believe there are gaps that could cause problems: 28 + - "This approach assumes X—is that intentional?" 29 + - "The current implementation of Y in `src/foo.ts:42` handles Z differently. Should we align or diverge?" 30 + 31 + **Question style**: 32 + - Ask one focused question at a time, or a small related cluster 33 + - Ground questions in what you've learned from the codebase 34 + - Avoid hypotheticals that don't apply to this codebase 35 + - When presenting options, describe tradeoffs without recommending unless asked 36 + 37 + ### 3. Spec Writing 38 + 39 + When the designer indicates readiness, produce the spec document. 40 + 41 + ## Output 42 + 43 + Save to: `/specs/YYYY-MM-DD-short-name.md` 44 + 45 + ### Required Elements 46 + 47 + **Title and status** (draft | approved | implemented) 48 + 49 + **Goal**: What this achieves and why. 1-3 sentences. 50 + 51 + **Design**: The substance of what will be built/changed. For each significant component or concern: 52 + - Describe the approach 53 + - State key decisions with their rationale inline 54 + - Reference specific files, functions, and types where relevant 55 + - Structure subsections to fit the problem—no fixed format 56 + 57 + **Implementation**: Ordered steps that an agent or developer can execute. Each step should: 58 + - Be concrete and actionable 59 + - Reference specific files/functions to modify or create 60 + - Be sequenced correctly (dependencies before dependents) 61 + 62 + ### Optional 63 + 64 + - Background context (only if necessary for understanding) 65 + - Open questions (only if unresolved items remain) 66 + 67 + Add other sections if the problem demands it. 68 + 69 + ## Writing Style 70 + 71 + - No filler, hedging, or preamble 72 + - No "This document describes..." or "In this spec we will..." 73 + - Start sections with substance, not meta-commentary 74 + - Use precise technical language 75 + - Keep decisions and rationale tight—one sentence each when possible 76 + - Code references use `file/path.ts:lineNumber` or `functionName` in backticks 77 + - Prefer concrete over abstract; specific over general 78 + 79 + ## Status Lifecycle 80 + 81 + - **draft**: Work in progress. Should not be merged to main. 82 + - **approved**: Designer has signed off. Ready for implementation. 83 + - **implemented**: Work is complete. Spec is now historical record. 84 + 85 + Update status in the document as it progresses. Specs are point-in-time snapshots—do not update content after implementation begins except to change status.