@gallopsystems/agent-skills 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/README.md +137 -0
  2. package/package.json +26 -0
  3. package/plugins/doctl/.claude-plugin/plugin.json +8 -0
  4. package/plugins/doctl/skills/doctl/SKILL.md +93 -0
  5. package/plugins/kysely-postgres/.claude-plugin/plugin.json +8 -0
  6. package/plugins/kysely-postgres/skills/kysely-postgres/SKILL.md +1101 -0
  7. package/plugins/kysely-postgres/skills/kysely-postgres/references/aggregations.ts +167 -0
  8. package/plugins/kysely-postgres/skills/kysely-postgres/references/ctes.ts +165 -0
  9. package/plugins/kysely-postgres/skills/kysely-postgres/references/expressions.ts +272 -0
  10. package/plugins/kysely-postgres/skills/kysely-postgres/references/joins.ts +206 -0
  11. package/plugins/kysely-postgres/skills/kysely-postgres/references/json-arrays.ts +398 -0
  12. package/plugins/kysely-postgres/skills/kysely-postgres/references/mutations.ts +199 -0
  13. package/plugins/kysely-postgres/skills/kysely-postgres/references/orderby-pagination.ts +117 -0
  14. package/plugins/kysely-postgres/skills/kysely-postgres/references/relations.ts +176 -0
  15. package/plugins/kysely-postgres/skills/kysely-postgres/references/select-where.ts +146 -0
  16. package/plugins/linear/.claude-plugin/plugin.json +8 -0
  17. package/plugins/linear/skills/linear/SKILL.md +1040 -0
  18. package/plugins/linear/skills/linear/bin/linear.mjs +1228 -0
  19. package/plugins/linear/skills/linear/tech-stack.md +273 -0
  20. package/plugins/nitro-testing/.claude-plugin/plugin.json +8 -0
  21. package/plugins/nitro-testing/skills/nitro-testing/SKILL.md +497 -0
  22. package/plugins/nitro-testing/skills/nitro-testing/async-testing.md +270 -0
  23. package/plugins/nitro-testing/skills/nitro-testing/ci-setup.md +226 -0
  24. package/plugins/nitro-testing/skills/nitro-testing/examples/global-setup.ts +90 -0
  25. package/plugins/nitro-testing/skills/nitro-testing/examples/handler.test.ts +167 -0
  26. package/plugins/nitro-testing/skills/nitro-testing/examples/setup.ts +29 -0
  27. package/plugins/nitro-testing/skills/nitro-testing/examples/test-utils-index.ts +297 -0
  28. package/plugins/nitro-testing/skills/nitro-testing/examples/vitest.config.ts +42 -0
  29. package/plugins/nitro-testing/skills/nitro-testing/factories.md +278 -0
  30. package/plugins/nitro-testing/skills/nitro-testing/frontend-testing.md +512 -0
  31. package/plugins/nitro-testing/skills/nitro-testing/test-utils.md +262 -0
  32. package/plugins/nitro-testing/skills/nitro-testing/transaction-rollback.md +183 -0
  33. package/plugins/nitro-testing/skills/nitro-testing/vitest-config.md +236 -0
  34. package/plugins/nuxt-nitro-api/.claude-plugin/plugin.json +8 -0
  35. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/SKILL.md +260 -0
  36. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/auth-patterns.md +228 -0
  37. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/composables-utils.md +174 -0
  38. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/deep-linking.md +190 -0
  39. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-middleware.ts +32 -0
  40. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/auth-utils.ts +51 -0
  41. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/deep-link-page.vue +61 -0
  42. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/service-util.ts +63 -0
  43. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/sse-endpoint.ts +59 -0
  44. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/examples/validation-endpoint.ts +38 -0
  45. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/fetch-patterns.md +178 -0
  46. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/nitro-tasks.md +243 -0
  47. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/page-structure.md +162 -0
  48. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/server-services.md +238 -0
  49. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/sse.md +221 -0
  50. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/ssr-client.md +166 -0
  51. package/plugins/nuxt-nitro-api/skills/nuxt-nitro-api/validation.md +131 -0
  52. package/scripts/link-skills.mjs +252 -0
@@ -0,0 +1,221 @@
1
+ # Server-Sent Events (SSE)
2
+
3
+ > **Example:** [sse-endpoint.ts](./examples/sse-endpoint.ts)
4
+
5
+ Real-time streaming without WebSockets. Good for long-running operations, AI streaming, job progress.
6
+
7
+ ## Server-Side (Nitro)
8
+
9
+ ```typescript
10
+ // server/api/stream/[id].get.ts
11
+ export default defineEventHandler(async (event) => {
12
+ const { id } = getRouterParams(event);
13
+
14
+ // Create the event stream
15
+ const eventStream = createEventStream(event);
16
+
17
+ let done = false;
18
+
19
+ // Handle client disconnect
20
+ eventStream.onClosed(async () => {
21
+ console.log("Client disconnected");
22
+ done = true;
23
+ await eventStream.close();
24
+ });
25
+
26
+ // Async loop to push events
27
+ (async () => {
28
+ while (!done) {
29
+ const data = await getNextChunk(id);
30
+
31
+ if (data) {
32
+ await eventStream.push(JSON.stringify(data));
33
+
34
+ if (data.type === "done" || data.type === "error") {
35
+ done = true;
36
+ }
37
+ } else {
38
+ await new Promise((r) => setTimeout(r, 1000));
39
+ }
40
+ }
41
+
42
+ await eventStream.close();
43
+ })();
44
+
45
+ return eventStream.send();
46
+ });
47
+ ```
48
+
49
+ ### Heartbeat Pattern
50
+
51
+ Keep connections alive:
52
+
53
+ ```typescript
54
+ const heartbeatInterval = setInterval(async () => {
55
+ await eventStream.push(JSON.stringify({ type: "heartbeat" }));
56
+ }, 30000);
57
+
58
+ eventStream.onClosed(() => {
59
+ clearInterval(heartbeatInterval);
60
+ });
61
+ ```
62
+
63
+ ## Client-Side Option 1: VueUse (Recommended)
64
+
65
+ ```typescript
66
+ import { useEventSource } from "@vueuse/core";
67
+
68
+ const { status, data, error, close } = useEventSource(
69
+ `/api/stream/${sessionId}`,
70
+ [], // Event names (empty = default "message")
71
+ {
72
+ autoReconnect: {
73
+ retries: 3,
74
+ delay: 1000,
75
+ onFailed() {
76
+ console.error("Failed to reconnect");
77
+ },
78
+ },
79
+ }
80
+ );
81
+
82
+ watch(data, (newData) => {
83
+ if (newData) {
84
+ const parsed = JSON.parse(newData);
85
+ // Handle the event...
86
+ }
87
+ });
88
+
89
+ onUnmounted(close);
90
+ ```
91
+
92
+ ## Client-Side Option 2: Custom Composable
93
+
94
+ For more control:
95
+
96
+ ```typescript
97
+ // composables/useSSE.ts
98
+ export function useSSE() {
99
+ const eventSource = ref<EventSource | null>(null);
100
+ const data = ref<any>(null);
101
+ const error = ref<string | null>(null);
102
+ const status = ref<"connecting" | "connected" | "closed">("connecting");
103
+
104
+ const connect = (url: string) => {
105
+ stop();
106
+
107
+ eventSource.value = new EventSource(url);
108
+
109
+ eventSource.value.onopen = () => {
110
+ status.value = "connected";
111
+ };
112
+
113
+ eventSource.value.onmessage = (event) => {
114
+ try {
115
+ const parsed = JSON.parse(event.data);
116
+ data.value = parsed;
117
+
118
+ if (parsed.type === "done" || parsed.type === "error") {
119
+ stop();
120
+ }
121
+ } catch (e) {
122
+ console.error("Parse error:", e);
123
+ }
124
+ };
125
+
126
+ eventSource.value.onerror = () => {
127
+ error.value = "Connection error";
128
+ status.value = "closed";
129
+
130
+ // Auto-reconnect
131
+ setTimeout(() => {
132
+ if (status.value === "closed") {
133
+ connect(url);
134
+ }
135
+ }, 2000);
136
+ };
137
+ };
138
+
139
+ const stop = () => {
140
+ if (eventSource.value) {
141
+ eventSource.value.close();
142
+ eventSource.value = null;
143
+ }
144
+ status.value = "closed";
145
+ };
146
+
147
+ onUnmounted(stop);
148
+
149
+ return { connect, stop, data, error, status };
150
+ }
151
+ ```
152
+
153
+ ## Usage in Component
154
+
155
+ ```typescript
156
+ const { connect, stop, data, status } = useSSE();
157
+
158
+ const startAnalysis = async () => {
159
+ const { sessionId } = await $fetch("/api/analysis/start", { method: "POST" });
160
+ connect(`/api/analysis/${sessionId}/stream`);
161
+ };
162
+
163
+ watch(data, (newData) => {
164
+ if (newData?.type === "chunk") {
165
+ output.value += newData.text;
166
+ } else if (newData?.type === "done") {
167
+ isComplete.value = true;
168
+ }
169
+ });
170
+
171
+ onUnmounted(stop);
172
+ ```
173
+
174
+ ## Position-Based Resumption
175
+
176
+ Resume from where client left off:
177
+
178
+ ```typescript
179
+ // Client tracks position
180
+ const position = ref(0);
181
+
182
+ eventSource.value.onmessage = (event) => {
183
+ position.value++;
184
+ // handle data...
185
+ };
186
+
187
+ const reconnect = () => {
188
+ connect(`/api/stream/${id}?position=${position.value}`);
189
+ };
190
+ ```
191
+
192
+ ```typescript
193
+ // Server reads position
194
+ const { position } = await getValidatedQuery(event, schema);
195
+ const chunks = await getChunksFromPosition(id, position);
196
+ ```
197
+
198
+ ## Fallback to Polling
199
+
200
+ When SSE isn't available:
201
+
202
+ ```typescript
203
+ // Server returns non-SSE response
204
+ if (!redisAvailable) {
205
+ return { type: "pending", message: "Use polling" };
206
+ }
207
+
208
+ // Client detects and falls back
209
+ if (data.value?.type === "pending") {
210
+ stopSSE();
211
+ startPolling();
212
+ }
213
+ ```
214
+
215
+ ## Key Gotchas
216
+
217
+ 1. **Always clean up** - Call `eventSource.close()` on unmount
218
+ 2. **Parse JSON** - SSE data is always strings
219
+ 3. **Handle reconnection** - Connections drop, plan for it
220
+ 4. **Timeouts** - Long streams need heartbeats
221
+ 5. **No binary data** - SSE is text-only, use base64 if needed
@@ -0,0 +1,166 @@
1
+ # SSR + Client-side Patterns
2
+
3
+ ## The Problem
4
+
5
+ `localStorage` and other browser APIs don't exist on the server. Accessing them during SSR causes errors or hydration mismatches.
6
+
7
+ ## Solutions
8
+
9
+ ### 1. `<ClientOnly>` Component
10
+
11
+ Wrap components that need browser APIs:
12
+
13
+ ```vue
14
+ <ClientOnly>
15
+ <DataTable :value="items" />
16
+
17
+ <template #fallback>
18
+ <div>Loading table...</div>
19
+ </template>
20
+ </ClientOnly>
21
+ ```
22
+
23
+ **Use for:**
24
+ - Complex interactive components (DataTables, Maps, Charts)
25
+ - Components using DOM APIs
26
+ - Third-party components without SSR support
27
+
28
+ ### 2. `import.meta.client` Guard
29
+
30
+ Check runtime environment before using browser APIs:
31
+
32
+ ```typescript
33
+ watch(viewMode, (newMode) => {
34
+ if (import.meta.client) {
35
+ localStorage.setItem("view-mode", newMode);
36
+ }
37
+ });
38
+
39
+ const savePreference = (key: string, value: string) => {
40
+ if (import.meta.client) {
41
+ localStorage.setItem(key, value);
42
+ }
43
+ };
44
+ ```
45
+
46
+ Also available: `import.meta.server` for server-only code.
47
+
48
+ ### 3. `onMounted` for Client Initialization
49
+
50
+ Read from localStorage only after hydration:
51
+
52
+ ```typescript
53
+ const viewMode = ref("table"); // Default for SSR
54
+ const isReady = ref(false);
55
+
56
+ onMounted(() => {
57
+ const saved = localStorage.getItem("view-mode");
58
+ if (saved === "table" || saved === "kanban") {
59
+ viewMode.value = saved;
60
+ }
61
+ isReady.value = true;
62
+ });
63
+ ```
64
+
65
+ **Pattern for URL params + localStorage fallback:**
66
+ ```typescript
67
+ onMounted(() => {
68
+ const queryTab = route.query.tab as string;
69
+
70
+ if (queryTab && validTabs.includes(queryTab)) {
71
+ activeTab.value = queryTab;
72
+ } else {
73
+ const savedTab = localStorage.getItem("last-tab");
74
+ if (savedTab && validTabs.includes(savedTab)) {
75
+ activeTab.value = savedTab;
76
+ router.replace({ query: { tab: savedTab } });
77
+ }
78
+ }
79
+ });
80
+ ```
81
+
82
+ ### 4. VueUse `useLocalStorage` (SSR-Safe)
83
+
84
+ Automatically handles SSR - reads on client after hydration:
85
+
86
+ ```typescript
87
+ // Returns default during SSR, actual value on client
88
+ const theme = useLocalStorage("theme", "light");
89
+ const settings = useLocalStorage("settings", { compact: false });
90
+
91
+ // Use normally - syncs automatically
92
+ theme.value = "dark";
93
+ ```
94
+
95
+ For delayed initialization to avoid hydration issues:
96
+ ```typescript
97
+ const theme = useLocalStorage("theme", "light", {
98
+ initOnMounted: true, // Don't read until mounted
99
+ });
100
+ ```
101
+
102
+ ## VueUse SSR Notes
103
+
104
+ With `@vueuse/nuxt`, these are auto-imported:
105
+ - `refDebounced` - Yes, auto-imported
106
+ - `useDebounceFn` - Yes
107
+ - `useLocalStorage` - Yes
108
+ - `useUrlSearchParams` - Yes
109
+
110
+ **Disabled by default** (conflict with Nuxt):
111
+ - `useRoute` - use Nuxt's version
112
+ - `useRouter` - use Nuxt's version
113
+ - `useFetch` - use Nuxt's version
114
+ - `useHead` - use Nuxt's version
115
+
116
+ ## Hydration Mismatch Prevention
117
+
118
+ **Problem:** Server renders with default, client reads different value = mismatch.
119
+
120
+ **Solutions:**
121
+
122
+ 1. **Don't render during SSR:**
123
+ ```vue
124
+ <ClientOnly>
125
+ <span>{{ preference }}</span>
126
+ </ClientOnly>
127
+ ```
128
+
129
+ 2. **Use a ready flag:**
130
+ ```typescript
131
+ const preference = ref("default");
132
+ const ready = ref(false);
133
+
134
+ onMounted(() => {
135
+ preference.value = localStorage.getItem("pref") || "default";
136
+ ready.value = true;
137
+ });
138
+ ```
139
+ ```vue
140
+ <span v-if="ready">{{ preference }}</span>
141
+ <span v-else>Loading...</span>
142
+ ```
143
+
144
+ 3. **Use `useLocalStorage` with matching initial:**
145
+ ```typescript
146
+ const count = useLocalStorage("count", 0);
147
+ // Initial matches SSR, updates after hydration
148
+ ```
149
+
150
+ ## Summary Table
151
+
152
+ | Approach | When to Use | SSR-Safe |
153
+ |----------|-------------|----------|
154
+ | `<ClientOnly>` | Entire component needs browser | Yes |
155
+ | `import.meta.client` | Conditional browser API calls | Yes |
156
+ | `onMounted` | Initialize from localStorage | Yes |
157
+ | `useLocalStorage` | Reactive persistent state | Yes |
158
+ | Direct `localStorage` | Never at top level | No |
159
+
160
+ ## Key Gotchas
161
+
162
+ 1. **Never access `localStorage` at module top-level**
163
+ 2. **`useLocalStorage` returns default during SSR**
164
+ 3. **URL query params are SSR-safe** - can read via `useRoute()`
165
+ 4. **Watch handlers run during SSR** - always guard with `import.meta.client`
166
+ 5. **`onMounted` never runs on server** - safe for all browser APIs
@@ -0,0 +1,131 @@
1
+ # Validation Patterns
2
+
3
+ > **Example:** [validation-endpoint.ts](./examples/validation-endpoint.ts)
4
+
5
+ ## Available Utilities (all auto-imported from h3)
6
+
7
+ | Raw | Validated |
8
+ |-----|-----------|
9
+ | `readBody(event)` | `readValidatedBody(event, validator)` |
10
+ | `getQuery(event)` | `getValidatedQuery(event, validator)` |
11
+ | `getRouterParams(event)` | `getValidatedRouterParams(event, validator)` |
12
+
13
+ Note: It's `getRouterParams` (plural), not `getRouterParam`.
14
+
15
+ ## Pattern 1: Direct Schema (h3 v2+ with Standard Schema)
16
+
17
+ h3 v2+ supports Standard Schema, meaning you can pass Zod schemas directly:
18
+
19
+ ```typescript
20
+ const querySchema = z.object({
21
+ search: z.string().min(1),
22
+ page: z.coerce.number().default(1),
23
+ });
24
+
25
+ // Pass schema directly (recommended)
26
+ const query = await getValidatedQuery(event, querySchema);
27
+
28
+ // Also works for body and params
29
+ const body = await readValidatedBody(event, bodySchema);
30
+ const params = await getValidatedRouterParams(event, paramsSchema);
31
+ ```
32
+
33
+ **Pros:** Simplest syntax, cleaner code
34
+ **Cons:** ZodError thrown directly - not user-friendly
35
+
36
+ ## Pattern 2: Manual Validator Function
37
+
38
+ For custom validation logic:
39
+
40
+ ```typescript
41
+ const query = await getValidatedQuery(event, (data) => querySchema.parse(data));
42
+ ```
43
+
44
+ ## Pattern 3: safeParse for Better Errors
45
+
46
+ ```typescript
47
+ import { fromZodError } from "zod-validation-error";
48
+
49
+ const rawQuery = getQuery(event);
50
+ const result = querySchema.safeParse(rawQuery);
51
+
52
+ if (!result.success) {
53
+ console.error("Validation error:", result.error); // Dev log
54
+ const userError = fromZodError(result.error); // User-friendly
55
+ throw createError({
56
+ statusCode: 400,
57
+ statusMessage: "Bad Request",
58
+ message: userError.message,
59
+ });
60
+ }
61
+
62
+ return result.data;
63
+ ```
64
+
65
+ ## Common Zod Patterns
66
+
67
+ ### Query Parameters
68
+
69
+ ```typescript
70
+ const querySchema = z.object({
71
+ // Optional string
72
+ search: z.string().optional(),
73
+
74
+ // Coerce to number (query params are strings)
75
+ page: z.coerce.number().default(1),
76
+ limit: z.coerce.number().max(100).default(20),
77
+
78
+ // Boolean from string
79
+ active: z.enum(["true", "false"]).transform(v => v === "true").optional(),
80
+
81
+ // Enum
82
+ status: z.enum(["pending", "active", "closed"]).optional(),
83
+
84
+ // Array from comma-separated
85
+ tags: z.string().transform(s => s.split(",")).optional(),
86
+ });
87
+ ```
88
+
89
+ ### Request Body
90
+
91
+ ```typescript
92
+ const createUserSchema = z.object({
93
+ email: z.string().email(),
94
+ name: z.string().min(1).max(100),
95
+ role: z.enum(["admin", "user"]).default("user"),
96
+ metadata: z.record(z.string(), z.any()).optional(),
97
+ });
98
+ ```
99
+
100
+ ### Path Parameters
101
+
102
+ ```typescript
103
+ const paramsSchema = z.object({
104
+ id: z.coerce.number().positive(),
105
+ });
106
+
107
+ // In /api/users/[id].get.ts
108
+ const { id } = await getValidatedRouterParams(event, paramsSchema);
109
+ ```
110
+
111
+ ## Type Inference from Schemas
112
+
113
+ Export schemas for client-side type reuse:
114
+
115
+ ```typescript
116
+ // types/api.ts
117
+ import { z } from "zod";
118
+
119
+ export const CreateUserSchema = z.object({
120
+ email: z.string().email(),
121
+ name: z.string().min(1),
122
+ });
123
+
124
+ export type CreateUserInput = z.infer<typeof CreateUserSchema>;
125
+
126
+ // Client usage
127
+ import type { CreateUserInput } from "~/types/api";
128
+ const body: CreateUserInput = { email: "test@example.com", name: "Test" };
129
+ ```
130
+
131
+ **Note:** Nitro auto-generates response types, but NOT input types from Zod schemas.