cherryhq/cherry-studio/office-transform

Claude Code and agent skill by cherryhq/cherry-studio

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Extract or edit a selection from xlsx, docx, pdf, or pptx into a new file; the source is never modified.

Install

npx vskill@latest install cherryhq/cherry-studio/office-transform

SKILL.md

Office Transform

Turn a structural selection of an Office/PDF document into a new derived file.

Two invariants hold for every operation:

  1. The source file is read-only. Every result is a new file; both scripts refuse to

write to the source path or overwrite an existing file. Never work around this with ad-hoc shell edits to the original.

  1. Anchors are structural. Selections address the document's own coordinates —

worksheet + A1 range, body-level paragraph ordinal, one-based page number — never screen or DOM positions.

When to use which tool

  • Only need to read a document to summarize or answer questions → use

mcp__cherry-tools__to_markdown instead (see the cherry-tool-guide skill); it is lossy but built for reading.

  • The user wants a file out — an extracted range, a converted fragment, an edited

copy → this skill.

Input: selection references

Chat surfaces may hand you a fenced selection-ref block:

selection-ref
{"path": "/abs/report.xlsx", "anchor": {"format": "xlsx", "sheet": "Sheet1", "range": "A1:C10"}, "excerpt": "…", "fileStamp": {"size": 1024, "mtimeMs": 1700000000000}}

The anchor object is exactly what the scripts take via --anchor.

A block may also carry comment: what the user wants done at that location, in their own words. Treat it as the user's instruction for that anchor. It is not part of the anchor, so never pass it to a script.

Freshness check. fileStamp.mtimeMs is milliseconds since the Unix epoch, floored to whole milliseconds. Do not compare it to stat's default output: stat -f %m (macOS) and stat -c %Y (GNU) give whole seconds, so multiplying by 1000 loses the sub-second part and never matches. Read whole milliseconds the way the app wrote them:

bash
uv run python -c "import os,sys;st=os.stat(sys.argv[1]);print(st.st_size, st.st_mtime_ns//1_000_000)" '/abs/report.xlsx'

Treat the file as changed when the size differs, or when the mtimes differ by more than 2 ms. The tolerance is small on purpose: both sides floor the same nanosecond timestamp to milliseconds, so they agree exactly, and the couple of milliseconds only covers the rounding of a double that can no longer represent current epoch milliseconds exactly. A wider window would hide real edits — a same-size change made within it would read as unchanged, which is why the anchor check below is not optional. On a change, tell the user the file changed since they selected and ask them to re-select — never silently re-anchor.

Anchor check. Before a patch-copy edit, also verify the anchor still points where the user thinks: extract the anchored region and compare its text with the reference's excerpt, normalizing both sides the same way (NFC, collapse each whitespace run to one space, trim). The two are not equal and are not meant to be — the excerpt is what the user selected, while the extract is the whole region an edit would replace. A selection sitting inside one region makes the excerpt the shorter side; a selection running past that region anchors to where it starts, which makes it the longer one. So the test is containment, whichever way round fits: the excerpt appears inside the extract, or the extract's tail is where the excerpt begins. Only when neither holds has the anchor stopped pointing at the selection — then stop and tell the user, never edit at a mismatched anchor and never go searching for a "close enough" location. Two cases need care because the two sides are not directly comparable as-is:

  • xlsx: the excerpt is tab-separated cells and newline-separated rows, while extraction

writes csv or md. Compare cell values, not the raw file — read the csv with a csv reader and join the fields with single spaces before normalizing, so , and | never count as a difference. That join drops the cell boundaries on both sides, so a match proves the text is unchanged, not the grid — the same words re-split across cells still reads as a match. The freshness check above is what catches that; never skip it on a passing anchor check. Number formats are the second asymmetry: the excerpt holds what Excel displays, because that is what the user pointed at (1,234.50, 45.67%), while extraction writes the stored value (1234.5, 0.4567). Neither side is wrong, so a formatted cell never matches literally. Compare those cells by value, and leave them out of the joined strings so the containment test runs on the text cells alone. Dates, times and durations are the same asymmetry seen from the other side: extraction writes one fixed shape (2024-01-03, 2024-01-03 15:04:05, 26:30:00) rather than the cell's format, so treat a cell that differs only in date or time presentation as a format difference too. A difference that is only the number format is not a moved anchor, and stopping on one sends the user back to re-select a selection that never moved.

  • docx with `charRange`: the slice is only part of what patch-copy compares and replaces —

it rewrites the whole paragraph. Extract the paragraph without charRange as well, and read "Edit docx" below before writing.

Users may also describe the region in words ("sheet 2, columns A through C"); build the anchor JSON yourself, confirming the worksheet name or paragraph if ambiguous.

Anchor shapes:

FormatAnchor
xlsx{"format":"xlsx","sheet":"Sheet1","range":"A1:C10"} (range may be one cell)
docx{"format":"docx","paragraph":3,"paraId":"502E8D33","charRange":[0,12]} (paraId optional = the paragraph's w14:paraId, resolved first when present; charRange optional; ordinal counts body-level paragraphs only, tables excluded)
pdf{"format":"pdf","page":3,"charRange":[0,120]} (charRange optional, applies to extracted text)
pptx{"format":"pptx","slide":2,"nodeId":"4","paragraph":0} or {"format":"pptx","slide":2,"nodeId":"7","tableCell":{"row":1,"col":0}} (slide is one-based; nodeId is the OOXML shape id — omit for the whole slide; paragraph and tableCell are optional, mutually exclusive, and only valid together with nodeId)

Operations

Scripts live in this skill's scripts/ directory; resolve paths relative to this skill folder. The library-edit recipes routed to below live in references/ beside them. Python dependencies are per-format and provided at invocation time via uv run --with <pkg> (the bundled-shell idiom — do not pip install globally).

Which route an edit takes follows from the library that can write the format: openpyxl drops charts and drawings on a round-trip, which is why xlsx edits go through patch-copy, while python-pptx keeps XML it does not understand, which is why pptx edits go through the library.

Extract — pull the anchored region into a new file

Always single-quote paths — real documents have spaces in their names (Q1 report.xlsx). If a path itself contains a single quote, close and reopen the quoting around it: '/abs/Bob'\''s deck.pptx'.

bash
uv run --with openpyxl python scripts/office_extract.py \
--file '/abs/report.xlsx' \
--anchor '{"format":"xlsx","sheet":"Sheet1","range":"A1:C10"}' \
--out '/abs/report-q1-range.csv'

The output format is inferred from --out's extension:

SourceDependency (--with)Output formats
xlsxopenpyxlxlsx, csv, md
docx'python-docx>=1.1,<2'docx, txt, md
pdfpypdfpdf (page copy), txt, md
pptxpython-pptxtxt, md (slide, shape, paragraph, or table-cell text)

The docx pin is not optional. Patch-copy's expectText gate compares a paragraph read with python-docx against the same paragraph read by the script's own paragraph_text, which reproduces python-docx's Paragraph.text element for element. That equivalence was checked against 1.x; a release that changes what .text spells would make the gate refuse paragraphs nobody edited. Quote the specifier — > and < are redirects to a shell.

xlsx extraction reads computed values (data_only), so formula cells yield their last saved result. docx extraction to docx carries text only, not run styling.

Patch-copy — derive an edited copy, standard library only
bash
uv run python scripts/office_patch_copy.py \
--file '/abs/report.xlsx' \
--edits '{"format":"xlsx","sheet":"Sheet1","cells":{"B2":42,"C3":"hello"}}' \
--out '/abs/report-updated.xlsx'

OOXML packages are ZIPs of XML parts. Patch-copy copies every part byte-for-byte and re-serializes only what an edit reaches — the target worksheet or word/document.xml, plus the workbook bookkeeping noted below for xlsx — so styles, charts, images, and macros in untouched parts survive exactly. Edit shapes:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 93 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
52,248
Repository forks
5,025
7-day trend score
54

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/cherryhq/cherry-studio/office-transform/badge)](https://verified-skill.com/skills/cherryhq/cherry-studio/office-transform)

Something wrong with this skill or its page? Tell us.