📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-06-07 16:02:11 +00:00
parent 772a1da63c
commit ac39ab52f3
219 changed files with 5905 additions and 833 deletions
@@ -10,6 +10,11 @@ date_added: "2026-06-05"
author: 2slides
tags: [presentations, slides, powerpoint, ai, api-integration, pdf, narration, document-summarization]
tools: [claude, cursor, gemini, codex, antigravity]
plugin:
setup:
type: manual
summary: "Install Python requirements and configure a 2slides API key before running generation scripts."
docs: SKILL.md
---
# 2slides Presentation Generation
@@ -269,6 +274,12 @@ Section 2: [Subtopic]
Use the `create_pdf_slides.py` script:
Install the Python dependency first if it is not already available:
```bash
python -m pip install -r requirements.txt
```
```bash
# Basic generation
python scripts/create_pdf_slides.py --content "Your content here"
@@ -294,7 +305,6 @@ python scripts/create_pdf_slides.py \
--resolution 1K|2K|4K # default: 2K
--page N # 0=auto, 1-100 (default: 1)
--content-detail concise|standard # default: standard
--mode sync|async # default: async
```
**Step 3: Handle Results**
@@ -706,7 +716,7 @@ All scripts accept parameters that match [2slides API](https://2slides.com/api.m
| | `--aspect-ratio` | 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9 |
| | `--resolution` | 1K, 2K, 4K |
| | `--content-detail` | concise, standard |
| `create_pdf_slides.py` | Same as above + `--design-style` (free text), `--mode` (default async) | |
| `create_pdf_slides.py` | Same as above + `--design-style` / `--design-spec` (free text) | |
| `generate_narration.py` | `--mode` | single, multi |
| | `--voice` | 30 voices (Puck, Aoede, Charon, …); use `--list-voices` |
| | `--content-mode` | concise, standard |
@@ -0,0 +1 @@
requests>=2.31.0
@@ -126,7 +126,7 @@ Examples:
)
parser.add_argument("--content", required=True, help="Content for slides")
parser.add_argument("--design-spec", help="Optional design specifications")
parser.add_argument("--design-spec", "--design-style", dest="design_spec", help="Optional design specifications")
parser.add_argument("--language", default="Auto", help="Response language (default: Auto)")
parser.add_argument("--aspect-ratio", default="16:9", help="Aspect ratio in width:height format (default: 16:9)")
parser.add_argument("--resolution", choices=["1K", "2K", "4K"], default="2K",
@@ -36,7 +36,10 @@ Branch switching triggers a rebuild but not a browser reload — the CLI opens a
```bash
git diff --quiet && git diff --cached --quiet || git stash push -u -m "accesslint-diff-branch"
git checkout <branch>
branch="<branch>"
git check-ref-format --branch "$branch" >/dev/null
case "$branch" in -*) echo "Refusing option-like branch name: $branch" >&2; exit 1 ;; esac
git checkout -- "$branch"
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --update-snapshot [--wait-for "<selector>"]
git checkout - && git stash pop 2>/dev/null
npx -y @accesslint/cli@latest "<url>" --port "$PORT" --snapshot accesslint-diff --snapshot-dir /tmp --format json [--wait-for "<selector>"]
@@ -0,0 +1,159 @@
---
name: article-illustrations
description: "Generate hand-drawn 16:9 article illustrations with the Grav character IP, sparse annotations, and absurd but clear visual metaphors."
category: creative
risk: safe
source: community
source_repo: vipin-si/article-illustrations
source_type: community
license: MIT
license_source: https://github.com/vipin-si/article-illustrations/blob/main/LICENSE
date_added: "2026-06-06"
author: vipin-si
tags: [illustration, article-graphics, visual-metaphors, image-generation, whiteboard-sketch]
tools: [image-generation]
---
# Article Illustrations — Grav Hand-Drawn Style
## Overview
Generate 16:9 landscape hand-drawn illustrations for articles, blog posts, and technical content. Each illustration captures one cognitive anchor point from an article and turns it into a clean, absurd, memorable whiteboard-sketch explanation.
The skill uses a recurring character IP called **Grav**: a small, round, always-floating figure with dot eyes and a thin antenna. Grav participates in the core action of every illustration — never just decoration.
**Repository:** [vipin-si/article-illustrations](https://github.com/vipin-si/article-illustrations)
## When to Use This Skill
- Use when writing articles, blog posts, or documentation that need inline illustrations
- Use when you want to turn abstract concepts into concrete visual metaphors
- Use when you want a consistent visual language across multiple articles
- Use when you need hand-drawn explanation sketches, not PPT infographics
## How It Works
### Step 1: Digest the Article
Read the article and identify cognitive anchor points — core judgments, turning points, input/output loops, before/after contrasts, and common pitfalls. Don't distribute illustrations evenly; prioritize moments that benefit from visual explanation.
### Step 2: Plan a Shot List
For each illustration, define:
- **Placement**: After which section
- **Theme**: What this image is about
- **Core Meaning**: The one idea it conveys
- **Structure Type**: One of 8 composition patterns (Workflow, System Closeup, Before/After, Role States, Conceptual Metaphor, Layered Method, Map Route, Mini Comic)
- **Grav's Action**: What Grav is doing in the scene
- **Annotation Labels**: 35 short English labels
### Step 3: Generate Images
Use the `generate_image` tool with the built-in prompt template. Each image follows strict style rules:
- Pure white background, no textures
- Black hand-drawn line art with slight wobble
- Sparse red/orange/blue handwritten annotations
- Grav always floating (never touching surfaces)
- One core idea per image
- 4060% canvas usage, 35%+ whitespace
### Step 4: QA Check
Verify each image against the QA checklist: correct format, Grav present and active, original metaphor, clean composition, sparse annotations, correct color usage.
## Examples
### Example 1: Plan illustrations for an article
```
Analyze this article and create a shot list of 5 illustrations.
Don't generate images yet — just plan which cognitive anchor points
deserve illustrations and what each image should convey.
<paste article>
```
### Example 2: Generate illustrations directly
```
Generate 4 Grav-style illustrations for this article.
Requirements: 16:9 landscape, pure white background, black hand-drawn
line art, sparse red/orange/blue English annotations.
<paste article>
```
### Example 3: Single concept illustration
```
Generate one 16:9 illustration for this concept:
"Trust isn't declared — it's built one piece of evidence at a time."
Grav must perform the core action. Maximum 5 annotation labels.
```
### Example 4: Iterate on a result
```
This illustration is on the right track, but Grav feels like decoration.
Keep the core meaning but regenerate: make Grav the one actually
driving the structure.
```
## Visual Style
| Element | Rule |
|:--------|:-----|
| Background | Pure white — no cream, texture, gradients, or shadows |
| Line art | Black, hand-drawn, slightly wobbly, not mechanical |
| Whitespace | Main subject 4060% of canvas, 35%+ empty space |
| Annotations | Handwritten English, 25 words each, max 58 per image |
| Color: Black | Main line art, characters, structures, objects |
| Color: Red | Key highlights, problems, warnings, results |
| Color: Orange | Main flow, paths, arrows, direction |
| Color: Blue | Supplementary notes, feedback, system state |
| Prohibited | Green, purple, yellow, pink, gradients, drop shadows, 3D, realistic UI |
## Character: Grav
- Small round body (pebble/potato shape)
- Two dot eyes (slightly asymmetric)
- One thin bent antenna with tiny circle tip
- Thin stick legs that dangle without touching surfaces
- Always hovering — visible gap between Grav and any surface
- Expression: calm, focused, deadpan
- Role: active participant in the system, never decoration
## Best Practices
- ✅ Start with a shot list before generating images
- ✅ Invent a new metaphor for every illustration — never reuse compositions
- ✅ Make Grav the action protagonist, not a bystander
- ✅ Keep it absurd but structurally clear
- ✅ Use color sparingly — when in doubt, use black
- ❌ Don't make PPT infographics or formal flowcharts
- ❌ Don't add title bars or decorative frames
- ❌ Don't let Grav touch the ground or stand on surfaces
- ❌ Don't make Grav cute, smiling, or emoji-like
## Limitations
- Requires access to an image-generation tool that can follow composition, line-art, and annotation constraints.
- The recurring Grav character style can drift between generations; verify every output against the QA checklist.
- Text in generated images may be misspelled or distorted, so short labels and post-generation review are required.
- The style is intended for explanatory article illustrations, not photorealistic product imagery or brand-final artwork.
## Common Pitfalls
- **Problem:** Illustration looks like a PPT slide
**Solution:** Remove 30% of elements, increase whitespace, make it weirder
- **Problem:** Grav is just standing next to the action
**Solution:** Redesign so Grav IS the mechanism — becomes the funnel, dangles from the lever, is suspended inside the machine
- **Problem:** Same metaphor as a previous illustration
**Solution:** Replace the physical object entirely — same concept, different analogy
## Additional Resources
- [Full skill with prompt templates and QA checklist](https://github.com/vipin-si/article-illustrations)
- [Example illustrations](https://github.com/vipin-si/article-illustrations#examples)
@@ -0,0 +1,874 @@
---
name: cv-generator
description: "Generate professional, ATS-optimized CVs for FlowCV, Canva, Google Docs, or Word. Handles multi-source merging, JD targeting, seniority adaptation, and humanized rewriting. Outputs paste-ready text with an ATS flaw report and improvement suggestions."
category: content
risk: safe
source: community
date_added: "2026-06-06"
author: "WHOISABHISHEKADHIKARI"
user-invokable: true
tags:
- cv
- resume
- ats
- career
- job-application
- career-change
---
# CV Generator Skill — FlowCV / Canva Edition
## When to Use
Use this skill when you need to:
- Generate a professional, ATS-optimized CV from multiple sources (LinkedIn, GitHub, Portfolio).
- Tailor an existing CV for a specific Job Description (JD).
- Improve the language, metrics, and structure of a draft resume.
- Prepare a paste-ready version of your CV for tools like FlowCV or Canva.
Turns raw profile data into a polished, ATS-ready CV. Outputs a paste-ready plain-text
version formatted for FlowCV, Canva, Google Docs, or Word — with a flaw report and
missing-info checklist.
---
## FLAW REGISTER — KNOWN ISSUES FIXED IN THIS VERSION
The following issues were identified across the two prior skill drafts and are corrected here:
| # | Flaw | Fix applied |
|---|------|-------------|
| F-01 | Output was Markdown-first, not paste-ready plain text | Final output is plain text; Markdown is internal staging only |
| F-02 | FlowCV/Canva field structure was never addressed | Section mapping to tool fields added (section 11c) |
| F-03 | Questionnaire dumped all 20 questions at once in practice | Hard rule: one question at a time, wait for answer |
| F-04 | Anti-hallucination rules listed but never enforced structurally | Enforcement gate added before every output (section 10) |
| F-05 | Cover letter was offered but never scoped for these tools | Cover letter now outputs to a separate plain-text block, not inline |
| F-06 | ATS check listed but had no scored output | Flaw report now scores 0100 with per-item pass/fail |
| F-07 | Seniority detection was "detect or ask" with no fallback | Default is mid-level if undetectable; user is told the assumption |
| F-08 | No guidance on what FlowCV/Canva cannot render | Added explicit field-by-field paste map (section 11c) |
| F-09 | Tense rules stated but never verified in quality gate | Tense check is now a hard gate — output blocked until corrected |
| F-10 | "Passionate about" and similar banned phrases still appeared in examples | Phrase blocklist now machine-checkable (section 7c) |
| F-11 | Nepal/South Asia market conventions were present but incomplete | Confirmed and expanded (section 14) |
| F-12 | No explicit rule on what to do when LinkedIn scraping is blocked | Hard fallback rule: ask for PDF export immediately, do not proceed empty |
| F-13 | File naming convention mentioned once, never enforced | File name rule is part of the final output block (section 11) |
| F-14 | Skill had no version history or upgrade path | Version field added to frontmatter |
| F-15 | GitHub was listed as a source but extraction rules were missing | GitHub extraction rules added (section 4f) |
---
## 1. Invocation
```
Use @cv-generator to build my CV from my LinkedIn PDF.
Use @cv-generator to tailor my CV for this job description.
Use @cv-generator to improve my existing draft.
Use @cv-generator to create a fresh CV via questionnaire.
Use @cv-generator — I want a FlowCV-ready output.
```
Any combination of sources is valid. Multiple sources are merged and deduplicated
before writing begins.
---
## Source Selection
Ask the user which source(s) to use. At least one is required.
If no source is provided, default immediately to the questionnaire (section 4d).
| # | Source | Instruction |
|---|--------|-------------|
| 1 | LinkedIn profile URL | Fetch page; extract all visible sections. **If blocked or empty: immediately ask for a LinkedIn PDF — do not proceed on an empty extraction.** |
| 2 | LinkedIn PDF export | Parse uploaded file. If scanned image: apply OCR and warn the user to verify accuracy. |
| 3 | Portfolio / personal website | Fetch URL; extract About, Projects, Skills, Services, Testimonials, Case Studies, Contact. |
| 4 | Questionnaire | Step-by-step (section 4d). One question at a time. |
| 5 | Existing CV or draft | Upload or paste; improve only — never alter facts. |
| 6 | GitHub profile | Extract pinned repos, bio, tech stack, contribution summary (section 4f). |
| 7 | Resume file (DOCX / PDF / TXT) | Parse and rewrite. Flag scanned PDFs; apply OCR. |
---
## Purpose, seniority, and format
### Purpose
Ask after source selection:
> "What is the main purpose of this CV?"
| Purpose | Key adaptation |
|---------|----------------|
| Applying for a specific job | Full JD analysis + keyword targeting (section 9) |
| General professional CV | Balanced, role-agnostic, reverse-chronological |
| Internship / entry-level | Education and projects lead; transferable skills foregrounded |
| Academic / research | Publications, grants, teaching, research interests |
| Freelance / client proposal | Deliverables, outcomes, services |
| Career change | Functional or hybrid; transferable skills reframed |
| Executive / board-level | Executive summary, board positions, P&L scope |
| Military-to-civilian | Translate ranks and jargon to civilian equivalents |
| Return to work / career break | Frame gap positively; emphasise upskilling |
| Other | Ask the user to describe the goal in one sentence |
### Seniority
Detect from data. If undetectable, **default to mid-level and tell the user:**
> "I've assumed mid-level (38 years). Let me know if this should be different."
| Level | Years | CV emphasis |
|-------|-------|-------------|
| Student / fresh graduate | 01 | Education first; projects; extracurriculars; 1 page |
| Junior / entry | 13 | Skills + education prominent; 1 page |
| Mid-level | 38 | Experience leads; achievements over duties; 12 pages |
| Senior | 815 | Leadership, scope, impact, mentoring; 2 pages |
| Executive / C-suite | 15+ | Strategic narrative; board roles; P&L; 23 pages |
| Academic | Any | No page limit; publications; grants; teaching |
### Format
| Format | Use when |
|--------|----------|
| Chronological (default) | Clear career progression; most job applications |
| Functional / skills-first | Career changers; large gaps; military-to-civilian |
| Hybrid / combination | Senior professionals rebranding; career changers with strong experience |
| Academic CV | University, research, PhDs, postdocs |
| Executive / Board bio | C-suite, NED, advisory |
| Portfolio-led | Designers, architects, creatives |
---
## Data extraction rules
### LinkedIn URL
If the page is blocked or returns no content, **stop immediately** and ask:
> "LinkedIn blocked the fetch. Please export your LinkedIn profile as a PDF
> (LinkedIn → Me → Settings → Data Privacy → Get a copy of your data) and upload it."
If accessible, extract in order:
1. Full name and headline
2. Contact information (email, phone, location — public only)
3. About / Professional Summary
4. Work experience: title, company, location, dates, bullets
5. Education: degree, institution, dates, grade/honours
6. Skills (flag top endorsed skills)
7. Certifications and licences
8. Projects
9. Achievements, honours, awards
10. Volunteer experience
11. Languages and proficiency
12. Publications, patents, courses
### LinkedIn PDF
Hard rules:
- Extract only what is physically present in the document.
- Preserve all dates exactly as written.
- If a section is absent, mark it **[Not provided]** — do not skip silently.
- Do not merge bullets across different roles.
- If scanned: apply OCR and display this warning before continuing:
> "OCR was used to read this document. Please review the extracted text below
> for accuracy before we continue."
### Portfolio / personal website
Extract:
- About / bio → Professional Summary
- Projects: name, description, technologies, outcomes, live/repo URLs
- Skills and services
- Testimonials or client logos → Achievements
- Case studies → 24 bullets each
- Blog posts or articles → Publications / Thought Leadership
- Contact details
### Questionnaire
**One question at a time. Wait for the answer before continuing.**
Do not display the full list unless the user explicitly asks for a form.
```
Q1. Full legal name (as it should appear on the CV)
Q2. Target job title or role
Q3. Email address
Q4. Phone number including country code (optional but recommended)
Q5. City and country of residence
Q6. LinkedIn URL (optional)
Q7. Portfolio, GitHub, or personal website URL (optional)
Q8. Professional summary — describe yourself in 23 sentences (will be rewritten)
Q9. Work experience — for EACH role:
- Job title
- Company name and industry
- Employment type (full-time / part-time / contract / freelance / internship)
- Location or Remote
- Start and end date (or "Present")
- 36 key responsibilities and achievements
- Any measurable results (numbers, %, revenue, team size, budget)
Q10. Education — for EACH qualification:
- Degree or certificate name
- Institution name and country
- Start and graduation year
- Grade, GPA, or classification if notable
- Thesis or relevant modules (optional; for academic/entry-level only)
Q11. Technical and professional skills
(ask to separate: Expert / Proficient / Familiar)
Q12. Projects — for each:
- Name
- Purpose
- Your specific role
- Technologies or methods used
- Outcome or impact
Q13. Certifications (name, issuing body, date, expiry if applicable)
Q14. Achievements, awards, or recognitions
Q15. Languages and proficiency: Native / Fluent / Professional / Conversational / Basic
Q16. Volunteer or open-source work (optional)
Q17. Publications, speaking engagements, press mentions (optional)
Q18. Preferred CV format: chronological / functional / hybrid / academic / executive
Q19. Target country or job market
Q20. Any employment gaps? Dates and brief reason — will be framed constructively.
```
### Existing CV or draft
Rules:
- Preserve every fact: titles, companies, dates, institutions, grades.
- Rewrite weak or passive bullets with strong action verbs.
- Remove repetition across roles.
- Correct grammar, punctuation, spelling.
- Fix tense: past for completed roles, present for current role.
- Replace all banned phrases (section 7c).
- Improve ATS keyword density where natural — do not keyword-stuff.
- Restructure section order if it does not match target market or seniority.
- **Do not add experience, qualifications, metrics, or skills not present in the original.**
### GitHub profile
Extract:
- Bio / tagline → supplement Professional Summary
- Pinned repositories: name, description, tech stack, stars/forks
- Contribution activity (years active, languages used)
- README content for context on major projects
- Do not infer seniority from commit count alone
### Employment gaps and special situations
**Gap under 3 months:** no special treatment.
**Gap 312 months:** one-line entry:
> "Career break — [brief honest reason: personal development / caregiving / travel / health]"
**Gap over 12 months:** add a neutral framing entry in the experience section;
highlight any upskilling, freelance, volunteering, or relevant activity during the gap.
Never fabricate activity.
**Contract / freelance / part-time:** label employment type clearly. Group multiple
short contracts under one umbrella entry (e.g. "Freelance Consultant") if they share
a skill area.
**Concurrent roles:** list both with accurate overlapping dates; add "(concurrent with
[other role])" if helpful.
**Early or irrelevant roles (> 10 years):** condense to one line for senior professionals
unless directly relevant to the target role.
**Fresh graduate:** lead with Education → Projects → Skills → Internships.
Use academic projects as proof of practical skills.
**Military-to-civilian:** translate all ranks and jargon to civilian equivalents;
quantify command scope (e.g. "Managed 35 personnel and $2M in equipment").
**Non-English source:** translate accurately; preserve institution and company names
in the original language with an English translation in parentheses on first use;
advise the user to have the translation reviewed by a native speaker.
---
## Multi-source merging
1. Build a master profile combining all extracted data.
2. Deduplicate: keep the most detailed version of each entry.
3. If two sources conflict on a date or title, flag it and ask the user to confirm.
4. Identify gaps; ask follow-up questions only for critical missing data.
5. Never fabricate a detail — mark it **[Not provided]** until the user confirms.
---
## CV section order
### Chronological (default — mid / senior)
```
1. Full Name
2. Contact Information (email | phone | LinkedIn | portfolio | city, country)
3. Professional Summary
4. Core Skills
5. Work Experience (reverse chronological)
6. Education (reverse chronological)
7. Certifications and Licences
8. Projects
9. Technical Skills (grouped: Languages | Frameworks | Tools | Platforms)
10. Achievements and Awards
11. Volunteer Experience
12. Publications / Speaking
13. Languages
14. Additional Information
```
### Fresh graduate / student
```
1. Full Name + Contact Information
2. Professional Summary / Objective
3. Education
4. Projects and Coursework
5. Skills
6. Work Experience / Internships
7. Certifications
8. Extracurricular / Volunteer
9. Languages
```
### Functional / skills-first (career changers, large gaps)
```
1. Full Name + Contact Information
2. Professional Summary
3. Core Competencies / Skills
4. Key Achievements
5. Work History (company, title, dates — minimal bullets)
6. Education
7. Certifications
8. Languages
```
### Academic CV
```
1. Full Name + Contact + ORCID / ResearchGate
2. Research Interests
3. Education
4. Academic Positions
5. Publications
6. Grants and Funding
7. Teaching Experience
8. Supervision
9. Awards and Honours
10. Conference Presentations
11. Professional Memberships
12. Skills
13. References
```
### Executive / Board
```
1. Full Name + Contact Information
2. Executive Summary
3. Core Competencies
4. Board and Advisory Roles
5. Executive Experience
6. Education and Qualifications
7. Publications / Media / Speaking
8. Professional Memberships
```
---
## Writing rules
### Professional Summary
Write 35 sentences (executive: 57) covering:
1. Who the person is: job title + years of experience
2. Primary domain of expertise
3. One concrete differentiator or standout achievement
4. Value proposition aligned to the target role
- Do not open with "I am".
- Do not open with any banned phrase (section 7c).
- Base strictly on data collected — no padding.
Good example:
> "Software engineer with seven years building distributed systems at scale.
> Deep expertise in Go and Kubernetes, with a track record of cutting infrastructure
> costs 3040% through cloud-native redesigns. Seeking a staff-level role where
> systems reliability and platform engineering intersect."
### Experience bullets — STAR-lite
Pattern: `[Strong verb] + [what you did] + [scale/scope] + [outcome if available]`
Rules:
- 36 bullets per role (23 for short-tenure or early roles)
- Past tense for completed roles; present tense for current role
- 1530 words per bullet
- Different verb to open each bullet — never repeat within one role
- If no metric was provided: write a result-focused statement without inventing numbers
- Never fabricate metrics — if the user says "we grew a lot", ask for specifics
Action verb bank:
```
Leadership: Led, Directed, Managed, Supervised, Mentored, Coached, Championed
Building: Built, Developed, Engineered, Architected, Designed, Implemented, Launched, Shipped
Improvement: Reduced, Improved, Optimised, Streamlined, Accelerated, Automated, Consolidated
Analysis: Analysed, Researched, Evaluated, Identified, Diagnosed, Assessed, Mapped
Communication: Presented, Authored, Documented, Trained, Negotiated, Advised, Collaborated
Growth: Grew, Expanded, Scaled, Generated, Increased, Secured, Delivered
Strategy: Defined, Established, Prioritised, Planned, Coordinated, Oversaw, Aligned
```
Rewrites:
```
BEFORE: "Responsible for managing the team"
AFTER: "Managed a cross-functional team of 8 engineers, delivering the product roadmap
on schedule for three consecutive quarters"
BEFORE: "Helped with developing new features"
AFTER: "Developed four customer-facing features in React, reducing support tickets by 25%"
BEFORE: "Was involved in the migration project"
AFTER: "Led migration from monolith to microservices, cutting deployment time from
45 minutes to under 4 minutes"
```
### Banned phrases — machine-checkable blocklist
Before output, scan the full CV text and **reject any bullet or sentence containing**
any of the following strings (case-insensitive):
```
results-driven
dynamic individual
highly motivated
team player
proven track record
passionate about
passionate professional
detail-oriented
self-starter
hard worker
strong communication skills
excellent communication
synergy
leverage (when used as a verb meaning "use")
paradigm shift
thought leader
go-getter
innovative thinker
outside the box
people person
visionary
change agent
```
If found: rewrite the sentence to show the specific evidence instead.
### Tense enforcement
This is a hard gate — output is blocked until tense is correct:
- **Completed role** → all bullets in past tense (Led, Built, Reduced...)
- **Current role** → all bullets in present tense (Lead, Build, Reduce...)
- **Mixed tense within one role** → always fail; fix before output
### Acronym and terminology
- Spell out on first use: "Machine Learning (ML)"; use abbreviation thereafter.
- Consistent capitalisation throughout: "JavaScript" not "Javascript".
- Mirror exact JD phrasing where applicable.
- Include both full form and abbreviation for searchability.
---
## ATS optimisation
### Structural rules
| Rule | Why it matters |
|------|----------------|
| Name must be the very first line of the body | Parsers read top-to-bottom; name in header/footer is often missed |
| Contact info in body, not in header or footer | Header/footer text is invisible to Taleo, Workday, iCIMS |
| Single-column layout only | Two-column layouts break ATS text extraction order |
| No tables for layout | Table cells are read in unpredictable order |
| No text boxes, shapes, or SmartArt | Text inside shapes is invisible to ATS |
| No images or photos (unless market requires it) | Images are ignored; photos risk bias filtering |
| No icons in bullets or headings | Symbols like ➤ ✓ ★ corrupt parsed text |
| Bullet characters: hyphen (-) or plain dot (•) only | Safe across all ATS platforms |
| Standard section headings only | Non-standard headings cause misclassification |
| No "Objective" heading | Flags CV as outdated; use "Professional Summary" |
| Font: minimum 10pt body, 1214pt headings | Smaller text garbles in PDF-to-text conversion |
| Margins: minimum 0.5 in / 1.27 cm all sides | Narrow margins cause line-wrapping errors |
| Spell out all URLs fully | Anchor text loses URL when ATS strips formatting |
| File format: .docx preferred for ATS; PDF for email | DOCX parses more accurately in most ATS |
| File name: FirstName_LastName_CV.docx | Generic names ("resume.pdf") get buried in recruiter files |
### Keyword strategy
1. Extract top 1020 keywords from the JD (if provided).
2. Categorise: hard skills | soft skills | qualifications | industry terms.
3. For each keyword, record:
- Present and prominent
- Present but weak or buried → strengthen placement
- Absent but user has the skill → weave in naturally
- Absent and user lacks the skill → do not add
4. Target keyword density: 24 natural occurrences per hard skill across the full CV.
5. Include both spelled-out form and abbreviation for key terms.
6. Mirror exact JD phrasing for shared responsibilities.
### ATS platform quick notes
| Platform | Key quirk |
|----------|-----------|
| Workday | DOCX preferred; complex PDF tables fail |
| Taleo | Strictest; no special characters; plain text preferred |
| Greenhouse | Lenient; weights keyword frequency |
| Lever | Modern parser; handles most formats |
| iCIMS | DOCX preferred; strips header/footer text |
| SmartRecruiters | Handles DOCX and PDF; relatively lenient |
Default when platform is unknown: apply Taleo-level strictness.
---
## Job description integration
When a JD is provided, run four steps:
**Step 1 — Parse:**
- Job title and seniority signals
- Required vs preferred qualifications
- Hard skills: tools, languages, platforms, methodologies
- Soft skills and collaboration patterns
- Industry terminology
- Responsibility verb phrases (mirror these in bullets)
**Step 2 — Score:**
For each of the top 15 keywords, mark: present and prominent / present but weak /
absent.
**Step 3 — Integrate:**
- Strengthen weak keyword placements.
- Weave in missing keywords the user genuinely has experience with.
- Never add a keyword the user cannot truthfully claim.
**Step 4 — Report (include at end of output):**
```
JD KEYWORD MATCH REPORT
Total JD keywords identified: 18
Matched in CV: 14 (78%)
Added naturally during generation: 3
Not added (user lacks skill): 1 — Salesforce
Recommendation: even limited Salesforce exposure is worth noting if any exists
```
---
## Anti-hallucination enforcement gate
Before any output is produced, confirm every item in the CV passes this check.
**Output is blocked until all items pass.**
| Item | Rule |
|------|------|
| Job titles | Sourced directly from user data — not inferred or upgraded |
| Company names | Sourced directly — not corrected, normalised, or embellished |
| Dates | Reproduced exactly as provided — no normalisation without noting it |
| Degrees and institutions | Reproduced exactly as provided |
| Certifications | Only those explicitly named by the user |
| Metrics and numbers | Only those provided by the user — never approximated or invented |
| Awards and achievements | Only those named by the user |
| Skills and tools | Only those provided or clearly evidenced in source data |
| Projects | Only those named by the user |
If any item cannot be verified: mark it **[Not provided]** and include it in the
missing information checklist (section 11d). Never fill gaps silently.
---
## Final output — deliver in this exact order
### Formatted CV (staging draft)
Clean plain-text draft with clear section labels. Used as the working version
before generating the tool-specific paste copies below.
### FlowCV paste-ready version
FlowCV uses structured text fields, not free-form documents. Format accordingly:
```
FULL NAME
[First name] [Last name]
PROFESSIONAL TITLE
[Target job title]
CONTACT
Email: [email]
Phone: [+country code number]
Location: [City, Country]
LinkedIn: [full URL]
Portfolio: [full URL if applicable]
PROFESSIONAL SUMMARY
[35 sentence plain paragraph — no bullets, no Markdown]
CORE SKILLS
[skill], [skill], [skill], [skill]
[skill], [skill], [skill], [skill]
WORK EXPERIENCE
[Job Title]
[Company Name] | [City, Country] | [Mon YYYY] [Mon YYYY or Present]
[Employment type if not full-time: Contract / Freelance / Part-time]
- [Bullet one: action verb + context + outcome]
- [Bullet two]
- [Bullet three]
[Repeat for each role]
EDUCATION
[Degree Name]
[Institution Name], [Country] | [YYYY] [YYYY]
[Grade or classification if notable]
[Repeat for each qualification]
CERTIFICATIONS
[Certificate Name] — [Issuing Body] — [Month YYYY]
PROJECTS
[Project Name]
[Technologies: tool, tool, tool]
- [What it does / your role / outcome]
ACHIEVEMENTS
- [Achievement one]
- [Achievement two]
VOLUNTEER EXPERIENCE
[Role] — [Organisation] — [YYYYYYYY]
- [One-line description]
LANGUAGES
[Language]: [Native / Fluent / Professional / Conversational / Basic]
ADDITIONAL INFORMATION
[Anything else: open-source, interests relevant to role]
```
### Canva paste-ready version
Canva CV templates use individual text boxes per section. Provide each section as
a separate clearly labelled block, with no Markdown symbols.
```
--- PASTE INTO: Name field ---
[Full name]
--- PASTE INTO: Job title / headline field ---
[Target job title]
--- PASTE INTO: Contact block ---
[email] | [phone] | [city, country] | [LinkedIn URL]
--- PASTE INTO: Summary / About field ---
[35 sentence paragraph, plain text, no hyphens or bullets]
--- PASTE INTO: Skills field ---
[skill] | [skill] | [skill] | [skill] | [skill]
--- PASTE INTO: Experience entry 1 ---
[Job Title]
[Company] | [Location] | [Mon YYYY Mon YYYY]
- [Bullet]
- [Bullet]
- [Bullet]
[Continue for each role as a separate block]
--- PASTE INTO: Education entry 1 ---
[Degree]
[Institution], [Country] | [YYYY YYYY]
[Grade if notable]
--- PASTE INTO: Certifications ---
[Certificate] | [Issuer] | [YYYY]
--- PASTE INTO: Languages ---
[Language] ([Proficiency])
```
### Missing information checklist
```
MISSING INFORMATION
[ ] Phone number
[ ] LinkedIn URL
[ ] Portfolio or GitHub URL
[ ] Measurable results for [Role] at [Company]
[ ] Certifications — do you hold any?
[ ] Languages — list any beyond English
[ ] Employment gap [Mon YYYY Mon YYYY] — add a brief framing note
[ ] [Any other flagged item]
```
### CV flaw report (scored 0100)
Run all checks. Display a scored report:
```
CV FLAW REPORT
──────────────────────────────────────
Score: [X]/100
PASS Truthfulness — all facts sourced from user data
PASS No hallucination — no fabricated details
PASS Tense correctness — past for completed, present for current
PASS ATS structure — single column, no tables or images
PASS Standard headings — all recognisable by parsers
PASS No forbidden characters — no ➤ ✓ ★
PASS Humanized — no banned phrases found
PASS Contact info in body (not header/footer)
FAIL [Check name] — [specific issue and location in CV]
──────────────────────────────────────
Deductions: -[N] per FAIL item
Final score: [X]/100
ISSUES TO FIX:
1. [Exact location] — [Exact problem] — [Suggested fix]
2. [Exact location] — [Exact problem] — [Suggested fix]
```
Score deductions: -10 per FAIL on truthfulness or hallucination;
-5 per FAIL on tense, ATS structure, or banned phrases;
-3 per FAIL on formatting issues.
### Improvement suggestions (37, specific and actionable)
- "Your summary does not state the target role. Open with your job title explicitly."
- "The [Company] role has no metrics. Even approximate scope (team size, users, budget range) strengthens credibility."
- "Skills section mixes expert and basic tools without distinction. Group into Proficient / Familiar."
- "Add a GitHub or portfolio URL — technical recruiters check it before the interview."
- "Three bullets begin with 'Responsible for' — replace with direct action verbs."
- "CV is [N] pages for [N] years of experience. Target is [N] pages; trim older roles to one line."
### Suggested file name
```
Suggested filename: [FirstName]_[LastName]_CV.docx
```
---
## Cover letter companion (optional)
After the CV output, offer:
> "Would you like a tailored cover letter for this application?"
If yes, output as a **separate clearly labelled plain-text block** — not inline with the CV.
Rules:
- Opens with a specific hook — not "I am writing to apply for…"
- References company and role by name
- Bridges 23 strongest CV points to the JD's key requirements
- Closes with a clear call to action
- Matches tone of the target industry
- 3 paragraphs maximum, 250350 words
- Does not repeat the CV verbatim
---
## Limitations
- **No hallucination.** Never invent a title, company, date, degree, cert, skill, metric, or award.
- **No fake metrics.** If the user says "we grew a lot", ask for specifics — never insert a percentage.
- **Respect source truth.** "Junior Developer" stays "Junior Developer" — suggest a reframe if needed; never silently change it.
- **No silent changes.** If something is materially reworded, note the change.
- **One version at a time.** Complete the CV before offering variants.
- **Privacy.** Do not expose full home address, national ID, DOB, marital status, or religion unless the user's target market requires it.
- **No keyword stuffing.** Adding skills the user does not have is fraud. Flag gaps; never fabricate.
- **OCR warning.** Always display before continuing: "OCR was used — please verify the extracted text for accuracy."
---
## Country and market conventions
| Market | Length | Photo | DOB | Marital status | References |
|--------|--------|-------|-----|----------------|------------|
| USA | 12 pages | No | No | No | "Available on request" |
| Canada | 12 pages | No | No | No | "Available on request" |
| UK | 2 pages | No | No | No | "Available on request" |
| Ireland | 2 pages | No | No | No | "Available on request" |
| Australia / NZ | 23 pages | No | No | No | "Available on request" |
| Germany / Austria / Switzerland | 23 pages | Yes (expected) | Yes | Sometimes | Listed or on request |
| France | 12 pages | Optional | No (illegal to require) | No | On request |
| Netherlands / Scandinavia | 12 pages | Optional | No | No | On request |
| Japan | 12 pages (rirekisho) | Yes | Yes | Yes | Listed |
| South Korea | 12 pages | Yes | Yes | Yes | Listed |
| China | 12 pages | Yes | Yes | Yes | Listed |
| India | 23 pages | Optional | Yes (common) | Sometimes | Listed |
| Nepal | 23 pages | Yes (common) | Yes | Sometimes | Listed |
| Bangladesh / Sri Lanka | 23 pages | Yes (common) | Yes | Sometimes | Listed |
| UAE / Gulf (GCC) | 23 pages | Yes (common) | Yes | Yes (sometimes) | Listed |
| Nigeria / East Africa | 23 pages | Yes (common) | Yes | Sometimes | Listed |
| South Africa | 35 pages | Optional | Yes (common) | No | Listed |
| Brazil | 12 pages | Optional | Yes (common) | No | On request |
| Academic (global) | No limit | Varies | Varies | No | Full list required |
| Executive / board (global) | 23 pages | No | No | No | On request |
Default when market is unknown: UK / international conventions (no photo, no DOB, 2 pages,
"Available on request").
---
## Decision tree
```
User invokes @cv-generator
|
v
Source provided? --No--> Run questionnaire (Q1Q20, one at a time)
|Yes
v
LinkedIn URL blocked? --Yes--> Ask for PDF export immediately; do not proceed empty
|No
v
Collect all sources --> merge and deduplicate (section 5)
|
v
Ask: Purpose? --> Detect or assume seniority (default: mid-level; tell the user)
|
v
Select format (section 3c)
|
v
Select section order (section 6)
|
v
JD provided? --Yes--> Parse JD --> extract and score keywords (section 9)
|No |
v v
Write CV content Integrate keywords naturally
(sections 78) |
|<-------------------+
v
Run anti-hallucination gate (section 10) --> block output until all pass
|
v
Run tense enforcement (section 7d) --> block output until all pass
|
v
Run banned phrase scan (section 7c) --> fix any found
|
v
Output in order:
Formatted CV (staging draft)
FlowCV paste-ready version
Canva paste-ready version
Missing information checklist
CV flaw report (scored)
Improve suggestions
Suggested file name
|
v
Offer cover letter (section 12)
```
@@ -19,6 +19,14 @@ tags:
# ExamPrep AI
## When to Use
Use this skill when you need to:
- Convert a syllabus, past papers, or study notes into a prioritized roadmap.
- Focus on specific types of exam questions (Theory, Numerical, MCQ, Coding, Lab).
- Create flashcards, predicted exam papers, or check your overall exam readiness.
- Perform last-minute revision or deep-dive into important exam topics.
## 🎯 Selective Reading Rule — Read ONLY the section matching the request
| What the student asks for | Jump to |
@@ -6,7 +6,7 @@ risk: unknown
---
Install by downloading the installer script first, reviewing it, and then running it locally. Example:
`curl -LsSf https://hf.co/cli/install.sh -o /tmp/hf-install.sh && less /tmp/hf-install.sh && bash /tmp/hf-install.sh`
`tmpdir="$(mktemp -d)" && trap 'rm -rf "$tmpdir"' EXIT && curl -LsSf https://hf.co/cli/install.sh -o "$tmpdir/hf-install.sh" && less "$tmpdir/hf-install.sh" && bash "$tmpdir/hf-install.sh"`
## When to Use
Use this skill when you need the `hf` CLI for Hub authentication, downloads, uploads, repo management, or basic compute operations.
@@ -182,7 +182,7 @@ Generated with `huggingface_hub v1.8.0`. Run `hf skills add --force` to regenera
To mount Hub repositories or buckets as local filesystems — no download, no copy, no waiting — use `hf-mount`. Files are fetched on demand. GitHub: https://github.com/huggingface/hf-mount
Install by downloading the installer locally, reviewing it, and then running it. Example:
`curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh -o /tmp/hf-mount-install.sh && less /tmp/hf-mount-install.sh && sh /tmp/hf-mount-install.sh`
`tmpdir="$(mktemp -d)" && trap 'rm -rf "$tmpdir"' EXIT && curl -fsSL https://raw.githubusercontent.com/huggingface/hf-mount/main/install.sh -o "$tmpdir/hf-mount-install.sh" && less "$tmpdir/hf-mount-install.sh" && sh "$tmpdir/hf-mount-install.sh"`
Some command examples:
- `hf-mount start repo openai-community/gpt2 /tmp/gpt2` — mount a repo (read-only)
@@ -0,0 +1,101 @@
---
name: open-dynamic-workflows
description: "Plan, orchestrate, and adversarially verify parallel AI coding agents with a dynamic multi-agent workflow engine."
category: ai-agents
risk: critical
source: community
source_repo: Suraj1235/open-dynamic-workflows
source_type: community
date_added: "2026-06-06"
author: Suraj1235
tags: [multi-agent, orchestration, workflow, adversarial-verification, coding-agents]
tools: [claude, cursor, codex, gemini, antigravity]
# Optional: declare the upstream license if source_repo is set
license: "MIT"
license_source: "https://github.com/Suraj1235/open-dynamic-workflows/blob/main/LICENSE"
---
# Open Dynamic Workflows
## Overview
Open Dynamic Workflows (ODW) is an open-source dynamic multi-agent workflow engine for AI coding agents such as OpenCode, Codex, Antigravity, and VS Code. It lets you plan a task, orchestrate multiple agents working in parallel, and adversarially verify their output before it lands. ODW ships a Codex/Antigravity skill folder (`SKILL.md` plus a daemon bridge) and an OpenCode plugin, and it is bring-your-own-model (Anthropic, OpenAI-compatible, or Ollama). This skill is adapted from the community project at `Suraj1235/open-dynamic-workflows`.
## When to Use This Skill
- Use when you need to decompose a coding task into independent subtasks and run multiple agents in parallel.
- Use when working across more than one AI coding tool (OpenCode, Codex, Antigravity, VS Code) and want a single orchestration layer.
- Use when the user asks for adversarial review or verification of agent-generated changes before merging.
## How It Works
### Step 1: Plan
ODW takes a high-level goal and produces a dynamic workflow graph of subtasks, identifying which can run in parallel and which have dependencies.
### Step 2: Orchestrate
The engine dispatches subtasks to parallel agents through the OpenCode plugin or the Codex/Antigravity daemon bridge, using your configured model provider (Anthropic, OpenAI-compatible, or Ollama).
### Step 3: Adversarially Verify
Completed work is routed through an adversarial verification pass that challenges the output before results are synthesized and returned.
## Examples
### Example 1: Run a parallel workflow
ODW is installed from source (clone the repo, then `npm install`). The CLI is
`odw-daemon` — run it as `npm run odw -- <args>` from inside the repo, or as
`npx odw-daemon <args>` / a global `odw-daemon` if you link the bin.
```bash
# Configure your model provider (bring-your-own-model)
export ANTHROPIC_API_KEY=... # or an OpenAI-compatible / Ollama endpoint
# One-time setup: generate ~/.odw/config.json
npm run setup
# Start the local workflow daemon (once)
npm run odw -- start
# Plan, orchestrate, and verify a task across parallel agents
npm run odw -- run --prompt "refactor the auth module and add tests"
```
### Example 2: Use the Codex/Antigravity skill bridge
```bash
# ODW ships a SKILL.md + daemon bridge consumed by Codex / Antigravity.
# Start the daemon, then run a saved orchestration script through it:
npm run odw -- start
npm run odw -- run --script examples/workflows/studio-prime.workflow.js --cwd .
```
## Best Practices
- ✅ Scope each subtask so agents can run without shared state.
- ✅ Keep the adversarial verification pass enabled before merging agent output.
- ❌ Don't run interdependent subtasks in parallel without declaring their dependencies.
- ❌ Don't commit provider API keys; use environment variables or a secrets manager.
## Limitations
- This skill does not replace environment-specific validation, testing, or expert review.
- Stop and ask for clarification if required inputs, permissions, or safety boundaries are missing.
## Security & Safety Notes
- ODW executes agent-generated code and shell commands; run it only in an authorized, local, or sandboxed environment.
- Model provider credentials (Anthropic / OpenAI-compatible / Ollama) must be supplied via environment variables, never committed to source.
- Review adversarial-verification output before applying changes to a production branch.
## Common Pitfalls
- **Problem:** Parallel agents collide on the same files.
**Solution:** Give each subtask exclusive file/module ownership and run conflicting tasks sequentially.
## Related Skills
- `@multi-agent-orchestration` - When coordinating multiple agents on one goal.
- `@code-review` - How adversarial verification complements human review.
@@ -18,5 +18,5 @@ Use this when optimizing opencode's permission settings, reviewing allowed comma
- **Config review**: Loads `~/.config/opencode/opencode.json` or project-level config
- **Permission summary**: Identifies currently allowed commands and skill permissions
- **Safe commands**: Suggests read-only commands (ls*, git status*, git log*, rg, grep, cat, etc.)
- **Safe commands**: Suggests reviewed read-only commands such as `git status --short`, `git log --oneline`, `rg`, `grep`, and `cat`; broad trailing wildcards need manual review.
- **Change application**: Edits config to add/remove permission entries, validates JSON
@@ -33,7 +33,8 @@ Complements opencode's built-in allow/deny/ask permissions by auditing current c
## Key Rules
- Never allow commands that modify files, commit, push, or change system state
- Use wildcards appropriately (e.g., `git status*` not just `git status`)
- Prefer exact command entries such as `git status --short`, `git diff --stat`, and `ls -la`
- Avoid trailing wildcards such as `git status*` unless the expanded command family has been manually reviewed as read-only
- Confirm with user before modifying permission config
- Distinguish between bash command permissions and skill permissions
- Keep config organized: group related commands together
@@ -2,7 +2,7 @@
name: polis-protocol
description: "Coordinate multi-vendor AI agents as a self-improving team — a learning router assigns work by track record and citizens can amend the protocol's own rules."
category: orchestration
risk: safe
risk: critical
source: community
source_repo: yehudalevy-collab/polis-protocol
source_type: community
@@ -12,6 +12,10 @@ tags: [multi-agent, coordination, routing, orchestration, governance, vendor-agn
tools: [claude, cursor, gemini, codex, antigravity]
license: "MIT"
license_source: "https://github.com/yehudalevy-collab/polis-protocol/blob/main/LICENSE"
plugin:
targets:
codex: blocked
claude: blocked
---
# Polis Protocol — a team of agents that develops
@@ -33,11 +37,13 @@ In Antigravity specifically, this turns Manager View's fixed pipeline into a tea
### Step 1: Found a polis
Clone the repo and run the scaffolder directly (review `install.sh` first if you prefer the one-line installer):
Clone a reviewed revision of the repo and run the scaffolder directly (review `install.sh` first if you prefer the one-line installer):
```bash
git clone https://github.com/yehudalevy-collab/polis-protocol.git
python3 polis-protocol/scripts/init_polis.py \
cd polis-protocol
git checkout <reviewed-commit-sha>
python3 scripts/init_polis.py \
--project-root . \
--agent-id gemini-antigravity-yourproject \
--vendor google --model gemini-3 --tool antigravity
@@ -68,7 +74,9 @@ A settled contract files a lesson; `--reconcile` folds it into `routing_stats.ym
```bash
git clone https://github.com/yehudalevy-collab/polis-protocol.git
cd polis-protocol && bash scripts/demo.sh
cd polis-protocol
git checkout <reviewed-commit-sha>
bash scripts/demo.sh
```
The router recommends Gemini for a Spanish-translation contract — because settled work taught it she has the best record on that tag, not because anyone reassigned it.
@@ -91,3 +99,4 @@ python3 scripts/route_contract.py --polis-root examples/research-team/_polis \
- Routing quality depends on accurate citizen capability cards and enough settled work history to learn from.
- The protocol coordinates agent work but does not replace review, tests, or explicit maintainer approval.
- Multi-agent voting and amendments can add process overhead for small, single-owner tasks.
- The upstream scripts are external code; pin to a reviewed commit and run `--dry-run` before allowing writes to a project.
@@ -0,0 +1,140 @@
---
name: runapi-cli
description: Generate AI images, videos, and music/audio from agents using the RunAPI CLI.
category: development
risk: critical
source: official
source_repo: runapi-ai/cli-skill
source_type: official
date_added: "2026-06-07"
author: runapi-ai
tags: [runapi, cli, models, automation, codex, claude, gemini]
tools: [claude, codex, gemini, cursor, antigravity]
license: "Apache-2.0"
license_source: "https://github.com/runapi-ai/cli-skill/blob/main/LICENSE"
---
# RunAPI CLI
## Overview
The `runapi` CLI is the execution layer for RunAPI model tasks. Use it when an agent needs to generate AI images, videos, or music/audio, run a one-off model job, pass a JSON request body, wait for an async task, or script RunAPI from a terminal, server, or CI job.
Source repository: [github.com/runapi-ai/cli-skill](https://github.com/runapi-ai/cli-skill) (Apache-2.0)
## When to Use This Skill
- Use when the user asks to run a RunAPI model from an agent.
- Use when the user needs to inspect RunAPI CLI auth or account status.
- Use when the user wants to pass JSON request bodies to RunAPI services.
- Use when the user wants to submit async RunAPI tasks and wait for completion.
- Use when the user wants to install the RunAPI CLI on a local machine, server, or CI runner.
## Install
### macOS / Linux
```shell
brew install runapi-ai/tap/runapi
```
### Server / CI
Download the installer, inspect it, then run it locally.
```shell
curl -fsSL https://runapi.ai/cli/install.sh -o runapi-install.sh
less runapi-install.sh
sh runapi-install.sh
```
To pin a specific version:
```shell
sh runapi-install.sh --version v0.1.0
```
The installer detects OS and architecture, verifies the SHA-256 checksum from `https://runapi.ai/cli/latest.json`, and refuses to write the binary if verification fails.
## Authentication
Treat RunAPI authentication and generation as security-sensitive: commands can call remote services, consume credits, and expose account state. Review installer scripts before running them and keep API keys in environment variables or stdin, not shell history.
Check the current state first:
```shell
runapi auth status
```
| Source | How |
|---|---|
| Environment | Read `RUNAPI_API_KEY` from the environment |
| Saved config | `printf '%s' "$RUNAPI_API_KEY" \| runapi auth import-token --token -` |
| Browser login | `runapi login` only when the user explicitly wants browser auth |
`RUNAPI_BASE_URL` overrides the default base URL.
Avoid passing secrets directly in command arguments. Prefer `RUNAPI_API_KEY` or stdin token import with `--token -`.
## Discover Services, Commands, and Fields
The CLI is JSON-first. Every service exposes typed commands, and each command documents its request fields through `--help`. Inspect command help before composing a request.
```shell
runapi --help
runapi suno --help
runapi suno text-to-music --help
```
## Run a Model
Pass the request body as JSON through `--input-file`, `--input`, or stdin. The default flow is synchronous and polls until the task completes.
```shell
runapi suno text-to-music --input-file request.json
runapi suno text-to-music --async --input-file request.json
runapi wait <task-id> --service suno --action text-to-music
runapi get <task-id> --service suno --action text-to-music
```
JSON responses go to stdout; progress lines go to stderr. Pipe to `jq` for downstream parsing.
## Account
```shell
runapi account info
runapi account balance
```
## Install the Skill Into Another Agent Runtime
```shell
runapi agent install-skill --target claude
runapi agent install-skill --target codex
runapi agent install-skill --target gemini
runapi agent install-skill --target openclaw
runapi agent list-targets
runapi agent install-skill --target-dir <path>
```
## Limitations
- RunAPI model calls require a valid RunAPI account or API key.
- Some model tasks are long-running and should use `--async` plus `runapi wait`.
- Browser login is interactive and should not be the default path for agents.
- This skill does not replace model-specific parameter validation; inspect command help before building request JSON.
## Security & Safety Notes
- Never paste API keys into example commands or PR text.
- Prefer `RUNAPI_API_KEY` or stdin token import instead of command-line secrets.
- Do not run interactive `runapi login` by default from an agent.
- Check the CLI exit code before assuming a task succeeded.
## References
- RunAPI CLI skill: https://github.com/runapi-ai/cli-skill
- RunAPI CLI repository: https://github.com/runapi-ai/cli
- RunAPI model catalog: https://runapi.ai/models.md
@@ -33,10 +33,11 @@ The cleanest approach is a reusable `JsonLd` component:
```jsx
// components/JsonLd.jsx
export function JsonLd({ data }) {
const json = JSON.stringify(data).replace(/</g, '\\u003c');
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{ __html: json }}
/>
);
}
@@ -61,8 +61,10 @@ Show the proposed branch name and ask for one-word confirmation (or type alterna
- If not on main/master: check if current branch matches proposed name
- If yes: stay on it
- If no: ask to switch or create new
- Create branch: `git checkout -b <branch-name>`
- Stage changes: `git add <grouped-files>`
- Create branch only after validating the branch name, then use `git checkout -b "$branch_name"`
- Stage explicit pathspecs only: `git add -- path/to/file ...`
- If file paths are generated, keep them NUL-delimited (`git diff -z --name-only`) and pass them as pathspec arguments.
- Never concatenate untrusted filenames into a shell command and never run the placeholder text literally.
- Auto-generate commit message from changes:
- First line: `<type>: <short description>` (max 72 chars)
- Body: grouped file changes with brief descriptions
@@ -3,13 +3,20 @@ import re
from pathlib import Path
class UsthtSafetyError(RuntimeError):
"""Raised when a runtime path would escape .ustht/."""
def find_ustht() -> Path | None:
"""Find .ustht/ in the current directory or one of its parents."""
cwd = Path.cwd()
for d in [cwd, *cwd.parents]:
ustht = d / ".ustht"
if ustht.is_dir():
return ustht
if ustht.exists():
if ustht.is_symlink():
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {ustht}")
if ustht.is_dir():
return ustht.resolve()
return None
@@ -28,7 +35,7 @@ def read_define_ini(ustht: Path) -> dict:
if not ini.exists():
return {}
result = {}
for line in ini.read_text(encoding="utf-8").splitlines():
for line in safe_read_text(ustht, ini).splitlines():
line = line.strip()
if "=" in line and not line.startswith("#"):
k, v = line.split("=", 1)
@@ -40,15 +47,95 @@ def write_define_ini(ustht: Path, cfg: dict):
"""Replace define.ini with the provided key/value pairs."""
ini = ustht / "define.ini"
lines = [f"{k}={v}" for k, v in cfg.items()]
ini.write_text("\n".join(lines) + "\n", encoding="utf-8")
safe_write_text(ustht, ini, "\n".join(lines) + "\n")
def is_processed(filepath: Path) -> bool:
def is_processed(filepath: Path, ustht: Path | None = None) -> bool:
"""Return true when the first raw-file line is the processed marker."""
first_line = filepath.read_text(encoding="utf-8").split("\n", 1)[0].strip()
content = safe_read_text(ustht, filepath) if ustht else filepath.read_text(encoding="utf-8")
first_line = content.split("\n", 1)[0].strip()
return first_line == "<!-- processed -->"
def ensure_runtime_path(ustht: Path, path: Path, *, must_exist: bool = False) -> Path:
"""Return a path only if its real location stays inside .ustht/."""
base = Path(ustht)
if base.is_symlink():
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {base}")
base_real = base.resolve(strict=True)
target = Path(path)
if not target.is_absolute():
target = base_real / target
if target.exists() and target.is_symlink():
raise UsthtSafetyError(f"Refusing symlinked runtime path: {target}")
if must_exist and not target.exists():
raise UsthtSafetyError(f"Runtime path does not exist: {target}")
target_real = target.resolve(strict=must_exist)
try:
target_real.relative_to(base_real)
except ValueError as exc:
raise UsthtSafetyError(f"Runtime path escapes .ustht/: {target}") from exc
rel = target.relative_to(base_real)
current = base_real
for part in rel.parts:
current = current / part
if current.exists() and current.is_symlink():
raise UsthtSafetyError(f"Refusing symlinked runtime path: {current}")
return target
def ensure_runtime_dir(ustht: Path, path: Path, *, create: bool = False) -> Path:
"""Return a safe runtime directory, creating it when requested."""
directory = ensure_runtime_path(ustht, path, must_exist=False)
if create:
directory.mkdir(parents=True, exist_ok=True)
if directory.exists() and not directory.is_dir():
raise UsthtSafetyError(f"Runtime path is not a directory: {directory}")
return directory
def safe_read_text(ustht: Path | None, path: Path) -> str:
"""Read a runtime file after symlink and containment checks."""
safe_path = ensure_runtime_path(ustht, path, must_exist=True) if ustht else path
return safe_path.read_text(encoding="utf-8")
def safe_write_text(ustht: Path, path: Path, content: str):
"""Write a runtime file after symlink and containment checks."""
safe_path = ensure_runtime_path(ustht, path, must_exist=False)
ensure_runtime_dir(ustht, safe_path.parent, create=True)
safe_path.write_text(content, encoding="utf-8")
def safe_markdown_files(ustht: Path, directory: Path, *, reverse: bool = False) -> list[Path]:
"""List safe markdown files under one runtime directory."""
safe_dir = ensure_runtime_dir(ustht, directory)
if not safe_dir.exists():
return []
files = []
for file_path in safe_dir.glob("*.md"):
safe_path = ensure_runtime_path(ustht, file_path, must_exist=True)
if safe_path.is_file():
files.append(safe_path)
return sorted(files, reverse=reverse)
def safe_markdown_tree(ustht: Path, directory: Path) -> list[Path]:
"""List safe markdown files recursively under one runtime directory."""
safe_dir = ensure_runtime_dir(ustht, directory)
if not safe_dir.exists():
return []
files = []
for file_path in safe_dir.rglob("*.md"):
safe_path = ensure_runtime_path(ustht, file_path, must_exist=True)
if safe_path.is_file():
files.append(safe_path)
return sorted(files)
def validate_dim_name(dim: str) -> bool:
"""Validate a dimension path made of safe kebab-case segments."""
reserved = {"raw", "ignored", "export", "define", "readme-ai"}
@@ -3,7 +3,7 @@ import sys
from datetime import datetime
from pathlib import Path
from common import find_ustht
from common import ensure_runtime_dir, find_ustht, safe_markdown_files, safe_read_text, safe_write_text
HELP = """Usage: python ignore_ops.py show|remove_last|add_suffix "text" [--help]
@@ -14,11 +14,11 @@ Subcommands:
"""
def find_last_raw_entry(raw_dir: Path):
def find_last_raw_entry(ustht: Path, raw_dir: Path):
"""Return (file path, line index, entry text) for the latest raw entry."""
files = sorted(raw_dir.glob("*.md"), reverse=True)
files = safe_markdown_files(ustht, raw_dir, reverse=True)
for f in files:
lines = f.read_text(encoding="utf-8").splitlines()
lines = safe_read_text(ustht, f).splitlines()
if lines and lines[0].strip() == "<!-- processed -->":
continue
for idx in range(len(lines) - 1, -1, -1):
@@ -27,16 +27,16 @@ def find_last_raw_entry(raw_dir: Path):
return None, None, None
def remove_line(filepath: Path, idx: int):
def remove_line(ustht: Path, filepath: Path, idx: int):
"""Remove one line from a file."""
lines = filepath.read_text(encoding="utf-8").splitlines()
lines = safe_read_text(ustht, filepath).splitlines()
del lines[idx]
filepath.write_text("\n".join(lines) + ("\n" if lines else ""), encoding="utf-8")
safe_write_text(ustht, filepath, "\n".join(lines) + ("\n" if lines else ""))
def append_to_ignored(ignored_dir: Path, text: str, reason: str):
def append_to_ignored(ustht: Path, ignored_dir: Path, text: str, reason: str):
"""Append one ignored entry to today's ignored file."""
ignored_dir.mkdir(exist_ok=True)
ignored_dir = ensure_runtime_dir(ustht, ignored_dir, create=True)
today = datetime.now().strftime("%Y-%m-%d")
now = datetime.now().strftime("%H:%M")
f = ignored_dir / f"{today}.md"
@@ -45,23 +45,23 @@ def append_to_ignored(ignored_dir: Path, text: str, reason: str):
clean = clean.rsplit(" | suggested-dim:", 1)[0]
entry = f"- [{now}] {clean} ({reason})"
if f.exists():
content = f.read_text(encoding="utf-8").rstrip()
f.write_text(f"{content}\n{entry}\n", encoding="utf-8")
content = safe_read_text(ustht, f).rstrip()
safe_write_text(ustht, f, f"{content}\n{entry}\n")
else:
f.write_text(f"{entry}\n", encoding="utf-8")
safe_write_text(ustht, f, f"{entry}\n")
def show_ignored(ignored_dir: Path):
def show_ignored(ustht: Path, ignored_dir: Path):
"""Print all ignored entries."""
if not ignored_dir.exists():
print("No ignored entries.")
return
files = sorted(ignored_dir.glob("*.md"), reverse=True)
files = safe_markdown_files(ustht, ignored_dir, reverse=True)
if not files:
print("No ignored entries.")
return
for f in files:
entries = [line for line in f.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- [")]
entries = [line for line in safe_read_text(ustht, f).splitlines() if line.strip().startswith("- [")]
if entries:
print(f"#{f.name} ({len(entries)} entries):")
for entry in entries:
@@ -73,12 +73,12 @@ def remove_last(ustht: Path):
if not raw_dir.exists():
print("No previous thought to ignore.")
return
filepath, idx, entry = find_last_raw_entry(raw_dir)
filepath, idx, entry = find_last_raw_entry(ustht, raw_dir)
if filepath is None:
print("No previous thought to ignore.")
return
remove_line(filepath, idx)
append_to_ignored(ustht / "ignored", entry, "ignored with --last")
remove_line(ustht, filepath, idx)
append_to_ignored(ustht, ustht / "ignored", entry, "ignored with --last")
display = entry
if "] " in display:
display = display.split("] ", 1)[1]
@@ -88,7 +88,7 @@ def remove_last(ustht: Path):
def add_suffix(ustht: Path, text: str):
append_to_ignored(ustht / "ignored", text, "ignored by suffix")
append_to_ignored(ustht, ustht / "ignored", text, "ignored by suffix")
print("Ignored current message.")
@@ -108,7 +108,7 @@ def main():
cmd = sys.argv[1]
if cmd == "show":
show_ignored(ustht / "ignored")
show_ignored(ustht, ustht / "ignored")
elif cmd == "remove_last":
remove_last(ustht)
elif cmd == "add_suffix":
@@ -3,7 +3,7 @@ import shutil
import sys
from pathlib import Path
from common import find_skill_dir
from common import UsthtSafetyError, find_skill_dir
HELP = """Usage: python init.py [--help]
@@ -34,6 +34,8 @@ def main():
target = Path.cwd() / ".ustht"
if target.exists():
if target.is_symlink():
raise UsthtSafetyError(f"Refusing symlinked runtime directory: {target}")
print("Already initialized; .ustht/ exists, skipping creation.")
sys.exit(0)
@@ -2,7 +2,7 @@
import sys
from pathlib import Path
from common import find_ustht, validate_dim_name
from common import find_ustht, safe_markdown_tree, safe_read_text, validate_dim_name
HELP = """Usage: python show_mdbase.py show [--all|--dimension] [--help]
@@ -18,14 +18,14 @@ def show_index(mdbase: Path):
if not index.exists():
print("mdbase/README.ai.md does not exist.")
return
print(index.read_text(encoding="utf-8"))
print(safe_read_text(mdbase.parent, index))
def list_dims(mdbase: Path):
details = mdbase / "details"
if not details.exists():
return []
return sorted(p.relative_to(details).with_suffix("").as_posix() for p in details.rglob("*.md"))
return sorted(p.relative_to(details).with_suffix("").as_posix() for p in safe_markdown_tree(mdbase.parent, details))
def show_dim(mdbase: Path, dim: str):
@@ -39,7 +39,7 @@ def show_dim(mdbase: Path, dim: str):
if not path.exists():
print(f"mdbase/details/{dim}.md does not exist yet.")
return
print(path.read_text(encoding="utf-8"))
print(safe_read_text(mdbase.parent, path))
def show_all(mdbase: Path):
@@ -54,7 +54,7 @@ def show_all(mdbase: Path):
print(f"mdbase has {len(dims)} dimensions:")
for dim in dims:
path = details / f"{dim}.md"
lines = [line for line in path.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- ")]
lines = [line for line in safe_read_text(mdbase.parent, path).splitlines() if line.strip().startswith("- ")]
print(f" {dim}.md: {len(lines)} entries")
@@ -2,7 +2,7 @@
import sys
from pathlib import Path
from common import find_ustht, is_processed
from common import find_ustht, is_processed, safe_markdown_files, safe_read_text
HELP = """Usage: python show_raw.py [--help]
@@ -25,13 +25,13 @@ def main():
print("No unprocessed records.")
return
files = [f for f in sorted(raw_dir.glob("*.md"), reverse=True) if not is_processed(f)]
files = [f for f in safe_markdown_files(ustht, raw_dir, reverse=True) if not is_processed(f, ustht)]
if not files:
print("No unprocessed records. All raw files are marked processed.")
return
for f in files:
content = f.read_text(encoding="utf-8").strip()
content = safe_read_text(ustht, f).strip()
entry_count = sum(1 for line in content.splitlines() if line.strip().startswith("- ["))
print(f"#{f.name} ({entry_count} unprocessed entries):")
print(content)
@@ -5,7 +5,18 @@ from collections import defaultdict
from datetime import datetime
from pathlib import Path
from common import find_ustht, read_define_ini, write_define_ini, is_processed, validate_dim_name
from common import (
ensure_runtime_dir,
safe_markdown_files,
safe_markdown_tree,
safe_read_text,
safe_write_text,
find_ustht,
read_define_ini,
write_define_ini,
is_processed,
validate_dim_name,
)
HELP = """Usage: python sortin.py [--dry] [--help]
@@ -18,7 +29,7 @@ Options:
"""
def parse_raw_file(filepath: Path):
def parse_raw_file(ustht: Path, filepath: Path):
"""Parse raw entries from one file."""
entries = []
date = filepath.stem.split("-", 3)
@@ -27,7 +38,7 @@ def parse_raw_file(filepath: Path):
else:
date = datetime.now().strftime("%Y-%m-%d")
for line in filepath.read_text(encoding="utf-8").splitlines():
for line in safe_read_text(ustht, filepath).splitlines():
line = line.strip()
match = re.match(r"^- \[(\d{2}:\d{2})\] (.*)$", line)
if not match:
@@ -51,31 +62,31 @@ def dim_path(mdbase: Path, dim: str) -> Path:
return mdbase / "details" / f"{dim}.md"
def count_entries(path: Path) -> int:
def count_entries(ustht: Path, path: Path) -> int:
if not path.exists():
return 0
return sum(1 for line in path.read_text(encoding="utf-8").splitlines() if line.strip().startswith("- "))
return sum(1 for line in safe_read_text(ustht, path).splitlines() if line.strip().startswith("- "))
def append_entries(path: Path, entries):
def append_entries(ustht: Path, path: Path, entries):
"""Append entries grouped by date to one dimension file."""
by_date = defaultdict(list)
for entry in entries:
by_date[entry["date"]].append(entry)
path.parent.mkdir(parents=True, exist_ok=True)
ensure_runtime_dir(ustht, path.parent, create=True)
if not path.exists():
title = path.stem.replace("-", " ").title()
path.write_text(f"# {title}\n\n> Project memory for `{path.stem}`.\n\n", encoding="utf-8")
safe_write_text(ustht, path, f"# {title}\n\n> Project memory for `{path.stem}`.\n\n")
content = path.read_text(encoding="utf-8").rstrip()
content = safe_read_text(ustht, path).rstrip()
for date, date_entries in sorted(by_date.items()):
lines = [f"- {entry['text']}" for entry in date_entries]
block = "\n".join(lines)
heading = f"## {date}"
if heading in content:
content_lines = content.splitlines()
heading_idx = next(i for i, line in enumerate(content_lines) if line.strip() == heading)
content_lines = content.splitlines()
heading_idx = next((i for i, line in enumerate(content_lines) if line.strip() == heading), None)
if heading_idx is not None:
insert_idx = len(content_lines)
for i in range(heading_idx + 1, len(content_lines)):
if content_lines[i].startswith("## "):
@@ -92,31 +103,31 @@ def append_entries(path: Path, entries):
content = "\n".join(before).rstrip()
else:
content = f"{content}\n\n{heading}\n\n{block}".rstrip()
path.write_text(content + "\n", encoding="utf-8")
safe_write_text(ustht, path, content + "\n")
def mark_processed(filepath: Path):
def mark_processed(ustht: Path, filepath: Path):
"""Insert the processed marker at the top of a raw file."""
content = filepath.read_text(encoding="utf-8")
content = safe_read_text(ustht, filepath)
if content.split("\n", 1)[0].strip() != "<!-- processed -->":
filepath.write_text("<!-- processed -->\n" + content, encoding="utf-8")
safe_write_text(ustht, filepath, "<!-- processed -->\n" + content)
def update_index(mdbase: Path):
def update_index(ustht: Path, mdbase: Path):
"""Rebuild mdbase/README.ai.md with dimension counts."""
now = datetime.now().strftime("%Y-%m-%d %H:%M")
details = mdbase / "details"
dims = []
if details.exists():
dims = sorted(p.relative_to(details).with_suffix("").as_posix() for p in details.rglob("*.md"))
dims = sorted(p.relative_to(details).with_suffix("").as_posix() for p in safe_markdown_tree(ustht, details))
rows = ["| File | Dimension | Entries |", "|------|-----------|---------|"]
backlog = mdbase / "backlog.md"
if backlog.exists():
rows.append(f"| [backlog.md](backlog.md) | backlog | {count_entries(backlog)} |")
rows.append(f"| [backlog.md](backlog.md) | backlog | {count_entries(ustht, backlog)} |")
for dim in dims:
path = details / f"{dim}.md"
rows.append(f"| [details/{dim}.md](details/{dim}.md) | {dim} | {count_entries(path)} |")
rows.append(f"| [details/{dim}.md](details/{dim}.md) | {dim} | {count_entries(ustht, path)} |")
content = "\n".join([
"# user-thoughts mdbase Index",
@@ -137,7 +148,7 @@ def update_index(mdbase: Path):
*rows,
"",
])
(mdbase / "README.ai.md").write_text(content, encoding="utf-8")
safe_write_text(ustht, mdbase / "README.ai.md", content)
def main():
@@ -161,7 +172,7 @@ def main():
print("No unprocessed records.")
return
raw_files = [f for f in sorted(raw_dir.glob("*.md")) if not is_processed(f)]
raw_files = [f for f in safe_markdown_files(ustht, raw_dir) if not is_processed(f, ustht)]
if not raw_files:
print("No unprocessed records. All raw files are marked processed.")
return
@@ -169,7 +180,7 @@ def main():
all_entries = []
entries_by_file = {}
for f in raw_files:
entries = parse_raw_file(f)
entries = parse_raw_file(ustht, f)
entries_by_file[f] = entries
all_entries.extend(entries)
@@ -194,16 +205,16 @@ def main():
return
for dim, entries in grouped.items():
append_entries(dim_path(mdbase, dim), entries)
append_entries(ustht, dim_path(mdbase, dim), entries)
for f in raw_files:
if entries_by_file.get(f):
mark_processed(f)
mark_processed(ustht, f)
now = datetime.now().strftime("%Y-%m-%d %H:%M")
cfg["LAST_SORTIN"] = now
write_define_ini(ustht, cfg)
update_index(mdbase)
update_index(ustht, mdbase)
print(f" LAST_SORTIN updated to {now}")
@@ -2,7 +2,7 @@
import sys
from pathlib import Path
from common import find_ustht, read_define_ini, is_processed
from common import find_ustht, read_define_ini, is_processed, safe_markdown_files, safe_markdown_tree
HELP = """Usage: python status.py [--help]
@@ -11,21 +11,21 @@ dimension counts.
"""
def count_raw(raw_dir: Path):
def count_raw(ustht: Path, raw_dir: Path):
"""Return total and unprocessed raw file counts."""
if not raw_dir.exists():
return 0, 0
files = list(raw_dir.glob("*.md"))
unprocessed = sum(1 for f in files if not is_processed(f))
files = safe_markdown_files(ustht, raw_dir)
unprocessed = sum(1 for f in files if not is_processed(f, ustht))
return len(files), unprocessed
def count_dims(mdbase: Path):
def count_dims(ustht: Path, mdbase: Path):
"""Count dimension files under mdbase/details/."""
details = mdbase / "details"
if not details.exists():
return 0
return len(list(details.rglob("*.md")))
return len(safe_markdown_tree(ustht, details))
def main():
@@ -42,8 +42,8 @@ def main():
skill_status = cfg.get("SKILL_STATUS", "unknown")
instant_status = cfg.get("INSTANT_STATUS", "unknown")
last_sortin = cfg.get("LAST_SORTIN", "never") or "never"
total_raw, unprocessed_raw = count_raw(ustht / "raw")
dims = count_dims(ustht / "mdbase")
total_raw, unprocessed_raw = count_raw(ustht, ustht / "raw")
dims = count_dims(ustht, ustht / "mdbase")
print(f"SKILL_STATUS={skill_status}")
print(f"INSTANT_STATUS={instant_status}")
@@ -3,7 +3,15 @@ import sys
from datetime import datetime
from pathlib import Path
from common import find_ustht, read_define_ini, validate_dim_name
from common import (
ensure_runtime_dir,
safe_markdown_files,
safe_read_text,
safe_write_text,
find_ustht,
read_define_ini,
validate_dim_name,
)
HELP = """Usage: python write_raw.py "thought text" [--dim dimension] [--help]
@@ -21,12 +29,14 @@ Behavior:
"""
def count_today_raw(raw_dir: Path) -> int:
def count_today_raw(ustht: Path, raw_dir: Path) -> int:
"""Count unprocessed entries across today's raw files."""
today = datetime.now().strftime("%Y-%m-%d")
count = 0
for f in sorted(raw_dir.glob(f"{today}*.md")):
content = f.read_text(encoding="utf-8")
for f in safe_markdown_files(ustht, raw_dir):
if not f.name.startswith(today):
continue
content = safe_read_text(ustht, f)
first_line = content.split("\n", 1)[0].strip()
if first_line == "<!-- processed -->":
continue
@@ -72,15 +82,14 @@ def main():
print(f"Invalid dimension name: {dim}. Use lowercase letters, digits, hyphens, and optional / subdirectories.")
sys.exit(1)
raw_dir = ustht / "raw"
raw_dir.mkdir(exist_ok=True)
raw_dir = ensure_runtime_dir(ustht, ustht / "raw", create=True)
today = datetime.now().strftime("%Y-%m-%d")
now = datetime.now().strftime("%H:%M")
raw_file = raw_dir / f"{today}.md"
if raw_file.exists():
first_line = raw_file.read_text(encoding="utf-8").split("\n", 1)[0].strip()
first_line = safe_read_text(ustht, raw_file).split("\n", 1)[0].strip()
if first_line == "<!-- processed -->":
seq = 2
while (raw_dir / f"{today}-{seq}.md").exists():
@@ -92,12 +101,12 @@ def main():
entry = f"- [{now}] {thought_clean}{suffix}"
if raw_file.exists():
content = raw_file.read_text(encoding="utf-8").rstrip()
raw_file.write_text(f"{content}\n{entry}\n", encoding="utf-8")
content = safe_read_text(ustht, raw_file).rstrip()
safe_write_text(ustht, raw_file, f"{content}\n{entry}\n")
else:
raw_file.write_text(f"{entry}\n", encoding="utf-8")
safe_write_text(ustht, raw_file, f"{entry}\n")
count = count_today_raw(raw_dir)
count = count_today_raw(ustht, raw_dir)
if count > 5:
print(f"Today has {count} recorded thoughts. Consider running /ustht sortin.")
@@ -20,21 +20,22 @@ Before running any Vercel CLI commands, identify where the token is coming from.
### A) `VERCEL_TOKEN` is already set in the environment
```bash
printenv VERCEL_TOKEN
[ -n "${VERCEL_TOKEN:-}" ] && printf 'VERCEL_TOKEN is set\n'
```
If this returns a value, you're ready. Skip to Step 2.
If this reports a configured token, you're ready. Skip to Step 2.
### B) Token is in a `.env` file under `VERCEL_TOKEN`
```bash
grep '^VERCEL_TOKEN=' .env 2>/dev/null
grep -q '^VERCEL_TOKEN=' .env 2>/dev/null && printf 'VERCEL_TOKEN is present in .env\n'
```
If found, export it:
```bash
export VERCEL_TOKEN=$(grep '^VERCEL_TOKEN=' .env | cut -d= -f2-)
VERCEL_TOKEN="$(sed -n 's/^VERCEL_TOKEN=//p' .env | tail -n 1)"
export VERCEL_TOKEN
```
### C) Token is in a `.env` file under a different name
@@ -42,13 +43,15 @@ export VERCEL_TOKEN=$(grep '^VERCEL_TOKEN=' .env | cut -d= -f2-)
Look for any variable that looks like a Vercel token (Vercel tokens typically start with `vca_`):
```bash
grep -i 'vercel' .env 2>/dev/null
grep -Eio '^[A-Z0-9_]*VERCEL[A-Z0-9_]*(?==)' .env 2>/dev/null
```
Inspect the output to identify which variable holds the token, then export it as `VERCEL_TOKEN`:
```bash
export VERCEL_TOKEN=$(grep '^<VARIABLE_NAME>=' .env | cut -d= -f2-)
vercel_var="<VARIABLE_NAME>"
VERCEL_TOKEN="$(sed -n "s/^${vercel_var}=//p" .env | tail -n 1)"
export VERCEL_TOKEN
```
### D) No token found — ask the user
@@ -64,7 +67,7 @@ If none of the above yield a token, ask the user to provide one. They can create
vercel deploy --token "vca_abc123"
# Good — CLI reads VERCEL_TOKEN from the environment
export VERCEL_TOKEN="vca_abc123"
[ -n "${VERCEL_TOKEN:-}" ] || { echo "Set VERCEL_TOKEN first" >&2; exit 1; }
vercel deploy
```
@@ -74,11 +77,11 @@ Similarly, check for the project ID and team scope. These let the CLI target the
```bash
# Check environment
printenv VERCEL_PROJECT_ID
printenv VERCEL_ORG_ID
[ -n "${VERCEL_PROJECT_ID:-}" ] && printf 'VERCEL_PROJECT_ID is set\n'
[ -n "${VERCEL_ORG_ID:-}" ] && printf 'VERCEL_ORG_ID is set\n'
# Or check .env
grep -i 'vercel' .env 2>/dev/null
grep -Eio '^[A-Z0-9_]*VERCEL[A-Z0-9_]*(?==)' .env 2>/dev/null
```
**If you have a project URL** (e.g. `https://vercel.com/my-team/my-project`), extract the team slug:
@@ -317,8 +320,8 @@ Full details: https://vercel.com/docs/plans/pro-plan
Check the environment and any `.env` files present:
```bash
printenv | grep -i vercel
grep -i vercel .env 2>/dev/null
env | grep -Eio '^[A-Z0-9_]*VERCEL[A-Z0-9_]*(?==)'
grep -Eio '^[A-Z0-9_]*VERCEL[A-Z0-9_]*(?==)' .env 2>/dev/null
```
### Authentication error
@@ -0,0 +1,103 @@
---
name: video-content-extractor
description: "Extract key frames from MP4 videos at configurable intervals, run Tesseract OCR, and generate structured Markdown reports with video metadata and timestamped text transcripts."
category: media-processing
risk: safe
source: community
source_repo: 274326424/video-content-extractor
source_type: community
date_added: "2026-06-06"
author: 274326424
tags: [video, ocr, ffmpeg, tesseract, frame-extraction, media]
tools: [codex]
---
# Video Content Extractor
## Overview
Automatically extracts key frames from MP4 video files at configurable time intervals, performs OCR text recognition on each frame, and generates a structured Markdown report. The report includes video metadata (duration, resolution, codecs) and frame-by-frame OCR transcripts with timestamp references.
This skill is designed for Codex CLI and requires FFmpeg and Tesseract OCR installed on the local machine.
## When to Use This Skill
- Use when you need to extract text content from video presentations, lectures, or screencasts.
- Use when you want to create searchable transcripts from video files without embedded subtitles.
- Use when you need to analyze video content programmatically and generate structured summaries.
- Use when the user asks to "read what is on screen" or "extract the content from this video."
## How It Works
### Step 1: Analyze Video Metadata
The skill uses ffprobe to extract video metadata: duration, resolution, frame rate, codec information, and file size.
### Step 2: Extract Key Frames
Using FFmpeg, the skill captures frames at the configured interval (default: every 30 seconds). Each frame is saved as a timestamped JPEG image.
### Step 3: OCR Text Recognition
Each extracted frame is processed by Tesseract OCR. If the default PSM mode returns no meaningful text, it falls back to fully automatic page segmentation.
### Step 4: Generate Markdown Report
All extracted data is assembled into a structured Markdown document.
## Examples
### Example 1: Basic Extraction
Agent prompt:
Use the video-content-extractor skill to extract content from lecture.mp4
Output generates lecture.md and lecture_frames/ directory.
### Example 2: Custom Interval
Parameters: video_path, output_dir, interval(seconds), lang
Extract every 60 seconds with English-only OCR:
python scripts/extract_video.py recording.mp4 ./output 60 eng
### Example 3: Bilingual Content
Extract with default Chinese + English OCR:
python scripts/extract_video.py lecture.mp4 . 15 chi_sim+eng
## Best Practices
- Use shorter intervals (10-15s) for fast-paced content with frequent text changes.
- Use longer intervals (30-60s) for presentation slides or slow lectures to reduce duplicate frames.
- For Chinese content, ensure Tesseract Chinese language pack is installed (chi_sim).
## Limitations
- Requires FFmpeg and Tesseract OCR to be installed and accessible via PATH.
- Tesseract OCR accuracy depends on video quality, text size, and font clarity.
- Does not extract audio or perform speech-to-text transcription.
- Frame extraction is time-based (not scene-change-based), which may produce near-duplicate frames.
- Large videos with short intervals can generate many frames - ensure sufficient disk space.
## Security and Safety Notes
- This skill only reads video files and writes extracted frames and Markdown reports.
- It does NOT send any data over the network - all processing is local.
- FFmpeg and Tesseract are invoked with fixed, pre-vetted arguments.
- The skill does not modify or delete the original video file.
## Common Pitfalls
- Problem: Tesseract returns garbled text
Solution: Ensure the correct language pack is installed. Run tesseract --list-langs to verify.
- Problem: FFmpeg fails with "not found"
Solution: Make sure FFmpeg is on PATH. Run ffmpeg -version to verify.
- Problem: OCR is slow on large videos
Solution: Increase the interval parameter to reduce frames processed.
## Related Skills
- @media-summarizer - For summarizing video content using visual and audio cues.
- @document-ocr - For OCR on static images or scanned documents without video processing.