📦 deps(thirdparty): update snapshots
This commit is contained in:
+12
-2
@@ -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 |
|
||||
|
||||
+1
@@ -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",
|
||||
|
||||
+4
-1
@@ -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>"]
|
||||
|
||||
+159
@@ -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)
|
||||
+874
@@ -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)
|
||||
```
|
||||
+8
@@ -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 |
|
||||
|
||||
+2
-2
@@ -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)
|
||||
|
||||
+101
@@ -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.
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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
|
||||
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
---
|
||||
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
|
||||
source: community
|
||||
source_repo: yehudalevy-collab/polis-protocol
|
||||
source_type: community
|
||||
date_added: "2026-06-02"
|
||||
author: yehudalevy-collab
|
||||
tags: [multi-agent, coordination, routing, orchestration, governance, vendor-agnostic]
|
||||
tools: [claude, cursor, gemini, codex, antigravity]
|
||||
license: "MIT"
|
||||
license_source: "https://github.com/yehudalevy-collab/polis-protocol/blob/main/LICENSE"
|
||||
---
|
||||
|
||||
# Polis Protocol — a team of agents that develops
|
||||
|
||||
## Overview
|
||||
|
||||
Most agent coordination is a passive board: claim a task, do it, mark it done. It records, but it never gets smarter, and its rules are frozen. Polis Protocol is the active alternative — a folder of markdown where each agent is a "citizen" with a capability card, work is routed by a learning bandit to whoever has the best track record on the task's tags, settled work files lessons that update the routing, and citizens can propose and vote on amendments to the protocol itself. It is vendor-agnostic: Antigravity, Claude, Codex, and Gemini agents can all share one `_polis/`.
|
||||
|
||||
In Antigravity specifically, this turns Manager View's fixed pipeline into a team that learns who is actually best at each kind of work, instead of running the same roles in the same order every time.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Use when 2+ agents (especially across vendors) work on one project and "who should do this" is a real question.
|
||||
- Use when you want the team to measurably improve over time — routing that adapts from outcomes, not static role labels.
|
||||
- Use when you need a durable, git-auditable record of who did what, what was learned, and which rules changed.
|
||||
- Use when Antigravity's default orchestration is too rigid and you want routing + governance on top of it.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Step 1: Found a polis
|
||||
|
||||
Clone 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 \
|
||||
--project-root . \
|
||||
--agent-id gemini-antigravity-yourproject \
|
||||
--vendor google --model gemini-3 --tool antigravity
|
||||
```
|
||||
|
||||
This writes `_polis/` plus the skill into `.antigravity/skills/`, and bridge pointers (`GEMINI.md`, `AGENTS.md`) that point every tool at `_polis/CONSTITUTION.md`. Tip: add `--dry-run` to preview every file before anything is written.
|
||||
|
||||
### Step 2: Register citizens and open contracts
|
||||
|
||||
Each agent publishes a capability card under `_polis/citizens/`. Work is opened as a contract with `required_tags`, not assigned to a fixed role.
|
||||
|
||||
### Step 3: Route by track record
|
||||
|
||||
```bash
|
||||
python3 polis-protocol/scripts/route_contract.py --polis-root _polis \
|
||||
--contract _polis/contracts/open/your-task.md --explain
|
||||
```
|
||||
|
||||
The router prints a score breakdown (history / self-rating / cost / availability) and recommends the citizen with the strongest record on the task's tags.
|
||||
|
||||
### Step 4: Settle, learn, and amend
|
||||
|
||||
A settled contract files a lesson; `--reconcile` folds it into `routing_stats.yml` so the next similar task routes better. When a rule stops working, a citizen proposes an amendment and the others vote.
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: See the team learn (no install, 30 seconds)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/yehudalevy-collab/polis-protocol.git
|
||||
cd polis-protocol && 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.
|
||||
|
||||
### Example 2: Explain any routing decision
|
||||
|
||||
```bash
|
||||
python3 scripts/route_contract.py --polis-root examples/research-team/_polis \
|
||||
--contract examples/research-team/_polis/contracts/open/parent-newsletter-issue-3.md --explain
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- No server, no runtime, no database — the whole protocol is markdown plus two small Python scripts.
|
||||
- Vendor-agnostic by design; a Claude or Codex agent can join the same polis an Antigravity agent created.
|
||||
- Full Antigravity integration guide: https://github.com/yehudalevy-collab/polis-protocol/blob/main/docs/antigravity.md
|
||||
|
||||
## Limitations
|
||||
|
||||
- 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.
|
||||
+140
@@ -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
|
||||
+2
-1
@@ -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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+4
-2
@@ -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
|
||||
|
||||
+93
-6
@@ -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"}
|
||||
|
||||
+20
-20
@@ -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
-1
@@ -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)
|
||||
|
||||
|
||||
+5
-5
@@ -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")
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+37
-26
@@ -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}")
|
||||
|
||||
|
||||
|
||||
+8
-8
@@ -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}")
|
||||
|
||||
+20
-11
@@ -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.")
|
||||
|
||||
|
||||
+15
-12
@@ -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
|
||||
|
||||
+103
@@ -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