migrate-image-fields
Migrate Keystone image and file fields to OpenSaaS Stack. Leads with the non-destructive multi-column path (map onto the existing Keystone columns in place — no data migration, no re-upload). Invoke as a forked subagent, passing the project details.
What this skill does
Migrate the image and file fields described below from Keystone format to OpenSaaS Stack.
**Lead with the non-destructive multi-column path.** OpenSaaS Stack's `image()` / `file()` fields can map directly onto the existing Keystone per-part columns (`db.columns: 'keystone'`), assembling them into an `ImageMetadata` / `FileMetadata` on read and splitting back on write. A migrating project reaches a clean schema diff and a fully functional field with **no data migration, no dropped columns, and no re-upload of existing assets**. This is the default recommendation and respects the **Schema parity** guardrail (see ADR-0006).
JSON consolidation (collapsing the Keystone columns into a single `Json?` column) is offered only as an explicitly-flagged, **destructive** opt-in further below. It drops columns and requires a verified backup — only do it if the user explicitly asks for it.
$ARGUMENTS
## The Two Schemas
**Keystone** stores an image across 7 per-part columns and a file across 3:
```prisma
model Teacher {
// image field "avatar"
avatar_url String?
avatar_width Int?
avatar_height Int?
avatar_filesize Int?
avatar_contentType String?
avatar_contentDisposition String?
avatar_pathname String?
// file field "resume"
resume_filename String?
resume_filesize Int?
resume_url String?
}
```
**OpenSaaS Stack** (greenfield default) uses a single `Json?` column per field:
```prisma
model Teacher {
avatar Json? // ImageMetadata
resume Json? // FileMetadata
}
```
`image()` / `file()` return these metadata shapes to your app and the admin UI in BOTH modes:
```typescript
// from @opensaas/stack-storage
interface FileMetadata {
filename: string // storage key / generated filename
originalFilename: string // original upload filename
url: string // public URL
mimeType: string // e.g. 'image/jpeg'
size: number // bytes
uploadedAt: string // ISO 8601 timestamp
storageProvider: string // storage provider name, e.g. 'images'
metadata?: Record<string, unknown> // provider-specific extras
}
interface ImageMetadata extends FileMetadata {
width: number
height: number
transformations?: Record<string, { url: string; width: number; height: number; size: number }>
}
```
The multi-column mode bridges the gap: it presents the same `ImageMetadata` / `FileMetadata` to your app while leaving the physical Keystone columns untouched on disk.
---
## Recommended path: non-destructive multi-column mode
This is the path to use for almost every Keystone migration. **No SQL migration, no data loss, no re-upload.**
### Step 1: Set the field to multi-column mode
Replace the Keystone `image()` / `file()` field with the OpenSaaS Stack equivalent and add `db: { columns: 'keystone' }`:
```typescript
// Before (Keystone)
import { image, file } from '@keystone-6/core/fields'
avatar: image({ storage: 'my_local_images' })
resume: file({ storage: 'my_local_files' })
// After (OpenSaaS Stack — maps onto the existing columns in place)
import { image, file } from '@opensaas/stack-storage/fields'
avatar: image({ storage: 'images', db: { columns: 'keystone' } })
resume: file({ storage: 'files', db: { columns: 'keystone' } })
```
`db.columns: 'keystone'` uses Keystone's default `<field>_<part>` column names. If the live columns use different names, override per part — you only need to specify the parts that differ:
```typescript
avatar: image({
storage: 'images',
db: {
columns: {
mode: 'keystone',
map: {
url: 'avatar_image_url', // physical column for the URL part
pathname: 'avatar_image_key',
},
},
},
})
```
Image parts: `url`, `width`, `height`, `filesize`, `contentType`, `contentDisposition`, `pathname`.
File parts: `filename`, `filesize`, `url`.
### Step 2: Configure storage
The field's `storage` key references a provider in `config.storage`. The provider does NOT need to host the existing assets to read them — the stored URLs are read straight out of the columns, so existing images/files display without any re-upload. The provider is only used when a NEW file is uploaded. See the **Storage providers** section for local, S3, and Vercel Blob options.
### Step 3: Generate and verify a clean diff
```bash
# Generate the Prisma schema from the OpenSaaS config
pnpm opensaas generate
# Generate the Prisma Client
npx prisma generate
# Verify there are NO destructive changes — the multi-column field maps
# onto the existing columns, so the diff against the live DB should be empty
# (or additive only). Compare the generated schema to your live database:
npx prisma migrate diff \
--from-url "$DATABASE_URL" \
--to-schema-datamodel prisma/schema.prisma \
--script
# Or, for a SQLite dev loop:
npx prisma db push
```
The generated schema emits the per-part columns with `@map` onto the live Keystone columns, so Prisma sees no drift:
```prisma
model Teacher {
avatar_url String? @map("avatar_url")
avatar_width Int? @map("avatar_width")
avatar_height Int? @map("avatar_height")
avatar_filesize Int? @map("avatar_filesize")
avatar_contentType String? @map("avatar_contentType")
avatar_contentDisposition String? @map("avatar_contentDisposition")
avatar_pathname String? @map("avatar_pathname")
}
```
### What you get
- Existing rows assemble into `ImageMetadata` / `FileMetadata` on read. Partially-populated legacy rows (e.g. only `avatar_url` set) still produce a valid object — missing scalar parts default to `0`, and an absent `contentType` becomes `application/octet-stream`.
- New uploads split the metadata back into the same per-part columns on write.
- An existing metadata value (or populated columns) is authoritative and is **never** re-uploaded — only a `File`-like input triggers a storage upload. This is locked by a test (see ADR-0006).
### Steps to run
1. Read the config to find all `image()` and `file()` fields and the models they belong to.
2. For each, update the import to `@opensaas/stack-storage/fields` and add `db: { columns: 'keystone' }` (with per-part `map` overrides only if the live column names differ from Keystone defaults).
3. Confirm the `storage` provider exists in `config.storage` (add one if needed — see below).
4. Run `pnpm opensaas generate` and a `prisma migrate diff` to confirm the diff is clean / non-destructive.
5. Report the config changes and confirm: no SQL run, no data migration, no re-upload.
---
## Storage providers
The `storage` key on each field points to a named provider in `config.storage`. All of these implement the same provider interface and work with both single-Json? and multi-column modes. Pick whichever the project already uses for assets — **it is not S3-only.**
### Local filesystem
```typescript
import { localStorage } from '@opensaas/stack-storage'
storage: {
images: localStorage({ uploadDir: './public/uploads', serveUrl: '/uploads' }),
}
```
### S3 / S3-compatible
```typescript
import { s3Storage } from '@opensaas/stack-storage-s3'
storage: {
images: s3Storage({ bucket: 'my-bucket', region: 'us-east-1' }),
}
```
### Vercel Blob (first-class)
`@opensaas/stack-storage-vercel` ships a fully supported Vercel Blob provider — a great fit for projects deployed on Vercel.
```bash
pnpm add @opensaas/stack-storage @opensaas/stack-storage-vercel
```
```typescript
import { vercelBlobStorage } from '@opensaas/stack-storage-vercel'
storage: {
images: vercelBlobStorage({
// Token defaults to the BLOB_READ_WRITE_TOKEN env var.
token: process.env.BLOB_READ_WRITE_TOKEN,
pathPrefix: 'images',
}),
}
```
Because the multi-column mode reads existing stored URLs in place, you can adopt any of these providers without re-uploading the assets already referenced by the live columns. The provider only handles new uploads.
---
## Destructive alternative (opt-in): consolidate toRelated in Image & Video
watch
IncludedWatch a video (URL or local path). Downloads with yt-dlp, extracts auto-scaled frames with ffmpeg, pulls the transcript from captions (or Whisper API fallback), and hands the result to Claude so it can answer questions about what's in the video.
physical-ai-defect-image-generation
IncludedUse when the user wants to orchestrate defect image generation, run associated setup, or handle outputs on OSMO. The Day 0 path handles cold-start with USD-to-ROI, image-edit augmentation, and AnomalyGen to create initial PCBA datasets. The Day 1 path performs inference and labeling on real images. This skill helps with first-time asset setup, creation of finetuning checkpoints, and configuring deployment. Trigger keywords: defect image generation, dig workflow, dig pipeline, defect image detection workflow, aoi pipeline, aoi anomalygen, usd2roi anomalygen, day 0 pcba, day 1 pcba, day 1 real-photo alignment, day 1 manual roi, metal surface anomaly, glass defect, anomalygen finetune, setup_pcb, setup_metal, setup_glass, setup_pretrained, dig setup, dig datasets, dig pretrained checkpoint, dig image-edit endpoint.
accelint-react-best-practices
IncludedReact performance optimization and best practices. ALWAYS use this skill when working with any React code - writing components, hooks, JSX; refactoring; optimizing re-renders, memoization, state management; reviewing for performance; fixing hydration mismatches; debugging infinite re-renders, stale closures, input focus loss, animations restarting; preventing remounting; implementing transitions, lazy initialization, effect dependencies. Even simple React tasks benefit from these patterns. Covers React 19+ (useEffectEvent, Activity, ref props). Triggers - useEffect, useState, useMemo, useCallback, memo, inline components, nested components, components inside components, re-render, performance, hydration, SSR, Next.js, useDeferredValue, combined hooks.
elevenlabs-agents
IncludedBuild conversational AI voice agents with ElevenLabs Platform using React, JavaScript, React Native, or Swift SDKs. Configure agents, tools (client/server/MCP), RAG knowledge bases, multi-voice, and Scribe real-time STT. Use when: building voice chat interfaces, implementing AI phone agents with Twilio, configuring agent workflows or tools, adding RAG knowledge bases, testing with CLI "agents as code", or troubleshooting deprecated @11labs packages, Android audio cutoff, CSP violations, dynamic variables, or WebRTC config. Keywords: ElevenLabs Agents, ElevenLabs voice agents, AI voice agents, conversational AI, @elevenlabs/react, @elevenlabs/client, @elevenlabs/react-native, @elevenlabs/elevenlabs-js, @elevenlabs/agents-cli, elevenlabs SDK, voice AI, TTS, text-to-speech, ASR, speech recognition, turn-taking model, WebRTC voice, WebSocket voice, ElevenLabs conversation, agent system prompt, agent tools, agent knowledge base, RAG voice agents, multi-voice agents, pronunciation dictionary, voice speed control, elevenlabs scribe, @11labs deprecated, Android audio cutoff, CSP violation elevenlabs, dynamic variables elevenlabs, case-sensitive tool names, webhook authentication
humanizer
IncludedHumanize AI-generated text by detecting and removing patterns typical of LLM output. Rewrites text to sound natural, specific, and human. Uses 28 pattern detectors, 560+ AI vocabulary terms across 3 tiers, and statistical analysis (burstiness, type-token ratio, readability) for comprehensive detection. Use when asked to humanize text, de-AI writing, make content sound more natural/human, review writing for AI patterns, score text for AI detection, or improve AI-generated drafts. Covers content, language, style, communication, and filler categories.
generating-mermaid-diagrams
IncludedSalesforce architecture diagrams using Mermaid with ASCII fallback. Use this skill when generating text-based diagrams for Salesforce architecture, OAuth flows, ERDs, integration sequences, or Agentforce structure. TRIGGER when: user says "diagram", "visualize", "ERD", or asks for sequence diagrams, flowcharts, class diagrams, or architecture visualizations in Mermaid. DO NOT TRIGGER when: user wants PNG/SVG image output (use generating-visual-diagrams), or asks about non-Salesforce systems.