| name | office-install |
| description | Environment setup for office-skills. When Claude needs to verify or install prerequisites before running any office skill workflow — detects platform, checks what's present, acquires what's missing to a local tools/ directory, resolves npm dependencies, and writes a paths.json that other skills read. |
Office Skills — Environment Setup
This skill installs the tools required by the office-skills repository into a single local directory, resolves npm dependencies, and writes a paths.json that other skills use to locate them. Follow it sequentially — each phase gates the next. Skip phases where all checks already pass.
Do not run this proactively. Only follow this workflow when:
- A script fails because a tool is missing
- The user explicitly asks to set up the environment
- You're operating in a new environment for the first time
Non-interactive execution
All command line calls must use non-interactive flags. The user interacts with the agent, not the terminal. This means:
- Use
-y, --yes, -f, --force, --no-pager, and non-interactive equivalents whenever available
- Never use
git commit without -m (always provide the message)
- Never use interactive
cp, mv, or rm confirmations — use -f or equivalent
- Pipe
yes | to commands that stubbornly require confirmation when no flag exists
- Always redirect output appropriately; if you need to see output, capture it and surface it to the user
The user should never see a prompt hang waiting for their input. If a command blocks, you have failed this requirement.
Tool home
All tools are installed under:
.agents/skills/office-install/tools/
If this repo is used globally (e.g. from ~/.agents/skills/), that path is ~/.agents/skills/office-install/tools/. The install script resolves the correct root automatically.
After installation, a paths.json is written to the same tools/ directory. Other skills read this file to locate uv, deno, soffice, and the node_modules directory without assuming they are on PATH.
Two install scenarios
This skill works identically in two layouts:
| Project | Global |
|---|
| Skill root | {project}/.agents/skills/office-install/ | ~/.agents/skills/office-install/ |
deno.json / deno.lock | Copied from resources/ to project root | Adapted version from resources/ in skill root |
node_modules/ | {project_root}/node_modules/ | {skill_root}/node_modules/ |
| Deno invocation | Bare deno from project root | officedeno wrapper script |
Phase 0 determines which scenario applies. All later phases branch on it.
Phase 0: Detect environment
Run these checks and record the results. Every later decision branches on them.
Legacy skill detection
Check for old, deprecated individual office skills (pptx, docx, pdf, xlsx) in the standard skills paths. If found, ask the user if they want to uninstall them and replace them with the office-skills versions.
macOS / Linux:
for skill in pptx docx pdf xlsx; do
if [ -d "$HOME/.agents/skills/$skill" ] || [ -d ".agents/skills/$skill" ]; then
echo "Legacy skill found: $skill"
fi
done
Windows (PowerShell):
$legacySkills = @("pptx", "docx", "pdf", "xlsx")
$paths = @(
(Join-Path $env:USERPROFILE ".agents\skills"),
(Join-Path $PWD ".agents\skills")
)
foreach ($path in $paths) {
foreach ($skill in $legacySkills) {
$skillPath = Join-Path $path $skill
if (Test-Path $skillPath) {
Write-Host "Legacy skill found: $skill at $skillPath"
}
}
}
If legacy skills are detected, ask the user: "I've detected old versions of Office skills (e.g., pptx, docx). Would you like to uninstall them and replace them with the new office-skills suite?" If yes, delete the folders before proceeding to Phase 1.
Platform detection
macOS / Linux:
uname -s 2>/dev/null || echo WINDOWS
uname -m 2>/dev/null
echo "$SHELL"
Windows (PowerShell):
[System.Runtime.InteropServices.RuntimeInformation]::OSDescription
[Environment]::Is64BitOperatingSystem
Record:
- Platform: macOS / Linux / Windows
- Architecture: arm64 / x86_64
- Has admin/elevation: yes / no / unknown
If on Windows and the user hasn't stated whether elevation is available, ask before proceeding. Do not assume.
Skill-root resolution
Determine whether the skill is installed project-local or globally:
SKILL_ROOT=""
for candidate in \
".agents/skills/office-install" \
"$HOME/.agents/skills/office-install"; do
if [ -f "$candidate/SKILL.md" ]; then
SKILL_ROOT="$(cd "$candidate" && pwd)"
break
fi
done
IS_GLOBAL=false
if [[ "$SKILL_ROOT" == "$HOME/"* ]]; then
IS_GLOBAL=true
fi
Windows (PowerShell):
$skillRoot = ""
$isGlobal = $false
$projectPath = Join-Path $PWD ".agents\skills\office-install\SKILL.md"
$globalPath = Join-Path $env:USERPROFILE ".agents\skills\office-install\SKILL.md"
if (Test-Path $projectPath) {
$skillRoot = Join-Path $PWD ".agents\skills\office-install"
} elseif (Test-Path $globalPath) {
$skillRoot = Join-Path $env:USERPROFILE ".agents\skills\office-install"
$isGlobal = $true
}
Record:
- SKILL_ROOT: absolute path to
office-install/ directory
- IS_GLOBAL:
true or false
Phase 1: Python runtime — uv
uv is the single prerequisite for all Python-based skills. It bundles its own Python — no system Python needed.
Check
if [ -f "$SKILL_ROOT/tools/uv" ] || [ -f "$SKILL_ROOT/tools/uv.exe" ]; then
echo "uv found in tools/"
elif command -v uv &>/dev/null; then
echo "uv on PATH"
fi
If uv is already in tools/ or on PATH (version 0.4+), skip to Phase 2.
Corporate network / TLS certificate support
uv does not use the system certificate store by default. On corporate networks that proxy TLS with a self-signed certificate, uv will fail with certificate errors.
Set this environment variable to tell uv to use the system native TLS certificates:
export UV_NATIVE_TLS=1
On Windows (PowerShell profile):
[System.Environment]::SetEnvironmentVariable('UV_NATIVE_TLS', '1', 'User')
If that doesn't resolve it, use:
export UV_CERT=/path/to/corporate-ca.pem
uv --cert /path/to/corporate-ca.pem ...
Always set UV_NATIVE_TLS=1 before running uv in this skill. This uses the system certificate store which on corporate machines already has the corporate root certificate.
Install to tools/
macOS / Linux:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
curl -LsSf https://astral.sh/uv/install.sh | DENO_INSTALL=no UV_INSTALL_DIR="$TOOLS_DIR" sh -s -- --no-modify-path
Windows — elevation available:
$toolsDir = Join-Path $skillRoot "tools"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
winget install astral-sh.uv --location $toolsDir
Windows — no elevation:
$toolsDir = Join-Path $skillRoot "tools"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
$arch = if ([Environment]::Is64BitOperatingSystem) { "x86_64" } else { "i686" }
$uvVersion = "0.7.12"
$url = "https://github.com/astral-sh/uv/releases/download/$uvVersion/uv-$arch-pc-windows-msvc.zip"
$zip = Join-Path $toolsDir "uv.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest $url -OutFile $zip
Expand-Archive $zip $toolsDir -Force
Remove-Item $zip
Verify
"$SKILL_ROOT/tools/uv" --version
If uv was already on PATH and not in tools/, copy it:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
cp "$(command -v uv)" "$TOOLS_DIR/"
Phase 2: JavaScript runtime — deno
Required only for the pptx html2pptx workflow. If the user is only working with DOCX, PDF, or XLSX, skip this phase entirely.
Check
deno --version
If this succeeds (any version 2.0+), copy it to tools/ and skip the download:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
cp "$(command -v deno)" "$TOOLS_DIR/"
Install to tools/
macOS / Linux:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
curl -fsSL https://deno.land/install.sh | DENO_INSTALL="$TOOLS_DIR" sh
Windows — elevation available:
$toolsDir = Join-Path $skillRoot "tools"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
winget install DenoLand.Deno --location $toolsDir
Windows — no elevation:
$toolsDir = Join-Path $skillRoot "tools"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
$denoVersion = "2.3.2"
$url = "https://github.com/denoland/deno/releases/download/v$denoVersion/deno-x86_64-pc-windows-msvc.zip"
$zip = Join-Path $toolsDir "deno.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest $url -OutFile $zip
Expand-Archive $zip $toolsDir -Force
Remove-Item $zip
Corporate network / TLS certificate support
Deno does not use the system certificate store by default. On corporate networks that proxy TLS with a self-signed certificate, Deno will fail with certificate errors.
Set this environment variable to tell Deno to read the system CA store:
export DENO_TLS_CA_STORE=system
On Windows (PowerShell profile):
[System.Environment]::SetEnvironmentVariable('DENO_TLS_CA_STORE', 'system', 'User')
If that doesn't resolve it, use:
export DENO_CERT=/path/to/corporate-ca.pem
deno --cert /path/to/corporate-ca.pem ...
Always set DENO_TLS_CA_STORE=system before running deno in this skill.
Verify
macOS / Linux:
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" eval "console.log('deno works')"
Windows (PowerShell):
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" eval "console.log('deno works')"
Phase 3: Deno npm dependencies + Playwright browser
Required only for the pptx html2pptx workflow and pdf JavaScript libraries (same gate as Phase 2). If the user is only working with DOCX or XLSX text operations, skip this phase entirely.
What this installs
The following npm packages are declared in deno.json (shipped at $SKILL_ROOT/resources/deno.json) and required by office skill scripts:
| Package | Purpose |
|---|
| playwright | Headless browser for HTML rendering |
| sharp | Image processing (SVG→PNG rasterization, has platform-specific native binaries) |
| pptxgenjs | PowerPoint file generation |
| react | React runtime for icon rendering |
| react-dom | Server-side React rendering |
| react-icons | Icon component library |
| pdf-lib | PDF creation and modification |
| pdfjs-dist | PDF text extraction |
Plus the Playwright Chromium browser binary (~150MB).
Check
Verify that node_modules/ exists with the key packages installed, including a platform-matching @img/sharp-* native binary:
macOS / Linux:
if [ -d "node_modules" ] && [ -d "node_modules/sharp" ] && [ -d "node_modules/playwright" ]; then
echo "Deno npm dependencies appear installed"
else
echo "Deno npm dependencies need installation"
fi
Windows (PowerShell):
$nmDir = if ($isGlobal) { Join-Path $skillRoot "node_modules" } else { Join-Path $PWD "node_modules" }
if ((Test-Path $nmDir) -and (Test-Path (Join-Path $nmDir "sharp")) -and (Test-Path (Join-Path $nmDir "playwright"))) {
Write-Host "Deno npm dependencies appear installed"
} else {
Write-Host "Deno npm dependencies need installation"
}
If the check passes, skip to the Playwright browser step below.
Step 1: Place deno.json and deno.lock
The shipped deno.json and deno.lock are at $SKILL_ROOT/resources/deno.json and $SKILL_ROOT/resources/deno.lock. They define the import map and integrity-pin all packages.
Project case — copy to the project root:
cp "$SKILL_ROOT/resources/deno.json" "./deno.json"
cp "$SKILL_ROOT/resources/deno.lock" "./deno.lock"
Windows (PowerShell):
Copy-Item (Join-Path $skillRoot "resources\deno.json") (Join-Path $PWD "deno.json") -Force
Copy-Item (Join-Path $skillRoot "resources\deno.lock") (Join-Path $PWD "deno.lock") -Force
Global case — copy to skill root:
macOS / Linux:
cp "$SKILL_ROOT/resources/deno.json" "$SKILL_ROOT/deno.json"
cp "$SKILL_ROOT/resources/deno.lock" "$SKILL_ROOT/deno.lock"
Windows (PowerShell):
Copy-Item (Join-Path $skillRoot "resources\deno.json") (Join-Path $skillRoot "deno.json") -Force
Copy-Item (Join-Path $skillRoot "resources\deno.lock") (Join-Path $skillRoot "deno.lock") -Force
The deno.lock file does NOT need modification — it pins package integrity hashes, not import paths.
Step 2: Run deno install
This downloads all npm packages declared in deno.json into a local node_modules/ directory, enforced by the integrity hashes in deno.lock. No system Node.js or npm is required.
Project case — run from project root (where deno.json was just copied):
macOS / Linux:
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" install
Windows (PowerShell):
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" install
Global case — run from the skill root with explicit config/lock paths:
macOS / Linux:
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" install --config "$SKILL_ROOT/deno.json" --lock "$SKILL_ROOT/deno.lock"
Windows (PowerShell):
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" install --config "$skillRoot\deno.json" --lock "$skillRoot\deno.lock"
This creates node_modules/ in the same directory as deno.json. For the project case, that's the project root. For the global case, that's the skill root.
Step 3: Install Playwright Chromium browser
Project case:
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" run --node-modules-dir=auto npm:playwright install chromium
Global case:
export DENO_NODE_MODULES_DIR="$SKILL_ROOT"
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" run --config "$SKILL_ROOT/deno.json" --node-modules-dir npm:playwright install chromium
Windows (PowerShell) — global case:
$env:DENO_NODE_MODULES_DIR = $skillRoot
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" run --config "$skillRoot\deno.json" --node-modules-dir npm:playwright install chromium
This downloads Chromium to a userspace cache (~150MB). No elevation needed.
If the download fails due to network policy:
- Set
HTTPS_PROXY / HTTP_PROXY environment variables
- Or download the Chromium archive manually and set
PLAYWRIGHT_BROWSERS_PATH
Step 4: (Global only) Write officedeno wrapper to working directory
In a global install, the user runs deno from their project directory — which has no deno.json. The officedeno wrapper script points deno at the global deno.json, import map, and node_modules/.
macOS / Linux:
if [ "$IS_GLOBAL" = true ]; then
sed "s|__SKILL_ROOT_PLACEHOLDER__|$SKILL_ROOT|g" \
"$SKILL_ROOT/scripts/officedeno" > ./officedeno
chmod +x ./officedeno
fi
Windows (PowerShell):
if ($isGlobal) {
$template = Get-Content (Join-Path $skillRoot "scripts\officedeno.ps1") -Raw
$script = $template -replace '__SKILL_ROOT_PLACEHOLDER__', $skillRoot
$script | Set-Content (Join-Path $PWD "officedeno.ps1") -Encoding UTF8NoBOM
}
How other skills use the wrapper: instead of DENO_TLS_CA_STORE=system deno run --allow-all script.js, they use ./officedeno run --allow-all script.js (or .\officedeno.ps1 on Windows). This resolves the import map and node_modules from the global skill root automatically.
Step 5: Cross-platform targeting (optional)
If you need native binaries for a platform other than the current one (e.g., preparing a project on macOS to run on Windows), set environment variables before installing:
macOS / Linux:
npm_config_platform=win32 npm_config_arch=x64 DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" install
Windows (PowerShell):
$env:npm_config_platform = "win32"
$env:npm_config_arch = "x64"
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" install
Supported platform/arch combinations:
linux + x64 | arm64
darwin + x64 | arm64
win32 + x64
Step 6: Verify
Verify all 8 npm packages resolve and sharp's native binary loads:
macOS / Linux:
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" eval '
await import("npm:pptxgenjs");
await import("npm:sharp");
await import("npm:playwright");
await import("npm:pdf-lib");
console.log("All Deno npm dependencies OK");
'
DENO_TLS_CA_STORE=system "$SKILL_ROOT/tools/deno" eval '
const sharp = (await import("npm:sharp")).default;
await sharp(Buffer.from("<svg></svg>")).png().toBuffer();
console.log("sharp native binary OK");
'
Windows (PowerShell):
$env:DENO_TLS_CA_STORE = "system"
& "$skillRoot\tools\deno.exe" eval @"
await import("npm:pptxgenjs");
await import("npm:sharp");
await import("npm:playwright");
await import("npm:pdf-lib");
console.log("All Deno npm dependencies OK");
"@
& "$skillRoot\tools\deno.exe" eval @"
const sharp = (await import("npm:sharp")).default;
await sharp(Buffer.from("<svg></svg>")).png().toBuffer();
console.log("sharp native binary OK");
"@
For the global case, append --config "$SKILL_ROOT/deno.json" --node-modules-dir to each deno invocation, and ensure DENO_NODE_MODULES_DIR="$SKILL_ROOT" is set.
Phase 4: LibreOffice
Required for: PPTX-to-PDF conversion, XLSX formula recalculation, and document validation rendering. If the user only needs text extraction, XML editing, or PDF form filling, skip this phase.
Check
soffice --version 2>/dev/null || echo "not found"
If soffice is on PATH, symlink it into tools/ and skip the download:
macOS / Linux:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
ln -sf "$(command -v soffice)" "$TOOLS_DIR/soffice"
Install to tools/
The goal is to extract a portable soffice binary into tools/ without requiring admin rights or system-wide installation.
macOS — download DMG, extract app:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
ARCH=$(uname -m)
if [ "$ARCH" = "arm64" ]; then
LO_ARCH="aarch64"
else
LO_ARCH="x86_64"
fi
LO_VERSION="26.2.2"
DMG_URL="https://download.documentfoundation.org/libreoffice/stable/${LO_VERSION}/mac/${LO_ARCH}/LibreOffice_${LO_VERSION}_MacOS_${LO_ARCH}.dmg"
DMG_PATH="$TOOLS_DIR/LibreOffice.dmg"
echo "Downloading LibreOffice ${LO_VERSION} for macOS ${LO_ARCH}..."
curl -fSL -o "$DMG_PATH" "$DMG_URL"
hdiutil attach "$DMG_PATH" -quiet -mountpoint "$TOOLS_DIR/dmg_mount"
cp -R "$TOOLS_DIR/dmg_mount/LibreOffice.app" "$TOOLS_DIR/"
hdiutil detach "$TOOLS_DIR/dmg_mount" -quiet
rm "$DMG_PATH"
ln -sf "$TOOLS_DIR/LibreOffice.app/Contents/MacOS/soffice" "$TOOLS_DIR/soffice"
Linux — download AppImage:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
ARCH=$(uname -m)
LO_VERSION="26.2.2"
APPIMAGE_URL="https://libreoffice.soluzioniopen.com/download/LibreOffice-${LO_VERSION}-standard-${ARCH}.AppImage"
echo "Downloading LibreOffice ${LO_VERSION} AppImage for ${ARCH}..."
curl -fSL -o "$TOOLS_DIR/LibreOffice.AppImage" "$APPIMAGE_URL"
chmod +x "$TOOLS_DIR/LibreOffice.AppImage"
ln -sf "$TOOLS_DIR/LibreOffice.AppImage" "$TOOLS_DIR/soffice"
Linux — alternative: download .tar.gz from official site:
TOOLS_DIR="$SKILL_ROOT/tools"
mkdir -p "$TOOLS_DIR"
ARCH=$(uname -m)
LO_VERSION="26.2.2"
if command -v dpkg &>/dev/null; then
PKG_TYPE="deb"
elif command -v rpm &>/dev/null; then
PKG_TYPE="rpm"
fi
TAR_URL="https://download.documentfoundation.org/libreoffice/stable/${LO_VERSION}/linux/${ARCH}/${PKG_TYPE}/LibreOffice_${LO_VERSION}_Linux_${ARCH}_${PKG_TYPE}.tar.gz"
echo "Downloading LibreOffice ${LO_VERSION} for Linux ${ARCH} (${PKG_TYPE})..."
curl -fSL -o "$TOOLS_DIR/libreoffice.tar.gz" "$TAR_URL"
cd "$TOOLS_DIR"
tar xzf libreoffice.tar.gz
cd LibreOffice_*/
if [ "$PKG_TYPE" = "deb" ]; then
dpkg-deb -x DEBS/libreoffice*-core_*.deb "$TOOLS_DIR/libreoffice-opt/"
dpkg-deb -x DEBS/libreoffice*-core-*.deb "$TOOLS_DIR/libreoffice-opt/" 2>/dev/null || true
for deb in DEBS/libreoffice*.deb; do dpkg-deb -x "$deb" "$TOOLS_DIR/libreoffice-opt/"; done
else
for rpm in RPMS/*.rpm; do rpm2cpio "$rpm" | cpio -id -D "$TOOLS_DIR/libreoffice-opt/"; done 2>/dev/null || true
fi
rm libreoffice.tar.gz
ln -sf "$TOOLS_DIR/libreoffice-opt/opt/libreoffice*/program/soffice" "$TOOLS_DIR/soffice"
Windows — download LibreOffice Portable:
$toolsDir = Join-Path $skillRoot "tools"
New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null
$loVersion = "26.2.2"
$url = "https://download.documentfoundation.org/libreoffice/portable/$loVersion/LibreOfficePortable_${loVersion}_MultilingualStandard.paf.exe"
$installer = Join-Path $toolsDir "LibreOfficePortable.paf.exe"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest $url -OutFile $installer
# PortableApps installer can be extracted with 7-Zip without running it
# The soffice binary will be at: tools\LibreOfficePortable\App\libreoffice\program\soffice.exe
Write-Host "Run the LibreOffice Portable installer and point it to: $toolsDir"
Write-Host "Or extract with 7-Zip: 7z x $installer -o$toolsDir\LibreOfficePortable"
Verify
"$SKILL_ROOT/tools/soffice" --version
Phase 5: Write tools/paths.json
After all required tools are installed, write a paths.json to the tools/ directory. This is the single file that other skills read to locate tools — they check it before falling back to PATH.
Format
{
"tools_dir": "<absolute path to office-install/tools/>",
"skill_root": "<absolute path to office-install/>",
"uv": "<absolute path to uv binary>",
"deno": "<absolute path to deno binary>",
"soffice": "<absolute path to soffice binary>",
"node_modules_dir": "<absolute path to node_modules/>",
"global_install": false,
"env": {
"DENO_TLS_CA_STORE": "system"
}
}
Every path must be absolute. Use the resolved path — no ~, no relative paths.
If a tool was not installed (e.g. deno skipped because user only needs DOCX), set its value to null:
{
"tools_dir": "/abs/path/to/tools",
"skill_root": "/abs/path/to/office-install",
"uv": "/abs/path/to/tools/uv",
"deno": null,
"soffice": "/abs/path/to/tools/soffice",
"node_modules_dir": null,
"global_install": false,
"env": {
"DENO_TLS_CA_STORE": "system"
}
}
Writing the file
macOS / Linux:
TOOLS_DIR="$SKILL_ROOT/tools"
NODE_MODULES_DIR=""
if [ -d "./node_modules" ] && [ "$IS_GLOBAL" = false ]; then
NODE_MODULES_DIR="$(cd ./node_modules && pwd)"
elif [ -d "$SKILL_ROOT/node_modules" ] && [ "$IS_GLOBAL" = true ]; then
NODE_MODULES_DIR="$SKILL_ROOT/node_modules"
fi
python3 -c "
import json, os
paths = {
'tools_dir': os.environ['TOOLS_DIR'],
'skill_root': os.environ['SKILL_ROOT'],
'uv': os.path.join(os.environ['TOOLS_DIR'], 'uv'),
'deno': os.path.join(os.environ['TOOLS_DIR'], 'deno'),
'soffice': os.path.join(os.environ['TOOLS_DIR'], 'soffice'),
'node_modules_dir': os.environ.get('NODE_MODULES_DIR') or None,
'global_install': os.environ['IS_GLOBAL'] == 'true',
'env': {'DENO_TLS_CA_STORE': 'system'}
}
with open(os.path.join(os.environ['TOOLS_DIR'], 'paths.json'), 'w') as f:
json.dump(paths, f, indent=2)
f.write('\n')
" TOOLS_DIR="$TOOLS_DIR" SKILL_ROOT="$SKILL_ROOT" NODE_MODULES_DIR="$NODE_MODULES_DIR" IS_GLOBAL="$IS_GLOBAL"
Windows (PowerShell):
$toolsDir = Join-Path $skillRoot "tools"
$nmDir = $null
if (-not $isGlobal -and (Test-Path (Join-Path $PWD "node_modules"))) {
$nmDir = Join-Path $PWD "node_modules"
} elseif ($isGlobal -and (Test-Path (Join-Path $skillRoot "node_modules"))) {
$nmDir = Join-Path $skillRoot "node_modules"
}
$paths = @{
tools_dir = $toolsDir
skill_root = $skillRoot
uv = Join-Path $toolsDir "uv.exe"
deno = Join-Path $toolsDir "deno.exe"
soffice = Join-Path $toolsDir "soffice.exe"
node_modules_dir = $nmDir
global_install = $isGlobal
env = @{
DENO_TLS_CA_STORE = "system"
}
}
$paths | ConvertTo-Json -Depth 3 | Set-Content (Join-Path $toolsDir "paths.json") -Encoding UTF8NoBOM
Always rewrite paths.json at the end of every run of this skill, even if some tools were already present. This ensures the file stays in sync with what's actually installed.
Phase 6: Verify full stack
Run the checks below for the skills the user needs. Only test what's relevant. Use the paths from paths.json.
Read paths.json
macOS / Linux:
TOOLS_DIR="$SKILL_ROOT/tools"
UV=$(python3 -c 'import json,os; d=json.load(open(os.environ["_PF"])); print(d["uv"] or "uv")' _PF="$TOOLS_DIR/paths.json")
DENO=$(python3 -c 'import json,os; d=json.load(open(os.environ["_PF"])); print(d["deno"] or "deno")' _PF="$TOOLS_DIR/paths.json")
SOFFICE=$(python3 -c 'import json,os; d=json.load(open(os.environ["_PF"])); print(d["soffice"] or "soffice")' _PF="$TOOLS_DIR/paths.json")
Python skills (PPTX/DOCX/PDF/XLSX editing)
"$UV" run public/office-pptx/scripts/inventory.py --help
html2pptx (PPTX creation from scratch)
DENO_TLS_CA_STORE=system "$DENO" eval "import('npm:pptxgenjs').then(m => console.log('pptxgenjs', m.default ? 'ok' : 'fail'))"
sharp native binary
DENO_TLS_CA_STORE=system "$DENO" eval "
const sharp = (await import('npm:sharp')).default;
await sharp(Buffer.from('<svg></svg>')).png().toBuffer();
console.log('sharp native binary OK');
"
pdf-lib (PDF creation/modification)
DENO_TLS_CA_STORE=system "$DENO" eval "import('npm:pdf-lib').then(m => console.log('pdf-lib OK'))"
Visual verification (PPTX/DOCX rendering)
"$SOFFICE" --headless --version
PDF to images
"$UV" run public/office-pdf/scripts/convert_pdf_to_images.py --help
Report which capabilities are available and which are not. Frame missing capabilities as "these specific workflows won't work" rather than "setup failed."
Skills install and update — from GitHub
The office-skills project lives at https://github.com/cristoslc/office-skills. This section handles installing the skills (SKILL.md files, scripts, schemas) from the source repo and checking for updates.
Source vs. install layout
This repository is the source. Skills are authored under public/office-*/. Each public/office-*/ directory contains a SKILL.md entrypoint and supporting files (scripts, schemas, references).
Consumers install skills into the standard agent skills path. The canonical install location is .agents/skills/ — either project-local or global:
| Scope | Path | Used by |
|---|
| Project-local | <repo-root>/.agents/skills/office-*/ | When office-skills is a dependency of another project |
| Global | ~/.agents/skills/office-*/ | When office-skills is used standalone across projects |
The install process takes the public/office-*/ directories from the source repo and places them at the standard .agents/skills/ path. The tools/ directory, node_modules/, and paths.json live inside office-install/ — they are generated at install time, not shipped in the repo.
Install methods (ordered by preference)
1. npx skills (standard agent skills CLI)
The Vercel skills CLI is the standard installer for the Agent Skills open format:
npx skills add cristoslc/office-skills --all
npx skills add cristoslc/office-skills --all -g
npx skills add cristoslc/office-skills --all -g -y
npx skills add cristoslc/office-skills --skill office-install --skill office-pptx -g
2. Git clone (if git is available)
git clone https://github.com/cristoslc/office-skills.git /tmp/office-skills
SKILLS_DIR="$HOME/.agents/skills"
mkdir -p "$SKILLS_DIR"
for skill_dir in /tmp/office-skills/public/office-*/; do
skill_name=$(basename "$skill_dir")
ln -sf "$skill_dir" "$SKILLS_DIR/$skill_name"
done
echo "$(git -C /tmp/office-skills rev-parse HEAD)" > "$SKILLS_DIR/office-install/.installed-sha"
3. Direct download (no git, no npm)
macOS / Linux:
SKILLS_DIR="$HOME/.agents/skills"
mkdir -p "$SKILLS_DIR"
echo "Downloading office-skills from GitHub..."
curl -fSL "https://github.com/cristoslc/office-skills/archive/refs/heads/trunk.tar.gz" -o /tmp/office-skills.tar.gz
tar xzf /tmp/office-skills.tar.gz -C /tmp/ "office-skills-trunk/public"
cp -R /tmp/office-skills-trunk/public/ "$SKILLS_DIR/"
rm -rf /tmp/office-skills.tar.gz /tmp/office-skills-trunk
REMOTE_SHA=$(curl -fsSL "https://api.github.com/repos/cristoslc/office-skills/commits/trunk" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['sha'])" 2>/dev/null || echo "unknown")
echo "$REMOTE_SHA" > "$SKILLS_DIR/office-install/.installed-sha"
echo "Skills installed to $SKILLS_DIR/ (SHA: $REMOTE_SHA)"
Windows (PowerShell):
$skillsDir = Join-Path $env:USERPROFILE ".agents\skills"
New-Item -ItemType Directory -Force -Path $skillsDir | Out-Null
$url = "https://github.com/cristoslc/office-skills/archive/refs/heads/trunk.zip"
$zip = Join-Path $env:TEMP "office-skills.zip"
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest $url -OutFile $zip
Expand-Archive $zip $env:TEMP -Force
$extracted = Join-Path $env:TEMP "office-skills-trunk\public"
Copy-Item -Path "$extracted\office-*" -Destination $skillsDir -Recurse -Force
$sha = (Invoke-RestMethod -Uri "https://api.github.com/repos/cristoslc/office-skills/commits/trunk" -ErrorAction SilentlyContinue).sha
if ($sha) { $sha | Out-File (Join-Path $skillsDir "office-install\.installed-sha") -Encoding utf8 }
Remove-Item $zip -Force
Remove-Item (Join-Path $env:TEMP "office-skills-trunk") -Recurse -Force
Write-Host "Skills installed to $skillsDir"
Check for updates
LOCAL_SHA_FILE="$HOME/.agents/skills/office-install/.installed-sha"
if [ -f "$LOCAL_SHA_FILE" ]; then
LOCAL_SHA=$(cat "$LOCAL_SHA_FILE")
REMOTE_SHA=$(curl -fsSL "https://api.github.com/repos/cristoslc/office-skills/commits/trunk" | python3 -c "import json,sys; print(json.load(sys.stdin)['sha'])" 2>/dev/null)
if [ "$REMOTE_SHA" = "$LOCAL_SHA" ]; then
echo "Skills are up to date ($REMOTE_SHA)"
else
echo "Update available: local=$LOCAL_SHA remote=$REMOTE_SHA"
fi
else
echo "No local version recorded. Run the install to get skills."
fi
Apply updates
Re-run the same install method used originally. Tools, node_modules/, and paths.json are preserved — they live inside office-install/ and are not overwritten by the skill download.
Using npx skills:
npx skills add cristoslc/office-skills --all -g -y
Using git:
git -C /tmp/office-skills pull --ff-only
Using direct download: repeat the "Direct download" steps above.
After updating the skill files, re-run this skill (Phases 0–6) to regenerate paths.json and install any newly-added npm dependencies.
GitHub API rate limits
Unauthenticated GitHub API requests are rate-limited to 60/hour. If you hit the limit:
- Use the archive URL directly (no API call):
https://github.com/cristoslc/office-skills/archive/refs/heads/trunk.tar.gz
- Set
GITHUB_TOKEN in the environment for 5000/hour rate limit
Offline / air-gapped environments
If the machine has no internet access:
- Download the tarball on another machine:
curl -fSL -o office-skills.tar.gz https://github.com/cristoslc/office-skills/archive/refs/heads/trunk.tar.gz
- Transfer it via USB, shared drive, etc.
- Extract manually:
tar xzf office-skills.tar.gz && cp -R office-skills-trunk/public/ ~/.agents/skills/
Tool discovery — how other skills find tools
Other skills (pptx, docx, pdf, xlsx) locate tools using this search order:
- Read
paths.json from {skill_root}/tools/paths.json
- If a tool's value is a non-null string, use that path
- If a tool's value is
null or the key is missing, fall back to PATH (command -v <tool>)
- Set environment variables from the
env object (e.g. DENO_TLS_CA_STORE=system)
- If
global_install is true, use the officedeno wrapper (in the working directory) instead of bare deno
Shell helper — resolve a tool path
office_tool() {
local tool="$1"
local paths_file
for dir in ".agents/skills/office-install/tools" "$HOME/.agents/skills/office-install/tools"; do
if [ -f "$dir/paths.json" ]; then
paths_file="$dir"
break
fi
done
if [ -n "$paths_file" ]; then
local resolved
resolved=$(python3 -c 'import json,sys,os; d=json.load(open(os.environ["_PFILE"])); v=d.get(os.environ["_PNAME"]); print(v if v else "")' _PFILE="$paths_file/paths.json" _PNAME="$tool" 2>/dev/null)
if [ -n "$resolved" ]; then
echo "$resolved"
return
fi
fi
command -v "$tool"
}
Resolving the deno command
For skills that need to run deno scripts, check global_install in paths.json:
TOOLS_DIR="$(office_tool tools_dir)"
GLOBAL=$(python3 -c 'import json,os; d=json.load(open(os.environ["_PF"])); print(d.get("global_install", False))' _PF="$TOOLS_DIR/paths.json" 2>/dev/null)
if [ "$GLOBAL" = "True" ]; then
DENO_CMD="./officedeno"
else
DENO_CMD="$(office_tool deno) || deno"
fi
DENO_TLS_CA_STORE=system $DENO_CMD run --allow-all script.js
PowerShell helper — resolve a tool path
function Get-OfficeTool {
param([string]$ToolName)
$pathsFile = $null
foreach ($dir in @(
(Join-Path $PWD ".agents\skills\office-install\tools"),
(Join-Path $env:USERPROFILE ".agents\skills\office-install\tools")
)) {
$candidate = Join-Path $dir "paths.json"
if (Test-Path $candidate) { $pathsFile = $candidate; break }
}
if ($pathsFile) {
$paths = Get-Content $pathsFile -Raw | ConvertFrom-Json
$result = $paths.PSObject.Properties | Where-Object { $_.Name -eq $ToolName } | Select-Object -ExpandProperty Value
if ($result -and $result -ne "null") { return $result }
}
Get-Command $ToolName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
}
Troubleshooting
Deno certificate errors on corporate networks
Set DENO_TLS_CA_STORE=system before all deno invocations:
export DENO_TLS_CA_STORE=system
If this doesn't work, your corporate cert may not be in the system store. Export it as PEM and use:
export DENO_CERT=/path/to/corporate-ca.pem
deno --cert /path/to/corporate-ca.pem ...
"Execution of scripts is disabled on this system" (Windows PowerShell)
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
This is a per-user setting and does not require elevation.
Downloaded binary blocked by SmartScreen / endpoint protection
The executable was flagged by the organization's security policy. Options:
- Right-click the .exe → Properties → check "Unblock" (if available)
- Ask IT to allowlist the specific binary hash
- Use a pre-approved Python/Node installation if one exists on the system
Do not try to bypass security controls. Inform the user and ask how to proceed.
Proxy / firewall blocks downloads
If Invoke-WebRequest or curl fails:
- Check if
HTTPS_PROXY is set: echo $HTTPS_PROXY
- Ask the user for their proxy URL and set it before retrying
- Some corporate environments require a PAC file — the user will know
uv run fails to download Python
uv downloads Python from GitHub releases. If this is blocked:
- The user can pre-install Python via their IT-approved method
- Then set
UV_PYTHON to point to it: export UV_PYTHON="/path/to/python3"
- uv will use that Python instead of downloading its own
sharp native binary errors
If sharp fails at runtime with "Could not load the 'sharp' module" or a platform binary error:
- Confirm the platform/arch matches:
DENO_TLS_CA_STORE=system "$DENO" eval "console.log(Deno.build.os, Deno.build.arch)"
- Delete
node_modules/ and reinstall with explicit platform:
rm -rf node_modules/
npm_config_platform=win32 npm_config_arch=x64 DENO_TLS_CA_STORE=system "$DENO" install
- For offline/airgapped systems, download the platform-specific sharp binary from sharp releases and extract into
node_modules/@img/sharp-<platform>-<arch>/
deno install integrity check failure
If deno install fails with a hash/integrity error, the deno.lock may be stale (e.g., after a skill update added new packages). Re-run from the skill root:
DENO_TLS_CA_STORE=system "$DENO" install --config "$SKILL_ROOT/deno.json"
This regenerates deno.lock with updated hashes. The developer should then copy the updated lock to resources/deno.lock and commit both resources/deno.json and resources/deno.lock together.
LibreOffice download is too large
The soffice binary alone is ~350MB+. If the download is impractical:
- On macOS:
brew install --cask libreoffice (if Homebrew is available)
- On Linux:
sudo apt-get install -y libreoffice-core --no-install-recommends (Debian/Ubuntu, smaller footprint)
- Then symlink the system
soffice into tools/