[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71KB510MAQAY9MNDM553NP5":3,"skill-same:01M71KB510MAQAY9MNDM553NP5":77},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":42,"linkOnly":43,"markdownContent":44,"markdownBody":45,"frontMatter":46,"refName":47,"commitSha":48,"skillDirectory":49,"forksCount":50,"repositoryHtmlUrl":51,"repositoryHomepageUrl":52,"ownerHtmlUrl":53,"ownerBlogUrl":54,"ownerDescription":55,"updatedAt":56,"install":57,"resources":70},"01M71KB510MAQAY9MNDM553NP5","resume-maker",null,"Build professional resumes with viewable HTML and downloadable PDF\u002FDOCX export.","clawdsolana\u002FOpenClawd","clawdsolana","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F279036008?v=4","01M71JTVF9FNGKE2SVJRPTQM31","OpenClawd","01M71JTVGCKWBSS8ZYC1HYWVA5",2,"MIT","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fsecondary_skills\u002Fresume-maker\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000038","人力资源","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41],"maker","resume","resume maker","docx","PDF","professional","with","build","export","html","2026-11-10 10:12:30",false,"---\nname: resume-maker\ndescription: Build professional resumes with viewable HTML and downloadable PDF\u002FDOCX export.\n---\n\n# Resume Maker\n\n## OpenClawd Operator Adaptation\n\nRun this skill as part of the OpenClawd operator deck. Preserve the skill-specific workflow below, but frame outputs for Solana-native agents when relevant: prefer OpenClawd language, note whether the work can support autonomous agent operations, and keep financial, legal, tax, hiring, medical, or other regulated outputs informational unless the skill already requires a stricter disclaimer. Use the Llobster Legend persona only as light operator framing; do not let branding override accuracy, safety, or the user's stated domain.\n\n\nBuild a resume as a React web page backed by a generation script that produces pixel-perfect PDF and DOCX files from a single data source. The web page reads spacing values from the same JSON so the HTML preview closely matches the generated documents.\n\n## Before You Start Building — Gather Information First\n\n**Do NOT start building the resume until you have enough information to write meaningful, specific content.** A resume with placeholder text or vague bullets is useless. You need real details to create something valuable.\n\nIf the user provides a complete resume or detailed background\n\nGo ahead and start building immediately. You have what you need.\n\nIf the user asks to \"make me a resume\" without providing details\n\nBefore asking them to type everything out manually, **offer two fast-start options** to save them time:\n\n\"To save you time, you can get started in one of two ways:\n\n- `**Upload your LinkedIn PDF**` — Go to your LinkedIn profile, click \"More\" → \"Save to PDF\", and upload that file here. I'll extract all your info automatically.\n- **Copy-paste from LinkedIn** — Go to your LinkedIn profile page, select all the text (Ctrl\u002FCmd+A), copy it (Ctrl\u002FCmd+C), and paste it here. I'll parse through it and pull out your experience, education, and skills.\n\nOr if you'd prefer, you can just tell me your details and we'll build it from scratch.\"\n\nIf the user uploads a LinkedIn PDF\n\nParse the PDF content to extract: name, headline, contact info, work experience (titles, companies, dates, descriptions), education, skills, certifications, and any other sections. LinkedIn PDFs follow a consistent format — use the structure to map content into the resume data model. After parsing, confirm what you extracted and ask if anything needs correction before building.\n\nIf the user pastes LinkedIn profile text\n\nParse the pasted text to extract the same fields. LinkedIn profile text typically follows this structure:\n\n- Name and headline at the top\n- \"Experience\" section with role titles, company names, date ranges, and descriptions\n\n- \"Education\" section with degrees, schools, and dates\n- \"Skills\" section with a list of skills\n\n- Optional sections like \"Certifications\", \"Publications\", \"Volunteer Experience\"\n\nMap each section into the resume data model. After parsing, confirm what you extracted and ask if anything needs correction.\n\nIf the user prefers to provide details manually\n\nAsk clarifying questions before writing any code. Ask about:\n\n- **Target role** — What job or type of role are they applying for? (This shapes the entire resume's framing)\n- **Work experience** — For each role: job title, company name, dates, location, and what they accomplished (not just responsibilities — ask for specific achievements, numbers, outcomes)\n\n- **Education** — Degree(s), school(s), graduation year(s), honors\u002FGPA if notable\n- **Skills** — Technical skills, tools, languages, frameworks, certifications\n\n- **Contact info** — Full name, email, phone, LinkedIn, location, portfolio\u002Fwebsite\n- **Optional extras** — Publications, talks, awards, volunteer work, projects\n\nHow to ask\n\nWhen offering the LinkedIn import options, lead with those first since they save the most time. Only fall back to manual questions if the user doesn't have LinkedIn or prefers to start fresh.\n\nIf going the manual route, don't dump all questions at once. Start with the most critical:\n\n\"No problem! Let's start with the basics:\n\n- What's your full name and contact info (email, phone, LinkedIn, location)?\n- What type of role are you targeting?\n\n- Walk me through your work history — for each job, give me the title, company, dates, and your biggest accomplishments with specific numbers if possible.\"\n\nThen follow up for education, skills, and anything else based on what they share. If they give vague bullets like \"managed a team,\" push back and ask for specifics: \"How large was the team? What did you deliver? Any measurable outcomes?\"\n\nEnhancing LinkedIn-imported content\n\nWhether imported from PDF or pasted text, LinkedIn data often needs improvement for a strong resume:\n\n- **Bullets are often weak** — LinkedIn descriptions tend to list responsibilities, not achievements. After importing, rewrite bullets to emphasize measurable outcomes and impact. Ask the user for specific numbers if the LinkedIn content is vague.\n- **Skills may be bloated** — LinkedIn lets users add dozens of skills. Curate the list down to the most relevant ones for the target role.\n\n- **Summary may be missing or generic** — LinkedIn summaries are often written in first person or as a pitch. Adapt it to resume conventions (third-person implied, concise, role-targeted).\n- **Dates and locations may need cleanup** — LinkedIn sometimes uses inconsistent formats. Normalize them.\n\nIf the resume feels thin\n\nIf the user doesn't have enough experience to fill a page, ask about additional material: volunteer work, side projects, open-source contributions, certifications, coursework, publications, talks, or awards. Proactively suggest these categories — don't just leave the page half-empty.\n\nIf the user provides a job posting\n\nAsk them for their background details, then tailor the resume to match the posting's keywords and requirements. Mirror the posting's language.\n\n## Build Order — Files First, Website Last\n\nFollow this exact order so the user gets fast, tangible results:\n\n- **Write the generation script** (generate-resume-files.ts) with all resume content and run it to produce the PDF, DOCX, and resume-data.json\n- **Present the PDF to the user in chat** — show the generated PDF file immediately so they can see their resume right away\n\n- **Present the DOCX file to the user in chat** — make the DOCX available for download so they can grab it\n- **Build the web app last** — set up the React artifact and API routes that serve the resume page and download endpoints\n\nThe user cares most about seeing their resume quickly. The PDF and DOCX are the primary deliverables — get those into the user's hands first. The web app is a nice-to-have that comes after. Do NOT spend time building the web app before the user has seen their PDF and downloaded their DOCX.\n\n## Architecture\n\nscripts\u002Fsrc\u002Fgenerate-resume-files.ts `← single source of truth for content + layout\n\n└─ output\u002F\n\n`├─ resume-data.json ← consumed by the web frontend\n\n`├─ \u003Cname>-resume.pdf ← downloadable PDF\n\n`└─ \u003Cname>-resume.docx ← downloadable DOCX\n\nartifacts\u002F\u003Cname>\u002Fsrc\u002Fpages\u002Fresume.tsx `← React page, reads resume-data.json via API\n\nartifacts\u002Fapi-server\u002Fsrc\u002Froutes\u002Fdownload.ts ← serves PDF, DOCX, and JSON\n\nData flow\n\n- generate-resume-files.ts holds all resume content in a getResumeData() function.\n- The script renders the PDF with jsPDF, measures content height, and auto-adjusts spacing to fill exactly one US Letter page (612×792pt, 36pt margins).\n\n- It writes the computed spacing values into resume-data.json alongside the content.\n- The React page fetches resume-data.json at runtime and uses the spacing values (converted pt→px via 96\u002F72) to render a matching HTML preview.\n\n## Content Rules\n\n- **Summary\u002Fblurb**: If you include a personal summary at the top, keep it to 2 sentences max. Long plaintext paragraphs waste prime real estate and recruiters skip them.\n- **One page hard limit**: The resume MUST fit on exactly one US Letter page (612×792pt). The auto-fit algorithm handles this, but be mindful when writing content — more bullets doesn't mean better.\n\n## Flag Guesses and Inferred Details\n\nIf you had to guess or infer any details — dates, job titles, specific contributions, metrics, technologies — you MUST tell the user what you made up. After presenting the first draft, explicitly list anything you weren't sure about and ask if they want to correct it. For example:\n\n\"I made a few assumptions I want to flag:\n\n- I estimated your dates at Company X as 2021–2023 — are those right?\n- I wrote that you 'reduced API latency by 40%' based on your mention of performance work — is that accurate, or should I adjust the number\u002Fframing?\n\n- I guessed TypeScript and Python for your skills — anything to add or remove?\"\n\nDo NOT silently present fabricated details as fact. The user trusts you to be honest about what you know vs. what you inferred.\n\n## Iteration and Changes\n\nWhen the user requests changes (rewording bullets, adding\u002Fremoving sections, reordering, etc.):\n\n- Make the requested changes in generate-resume-files.ts\n- **Re-run the generation script** to produce updated PDF, DOCX, and JSON\n\n- **Verify the output still fits on one page** — if the changes pushed content past the page boundary, the auto-fit algorithm should handle it, but visually confirm. If content is getting clipped or the scale factor is too aggressive (text becoming unreadably small), trim lower-priority bullets or reduce spacing before delivering.\n- **Re-present the updated PDF and DOCX** to the user in chat\n\nNever deliver an updated resume without re-running the generation script. The PDF\u002FDOCX must always reflect the latest content. Every iteration cycle ends with the user seeing a fresh PDF and having a fresh DOCX to download.\n\n## Critical Unit Rules\n\nDOCX (docx npm package)\n\nThe docx package uses two different unit systems — mixing them up causes 10× sizing bugs:\n\n- **TextRun.size**`= **half-points**:`ptToHalfPt = pt * 2 (e.g., 11pt → 22)\n- **Spacing, margins, page dimensions**=**twips**: ptToTwip = pt \\* 20 (e.g., 36pt → 720)\n\nAlways define separate converter functions:\n\nconst ptToHalfPt = (pt: number) => Math.round(pt \\* 2);\n\nconst ptToTwip = (pt: number) => Math.round(pt \\* 20);\n\njsPDF baseline rule\n\ndoc.text(text, x, y) draws text with y as the **baseline** — the text body extends upward from y. When placing a horizontal rule after text:\n\n- **Wrong**: y += 2 after drawing a rule — text overlaps the line\n- **Right**: y += lineHeight after drawing a rule — full clearance before next text\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\ny += lineHeight; `\u002F\u002F NOT y += 4`\n\nWeb CSS alignment\n\nWhen using borderBottom as a section divider:\n\n- Put the border directly on the element (e.g., h2) rather than a separate \u003Cdiv>\n- Use paddingBottom to separate text from the line\n\n- Use marginBottom with at least lineHeight to separate the line from content below\n\n## One-Page Auto-Fit Algorithm\n\nThe generation script measures content height, then adjusts spacing to fill the target:\n\n`1.` Render with BASE_SPACING → measure finalY\n\n1. If finalY > TARGET_Y: shorten bullets until it fits\n2. If finalY \u003C TARGET_Y: distribute slack across sectionGap, roleGap, bulletGap, lineHeight\n\n`4.` Re-render with adjusted spacing → write final files\n\nSlack distribution weights: sectionGap: 3, roleGap: 2, bulletGap: 1, lineHeight: 0.5\n\n## Skeleton: Generation Script\n\nimport { jsPDF } from \"jspdf\";\n\nimport`{`\n\nDocument, Packer, Paragraph, TextRun, BorderStyle,\n\nTabStopPosition, TabStopType, AlignmentType,\n\n`}`from` `\"docx\"`;`\n\nimport`fs`from` `\"fs\"`;`\n\nimport`path`from` `\"path\"`;`\n\n\u002F\u002F --- Interfaces ---\n\ninterface` `ResumeRole`{`\n\n` `title`:`string`;`\n\n` `company`:`string`;`\n\n` `location`:`string`;`\n\n` `startDate`:`string`;`\n\n` `endDate`:`string`;`\n\n` `bullets`:`string`[];`\n\n}\n\ninterface` `Spacing`{`\n\n` `sectionGap`:`number`;`\n\n` `roleGap`:`number`;`\n\n` `bulletGap`:`number`;`\n\n` `lineHeight`:`number`;`\n\n` `bodyFontSize`:`number`;`\n\n` `headlineFontSize`:`number`;`\n\n}\n\ninterface` `ResumeData`{`\n\n` `name`:`string`;`\n\n` `headline`:`string`;`\n\n` `contact`: {`email`:`string`;`phone`:`string`;`location`:`string`;`linkedin`:`string`;`website`:`string`};`\n\n` `summary`:`string`;`\n\n` `roles`:`ResumeRole`[];`\n\n` `skills`: {`category`:`string`;`items`:`string`}[];`\n\n` `education`: {`degree`:`string`;`school`:`string`;`location`:`string`;`dates`:`string`}[];`\n\n` `spacing`:`Spacing`;`\n\n}\n\n\u002F\u002F --- Constants ---\n\nconst`PAGE_W`=` `612`;`\u002F\u002F US Letter width in points\n\nconst`PAGE_H`=` `792`;`\u002F\u002F US Letter height in points\n\nconst`MARGIN`=` `36`;`\u002F\u002F 0.5 inch margins\n\nconst CONTENT*W = PAGE*W - 2 \\* MARGIN;\n\nconst TARGET*Y = PAGE*H - MARGIN;\n\nconst BASE_SPACING: Spacing = {\n\n` `sectionGap`:`8`,`\n\n` `roleGap`:`4`,`\n\n` `bulletGap`:`1`,`\n\n` `lineHeight`:`14`,`\n\n` `bodyFontSize`:`11`,`\n\n` `headlineFontSize`:`11`,`\n\n};\n\n\u002F\u002F --- Unit converters (DOCX) ---\n\nconst ptToHalfPt = (pt: number) => Math.round(pt \\* 2); `\u002F\u002F TextRun.size`\n\nconst ptToTwip = (pt: number) => Math.round(pt \\* 20); `\u002F\u002F spacing\u002Fmargins`\n\n\u002F\u002F --- Resume data ---\n\nfunction` `getResumeData`():`Omit`\u003C`ResumeData`,`\"spacing\"`> {`\n\n` `return`{`\n\n` `name`:`\"FULL NAME\"`,`\n\n` `headline`:`\"Title | Specialty\"`,`\n\n` `contact`: {`\n\n` `email`:`\"\u003Cemail@example.com>\"`,`\n\nphone: \"\",\n\n` `location`:`\"City, State\"`,`\n\n` `linkedin`:`\"linkedin.com\u002Fin\u002Fhandle\"`,`\n\n` `website`:`\"example.com\"`,`\n\n},\n\n` `summary`:`\"Professional summary paragraph.\"`,`\n\nroles: [\n\n{\n\n` `title`:`\"Job Title\"`,`\n\ncompany: \"Company\",\n\n` `location`:`\"City, State\"`,`\n\nstartDate: \"MM\u002FYYYY\",\n\nendDate: \"Present\",\n\n` `bullets`: [`\"Achievement or responsibility\"`],`\n\n},\n\n],\n\nskills: [\n\n` `{ category`:`\"Category\"`,`items`:`\"Skill1, Skill2, Skill3\"`},`\n\n],\n\n` `education`: [`\n\n{\n\ndegree: \"Degree\",\n\n` `school`:`\"University\"`,`\n\n` `location`:`\"City, Country\"`,`\n\n` `dates`:`\"YYYY - YYYY\"`,`\n\n},\n\n],\n\n};\n\n}\n\n\u002F\u002F --- PDF rendering ---\n\nfunction` `renderPDF`(doc:`jsPDF`, data:`Omit`\u003C`ResumeData`,`\"spacing\"`>, spacing:`Spacing`):`number`{`\n\nlet y = MARGIN;\n\n` `const`{`bodyFontSize`,`headlineFontSize`,`lineHeight`,`sectionGap`,`roleGap`,`bulletGap`}`=`spacing;`\n\n` `\u002F\u002F Header\n\n` `doc.setFont`(`\"helvetica\"`,`\"bold\"`);`\n\n` `doc.setFontSize`(`20`);`\n\ndoc.text(data.name, PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += 16;\n\n` `doc.setFont`(`\"helvetica\"`,`\"normal\"`);`\n\ndoc.setFontSize(headlineFontSize);\n\ndoc.text(data.headline, PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += lineHeight;\n\n` `\u002F\u002F Contact line\n\n` `const`contactParts`=`[data`.contact.location`, data`.contact.email`, data`.contact.linkedin`, data`.contact.website`]`.filter`(Boolean);`\n\n` `doc.setFontSize`(`9`);`\n\ndoc.text(contactParts.join(\" | \"), PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += lineHeight + 2;\n\n` `\u002F\u002F Divider — use lineHeight gap after rule, NOT a small constant\n\n` `doc.setDrawColor`(`0`);`\n\n` `doc.setLineWidth`(`0.5`);`\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\ny += lineHeight;\n\n` `\u002F\u002F Summary\n\n` `doc.setFont`(`\"helvetica\"`,`\"normal\"`);`\n\ndoc.setFontSize(bodyFontSize);\n\nconst summaryLines = doc.splitTextToSize(data.summary, CONTENT_W) as string[];\n\nfor (const line of summaryLines) {\n\ndoc.text(line, MARGIN, y);\n\ny += lineHeight;\n\n}\n\n` `\u002F\u002F Section header helper\n\n` `function` `renderSectionHeader`(title:`string`) {`\n\ny += sectionGap;\n\n` `doc.setFont`(`\"helvetica\"`,`\"bold\"`);`\n\n` `doc.setFontSize`(`11`);`\n\ndoc.text(title.toUpperCase(), MARGIN, y);\n\ny += 3;\n\n` `doc.setLineWidth`(`0.3`);`\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\n` `y +=`lineHeight;`\u002F\u002F full lineHeight after rule\n\n}\n\n` `\u002F\u002F Experience, Skills, Education sections...\n\n` `\u002F\u002F (render each section using the spacing values)\n\n` `return`y;`\n\n}\n\n\u002F\u002F --- DOCX rendering ---\n\nfunction` `buildDocx`(data:`Omit`\u003C`ResumeData`,`\"spacing\"`>, spacing:`Spacing`):`Document`{`\n\n` `const`{`bodyFontSize`,`headlineFontSize`,`lineHeight`,`sectionGap`,`roleGap`,`bulletGap`}`=`spacing;`\n\n` `\u002F\u002F Use ptToHalfPt() for ALL TextRun.size values\n\n` `\u002F\u002F Use ptToTwip() for ALL spacing.before, spacing.after, margins, page dimensions\n\nconst doc = new Document({\n\n` `sections`: [{`\n\n` `properties`: {`\n\npage: {\n\nsize: { width: ptToTwip(PAGE*W), height: ptToTwip(PAGE*H) },\n\nmargin: {\n\ntop: ptToTwip(MARGIN),\n\nbottom: ptToTwip(MARGIN),\n\nleft: ptToTwip(MARGIN),\n\nright: ptToTwip(MARGIN),\n\n},\n\n},\n\n},\n\nchildren: [\n\n` `\u002F\u002F Build paragraphs here using the data\n\n],\n\n}],\n\n});\n\nreturn doc;\n\n}\n\n\u002F\u002F --- Auto-fit and generate ---\n\nasync` `function` `main`() {`\n\n` `const`data`=` `getResumeData`();`\n\nconst outputDir = path.resolve(import.meta.dirname, \"..\", \"output\");\n\nif (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n` `\u002F\u002F Measure with base spacing\n\nlet spacing = { ...BASE_SPACING };\n\nlet doc = new jsPDF({ unit: \"pt\", format: \"letter\" });\n\nlet finalY = renderPDF(doc, data, spacing);\n\n` `\u002F\u002F Auto-fit: distribute slack\n\nconst slack = TARGET_Y - finalY;\n\nif (slack > 0) {\n\n` `const`weights`=`{`sectionGap`:`3`,`roleGap`:`2`,`bulletGap`:`1`,`lineHeight`:`0.5`};`\n\nconst totalWeight = Object.values(weights).reduce((a, b) => a + b, 0);\n\n` `\u002F\u002F Count occurrences of each spacing type to distribute evenly\n\n` `\u002F\u002F Adjust spacing values proportionally\n\n}\n\n` `\u002F\u002F Final render\n\ndoc = new jsPDF({ unit: \"pt\", format: \"letter\" });\n\nfinalY = renderPDF(doc, data, spacing);\n\n` `\u002F\u002F Write files\n\nconst pdfBuffer = Buffer.from(doc.output(\"arraybuffer\"));\n\nfs.writeFileSync(path.join(outputDir, \"person-name-resume.pdf\"), pdfBuffer);\n\nconst docxDoc = buildDocx(data, spacing);\n\nconst docxBuffer = await Packer.toBuffer(docxDoc);\n\nfs.writeFileSync(path.join(outputDir, \"person-name-resume.docx\"), docxBuffer);\n\nconst jsonData: ResumeData = { ...data, publications: [], spacing };\n\nfs.writeFileSync(path.join(outputDir, \"resume-data.json\"), JSON.stringify(jsonData, null, 2));\n\n` `console.log`(`\"All files generated successfully!\"`);`\n\n}\n\nmain().catch(console.error);\n\n## Skeleton: React Resume Page\n\nimport { useState, useEffect } from \"react\";\n\nconst PT*TO*PX = 96 \u002F 72;\n\ninterface` `ResumeData`{`\u002F*same interfaces as generation script*\u002F`}`\n\nfunction` `SectionHeader`({`title`,`lineHeightPx`}: {`title`:`string`;`lineHeightPx`:`number`}) {`\n\n` `return`(`\n\n`\u003C`h2 style={{\n\nfontSize: \"11pt\",\n\n` `fontWeight`:`700`,`\n\n` `textTransform`:`\"uppercase\"`,`\n\n` `letterSpacing`:`\"0.5px\"`,`\n\nmargin: 0,\n\npadding: 0,\n\n` `paddingBottom`:`\"4px\"`,`\n\nmarginBottom: `${lineHeightPx}px`,\n\n` `borderBottom`:`\"1px solid \\#1a1a2e\"`,`\n\n` `color`:`\"#1a1a2e\"`,`\n\n}}`>`\n\n{title}\n\n` `\u003C\u002Fh2>\n\n);\n\n}\n\nexport` `default` `function` `ResumePage`() {`\n\nconst [data, setData] = useState\u003CResumeData | null>(null);\n\nuseEffect(() => {\n\n` `const`basePath`=` `import.meta.env.BASE_URL`;`\n\n` `fetch`(```${basePath}`api\u002Fresume-data``)`\n\n.then((r) => r.json())\n\n.then(setData);\n\n}, []);\n\nif (!data) return `\u003C`p`>`Loading...`\u003C\u002F`p`>`;\n\nconst sp = data.spacing;\n\nconst sectionGapPx = sp.sectionGap \\* PT*TO*PX;\n\nconst roleGapPx = sp.roleGap \\* PT*TO*PX;\n\nconst bulletGapPx = sp.bulletGap \\* PT*TO*PX;\n\nconst lineHeightPx = sp.lineHeight \\* PT*TO*PX;\n\nconst bodyFontPx = sp.bodyFontSize \\* PT*TO*PX;\n\n` `return`(`\n\n` ``\u003C`div` `style=`{{`background`:`\"#f0f0f0\"`,`minHeight`:`\"100vh\"`}}``>`\n\n`\u003C`div style={{\n\n` `width`:`\"816px\"`,`\u002F\u002F 8.5in at 96dpi\n\n` `minHeight`:`\"1056px\"`,`\u002F\u002F 11in at 96dpi\n\nmargin: \"0 auto\",\n\nbackground: \"#fff\",\n\n` `padding`:`\"48px\"`,`\u002F\u002F 0.5in margins at 96dpi\n\n` `fontFamily`:`\"'Calibri', 'Arial', sans-serif\"`,`\n\nfontSize: `${bodyFontPx}px`,\n\nlineHeight: `${lineHeightPx}px`,\n\n}}`>`\n\n` `{\u002F*Header, summary, experience, skills, education*\u002F`}`\n\n` `{\u002F*Use SectionHeader with lineHeightPx for each section*\u002F`}`\n\n` `{\u002F*Use spacing values from sp for all gaps*\u002F`}`\n\n`\u003C\u002F`div`>`\n\n` `\u003C\u002Fdiv>\n\n);\n\n}\n\n## Skeleton: API Routes\n\nimport { Router } from \"express\";\n\nimport`path`from` `\"path\"`;`\n\nimport`fs`from` `\"fs\"`;`\n\nconst router = Router();\n\nconst outputDir = path.resolve(import.meta.dirname, \"..\", \"..\", \"..\", \"..\", \"output\");\n\nrouter.get(\"\u002Fdownload\u002Fpdf\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"person-name-resume.pdf\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"PDF not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\n` `res.setHeader`(`\"Content-Disposition\"`,`\"attachment; filename=person-name-resume.pdf\"`);`\n\n` `res.setHeader`(`\"Content-Type\"`,`\"application\u002Fpdf\"`);`\n\nres.sendFile(filePath);\n\n});\n\nrouter.get(\"\u002Fdownload\u002Fdocx\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"person-name-resume.docx\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"DOCX not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\n` `res.setHeader`(`\"Content-Disposition\"`,`\"attachment; filename=person-name-resume.docx\"`);`\n\n` `res.setHeader`(`\"Content-Type\"`,`\"application\u002Fvnd.openxmlformats-officedocument.wordprocessingml.document\"`);`\n\nres.sendFile(filePath);\n\n});\n\nrouter.get(\"\u002Fresume-data\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"resume-data.json\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"Resume data not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\nres.json(JSON.parse(fs.readFileSync(filePath, \"utf-8\")));\n\n});\n\nexport` `default`router;`\n\n## Common Pitfalls\n\n| Problem | Cause | Fix |\n\n|---|---|---|\n\n| DOCX text is 10× too large | Used ptToTwip (×20) for TextRun.size | Use ptToHalfPt (×2) for font sizes |\n\n| PDF text overlaps horizontal rule | Used y += 4 after doc.line() | Use y += lineHeight after any rule |\n\n| Web section header line cuts through text | Border on a sibling `\u003Cdiv>`instead of the`\u003Ch2>`| Put borderBottom on the`\u003Ch2>` with paddingBottom |\n\n| Downloads fail in Replit preview iframe | Proxy blocks file downloads in iframe | Use target=\"_blank\" on download links, or present files directly |\n\n| Web spacing doesn't match PDF | Hardcoded px values in CSS | Convert all pt values via PT_TO_PX = 96\u002F72 |\n\n## Dependencies\n\n- jspdf — PDF generation\n- docx — DOCX generation\n\n- tsx — TypeScript script runner (dev dependency)\n","# Resume Maker\n\n## OpenClawd Operator Adaptation\n\nRun this skill as part of the OpenClawd operator deck. Preserve the skill-specific workflow below, but frame outputs for Solana-native agents when relevant: prefer OpenClawd language, note whether the work can support autonomous agent operations, and keep financial, legal, tax, hiring, medical, or other regulated outputs informational unless the skill already requires a stricter disclaimer. Use the Llobster Legend persona only as light operator framing; do not let branding override accuracy, safety, or the user's stated domain.\n\n\nBuild a resume as a React web page backed by a generation script that produces pixel-perfect PDF and DOCX files from a single data source. The web page reads spacing values from the same JSON so the HTML preview closely matches the generated documents.\n\n## Before You Start Building — Gather Information First\n\n**Do NOT start building the resume until you have enough information to write meaningful, specific content.** A resume with placeholder text or vague bullets is useless. You need real details to create something valuable.\n\nIf the user provides a complete resume or detailed background\n\nGo ahead and start building immediately. You have what you need.\n\nIf the user asks to \"make me a resume\" without providing details\n\nBefore asking them to type everything out manually, **offer two fast-start options** to save them time:\n\n\"To save you time, you can get started in one of two ways:\n\n- `**Upload your LinkedIn PDF**` — Go to your LinkedIn profile, click \"More\" → \"Save to PDF\", and upload that file here. I'll extract all your info automatically.\n- **Copy-paste from LinkedIn** — Go to your LinkedIn profile page, select all the text (Ctrl\u002FCmd+A), copy it (Ctrl\u002FCmd+C), and paste it here. I'll parse through it and pull out your experience, education, and skills.\n\nOr if you'd prefer, you can just tell me your details and we'll build it from scratch.\"\n\nIf the user uploads a LinkedIn PDF\n\nParse the PDF content to extract: name, headline, contact info, work experience (titles, companies, dates, descriptions), education, skills, certifications, and any other sections. LinkedIn PDFs follow a consistent format — use the structure to map content into the resume data model. After parsing, confirm what you extracted and ask if anything needs correction before building.\n\nIf the user pastes LinkedIn profile text\n\nParse the pasted text to extract the same fields. LinkedIn profile text typically follows this structure:\n\n- Name and headline at the top\n- \"Experience\" section with role titles, company names, date ranges, and descriptions\n\n- \"Education\" section with degrees, schools, and dates\n- \"Skills\" section with a list of skills\n\n- Optional sections like \"Certifications\", \"Publications\", \"Volunteer Experience\"\n\nMap each section into the resume data model. After parsing, confirm what you extracted and ask if anything needs correction.\n\nIf the user prefers to provide details manually\n\nAsk clarifying questions before writing any code. Ask about:\n\n- **Target role** — What job or type of role are they applying for? (This shapes the entire resume's framing)\n- **Work experience** — For each role: job title, company name, dates, location, and what they accomplished (not just responsibilities — ask for specific achievements, numbers, outcomes)\n\n- **Education** — Degree(s), school(s), graduation year(s), honors\u002FGPA if notable\n- **Skills** — Technical skills, tools, languages, frameworks, certifications\n\n- **Contact info** — Full name, email, phone, LinkedIn, location, portfolio\u002Fwebsite\n- **Optional extras** — Publications, talks, awards, volunteer work, projects\n\nHow to ask\n\nWhen offering the LinkedIn import options, lead with those first since they save the most time. Only fall back to manual questions if the user doesn't have LinkedIn or prefers to start fresh.\n\nIf going the manual route, don't dump all questions at once. Start with the most critical:\n\n\"No problem! Let's start with the basics:\n\n- What's your full name and contact info (email, phone, LinkedIn, location)?\n- What type of role are you targeting?\n\n- Walk me through your work history — for each job, give me the title, company, dates, and your biggest accomplishments with specific numbers if possible.\"\n\nThen follow up for education, skills, and anything else based on what they share. If they give vague bullets like \"managed a team,\" push back and ask for specifics: \"How large was the team? What did you deliver? Any measurable outcomes?\"\n\nEnhancing LinkedIn-imported content\n\nWhether imported from PDF or pasted text, LinkedIn data often needs improvement for a strong resume:\n\n- **Bullets are often weak** — LinkedIn descriptions tend to list responsibilities, not achievements. After importing, rewrite bullets to emphasize measurable outcomes and impact. Ask the user for specific numbers if the LinkedIn content is vague.\n- **Skills may be bloated** — LinkedIn lets users add dozens of skills. Curate the list down to the most relevant ones for the target role.\n\n- **Summary may be missing or generic** — LinkedIn summaries are often written in first person or as a pitch. Adapt it to resume conventions (third-person implied, concise, role-targeted).\n- **Dates and locations may need cleanup** — LinkedIn sometimes uses inconsistent formats. Normalize them.\n\nIf the resume feels thin\n\nIf the user doesn't have enough experience to fill a page, ask about additional material: volunteer work, side projects, open-source contributions, certifications, coursework, publications, talks, or awards. Proactively suggest these categories — don't just leave the page half-empty.\n\nIf the user provides a job posting\n\nAsk them for their background details, then tailor the resume to match the posting's keywords and requirements. Mirror the posting's language.\n\n## Build Order — Files First, Website Last\n\nFollow this exact order so the user gets fast, tangible results:\n\n- **Write the generation script** (generate-resume-files.ts) with all resume content and run it to produce the PDF, DOCX, and resume-data.json\n- **Present the PDF to the user in chat** — show the generated PDF file immediately so they can see their resume right away\n\n- **Present the DOCX file to the user in chat** — make the DOCX available for download so they can grab it\n- **Build the web app last** — set up the React artifact and API routes that serve the resume page and download endpoints\n\nThe user cares most about seeing their resume quickly. The PDF and DOCX are the primary deliverables — get those into the user's hands first. The web app is a nice-to-have that comes after. Do NOT spend time building the web app before the user has seen their PDF and downloaded their DOCX.\n\n## Architecture\n\nscripts\u002Fsrc\u002Fgenerate-resume-files.ts `← single source of truth for content + layout\n\n└─ output\u002F\n\n`├─ resume-data.json ← consumed by the web frontend\n\n`├─ \u003Cname>-resume.pdf ← downloadable PDF\n\n`└─ \u003Cname>-resume.docx ← downloadable DOCX\n\nartifacts\u002F\u003Cname>\u002Fsrc\u002Fpages\u002Fresume.tsx `← React page, reads resume-data.json via API\n\nartifacts\u002Fapi-server\u002Fsrc\u002Froutes\u002Fdownload.ts ← serves PDF, DOCX, and JSON\n\nData flow\n\n- generate-resume-files.ts holds all resume content in a getResumeData() function.\n- The script renders the PDF with jsPDF, measures content height, and auto-adjusts spacing to fill exactly one US Letter page (612×792pt, 36pt margins).\n\n- It writes the computed spacing values into resume-data.json alongside the content.\n- The React page fetches resume-data.json at runtime and uses the spacing values (converted pt→px via 96\u002F72) to render a matching HTML preview.\n\n## Content Rules\n\n- **Summary\u002Fblurb**: If you include a personal summary at the top, keep it to 2 sentences max. Long plaintext paragraphs waste prime real estate and recruiters skip them.\n- **One page hard limit**: The resume MUST fit on exactly one US Letter page (612×792pt). The auto-fit algorithm handles this, but be mindful when writing content — more bullets doesn't mean better.\n\n## Flag Guesses and Inferred Details\n\nIf you had to guess or infer any details — dates, job titles, specific contributions, metrics, technologies — you MUST tell the user what you made up. After presenting the first draft, explicitly list anything you weren't sure about and ask if they want to correct it. For example:\n\n\"I made a few assumptions I want to flag:\n\n- I estimated your dates at Company X as 2021–2023 — are those right?\n- I wrote that you 'reduced API latency by 40%' based on your mention of performance work — is that accurate, or should I adjust the number\u002Fframing?\n\n- I guessed TypeScript and Python for your skills — anything to add or remove?\"\n\nDo NOT silently present fabricated details as fact. The user trusts you to be honest about what you know vs. what you inferred.\n\n## Iteration and Changes\n\nWhen the user requests changes (rewording bullets, adding\u002Fremoving sections, reordering, etc.):\n\n- Make the requested changes in generate-resume-files.ts\n- **Re-run the generation script** to produce updated PDF, DOCX, and JSON\n\n- **Verify the output still fits on one page** — if the changes pushed content past the page boundary, the auto-fit algorithm should handle it, but visually confirm. If content is getting clipped or the scale factor is too aggressive (text becoming unreadably small), trim lower-priority bullets or reduce spacing before delivering.\n- **Re-present the updated PDF and DOCX** to the user in chat\n\nNever deliver an updated resume without re-running the generation script. The PDF\u002FDOCX must always reflect the latest content. Every iteration cycle ends with the user seeing a fresh PDF and having a fresh DOCX to download.\n\n## Critical Unit Rules\n\nDOCX (docx npm package)\n\nThe docx package uses two different unit systems — mixing them up causes 10× sizing bugs:\n\n- **TextRun.size**`= **half-points**:`ptToHalfPt = pt * 2 (e.g., 11pt → 22)\n- **Spacing, margins, page dimensions**=**twips**: ptToTwip = pt \\* 20 (e.g., 36pt → 720)\n\nAlways define separate converter functions:\n\nconst ptToHalfPt = (pt: number) => Math.round(pt \\* 2);\n\nconst ptToTwip = (pt: number) => Math.round(pt \\* 20);\n\njsPDF baseline rule\n\ndoc.text(text, x, y) draws text with y as the **baseline** — the text body extends upward from y. When placing a horizontal rule after text:\n\n- **Wrong**: y += 2 after drawing a rule — text overlaps the line\n- **Right**: y += lineHeight after drawing a rule — full clearance before next text\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\ny += lineHeight; `\u002F\u002F NOT y += 4`\n\nWeb CSS alignment\n\nWhen using borderBottom as a section divider:\n\n- Put the border directly on the element (e.g., h2) rather than a separate \u003Cdiv>\n- Use paddingBottom to separate text from the line\n\n- Use marginBottom with at least lineHeight to separate the line from content below\n\n## One-Page Auto-Fit Algorithm\n\nThe generation script measures content height, then adjusts spacing to fill the target:\n\n`1.` Render with BASE_SPACING → measure finalY\n\n1. If finalY > TARGET_Y: shorten bullets until it fits\n2. If finalY \u003C TARGET_Y: distribute slack across sectionGap, roleGap, bulletGap, lineHeight\n\n`4.` Re-render with adjusted spacing → write final files\n\nSlack distribution weights: sectionGap: 3, roleGap: 2, bulletGap: 1, lineHeight: 0.5\n\n## Skeleton: Generation Script\n\nimport { jsPDF } from \"jspdf\";\n\nimport`{`\n\nDocument, Packer, Paragraph, TextRun, BorderStyle,\n\nTabStopPosition, TabStopType, AlignmentType,\n\n`}`from` `\"docx\"`;`\n\nimport`fs`from` `\"fs\"`;`\n\nimport`path`from` `\"path\"`;`\n\n\u002F\u002F --- Interfaces ---\n\ninterface` `ResumeRole`{`\n\n` `title`:`string`;`\n\n` `company`:`string`;`\n\n` `location`:`string`;`\n\n` `startDate`:`string`;`\n\n` `endDate`:`string`;`\n\n` `bullets`:`string`[];`\n\n}\n\ninterface` `Spacing`{`\n\n` `sectionGap`:`number`;`\n\n` `roleGap`:`number`;`\n\n` `bulletGap`:`number`;`\n\n` `lineHeight`:`number`;`\n\n` `bodyFontSize`:`number`;`\n\n` `headlineFontSize`:`number`;`\n\n}\n\ninterface` `ResumeData`{`\n\n` `name`:`string`;`\n\n` `headline`:`string`;`\n\n` `contact`: {`email`:`string`;`phone`:`string`;`location`:`string`;`linkedin`:`string`;`website`:`string`};`\n\n` `summary`:`string`;`\n\n` `roles`:`ResumeRole`[];`\n\n` `skills`: {`category`:`string`;`items`:`string`}[];`\n\n` `education`: {`degree`:`string`;`school`:`string`;`location`:`string`;`dates`:`string`}[];`\n\n` `spacing`:`Spacing`;`\n\n}\n\n\u002F\u002F --- Constants ---\n\nconst`PAGE_W`=` `612`;`\u002F\u002F US Letter width in points\n\nconst`PAGE_H`=` `792`;`\u002F\u002F US Letter height in points\n\nconst`MARGIN`=` `36`;`\u002F\u002F 0.5 inch margins\n\nconst CONTENT*W = PAGE*W - 2 \\* MARGIN;\n\nconst TARGET*Y = PAGE*H - MARGIN;\n\nconst BASE_SPACING: Spacing = {\n\n` `sectionGap`:`8`,`\n\n` `roleGap`:`4`,`\n\n` `bulletGap`:`1`,`\n\n` `lineHeight`:`14`,`\n\n` `bodyFontSize`:`11`,`\n\n` `headlineFontSize`:`11`,`\n\n};\n\n\u002F\u002F --- Unit converters (DOCX) ---\n\nconst ptToHalfPt = (pt: number) => Math.round(pt \\* 2); `\u002F\u002F TextRun.size`\n\nconst ptToTwip = (pt: number) => Math.round(pt \\* 20); `\u002F\u002F spacing\u002Fmargins`\n\n\u002F\u002F --- Resume data ---\n\nfunction` `getResumeData`():`Omit`\u003C`ResumeData`,`\"spacing\"`> {`\n\n` `return`{`\n\n` `name`:`\"FULL NAME\"`,`\n\n` `headline`:`\"Title | Specialty\"`,`\n\n` `contact`: {`\n\n` `email`:`\"\u003Cemail@example.com>\"`,`\n\nphone: \"\",\n\n` `location`:`\"City, State\"`,`\n\n` `linkedin`:`\"linkedin.com\u002Fin\u002Fhandle\"`,`\n\n` `website`:`\"example.com\"`,`\n\n},\n\n` `summary`:`\"Professional summary paragraph.\"`,`\n\nroles: [\n\n{\n\n` `title`:`\"Job Title\"`,`\n\ncompany: \"Company\",\n\n` `location`:`\"City, State\"`,`\n\nstartDate: \"MM\u002FYYYY\",\n\nendDate: \"Present\",\n\n` `bullets`: [`\"Achievement or responsibility\"`],`\n\n},\n\n],\n\nskills: [\n\n` `{ category`:`\"Category\"`,`items`:`\"Skill1, Skill2, Skill3\"`},`\n\n],\n\n` `education`: [`\n\n{\n\ndegree: \"Degree\",\n\n` `school`:`\"University\"`,`\n\n` `location`:`\"City, Country\"`,`\n\n` `dates`:`\"YYYY - YYYY\"`,`\n\n},\n\n],\n\n};\n\n}\n\n\u002F\u002F --- PDF rendering ---\n\nfunction` `renderPDF`(doc:`jsPDF`, data:`Omit`\u003C`ResumeData`,`\"spacing\"`>, spacing:`Spacing`):`number`{`\n\nlet y = MARGIN;\n\n` `const`{`bodyFontSize`,`headlineFontSize`,`lineHeight`,`sectionGap`,`roleGap`,`bulletGap`}`=`spacing;`\n\n` `\u002F\u002F Header\n\n` `doc.setFont`(`\"helvetica\"`,`\"bold\"`);`\n\n` `doc.setFontSize`(`20`);`\n\ndoc.text(data.name, PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += 16;\n\n` `doc.setFont`(`\"helvetica\"`,`\"normal\"`);`\n\ndoc.setFontSize(headlineFontSize);\n\ndoc.text(data.headline, PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += lineHeight;\n\n` `\u002F\u002F Contact line\n\n` `const`contactParts`=`[data`.contact.location`, data`.contact.email`, data`.contact.linkedin`, data`.contact.website`]`.filter`(Boolean);`\n\n` `doc.setFontSize`(`9`);`\n\ndoc.text(contactParts.join(\" | \"), PAGE_W \u002F 2, y, { align: \"center\" });\n\ny += lineHeight + 2;\n\n` `\u002F\u002F Divider — use lineHeight gap after rule, NOT a small constant\n\n` `doc.setDrawColor`(`0`);`\n\n` `doc.setLineWidth`(`0.5`);`\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\ny += lineHeight;\n\n` `\u002F\u002F Summary\n\n` `doc.setFont`(`\"helvetica\"`,`\"normal\"`);`\n\ndoc.setFontSize(bodyFontSize);\n\nconst summaryLines = doc.splitTextToSize(data.summary, CONTENT_W) as string[];\n\nfor (const line of summaryLines) {\n\ndoc.text(line, MARGIN, y);\n\ny += lineHeight;\n\n}\n\n` `\u002F\u002F Section header helper\n\n` `function` `renderSectionHeader`(title:`string`) {`\n\ny += sectionGap;\n\n` `doc.setFont`(`\"helvetica\"`,`\"bold\"`);`\n\n` `doc.setFontSize`(`11`);`\n\ndoc.text(title.toUpperCase(), MARGIN, y);\n\ny += 3;\n\n` `doc.setLineWidth`(`0.3`);`\n\ndoc.line(MARGIN, y, PAGE_W - MARGIN, y);\n\n` `y +=`lineHeight;`\u002F\u002F full lineHeight after rule\n\n}\n\n` `\u002F\u002F Experience, Skills, Education sections...\n\n` `\u002F\u002F (render each section using the spacing values)\n\n` `return`y;`\n\n}\n\n\u002F\u002F --- DOCX rendering ---\n\nfunction` `buildDocx`(data:`Omit`\u003C`ResumeData`,`\"spacing\"`>, spacing:`Spacing`):`Document`{`\n\n` `const`{`bodyFontSize`,`headlineFontSize`,`lineHeight`,`sectionGap`,`roleGap`,`bulletGap`}`=`spacing;`\n\n` `\u002F\u002F Use ptToHalfPt() for ALL TextRun.size values\n\n` `\u002F\u002F Use ptToTwip() for ALL spacing.before, spacing.after, margins, page dimensions\n\nconst doc = new Document({\n\n` `sections`: [{`\n\n` `properties`: {`\n\npage: {\n\nsize: { width: ptToTwip(PAGE*W), height: ptToTwip(PAGE*H) },\n\nmargin: {\n\ntop: ptToTwip(MARGIN),\n\nbottom: ptToTwip(MARGIN),\n\nleft: ptToTwip(MARGIN),\n\nright: ptToTwip(MARGIN),\n\n},\n\n},\n\n},\n\nchildren: [\n\n` `\u002F\u002F Build paragraphs here using the data\n\n],\n\n}],\n\n});\n\nreturn doc;\n\n}\n\n\u002F\u002F --- Auto-fit and generate ---\n\nasync` `function` `main`() {`\n\n` `const`data`=` `getResumeData`();`\n\nconst outputDir = path.resolve(import.meta.dirname, \"..\", \"output\");\n\nif (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });\n\n` `\u002F\u002F Measure with base spacing\n\nlet spacing = { ...BASE_SPACING };\n\nlet doc = new jsPDF({ unit: \"pt\", format: \"letter\" });\n\nlet finalY = renderPDF(doc, data, spacing);\n\n` `\u002F\u002F Auto-fit: distribute slack\n\nconst slack = TARGET_Y - finalY;\n\nif (slack > 0) {\n\n` `const`weights`=`{`sectionGap`:`3`,`roleGap`:`2`,`bulletGap`:`1`,`lineHeight`:`0.5`};`\n\nconst totalWeight = Object.values(weights).reduce((a, b) => a + b, 0);\n\n` `\u002F\u002F Count occurrences of each spacing type to distribute evenly\n\n` `\u002F\u002F Adjust spacing values proportionally\n\n}\n\n` `\u002F\u002F Final render\n\ndoc = new jsPDF({ unit: \"pt\", format: \"letter\" });\n\nfinalY = renderPDF(doc, data, spacing);\n\n` `\u002F\u002F Write files\n\nconst pdfBuffer = Buffer.from(doc.output(\"arraybuffer\"));\n\nfs.writeFileSync(path.join(outputDir, \"person-name-resume.pdf\"), pdfBuffer);\n\nconst docxDoc = buildDocx(data, spacing);\n\nconst docxBuffer = await Packer.toBuffer(docxDoc);\n\nfs.writeFileSync(path.join(outputDir, \"person-name-resume.docx\"), docxBuffer);\n\nconst jsonData: ResumeData = { ...data, publications: [], spacing };\n\nfs.writeFileSync(path.join(outputDir, \"resume-data.json\"), JSON.stringify(jsonData, null, 2));\n\n` `console.log`(`\"All files generated successfully!\"`);`\n\n}\n\nmain().catch(console.error);\n\n## Skeleton: React Resume Page\n\nimport { useState, useEffect } from \"react\";\n\nconst PT*TO*PX = 96 \u002F 72;\n\ninterface` `ResumeData`{`\u002F*same interfaces as generation script*\u002F`}`\n\nfunction` `SectionHeader`({`title`,`lineHeightPx`}: {`title`:`string`;`lineHeightPx`:`number`}) {`\n\n` `return`(`\n\n`\u003C`h2 style={{\n\nfontSize: \"11pt\",\n\n` `fontWeight`:`700`,`\n\n` `textTransform`:`\"uppercase\"`,`\n\n` `letterSpacing`:`\"0.5px\"`,`\n\nmargin: 0,\n\npadding: 0,\n\n` `paddingBottom`:`\"4px\"`,`\n\nmarginBottom: `${lineHeightPx}px`,\n\n` `borderBottom`:`\"1px solid \\#1a1a2e\"`,`\n\n` `color`:`\"#1a1a2e\"`,`\n\n}}`>`\n\n{title}\n\n` `\u003C\u002Fh2>\n\n);\n\n}\n\nexport` `default` `function` `ResumePage`() {`\n\nconst [data, setData] = useState\u003CResumeData | null>(null);\n\nuseEffect(() => {\n\n` `const`basePath`=` `import.meta.env.BASE_URL`;`\n\n` `fetch`(```${basePath}`api\u002Fresume-data``)`\n\n.then((r) => r.json())\n\n.then(setData);\n\n}, []);\n\nif (!data) return `\u003C`p`>`Loading...`\u003C\u002F`p`>`;\n\nconst sp = data.spacing;\n\nconst sectionGapPx = sp.sectionGap \\* PT*TO*PX;\n\nconst roleGapPx = sp.roleGap \\* PT*TO*PX;\n\nconst bulletGapPx = sp.bulletGap \\* PT*TO*PX;\n\nconst lineHeightPx = sp.lineHeight \\* PT*TO*PX;\n\nconst bodyFontPx = sp.bodyFontSize \\* PT*TO*PX;\n\n` `return`(`\n\n` ``\u003C`div` `style=`{{`background`:`\"#f0f0f0\"`,`minHeight`:`\"100vh\"`}}``>`\n\n`\u003C`div style={{\n\n` `width`:`\"816px\"`,`\u002F\u002F 8.5in at 96dpi\n\n` `minHeight`:`\"1056px\"`,`\u002F\u002F 11in at 96dpi\n\nmargin: \"0 auto\",\n\nbackground: \"#fff\",\n\n` `padding`:`\"48px\"`,`\u002F\u002F 0.5in margins at 96dpi\n\n` `fontFamily`:`\"'Calibri', 'Arial', sans-serif\"`,`\n\nfontSize: `${bodyFontPx}px`,\n\nlineHeight: `${lineHeightPx}px`,\n\n}}`>`\n\n` `{\u002F*Header, summary, experience, skills, education*\u002F`}`\n\n` `{\u002F*Use SectionHeader with lineHeightPx for each section*\u002F`}`\n\n` `{\u002F*Use spacing values from sp for all gaps*\u002F`}`\n\n`\u003C\u002F`div`>`\n\n` `\u003C\u002Fdiv>\n\n);\n\n}\n\n## Skeleton: API Routes\n\nimport { Router } from \"express\";\n\nimport`path`from` `\"path\"`;`\n\nimport`fs`from` `\"fs\"`;`\n\nconst router = Router();\n\nconst outputDir = path.resolve(import.meta.dirname, \"..\", \"..\", \"..\", \"..\", \"output\");\n\nrouter.get(\"\u002Fdownload\u002Fpdf\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"person-name-resume.pdf\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"PDF not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\n` `res.setHeader`(`\"Content-Disposition\"`,`\"attachment; filename=person-name-resume.pdf\"`);`\n\n` `res.setHeader`(`\"Content-Type\"`,`\"application\u002Fpdf\"`);`\n\nres.sendFile(filePath);\n\n});\n\nrouter.get(\"\u002Fdownload\u002Fdocx\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"person-name-resume.docx\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"DOCX not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\n` `res.setHeader`(`\"Content-Disposition\"`,`\"attachment; filename=person-name-resume.docx\"`);`\n\n` `res.setHeader`(`\"Content-Type\"`,`\"application\u002Fvnd.openxmlformats-officedocument.wordprocessingml.document\"`);`\n\nres.sendFile(filePath);\n\n});\n\nrouter.get(\"\u002Fresume-data\", (_req, res) => {\n\nconst filePath = path.join(outputDir, \"resume-data.json\");\n\nif (!fs.existsSync(filePath)) {\n\n` `res.status`(`404`)`.json`({`error`:`\"Resume data not found. Run the generation script first.\"`});`\n\n` `return`;`\n\n}\n\nres.json(JSON.parse(fs.readFileSync(filePath, \"utf-8\")));\n\n});\n\nexport` `default`router;`\n\n## Common Pitfalls\n\n| Problem | Cause | Fix |\n\n|---|---|---|\n\n| DOCX text is 10× too large | Used ptToTwip (×20) for TextRun.size | Use ptToHalfPt (×2) for font sizes |\n\n| PDF text overlaps horizontal rule | Used y += 4 after doc.line() | Use y += lineHeight after any rule |\n\n| Web section header line cuts through text | Border on a sibling `\u003Cdiv>`instead of the`\u003Ch2>`| Put borderBottom on the`\u003Ch2>` with paddingBottom |\n\n| Downloads fail in Replit preview iframe | Proxy blocks file downloads in iframe | Use target=\"_blank\" on download links, or present files directly |\n\n| Web spacing doesn't match PDF | Hardcoded px values in CSS | Convert all pt values via PT_TO_PX = 96\u002F72 |\n\n## Dependencies\n\n- jspdf — PDF generation\n- docx — DOCX generation\n\n- tsx — TypeScript script runner (dev dependency)\n",{"name":5,"description":7},"main","6b4a291df75c639681e93b4b5b812126443fd6ed","secondary_skills\u002Fresume-maker",3,"https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd","https:\u002F\u002Fsolanaclawd.com\u002Fopenclawd","https:\u002F\u002Fgithub.com\u002Fclawdsolana","https:\u002F\u002Fmusebook.trade","Welcome to Open Clawd. The future of Ai on and off chain. Bringing webMCP to web 3 using X402, A2A, MPP and so much more!","2026-08-01 09:30:51",{"source":8,"skillName":5,"commands":58,"prompt":68,"zipAvailable":69},[59,62,65],{"tool":60,"command":61},"npx","npx skills add clawdsolana\u002FOpenClawd --skill resume-maker",{"tool":63,"command":64},"bunx","bunx skills add clawdsolana\u002FOpenClawd --skill resume-maker",{"tool":66,"command":67},"pnpm","pnpm dlx skills add clawdsolana\u002FOpenClawd --skill resume-maker","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fsecondary_skills\u002Fresume-maker\u002FSKILL.md",true,[71],{"path":72,"resourceType":73,"resourceTypeDesc":74,"sizeBytes":75,"sourceHtmlUrl":76},".fingerprint","OTHER","其他",32,"https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fsecondary_skills\u002Fresume-maker\u002F.fingerprint",[78,109,137,163,173,197],{"skillNo":79,"name":80,"nameZh":6,"description":81,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":82,"categories":83,"keywords":87,"publishedAt":108},"01M71KRBD0W01EAXJ8YM4M1SJ2","solana-dev","End-to-end Solana development playbook (Jan 2026). Prefer Solana Foundation framework-kit (@solana\u002Fclient + @solana\u002Freact-hooks) for React\u002FNext.js UI. Prefer @solana\u002Fkit for all new client\u002FRPC\u002Ftransaction code. When legacy dependencies require web3.js, isolate it behind @solana\u002Fweb3-compat (or @solana\u002Fweb3.js as a true legacy fallback). Covers wallet-standard-first connection (incl. ConnectorKit), Anchor\u002FPinocchio programs, Codama-based client generation, LiteSVM\u002FMollusk\u002FSurfpool testing, and security checklists.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fsrc\u002Fskills\u002Fsolana-dev\u002FSKILL.md",[84],{"categoryNo":85,"categoryName":86,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量",[88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107],"solana dev","dev","solana","Wallet","end","testing","hooks","kit","new","when","web3","code","react","next","2026","playbook","security","ui","fallback","framework","2026-11-10 10:56:23",{"skillNo":110,"name":111,"nameZh":6,"description":112,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":113,"categories":114,"keywords":118,"publishedAt":108},"01M71KR8QPESSY61SVE8ZTFNVT","xurl","A CLI tool for making authenticated requests to the X (Twitter) API. Use this skill when you need to post tweets, reply, quote, search, read posts, manage followers, send DMs, upload media, or interact with any X API v2 endpoint.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fxurl\u002FSKILL.md",[115],{"categoryNo":116,"categoryName":117,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[111,119,120,121,122,123,124,97,125,126,127,128,129,130,131,132,133,134,135,136],"posts","Media","x api","search","send","tap","you","api","cli","read","install","cli tool","endpoint","brew","for","manage","tool","twitter",{"skillNo":138,"name":139,"nameZh":6,"description":140,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":141,"categories":142,"keywords":144,"publishedAt":108},"01M71KR6FSXV88YDKWP7NBY3SH","wurk-integration","WURK API integration for monetizing agents with x402 payments on Solana\u002FBase - create social jobs, custom challenges, and agent help tasks","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fwurk-integration\u002FSKILL.md",[143],{"categoryNo":116,"categoryName":117,"taxonomyType":21,"taxonomyTypeDesc":22},[145,146,147,148,149,150,151,152,126,153,154,155,156,157,158,159,133,160,161,162],"wurk","wurk integration","integration","x402","social","payments","tasks","team","create","social-jobs","command","agent-help","custom-jobs","clawd","safe","api integration","custom","true",{"skillNo":164,"name":165,"nameZh":6,"description":166,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":167,"categories":168,"keywords":169,"publishedAt":108},"01M71KR4HCX41BPAK9GP7RPRRP","workflows","Manage application workflows including configuration, restart, and removal.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fworkflows\u002FSKILL.md",[],[165,134,170,171,172],"configuration","application","restart",{"skillNo":174,"name":175,"nameZh":6,"description":176,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":177,"categories":178,"keywords":179,"publishedAt":108},"01M71KR1VM0F5GPEQDNG5J7A0H","web-search","Search the web, fetch content, extract branding profiles, and capture screenshots from URLs. Use for real-time information, API documentation, current events, design matching, and visual reference.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fweb-search\u002FSKILL.md",[],[180,122,181,182,183,133,184,185,186,187,188,189,190,126,191,192,193,194,195,196],"web search","web","fetch","api documentation","real-time","time","profiles","design","use","information","visual","documentation","branding","matching","reference","capture","content",{"skillNo":198,"name":199,"nameZh":6,"description":200,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":201,"categories":202,"keywords":203,"publishedAt":108},"01M71KQZN5MR1JFQHG5GWXYAHS","weather","Get current weather and forecasts via wttr.in or Open-Meteo. Use when: user asks about weather, temperature, or forecasts for any location. NOT for: historical weather data, severe weather alerts, or detailed meteorological analysis. No API key needed.","https:\u002F\u002Fgithub.com\u002Fclawdsolana\u002FOpenClawd\u002Fblob\u002Fmain\u002Fskills\u002Fweather\u002FSKILL.md",[],[199,204,133,205,188,97,206,126,207,208,209,210,211,212,213,214,215,216],"get","open","analysis","alerts","temperature","curl","via","help","about","https","location","data","user"]