omp packages

pi-redmine0.6.0

Pi package that adds curated Redmine tools to the Pi coding agent via the Redmine REST API.

GitHub Actions Published 7 days ago MIT ~309 downloads/mo

Untested on omp

This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.

Did it work for you?

Result

One report per person. A new report replaces your old one.

Reports 0

No reports yet.

Readme

pi-redmine

A Pi package that adds curated Redmine tools to the Pi coding agent. It calls the Redmine REST API directly (no MCP server / Python process required).

Install

# from npm (Pi package gallery)
pi install npm:pi-redmine

# or from git
pi install git:github.com/meraks/pi-redmine

# try without installing
pi -e npm:pi-redmine
# or
pi -e git:github.com/meraks/pi-redmine

Source: https://github.com/meraks/pi-redmine

Configure

In Pi, run /redmine-config (interactive), or create ~/.pi/pi-redmine/config.json:

{
  "url": "https://redmine.example.com",
  "apiKey": "your_api_key",
  "projectAccess": { "my-project": "read" }
}

A new directory gets no access at all — projectAccess is what grants it (see Access control). Without it every tool answers with the current grants and a pointer to /redmine-allow.

Optional keys: username + password for basic auth (instead of apiKey).

A per-project override lives at ~/.pi/pi-redmine/projects/<key>/config.json (key = project basename + short hash of its absolute path). /redmine-config asks whether to save globally or for the current project only.

Precedence (highest first): .env in the project → project config file → global config file → environment variables:

export REDMINE_URL=https://redmine.example.com
export REDMINE_API_KEY=your_api_key          # preferred
# export REDMINE_USERNAME=you
# export REDMINE_PASSWORD=secret
# export REDMINE_ACCESS_MODE=readonly        # cap this session at read everywhere

Access is denied by default in every directory — see Access control before the first call.

A project's .env is the one source that a repository itself controls, so its REDMINE_URL is only honored when that same file also carries credentials (REDMINE_API_KEY, or REDMINE_USERNAME + REDMINE_PASSWORD). Without that rule, any repo you open could name a host and have your configured key sent to it.

TLS / self-signed Redmine

There is deliberately no "skip verification" switch: the only way to implement one is to disable certificate checks for the whole Node process, which would silently cover the LLM provider calls and every other extension too. To trust an internal or self-signed certificate, either install its CA or point Node at the PEM before starting Pi:

export NODE_EXTRA_CA_CERTS=/path/to/your-internal-ca.pem

Requests time out after 30s, accept cancellation, and never follow redirects (so the API key cannot be replayed to another host) — point REDMINE_URL at the final URL, not at a redirecting alias.

Access control

Every directory is denied by default. A call is only sent when this directory's config grants the request's project read or write — the rule in full:

One table per directory. A project's level is the most specific entry: project name > "*" > accessMode within a file, and the per-project config file overrides the global one entry by entry. REDMINE_ACCESS_MODE=readonly caps every write at read afterwards.

{
  "url": "https://redmine.example.com",
  "apiKey": "your_api_key",
  "projectAccess": {
    "my-project": "write",
    "other-project": "read"
  }
}

Values are write, read, none; a project that is not listed is none. "*" is the default for everything else ("*": "read" = read-only everywhere). Keys are a project identifier or numeric id — both work. The key is called projectAccess rather than projects on purpose: it names the right, like GitHub Actions' permissions: {contents: read, issues: write}, Flatpak's --filesystem=home:ro, systemd's ReadOnlyPaths= / ReadWritePaths=, or Deno's --allow-read / --allow-write. The same shape everywhere: a scope, then read or write.

accessMode: "readonly" is shorthand for one wildcard entry ("*": "read"), "full" for "*": "write" (the pre-0.5 behaviour); a project entry in the same file is more specific and wins. Note that a project config which says "my-project": "write" therefore widens a global "*": "read" — the global file sets defaults, the per-directory file is what you edit for this repo. The env var is the only real cap: REDMINE_ACCESS_MODE=readonly cannot be widened by any config, and an unreadable value is treated as readonly.

Granting access quickly

/redmine-allow                 # interactive: pick a visible project, then read/write/none
/redmine-allow my-project write
/redmine-allow "*" read        # read-only everywhere
/redmine-allow                 # the picker also shows the current grants

/redmine prints the effective levels for the current directory (levels already capped by the env var, so it never promises more than a call gets); /redmine-scope checks what the configured key can actually reach and warns when it is broader than the grants.

The write tools are only registered when this directory may write somewhere — with no grant, or only read ones, the four write tools do not exist for the model. Registration happens at load: a grant added by /redmine-allow … write in a read-only directory shows up after restarting Pi (the command says so); a grant removed takes effect immediately.

What this protects, and what it does not

Grants stop accidental and prompt-injected tool calls (an issue body that says "close all tickets", a loop that keeps updating). They are enforced in one place — every request the model can trigger passes the check, and requests Redmine cannot narrow to one project (site-wide search, unfiltered issue/time lists) need a "*" grant instead of silently scanning everything. redmine_list_projects is filtered down to the granted projects.

They cannot stop an agent that runs curl with the same key, and they cannot hide cross-project references inside an issue (relations, children) — only the server can. The hard boundary is what the key is allowed to do in Redmine, so for a real boundary use one Redmine user per repo:

  1. Admin creates a role with only View issues / View wiki / View time entries (pi-read) — and, if the agent should write, a second role adding Add/Edit issues, Log spent time, Edit wiki (pi-write).
  2. Admin creates a user agent-<repo>, member of that one project only, with that role.
  3. Put its API key in that directory's config and confirm with /redmine-scope: it should say "Key can see 1 project".

Then a compromised agent has nothing to spend its shell on. That step needs an admin once per repo — cheaper than auditing what an agent did with a broad key.

Upgrading from 0.4: nothing is granted until you add projectAccess. To keep the old "everything, read and write" behaviour, use "accessMode": "full" or "projectAccess": {"*": "write"}.

Tools

Tool Purpose
redmine_list_projects List visible projects
redmine_get_project Get a project (trackers + memberships)
redmine_list_issues Filter/list issues (standard + custom field filters)
redmine_get_issue Full issue detail (comments, relations, attachments)
redmine_create_issue Open a new issue (standard + custom fields)
redmine_update_issue Change fields / add a comment (standard + custom fields)
redmine_list_time_entries List logged time
redmine_log_time Log time against an issue/project
redmine_get_wiki_page Read a wiki page (title raw — spaces are fine, the tool URL-encodes it)
redmine_update_wiki_page Create/update a wiki page
redmine_search Site-wide search (issues defaults to true; pass issues: false to search only wiki pages/projects — at least one scope must be true)

The four tools that write to Redmine are only registered when this directory grants write somewhere, and every tool is refused outside the directory's grants (see Access control).

A /redmine command prints the current connection status and this directory's effective access; /redmine-config sets the connection interactively; /redmine-allow grants project access (see Access control); /redmine-scope reports how broad your API key really is; /redmine-fields [project] discovers custom fields (see below).

Issue fields & filters

Standard fields accepted by redmine_create_issue / redmine_update_issue:

subject, description, tracker_id, priority_id, status_id, assigned_to_id, category_id, fixed_version_id, parent_issue_id, start_date, due_date, done_ratio, estimated_hours (create adds project_id).

Clearing a field must be explicit. Redmine represents "empty" as an empty string, which a typed field cannot express (and an accidentally empty value must not silently wipe a field), so pass the field names to clear:

redmine_update_issue(issue_id=123, clear=["assigned_to_id"])            # unassign
redmine_update_issue(issue_id=123, clear=["due_date", "start_date"])   # blank the dates

Clearable: assigned_to_id, category_id, fixed_version_id, parent_issue_id, due_date, start_date, description — anything else is rejected with the list of valid names. Sending "" to a field is ignored, not treated as a clear.

What Redmine does with an empty value (verified against a live instance; "required" means it answers 422):

field empty value
assigned_to_id, category_id, fixed_version_id, parent_issue_id, due_date, start_date, description clears the field use clear
subject, priority_id 422 — required, cannot be empty pass a real value
tracker_id, status_id accepted but silently ignored, nothing changes pass a real id; redmine_get_project lists the trackers
done_ratio becomes 0 its "empty" is 0, no clear needed
a required custom field 422 — required, cannot be empty pass a real value
a non-required custom field cleared goes through custom_fields: [{id, value: ""}], not clear

"It succeeded but nothing changed": Redmine silently ignores fields that the project's tracker/workflow marks as not editable, and versions that are not shared with the project — the API still answers 201/204. If an update appears to have no effect (fixed_version_id and estimated_hours behave this way on some trackers), check that field's configuration in Redmine rather than assuming the tool failed.

Filtering with redmine_list_issues supports project_id, status_id, assigned_to_id, tracker_id, priority_id, category_id, fixed_version_id, parent_id, subject, sort, and custom-field filters via cf: [{id, value}]:

subject is a contains match: Redmine's own default operator for it is exact match, so the tool prefixes ~ unless you supply an operator yourself (e.g. =text for exact, !~text to exclude).

redmine_list_issues(project_id="my-project", cf=[{id:123, value:"..."}])

Custom-field ids come from /redmine-fields. Filtering uses each field's default operator (list = exact match, text = contains); != / empty / date-range filters need Redmine's advanced f[]/op[]/v[] syntax, which is not exposed here.

Custom fields are set via custom_fields: [{id, value}]. Redmine's custom-field definitions (format / tracker bindings) are admin-only via the API — a normal API key cannot list them. /redmine-fields works around this: it scans issues (read-only) and caches each field's id, name, and up to 5 sample values into ~/.pi/pi-redmine/config.json, which is usually enough to fill values on create/update:

/redmine-fields               # scan the newest 100 issues site-wide
/redmine-fields my-project  # scan issues in one project

redmine-fields only reads Redmine (a single GET /issues.json) and writes the local config cache — it never modifies Redmine data.

Develop

The extension is a single TypeScript module: extensions/redmine.ts. Pi loads .ts extensions directly, so no build step is needed. To try a local copy:

cd /tmp/scratch   # not this repo: its .env points at a real Redmine
pi -e /path/to/pi-redmine/extensions/redmine.ts

npm test is the safe path (local mock only):

npm test runs test/selfcheck.mjs — plain node:assert, no framework, no network: it points the tools at a local mock Redmine and checks config precedence, the .env rule, the access grants (deny by default, read/write per project, accessMode shorthand, the REDMINE_ACCESS_MODE ceiling, site-wide requests, which tools get registered), URL encoding, the search scope flags, the subject operator, and cancellation. fetch is patched so that a request to anything but the mock fails the run — the suite can never touch a real Redmine, even though this repo's own .env names one.

Live checks against a real instance are opt-in and deliberately narrow: npm run test:live needs REDMINE_URL and REDMINE_API_KEY in the environment (never from .env), refuses to run unless that key belongs to the dedicated test account named in REDMINE_TEST_ACCOUNT (see test/live.mjs), and rejects any non-GET request — it verifies reads, it never creates, updates or logs anything.

Then ask Pi something like "list my open Redmine issues" or "show issue 123".

Notes

  • Output is truncated to Pi's safe limits (~50KB / 2000 lines); refine queries if you hit the cap.
  • This package is intentionally a curated subset of Redmine. For the full surface, run redmine-mcp-server and bridge it from a separate extension.