Files
open-project/skills/openproject/SKILL.md
T

159 lines
9.1 KiB
Markdown
Raw Normal View History

2026-06-27 17:25:55 -05:00
---
name: openproject
description: Interact with the user's self-hosted OpenProject instance over its REST API v3. Use whenever the user wants to view, search, create, update, comment on, close, delete, or report on OpenProject projects, work packages, tasks, bugs, features, milestones, tickets, or time entries — e.g. "what's assigned to me in OpenProject", "create a task in the Infra project", "move #1234 to In Progress", "log 2 hours on that ticket", "show open bugs", "comment on work package 88", "list my projects". Also triggers on bare references to a work package number (#1234) or an OpenProject URL when the intent is to inspect or change it. Do NOT use for unrelated project-management tools (Jira, Asana, Trello, GitHub issues, Gitea) or for generic local task tracking.
---
# OpenProject
Read and manage the user's self-hosted **OpenProject** instance through its API v3.
The server URL and API token are configured in `config.json` at the plugin root
(baked for this single-user, local-network install). Everything runs through one
stdlib-only Python CLI — **no pip installs, no external dependencies**, works on
macOS/Linux/Windows with just a Python 3 interpreter.
## The tool
All logic lives in `${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py`.
Invoke it with `python3` (use `python` or `py -3` on Windows if `python3` isn't on PATH):
```bash
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
python3 "$OP" <command> [options]
```
If `${CLAUDE_PLUGIN_ROOT}` is not set in your shell, use the absolute path to this
skill's `scripts/op.py`. Every command accepts `--json` for raw HAL+JSON output
(prefer this when you need exact field values to chain into another call; otherwise
the default human-readable tables are easier to read back to the user).
**Always run `ping` first in a session** if you're unsure the token is valid — it
fails fast with a clear message instead of a confusing error mid-task.
## Command reference
| Goal | Command |
|------|---------|
| Verify connection + auth | `op.py ping` |
| Who am I | `op.py me` |
| List / search projects | `op.py projects [--search TEXT]` |
| List / search work packages | `op.py wp list [--project P] [--assignee me] [--status open\|closed\|NAME] [--type T] [--query TEXT] [--sort field:asc] [--limit N]` |
| Show one work package | `op.py wp show ID` |
| Create a work package | `op.py wp create --project P --subject "..." [--type T] [--description "..."] [--assignee U] [--status S] [--priority P] [--parent ID] [--start YYYY-MM-DD] [--due YYYY-MM-DD]` |
| Update a work package | `op.py wp update ID [--subject] [--status] [--assignee] [--description] [--priority] [--done-ratio N] [--start] [--due] [--parent]` |
| Add a comment | `op.py wp comment ID --text "..."` |
| Change status / close | `op.py wp close ID [--status-name "Closed"]` |
| Delete (guarded) | `op.py wp delete ID --yes` |
| Log time | `op.py time log --wp ID --hours 1.5 [--comment "..."] [--date YYYY-MM-DD] [--activity NAME]` |
| Relate two work packages | `op.py wp relate ID --to ID --type blocks\|precedes\|relates\|requires\|...` |
| Create a project | `op.py project create --name "..." [--identifier slug] [--description] [--parent REF]` |
| Add a project member | `op.py members add --project P --user U --role "Member"` |
| Import a spreadsheet | `op.py import xlsx FILE.xlsx --sheet "Sheet" --project-name "..." [--execute]` |
| List users / roles | `op.py users [--search TEXT]` · `op.py roles` |
| List types / statuses / priorities | `op.py types [--project P]` · `op.py statuses` · `op.py priorities` |
| Anything else (escape hatch) | `op.py raw GET\|POST\|PATCH\|DELETE /api/v3/... [--data '{...}']` |
## How to work with this
- **Friendly references resolve automatically.** `--project`, `--assignee`, `--type`,
`--status`, `--priority` accept either a numeric id **or a name** (e.g.
`--project "Infrastructure"`, `--assignee me`, `--status "In progress"`,
`--priority High`). The CLI looks up the id for you. `--assignee me` and
`--assignee none` (unassign) are special.
- **Updates are concurrency-safe.** `wp update` and `wp close` fetch the current
`lockVersion` automatically before PATCHing, so you never need to manage it. If an
update fails with a lockVersion error, the record changed underneath you — just retry.
- **Status filtering shortcuts:** `--status open` and `--status closed` use the API's
open/closed operators; any other value is treated as a status name/id.
- **Prefer the typed subcommands** over `raw`. Reach for `raw` only for endpoints the
CLI doesn't wrap (versions, categories, queries, attachments, notifications, etc.) —
see `reference/api-map.md` for the endpoint catalog and `reference/filters.md` for
the filter JSON syntax.
## Importing a spreadsheet into a project
`op.py import xlsx` reads an `.xlsx` (via the bundled stdlib `xlsx_read.py` — no
openpyxl) and builds a project: one parent **Summary task** per Category, with each
row as a child Task. It is a **dry run by default** — it prints the plan (row count,
category hierarchy, owner matches, value mappings, a sample) and writes nothing until
you add `--execute`. Always run the dry run first and show the plan to the user.
```bash
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
# 1) Preview
python3 "$OP" import xlsx "Plan.xlsx" --sheet "To-Do List" --project-name "My Project"
# 2) Commit
python3 "$OP" import xlsx "Plan.xlsx" --sheet "To-Do List" --project-name "My Project" \
--identifier my-project --execute
```
- **Column detection** is automatic and case-insensitive by prefix: a Task/Title/Name
column → subject; Category/Group → parent; Owner/Assignee → assignee; plus
Priority, Status, Due, Notes/Dependencies, and `#`/Id. Notes, due-date text, owner,
and source row # are preserved in each task's description.
- **Value mapping:** priority H/M/L → High/Normal/Low; status Open/In Progress/
Complete → New/In progress/Closed (unknown → New). Setting a non-default status on
create is handled with an automatic create-then-patch fallback.
- **Owners:** matched to existing users when possible; otherwise the name is recorded
in the description (no failed rows). People aren't auto-created (no emails to do so).
- **Blockers/dependencies** are only linked as real relations if the sheet has a
column referencing other rows. Free-text dependencies stay in the description —
use `op.py wp relate` to wire them up explicitly afterward.
- `--merge-category "Old=New"` (repeatable) merges near-duplicate category names;
`--limit N` imports only the first N rows (good for a trial run); `--task-type`
overrides the child type (default `Task`).
## Guardrails (full CRUD, with confirmation on destructive actions)
- **Deletes require `--yes`** and the CLI refuses otherwise. Before deleting anything,
confirm with the user in plain language (show what `wp show ID` returns first), then
run with `--yes`. The same caution applies to bulk changes and any `raw DELETE`.
- **Show before you change.** For an update/close/delete on a specific item, run
`wp show ID` first so you (and the user) see the current state.
- **Never print the API token.** It lives only in `config.json`. Don't echo it, don't
paste it into a work package, comment, or any external surface.
- When a write succeeds, report the resulting id/status and the web URL
(`<server>/work_packages/<id>`) so the user can click through.
## Common recipes
```bash
OP="${CLAUDE_PLUGIN_ROOT}/skills/openproject/scripts/op.py"
# What's on my plate (open items assigned to me)
python3 "$OP" wp list --assignee me --status open
# Open bugs in a project, newest first
python3 "$OP" wp list --project "Infrastructure" --type Bug --status open --sort id:desc
# File a task
python3 "$OP" wp create --project "Infrastructure" --type Task \
--subject "Rotate TLS certs" --description "Expires next month." \
--assignee me --priority High --due 2026-07-15
# Progress an item and log time against it
python3 "$OP" wp update 1234 --status "In progress" --done-ratio 40
python3 "$OP" wp comment 1234 --text "Halfway — blocked on DNS."
python3 "$OP" time log --wp 1234 --hours 1.5 --comment "cert work"
# Inspect, then close
python3 "$OP" wp show 1234
python3 "$OP" wp close 1234
```
## Troubleshooting
- `error: No API key configured` / `HTTP 401 Unauthenticated` → the token in
`config.json` is missing/placeholder/expired. Ask the user for a fresh token from
OpenProject → **My Account → Access tokens → API**, and write it into the `api_key`
field of the plugin's `config.json` (or set `OPENPROJECT_API_KEY` in the env, which
overrides the file).
- `Could not reach <url>` → the server is down or off-network. Confirm the URL in
`config.json` (`OPENPROJECT_URL` overrides it) and that the machine can reach it.
- `HTTP 422` on create/update → a required field or invalid link. The error message
echoes the API's validation detail; re-check type/status/priority names exist for
that project (`op.py types --project P`, `op.py statuses`).
- `HTTP 422: The chosen user is not allowed to be 'Assignee'` → that user isn't a
member of the project (with an assignable role). Add them in OpenProject first, or
create the work package without `--assignee`.