From ecbbaf300cbc7490bbb8e81d88c798c109499733 Mon Sep 17 00:00:00 2001 From: mama-samba-braima Date: Sat, 27 Jun 2026 21:26:21 +0100 Subject: [PATCH] feat: add headshots skill Generate polished LinkedIn-style profile-picture variations from one reference photo while preserving the subject's identity. - 10 styles (studio, workspace, editorial, B&W, keynote, boardroom, suit, lifestyle, rooftop, library) chosen via a checkbox Ask prompt - Fetches the reference once, then fans out one subagent per style - Self-contained: bundles its own Gemini generator (scripts/edit.ts), no dependency on other skills; resolves GEMINI_API_KEY from the env, the shared ~/amigoscode-skills/.env, or a local .env - Optional --smile and --background add-ons - Output to ~/amigoscode-skills/headshots/; never reads image bytes Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude-plugin/plugin.json | 5 +- VERSIONS.md | 2 + skills/headshots/.env.example | 5 + skills/headshots/SKILL.md | 93 +++ skills/headshots/package-lock.json | 1030 ++++++++++++++++++++++++++++ skills/headshots/package.json | 16 + skills/headshots/prompts.json | 68 ++ skills/headshots/run.sh | 113 +++ skills/headshots/scripts/edit.ts | 126 ++++ 9 files changed, 1457 insertions(+), 1 deletion(-) create mode 100644 skills/headshots/.env.example create mode 100644 skills/headshots/SKILL.md create mode 100644 skills/headshots/package-lock.json create mode 100644 skills/headshots/package.json create mode 100644 skills/headshots/prompts.json create mode 100755 skills/headshots/run.sh create mode 100644 skills/headshots/scripts/edit.ts diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 853e597..7310d4f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -20,6 +20,9 @@ "amigoscode", "explainer-video", "video", - "hyperframes" + "hyperframes", + "headshots", + "profile-picture", + "portrait" ] } diff --git a/VERSIONS.md b/VERSIONS.md index 5ca373e..3af58dd 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -10,10 +10,12 @@ Current versions of all skills. Agents can compare against local versions to che | x-card | 1.0.0 | 2026-06-23 | Generate a dark-mode X (Twitter) style quote card (1080x1350 PNG) with profile photo, blue tick, name, handle, and stats, plus a LinkedIn caption. | | linkedin-poster | 1.0.0 | 2026-06-23 | Fully autonomous LinkedIn poster that publishes immediately or schedules text, carousel, or image posts via Playwright. | | nanobanana | 1.0.0 | 2026-06-27 | Generate and edit images with Google's Gemini 3.1 Flash Image model: text-to-image, single-image edits, style transfer, and multi-image compositing. | +| headshots | 1.0.0 | 2026-06-27 | Generate polished LinkedIn-style profile-picture variations from one reference photo, preserving the person's identity. Picks styles via checkbox, fans out one subagent per style. Self-contained Gemini generator. | ## Recent Changes ### 2026-06-27 +- Added `headshots` skill: LinkedIn-style profile-picture variations from a reference photo (10 styles, checkbox selection, one subagent per style, self-contained Gemini generator) - Added `nanobanana` skill: AI image generation and editing via Gemini 3.1 Flash Image (text-to-image, edits, style transfer, multi-image compositing) ### 2026-06-24 diff --git a/skills/headshots/.env.example b/skills/headshots/.env.example new file mode 100644 index 0000000..cd92f70 --- /dev/null +++ b/skills/headshots/.env.example @@ -0,0 +1,5 @@ +# Optional: this skill also reads ~/amigoscode-skills/.env (shared across +# Amigoscode skills) and the shell environment. Any one of them works. +# Copy this file to .env and add your key, or export GEMINI_API_KEY instead. +# Get a key at https://aistudio.google.com/apikey +GEMINI_API_KEY=your-gemini-api-key-here diff --git a/skills/headshots/SKILL.md b/skills/headshots/SKILL.md new file mode 100644 index 0000000..bf6bb7d --- /dev/null +++ b/skills/headshots/SKILL.md @@ -0,0 +1,93 @@ +--- +name: headshots +description: "Generate polished LinkedIn-style profile-picture variations from a single reference photo, while keeping the person's face and identity intact. Self-contained (bundles its own Gemini image generator). Use this skill whenever the user provides a reference photo (a LinkedIn CDN URL, Skool URL, GitHub avatar, any image URL, or a local file path) and wants professional headshots, profile pictures, or portraits. Trigger on: 'linkedin variations', 'linkedin headshot', 'profile pic for [name]', 'professional portraits of [person]', 'headshots from this photo', 'do the linkedin thing for [URL]', 'generate variations of [URL]', 'do the same for [URL]'. Even if the user just pastes a photo URL and a name and says 'make these professional', this skill applies. For general image edits use the nanobanana skill; for HOW-X-WORKS diagrams use the infographic skill." +--- + +# Headshots — LinkedIn Profile-Picture Variations + +Turn one reference photo into a set of polished, professional LinkedIn portraits in different styles (studio, boardroom, editorial, B&W, and more), each preserving the subject's exact face and identity. The reference is fetched once, then each style is generated by its own subagent in parallel, each calling the bundled `run.sh` (which wraps the skill's own `scripts/edit.ts` Gemini generator). The skill is self-contained: no other skill needs to be installed. + +Output always goes to `~/amigoscode-skills/headshots/` (the shared Amigoscode skills folder), named `--.jpg`. + +## CRITICAL: never Read the generated images + +Do **NOT** use the Read tool on any image file (the reference or the outputs). Reading binary images sends their bytes to the Claude API and corrupts the request. Refer to images by **file path only**. The run script already prints every output path, so you never need to open them. + +## Prerequisites + +- **Node.js** with `npx`. The first `fetch` runs `npm install` in the skill folder automatically (installs the bundled generator's deps). +- A **`GEMINI_API_KEY`**. The bundled generator resolves it from, in order: the shell environment, the shared `~/amigoscode-skills/.env` (used across Amigoscode skills), then this skill's own `.env` (copy `.env.example`). Get a key at https://aistudio.google.com/apikey. If a run fails with `GEMINI_API_KEY ... not set`, set it one of those ways and retry. + +## Workflow + +### Step 1: Resolve the subject + +You need two things from the user: +- **source**: the reference photo. A URL (LinkedIn/Skool/GitHub/any image link) or a local file path. +- **name**: a short kebab-case label for the person (e.g. `ricardo`, `thays`). Used in filenames. If the user did not give a name, derive an obvious one from context and confirm it is fine. + +Do not download anything yourself. `run.sh` handles fetching and validating the photo. + +### Step 2: Ask which styles (checkbox) + +Let the user choose the styles instead of guessing. Call the **Ask tool once** with these three **multi-select** questions. The user ticks any combination across the groups; ticking everything means all ten. The slug in parentheses is what you pass to `run.sh` (do not show slugs to the user, they are for you). + +- **Studio & office**: Studio headshot (`studio`), Tech workspace (`workspace`), High-key keynote (`keynote`), Executive boardroom (`boardroom`) +- **Editorial & dramatic**: Outdoor editorial / founder (`editorial`), Monochrome B&W (`bw`), Urban rooftop sunset (`rooftop`) +- **Formal & lifestyle**: Formal executive suit (`suit`), Natural lifestyle outdoor (`lifestyle`), Intellectual library (`library`) + +Map the selected labels back to their slugs. If the user selected all ten (or said "all" / "every style"), use `--all` instead of listing slugs. + +The full style descriptions live in `prompts.json` if you need to describe one to the user. + +### Step 3: Fetch the reference once + +Download and validate the photo a single time, so the parallel agents in Step 4 all share one local file (no duplicate downloads, no race): + +```bash +bash ~/.claude/skills/headshots/run.sh fetch --source "" --name "" +``` + +The command prints the resolved reference path on the last stdout line. Capture it as ``. If it errors (expired link, not an image), handle it per **Edge cases** below before continuing. + +### Step 4: Generate — one agent per style + +Generate each selected style in its **own subagent**, so they run concurrently and a failure in one does not block the others. **Dispatch all of them in a single message** so they run in parallel. + +Give each agent this task (substitute the slug, the captured ``, the name, and any `--smile` / `--background ` flags the user asked for): + +> Run exactly this command and nothing else: +> `bash ~/.claude/skills/headshots/run.sh one --ref "" --name "" --style ` +> It generates one portrait and prints the saved image path on the last line. Report back that path, or the error if it failed. **Do NOT use the Read tool on any image file** — reading image bytes corrupts the API. Refer to images by path only. + +Optional add-ons (append to the command for every agent when the user asked for them): +- `--smile` — force a genuine warm smile (friendlier look). +- `--background ` — saturated single-colour studio backdrop with a halo, e.g. `--background purple`. + +### Step 5: Deliver + +Collect the path each agent reports and present a table (style → file path), plus the output folder `~/amigoscode-skills/headshots/`. Do not open or Read the images to "check" them — trust the paths the agents returned. + +## Edge cases + +- **URL expired (404/403)**: the script stops with a clear message. Tell the user the link expired and ask for a fresh URL (LinkedIn/Skool CDN links are short-lived). +- **Reference already downloaded**: the script reuses `~/amigoscode-skills/headshots/-reference.*` and skips re-downloading. To force a fresh photo, delete that file first. +- **A single variation fails** (e.g. `Edit failed: ENOENT`): the reference may have moved. Re-run `fetch`, then dispatch a fresh agent for just that style (`run.sh one --style `). +- **"Give me N more" / "more variations"**: ask which additional styles (Step 2) and dispatch agents for only those slugs. Filenames are keyed to the style number, so they will not collide with the existing ones. + +## Style catalog + +| # | slug | look | +|---|------|------| +| 1 | studio | Studio headshot — navy crewneck, soft grey backdrop, three-point light | +| 2 | workspace | Tech workspace — merino/button-down, blurred office bokeh, window light | +| 3 | editorial | Outdoor founder — zip-neck knit, urban architecture, golden hour, 85mm | +| 4 | bw | Monochrome B&W — black turtleneck, Rembrandt key, film grain | +| 5 | keynote | High-key keynote — light blazer, creamy backdrop, optimistic smile | +| 6 | boardroom | Executive boardroom — navy blazer, skyline windows, warm interior | +| 7 | suit | Formal suit — tailored charcoal/navy, open collar, C-suite vibe | +| 8 | lifestyle | Lifestyle outdoor — cardigan over tee, blurred greenery, warm smile | +| 9 | rooftop | Rooftop sunset — dark blazer, golden-hour skyline, cinematic grade | +| 10 | library | Intellectual library — camel blazer, warm bookshelves, thoughtful | + +Every prompt preserves the subject's exact face, hair, skin tone, and eye colour, and renders a photorealistic, head-and-shoulders, square 1:1 portrait. The prompt assembly (shared prefix/suffix, smile and background add-ons) is defined in `prompts.json`. diff --git a/skills/headshots/package-lock.json b/skills/headshots/package-lock.json new file mode 100644 index 0000000..77e5bc8 --- /dev/null +++ b/skills/headshots/package-lock.json @@ -0,0 +1,1030 @@ +{ + "name": "headshots-skill", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "headshots-skill", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@google/genai": "^1.46.0", + "dotenv": "^17.3.1", + "mime": "^4.1.0" + }, + "devDependencies": { + "tsx": "^4.19.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.0.1.tgz", + "integrity": "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", + "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/skills/headshots/package.json b/skills/headshots/package.json new file mode 100644 index 0000000..7438840 --- /dev/null +++ b/skills/headshots/package.json @@ -0,0 +1,16 @@ +{ + "name": "headshots-skill", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Generate LinkedIn-style profile-picture variations from a reference photo with Gemini.", + "license": "MIT", + "dependencies": { + "@google/genai": "^1.46.0", + "dotenv": "^17.3.1", + "mime": "^4.1.0" + }, + "devDependencies": { + "tsx": "^4.19.0" + } +} diff --git a/skills/headshots/prompts.json b/skills/headshots/prompts.json new file mode 100644 index 0000000..bc0f512 --- /dev/null +++ b/skills/headshots/prompts.json @@ -0,0 +1,68 @@ +{ + "prefix": "Professional LinkedIn portrait of the person from the reference image. Preserve their exact face, hair, hairstyle, facial features, skin tone, and eye color identically, keeping identity precisely intact.", + "suffix": "Photorealistic, sharp focus on eyes, head-and-shoulders crop, square 1:1 aspect ratio.", + "smile_addon": "Change the EXPRESSION to a genuine warm friendly smile with teeth slightly showing, eyes lighting up (Duchenne smile, slight crinkle at eye corners), looking approachable and confident.", + "background_template": "Replace the backdrop with a saturated {COLOR} single-color studio backdrop with a soft radial halo behind the head.", + "styles": [ + { + "n": 1, + "slug": "studio", + "label": "Studio headshot", + "body": "Classic studio headshot. Navy crewneck over a white tee collar, soft charcoal-to-warm-grey backdrop, three-point lighting, slight natural smile." + }, + { + "n": 2, + "slug": "workspace", + "label": "Tech workspace", + "body": "Modern tech workspace. Heather-grey merino or dark button-down shirt, blurred open-plan office bokeh (wood, plants, monitors), natural side-window light." + }, + { + "n": 3, + "slug": "editorial", + "label": "Outdoor editorial / founder", + "body": "Outdoor editorial founder look. Olive or charcoal zip-neck knit, out-of-focus modern urban architecture, golden-hour 45-degree key light with a subtle lens flare, 85mm lens look." + }, + { + "n": 4, + "slug": "bw", + "label": "Monochrome B&W editorial", + "body": "Monochrome black-and-white editorial. Black turtleneck or dark blazer, dramatic Rembrandt key light from the upper right, deep dark-grey gradient background, fine film grain." + }, + { + "n": 5, + "slug": "keynote", + "label": "High-key keynote", + "body": "High-key bright keynote look. Cream or white shirt with a light blazer, creamy off-white seamless backdrop, even high-key three-point lighting, optimistic warm smile." + }, + { + "n": 6, + "slug": "boardroom", + "label": "Executive boardroom", + "body": "Executive boardroom. Sharp navy or charcoal blazer over a crisp shirt, blurred boardroom with floor-to-ceiling windows and a city skyline, warm interior light." + }, + { + "n": 7, + "slug": "suit", + "label": "Formal executive suit", + "body": "Formal executive suit. Sharp tailored charcoal or navy two-piece suit, crisp white shirt with an open collar and no tie, optional pocket square, clean charcoal studio backdrop, polished confident expression. Banking, consulting, C-suite vibe." + }, + { + "n": 8, + "slug": "lifestyle", + "label": "Natural lifestyle outdoor", + "body": "Natural lifestyle outdoor. Cream or beige cardigan over a white tee, heavily blurred greenery or cafe terrace, dappled daylight, genuine warm smile." + }, + { + "n": 9, + "slug": "rooftop", + "label": "Urban rooftop sunset", + "body": "Urban rooftop at sunset. Sleek dark blazer over a fitted black tee, blurred city skyline at golden-hour sunset, warm rim light, cinematic colour grading." + }, + { + "n": 10, + "slug": "library", + "label": "Intellectual library", + "body": "Intellectual library. Camel or beige blazer over cream silk, out-of-focus warm wooden bookshelves and a brass desk lamp, warm window key light, refined thoughtful smile. Author, professor, thought-leader vibe." + } + ] +} diff --git a/skills/headshots/run.sh b/skills/headshots/run.sh new file mode 100755 index 0000000..24a55a9 --- /dev/null +++ b/skills/headshots/run.sh @@ -0,0 +1,113 @@ +#!/usr/bin/env bash +# +# headshots/run.sh — helpers for generating LinkedIn-style portraits from a +# reference photo using the bundled scripts/edit.ts generator. Two subcommands: +# +# run.sh fetch --source --name +# Download + validate the reference photo into the output folder ONCE. +# Prints the resolved reference path (and nothing else) on stdout. +# +# run.sh one --ref --name --style [--smile] [--background ] +# Generate ONE style with the bundled generator. Prints the saved image path. +# +# The skill calls `fetch` once, then dispatches one subagent per style, each +# running `one`. Output goes to ~/amigoscode-skills/headshots/. + +set -uo pipefail + +SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SCRIPTS="$SKILL_DIR/scripts" +OUTDIR="$HOME/amigoscode-skills/headshots" +PROMPTS="$SKILL_DIR/prompts.json" + +err() { echo "Error: $1" >&2; exit "${2:-1}"; } + +CMD="${1:-}"; shift || true +SOURCE="" NAME="" REF="" STYLE="" SMILE="" BGCOLOR="" +while [ $# -gt 0 ]; do + case "$1" in + --source) SOURCE="$2"; shift 2 ;; + --name) NAME="$2"; shift 2 ;; + --ref) REF="$2"; shift 2 ;; + --style) STYLE="$2"; shift 2 ;; + --smile) SMILE="1"; shift ;; + --background) BGCOLOR="$2"; shift 2 ;; + *) err "unknown arg: $1" ;; + esac +done + +mkdir -p "$OUTDIR" + +case "$CMD" in + fetch) + [ -n "$SOURCE" ] || err "--source is required" + [ -n "$NAME" ] || err "--name is required" + # Bootstrap dependencies once here (fetch runs a single time before the + # per-style agents fan out, so there is no concurrent npm-install race). + if [ ! -d "$SKILL_DIR/node_modules" ]; then + echo "Installing dependencies (first run)..." >&2 + ( cd "$SKILL_DIR" && npm install ) >&2 || err "npm install failed in $SKILL_DIR" + fi + REF_BASE="$OUTDIR/$NAME-reference" + EXISTING="$(ls "$REF_BASE".* 2>/dev/null | grep -vE '\.(log|download)$' | head -1)" + if [ -n "$EXISTING" ] && [ -s "$EXISTING" ]; then + echo "Reference already present (skipping download)" >&2 + echo "$EXISTING"; exit 0 + fi + TMP="$REF_BASE.download" + if [[ "$SOURCE" =~ ^https?:// ]]; then + echo "Downloading reference..." >&2 + curl -fL --retry 2 -s "$SOURCE" -o "$TMP" \ + || err "could not download the photo (404/403). LinkedIn/Skool links expire fast — ask the user for a fresh URL." 2 + else + SRC="${SOURCE/#\~/$HOME}" + [ -f "$SRC" ] || err "local source not found: $SOURCE" 2 + cp "$SRC" "$TMP" + fi + case "$(file -b --mime-type "$TMP")" in + image/jpeg) EXT="jpg" ;; + image/png) EXT="png" ;; + image/webp) EXT="webp" ;; + *) rm -f "$TMP"; err "source is not a valid JPEG/PNG/WebP image" 2 ;; + esac + mv "$TMP" "$REF_BASE.$EXT" + echo "Reference saved: $REF_BASE.$EXT" >&2 + echo "$REF_BASE.$EXT" + ;; + + one) + [ -n "$REF" ] || err "--ref is required" + [ -n "$NAME" ] || err "--name is required" + [ -n "$STYLE" ] || err "--style is required" + [ -s "$REF" ] || err "reference not found at $REF (re-run fetch)" 2 + [ -f "$SCRIPTS/edit.ts" ] || err "bundled generator missing at $SCRIPTS/edit.ts" + [ -d "$SKILL_DIR/node_modules" ] || err "dependencies not installed (run fetch first, or 'npm install' in $SKILL_DIR)" + + MAPPED="$(STYLE="$STYLE" SMILE="$SMILE" BGCOLOR="$BGCOLOR" python3 - "$PROMPTS" <<'PY' +import json, os, sys +data = json.load(open(sys.argv[1])) +slug = os.environ["STYLE"] +st = next((s for s in data["styles"] if s["slug"] == slug), None) +if not st: sys.exit(f"unknown style slug: {slug}") +parts = [data["prefix"], st["body"]] +if os.environ.get("SMILE"): parts.append(data["smile_addon"]) +bg = os.environ.get("BGCOLOR","").strip() +if bg: parts.append(data["background_template"].replace("{COLOR}", bg)) +parts.append(data["suffix"]) +print(f'{st["n"]}\t{" ".join(parts)}') +PY +)" + N="${MAPPED%%$'\t'*}"; PROMPT="${MAPPED#*$'\t'}" + OUT="$OUTDIR/$NAME-$N-$STYLE.png" + # Route edit.ts chatter (dotenv lines, "Image saved") to stderr so this + # command's stdout is ONLY the final image path the agent should report. + ( cd "$SKILL_DIR" && npx tsx scripts/edit.ts --input "$REF" --prompt "$PROMPT" --output "$OUT" ) >&2 \ + || err "generation failed for style '$STYLE' (if ENOENT, the reference moved — re-run fetch)" 3 + FINAL="$(ls "$OUTDIR/$NAME-$N-$STYLE".* 2>/dev/null | grep -vE '\.(log|download)$' | head -1)" + echo "${FINAL:-$OUT}" + ;; + + *) + err "usage: run.sh fetch|one ... (see header)" + ;; +esac diff --git a/skills/headshots/scripts/edit.ts b/skills/headshots/scripts/edit.ts new file mode 100644 index 0000000..233e6a6 --- /dev/null +++ b/skills/headshots/scripts/edit.ts @@ -0,0 +1,126 @@ +import { config as dotenvConfig } from "dotenv"; +import { fileURLToPath } from "url"; +import { GoogleGenAI } from "@google/genai"; +import mime from "mime"; +import { writeFile, readFile, mkdir } from "fs/promises"; +import { dirname, join } from "path"; +import { homedir } from "os"; + +// Self-contained: this skill bundles its own generator so it does not depend on +// any other skill being installed. Env precedence: the shell environment always +// wins, then the shared ~/amigoscode-skills/.env (one key file for every +// Amigoscode skill), then this skill's own .env (repo root) as a fallback. +// dotenv never overrides an already-set variable, and an earlier load beats a +// later one, so this gives shell > shared > skill-local. +dotenvConfig({ path: join(homedir(), "amigoscode-skills", ".env") }); +dotenvConfig({ path: join(dirname(fileURLToPath(import.meta.url)), "..", ".env") }); + +// Parse args manually to support multiple --input flags +const args = process.argv.slice(2); +const inputs: string[] = []; +let prompt = ""; +let output = "edited.png"; +let size = "1K"; +let model = "gemini-3.1-flash-image-preview"; + +for (let i = 0; i < args.length; i++) { + if (args[i] === "--input" && args[i + 1]) { + inputs.push(args[++i]); + } else if (args[i] === "--prompt" && args[i + 1]) { + prompt = args[++i]; + } else if (args[i] === "--output" && args[i + 1]) { + output = args[++i]; + } else if (args[i] === "--size" && args[i + 1]) { + size = args[++i]; + } else if (args[i] === "--model" && args[i + 1]) { + model = args[++i]; + } +} + +if (!prompt) { + console.error("Error: --prompt is required"); + process.exit(1); +} + +if (inputs.length === 0) { + console.error("Error: at least one --input is required (path to source image)"); + process.exit(1); +} + +if (!process.env.GEMINI_API_KEY) { + console.error( + "Error: GEMINI_API_KEY is not set. Export it, add it to ~/amigoscode-skills/.env, " + + "or copy .env.example to .env in this skill. Get a key at https://aistudio.google.com/apikey" + ); + process.exit(1); +} + +async function main() { + const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! }); + + // Build parts array: all images first, then the text prompt + const parts: any[] = []; + + for (const inputPath of inputs) { + const imageBuffer = await readFile(inputPath); + const imageMimeType = mime.getType(inputPath) || "image/png"; + const imageBase64 = imageBuffer.toString("base64"); + parts.push({ + inlineData: { + mimeType: imageMimeType, + data: imageBase64, + }, + }); + } + + parts.push({ text: prompt }); + + const config = { + responseModalities: ["IMAGE", "TEXT"] as const, + thinkingConfig: { thinkingLevel: "MINIMAL" as const }, + imageConfig: { imageSize: size }, + }; + + const response = await ai.models.generateContentStream({ + model, + config, + contents: [ + { + role: "user", + parts, + }, + ], + }); + + await mkdir(dirname(output), { recursive: true }); + + let saved = false; + for await (const chunk of response) { + if (!chunk.candidates || !chunk.candidates[0]?.content?.parts) { + continue; + } + + for (const part of chunk.candidates[0].content.parts) { + if (part.inlineData) { + const ext = mime.getExtension(part.inlineData.mimeType || "") || "png"; + const finalPath = output.replace(/\.[^.]+$/, "") + "." + ext; + const buffer = Buffer.from(part.inlineData.data || "", "base64"); + await writeFile(finalPath, buffer); + console.log(`Image saved: ${finalPath}`); + saved = true; + } else if (part.text) { + console.log(part.text); + } + } + } + + if (!saved) { + console.error("No image was generated. Try refining your prompt."); + process.exit(1); + } +} + +main().catch((err) => { + console.error("Edit failed:", err.message || err); + process.exit(1); +});