| Signal | Ships in | Command(s) | Access | Write restrictions |
|---|---|---|---|---|
| GitHub | stock | github query, github show |
Read-only | — |
| GitLab | stock | gitlab query, gitlab show |
Read-only | — |
| Gitea / Forgejo | stock | gitea query, gitea show (forgejo) |
Read-only | — |
| Notes / Tasks / Reminders / Buckets | stock | mino notes |
Read + write | Local DuckDB store under <home>/.data: notes, tasks, reminders, plus buckets and bucket_members. |
| Google Calendar | overlay | calendar query (cal) |
Read-only | — |
| Gmail | overlay | gmail query |
Read-only | — |
| Google Docs | overlay | docs query |
Read-only | — |
| Google Drive | overlay | drive query, drive add |
Read + write | Creates a file only in the configured plugins.drive.dir; a write to any other folder is rejected before the API call. Reads any folder. Uses the full drive OAuth scope (folder discovery + create). |
| Google Tasks | overlay | tasks query, tasks add |
Read + write | Creates a task only in the configured plugins.tasks.list; a write to any other list is rejected before the API call. Reads any list. |
| Slack | overlay | slack query (--channel, --mentions, --dms, --search), slack show <permalink> |
Read-only | — |
| Kubernetes | overlay | kubectl query (k8s) |
Read-only | Shells out to kubectl get … -o json only. Registers no actions, and never writes to your kubeconfig — see Kubernetes. |
| ArgoCD | overlay | argocd query, argocd show <url> |
Read-only | Never sends refresh, which would force a reconcile against the cluster. |
| Demo | overlay | query demo |
Read-only | Synthetic items for smoke-testing notifications. |
The write restriction is Mino policy enforced in cmd/tasks.go:resolveWriteTarget
before the API call — the OAuth token itself grants broader write access, so the
guardrail is Mino's, not the scope's. Writes are recorded in the audit trail as
write runs.
mino tasks add "review the RFC" --due 2026-07-25 --notes "focus on the API"
mino tasks add "oops" --list "Someone Else's List" # → rejected: read-only
mino drive add "notes.txt" --content "hello" --mime text/plain
mino drive add "x" --dir "Some Other Folder" # → rejected: read-onlyGitea has no search language. Its cross-repository endpoint takes typed parameters
instead, and the actor filters (created, assigned, mentioned, review_requested,
reviewed) are resolved against the token's own user rather than a name in the query. So
params.query is a small key:value expression that mino parses into those parameters, and an
unsupported qualifier is a config error rather than a silently ignored text term:
name: gitea-review-requests
type: query
signal: gitea
params:
title: "git.acme · review requests"
query: "type:pulls state:open review_requested:@me"name: tools-bugs
type: query
signal: gitea
params:
query: 'repo:acme/tools type:issues state:open labels:bug,regression'| Qualifier | Effect |
|---|---|
type:pulls|issues|all |
aliases: pr, prs, pull, issue |
state:open|closed|all |
defaults to open |
is:open, is:closed, is:pr, is:issue |
GitHub-shaped shorthands for the two above |
repo:owner/name |
switches to the single-repository endpoint; at most one |
owner:acme |
aliases: org:, user:; every repository under that owner |
team:platform |
cross-repository only |
labels:a,b, milestone:m |
comma-OR'd; quote values with spaces |
created:@me |
alias author:; also assigned: (assignee:), mentioned: (mentions:), review_requested: (review-requested:), reviewed: |
since:/before: |
an RFC3339 timestamp, or a window like 7d or 24h |
q:"free text", or any bare word |
full-text search |
Query expressions are parsed when the signal is built, so a typo fails as soon as the query runs rather than returning an empty section.
@me means the token, not a name. Where GitHub needs github.viewer to stand in for
@me, Gitea already scopes those filters to whoever the credential belongs to — there is no
substitution and no "matches nothing as a service identity" trap. The flip side is that
Gitea cannot express "review requested from someone else" across repositories, so
review_requested:alice is a config error rather than a query that quietly means something
else. Two places do need a login, and gitea.viewer supplies it (otherwise mino asks the
instance once per run): owner:@me, and the per-repository forms of created/assigned/
mentioned, which Gitea spells created_by/assigned_by/mentioned_by and which take a
name. Setting gitea.viewer to someone other than the token's user does not redirect the
boolean filters.
Two more differences worth knowing:
- Gitea returns a bare array and puts the total in the
X-Total-Countheader. When a response fills the page and carries no such header, the section is marked truncated rather than presented as the whole answer. - The realtime stream cursors on the
sinceparameter, because Gitea sends noLast-Modifiedfor/notifications.sinceis inclusive, so the boundary thread is re-read each poll and dropped by the seen set; the notification you already had whenmino servestarted is treated as history, not news.
The github signal has two modes. With query: it runs a GitHub search
(is:open is:pr author:@me). With project: it reads a Projects v2 board —
one section per board column, which search alone cannot express, because a
column is a project field value and status: is not a search qualifier:
name: board-in-progress
type: query
signal: github
params:
project: acme/17 # owner/number, or a project URL
filter: 'status:"In Progress" repo:acme/escalations is:open -is:pr'
title: Escalations · In Progress # optional section heading
field: Status # optional, defaults to Status
team: acme/platform # optional, owner/team-slugfilter: takes the same syntax as a board view's filter bar, so a view's filter
copies straight across: status:, repo:, is: (open/closed/merged/
draft/issue/pr), assignee:, author:, label:, no:, sort:, plus
bare words as title/body text. Values are comma-OR'd, - negates, quote values
containing spaces, and @me resolves to the authenticated user. An unsupported
qualifier is a config error rather than a silently-ignored text term.
Everything except status:/no: runs server-side through the search API scoped
to project:owner/number; the field value is read from each result's
projectItems and matched locally. This keeps a query to one or two API calls —
paging a whole board would be one call per 100 items. Board columns hold only
issues and pull requests this way; draft (note) cards are not searchable.
Reading a project needs the read:project scope, which is not in the default
device-flow scope set: gh auth refresh -s read:project, or add it to
github.oauth_scopes before mino login github.
Each item carries the field value in meta.status, so filter rules can narrow
further (field: meta.status, field: meta.labels, field: meta.assignees).
GitLab's REST API has no free-text search DSL, so the gitlab signal takes a
selector: space-separated key:value terms, each mapping 1:1 onto a
documented REST parameter or path prefix. It is a parameter spelling, not a
search language, and an unsupported term is a config error rather than a
silently-ignored text term.
name: gitlab-my-mrs
type: query
signal: gitlab
params:
query: "kind:mr scope:assigned state:opened label:backend"| Term | Values | Maps to |
|---|---|---|
kind: |
mr (default), issue, pipeline |
the surface and its endpoint |
project: |
group/sub/project or a numeric id |
/projects/{id}/… |
group: |
group/subgroup |
/groups/{id}/… |
state: |
opened, closed, merged, locked, all |
state (merged/locked are MR-only) |
scope: |
assigned, created, all (or GitLab's own literals); for pipelines running, pending, finished, branches, tags |
scope |
author: assignee: reviewer: |
a username or @me |
author_username, assignee_username, reviewer_username (reviewer: is MR-only) |
label: |
repeatable | labels, comma-joined (GitLab ANDs them) |
milestone: |
a title; quote to include spaces | milestone |
search: |
free text | search — the escape hatch |
draft: |
true, false |
wip (MR-only) |
since: |
7d or 2026-01-02 |
updated_after |
sort: |
updated, created |
order_by + sort=desc |
status: ref: username: |
pipeline-only | status, ref, username |
Two constraints worth knowing:
kind:pipelinerequires aproject:. GitLab has no instance-wide pipelines endpoint.@meis resolved by mino, not by GitLab. GitLab has no server-side alias:author_username=@mematches a user literally named@meand returns an empty list with a 200. Mino substitutesgitlab.viewerif set, otherwise one cachedGET /user; if neither resolves, the query fails rather than returning nothing.scope:assignedandscope:createdresolve server-side and need no viewer at all — prefer them, especially for a service identity.
mino gitlab show <url> renders a merge request, issue or pipeline. mino show
picks the signal from the URL's host, so it works without --signal for
gitlab.com, github.com, and any host matching a configured gitlab.api_url or
github.api_url.
Read-only, against the ArgoCD REST API. One item per Application, ordered worst-first so a broken deploy sits at the top of the panel.
name: argocd-unhealthy
type: query
signal: argocd
params:
only_unhealthy: "true"
project: platform
max: "25"Minimum config is the server URL:
plugins:
argocd:
server_url: https://argocd.example.comserver_url must be https — mino refuses to send the API token over plain
http, the same rule the GitHub signal applies to github.api_url. For a private
or self-signed CA, point plugins.argocd.ca_file at the PEM bundle rather than
disabling verification.
Mint a token and grant it read access:
argocd account generate-token --account mino
export ARGOCD_AUTH_TOKEN=...p, role:mino, applications, get, */*, allow
g, mino, role:mino
A token sealed under the argocd credential key takes precedence over the
environment variable, so a stale $ARGOCD_AUTH_TOKEN from the argocd CLI
cannot silently repoint mino at a different server. Change which variable is
consulted with plugins.argocd.token_env.
The plugin never sends refresh. GET /applications?refresh=hard forces a
reconcile, which is a write-ish side effect against the cluster; a test asserts
the parameter's absence.
Each item carries the sync/health rollup in meta.state (synced,
out of sync, progressing, degraded, missing, failed, suspended,
unknown) plus meta.sync, meta.health, meta.phase, meta.revision,
meta.project, meta.cluster, and meta.namespace — all available to filter
rules and formatters. An application mid-sync sets meta.in_progress, which
drives the row spinner and the detail view's live re-poll.
mino argocd show <application-url> opens the detail panel: resource breakdown
with per-resource health, the last five sync-history entries, the last
operation with any failed resources, commit metadata, and the ArgoCD conditions
that explain a stuck application.
Two namespace settings exist and they are not interchangeable. app_namespace
is where the Application resource lives and is sent to the API as
appNamespace; apps outside the default argocd namespace need it. namespaces
filters on spec.destination.namespace, where the workloads land — the list API
cannot filter on that, so mino applies it client-side after fetching.
For a board column like Waiting, the useful question is not who opened an item
but who spoke last. Every project item carries meta.last_comment_by — the
author of the last human comment, skipping bots, falling back to the issue
author when there are no comments. Only the last few comments are inspected, so a
thread whose recent history is all bots reports the author.
meta.last_comment_at carries when that comment landed (RFC3339, the item's open
time for the author fallback).
Rows render a reply chip next to the author, ending in how long ago that comment
landed: ↩ @cust22 ·3d ago. Because the chip already dates the thread, it
replaces the row's usual updatedAt time rather than sitting beside it.
Set team: owner/team-slug and each item also gets meta.last_comment_team
(true when the last commenter is on that team). The chip then reads green
↩ @alice ·team ·3d ago when a teammate replied last and amber
↩ @cust22 ·3d ago when the reply came from outside, and a filter rule can keep
only one side:
name: escalations-waiting
type: query
signal: github
params:
project: acme/17
filter: 'status:Waiting repo:acme/escalations is:open -is:pr'
team: acme/platform
rules:
- field: meta.last_comment_team
include: "false"Team membership costs one extra GraphQL call, cached for 24h in
.data/serve.duckdb, and needs the read:org scope (part of the default
scope set). Without team:, meta.last_comment_team is absent and the chip
renders dim — so a missing key always means "not configured", never "external".
meta.last_comment_at is unaffected by team: and present either way.
The kubectl signal is an overlay plugin (external.kubectl). It reads a
cluster by shelling out to the kubectl binary already on your PATH — there is
no client-go in mino — so your kubeconfig, exec-credential plugins, OIDC, and
proxies all work exactly as they do in your shell. kubectl must be installed;
point plugins.kubectl.binary at it if it lives somewhere unusual.
Four collectors run concurrently, one section each:
| Collector | Reports |
|---|---|
pods |
Pods that are failed, pending, crash-looping, unready, or above the restart threshold. Succeeded pods and ones still starting up are skipped. |
events |
type=Warning events inside the since window, newest first. |
nodes |
Nodes that are NotReady, cordoned, or under memory/disk/PID/network pressure. |
workloads |
Deployments, StatefulSets, and DaemonSets with fewer ready replicas than desired, or a rollout still updating. Scaled-to-zero workloads are skipped. |
A fifth collector, context, reports the selected context instead of querying
anything. Pick collectors with --what, or the what: query param:
mino kubectl query # all four, all namespaces
mino kubectl query -n payments --what pods,events
mino kubectl query --context prod --since 6h -o jsonA healthy cluster produces empty sections rather than noise, and a collector that fails — RBAC, an unreachable API server — comes back as a section with an error on it, so one broken cluster never aborts a flight.
mino context switch kubectl prod and a role's contexts: { kubectl: prod }
binding record the selection in mino only. Mino then passes it as
--context prod on every read. It never runs kubectl config use-context, so
your kubeconfig's current context — and every other shell on the machine — is
untouched. The precedence for which context gets read is:
- the
contextquery param (--context) - the in-process selection from
mino context switchor a role binding plugins.kubectl.contextin config- whatever your kubeconfig already says
The signal is read-only end to end: it registers no actions, and every
invocation is a kubectl get.