Claude Skill

lov-business-card

Generate a professional, editorial-style business card (2:1) as a high-resolution PNG and a self-contained interactive HTML. Collects the user's identity (name, roles, tagline, interests, locations, optional avatar), fills a parameterized design template with a chosen theme, and

LLM Mart · 0 points · 10 views 12 listing impressions 0 install-command copies

#design

Virus-scanned Reviewed automatically before listing.

Full trust report

Download lovstudio-skills-skills_business-card-0b16007.zip · 5136 KB
Part of lovstudio/skills — 83 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/lovstudio/skills/tree/main/skills/business-card
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lovstudio-skills@llmmart
Git git clone https://github.com/lovstudio/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lovstudio/skills collection as a plugin from our marketplace. Git is the plain clone.

README

business-card example

lov-business-card

把任何人的姓名、头衔与金句,一键变成精美的编辑式名片。
高清 PNG(默认 4800×2400) · 可点击下载的自包含 HTML · 三套主题

version license example.com · part of general-skills


这是什么

把一段身份信息渲染成一张 2:1 的**编辑式(editorial / luxury-minimal)**名片:瑞士排版网格、 发丝线分隔、单一强调色、宋体 × Didot 字体配对、刊头与底栏分区、淡淡的姓名水印。靠字号、字距、 层级建立秩序,而不是堆装饰。没有头像?自动用姓名首字渲染衬线 monogram。

示例

上图为「品牌方」的实际名片(dark-terracotta 主题 + 头像)。同一套数据,三种主题:

dark-terracotta(暖深·陶土,默认) midnight(冷深·钢蓝) ivory(浅·象牙 + monogram 回退)
dark midnight ivory

Install

git clone https://example.com/skills/business-card-skill "${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-business-card"

Requires: Python 3.8+ and Google Chrome / Chromium (for PNG rendering). macOS 用内置 sips 裁切, 其他平台 pip install pillow。没有浏览器时跳过 PNG —— 打开 HTML 用右下角按钮下载即可。

Usage

SKILL_DIR="${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-business-card"
python3 "$SKILL_DIR/scripts/render_card.py" \
  --name "品牌方" --latin "Mark Shawn" \
  --brand "SKILL.AI" --index "STUDIO — Nº 2026" \
  --tags "背包客,超级开发者,AI / OPC 布道师" \
  --tagline "在 **Agent 时代**,|寻找**人**的意义" \
  --pursuits "旅行,羽毛球,计算机科学,心理学,哲学" \
  --bases "上海 陆家嘴数智港,北京 搜狐大厦清智孵化器" \
  --avatar ./me.png --theme dark-terracotta \
  --out ./output --format both

Options

Option Default Description
--name (required) 姓名,支持中文
--latin "" 英文名 / 罗马音(斜体小字)
--brand "" 左上品牌;含 . 时圆点会高亮
--index "" 右上编号,如 STUDIO — Nº 2026
--tags "" 角色标签,逗号分隔
--tagline "" 金句;**词** 高亮,\| 换行
--pursuits "" 爱好,逗号分隔(底栏左)
--bases "" 据点/城市,逗号分隔(底栏右,带定位针)
--avatar "" 头像图片路径;省略则用 monogram
--caption "" 头像底部图注(可选)
--watermark 姓名末字 背景巨型水印字
--theme dark-terracotta dark-terracotta | midnight | ivory
--out ./output 输出目录
--format both png | html | both
--scale 3 PNG 倍率(3 → 4800×2400)

Output

  • {name}-名片.png — 高清名片(默认 4800×2400)
  • {name}-名片.html + modern-screenshot.umd.js — 浏览器打开,点「下载为图片 PNG」自出 3× 高清图(两文件需同目录)

加主题

往 scripts/render_card.py 的 THEMES 字典加一组 CSS 变量即可,模板无需改动。

Star History

Star History Chart

License

MIT

Skill manifest

名片工坊 · Business Card Studio

把任意用户的身份信息,渲染成一张 2:1 的编辑式(editorial / luxury-minimal)名片: 瑞士排版网格、发丝线分隔、单一强调色、宋体 × Didot 字体配对。输出高分辨率 PNG (默认 4800×2400)+ 一个自包含、可在浏览器点击下载的 HTML。

工作流

1. 先用 AskUserQuestion 收集信息(必做)

不要直接跑脚本。 先确认主题与必填项。能从对话/记忆里推断的先预填,只问不知道的。

建议问题(一次最多 3 个):

  • 主题:dark-terracotta(暖深·陶土,默认)/ midnight(冷深·钢蓝)/ ivory(浅·象牙编辑风)
  • 是否有头像:有 → 要文件路径;无 → 用名字首字渲染 monogram
  • 强调词:tagline 里哪个词要高亮(强调色)

身份字段尽量从上下文取;缺的逐项问或让用户一次性给。

2. 映射到 CLI 参数

用户提供 flag 说明
姓名 --name 必填,支持中文
英文名/罗马音 --latin 名字旁的斜体小字
品牌(左上) --brand 如 SKILL.AI(含 . 会高亮圆点)
编号(右上) --index 如 STUDIO — Nº 2026
角色标签 --tags 逗号分隔,菱形分隔渲染
金句 --tagline 词 高亮,竖线 | 换行
爱好/兴趣 --pursuits 逗号分隔(底栏左)
据点/城市 --bases 逗号分隔,每项带定位针(底栏右)
头像 --avatar 图片路径;省略则用 monogram
头像图注 --caption 可选,肖像底部小字
主题 --theme dark-terracotta|midnight|ivory
输出目录 --out 默认 ./output
格式 --format png|html|both(默认 both)

3. 运行

python3 scripts/render_card.py \
  --name "品牌方" --latin "Mark Shawn" \
  --brand "SKILL.AI" --index "STUDIO — Nº 2026" \
  --tags "背包客,超级开发者,AI / OPC 布道师" \
  --tagline "在 **Agent 时代**,|寻找**人**的意义" \
  --pursuits "旅行,羽毛球,计算机科学,心理学,哲学" \
  --bases "上海 陆家嘴数智港,北京 搜狐大厦清智孵化器" \
  --avatar ./me.png --theme dark-terracotta \
  --out ./output --format both

4. 交付

把 PNG 展示给用户;告诉他们 HTML 可在浏览器打开、右下角「下载为图片 PNG」自出 3× 高清图 (HTML 与同目录的 modern-screenshot.umd.js 需一起保留)。

设计说明

  • 比例:固定 2:1(1600×800 画布),PNG 按 --scale 倍率导出(默认 3 → 4800×2400)。
  • 自适应:HTML 在浏览器里等比缩放铺满视口,绝不放大、不溢出;导出尺寸不受影响。
  • 无头像:自动用姓名首字渲染衬线 monogram,仍然专业。
  • 主题扩展:在 scripts/render_card.py 的 THEMES 字典加一组 CSS 变量即可。

User Configuration

本 skill 不假设任何私有工作区。所有身份、品牌、输出路径都由 CLI flag 传入; --out 默认当前目录下 ./output。如需复用默认身份,调用方可自行从用户 profile 预填这些 flag,但脚本本身不读取任何固定个人路径。

Runtime context (shared)

运行前读取本 Skill 包的 skill.yaml,由宿主提供 skill-runtime/v1 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。

  • 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
  • required: true 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
  • 报错提供可复制的 context_id、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。

通用反馈闭环

用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:

  1. 先判断意见是 task-specific(仅本次)还是 reusable(可跨任务复用)。
  2. task-specific 只修改当前任务,不改 Skill。
  3. reusable 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
  4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
  5. reusable 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
Files (skills)
  • assets
    • modern-screenshot.umd.js 28.6 KB
      (function(y,v){typeof exports=="object"&&typeof module!="undefined"?v(exports):typeof define=="function"&&define.amd?define(["exports"],v):(y=typeof globalThis!="undefined"?globalThis:y||self,v(y.modernScreenshot={}))})(this,function(y){"use strict";var rr=Object.defineProperty,nr=Object.defineProperties;var or=Object.getOwnPropertyDescriptors;var Z=Object.getOwnPropertySymbols;var xe=Object.prototype.hasOwnProperty,Me=Object.prototype.propertyIsEnumerable;var Oe=Math.pow,Le=(y,v,N)=>v in y?rr(y,v,{enumerable:!0,configurable:!0,writable:!0,value:N}):y[v]=N,D=(y,v)=>{for(var N in v||(v={}))xe.call(v,N)&&Le(y,N,v[N]);if(Z)for(var N of Z(v))Me.call(v,N)&&Le(y,N,v[N]);return y},M=(y,v)=>nr(y,or(v));var je=(y,v)=>{var N={};for(var R in y)xe.call(y,R)&&v.indexOf(R)<0&&(N[R]=y[R]);if(y!=null&&Z)for(var R of Z(y))v.indexOf(R)<0&&Me.call(y,R)&&(N[R]=y[R]);return N};var C=(y,v,N)=>new Promise((R,O)=>{var X=P=>{try{q(N.next(P))}catch(W){O(W)}},j=P=>{try{q(N.throw(P))}catch(W){O(W)}},q=P=>P.done?R(P.value):Promise.resolve(P.value).then(X,j);q((N=N.apply(y,v)).next())});var Be;function v(e,t){return e[13]=1,e[14]=t>>8,e[15]=t&255,e[16]=t>>8,e[17]=t&255,e}const N=112,R=72,O=89,X=115;let j;function q(){const e=new Int32Array(256);for(let t=0;t<256;t++){let r=t;for(let n=0;n<8;n++)r=r&1?3988292384^r>>>1:r>>>1;e[t]=r}return e}function P(e){let t=-1;j||(j=q());for(let r=0;r<e.length;r++)t=j[(t^e[r])&255]^t>>>8;return t^-1}function W(e){const t=e.length-1;for(let r=t;r>=4;r--)if(e[r-4]===9&&e[r-3]===N&&e[r-2]===R&&e[r-1]===O&&e[r]===X)return r-3;return 0}function ae(e,t,r=!1){const n=new Uint8Array(13);t*=39.3701,n[0]=N,n[1]=R,n[2]=O,n[3]=X,n[4]=t>>>24,n[5]=t>>>16,n[6]=t>>>8,n[7]=t&255,n[8]=n[4],n[9]=n[5],n[10]=n[6],n[11]=n[7],n[12]=1;const i=P(n),a=new Uint8Array(4);if(a[0]=i>>>24,a[1]=i>>>16,a[2]=i>>>8,a[3]=i&255,r){const s=W(e);return e.set(n,s),e.set(a,s+13),e}else{const s=new Uint8Array(4);s[0]=0,s[1]=0,s[2]=0,s[3]=9;const o=new Uint8Array(54);return o.set(e,0),o.set(s,33),o.set(n,37),o.set(a,50),o}}const qe="AAlwSFlz",We="AAAJcEhZ",He="AAAACXBI";function Ve(e){let t=e.indexOf(qe);return t===-1&&(t=e.indexOf(We)),t===-1&&(t=e.indexOf(He)),t}const se="[modern-screenshot]",U=typeof window!="undefined",ze=U&&"Worker"in window,ie=U&&"atob"in window,Xe=U&&"btoa"in window,ee=U?(Be=window.navigator)==null?void 0:Be.userAgent:"",le=ee.includes("Chrome"),G=ee.includes("AppleWebKit")&&!le,te=ee.includes("Firefox"),Ge=e=>e&&"__CONTEXT__"in e,Ye=e=>e.constructor.name==="CSSFontFaceRule",Je=e=>e.constructor.name==="CSSImportRule",Ke=e=>e.constructor.name==="CSSLayerBlockRule",I=e=>e.nodeType===1,H=e=>typeof e.className=="object",ce=e=>e.tagName==="image",Qe=e=>e.tagName==="use",V=e=>I(e)&&typeof e.style!="undefined"&&!H(e),Ze=e=>e.nodeType===8,et=e=>e.nodeType===3,$=e=>e.tagName==="IMG",Y=e=>e.tagName==="VIDEO",tt=e=>e.tagName==="CANVAS",rt=e=>e.tagName==="TEXTAREA",nt=e=>e.tagName==="INPUT",ot=e=>e.tagName==="STYLE",at=e=>e.tagName==="SCRIPT",st=e=>e.tagName==="SELECT",it=e=>e.tagName==="SLOT",lt=e=>e.tagName==="IFRAME",ct=(...e)=>console.warn(se,...e);function ut(e){var r;const t=(r=e==null?void 0:e.createElement)==null?void 0:r.call(e,"canvas");return t&&(t.height=t.width=1),!!t&&"toDataURL"in t&&!!t.toDataURL("image/webp").includes("image/webp")}const re=e=>e.startsWith("data:");function ue(e,t){if(e.match(/^[a-z]+:\/\//i))return e;if(U&&e.match(/^\/\//))return window.location.protocol+e;if(e.match(/^[a-z]+:/i)||!U)return e;const r=J().implementation.createHTMLDocument(),n=r.createElement("base"),i=r.createElement("a");return r.head.appendChild(n),r.body.appendChild(i),t&&(n.href=t),i.href=e,i.href}function J(e){var t;return(t=e&&I(e)?e==null?void 0:e.ownerDocument:e)!=null?t:window.document}const K="http://www.w3.org/2000/svg";function fe(e,t,r){const n=J(r).createElementNS(K,"svg");return n.setAttributeNS(null,"width",e.toString()),n.setAttributeNS(null,"height",t.toString()),n.setAttributeNS(null,"viewBox",`0 0 ${e} ${t}`),n}function de(e,t){let r=new XMLSerializer().serializeToString(e);return t&&(r=r.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu,"")),`data:image/svg+xml;charset=utf-8,${encodeURIComponent(r)}`}function ft(e,t="image/png",r=1){return C(this,null,function*(){try{return yield new Promise((n,i)=>{e.toBlob(a=>{a?n(a):i(new Error("Blob is null"))},t,r)})}catch(n){if(ie)return dt(e.toDataURL(t,r));throw n}})}function dt(e){var o,c;const[t,r]=e.split(","),n=(c=(o=t.match(/data:(.+);/))==null?void 0:o[1])!=null?c:void 0,i=window.atob(r),a=i.length,s=new Uint8Array(a);for(let u=0;u<a;u+=1)s[u]=i.charCodeAt(u);return new Blob([s],{type:n})}function ge(e,t){return new Promise((r,n)=>{const i=new FileReader;i.onload=()=>r(i.result),i.onerror=()=>n(i.error),i.onabort=()=>n(new Error(`Failed read blob to ${t}`)),t==="dataUrl"?i.readAsDataURL(e):t==="arrayBuffer"&&i.readAsArrayBuffer(e)})}const gt=e=>ge(e,"dataUrl"),mt=e=>ge(e,"arrayBuffer");function _(e,t){const r=J(t).createElement("img");return r.decoding="sync",r.loading="eager",r.src=e,r}function L(e,t){return new Promise(r=>{const{timeout:n,ownerDocument:i,onError:a,onWarn:s}=t!=null?t:{},o=typeof e=="string"?_(e,J(i)):e;let c=null,u=null;function l(){r(o),c&&clearTimeout(c),u==null||u()}if(n&&(c=setTimeout(l,n)),Y(o)){const d=o.currentSrc||o.src;if(!d)return o.poster?L(o.poster,t).then(r):l();if(o.readyState>=2)return l();const m=l,f=h=>{s==null||s("Failed video load",d,h),a==null||a(h),l()};u=()=>{o.removeEventListener("loadeddata",m),o.removeEventListener("error",f)},o.addEventListener("loadeddata",m,{once:!0}),o.addEventListener("error",f,{once:!0})}else{const d=ce(o)?o.href.baseVal:o.currentSrc||o.src;if(!d)return l();const m=()=>C(this,null,function*(){if($(o)&&"decode"in o)try{yield o.decode()}catch(h){s==null||s("Failed to decode image, trying to render anyway",o.dataset.originalSrc||d,h)}l()}),f=h=>{s==null||s("Failed image load",o.dataset.originalSrc||d,h),l()};if($(o)&&o.complete)return m();u=()=>{o.removeEventListener("load",m),o.removeEventListener("error",f)},o.addEventListener("load",m,{once:!0}),o.addEventListener("error",f,{once:!0})}})}function me(e,t){return C(this,null,function*(){V(e)&&($(e)||Y(e)?yield L(e,t):yield Promise.all(["img","video"].flatMap(r=>Array.from(e.querySelectorAll(r)).map(n=>L(n,t)))))})}const he=function(){let t=0;const r=()=>`0000${(Math.random()*Oe(36,4)<<0).toString(36)}`.slice(-4);return()=>(t+=1,`u${r()}${t}`)}();function we(e){return e==null?void 0:e.split(",").map(t=>t.trim().replace(/"|'/g,"").toLowerCase()).filter(Boolean)}let pe=0;function ht(e){const t=`${se}[#${pe}]`;return pe++,{time:r=>e&&console.time(`${t} ${r}`),timeEnd:r=>e&&console.timeEnd(`${t} ${r}`),warn:(...r)=>e&&ct(...r)}}function wt(e){return{cache:e?"no-cache":"force-cache"}}function k(e,t){return C(this,null,function*(){return Ge(e)?e:ye(e,M(D({},t),{autoDestruct:!0}))})}function ye(e,t){return C(this,null,function*(){var f,h,g,p,E;const{scale:r=1,workerUrl:n,workerNumber:i=1}=t||{},a=!!(t!=null&&t.debug),s=(f=t==null?void 0:t.features)!=null?f:!0,o=(h=e.ownerDocument)!=null?h:U?window.document:void 0,c=(p=(g=e.ownerDocument)==null?void 0:g.defaultView)!=null?p:U?window:void 0,u=new Map,l=M(D({width:0,height:0,quality:1,type:"image/png",scale:r,backgroundColor:null,style:null,filter:null,maximumCanvasSize:0,timeout:3e4,progress:null,debug:a,fetch:D({requestInit:wt((E=t==null?void 0:t.fetch)==null?void 0:E.bypassingCache),placeholderImage:"data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",bypassingCache:!1},t==null?void 0:t.fetch),fetchFn:null,font:{},drawImageInterval:100,workerUrl:null,workerNumber:i,onCloneEachNode:null,onCloneNode:null,onEmbedNode:null,onCreateForeignObjectSvg:null,includeStyleProperties:null,autoDestruct:!1},t),{__CONTEXT__:!0,log:ht(a),node:e,ownerDocument:o,ownerWindow:c,dpi:r===1?null:96*r,svgStyleElement:be(o),svgDefsElement:o==null?void 0:o.createElementNS(K,"defs"),svgStyles:new Map,defaultComputedStyles:new Map,workers:[...Array.from({length:ze&&n&&i?i:0})].map(()=>{try{const b=new Worker(n);return b.onmessage=w=>C(this,null,function*(){var A,F,B,$e;const{url:S,result:T}=w.data;T?(F=(A=u.get(S))==null?void 0:A.resolve)==null||F.call(A,T):($e=(B=u.get(S))==null?void 0:B.reject)==null||$e.call(B,new Error(`Error receiving message from worker: ${S}`))}),b.onmessageerror=w=>{var T,A;const{url:S}=w.data;(A=(T=u.get(S))==null?void 0:T.reject)==null||A.call(T,new Error(`Error receiving message from worker: ${S}`))},b}catch(b){return l.log.warn("Failed to new Worker",b),null}}).filter(Boolean),fontFamilies:new Map,fontCssTexts:new Map,acceptOfImage:`${[ut(o)&&"image/webp","image/svg+xml","image/*","*/*"].filter(Boolean).join(",")};q=0.8`,requests:u,drawImageCount:0,tasks:[],features:s,isEnable:b=>{var w,S;return b==="restoreScrollPosition"?typeof s=="boolean"?!1:(w=s[b])!=null?w:!1:typeof s=="boolean"?s:(S=s[b])!=null?S:!0},shadowRoots:[]});l.log.time("wait until load"),yield me(e,{timeout:l.timeout,onWarn:l.log.warn}),l.log.timeEnd("wait until load");const{width:d,height:m}=pt(e,l);return l.width=d,l.height=m,l})}function be(e){if(!e)return;const t=e.createElement("style"),r=t.ownerDocument.createTextNode(`
      .______background-clip--text {
        background-clip: text;
        -webkit-background-clip: text;
      }
      `);return t.appendChild(r),t}function pt(e,t){let{width:r,height:n}=t;if(I(e)&&(!r||!n)){const i=e.getBoundingClientRect();r=r||i.width||Number(e.getAttribute("width"))||0,n=n||i.height||Number(e.getAttribute("height"))||0}return{width:r,height:n}}function yt(e,t){return C(this,null,function*(){const{log:r,timeout:n,drawImageCount:i,drawImageInterval:a}=t;r.time("image to canvas");const s=yield L(e,{timeout:n,onWarn:t.log.warn}),{canvas:o,context2d:c}=bt(e.ownerDocument,t),u=()=>{try{c==null||c.drawImage(s,0,0,o.width,o.height)}catch(l){t.log.warn("Failed to drawImage",l)}};if(u(),t.isEnable("fixSvgXmlDecode"))for(let l=0;l<i;l++)yield new Promise(d=>{setTimeout(()=>{c==null||c.clearRect(0,0,o.width,o.height),u(),d()},l+a)});return t.drawImageCount=0,r.timeEnd("image to canvas"),o})}function bt(e,t){const{width:r,height:n,scale:i,backgroundColor:a,maximumCanvasSize:s}=t,o=e.createElement("canvas");o.width=Math.floor(r*i),o.height=Math.floor(n*i),o.style.width=`${r}px`,o.style.height=`${n}px`,s&&(o.width>s||o.height>s)&&(o.width>s&&o.height>s?o.width>o.height?(o.height*=s/o.width,o.width=s):(o.width*=s/o.height,o.height=s):o.width>s?(o.height*=s/o.width,o.width=s):(o.width*=s/o.height,o.height=s));const c=o.getContext("2d");return c&&a&&(c.fillStyle=a,c.fillRect(0,0,o.width,o.height)),{canvas:o,context2d:c}}function Se(e,t){if(e.ownerDocument)try{const a=e.toDataURL();if(a!=="data:,")return _(a,e.ownerDocument)}catch(a){t.log.warn("Failed to clone canvas",a)}const r=e.cloneNode(!1),n=e.getContext("2d"),i=r.getContext("2d");try{return n&&i&&i.putImageData(n.getImageData(0,0,e.width,e.height),0,0),r}catch(a){t.log.warn("Failed to clone canvas",a)}return r}function St(e,t){var r;try{if((r=e==null?void 0:e.contentDocument)!=null&&r.documentElement)return ne(e.contentDocument.documentElement,t)}catch(n){t.log.warn("Failed to clone iframe",n)}return e.cloneNode(!1)}function Et(e){const t=e.cloneNode(!1);return e.currentSrc&&e.currentSrc!==e.src&&(t.src=e.currentSrc,t.srcset=""),t.loading==="lazy"&&(t.loading="eager"),t}function Ct(e,t){return C(this,null,function*(){if(e.ownerDocument&&!e.currentSrc&&e.poster)return _(e.poster,e.ownerDocument);const r=e.cloneNode(!1);r.crossOrigin="anonymous",e.currentSrc&&e.currentSrc!==e.src&&(r.src=e.currentSrc);const n=r.ownerDocument;if(n){let i=!0;if(yield L(r,{onError:()=>i=!1,onWarn:t.log.warn}),!i)return e.poster?_(e.poster,e.ownerDocument):r;r.currentTime=e.currentTime,yield new Promise(s=>{r.addEventListener("seeked",s,{once:!0})});const a=n.createElement("canvas");a.width=e.offsetWidth,a.height=e.offsetHeight;try{const s=a.getContext("2d");s&&s.drawImage(r,0,0,a.width,a.height)}catch(s){return t.log.warn("Failed to clone video",s),e.poster?_(e.poster,e.ownerDocument):r}return Se(a,t)}return r})}function Tt(e,t){return tt(e)?Se(e,t):lt(e)?St(e,t):$(e)?Et(e):Y(e)?Ct(e,t):e.cloneNode(!1)}function vt(e){let t=e.sandbox;if(!t){const{ownerDocument:r}=e;try{r&&(t=r.createElement("iframe"),t.id=`__SANDBOX__${he()}`,t.width="0",t.height="0",t.style.visibility="hidden",t.style.position="fixed",r.body.appendChild(t),t.srcdoc='<!DOCTYPE html><meta charset="UTF-8"><title></title><body>',e.sandbox=t)}catch(n){e.log.warn("Failed to getSandBox",n)}}return t}const At=["width","height","-webkit-text-fill-color"],Nt=["stroke","fill"];function Ee(e,t,r){const{defaultComputedStyles:n}=r,i=e.nodeName.toLowerCase(),a=H(e)&&i!=="svg",s=a?Nt.map(g=>[g,e.getAttribute(g)]).filter(([,g])=>g!==null):[],o=[a&&"svg",i,s.map((g,p)=>`${g}=${p}`).join(","),t].filter(Boolean).join(":");if(n.has(o))return n.get(o);const c=vt(r),u=c==null?void 0:c.contentWindow;if(!u)return new Map;const l=u==null?void 0:u.document;let d,m;a?(d=l.createElementNS(K,"svg"),m=d.ownerDocument.createElementNS(d.namespaceURI,i),s.forEach(([g,p])=>{m.setAttributeNS(null,g,p)}),d.appendChild(m)):d=m=l.createElement(i),m.textContent=" ",l.body.appendChild(d);const f=u.getComputedStyle(m,t),h=new Map;for(let g=f.length,p=0;p<g;p++){const E=f.item(p);At.includes(E)||h.set(E,f.getPropertyValue(E))}return l.body.removeChild(d),n.set(o,h),h}function Ce(e,t,r){var o;const n=new Map,i=[],a=new Map;if(r)for(const c of r)s(c);else for(let c=e.length,u=0;u<c;u++){const l=e.item(u);s(l)}for(let c=i.length,u=0;u<c;u++)(o=a.get(i[u]))==null||o.forEach((l,d)=>n.set(d,l));function s(c){const u=e.getPropertyValue(c),l=e.getPropertyPriority(c),d=c.lastIndexOf("-"),m=d>-1?c.substring(0,d):void 0;if(m){let f=a.get(m);f||(f=new Map,a.set(m,f)),f.set(c,[u,l])}t.get(c)===u&&!l||(m?i.push(m):n.set(c,[u,l]))}return n}function Rt(e,t,r,n){var d,m,f,h;const{ownerWindow:i,includeStyleProperties:a,currentParentNodeStyle:s}=n,o=t.style,c=i.getComputedStyle(e),u=Ee(e,null,n);s==null||s.forEach((g,p)=>{u.delete(p)});const l=Ce(c,u,a);l.delete("transition-property"),l.delete("all"),l.delete("d"),l.delete("content"),r&&(l.delete("position"),l.delete("margin-top"),l.delete("margin-right"),l.delete("margin-bottom"),l.delete("margin-left"),l.delete("margin-block-start"),l.delete("margin-block-end"),l.delete("margin-inline-start"),l.delete("margin-inline-end"),l.set("box-sizing",["border-box",""])),((d=l.get("background-clip"))==null?void 0:d[0])==="text"&&t.classList.add("______background-clip--text"),le&&(l.has("font-kerning")||l.set("font-kerning",["normal",""]),(((m=l.get("overflow-x"))==null?void 0:m[0])==="hidden"||((f=l.get("overflow-y"))==null?void 0:f[0])==="hidden")&&((h=l.get("text-overflow"))==null?void 0:h[0])==="ellipsis"&&e.scrollWidth===e.clientWidth&&l.set("text-overflow",["clip",""]));for(let g=o.length,p=0;p<g;p++)o.removeProperty(o.item(p));return l.forEach(([g,p],E)=>{o.setProperty(E,g,p)}),l}function It(e,t){(rt(e)||nt(e)||st(e))&&t.setAttribute("value",e.value)}const kt=["::before","::after"],Dt=["::-webkit-scrollbar","::-webkit-scrollbar-button","::-webkit-scrollbar-thumb","::-webkit-scrollbar-track","::-webkit-scrollbar-track-piece","::-webkit-scrollbar-corner","::-webkit-resizer"];function Pt(e,t,r,n,i){const{ownerWindow:a,svgStyleElement:s,svgStyles:o,currentNodeStyle:c}=n;if(!s||!a)return;function u(l){var w;const d=a.getComputedStyle(e,l);let m=d.getPropertyValue("content");if(!m||m==="none")return;i==null||i(m),m=m.replace(/(')|(")|(counter\(.+\))/g,"");const f=[he()],h=Ee(e,l,n);c==null||c.forEach((S,T)=>{h.delete(T)});const g=Ce(d,h,n.includeStyleProperties);g.delete("content"),g.delete("-webkit-locale"),((w=g.get("background-clip"))==null?void 0:w[0])==="text"&&t.classList.add("______background-clip--text");const p=[`content: '${m}';`];if(g.forEach(([S,T],A)=>{p.push(`${A}: ${S}${T?" !important":""};`)}),p.length===1)return;try{t.className=[t.className,...f].join(" ")}catch(S){n.log.warn("Failed to copyPseudoClass",S);return}const E=p.join(`
        `);let b=o.get(E);b||(b=[],o.set(E,b)),b.push(`.${f[0]}${l}`)}kt.forEach(u),r&&Dt.forEach(u)}const Te=new Set(["symbol"]);function ve(e,t,r,n,i){return C(this,null,function*(){if(I(r)&&(ot(r)||at(r))||n.filter&&!n.filter(r))return;Te.has(t.nodeName)||Te.has(r.nodeName)?n.currentParentNodeStyle=void 0:n.currentParentNodeStyle=n.currentNodeStyle;const a=yield ne(r,n,!1,i);n.isEnable("restoreScrollPosition")&&Ut(e,a),t.appendChild(a)})}function Ae(e,t,r,n){return C(this,null,function*(){var a;let i=e.firstChild;I(e)&&e.shadowRoot&&(i=(a=e.shadowRoot)==null?void 0:a.firstChild,r.shadowRoots.push(e.shadowRoot));for(let s=i;s;s=s.nextSibling)if(!Ze(s))if(I(s)&&it(s)&&typeof s.assignedNodes=="function"){const o=s.assignedNodes();for(let c=0;c<o.length;c++)yield ve(e,t,o[c],r,n)}else yield ve(e,t,s,r,n)})}function Ut(e,t){if(!V(e)||!V(t))return;const{scrollTop:r,scrollLeft:n}=e;if(!r&&!n)return;const{transform:i}=t.style,a=new DOMMatrix(i),{a:s,b:o,c,d:u}=a;a.a=1,a.b=0,a.c=0,a.d=1,a.translateSelf(-n,-r),a.a=s,a.b=o,a.c=c,a.d=u,t.style.transform=a.toString()}function _t(e,t){const{backgroundColor:r,width:n,height:i,style:a}=t,s=e.style;if(r&&s.setProperty("background-color",r,"important"),n&&s.setProperty("width",`${n}px`,"important"),i&&s.setProperty("height",`${i}px`,"important"),a)for(const o in a)s[o]=a[o]}const Ft=/^[\w-:]+$/;function ne(e,t,r=!1,n){return C(this,null,function*(){var u,l,d,m;const{ownerDocument:i,ownerWindow:a,fontFamilies:s,onCloneEachNode:o}=t;if(i&&et(e))return n&&/\S/.test(e.data)&&n(e.data),i.createTextNode(e.data);if(i&&a&&I(e)&&(V(e)||H(e))){const f=yield Tt(e,t);if(t.isEnable("removeAbnormalAttributes")){const w=f.getAttributeNames();for(let S=w.length,T=0;T<S;T++){const A=w[T];Ft.test(A)||f.removeAttribute(A)}}const h=t.currentNodeStyle=Rt(e,f,r,t);r&&_t(f,t);let g=!1;if(t.isEnable("copyScrollbar")){const w=[(u=h.get("overflow-x"))==null?void 0:u[0],(l=h.get("overflow-y"))==null?void 0:l[0]];g=w.includes("scroll")||(w.includes("auto")||w.includes("overlay"))&&(e.scrollHeight>e.clientHeight||e.scrollWidth>e.clientWidth)}const p=(d=h.get("text-transform"))==null?void 0:d[0],E=we((m=h.get("font-family"))==null?void 0:m[0]),b=E?w=>{p==="uppercase"?w=w.toUpperCase():p==="lowercase"?w=w.toLowerCase():p==="capitalize"&&(w=w[0].toUpperCase()+w.substring(1)),E.forEach(S=>{let T=s.get(S);T||s.set(S,T=new Set),w.split("").forEach(A=>T.add(A))})}:void 0;return Pt(e,f,g,t,b),It(e,f),Y(e)||(yield Ae(e,f,t,b)),yield o==null?void 0:o(f),f}const c=e.cloneNode(!1);return yield Ae(e,c,t),yield o==null?void 0:o(c),c})}function Ne(e){if(e.ownerDocument=void 0,e.ownerWindow=void 0,e.svgStyleElement=void 0,e.svgDefsElement=void 0,e.svgStyles.clear(),e.defaultComputedStyles.clear(),e.sandbox){try{e.sandbox.remove()}catch(t){e.log.warn("Failed to destroyContext",t)}e.sandbox=void 0}e.workers=[],e.fontFamilies.clear(),e.fontCssTexts.clear(),e.requests.clear(),e.tasks=[],e.shadowRoots=[]}function Bt(e){const o=e,{url:t,timeout:r,responseType:n}=o,i=je(o,["url","timeout","responseType"]),a=new AbortController,s=r?setTimeout(()=>a.abort(),r):void 0;return fetch(t,D({signal:a.signal},i)).then(c=>{if(!c.ok)throw new Error("Failed fetch, not 2xx response",{cause:c});switch(n){case"arrayBuffer":return c.arrayBuffer();case"dataUrl":return c.blob().then(gt);case"text":default:return c.text()}}).finally(()=>clearTimeout(s))}function z(e,t){const{url:r,requestType:n="text",responseType:i="text",imageDom:a}=t;let s=r;const{timeout:o,acceptOfImage:c,requests:u,fetchFn:l,fetch:{requestInit:d,bypassingCache:m,placeholderImage:f},font:h,workers:g,fontFamilies:p}=e;n==="image"&&(G||te)&&e.drawImageCount++;let E=u.get(r);if(!E){m&&m instanceof RegExp&&m.test(s)&&(s+=(/\?/.test(s)?"&":"?")+new Date().getTime());const b=n.startsWith("font")&&h&&h.minify,w=new Set;b&&n.split(";")[1].split(",").forEach(F=>{p.has(F)&&p.get(F).forEach(B=>w.add(B))});const S=b&&w.size,T=D({url:s,timeout:o,responseType:S?"arrayBuffer":i,headers:n==="image"?{accept:c}:void 0},d);E={type:n,resolve:void 0,reject:void 0,response:null},E.response=C(this,null,function*(){if(l&&n==="image"){const A=yield l(r);if(A)return A}return!G&&r.startsWith("http")&&g.length?new Promise((A,F)=>{g[u.size&g.length-1].postMessage(D({rawUrl:r},T)),E.resolve=A,E.reject=F}):Bt(T)}).catch(A=>{if(u.delete(r),n==="image"&&f)return e.log.warn("Failed to fetch image base64, trying to use placeholder image",s),typeof f=="string"?f:f(a);throw A}),u.set(r,E)}return E.response}function Re(e,t,r,n){return C(this,null,function*(){if(!Ie(e))return e;for(const[i,a]of $t(e,t))try{const s=yield z(r,{url:a,requestType:n?"image":"text",responseType:"dataUrl"});e=e.replace(Lt(i),`$1${s}$3`)}catch(s){r.log.warn("Failed to fetch css data url",i,s)}return e})}function Ie(e){return/url\((['"]?)([^'"]+?)\1\)/.test(e)}const ke=/url\((['"]?)([^'"]+?)\1\)/g;function $t(e,t){const r=[];return e.replace(ke,(n,i,a)=>(r.push([a,ue(a,t)]),n)),r.filter(([n])=>!re(n))}function Lt(e){const t=e.replace(/([.*+?^${}()|\[\]\/\\])/g,"\\$1");return new RegExp(`(url\\(['"]?)(${t})(['"]?\\))`,"g")}const xt=["background-image","border-image-source","-webkit-border-image","-webkit-mask-image","list-style-image"];function Mt(e,t){return xt.map(r=>{const n=e.getPropertyValue(r);return!n||n==="none"?null:((G||te)&&t.drawImageCount++,Re(n,null,t,!0).then(i=>{!i||n===i||e.setProperty(r,i,e.getPropertyPriority(r))}))}).filter(Boolean)}function Ot(e,t){if($(e)){const r=e.currentSrc||e.src;if(!re(r))return[z(t,{url:r,imageDom:e,requestType:"image",responseType:"dataUrl"}).then(n=>{n&&(e.srcset="",e.dataset.originalSrc=r,e.src=n||"")})];(G||te)&&t.drawImageCount++}else if(H(e)&&!re(e.href.baseVal)){const r=e.href.baseVal;return[z(t,{url:r,imageDom:e,requestType:"image",responseType:"dataUrl"}).then(n=>{n&&(e.dataset.originalSrc=r,e.href.baseVal=n||"")})]}return[]}function jt(e,t){var o;const{ownerDocument:r,svgDefsElement:n}=t,i=(o=e.getAttribute("href"))!=null?o:e.getAttribute("xlink:href");if(!i)return[];const[a,s]=i.split("#");if(s){const c=`#${s}`,u=t.shadowRoots.reduce((l,d)=>l!=null?l:d.querySelector(`svg ${c}`),r==null?void 0:r.querySelector(`svg ${c}`));if(a&&e.setAttribute("href",c),n!=null&&n.querySelector(c))return[];if(u)return n==null||n.appendChild(u.cloneNode(!0)),[];if(a)return[z(t,{url:a,responseType:"text"}).then(l=>{n==null||n.insertAdjacentHTML("beforeend",l)})]}return[]}function De(e,t){const{tasks:r}=t;I(e)&&(($(e)||ce(e))&&r.push(...Ot(e,t)),Qe(e)&&r.push(...jt(e,t))),V(e)&&r.push(...Mt(e.style,t)),e.childNodes.forEach(n=>{De(n,t)})}function qt(e,t){return C(this,null,function*(){const{ownerDocument:r,svgStyleElement:n,fontFamilies:i,fontCssTexts:a,tasks:s,font:o}=t;if(!(!r||!n||!i.size))if(o&&o.cssText){const c=Ue(o.cssText,t);n.appendChild(r.createTextNode(`${c}
      `))}else{const c=Array.from(r.styleSheets).filter(f=>{try{return"cssRules"in f&&!!f.cssRules.length}catch(h){return t.log.warn(`Error while reading CSS rules from ${f.href}`,h),!1}}),u=r.implementation.createHTMLDocument(""),l=u.createElement("style");u.head.appendChild(l);const d=l.sheet;yield Promise.all(c.flatMap(f=>Array.from(f.cssRules).map(h=>C(this,null,function*(){if(Je(h)){const g=h.href;let p="";try{p=yield z(t,{url:g,requestType:"text",responseType:"text"})}catch(b){t.log.warn(`Error fetch remote css import from ${g}`,b)}const E=p.replace(ke,(b,w,S)=>b.replace(S,ue(S,g)));for(const b of Ht(E))try{d.insertRule(b,d.cssRules.length)}catch(w){t.log.warn("Error inserting rule from remote css import",{rule:b,error:w})}}})))),d.cssRules.length&&c.push(d);const m=[];c.forEach(f=>{oe(f.cssRules,m)}),m.filter(f=>{var h;return Ye(f)&&Ie(f.style.getPropertyValue("src"))&&((h=we(f.style.getPropertyValue("font-family")))==null?void 0:h.some(g=>i.has(g)))}).forEach(f=>{const h=f,g=a.get(h.cssText);g?n.appendChild(r.createTextNode(`${g}
      `)):s.push(Re(h.cssText,h.parentStyleSheet?h.parentStyleSheet.href:null,t).then(p=>{p=Ue(p,t),a.set(h.cssText,p),n.appendChild(r.createTextNode(`${p}
      `))}))})}})}const Wt=/(\/\*[\s\S]*?\*\/)/g,Pe=/((@.*?keyframes [\s\S]*?){([\s\S]*?}\s*?)})/gi;function Ht(e){if(e==null)return[];const t=[];let r=e.replace(Wt,"");for(;;){const a=Pe.exec(r);if(!a)break;t.push(a[0])}r=r.replace(Pe,"");const n=/@import[\s\S]*?url\([^)]*\)[\s\S]*?;/gi,i=new RegExp("((\\s*?(?:\\/\\*[\\s\\S]*?\\*\\/)?\\s*?@media[\\s\\S]*?){([\\s\\S]*?)}\\s*?})|(([\\s\\S]*?){([\\s\\S]*?)})","gi");for(;;){let a=n.exec(r);if(a)i.lastIndex=n.lastIndex;else if(a=i.exec(r),a)n.lastIndex=i.lastIndex;else break;t.push(a[0])}return t}const Vt=/url\([^)]+\)\s*format\((["']?)([^"']+)\1\)/g,zt=/src:\s*(?:url\([^)]+\)\s*format\([^)]+\)[,;]\s*)+/g;function Ue(e,t){const{font:r}=t,n=r?r==null?void 0:r.preferredFormat:void 0;return n?e.replace(zt,i=>{for(;;){const[a,,s]=Vt.exec(i)||[];if(!s)return"";if(s===n)return`src: ${a};`}}):e}function oe(e,t=[]){for(const r of Array.from(e))Ke(r)?t.push(...oe(r.cssRules)):"cssRules"in r?oe(r.cssRules,t):t.push(r);return t}const Xt=/\bx?link:?href\s*=\s*["'](?!data:)[^"']+["']/i;function Gt(e){return Xt.test(e.innerHTML)}function _e(e,t){return C(this,null,function*(){const r=yield k(e,t);if(I(r.node)&&H(r.node)&&!Gt(r.node))return r.node;const{ownerDocument:n,log:i,tasks:a,svgStyleElement:s,svgDefsElement:o,svgStyles:c,font:u,progress:l,autoDestruct:d,onCloneNode:m,onEmbedNode:f,onCreateForeignObjectSvg:h}=r;i.time("clone node");const g=yield ne(r.node,r,!0);if(s&&n){let S="";c.forEach((T,A)=>{S+=`${T.join(`,
      `)} {
        ${A}
      }
      `}),s.appendChild(n.createTextNode(S))}i.timeEnd("clone node"),yield m==null?void 0:m(g),u!==!1&&I(g)&&(i.time("embed web font"),yield qt(g,r),i.timeEnd("embed web font")),i.time("embed node"),De(g,r);const p=a.length;let E=0;const b=()=>C(this,null,function*(){for(;;){const S=a.pop();if(!S)break;try{yield S}catch(T){r.log.warn("Failed to run task",T)}l==null||l(++E,p)}});l==null||l(E,p),yield Promise.all([...Array.from({length:4})].map(b)),i.timeEnd("embed node"),yield f==null?void 0:f(g);const w=Yt(g,r);return o&&w.insertBefore(o,w.children[0]),s&&w.insertBefore(s,w.children[0]),d&&Ne(r),yield h==null?void 0:h(w),w})}function Yt(e,t){const{width:r,height:n}=t,i=fe(r,n,e.ownerDocument),a=i.ownerDocument.createElementNS(i.namespaceURI,"foreignObject");return a.setAttributeNS(null,"x","0%"),a.setAttributeNS(null,"y","0%"),a.setAttributeNS(null,"width","100%"),a.setAttributeNS(null,"height","100%"),a.append(e),i.appendChild(a),i}function Q(e,t){return C(this,null,function*(){var s;const r=yield k(e,t),n=yield _e(r),i=de(n,r.isEnable("removeControlCharacter"));r.autoDestruct||(r.svgStyleElement=be(r.ownerDocument),r.svgDefsElement=(s=r.ownerDocument)==null?void 0:s.createElementNS(K,"defs"),r.svgStyles.clear());const a=_(i,n.ownerDocument);return yield yt(a,r)})}function Jt(e,t){return C(this,null,function*(){const r=yield k(e,t),{log:n,type:i,quality:a,dpi:s}=r,o=yield Q(r);n.time("canvas to blob");const c=yield ft(o,i,a);if(["image/png","image/jpeg"].includes(i)&&s){const u=yield mt(c.slice(0,33));let l=new Uint8Array(u);return i==="image/png"?l=ae(l,s):i==="image/jpeg"&&(l=v(l,s)),n.timeEnd("canvas to blob"),new Blob([l,c.slice(33)],{type:i})}return n.timeEnd("canvas to blob"),c})}function x(e,t){return C(this,null,function*(){const r=yield k(e,t),{log:n,quality:i,type:a,dpi:s}=r,o=yield Q(r);n.time("canvas to data url");let c=o.toDataURL(a,i);if(["image/png","image/jpeg"].includes(a)&&s&&ie&&Xe){const[u,l]=c.split(",");let d=0,m=!1;if(a==="image/png"){const w=Ve(l);w>=0?(d=Math.ceil((w+28)/3)*4,m=!0):d=33/3*4}else a==="image/jpeg"&&(d=18/3*4);const f=l.substring(0,d),h=l.substring(d),g=window.atob(f),p=new Uint8Array(g.length);for(let w=0;w<p.length;w++)p[w]=g.charCodeAt(w);const E=a==="image/png"?ae(p,s,m):v(p,s),b=window.btoa(String.fromCharCode(...E));c=[u,",",b,h].join("")}return n.timeEnd("canvas to data url"),c})}function Fe(e,t){return C(this,null,function*(){const r=yield k(e,t),{width:n,height:i,ownerDocument:a}=r,s=yield x(r),o=fe(n,i,a),c=o.ownerDocument.createElementNS(o.namespaceURI,"image");return c.setAttributeNS(null,"href",s),c.setAttributeNS(null,"height","100%"),c.setAttributeNS(null,"width","100%"),o.appendChild(c),de(o,r.isEnable("removeControlCharacter"))})}function Kt(e,t){return C(this,null,function*(){const r=yield k(e,t),{ownerDocument:n,width:i,height:a,scale:s,type:o}=r,c=o==="image/svg+xml"?yield Fe(r):yield x(r),u=_(c,n);return u.width=Math.floor(i*s),u.height=Math.floor(a*s),u.style.width=`${i}px`,u.style.height=`${a}px`,u})}function Qt(e,t){return C(this,null,function*(){return x(yield k(e,M(D({},t),{type:"image/jpeg"})))})}function Zt(e,t){return C(this,null,function*(){const r=yield k(e,t),n=yield Q(r);return n.getContext("2d").getImageData(0,0,n.width,n.height).data})}function er(e,t){return C(this,null,function*(){return x(yield k(e,M(D({},t),{type:"image/png"})))})}function tr(e,t){return C(this,null,function*(){return x(yield k(e,M(D({},t),{type:"image/webp"})))})}y.createContext=ye,y.destroyContext=Ne,y.domToBlob=Jt,y.domToCanvas=Q,y.domToDataUrl=x,y.domToForeignObjectSvg=_e,y.domToImage=Kt,y.domToJpeg=Qt,y.domToPixel=Zt,y.domToPng=er,y.domToSvg=Fe,y.domToWebp=tr,y.loadMedia=L,y.waitUntilLoad=me,Object.defineProperty(y,Symbol.toStringTag,{value:"Module"})});
      
    • template.html 7.4 KB · in bundle
  • docs
    • images
      • example-dark-terracotta.png 2 MB · in bundle
      • example-ivory-monogram.png 1.1 MB · in bundle
      • example-midnight.png 2 MB · in bundle
  • references
    • user-config.md 1.6 KB
      # User Configuration
      
      This skill follows the portable agent skill profile contract. It must not
      assume a private workspace, personal absolute paths, or private brand assets.
      
      ## Resolution Order
      
      1. Explicit CLI flags.
      2. Environment variables.
      3. Shared profile JSON.
      4. Safe defaults such as the current working directory or `$HOME/Documents`.
      5. Ask the user once for missing required fields.
      
      ## Shared Profile
      
      Default profile path:
      
      ```bash
      ${SKILL_PROFILE_PATH:-$HOME/.skill-publisher/skills/profile.json}
      ```
      
      Example:
      
      ```json
      {
        "user": {
          "name": "Your Name",
          "language": "zh-CN",
          "timezone": "Asia/Shanghai"
        },
        "workspace": {
          "root": "$HOME/projects",
          "output_dir": "$HOME/Documents/lov-skill-output"
        },
        "brand": {
          "name": "Your Brand",
          "site": "https://example.com",
          "profile": "$HOME/.skill-publisher/skills/brand.json",
          "design_guide": "$HOME/.skill-publisher/skills/design-guide.md"
        }
      }
      ```
      
      Environment variable overrides:
      
      | Variable | Meaning |
      |----------|---------|
      | `SKILL_PROFILE_PATH` | Path to the shared profile JSON |
      | `SKILLS_CONFIG_DIR` | Shared Skill Publisher skills config/data directory |
      | `SKILL_WORKSPACE_ROOT` | User workspace root |
      | `SKILL_OUTPUT_DIR` | Default generated output directory |
      | `SKILL_PROFILE_PATH` | Brand profile JSON or Markdown |
      | `SKILL_DESIGN_GUIDE` | Design guide path |
      
      ## Implementation Notes
      
      - Scripts should accept explicit paths via CLI flags.
      - Missing profile fields should produce actionable errors.
      - Skill Publisher maintainer defaults belong in an optional profile, not in the workflow.
      
  • scripts
    • render_card.py 10.1 KB
      #!/usr/bin/env python3
      """
      render_card.py — Generate a professional editorial business card (2:1).
      
      Fills a parameterized HTML template with the user's identity and renders it to
      a high-resolution PNG via headless Chrome. The HTML itself is self-contained
      (avatar embedded as base64, modern-screenshot vendored) so it also works
      standalone in a browser with a click-to-download button.
      
      Usage:
          python3 render_card.py \
              --name "品牌方" --latin "Mark Shawn" \
              --brand "SKILL.AI" --index "Nº 2026" \
              --tags "背包客,超级开发者,AI / OPC 布道师" \
              --tagline "在 **Agent 时代**,|寻找**人**的意义" \
              --pursuits "旅行,羽毛球,计算机科学,心理学,哲学" \
              --bases "上海 陆家嘴数智港,北京 搜狐大厦清智孵化器" \
              --avatar ./me.png --theme dark-terracotta \
              --out ./output --format both
      
      Notes:
      - --tagline: use ** ** to mark accent words, and | to split lines.
      - --bases: comma-separated; each gets a pin marker.
      - --avatar omitted → a monogram (first glyph of --name) is rendered instead.
      - --format: png | html | both (default both).
      """
      
      import argparse
      import base64
      import html
      import mimetypes
      import os
      import re
      import shutil
      import subprocess
      import sys
      import tempfile
      from pathlib import Path
      
      ASSETS = Path(__file__).resolve().parent.parent / "assets"
      TEMPLATE = ASSETS / "template.html"
      SCREENSHOT_JS = ASSETS / "modern-screenshot.umd.js"
      
      # ---------------------------------------------------------------- themes
      THEMES = {
          "dark-terracotta": {
              "--page": "#0c0a08", "--bg": "#17120d", "--ink": "#ece4d6",
              "--ink-dim": "#b6ab98", "--muted": "#8a7d6c",
              "--terra": "#c9663e", "--terra-deep": "#a64f30",
              "--terra-line": "rgba(201,102,62,.5)", "--hair": "rgba(236,228,214,.13)",
              "--glow1": "rgba(201,102,62,.20)", "--glow2": "rgba(201,102,62,.07)",
              "--wm": "rgba(201,102,62,.045)",
              "--mono-a": "#241a12", "--mono-b": "#3a241a",
          },
          "midnight": {
              "--page": "#080b12", "--bg": "#0f1420", "--ink": "#e6ecf5",
              "--ink-dim": "#aab6c8", "--muted": "#7e8aa0",
              "--terra": "#6f9bd1", "--terra-deep": "#4f76a8",
              "--terra-line": "rgba(111,155,209,.5)", "--hair": "rgba(230,236,245,.12)",
              "--glow1": "rgba(111,155,209,.18)", "--glow2": "rgba(111,155,209,.06)",
              "--wm": "rgba(111,155,209,.05)",
              "--mono-a": "#121b2c", "--mono-b": "#1c2940",
          },
          "ivory": {
              "--page": "#eceae3", "--bg": "#f4f1ea", "--ink": "#1d1a16",
              "--ink-dim": "#4a443b", "--muted": "#6d6557",
              "--terra": "#b8563a", "--terra-deep": "#943f28",
              "--terra-line": "rgba(184,86,58,.55)", "--hair": "rgba(29,26,22,.14)",
              "--glow1": "rgba(184,86,58,.12)", "--glow2": "rgba(184,86,58,.05)",
              "--wm": "rgba(184,86,58,.05)",
              "--mono-a": "#e8e2d4", "--mono-b": "#ded5c2",
          },
      }
      
      CHROME_CANDIDATES = [
          "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
          "/Applications/Chromium.app/Contents/MacOS/Chromium",
          shutil.which("google-chrome") or "",
          shutil.which("chromium") or "",
          shutil.which("chromium-browser") or "",
      ]
      
      
      def find_chrome():
          for c in CHROME_CANDIDATES:
              if c and Path(c).exists():
                  return c
          return None
      
      
      def slugify(s):
          s = re.sub(r"\s+", "-", s.strip())
          s = re.sub(r"[^\w一-鿿-]", "", s)
          return s or "card"
      
      
      def esc(s):
          return html.escape(s, quote=True)
      
      
      def emphasize(text):
          """Convert **word** -> <em>word</em>, escaping the rest."""
          out, last = [], 0
          for m in re.finditer(r"\*\*(.+?)\*\*", text):
              out.append(esc(text[last:m.start()]))
              out.append("<em>" + esc(m.group(1)) + "</em>")
              last = m.end()
          out.append(esc(text[last:]))
          return "".join(out)
      
      
      def build_tagline(raw):
          lines = [seg for seg in raw.split("|")]
          return "<br>".join(emphasize(l) for l in lines)
      
      
      def build_roles(tags):
          items = [esc(t.strip()) for t in tags if t.strip()]
          sep = '<i class="sep"></i>'
          return sep.join(items)
      
      
      def build_pursuits(items):
          cleaned = [esc(p.strip()) for p in items if p.strip()]
          return '<i class="mid"></i>'.join(cleaned)
      
      
      def build_bases(items):
          parts = []
          for i, b in enumerate(items):
              b = b.strip()
              if not b:
                  continue
              pin = '<i class="pin n"></i>' if parts else '<i class="pin"></i>'
              parts.append(pin + esc(b))
          return "".join(parts)
      
      
      def build_portrait(avatar, name, caption):
          if avatar:
              p = Path(avatar).expanduser()
              if not p.exists():
                  sys.exit(f"avatar not found: {p}")
              mime = mimetypes.guess_type(str(p))[0] or "image/png"
              data = base64.b64encode(p.read_bytes()).decode()
              img = f'<img src="data:{mime};base64,{data}" alt="{esc(name)}">'
              cap = f'<div class="pcaption">{esc(caption)}</div>' if caption else ""
              return img + cap
          glyph = esc(name.strip()[:1]) if name.strip() else "·"
          return f'<div class="mono">{glyph}</div>'
      
      
      def theme_vars(theme):
          t = THEMES.get(theme)
          if not t:
              sys.exit(f"unknown theme: {theme} (choose from {', '.join(THEMES)})")
          body = ";".join(f"{k}:{v}" for k, v in t.items())
          return ".stage,.viewport,.dl-btn,body{" + body + "}"
      
      
      def render_html(args):
          tpl = TEMPLATE.read_text(encoding="utf-8")
          name = args.name
          latin = f'<p class="latin">{esc(args.latin)}</p>' if args.latin else ""
          repl = {
              "__TITLE__": esc(name) + " · 名片",
              "__THEME_VARS__": theme_vars(args.theme),
              "__WM__": esc(args.watermark or (name.strip()[-1:] if name.strip() else "")),
              "__BRAND__": render_brand(args.brand),
              "__INDEX__": esc(args.index or ""),
              "__PORTRAIT__": build_portrait(args.avatar, name, args.caption),
              "__NAME__": esc(name),
              "__LATIN__": latin,
              "__ROLES__": build_roles(args.tags.split(",")) if args.tags else "",
              "__TAGLINE__": build_tagline(args.tagline) if args.tagline else "",
              "__PURSUITS__": build_pursuits(args.pursuits.split(",")) if args.pursuits else "",
              "__BASES__": build_bases(args.bases.split(",")) if args.bases else "",
              "__SLUG__": slugify(name),
          }
          for k, v in repl.items():
              tpl = tpl.replace(k, v)
          return tpl
      
      
      def render_brand(brand):
          """Highlight a single dot in a brand like SKILL.AI."""
          if not brand:
              return ""
          if "." in brand:
              head, _, tail = brand.partition(".")
              return esc(head) + '<span class="ds">.</span>' + esc(tail)
          return esc(brand)
      
      
      def screenshot(html_path, png_path, chrome, scale=3):
          """Render full card to PNG by capturing a 1700x900 window then cropping."""
          with tempfile.TemporaryDirectory() as tmp:
              full = Path(tmp) / "full.png"
              cmd = [
                  chrome, "--headless=new", "--disable-gpu", "--hide-scrollbars",
                  "--virtual-time-budget=6000", "--run-all-compositor-stages-before-draw",
                  f"--force-device-scale-factor={scale}", "--window-size=1700,900",
                  f"--screenshot={full}", f"file://{html_path}#shoot",
              ]
              subprocess.run(cmd, check=True, capture_output=True)
              if not full.exists():
                  sys.exit("chrome screenshot failed")
              crop(full, png_path, 1600 * scale, 800 * scale)
      
      
      def crop(src, dst, w, h):
          """Center-crop src to w x h, writing dst. Uses sips (mac) or PIL."""
          if shutil.which("sips"):
              subprocess.run(["sips", "-c", str(h), str(w), str(src), "--out", str(dst)],
                             check=True, capture_output=True)
              return
          try:
              from PIL import Image
          except ImportError:
              sys.exit("need `sips` (macOS) or Pillow (`pip install pillow`) to crop")
          im = Image.open(src)
          W, H = im.size
          left, top = (W - w) // 2, (H - h) // 2
          im.crop((left, top, left + w, top + h)).save(dst)
      
      
      def main():
          ap = argparse.ArgumentParser(description="Generate an editorial business card (2:1).")
          ap.add_argument("--name", required=True, help="display name (CJK ok)")
          ap.add_argument("--latin", default="", help="secondary/romanized name")
          ap.add_argument("--brand", default="", help="top-left brand text, e.g. SKILL.AI")
          ap.add_argument("--index", default="", help="top-right line, e.g. 'STUDIO — Nº 2026'")
          ap.add_argument("--tags", default="", help="comma-separated role tags")
          ap.add_argument("--tagline", default="", help="hero line; **accent**, | splits lines")
          ap.add_argument("--pursuits", default="", help="comma-separated interests (footer left)")
          ap.add_argument("--bases", default="", help="comma-separated locations (footer right)")
          ap.add_argument("--avatar", default="", help="portrait image path (optional)")
          ap.add_argument("--caption", default="", help="portrait caption (optional)")
          ap.add_argument("--watermark", default="", help="giant background glyph (default: last char of name)")
          ap.add_argument("--theme", default="dark-terracotta",
                          help="dark-terracotta | midnight | ivory")
          ap.add_argument("--out", default="./output", help="output directory")
          ap.add_argument("--format", default="both", choices=["png", "html", "both"])
          ap.add_argument("--scale", type=int, default=3, help="PNG scale factor (default 3 -> 4800x2400)")
          args = ap.parse_args()
      
          out = Path(args.out).expanduser()
          out.mkdir(parents=True, exist_ok=True)
          slug = slugify(args.name)
      
          html_doc = render_html(args)
          html_path = out / f"{slug}-名片.html"
          html_path.write_text(html_doc, encoding="utf-8")
          # vendored screenshot lib next to the html so the download button works
          shutil.copy(SCREENSHOT_JS, out / "modern-screenshot.umd.js")
          print(f"✓ HTML  {html_path}")
      
          if args.format in ("png", "both"):
              chrome = find_chrome()
              if not chrome:
                  print("⚠ Chrome/Chromium not found — skipping PNG. Open the HTML and click 下载.")
              else:
                  png_path = out / f"{slug}-名片.png"
                  screenshot(str(html_path), str(png_path), chrome, args.scale)
                  print(f"✓ PNG   {png_path}  ({1600*args.scale}x{800*args.scale})")
      
      
      if __name__ == "__main__":
          main()
      
  • .gitignore 94 B · in bundle
  • CHANGELOG.md 317 B
    # Changelog
    
    All notable changes to this skill are documented here.
    Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning: [SemVer](https://semver.org/)
    
    ## [0.2.0] - 2026-08-24
    
    ### Added
    
    - add the shared feedback-classification and approval-invalidation gate used by every LovStudio Skill
    
  • LICENSE 1.1 KB · in bundle
  • README.md 3.9 KB
    <p align="center">
      <img src="docs/images/example-dark-terracotta.png" alt="business-card example" width="100%">
    </p>
    
    <h1 align="center">lov-business-card</h1>
    
    <p align="center">
      <strong>把任何人的姓名、头衔与金句,一键变成精美的编辑式名片。</strong><br>
      <sub>高清 PNG(默认 4800×2400) · 可点击下载的自包含 HTML · 三套主题</sub>
    </p>
    
    <p align="center">
      <img src="https://img.shields.io/badge/version-0.1.0-CC785C" alt="version">
      <img src="https://img.shields.io/badge/license-MIT-green" alt="license">
      <a href="https://example.com">example.com</a> · part of <a href="https://example.com/skills/general-skills">general-skills</a>
    </p>
    
    ---
    
    ## 这是什么
    
    把一段身份信息渲染成一张 2:1 的**编辑式(editorial / luxury-minimal)**名片:瑞士排版网格、
    发丝线分隔、单一强调色、宋体 × Didot 字体配对、刊头与底栏分区、淡淡的姓名水印。靠字号、字距、
    层级建立秩序,而不是堆装饰。没有头像?自动用姓名首字渲染衬线 monogram。
    
    ## 示例
    
    上图为「品牌方」的实际名片(`dark-terracotta` 主题 + 头像)。同一套数据,三种主题:
    
    | `dark-terracotta`(暖深·陶土,默认) | `midnight`(冷深·钢蓝) | `ivory`(浅·象牙 + monogram 回退) |
    |---|---|---|
    | ![dark](docs/images/example-dark-terracotta.png) | ![midnight](docs/images/example-midnight.png) | ![ivory](docs/images/example-ivory-monogram.png) |
    
    ## Install
    
    ```bash
    git clone https://example.com/skills/business-card-skill "${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-business-card"
    ```
    
    Requires: Python 3.8+ and Google Chrome / Chromium (for PNG rendering). macOS 用内置 `sips` 裁切,
    其他平台 `pip install pillow`。没有浏览器时跳过 PNG —— 打开 HTML 用右下角按钮下载即可。
    
    ## Usage
    
    ```bash
    SKILL_DIR="${SKILL_SKILLS_INSTALL_DIR:?Set SKILL_SKILLS_INSTALL_DIR}/lov-business-card"
    python3 "$SKILL_DIR/scripts/render_card.py" \
      --name "品牌方" --latin "Mark Shawn" \
      --brand "SKILL.AI" --index "STUDIO — Nº 2026" \
      --tags "背包客,超级开发者,AI / OPC 布道师" \
      --tagline "在 **Agent 时代**,|寻找**人**的意义" \
      --pursuits "旅行,羽毛球,计算机科学,心理学,哲学" \
      --bases "上海 陆家嘴数智港,北京 搜狐大厦清智孵化器" \
      --avatar ./me.png --theme dark-terracotta \
      --out ./output --format both
    ```
    
    ## Options
    
    | Option | Default | Description |
    |--------|---------|-------------|
    | `--name` | (required) | 姓名,支持中文 |
    | `--latin` | `""` | 英文名 / 罗马音(斜体小字) |
    | `--brand` | `""` | 左上品牌;含 `.` 时圆点会高亮 |
    | `--index` | `""` | 右上编号,如 `STUDIO — Nº 2026` |
    | `--tags` | `""` | 角色标签,逗号分隔 |
    | `--tagline` | `""` | 金句;`**词**` 高亮,`\|` 换行 |
    | `--pursuits` | `""` | 爱好,逗号分隔(底栏左) |
    | `--bases` | `""` | 据点/城市,逗号分隔(底栏右,带定位针) |
    | `--avatar` | `""` | 头像图片路径;省略则用 monogram |
    | `--caption` | `""` | 头像底部图注(可选) |
    | `--watermark` | 姓名末字 | 背景巨型水印字 |
    | `--theme` | `dark-terracotta` | `dark-terracotta` \| `midnight` \| `ivory` |
    | `--out` | `./output` | 输出目录 |
    | `--format` | `both` | `png` \| `html` \| `both` |
    | `--scale` | `3` | PNG 倍率(3 → 4800×2400) |
    
    ## Output
    
    - `{name}-名片.png` — 高清名片(默认 4800×2400)
    - `{name}-名片.html` + `modern-screenshot.umd.js` — 浏览器打开,点「下载为图片 PNG」自出 3× 高清图(两文件需同目录)
    
    ## 加主题
    
    往 `scripts/render_card.py` 的 `THEMES` 字典加一组 CSS 变量即可,模板无需改动。
    
    ## Star History
    
    [![Star History Chart](https://api.star-history.com/svg?repos=skill-publisher/business-card-skill&type=Date)](https://star-history.com/#skill-publisher/business-card-skill&Date)
    
    ## License
    
    MIT
    
  • SKILL.md 5.5 KB
    ---
    name: lov-business-card
    description: >
      Generate a professional, editorial-style business card (2:1) as a
      high-resolution PNG and a self-contained interactive HTML. Collects the user's
      identity (name, roles, tagline, interests, locations, optional avatar), fills a
      parameterized design template with a chosen theme, and renders it via headless
      Chrome. Trigger when the user wants to make a business card, personal card, name
      card, profile card, or "电子名片", or says "生成名片", "做一张名片", "个人名片",
      "make a business card", "design a name card".
    license: MIT
    compatibility: >
      Portable Agent Skills format. Requires Python 3.8+ and a Chromium-based browser
      (Google Chrome or Chromium) for PNG rendering; without one, the HTML still works
      in any browser via its built-in download button. macOS uses sips for cropping,
      other platforms need Pillow. All identity/brand/output values come from CLI
      flags — no personal paths are hard-coded.
    metadata:
      author: contributors
      version: "0.2.0"
      tags: business-card, design, branding, png, html
    ---
    
    # 名片工坊 · Business Card Studio
    
    把任意用户的身份信息,渲染成一张 2:1 的编辑式(editorial / luxury-minimal)名片:
    瑞士排版网格、发丝线分隔、单一强调色、宋体 × Didot 字体配对。输出高分辨率 PNG
    (默认 4800×2400)+ 一个自包含、可在浏览器点击下载的 HTML。
    
    ## 工作流
    
    ### 1. 先用 AskUserQuestion 收集信息(必做)
    
    **不要直接跑脚本。** 先确认主题与必填项。能从对话/记忆里推断的先预填,只问不知道的。
    
    建议问题(一次最多 3 个):
    
    - **主题**:dark-terracotta(暖深·陶土,默认)/ midnight(冷深·钢蓝)/ ivory(浅·象牙编辑风)
    - **是否有头像**:有 → 要文件路径;无 → 用名字首字渲染 monogram
    - **强调词**:tagline 里哪个词要高亮(强调色)
    
    身份字段尽量从上下文取;缺的逐项问或让用户一次性给。
    
    ### 2. 映射到 CLI 参数
    
    | 用户提供 | flag | 说明 |
    |---|---|---|
    | 姓名 | --name | 必填,支持中文 |
    | 英文名/罗马音 | --latin | 名字旁的斜体小字 |
    | 品牌(左上) | --brand | 如 SKILL.AI(含 . 会高亮圆点) |
    | 编号(右上) | --index | 如 STUDIO — Nº 2026 |
    | 角色标签 | --tags | 逗号分隔,菱形分隔渲染 |
    | 金句 | --tagline | **词** 高亮,竖线 \| 换行 |
    | 爱好/兴趣 | --pursuits | 逗号分隔(底栏左) |
    | 据点/城市 | --bases | 逗号分隔,每项带定位针(底栏右) |
    | 头像 | --avatar | 图片路径;省略则用 monogram |
    | 头像图注 | --caption | 可选,肖像底部小字 |
    | 主题 | --theme | dark-terracotta|midnight|ivory |
    | 输出目录 | --out | 默认 ./output |
    | 格式 | --format | png|html|both(默认 both) |
    
    ### 3. 运行
    
    ```bash
    python3 scripts/render_card.py \
      --name "品牌方" --latin "Mark Shawn" \
      --brand "SKILL.AI" --index "STUDIO — Nº 2026" \
      --tags "背包客,超级开发者,AI / OPC 布道师" \
      --tagline "在 **Agent 时代**,|寻找**人**的意义" \
      --pursuits "旅行,羽毛球,计算机科学,心理学,哲学" \
      --bases "上海 陆家嘴数智港,北京 搜狐大厦清智孵化器" \
      --avatar ./me.png --theme dark-terracotta \
      --out ./output --format both
    ```
    
    ### 4. 交付
    
    把 PNG 展示给用户;告诉他们 HTML 可在浏览器打开、右下角「下载为图片 PNG」自出 3× 高清图
    (HTML 与同目录的 modern-screenshot.umd.js 需一起保留)。
    
    ## 设计说明
    
    - 比例:固定 2:1(1600×800 画布),PNG 按 --scale 倍率导出(默认 3 → 4800×2400)。
    - 自适应:HTML 在浏览器里等比缩放铺满视口,绝不放大、不溢出;导出尺寸不受影响。
    - 无头像:自动用姓名首字渲染衬线 monogram,仍然专业。
    - 主题扩展:在 scripts/render_card.py 的 THEMES 字典加一组 CSS 变量即可。
    
    ## User Configuration
    
    本 skill 不假设任何私有工作区。所有身份、品牌、输出路径都由 CLI flag 传入;
    --out 默认当前目录下 ./output。如需复用默认身份,调用方可自行从用户 profile
    预填这些 flag,但脚本本身不读取任何固定个人路径。
    
    ## Runtime context (shared)
    
    运行前读取本 Skill 包的 `skill.yaml`,由宿主提供 `skill-runtime/v1` 上下文。字段解析顺序为:当前请求、项目上下文、个人 Preferences、品牌 Profile、通用默认值。
    
    - 只使用 Manifest 声明的字段;Profile 保存公开品牌事实,Preferences 保存个人工作偏好。
    - `required: true` 字段缺失时,按 Manifest 的问题配置向用户提出一个聚焦问题;用户明确同意后再保存回答。
    - 报错提供可复制的 `context_id`、字段路径与来源,诊断内容避开秘密、完整私人路径和原始配置。
    
    ## 通用反馈闭环
    
    用户在 Skill 驱动任务中提出修改意见时,继续当前产物前必须执行:
    
    1. 先判断意见是 `task-specific`(仅本次)还是 `reusable`(可跨任务复用)。
    2. `task-specific` 只修改当前任务,不改 Skill。
    3. `reusable` 先确定作用域:领域规则先更新对应 canonical Skill;适用于所有 Skill 的规则先更新共享规范。
    4. 完成规则更新、版本、lint 与分发核验后,再把修改应用到当前任务。
    5. `reusable` 修改会使此前的“确认”“继续”“发吧”失效;完成当前产物修改和回读后必须停下,等待用户下一步指示,不自动进入发布、提交或其他外部写入。
    
  • skill.yaml 840 B
    schema: skill-manifest/v1
    id: lov-business-card
    version: "0.2.0"
    runtime: skill-runtime/v1
    context:
      profile:
        fields:
        - path: identity.name
          required: true
          question: 如果本次输出需要品牌身份,请提供品牌名称。
        - path: identity.logo
          required: false
          question: 如果需要使用品牌 Logo,请提供 Logo 地址或文件路径。
        - path: brand.tone
          required: false
          question: 如果已有品牌语气或审美关键词,请提供它们。
      preferences:
        namespace: lov_business_card
        fields:
        - path: user.language
          required: false
          question: 希望使用哪种语言输出?
        - path: user.timezone
          required: false
          question: 需要使用哪个时区处理日期和时间?
      interaction:
        ask_missing: true
        max_questions: 1
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related