diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index 1142ec33..80adce43 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -97,6 +97,7 @@ jobs: docker build _external/clawdbot \ --build-arg OPENCLAW_EXTENSIONS=acpx \ --build-arg OPENCLAW_INSTALL_GH_CLI=1 \ + --build-arg OPENCLAW_INSTALL_DOC_TOOLCHAIN=1 \ -t "$REPO:$TAG" docker push "$REPO:$TAG" diff --git a/_external/clawdbot b/_external/clawdbot index 11878b43..4bf74dfc 160000 --- a/_external/clawdbot +++ b/_external/clawdbot @@ -1 +1 @@ -Subproject commit 11878b43c27fe5d34d8a5615a76d62e00b8dca28 +Subproject commit 4bf74dfc5a523615e5169f2447bfecad1ea79cd4 diff --git a/backend/routes/registry/presets.ts b/backend/routes/registry/presets.ts index de07ac80..d9b539e8 100644 --- a/backend/routes/registry/presets.ts +++ b/backend/routes/registry/presets.ts @@ -1282,6 +1282,13 @@ If nothing changed → no post. - Auto-source from GitHub when idle — don't wait for humans to assign work. - If tools unavailable → \`HEARTBEAT_OK\` immediately. `, + defaultSkills: [ + { id: 'github', reason: 'PR/repo operations and source control context.' }, + { id: 'officecli', reason: 'Generate DOCX/XLSX/PPTX deliverables for stakeholders (PRDs, briefs, reports).' }, + { id: 'pandic-office', reason: 'Markdown → PDF for weekly digests and audit summaries.' }, + { id: 'markdown-converter', reason: 'Read user-attached PDFs/DOCX as markdown for input.' }, + { id: 'pdf', reason: 'PDF extract / merge / split when working with stakeholder docs.' }, + ], }, { id: 'backend-engineer', @@ -1467,6 +1474,14 @@ If Branch A just completed a task with a PR: also post the API contract (endpoin - If tools unavailable → \`HEARTBEAT_OK\` immediately. - HEARTBEAT_OK is a return value, NOT a chat message. Never post it. `, + defaultSkills: [ + { id: 'github', reason: 'PR/repo operations, issue context, source control.' }, + { id: 'tmux', reason: 'Session management for long-running coding tasks.' }, + { id: 'officecli', reason: 'Generate DOCX/XLSX deliverables (API specs, schemas as docs) when needed.' }, + { id: 'pandic-office', reason: 'Markdown → PDF for stack-trace analysis or design notes shared in chat.' }, + { id: 'markdown-converter', reason: 'Read user-attached PDFs/DOCX/XLSX/specs as markdown for input.' }, + { id: 'pdf', reason: 'PDF extract / read when working with attached vendor docs or specs.' }, + ], }, { id: 'frontend-engineer', @@ -1699,6 +1714,14 @@ For any message asking about frontend components, UI status, implementation deci - Skip sender "pixel" — that's you. - If tools unavailable → \`HEARTBEAT_OK\` immediately. `, + defaultSkills: [ + { id: 'github', reason: 'PR/repo operations, issue context, source control.' }, + { id: 'tmux', reason: 'Session management for long-running coding tasks.' }, + { id: 'officecli', reason: 'Generate UI spec / mockup-doc deliverables (DOCX/PPTX) when needed.' }, + { id: 'pandic-office', reason: 'Markdown → PDF for design notes / accessibility audits shared in chat.' }, + { id: 'markdown-converter', reason: 'Read user-attached PDFs/DOCX/specs as markdown for input.' }, + { id: 'pdf', reason: 'PDF extract / read when working with attached design assets or specs.' }, + ], }, { id: 'devops-engineer', @@ -1924,6 +1947,14 @@ For any message asking about infrastructure status, deployment decisions, CI/CD - Skip sender "ops" — that's you. - If tools unavailable → \`HEARTBEAT_OK\` immediately. `, + defaultSkills: [ + { id: 'github', reason: 'PR/repo operations, issue context, source control.' }, + { id: 'tmux', reason: 'Session management for long-running coding tasks.' }, + { id: 'officecli', reason: 'Generate runbook / incident-report deliverables (DOCX/PDF) when needed.' }, + { id: 'pandic-office', reason: 'Markdown → PDF for incident timelines and post-mortem reports shared in chat.' }, + { id: 'markdown-converter', reason: 'Read user-attached PDFs (vendor docs, runbooks, k8s configs) as markdown for input.' }, + { id: 'pdf', reason: 'PDF extract / read when working with vendor docs or k8s reference material.' }, + ], }, { id: 'claude-code-agent', diff --git a/backend/routes/registry/provision.ts b/backend/routes/registry/provision.ts index c58d3f4a..db17667b 100644 --- a/backend/routes/registry/provision.ts +++ b/backend/routes/registry/provision.ts @@ -33,6 +33,7 @@ const { issueUserTokenForInstallation, } = require('./tokens'); const { PRESET_DEFINITIONS } = require('./presets'); +const { applyPresetDefaultSkills } = require('../../services/presetSkillsAutoImport'); const provisionRouter = express.Router(); @@ -283,6 +284,33 @@ provisionRouter.post('/pods/:podId/agents/:name/provision', auth, async (req: an } } + // ADR-013 Phase 1: auto-import the preset's defaultSkills as PodAssets so + // syncOpenClawSkills (called below) picks them up and writes their + // SKILL.md files to the agent's workspace on the gateway PVC. Resolution + // order: local bundle (commonly-bundled-skills//SKILL.md) → upstream + // catalog. Reuses the same upsertImportedSkillAsset path the manual + // /api/skills/import route uses; idempotent across reprovisions. + const explicitPresetIdForSkills = (configPayload as any)?.presetId || null; + const matchedPresetForSkills: any = PRESET_DEFINITIONS.find( + (p: any) => p.id === (explicitPresetIdForSkills || normalizedInstanceId), + ); + let presetSkillsApplied = null; + if (matchedPresetForSkills?.defaultSkills?.length && podId) { + try { + presetSkillsApplied = await applyPresetDefaultSkills({ + podId: String(podId), + preset: matchedPresetForSkills, + userId, + }); + } catch (skillErr: unknown) { + console.warn( + '[provision] applyPresetDefaultSkills failed:', + (skillErr as Error).message, + ); + presetSkillsApplied = { error: (skillErr as Error).message }; + } + } + let runtimeStart = null; try { runtimeStart = await startAgentRuntime(runtimeType, normalizedInstanceId, { gateway }); @@ -387,6 +415,7 @@ provisionRouter.post('/pods/:podId/agents/:name/provision', auth, async (req: an runtimeRestarted: runtimeRestart?.restarted || false, runtimeRestartError: runtimeRestart?.reason || null, skillsSynced, + presetSkillsApplied, sharedIdentity: true, agentUsername: agentUser.username, }); diff --git a/backend/routes/registry/reprovision.ts b/backend/routes/registry/reprovision.ts index d5663f1f..d2ae95b1 100644 --- a/backend/routes/registry/reprovision.ts +++ b/backend/routes/registry/reprovision.ts @@ -26,6 +26,7 @@ const { issueUserTokenForInstallation, } = require('./tokens'); const { PRESET_DEFINITIONS } = require('./presets'); +const { applyPresetDefaultSkills } = require('../../services/presetSkillsAutoImport'); const reprovisionInstallation = async ({ installation, @@ -167,6 +168,28 @@ const reprovisionInstallation = async ({ } } + // ADR-013 Phase 1: auto-import the preset's defaultSkills as PodAssets so + // syncOpenClawSkills (called below) picks them up and writes their SKILL.md + // files to the agent's workspace on the gateway PVC. Reuses the same + // upsertImportedSkillAsset path the manual /api/skills/import route uses; + // idempotent across reprovisions. + let presetSkillsApplied = null; + if (matchedPreset?.defaultSkills?.length && podId) { + try { + presetSkillsApplied = await applyPresetDefaultSkills({ + podId: String(podId), + preset: matchedPreset, + userId: installation.installedBy || null, + }); + } catch (skillErr: unknown) { + console.warn( + '[reprovision] applyPresetDefaultSkills failed:', + (skillErr as Error).message, + ); + presetSkillsApplied = { error: (skillErr as Error).message }; + } + } + let runtimeStart = null; try { runtimeStart = await startAgentRuntime(runtimeType, normalizedInstanceId, { gateway }); @@ -264,6 +287,7 @@ const reprovisionInstallation = async ({ runtimeRestartError: runtimeRestart?.reason || null, tokenRotated: Boolean(runtimeIssued.token), skillsSynced, + presetSkillsApplied, }; }; diff --git a/backend/services/presetSkillsAutoImport.ts b/backend/services/presetSkillsAutoImport.ts new file mode 100644 index 00000000..8f04e28a --- /dev/null +++ b/backend/services/presetSkillsAutoImport.ts @@ -0,0 +1,247 @@ +/** + * presetSkillsAutoImport — apply a preset's `defaultSkills` to a pod by + * reusing the existing skill-import pipeline. + * + * Per ADR-013 Phase 1: when a preset declares `defaultSkills: [{id, reason}, ...]` + * and an agent is provisioned/reprovisioned with that preset, those skills + * should land in the pod's PodAssets (so the gateway PVC sync picks them up + * on the same provision call). + * + * Resolution order for each skillId: + * 1. Local bundle: `commonly-bundled-skills//SKILL.md` + * Used for skills not yet in the upstream catalog (today: officecli). + * 2. Catalog index: `docs/skills/awesome-agent-skills-index.json` entry's + * `sourceUrl` field — content fetched via `fetchSkillContentFromSource`. + * 3. Skip with a warning if neither is found. + * + * This is pure REUSE — no new install plumbing. We call the same + * `PodAssetService.upsertImportedSkillAsset` + `syncOpenClawSkills` pair the + * `POST /api/skills/import` route already calls. Idempotent: re-running on + * an already-installed skill upserts the asset (same `skillKey` lookup). + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +import PodAssetService from './podAssetService'; +import { fetchSkillContentFromSource } from './skillsCatalogService'; + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const BUNDLED_SKILLS_DIR = path.join(REPO_ROOT, 'commonly-bundled-skills'); +const CATALOG_INDEX_PATH = path.join( + REPO_ROOT, + 'docs', + 'skills', + 'awesome-agent-skills-index.json', +); + +interface CatalogEntry { + id: string; + name: string; + description?: string; + sourceUrl?: string; + license?: { name?: string } | string; + tags?: string[]; + content?: string; +} + +let catalogCache: Map | null = null; + +const loadCatalog = async (): Promise> => { + if (catalogCache) return catalogCache; + try { + const raw = await fs.readFile(CATALOG_INDEX_PATH, 'utf8'); + const parsed = JSON.parse(raw); + const items: CatalogEntry[] = Array.isArray(parsed?.items) ? parsed.items : []; + catalogCache = new Map(items.map((entry) => [entry.id, entry])); + } catch (err) { + console.warn( + '[presetSkillsAutoImport] could not load catalog index:', + (err as Error).message, + ); + catalogCache = new Map(); + } + return catalogCache; +}; + +/** + * Read a locally-bundled skill's SKILL.md if it exists. + * Returns null if the directory or file is missing. + */ +const readBundledSkill = async ( + skillId: string, +): Promise<{ content: string; sourceUrl: string } | null> => { + const safeId = skillId.replace(/[^a-zA-Z0-9_.-]/g, ''); + if (safeId !== skillId || !safeId) return null; + const skillPath = path.join(BUNDLED_SKILLS_DIR, safeId, 'SKILL.md'); + try { + const content = await fs.readFile(skillPath, 'utf8'); + return { + content, + sourceUrl: `commonly-bundled-skills/${safeId}/SKILL.md`, + }; + } catch { + return null; + } +}; + +interface ResolvedSkill { + id: string; + content: string; + sourceUrl: string; + license?: string; + description?: string; + tags?: string[]; +} + +const resolveSkillContent = async ( + skillId: string, +): Promise => { + const bundled = await readBundledSkill(skillId); + if (bundled) { + return { + id: skillId, + content: bundled.content, + sourceUrl: bundled.sourceUrl, + license: 'See commonly-bundled-skills//LICENSE', + }; + } + + const catalog = await loadCatalog(); + const entry = catalog.get(skillId); + if (!entry) { + console.warn( + `[presetSkillsAutoImport] skill '${skillId}' not in local bundle or catalog — skipping`, + ); + return null; + } + if (entry.content && entry.content.length > 0) { + return { + id: skillId, + content: entry.content, + sourceUrl: entry.sourceUrl || `catalog:${skillId}`, + license: typeof entry.license === 'string' ? entry.license : entry.license?.name, + description: entry.description, + tags: entry.tags, + }; + } + if (!entry.sourceUrl) { + console.warn( + `[presetSkillsAutoImport] skill '${skillId}' has no inline content and no sourceUrl — skipping`, + ); + return null; + } + try { + const fetched = await fetchSkillContentFromSource(entry.sourceUrl); + if (!fetched?.content) { + console.warn( + `[presetSkillsAutoImport] failed to fetch SKILL.md for '${skillId}' from ${entry.sourceUrl}`, + ); + return null; + } + return { + id: skillId, + content: fetched.content, + sourceUrl: fetched.resolvedUrl || entry.sourceUrl, + license: typeof entry.license === 'string' ? entry.license : entry.license?.name, + description: entry.description, + tags: entry.tags, + }; + } catch (err) { + console.warn( + `[presetSkillsAutoImport] error fetching SKILL.md for '${skillId}':`, + (err as Error).message, + ); + return null; + } +}; + +interface DefaultSkillsEntry { + id?: string; + reason?: string; +} + +interface ApplyOptions { + podId: string; + preset: { defaultSkills?: DefaultSkillsEntry[] } | null | undefined; + userId?: string | null; +} + +interface ApplyResult { + podId: string; + attempted: number; + imported: string[]; + skipped: { id: string; reason: string }[]; +} + +/** + * Apply a preset's defaultSkills to a pod by upserting each as a PodAsset. + * + * The caller is responsible for triggering the gateway sync afterwards (or + * letting the next provision/reprovision do it). This helper does NOT call + * `syncOpenClawInstallationsForPodSkillChange` itself — that's owned by the + * provision/reprovision path which already runs `syncOpenClawSkills` on every + * call. Adding skills here means they'll be picked up by that sync. + */ +export const applyPresetDefaultSkills = async ({ + podId, + preset, + userId, +}: ApplyOptions): Promise => { + const result: ApplyResult = { + podId, + attempted: 0, + imported: [], + skipped: [], + }; + const entries = Array.isArray(preset?.defaultSkills) ? preset!.defaultSkills! : []; + if (entries.length === 0) return result; + + for (const entry of entries) { + const skillId = String(entry?.id || '').trim(); + if (!skillId) continue; + result.attempted += 1; + + const resolved = await resolveSkillContent(skillId); + if (!resolved) { + result.skipped.push({ id: skillId, reason: 'content not resolvable' }); + continue; + } + + try { + await PodAssetService.upsertImportedSkillAsset({ + podId, + name: skillId, + markdown: resolved.content, + tags: resolved.tags || [], + metadata: { + scope: 'pod', + sourceUrl: resolved.sourceUrl, + license: resolved.license || null, + description: resolved.description || null, + importedAt: new Date().toISOString(), + autoImportedBy: 'preset.defaultSkills', + presetReason: entry.reason || null, + }, + createdBy: userId || null, + }); + result.imported.push(skillId); + } catch (err) { + console.warn( + `[presetSkillsAutoImport] upsert failed for '${skillId}':`, + (err as Error).message, + ); + result.skipped.push({ + id: skillId, + reason: `upsert error: ${(err as Error).message}`, + }); + } + } + + return result; +}; + +// CommonJS interop for callers requiring this module +module.exports = { applyPresetDefaultSkills }; +module.exports.applyPresetDefaultSkills = applyPresetDefaultSkills; +module.exports.default = { applyPresetDefaultSkills }; diff --git a/commonly-bundled-skills/README.md b/commonly-bundled-skills/README.md new file mode 100644 index 00000000..6a002948 --- /dev/null +++ b/commonly-bundled-skills/README.md @@ -0,0 +1,44 @@ +# Commonly bundled skills + +Skills shipped with this repo that aren't (yet) in the upstream +`VoltAgent/awesome-agent-skills` catalog. The provisioner's preset auto-import +helper reads `commonly-bundled-skills//SKILL.md` first; if missing, +falls back to the catalog index at `docs/skills/awesome-agent-skills-index.json`. + +Each skill lives in its own subdirectory: + +``` +commonly-bundled-skills/ + / + SKILL.md # required — the skill prompt loaded by the agent runtime + LICENSE # required when bundling third-party content + README.md # optional — Commonly-side notes (provenance, why bundled) +``` + +## Why bundle locally instead of catalog-only + +The upstream catalog (`docs/skills/awesome-agent-skills-index.json`) is synced +periodically via `scripts/sync-awesome-agent-skills.sh`. New upstream skills land +in the catalog only after a sync. For skills we want to ship before the next +sync, or skills the upstream maintainer hasn't accepted (or that we maintain +ourselves), we bundle them locally. + +When upstream gains the skill, the local bundle can be deleted and the preset +declarations resolve through the catalog instead. + +## Current bundles + +- **`officecli`** — DOCX / XLSX / PPTX creation + editing via the + [`iOfficeAI/OfficeCLI`](https://github.com/iOfficeAI/OfficeCLI) static binary + (Apache-2.0). Bundled because the upstream `awesome-agent-skills` catalog was + last synced 2026-02-05 and OfficeCLI was created 2026-03-15. Pull request to + upstream catalog tracked separately. + +## Adding a new bundled skill + +1. Create `commonly-bundled-skills//SKILL.md` with the skill prompt. +2. If the content comes from a third party, include their `LICENSE` file. +3. Reference the skill ID from `defaultSkills` in `backend/routes/registry/presets.ts`. +4. The auto-importer (in `backend/services/presetSkillsAutoImport.ts`) picks it + up on the next provision/reprovision. +5. If applicable, file a PR upstream so we can drop the local bundle eventually. diff --git a/commonly-bundled-skills/officecli/LICENSE b/commonly-bundled-skills/officecli/LICENSE new file mode 100644 index 00000000..cb9fa586 --- /dev/null +++ b/commonly-bundled-skills/officecli/LICENSE @@ -0,0 +1,203 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. Please also get an + OpenSourceInitiative.org approved license identifier and put it + in the first line of your license text file. + + SPDX-License-Identifier: Apache-2.0 + + Copyright 2026 OfficeCli (https://OfficeCli.AI) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied. See the License for the specific language governing + permissions and limitations under the License. diff --git a/commonly-bundled-skills/officecli/SKILL.md b/commonly-bundled-skills/officecli/SKILL.md new file mode 100644 index 00000000..e59006a7 --- /dev/null +++ b/commonly-bundled-skills/officecli/SKILL.md @@ -0,0 +1,410 @@ +--- +name: officecli +description: Create, analyze, proofread, and modify Office documents (.docx, .xlsx, .pptx) using the officecli CLI tool. Use when the user wants to create, inspect, check formatting, find issues, add charts, or modify Office documents. +--- + +# officecli + +AI-friendly CLI for .docx, .xlsx, .pptx. Single binary, no dependencies, no Office installation needed. + +## Install + +If `officecli` is not installed: + +```bash +# macOS / Linux +curl -fsSL https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.sh | bash + +# Windows (PowerShell) +irm https://raw.githubusercontent.com/iOfficeAI/OfficeCLI/main/install.ps1 | iex +``` + +Verify with `officecli --version`. If still not found after install, open a new terminal. + +--- + +## Strategy + +**L1 (read) → L2 (DOM edit) → L3 (raw XML)**. Always prefer higher layers. Add `--json` for structured output. + +**Before doc work, check Specialized Skills** (bottom of this file). Fundraising decks, academic papers, financial models, dashboards, and Morph animations need their own skill loaded first — `load_skill` once, then proceed. + +--- + +## Help System (IMPORTANT) + +**When unsure about property names, value formats, or command syntax, ALWAYS run help instead of guessing.** One help query beats guess-fail-retry loops. + +`officecli help` ≡ `officecli --help`, and `officecli --help` ≡ `officecli help ` — same content. + +```bash +officecli help # All commands + global options + schema entry points +officecli help docx # List all docx elements +officecli help docx paragraph # Full schema: properties, aliases, examples, readbacks +officecli help docx set paragraph # Verb-filtered: only props usable with `set` +officecli help docx paragraph --json # Structured schema (machine-readable) +``` + +Format aliases: `word`→`docx`, `excel`→`xlsx`, `ppt`/`powerpoint`→`pptx`. Verbs: `add`, `set`, `get`, `query`, `remove`. MCP exposes the same schema via `{"command":"help","format":"docx","type":"paragraph"}`. + +--- + +## Performance: Resident Mode + +**Every command auto-starts a resident on first access** (60s idle timeout) — file-lock conflicts are automatically avoided. Explicit `open`/`close` is still recommended for longer sessions (12min idle): +```bash +officecli open report.docx # explicitly keep in memory +officecli set report.docx ... # no file I/O overhead +officecli close report.docx # save and release +``` + +Opt out of auto-start: `OFFICECLI_NO_AUTO_RESIDENT=1`. + +--- + +## Quick Start + +**PPT:** +```bash +officecli create slides.pptx +officecli add slides.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E +officecli add slides.pptx '/slide[1]' --type shape --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm --prop font=Arial --prop size=24 --prop color=FFFFFF +``` + +**Word:** +```bash +officecli create report.docx +officecli add report.docx /body --type paragraph --prop text="Executive Summary" --prop style=Heading1 +officecli add report.docx /body --type paragraph --prop text="Revenue increased by 25% year-over-year." +``` + +**Excel:** +```bash +officecli create data.xlsx +officecli set data.xlsx /Sheet1/A1 --prop value="Name" --prop bold=true +officecli set data.xlsx /Sheet1/A2 --prop value="Alice" +``` + +--- + +## L1: Create, Read & Inspect + +```bash +officecli create # Create blank .docx/.xlsx/.pptx (type from extension) +officecli view # outline | stats | issues | text | annotated | html +officecli get --depth N # Get a node and its children [--json] +officecli query # CSS-like query +officecli validate # Validate against OpenXML schema +``` + +### view modes + +| Mode | Description | Useful flags | +|------|-------------|-------------| +| `outline` | Document structure | | +| `stats` | Statistics (pages, words, shapes) | | +| `issues` | Formatting/content/structure problems | `--type format\|content\|structure`, `--limit N` | +| `text` | Plain text extraction | `--start N --end N`, `--max-lines N` | +| `annotated` | Text with formatting annotations | | +| `html` | Static HTML snapshot — same renderer as `watch`, no server needed | `--browser`, `--page N` (docx), `--start N --end N` (pptx) | + +Use `view html` for one-shot snapshots (CI artifacts, archival, diffing); use `watch` when you need live refresh or browser-side click-to-select. + +### get + +Any XML path via element localName. Use `--depth N` to expand children. Add `--json` for structured output. Default text output is grep-friendly: `path (type) "text" key=val key=val ...` + +```bash +officecli get report.docx '/body/p[3]' --depth 2 --json +officecli get slides.pptx '/slide[1]' --depth 1 # list all shapes on slide 1 +officecli get data.xlsx '/Sheet1/B2' --json +``` + +### Stable ID Addressing + +Elements with stable IDs return `@attr=value` paths instead of positional indices. Prefer these in multi-step workflows — positional indices shift on insert/delete, stable IDs do not. + +``` +/slide[1]/shape[@id=550950021] # PPT shape +/slide[1]/table[@id=1388430425]/tr[1]/tc[2] # PPT table +/body/p[@paraId=1A2B3C4D] # Word paragraph +/comments/comment[@commentId=1] # Word comment +``` + +PPT also accepts `@name=` (e.g. `shape[@name=Title 1]`), with morph `!!` prefix awareness. Elements without stable IDs (slide, run, tr/tc, row) fall back to positional indices. + +### query + +CSS-like selectors: `[attr=value]`, `[attr!=value]`, `[attr~=text]`, `[attr>=value]`, `[attr<=value]`, `:contains("text")`, `:empty`, `:has(formula)`, `:no-alt`. + +```bash +officecli query report.docx 'paragraph[style=Normal] > run[font!=Arial]' +officecli query slides.pptx 'shape[fill=FF0000]' +``` + +For large documents, use `--max-lines` to limit output. + +--- + +## Watch & Interactive Selection + +Live HTML preview that auto-refreshes on every file change. Browsers can click / shift-click / box-drag to select shapes; the CLI can read the current browser selection and act on it. + +```bash +officecli watch [--port N] # Start preview server (default port 18080) +officecli unwatch # Stop +officecli goto # Scroll watching browser(s) to element (docx: p / table / tr / tc) +``` + +Open the printed `http://localhost:N` URL. Click to select; shift/cmd/ctrl+click to multi-select; drag from empty space to box-select. PPT/Word use blue outline; Excel uses native-style green selection (double-click cell to edit inline; drag a chart to reposition). + +### `get selected` — read what the user clicked + +```bash +officecli get selected [--json] +``` + +Returns DocumentNodes for whatever is currently selected. Empty result if nothing selected. Exit code != 0 if no watch is running. + +```bash +# User clicks shapes in the browser, then asks "make these red" +PATHS=$(officecli get deck.pptx selected --json | jq -r '.data.Results[].path') +for p in $PATHS; do officecli set deck.pptx "$p" --prop fill=FF0000; done +``` + +### Key properties + +- **Selection survives file edits.** Paths use stable `@id=` form. +- **All connected browsers share one selection.** Last-write-wins. +- **Same-file single-watch.** A given file can have only one watch process at a time. +- **Group shapes select as a whole.** Drilling into individual children of a group is not supported in v1. +- **Coverage:** `.pptx` shapes/pictures/tables/charts/connectors/groups; `.docx` top-level paragraphs and tables. Inherited layout/master decorations and Word nested elements (table cells, run-level) are not addressable. **`.xlsx` does not emit `data-path`** — `mark`/`selection` on xlsx always resolve `stale=true` (v2 candidate). + +### Marks — edit proposals waiting for review + +Use `mark` when changes need human review BEFORE they hit the file. Marks live in the watch process only; a separate `set` pipeline applies accepted ones. For one-shot changes use `set` directly; for permanent file annotations use `add --type comment` (Word native). + +```bash +officecli mark [--prop find=... color=... note=... tofix=... regex=true] [--json] +officecli unmark [--path

| --all] [--json] +officecli get-marks [--json] +``` + +Props: `find` (literal or regex when `regex=true`; raw form `find='r"[abc]"'`), `color` (hex / `rgb(...)` / 22 named whitelist), `note`, `tofix` (drives apply pipeline). **Path** must be `data-path` format from watch HTML — see subskills for full pipeline. + +--- + +## L2: DOM Operations + +### set — modify properties + +```bash +officecli set --prop key=value [--prop ...] +``` + +**Any XML attribute is settable** via element path (found via `get --depth N`) — even attributes not currently present. Without `find=`, `set` applies format to the entire element. + +**Value formats:** + +| Type | Format | Examples | +|------|--------|---------| +| Colors | Hex (with/without `#`), named, RGB, theme | `FF0000`, `#FF0000`, `red`, `rgb(255,0,0)`, `accent1`..`accent6` | +| Spacing | Unit-qualified | `12pt`, `0.5cm`, `1.5x`, `150%` | +| Dimensions | EMU or suffixed | `914400`, `2.54cm`, `1in`, `72pt`, `96px` | + +**Dotted-attr aliases** — `font.` forms accepted on shape/run/paragraph/table/row/cell/section/styles, e.g. `--prop font.color=red --prop font.bold=true --prop font.size=14pt`. Run `officecli help ` for the full list. + +### find — format or replace matched text + +Use `find=` with `set` to target specific text for formatting or replacement. Format props are separate `--prop` flags — do NOT nest them. + +```bash +# Format matched text (auto-splits runs) +officecli set doc.docx '/body/p[1]' --prop find=weather --prop bold=true --prop color=red + +# Regex matching +officecli set doc.docx '/body/p[1]' --prop 'find=\d+%' --prop regex=true --prop color=red + +# Replace text (use `/` for whole-document scope) +officecli set doc.docx / --prop find=draft --prop replace=final + +# PPT — same syntax, different paths +officecli set slides.pptx / --prop find=draft --prop replace=final +``` + +**Path controls search scope:** `/` = whole document, `/body/p[1]` or `/slide[N]/shape[M]` = specific element, `/header[1]` / `/footer[1]` = headers/footers. + +**Notes:** +- Case-sensitive by default. Case-insensitive: `--prop 'find=(?i)error' --prop regex=true` +- Matches work across run boundaries +- No match = silent success. `--json` includes `"matched": N` +- **Excel:** only `find` + `replace` supported (no find + format props) + +### add — add elements or clone + +```bash +officecli add --type [--prop ...] +officecli add --type --after [--prop ...] # insert after anchor +officecli add --type --before [--prop ...] # insert before anchor +officecli add --type --index N [--prop ...] # 0-based position (legacy) +officecli add --from # clone existing element +``` + +`--after`, `--before`, `--index` are mutually exclusive. No position flag = append to end. + +**Element types (with aliases):** + +| Format | Types | +|--------|-------| +| **pptx** | slide (incl. hidden), shape (textbox — font.latin/ea/cs, direction=rtl), picture (SVG, brightness/contrast/glow/shadow), chart (direction=rtl), table (cell direction=rtl), row (tr), connector (connection/line), group, video (audio/media, trim), equation (formula/math), notes (direction=rtl, lang), comment (RTL via U+200F bidi mark; full CRUD via /slide[N]/comment[M]), paragraph (para), run, zoom (slidezoom), ole (oleobject/object/embed), placeholder (phType=title/body/subtitle/footer/...). slideLayout/slideMaster direction inheritance. | +| **docx** | paragraph (para — direction/font.latin/ea/cs, bold.cs/italic.cs/size.cs for RTL/CJK; lang.latin/ea/cs BCP-47 tags on run; wordWrap toggle), run, table (direction=rtl → bidiVisual), row (tr), cell (td), image (picture/img — SVG supported), header (direction), footer (direction), section (pageNumFmt full ECMA-376 enum incl. Hindi/Arabic/Thai/CJK numerals; direction=rtl on Add/Set; rtlGutter; pgBorders=box shorthand), bookmark, comment, footnote, endnote, formfield (text/checkbox/dropdown), sdt (contentcontrol), chart, equation, field (28 types incl. mergefield/ref/seq/styleref/docproperty/if), hyperlink, style (direction round-trip), toc, watermark, break (pagebreak/columnbreak), ole, **num / abstractNum / lvl** (numbering/list system), **tab** (paragraph or paragraph/table style tab stops). docDefaults.rtl document-wide override; `get /` exposes `locale`. Document protection: `set / --prop protection=forms\|readOnly\|comments\|trackedChanges\|none` | +| **xlsx** | sheet (visible/hidden/veryHidden, print margins, printTitleRows/Cols, rightToLeft sheetView, cascade-aware rename), row, cell (type=richtext+runs, merge=range/sweep, direction=rtl, phonetic guide on add), chart (direction=rtl on per-axis txPr / title; incl. pareto), image (picture — SVG), comment (direction=rtl), table (listobject), namedrange (definedname, volatile, `[@name=X]` selector), pivottable (pivot, calculatedField), sparkline, validation (datavalidation), autofilter, shape, textbox, databar/colorscale/iconset/formulacf/cellIs/topN/aboveAverage (conditional formatting), ole, csv (tsv). Query supports `merge`/`mergedrange` aliases for `mergeCell`. Workbook: password. `value="=SUM(...)"` auto-detects as formula. Chart/picture/shape/slicer accept `anchor=A1:E10`. | + +### Pivot tables (xlsx) + +```bash +officecli add data.xlsx /Sheet1 --type pivottable \ + --prop source="Sheet1!A1:E100" --prop rows=Region,Category \ + --prop cols=Year --prop values="Sales:sum,Qty:count" \ + --prop grandTotals=rows --prop subtotals=off --prop sort=asc +``` + +Key props: `rows`, `cols`, `values` (Field:func[:showDataAs]), `filters`, `source`, `position`, `layout` (compact/outline/tabular), `repeatLabels`, `blankRows`, `aggregate`, `showDataAs` (percent_of_total/row/col, running_total), `grandTotals`, `subtotals`, `sort`. Aggregators: sum, count, average, max, min, product, stdDev, stdDevp, var, varp, countNums. Date columns auto-group. Run `officecli help xlsx pivottable` for full schema. + +### Document-level properties (all formats) + +```bash +officecli set doc.docx / --prop docDefaults.font=Arial --prop docDefaults.fontSize=11pt +officecli set doc.docx / --prop protection=forms --prop evenAndOddHeaders=true +officecli set data.xlsx / --prop calc.mode=manual --prop calc.refMode=r1c1 +officecli set slides.pptx / --prop defaultFont=Arial --prop show.loop=true --prop print.what=handouts +``` + +Run `officecli help /` for all document-level properties (docDefaults, docGrid, CJK spacing, calc, print, show, theme, extended). + +### Sort (xlsx) + +```bash +officecli set data.xlsx /Sheet1 --prop sort="C desc" --prop sortHeader=true +officecli set data.xlsx '/Sheet1/A1:D100' --prop sort="A asc" --prop sortHeader=true +``` + +Format: `COL DIR[, COL DIR ...]`. Rejects ranges with merged cells or formulas. Sidecar metadata (hyperlinks, comments, conditional formatting, drawings) follows rows automatically. + +### Text-anchored insert (`--after find:X` / `--before find:X`) + +Locate an insertion point by text match within a paragraph. Inline types (run, picture, hyperlink) insert within the paragraph; block types (table, paragraph) auto-split it. PPT only supports inline. + +```bash +# Word: inline run after matched text +officecli add doc.docx '/body/p[1]' --type run --after find:weather --prop text=" (sunny)" + +# Word: block table after matched text (auto-splits paragraph) +officecli add doc.docx '/body/p[1]' --type table --after "find:First sentence." --prop rows=2 --prop cols=2 +``` + +### Clone + +`officecli add / --from '/slide[1]'` — copies with all cross-part relationships. + +### move, swap, remove + +```bash +officecli move [--to ] [--index N] [--after ] [--before ] +officecli swap +officecli remove '/body/p[4]' +``` + +When using `--after` or `--before`, `--to` can be omitted — the target container is inferred from the anchor. + +### batch — multiple operations in one save cycle + +Stops on first error by default. Use `--force` to continue. + +```bash +echo '[ + {"command":"set","path":"/Sheet1/A1","props":{"value":"Name","bold":"true"}}, + {"command":"set","path":"/Sheet1/B1","props":{"value":"Score","bold":"true"}} +]' | officecli batch data.xlsx --json + +officecli batch data.xlsx --commands '[{"op":"set","path":"/Sheet1/A1","props":{"value":"Done"}}]' --json +officecli batch data.xlsx --input updates.json --force --json +``` + +Supports: `add`, `set`, `get`, `query`, `remove`, `move`, `swap`, `view`, `raw`, `raw-set`, `validate`. Fields: `command` (or `op`), `path`, `parent`, `type`, `from`, `to`, `index`, `after`, `before`, `props`, `selector`, `mode`, `depth`, `part`, `xpath`, `action`, `xml`. + +--- + +## L3: Raw XML + +Use when L2 cannot express what you need. No xmlns declarations needed — prefixes auto-registered. + +```bash +officecli raw # view raw XML +officecli raw-set --xpath "..." --action replace --xml '...' +officecli add-part # create new document part (returns rId) +``` + +`raw-set` actions: `append`, `prepend`, `insertbefore`, `insertafter`, `replace`, `remove`, `setattr`. Run `officecli help raw` for available parts. + +--- + +## Common Pitfalls + +| Pitfall | Correct Approach | +|---------|-----------------| +| `--name "foo"` | Use `--prop name="foo"` — all attributes go through `--prop` | +| Unquoted `[N]` paths in zsh/bash | Always quote: `'/slide[1]'` or `"/slide[1]"` (shell glob-expands brackets) | +| PPT `shape[1]` for content | `shape[1]` is typically the title placeholder. Use `shape[2]+` for content shapes | +| `/shape[myname]` | Name indexing not supported. Use numeric index or `@name=` (PPT only) | +| Guessing property names | Run `officecli help ` to see exact names | +| Modifying an open file | Close the file in PowerPoint/WPS first | +| `\n` in shell strings | Use `\\n` for newlines in `--prop text="..."` | +| `$` in shell text | `--prop text="$15M"` strips `$15`. Use single quotes: `--prop text='$15M'`, or heredoc batch | + +--- + +## Specialized Skills + +`officecli load_skill ` — output is a SKILL.md, follow its rules. + +**Loading rule**: +- Pick the most specific match in "When to use"; if none fits, load the format default (`word` / `pptx` / `excel`). +- Scenes already contain the format default's rules — load **one** skill per artifact, never stack. +- Loaded rules persist across turns; don't re-load each reply. +- Two distinct artifacts → two separate loads. + +### Word (.docx) + +| Name | When to use | +|------|-------------| +| `word` | Reports, letters, memos, proposals, generic documents | +| `academic-paper` | Journal / conference / thesis: APA / Chicago / IEEE / MLA citations, equations, SEQ + PAGEREF cross-refs, multi-column journal layout, bibliography. NOT for business reports or letters (route those to `word`) | + +### PowerPoint (.pptx) + +| Name | When to use | +|------|-------------| +| `pptx` | Generic decks: board reviews, sales decks, all-hands, product launches | +| `pitch-deck` | **Fundraising only** — seed / Series A-C / SAFE / convertible / strategic raise. NOT for sales / product / board decks (route those to `pptx`) | +| `morph-ppt` | Cinematic Morph-animated presentations. NOT for static decks (route those to `pptx`) | +| `morph-ppt-3d` | 3D Morph: GLB models, camera moves, depth. NOT for 2D-only Morph (route those to `morph-ppt`) | + +### Excel (.xlsx) + +| Name | When to use | +|------|-------------| +| `excel` | Generic workbooks, formulas, pivots, trackers | +| `financial-model` | Financial models, scenarios, projections. NOT for general data analysis (route those to `excel`) | +| `data-dashboard` | CSV/tabular data → KPI / analytics / executive dashboards with charts and sparklines. NOT for raw data tracking (route those to `excel`) | + +Example: a fundraising deck task → `officecli load_skill pitch-deck` → use the printed rules. + +--- + +## Notes + +- Paths are **1-based** (XPath convention): `'/body/p[3]'` = third paragraph +- `--index` is **0-based** (array convention): `--index 0` = first position +- After modifications, verify with `validate` and/or `view issues` +- **When unsure**, run `officecli help ` instead of guessing