Draws the images you sent above the prompt until the turn that carried them ends. Pictures need a terminal with the kitty graphics protocol (kitty, Ghostty…

The complete skill library that Cure Consulting Group uses to build apps, platforms, and products. These skills encode our standards, frameworks, and processes — so every project ships with the same level of rigor.
Now available as a Claude Code Plugin — install once, get auto-updates across all projects.
ProductEngineeringSkills/
├── .claude-plugin/ # Plugin manifest
│ └── plugin.json
├── skills/{domain}/ # 80 skills, organized by domain (engineering/platform/product/business/marketing/security/legal)
│ ├── sdlc/
│ ├── android-feature-scaffold/
│ ├── incident-response/ # NEW
│ ├── accessibility-audit/ # NEW
│ ├── performance-review/ # NEW
│ ├── database-architect/ # NEW
│ ├── infrastructure-scaffold/ # NEW
│ ├── project-bootstrap/
│ ├── e2e-testing/
│ ├── test-accounts/
│ ├── uat/
│ ├── compliance-architect/
│ ├── data-migration/
│ ├── feature-flags/
│ ├── release-management/
│ ├── observability/
│ ├── client-handoff/
│ ├── llmops/
│ ├── disaster-recovery/
│ ├── dora-metrics/
│ ├── design-system/
│ ├── client-communication/
│ ├── i18n/
│ ├── notification-architect/
│ ├── offline-first/
│ ├── chaos-engineering/
│ ├── edge-computing/
│ ├── finops/
│ ├── micro-frontends/
│ ├── growth-engineering/
│ ├── green-software/
│ ├── proposal-generator/
│ ├── api-gateway/
│ ├── ... (75 total — see docs/OVERVIEW.md for full inventory)
│ └── legal-doc-scaffold/
├── agents/ # 35 custom subagent definitions
├── personas/ # 4 cross-domain engagement archetypes
│ ├── code-reviewer.md # Security + quality review agent
│ ├── project-bootstrapper.md # New project setup agent
│ ├── test-runner.md # Execute test suites, report coverage
│ ├── pr-reviewer.md # Automated PR diff review
│ ├── refactor-assistant.md # Safe refactoring with test validation
│ ├── ci-debugger.md # Diagnose failed CI/CD runs
│ ├── release-coordinator.md # Version bump, changelog, deploy validation
│ ├── doc-generator.md # API docs, ADRs, changelogs from code
│ ├── codebase-explainer.md # Onboarding — explain architecture, trace flows
│ ├── migration-validator.md # Database migration safety checks
│ ├── deployment-validator.md # Pre-deployment checklist validation
│ ├── dependency-auditor.md # Vulnerability and outdated package audit
│ ├── api-validator.md # OpenAPI spec and contract validation
│ ├── product-analyst.md # Feature adoption, analytics instrumentation
│ ├── ux-researcher.md # Usability analysis, friction mapping
│ ├── roadmap-strategist.md # RICE scoring, dependency mapping, roadmaps
│ ├── competitive-intel.md # Feature matrices, positioning, moat analysis
│ ├── content-strategist.md # Editorial calendars, SEO, content briefs
│ ├── campaign-analyst.md # Attribution, funnel analysis, channel ROI
│ ├── brand-guardian.md # Voice/tone, visual identity, microcopy audit
│ ├── growth-analyst.md # Activation, retention, viral mechanics
│ ├── financial-analyst.md # Revenue forecasts, unit economics, scenarios
│ ├── market-intelligence.md # TAM/SAM/SOM, trends, market timing
│ ├── investor-relations.md # Board updates, KPIs, fundraising narratives
│ ├── contract-reviewer.md # SOW/contract risk, terms, IP review
│ ├── data-analyst.md # Schema exploration, queries, data quality
│ ├── metrics-dashboard.md # KPI definitions, SLOs, dashboard wireframes
│ ├── ab-test-analyst.md # Experiment design, statistical analysis
│ ├── qa-engineer.md # Test planning, edge cases, regression, quality gates
│ ├── accessibility-checker.md # WCAG 2.2 automated compliance
│ └── firebase-security-auditor.md # Firestore rules and Functions audit
├── hooks/ # Multi-layer automated enforcement
│ └── hooks.json # Command + Prompt hooks (9 event types)
├── rules/ # 11 path-specific coding standards
│ ├── android.md # Loads for *.kt files
│ ├── ios.md # Loads for *.swift files
│ ├── web.md # Loads for *.ts/*.tsx files
│ ├── firebase.md # Loads for functions/**
│ ├── python.md # Loads for *.py files
│ ├── go.md # Loads for *.go files
│ ├── rust.md # Loads for *.rs files
│ ├── sql.md # Loads for *.sql, migrations/**
│ ├── docker.md # Loads for Dockerfile, *.dockerfile
│ ├── terraform.md # Loads for *.tf, *.tfvars
│ └── cicd.md # Loads for .github/workflows/**
├── output-styles/ # 9 custom output formatting styles
│ ├── prd/ # Product docs (PRDs, GTM, research)
│ ├── code-generation/ # Code scaffolds and implementations
│ ├── financial-analysis/ # Cost models, SaaS metrics
│ ├── audit-report/ # Audits, reviews, compliance
│ ├── api-specification/ # OpenAPI specs, endpoint docs
│ ├── architecture-decision/ # ADRs, RFCs, trade-off matrices
│ ├── runbook/ # Incident runbooks, DR procedures
│ ├── test-plan/ # Test plans, coverage reports
│ └── monitoring-alert/ # Alert definitions, thresholds
├── .mcp.json # MCP server configs (GitHub, Sentry, Firestore, PostgreSQL)
├── .lsp.json # LSP server configs (TypeScript, Python/Pyright)
├── marketplace.json # Plugin marketplace manifest
├── settings.json # Default permission rules
├── claude-commands/ # Legacy format (backwards compat, 64 files)
├── gemini skills/ # Google Gemini skills (.skill ZIP)
├── CLAUDE.md # Project instructions (Claude)
├── GEMINI.md # Project instructions (Gemini CLI)
├── AGENT-GUIDE.md # How to structure prompts for agents & skills
├── setup.sh # Setup script for Antigravity & other projects
└── README.md
Install the plugin as an npm package from GitHub Packages. This is the easiest way to keep all your projects up to date.
1. Authenticate with GitHub Packages (one-time setup):
# Create a Personal Access Token (PAT) with read:packages scope at
# https://github.com/settings/tokens, then:
npm login --scope=@cure-consulting-group --registry=https://npm.pkg.github.com
Or add to your project's .npmrc:
@cure-consulting-group:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
2. Install in your project:
npm install @cure-consulting-group/product-engineering-skills
The postinstall script automatically:
~/.claude/plugins/ProductEngineeringSkills~/.claude/settings.jsonAll 80 skills, 39 agents, 4 personas, hooks, rules, and output styles are immediately available.
3. Enable auto-updates with Dependabot (recommended):
Add .github/dependabot.yml to your project (or run setup.sh which does this automatically):
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-name: "@cure-consulting-group/product-engineering-skills"
labels:
- "dependencies"
- "skills-update"
commit-message:
prefix: "chore"
include: "scope"
Dependabot will open a PR in your project whenever a new version is published. Merge it and every agent on that project gets the updated skills.
4. Manual update:
npm update @cure-consulting-group/product-engineering-skills
# Load the plugin during a session
claude --plugin-dir /path/to/ProductEngineeringSkills
# Or for development/testing
claude --plugin-dir ./ProductEngineeringSkills
Once loaded, all skills are available as namespaced commands:
/cure-product-engineering:sdlc
/cure-product-engineering:feature-audit
/cure-product-engineering:android-feature-scaffold
/cure-product-engineering:incident-response
/cure-product-engineering:accessibility-audit
Hooks, agents, rules, output styles, and MCP servers are all included automatically.
The fastest way to onboard any project:
# From the target project directory
/path/to/ProductEngineeringSkills/setup.sh
# Or specify the project path
/path/to/ProductEngineeringSkills/setup.sh /path/to/antigravity-app
# Install globally for ALL projects
/path/to/ProductEngineeringSkills/setup.sh --global
# Legacy mode (just copy skills, no hooks/agents)
/path/to/ProductEngineeringSkills/setup.sh --legacy
The setup script will:
~/.claude/plugins/.claude/settings.local.json to .gitignore# Add the Cure Consulting marketplace
claude marketplace add https://github.com/Cure-Consulting-Group/ProductEngineeringSkills/marketplace.json
# Install the plugin
claude plugin install cure-product-engineering
Copy the claude-commands/ files into your project's .claude/commands/ directory:
cp claude-commands/*.md /path/to/your/project/.claude/commands/
Then use them as slash commands:
/sdlc — Generate SDLC artifacts
/android-feature-scaffold — Scaffold an Android feature module
/feature-audit — Audit a completed feature
Import the .skill files from gemini skills/ into your Gemini workspace. Each .skill file is a ZIP archive containing:
SKILL.md — The main skill definitionreferences/ — Supporting documents and templates| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| product-manager | OKRs, roadmaps, RICE prioritization, feature briefs | Yes |
| product-design | Apple HIG, Material Design 3, design tokens, accessibility-first | Yes |
| market-research | TAM/SAM/SOM, competitive analysis, ICP definition (read-only) | Yes |
| go-to-market | GTM plans, launch strategy, channel selection, growth playbooks | Yes |
| product-marketing | Brand strategy, messaging frameworks, campaigns | Yes |
| customer-onboarding | Activation flows, empty states, email sequences, retention | Yes |
| seo-content-engine | Technical SEO, structured data, content strategy | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| sdlc | PRDs, ADRs, RFCs, Epics, Stories, Task specs — full SDLC | Yes |
| android-feature-scaffold | Clean Architecture Android scaffolding (MVI, Compose, Hilt) | Yes |
| ios-architect | Swift/SwiftUI Clean Architecture, MVVM, structured concurrency | Yes |
| nextjs-feature-scaffold | App Router, Server/Client components, Tailwind patterns | Yes |
| firebase-architect | Firestore schema, security rules, Cloud Functions | Yes |
| api-architect | REST/GraphQL design, versioning, auth, rate limiting | Yes |
| api-gateway | API gateway and BFF layers, rate limiting, GraphQL federation | Yes |
| stripe-integration | Stripe payments + subscriptions via Firebase Functions | Yes |
| ai-feature-builder | LLM integration, RAG pipelines, prompt engineering | Yes |
| llmops | LLM operationalization — prompt versioning, eval pipelines, cost optimization, guardrails | Yes |
| database-architect | Schema design, migrations, indexing for Firestore/PostgreSQL/SQLite | Yes |
| data-migration | ETL pipelines, zero-downtime cutover, validation, rollback strategies | Yes |
| infrastructure-scaffold | Cloud infra configs for Firebase, GCP, Vercel, Docker | Yes |
| edge-computing | Edge functions, CDN strategies, cache invalidation, edge middleware | Yes |
| micro-frontends | Module federation, monorepo management, independent deployments | Yes |
| offline-first | Offline-first architecture, sync strategies, conflict resolution, optimistic UI | Yes |
| i18n | Internationalization — string extraction, RTL, locale-aware formatting, translation workflows | Yes |
| notification-architect | Push (FCM/APNs), in-app messaging, email, preference management | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| feature-audit | 5-phase post-completion audit with scored gap report | Yes (read-only, forked) |
| testing-strategy | Testing pyramid, platform standards, coverage rules | Yes |
| e2e-testing | E2E test suites with page objects, visual regression, CI integration | Yes |
| test-accounts | Test user personas, seed data scripts, environment credentials | Yes |
| uat | UAT plans, acceptance criteria checklists, go/no-go release gates | Yes |
| security-review | OWASP checklist, auth/data/API/mobile/web security | Yes (read-only, forked) |
| compliance-architect | HIPAA, COPPA, GDPR, PCI compliance frameworks, consent flows, audit trails | Yes |
| accessibility-audit | WCAG 2.2 compliance, screen readers, inclusive design | Yes (read-only, forked) |
| performance-review | Performance budgets, load testing, optimization strategies | Yes |
| chaos-engineering | Resilience testing, failure injection, graceful degradation, game days | Yes |
| green-software | Sustainable software practices, carbon-aware computing, energy efficiency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| project-bootstrap | Bootstrap repo with CLAUDE.md + STATE.md via codebase inspection and developer interview | Yes |
| project-manager | Sprint planning, RACI, risk registers, retrospectives | Yes |
| ci-cd-pipeline | GitHub Actions, build/test/deploy, environments, secrets | Yes |
| release-management | App store submissions, staged rollouts, versioning, ASO, changelogs | Yes |
| feature-flags | Progressive rollouts, A/B testing, kill switches, experimentation frameworks | Yes |
| observability | Structured logging, distributed tracing, alerting, SLO/SLI, dashboards | Yes |
| dora-metrics | DORA and SPACE metrics — deployment frequency, lead time, MTTR, developer experience | Yes |
| analytics-implementation | Event taxonomy, tracking plans, funnels, dashboards | Yes |
| incident-response | Runbooks, severity classification, post-mortems, escalation | Yes |
| disaster-recovery | DR and business continuity — RTO/RPO, backup strategies, failover, DR testing | Yes |
| growth-engineering | Activation funnels, referral programs, lifecycle automation, PLG patterns | Yes |
| design-system | Design tokens, component libraries, Storybook/Catalog, cross-platform consistency | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| engineering-cost-model | Project estimates, infrastructure costs, build vs buy | Yes (read-only) |
| saas-financial-model | Unit economics, MRR/ARR, pricing tiers, break-even | Yes (read-only) |
| finops | Cloud cost optimization, budget alerts, resource right-sizing, FinOps practices | Yes |
| investor-reporting | Investor updates, board decks, portfolio financials, cap table, runway modeling | Yes |
| fundraising-materials | Pitch decks, data rooms, investor updates, cap table scenarios, fundraising pipeline | Yes |
| burn-rate-tracker | Burn rates, runway scenarios, break-even analysis, cash flow projections | Yes |
| legal-doc-scaffold | ToS, Privacy Policy, SOW, NDA scaffolds | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| portfolio-registry | Product portfolio registry — single source of truth for all products, stacks, teams, stages | Yes |
| technology-radar | ThoughtWorks-style technology radar — Adopt/Trial/Assess/Hold across the portfolio | Yes |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| client-handoff | Handoff packages, runbooks, credential transfers, maintenance SLAs, knowledge transfer | Yes |
| client-communication | Sprint demo scripts, stakeholder updates, risk escalation, executive summaries | Yes |
| proposal-generator | Consulting proposals, SOWs, milestone pricing, engagement structure | No (manual only) |
| Skill | What It Does | Auto-Invoked? |
|---|---|---|
| android-design-expert | Material Design 3 — dynamic color, component tokens, adaptive layouts, motion, Compose patterns | Yes |
| ios-design-expert | Apple HIG — SF Symbols, Dynamic Type, navigation patterns, SwiftUI components | Yes |
| web-design-expert | Responsive design, CSS architecture, design tokens, container queries, accessibility-first, Tailwind | Yes |
| stitch-design | AI-native UI design via Stitch MCP — vibe design, mockups, screen generation, design tokens, component export | Yes |
The library ships a standard maintenance loop (self-provisioned to .claude/loop.md on first session start — run bare /loop to use it), Recurring Mode sections in the goal-shaped skills (finops, burn-rate-tracker, investor-reporting, security-review, and others), and copy-paste cloud-routine recipes in docs/AUTOMATION.md. Library upkeep cadence: docs/MAINTENANCE.md. Mechanism selection and unattended-run guardrails: /cure-product-engineering:engagement-automation.
The plugin ships command and prompt hooks across 9 event types: SessionStart, PreCompact, PostCompact, ConfigChange, PostToolUseFailure, UserPromptSubmit, PreToolUse, Stop, SubagentStop. Highlights: a Stop-hook quality gate (blocks "done" without verification), a PreToolUse static security guard on skill/agent/persona files, and a ConfigChange audit trigger when skill files change mid-session.
New in v4.0: Hooks now suggest and auto-trigger agents based on context. Every code edit, test run, deployment, and PR action recommends the most relevant agent(s).
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Welcome | SessionStart | Confirms plugin loaded with counts; full inventory stays in docs/OVERVIEW.md | Points to inventory |
| Git status | SessionStart | Reports current branch, uncommitted changes, last commit | — |
| Dependency check | SessionStart | Detects outdated packages | Suggests dependency-auditor |
| Code edit advisor | PostToolUse (Edit/Write) | Context-aware suggestions based on file type (.kt, .swift, .ts, .sql, .tf, etc.) | Suggests code-reviewer, test-runner, brand-guardian, migration-validator |
| Command advisor | PostToolUse (Bash) | Post-action guidance for tests, installs, deploys, PRs, releases | Suggests ci-debugger, dependency-auditor, pr-reviewer, release-coordinator |
| Failure recovery | PostToolUseFailure | Diagnoses failure type and suggests fix approach | Auto-suggests ci-debugger, deployment-validator, dependency-auditor |
| Destructive prompt guard | UserPromptSubmit | Detects destructive operations in prompts | Blocks and confirms |
| Protected files | PreToolUse (Edit/Write) | Blocks edits to .env, lock files, credentials, tfstate | — |
| Dangerous commands | PreToolUse (Bash) | Blocks force push, destructive rm, DROP TABLE, prod deploys | — |
| Context re-injection | PreCompact | Re-injects all 80 skills, 39 agents, 4 personas, and Cure standards | Full inventory preserved |
| Post-compact restore | PostCompact | Confirms context restored with agent availability | — |
| Subagent start banner | SubagentStart | Announces agent with role, standards, and companion agents | Lists companion agents |
| Subagent completion | SubagentStop | Suggests follow-up agents (test-runner, code-reviewer, pr-reviewer) | Agent chaining |
| Task quality check | TaskCompleted | Validates tests, security, docs, brand consistency | Suggests test-runner, code-reviewer, doc-generator, brand-guardian |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Code quality gate | PreToolUse (Edit/Write) | Haiku validates: no secrets, no debug logs, no disabled tests, no any types | — |
| Deployment safety | PreToolUse (Bash) | Haiku validates: blocks production deployments outside CI/CD | — |
| Intent classifier | UserPromptSubmit | Haiku classifies prompt intent and suggests the most relevant agent(s) from all 30 | Maps prompts → agents with confidence scores |
| Hook | Event | What It Does | Agent Integration |
|---|---|---|---|
| Completion validator | Stop | Validates: tests for new code, security review for sensitive changes, rollback for migrations, docs for features, brand consistency for UI, analytics for events, API contracts | Suggests specific agents for each gap found |
Pre-configured MCP servers in .mcp.json:
| Server | Type | What It Does |
|---|---|---|
| GitHub | HTTP | PR management, issue tracking, code search |
| Sentry | HTTP | Error monitoring, issue tracking, release health |
| Firestore | stdio | Direct database queries, schema inspection |
| PostgreSQL | stdio | Database queries, schema inspection, migrations |
Pre-configured LSP servers in .lsp.json:
| Server | Language | What It Provides |
|---|---|---|
| TypeScript | .ts, .tsx, .js | Type checking, auto-imports, refactoring, go-to-definition |
| Python (Pyright) | *.py | Static type analysis, import resolution, error diagnostics |
Custom output formatting for different artifact types:
| Style | Used By | Key Rules |
|---|---|---|
| prd | Product skills (PRDs, GTM, research) | Numbered sections, decision matrices, executive summaries |
| code-generation | Engineering skills (scaffolds) | File tree first, dependency order, complete runnable code |
| financial-analysis | Business skills (costs, models) | ASCII tables, explicit assumptions, sensitivity analysis |
| audit-report | Quality skills (audits, reviews) | Severity scoring, checklists, remediation with effort estimates |
| api-specification | API design skills | OpenAPI 3.0 blocks, endpoint tables, request/response examples |
| architecture-decision | ADR and RFC skills | Context/decision/consequences format, trade-off matrices |
| runbook | Incident response, disaster recovery | Numbered steps, command blocks, decision trees, escalation paths |
| test-plan | Testing strategy, QA skills | Coverage tables, test case templates, pass/fail criteria |
| monitoring-alert | Observability, incident response | Alert definition tables, threshold rationale, runbook links |
| Agent | Purpose | Tools | Auto-Triggered By |
|---|---|---|---|
| code-reviewer | Security + quality review against Cure standards | Read-only | Stop hook, SubagentStop |
| project-bootstrapper | Set up new projects with correct architecture |
hooks/register.tsx 151 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Pasted } from '../types'
5import { MAX_ROWS, MAX_SHOWN, caption, cellBox, fitsInline, imageDirs, imageIds, parseSize, sameIds } from './images'
6
7/**
8 * cure-image-viewer: draws the images you sent above the prompt until the
9 * turn that carried them ends. Claude Code saves a pasted image when the
10 * prompt is submitted, not when it is pasted, so there is nothing to draw
11 * while it is still a draft.
12 *
13 * The plugin API gives a pasted image's kind, never its bytes, so the mod
14 * reads the copy Claude Code saves in the session's own temp folder (see
15 * imageDirs). That layout is this build's, not a documented contract: when a
16 * file is not where expected the row says so instead of drawing nothing.
17 *
18 * The picture is drawn by the terminal through the kitty graphics protocol.
19 * Its bytes are sent inline, not as a file path for the terminal to open:
20 * Warp (0.2026.09.30) draws inline bytes and ignores a path, and inline is
21 * the form every kitty-protocol terminal accepts. An image over 2 MiB cannot
22 * go inline and is captioned instead.
23 *
24 * Setting, as an environment variable:
25 * CURE_IMAGE_DIR the folder to read `<N>.png` from, when the default is wrong
26 */
27
28const draft = atom({ plugin: 'cure-image-viewer', key: 'draft' } as const, [] as Pasted[])
29const sent = atom({ plugin: 'cure-image-viewer', key: 'sent' } as const, [] as Pasted[])
30
31let dirs: string[] | undefined
32// Base64 of each picture by path. Kept out of $.state: megabytes that every state read would copy.
33const pixels = new Map<string, string>()
34
35export const register: Register = on => {
36 on('prompt.edit', async ($, e, next) => {
37 const box = await next(e)
38 const ids = imageIds(box.text)
39 if (!sameIds(await read($, draft), ids)) {
40 const found = await locate($, ids)
41 await update($, draft, () => found)
42 }
43 return box
44 })
45
46 on('prompt.submit', async ($, e, next) => {
47 const ids = imageIds(e.text)
48 const found = ids.length > 0 ? await locate($, ids) : []
49 await update($, sent, () => found)
50 await update($, draft, () => [])
51 return next(e)
52 })
53
54 on('turn.complete', async ($, e, next) => {
55 await update($, sent, () => [])
56 return next(e)
57 })
58
59 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
60 // A draft's images are not on disk yet; show the ones that are, else the sent ones.
61 const drafted = (await read($, draft)).filter(image => image.path !== null)
62 const images = drafted.length > 0 ? drafted : await read($, sent)
63 if (images.length === 0 || e.props.hasSurvey) return next(e)
64
65 const table = $.ui.resolve(e)
66 const { Box, Text } = table
67 const isNarrow = e.props.bodyColumns < 100 && images.length > 1
68 const shown = isNarrow ? images.slice(0, 1) : images.slice(0, MAX_SHOWN)
69 const png = new Map<number, string>()
70 if ('Image' in table) {
71 for (const image of shown) {
72 const data = fitsInline(image) ? await bytesOf($, image.path as string) : undefined
73 if (data) png.set(image.id, data)
74 }
75 }
76 const each = Math.floor((e.props.bodyColumns - (shown.length - 1) * 2) / shown.length)
77 const maxRows = Math.min(MAX_ROWS, e.props.maxRows || MAX_ROWS)
78
79 return (
80 <Box flexDirection="column">
81 <Box flexDirection="row" gap={2}>
82 {shown.map(image => {
83 const data = png.get(image.id)
84 if (!('Image' in table) || data === undefined) return <Text dimColor>{caption(image)}</Text>
85 const size = cellBox(image.width, image.height, each, maxRows)
86 return (
87 <Box flexDirection="column">
88 <table.Image
89 key={`image-${image.id}`}
90 source={{ png: data }}
91 columns={size.columns}
92 rows={size.rows}
93 alt={caption(image)}
94 />
95 <Text dimColor>{caption(image)}</Text>
96 </Box>
97 )
98 })}
99 </Box>
100 {images.length > shown.length ? (
101 <Text dimColor>+{images.length - shown.length} more{isNarrow ? ` (${images.length} images pasted)` : ''}</Text>
102 ) : null}
103 </Box>
104 )
105 })
106}
107
108async function bytesOf($: EngineInterface, path: string): Promise<string | undefined> {
109 const held = pixels.get(path)
110 if (held !== undefined) return held
111 try {
112 const { base64 } = await $.fs.read(path, { as: 'bytes' })
113 pixels.set(path, base64)
114 return base64
115 } catch {
116 return undefined
117 }
118}
119
120async function candidateDirs($: EngineInterface): Promise<string[]> {
121 const override = await $.env.get('CURE_IMAGE_DIR')
122 if (override) return [override]
123 if (dirs) return dirs
124 const uid = (await $.process.run(['id', '-u'])).stdout.trim()
125 const id = await $.session.id()
126 const roots = [...new Set([await $.session.cwd(), await $.session.root()])]
127 dirs = roots.flatMap(cwd => imageDirs(uid, cwd, id))
128 return dirs
129}
130
131async function locate($: EngineInterface, ids: readonly number[]): Promise<Pasted[]> {
132 const folders = await candidateDirs($).catch(() => [] as string[])
133 const out: Pasted[] = []
134 for (const id of ids) {
135 let path: string | null = null
136 let bytes = 0
137 for (const folder of folders) {
138 const entries = await $.fs.list(folder).catch(() => [])
139 const hit = entries.find(entry => entry.kind === 'file' && entry.name.startsWith(`${id}.`))
140 if (hit) {
141 path = `${folder}/${hit.name}`
142 bytes = hit.size
143 break
144 }
145 }
146 const size = path ? parseSize((await $.process.run(['file', path]).catch(() => ({ stdout: '' }))).stdout) : { width: 0, height: 0 }
147 out.push({ id, path, ...size, bytes })
148 }
149 return out
150}
151hooks/images.ts 65 lines1import type { Pasted } from '../types'
2
3/** A terminal cell is about twice as tall as it is wide. */
4const CELL_ASPECT = 2
5export const MAX_ROWS = 10
6export const MAX_COLUMNS = 40
7export const MAX_SHOWN = 4
8/** The most an Image may carry inline, decoded: the element's own limit. */
9export const MAX_INLINE_BYTES = 2 * 1024 * 1024
10
11/** Whether the picture can be sent to the terminal as bytes. */
12export function fitsInline(image: Pasted): boolean {
13 return image.path !== null && image.path.endsWith('.png') && image.bytes > 0 && image.bytes <= MAX_INLINE_BYTES
14}
15
16/** The ids of every `[Image #N]` in a prompt, in order, each once. */
17export function imageIds(text: string): number[] {
18 const ids: number[] = []
19 for (const m of text.matchAll(/\[Image #(\d+)\]/g)) {
20 const id = Number(m[1])
21 if (!ids.includes(id)) ids.push(id)
22 }
23 return ids
24}
25
26/** The folder name Claude Code derives from a working directory: every character outside A-Z a-z 0-9 becomes `-`. */
27export function projectSlug(cwd: string): string {
28 return cwd.replace(/[^a-zA-Z0-9]/g, '-')
29}
30
31/** Where this build saves a session's pasted images: `<tmp>/claude-<uid>/<slug>/<session>/images`, one candidate per tmp root. */
32export function imageDirs(uid: string, cwd: string, sessionId: string): string[] {
33 return ['/private/tmp', '/tmp'].map(tmp => `${tmp}/claude-${uid}/${projectSlug(cwd)}/${sessionId}/images`)
34}
35
36/** Reads `680 x 336` out of `file`'s line for a PNG; zeros when it is not there. */
37export function parseSize(fileOutput: string): { width: number; height: number } {
38 const m = /PNG image data, (\d+) x (\d+)/.exec(fileOutput)
39 return m ? { width: Number(m[1]), height: Number(m[2]) } : { width: 0, height: 0 }
40}
41
42/** The box of cells a picture is drawn in: its own aspect, inside maxRows by `maxColumns`. */
43export function cellBox(width: number, height: number, maxColumns: number = MAX_COLUMNS, maxRows: number = MAX_ROWS): { columns: number; rows: number } {
44 if (width <= 0 || height <= 0) return { columns: Math.min(MAX_COLUMNS, maxColumns), rows: maxRows }
45 const limit = Math.max(1, Math.min(MAX_COLUMNS, maxColumns))
46 let rows = maxRows
47 let columns = Math.round((rows * CELL_ASPECT * width) / height)
48 if (columns > limit) {
49 columns = limit
50 rows = Math.max(1, Math.round((columns * height) / (width * CELL_ASPECT)))
51 }
52 return { columns: Math.max(1, columns), rows: Math.min(maxRows, rows) }
53}
54
55export function caption(image: Pasted): string {
56 if (image.path === null) return `[Image #${image.id}] not found on disk`
57 const size = image.width > 0 ? ` ${image.width}×${image.height}` : ''
58 const tooLarge = image.bytes > MAX_INLINE_BYTES ? ` · ${(image.bytes / (1024 * 1024)).toFixed(1)} MB, too large to draw` : ''
59 return `[Image #${image.id}]${size}${tooLarge}`
60}
61
62export function sameIds(a: readonly Pasted[], ids: readonly number[]): boolean {
63 return a.length === ids.length && a.every((image, i) => image.id === ids[i])
64}
65types/index.d.ts 23 lines1export type Pasted = {
2 /** The N of the prompt's `[Image #N]`. */
3 id: number
4 /** The saved file's absolute path; null when none was found for this id. */
5 path: string | null
6 /** Pixel size, 0 when it could not be read. */
7 width: number
8 height: number
9 /** The file's size in bytes, 0 when unknown. */
10 bytes: number
11}
12
13declare module 'claude-code' {
14 interface PluginState {
15 'cure-image-viewer': {
16 /** Images the draft in the prompt box names. */
17 draft: Pasted[]
18 /** Images the running turn's prompt carried; cleared when the turn ends. */
19 sent: Pasted[]
20 }
21 }
22}
23