@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,243 @@
1
+ # Nitro Tasks
2
+
3
+ Background jobs, scheduled tasks, and one-off operations.
4
+
5
+ ## Enabling Tasks
6
+
7
+ ```typescript
8
+ // nuxt.config.ts
9
+ export default defineNuxtConfig({
10
+ nitro: {
11
+ experimental: {
12
+ tasks: true,
13
+ },
14
+ scheduledTasks: {
15
+ // Cron format: minute hour day month weekday
16
+ "*/5 * * * *": ["scheduled:cleanup"], // Every 5 minutes
17
+ "0 0 * * *": ["scheduled:daily-report"], // Daily at midnight
18
+ "0 8 * * 1": ["scheduled:weekly-digest"], // Mondays at 8am
19
+ },
20
+ },
21
+ });
22
+ ```
23
+
24
+ ## Defining Tasks
25
+
26
+ Tasks live in `server/tasks/`. Directory structure = task name with colons:
27
+ - `server/tasks/hello.ts` → `hello`
28
+ - `server/tasks/scheduled/cleanup.ts` → `scheduled:cleanup`
29
+ - `server/tasks/jobs/send-email.ts` → `jobs:send-email`
30
+
31
+ ```typescript
32
+ // server/tasks/jobs/send-email.ts
33
+ import { z } from "zod";
34
+
35
+ const PayloadSchema = z.object({
36
+ to: z.string().email(),
37
+ subject: z.string(),
38
+ body: z.string(),
39
+ });
40
+
41
+ export default defineTask({
42
+ meta: {
43
+ name: "jobs:send-email",
44
+ description: "Send an email in the background",
45
+ },
46
+ async run({ payload }) {
47
+ const data = PayloadSchema.parse(payload);
48
+ await sendEmail(data);
49
+
50
+ return {
51
+ result: "success",
52
+ sentAt: new Date().toISOString(),
53
+ };
54
+ },
55
+ });
56
+ ```
57
+
58
+ ## Running Tasks
59
+
60
+ ### 1. Programmatically with `runTask`
61
+
62
+ ```typescript
63
+ export default defineEventHandler(async (event) => {
64
+ const result = await runTask("jobs:send-email", {
65
+ payload: {
66
+ to: "user@example.com",
67
+ subject: "Hello",
68
+ body: "Welcome!",
69
+ },
70
+ });
71
+
72
+ return { taskResult: result };
73
+ });
74
+ ```
75
+
76
+ ### 2. Fire-and-forget Pattern
77
+
78
+ ```typescript
79
+ // Don't wait for completion
80
+ runTask("jobs:send-email", {
81
+ payload: { to: "user@example.com", subject: "Hello", body: "Hi!" },
82
+ }).catch((error) => {
83
+ console.error("Task failed:", error);
84
+ });
85
+
86
+ return { message: "Email queued" };
87
+ ```
88
+
89
+ ### 3. Dev Server API (development only)
90
+
91
+ ```bash
92
+ GET /_nitro/tasks # List all tasks
93
+ GET /_nitro/tasks/jobs:send-email # Run task
94
+ POST /_nitro/tasks/jobs:send-email # Run with payload
95
+ ```
96
+
97
+ ### 4. CLI
98
+
99
+ ```bash
100
+ npx nitro task list
101
+ npx nitro task run jobs:send-email --payload '{"to":"user@example.com"}'
102
+ ```
103
+
104
+ ## Critical Limitation: Single Instance (By Design)
105
+
106
+ **Each task can only have ONE running instance at a time.**
107
+
108
+ If you call `runTask("my-task")` while it's already running:
109
+ - The second call returns the SAME result as the first
110
+ - It does NOT queue or start a new execution
111
+ - No error is thrown
112
+
113
+ ```typescript
114
+ // These share the same execution!
115
+ const [result1, result2] = await Promise.all([
116
+ runTask("long-running-task"),
117
+ runTask("long-running-task"),
118
+ ]);
119
+ // result1 === result2
120
+ ```
121
+
122
+ ## Workaround: Database Job Queue
123
+
124
+ For true background processing, use a database-backed queue:
125
+
126
+ ```typescript
127
+ // 1. Enqueue job
128
+ export async function enqueueJob(jobType: string, payload: any) {
129
+ const db = useDatabase();
130
+ const [job] = await db
131
+ .insertInto("job_queue")
132
+ .values({
133
+ job_type: jobType,
134
+ payload: JSON.stringify(payload),
135
+ status: "pending",
136
+ created_at: new Date(),
137
+ })
138
+ .returning(["id"])
139
+ .execute();
140
+ return job.id;
141
+ }
142
+
143
+ // 2. Job dispatcher (runs every few seconds)
144
+ // server/tasks/scheduled/job-dispatcher.ts
145
+ export default defineTask({
146
+ meta: { name: "scheduled:job-dispatcher" },
147
+ async run() {
148
+ const db = useDatabase();
149
+
150
+ // Dequeue with locking
151
+ const job = await db.transaction().execute(async (trx) => {
152
+ const job = await trx
153
+ .selectFrom("job_queue")
154
+ .selectAll()
155
+ .where("status", "=", "pending")
156
+ .orderBy("created_at", "asc")
157
+ .forUpdate() // Lock row
158
+ .skipLocked() // Skip locked rows
159
+ .limit(1)
160
+ .executeTakeFirst();
161
+
162
+ if (!job) return null;
163
+
164
+ await trx
165
+ .updateTable("job_queue")
166
+ .set({ status: "processing", started_at: new Date() })
167
+ .where("id", "=", job.id)
168
+ .execute();
169
+
170
+ return job;
171
+ });
172
+
173
+ if (!job) return { result: "No jobs" };
174
+
175
+ // Fire and forget the work
176
+ processJob(job).catch(console.error);
177
+
178
+ return { result: `Dispatched job ${job.id}` };
179
+ },
180
+ });
181
+ ```
182
+
183
+ ```typescript
184
+ // nuxt.config.ts
185
+ scheduledTasks: {
186
+ "*/2 * * * * *": ["scheduled:job-dispatcher"], // Every 2 seconds
187
+ }
188
+ ```
189
+
190
+ ## Retry Logic
191
+
192
+ ```typescript
193
+ export async function markJobFailed(jobId: number, error: Error) {
194
+ const db = useDatabase();
195
+ const job = await db
196
+ .selectFrom("job_queue")
197
+ .select(["attempt_count", "max_attempts"])
198
+ .where("id", "=", jobId)
199
+ .executeTakeFirst();
200
+
201
+ const shouldRetry = (job.attempt_count || 0) < (job.max_attempts || 3);
202
+
203
+ if (shouldRetry) {
204
+ // Exponential backoff: 1min, 2min, 4min...
205
+ const delay = Math.pow(2, job.attempt_count || 0) * 60000;
206
+ const jitter = Math.random() * 0.1 * delay;
207
+
208
+ await db
209
+ .updateTable("job_queue")
210
+ .set({
211
+ status: "pending",
212
+ scheduled_at: new Date(Date.now() + delay + jitter),
213
+ error_message: error.message,
214
+ })
215
+ .where("id", "=", jobId)
216
+ .execute();
217
+ } else {
218
+ await db
219
+ .updateTable("job_queue")
220
+ .set({ status: "failed", error_message: error.message })
221
+ .where("id", "=", jobId)
222
+ .execute();
223
+ }
224
+ }
225
+ ```
226
+
227
+ ## When to Use Each Pattern
228
+
229
+ | Pattern | Use Case |
230
+ |---------|----------|
231
+ | `runTask` with await | One-off tasks, need result |
232
+ | Fire-and-forget | Background work, no result needed |
233
+ | Scheduled tasks | Recurring jobs (cleanup, reports) |
234
+ | DB job queue | Concurrent jobs, retries, reliability |
235
+
236
+ ## Key Gotchas
237
+
238
+ 1. **Single instance limitation** - Can't run same task twice concurrently
239
+ 2. **No built-in queue** - Multiple calls share result
240
+ 3. **Scheduled tasks need server** - Won't work with `nuxt generate`
241
+ 4. **Dev API only in dev** - `/_nitro/tasks/*` not in production
242
+ 5. **Cron needs specific runtimes** - node-server, bun, deno-server, cloudflare
243
+ 6. **Fire-and-forget errors** - Must add `.catch()` or errors are swallowed
@@ -0,0 +1,162 @@
1
+ # Page Structure
2
+
3
+ Pages should be thin. Keep logic in components, use pages only for layout and routing.
4
+
5
+ ## The Pattern
6
+
7
+ ```vue
8
+ <!-- pages/users/[id].vue -->
9
+ <script setup lang="ts">
10
+ // 1. Parse route params
11
+ const route = useRoute();
12
+ const userId = computed(() => route.params.id as string);
13
+
14
+ // 2. Maybe check auth/permissions
15
+ const { user } = useUserSession();
16
+ </script>
17
+
18
+ <template>
19
+ <!-- 3. Layout + components only -->
20
+ <div class="page-container">
21
+ <PageHeader title="User Profile" />
22
+
23
+ <!-- Pass parsed params to components -->
24
+ <UserProfile :user-id="userId" />
25
+
26
+ <UserActivity :user-id="userId" v-if="user?.role === 'admin'" />
27
+ </div>
28
+ </template>
29
+ ```
30
+
31
+ ## What Goes Where
32
+
33
+ | In Page | In Component |
34
+ |---------|--------------|
35
+ | Route param parsing | Data fetching (useFetch) |
36
+ | Layout structure | Business logic |
37
+ | Component composition | Form handling |
38
+ | Auth guards (via middleware) | State management |
39
+ | Page meta (title, middleware) | Event handlers |
40
+
41
+ ## Pages Do
42
+
43
+ ```vue
44
+ <script setup lang="ts">
45
+ // ✅ Route params
46
+ const route = useRoute();
47
+ const id = computed(() => route.params.id as string);
48
+
49
+ // ✅ Query params (for passing to components)
50
+ const tab = computed(() => (route.query.tab as string) || 'overview');
51
+
52
+ // ✅ Page metadata
53
+ definePageMeta({
54
+ middleware: 'auth',
55
+ layout: 'dashboard',
56
+ });
57
+
58
+ // ✅ Page title
59
+ useHead({ title: 'User Profile' });
60
+ </script>
61
+
62
+ <template>
63
+ <!-- ✅ Layout wrapper -->
64
+ <NuxtLayout>
65
+ <!-- ✅ Component composition -->
66
+ <UserHeader :id="id" />
67
+ <UserTabs :active-tab="tab" :user-id="id" />
68
+ </NuxtLayout>
69
+ </template>
70
+ ```
71
+
72
+ ## Pages Don't
73
+
74
+ ```vue
75
+ <script setup lang="ts">
76
+ // ❌ Data fetching - move to component
77
+ const { data: user } = await useFetch(`/api/users/${route.params.id}`);
78
+
79
+ // ❌ Complex computed - move to component
80
+ const fullName = computed(() => `${user.value?.firstName} ${user.value?.lastName}`);
81
+
82
+ // ❌ Event handlers - move to component
83
+ const handleSave = async () => {
84
+ await $fetch(`/api/users/${route.params.id}`, { method: 'PATCH', body: form });
85
+ };
86
+
87
+ // ❌ Form state - move to component
88
+ const form = reactive({ name: '', email: '' });
89
+
90
+ // ❌ Watchers - move to component
91
+ watch(user, (newUser) => {
92
+ form.name = newUser?.name || '';
93
+ });
94
+ </script>
95
+
96
+ <template>
97
+ <!-- ❌ Too much logic in template -->
98
+ <form @submit.prevent="handleSave">
99
+ <input v-model="form.name" />
100
+ <input v-model="form.email" />
101
+ <button type="submit">Save</button>
102
+ </form>
103
+ </template>
104
+ ```
105
+
106
+ ## Component Does the Work
107
+
108
+ ```vue
109
+ <!-- components/UserProfile.vue -->
110
+ <script setup lang="ts">
111
+ const props = defineProps<{
112
+ userId: string;
113
+ }>();
114
+
115
+ // ✅ Data fetching in component
116
+ const { data: user, refresh } = await useFetch(() => `/api/users/${props.userId}`);
117
+
118
+ // ✅ Form state
119
+ const form = reactive({ name: '', email: '' });
120
+
121
+ // ✅ Sync form with data
122
+ watch(user, (newUser) => {
123
+ if (newUser) {
124
+ form.name = newUser.name;
125
+ form.email = newUser.email;
126
+ }
127
+ }, { immediate: true });
128
+
129
+ // ✅ Event handlers
130
+ const handleSave = async () => {
131
+ await $fetch(`/api/users/${props.userId}`, {
132
+ method: 'PATCH',
133
+ body: form,
134
+ });
135
+ refresh();
136
+ };
137
+ </script>
138
+
139
+ <template>
140
+ <form @submit.prevent="handleSave">
141
+ <input v-model="form.name" placeholder="Name" />
142
+ <input v-model="form.email" placeholder="Email" />
143
+ <button type="submit">Save</button>
144
+ </form>
145
+ </template>
146
+ ```
147
+
148
+ ## Benefits
149
+
150
+ 1. **Reusability** - Components can be used in multiple pages
151
+ 2. **Testability** - Components are easier to test in isolation
152
+ 3. **Readability** - Pages show structure at a glance
153
+ 4. **Maintainability** - Changes to logic don't affect page layout
154
+ 5. **Code splitting** - Nuxt can better optimize component loading
155
+
156
+ ## Key Gotchas
157
+
158
+ 1. **Don't fetch in pages** - Let components own their data
159
+ 2. **Props down, events up** - Pass params as props, emit events for actions
160
+ 3. **Pages are entry points** - Think of them as "controllers" that compose "views"
161
+ 4. **Middleware for auth** - Use `definePageMeta({ middleware: 'auth' })`, not inline checks
162
+ 5. **Layouts for shared UI** - Headers, footers, sidebars go in `/layouts`, not repeated in pages
@@ -0,0 +1,238 @@
1
+ # Server-Side Service Integrations
2
+
3
+ > **Example:** [service-util.ts](./examples/service-util.ts)
4
+
5
+ Composable-style utilities for third-party services in `/server/utils/`.
6
+
7
+ ## Basic Pattern
8
+
9
+ ```typescript
10
+ // server/utils/stripe.ts
11
+ import Stripe from "stripe";
12
+
13
+ // Initialize at module level with runtime config
14
+ const config = useRuntimeConfig();
15
+ const stripe = new Stripe(config.stripe.secretKey);
16
+
17
+ // Define typed methods
18
+ async function createPaymentIntent(options: {
19
+ amount: number;
20
+ currency: string;
21
+ metadata?: Record<string, string>;
22
+ }) {
23
+ return stripe.paymentIntents.create({
24
+ amount: options.amount,
25
+ currency: options.currency,
26
+ metadata: options.metadata,
27
+ });
28
+ }
29
+
30
+ async function getCustomer(customerId: string) {
31
+ return stripe.customers.retrieve(customerId);
32
+ }
33
+
34
+ // Export as use*()
35
+ export function useStripe() {
36
+ return { createPaymentIntent, getCustomer, client: stripe };
37
+ }
38
+ ```
39
+
40
+ ## Usage in API Handlers
41
+
42
+ ```typescript
43
+ // server/api/checkout/create.post.ts
44
+ export default defineEventHandler(async (event) => {
45
+ const { amount, currency } = await readBody(event);
46
+
47
+ const { createPaymentIntent } = useStripe();
48
+
49
+ const intent = await createPaymentIntent({
50
+ amount,
51
+ currency,
52
+ metadata: { source: "web" },
53
+ });
54
+
55
+ return { clientSecret: intent.client_secret };
56
+ });
57
+ ```
58
+
59
+ ## Service Composition
60
+
61
+ Services can use other services:
62
+
63
+ ```typescript
64
+ // server/utils/orders.ts
65
+ export function useOrders() {
66
+ const db = useDatabase();
67
+ const { createPaymentIntent } = useStripe();
68
+
69
+ async function createOrder(userId: number, items: CartItem[]) {
70
+ const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
71
+
72
+ // Create payment intent with Stripe
73
+ const paymentIntent = await createPaymentIntent({
74
+ amount: total,
75
+ currency: "usd",
76
+ metadata: { userId: String(userId) },
77
+ });
78
+
79
+ // Save order to database
80
+ const order = await db
81
+ .insertInto("orders")
82
+ .values({
83
+ user_id: userId,
84
+ total,
85
+ stripe_payment_intent_id: paymentIntent.id,
86
+ status: "pending",
87
+ })
88
+ .returning(["id"])
89
+ .executeTakeFirst();
90
+
91
+ return { order, clientSecret: paymentIntent.client_secret };
92
+ }
93
+
94
+ return { createOrder };
95
+ }
96
+ ```
97
+
98
+ ## Lazy Initialization
99
+
100
+ For expensive clients:
101
+
102
+ ```typescript
103
+ // server/utils/redis.ts
104
+ let redis: Redis | null = null;
105
+
106
+ export function useRedis(): Redis {
107
+ if (!redis) {
108
+ const config = useRuntimeConfig();
109
+
110
+ if (!config.redis?.url) {
111
+ throw new Error("NUXT_REDIS_URL not configured");
112
+ }
113
+
114
+ redis = new Redis(config.redis.url);
115
+ redis.on("error", (err) => console.error("Redis error:", err));
116
+ redis.on("connect", () => console.log("Redis connected"));
117
+ }
118
+
119
+ return redis;
120
+ }
121
+
122
+ // Health check
123
+ export async function isRedisAvailable(): Promise<boolean> {
124
+ try {
125
+ await useRedis().ping();
126
+ return true;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ ```
132
+
133
+ ## Error Handling
134
+
135
+ ```typescript
136
+ // server/utils/error-handling.ts
137
+ export function formatServiceError(error: unknown, service: string) {
138
+ const err = error as any;
139
+
140
+ // PostgreSQL constraint violations
141
+ if (err?.code === "23505") {
142
+ return { status: 409, message: "Resource already exists" };
143
+ }
144
+ if (err?.code === "23503") {
145
+ return { status: 400, message: "Referenced resource not found" };
146
+ }
147
+
148
+ // Network errors
149
+ if (err?.code === "ECONNREFUSED" || err?.code === "ETIMEDOUT") {
150
+ return { status: 503, message: `${service} service unavailable` };
151
+ }
152
+
153
+ // API errors
154
+ if (err?.response?.status) {
155
+ return { status: err.response.status, message: err.message };
156
+ }
157
+
158
+ return { status: 500, message: `${service} error: ${err?.message}` };
159
+ }
160
+
161
+ // Usage
162
+ async function callExternalApi() {
163
+ try {
164
+ return await client.doSomething();
165
+ } catch (error) {
166
+ const { status, message } = formatServiceError(error, "Stripe");
167
+ throw createError({ statusCode: status, message });
168
+ }
169
+ }
170
+ ```
171
+
172
+ ## Transaction Pattern
173
+
174
+ ```typescript
175
+ // server/utils/invoices.ts
176
+ export function useInvoices() {
177
+ const db = useDatabase();
178
+
179
+ async function createInvoice(params: CreateParams) {
180
+ return await db.transaction().execute(async (trx) => {
181
+ // All operations use trx, not db
182
+ const invoice = await trx
183
+ .insertInto("invoice")
184
+ .values(params)
185
+ .returning(["id"])
186
+ .executeTakeFirst();
187
+
188
+ await trx
189
+ .updateTable("session")
190
+ .set({ invoice_id: invoice.id, locked: true })
191
+ .where("id", "in", params.sessionIds)
192
+ .execute();
193
+
194
+ return invoice;
195
+ });
196
+ }
197
+
198
+ return { createInvoice };
199
+ }
200
+ ```
201
+
202
+ ## Common Structure
203
+
204
+ ```typescript
205
+ // server/utils/[service].ts
206
+
207
+ // 1. Import SDK
208
+ import { ServiceClient } from "service-sdk";
209
+
210
+ // 2. Initialize with runtime config
211
+ const config = useRuntimeConfig();
212
+ const client = new ServiceClient({ apiKey: config.service.apiKey });
213
+
214
+ // 3. Define typed methods
215
+ async function doAction(params: ActionParams): Promise<ActionResult> {
216
+ try {
217
+ return await client.action(params);
218
+ } catch (error) {
219
+ throw createError({ statusCode: 500, message: error.message });
220
+ }
221
+ }
222
+
223
+ // 4. Export as use*()
224
+ export function useService() {
225
+ return {
226
+ doAction,
227
+ client, // Expose for advanced usage
228
+ };
229
+ }
230
+ ```
231
+
232
+ ## Key Gotchas
233
+
234
+ 1. **Config at module level** - `useRuntimeConfig()` works at module scope
235
+ 2. **Singleton clients** - Initialize once, reuse across requests
236
+ 3. **Composition order** - Call use*() inside functions, not module level
237
+ 4. **Error transformation** - Convert SDK errors to HTTP errors
238
+ 5. **Transaction scope** - Pass `trx` when in transaction