📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -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
|
||||
+1
-1
@@ -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**: 3–5 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
|
||||
- 40–60% 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 40–60% of canvas, 35%+ empty space |
|
||||
| Annotations | Handwritten English, 2–5 words each, max 5–8 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 0–100 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 (3–8 years). Let me know if this should be different."
|
||||
|
||||
| Level | Years | CV emphasis |
|
||||
|-------|-------|-------------|
|
||||
| Student / fresh graduate | 0–1 | Education first; projects; extracurriculars; 1 page |
|
||||
| Junior / entry | 1–3 | Skills + education prominent; 1 page |
|
||||
| Mid-level | 3–8 | Experience leads; achievements over duties; 1–2 pages |
|
||||
| Senior | 8–15 | Leadership, scope, impact, mentoring; 2 pages |
|
||||
| Executive / C-suite | 15+ | Strategic narrative; board roles; P&L; 2–3 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 → 2–4 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 2–3 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")
|
||||
- 3–6 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 3–12 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 3–5 sentences (executive: 5–7) 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 30–40% 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:
|
||||
- 3–6 bullets per role (2–3 for short-tenure or early roles)
|
||||
- Past tense for completed roles; present tense for current role
|
||||
- 15–30 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, 12–14pt 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 10–20 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: 2–4 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
|
||||
[3–5 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] — [YYYY–YYYY]
|
||||
- [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 ---
|
||||
[3–5 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 0–100)
|
||||
|
||||
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 (3–7, 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 2–3 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, 250–350 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 | 1–2 pages | No | No | No | "Available on request" |
|
||||
| Canada | 1–2 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 | 2–3 pages | No | No | No | "Available on request" |
|
||||
| Germany / Austria / Switzerland | 2–3 pages | Yes (expected) | Yes | Sometimes | Listed or on request |
|
||||
| France | 1–2 pages | Optional | No (illegal to require) | No | On request |
|
||||
| Netherlands / Scandinavia | 1–2 pages | Optional | No | No | On request |
|
||||
| Japan | 1–2 pages (rirekisho) | Yes | Yes | Yes | Listed |
|
||||
| South Korea | 1–2 pages | Yes | Yes | Yes | Listed |
|
||||
| China | 1–2 pages | Yes | Yes | Yes | Listed |
|
||||
| India | 2–3 pages | Optional | Yes (common) | Sometimes | Listed |
|
||||
| Nepal | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||
| Bangladesh / Sri Lanka | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||
| UAE / Gulf (GCC) | 2–3 pages | Yes (common) | Yes | Yes (sometimes) | Listed |
|
||||
| Nigeria / East Africa | 2–3 pages | Yes (common) | Yes | Sometimes | Listed |
|
||||
| South Africa | 3–5 pages | Optional | Yes (common) | No | Listed |
|
||||
| Brazil | 1–2 pages | Optional | Yes (common) | No | On request |
|
||||
| Academic (global) | No limit | Varies | Varies | No | Full list required |
|
||||
| Executive / board (global) | 2–3 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 (Q1–Q20, 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 7–8) |
|
||||
|<-------------------+
|
||||
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.
|
||||
Reference in New Issue
Block a user