Skip to content

Latest commit

 

History

History
330 lines (273 loc) · 15.8 KB

File metadata and controls

330 lines (273 loc) · 15.8 KB

Data signals

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-only

Gitea and Forgejo queries

Gitea 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-Count header. 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 since parameter, because Gitea sends no Last-Modified for /notifications. since is inclusive, so the boundary thread is re-read each poll and dropped by the seen set; the notification you already had when mino serve started is treated as history, not news.

GitHub project boards

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-slug

filter: 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 selectors

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:pipeline requires a project:. GitLab has no instance-wide pipelines endpoint.
  • @me is resolved by mino, not by GitLab. GitLab has no server-side alias: author_username=@me matches a user literally named @me and returns an empty list with a 200. Mino substitutes gitlab.viewer if set, otherwise one cached GET /user; if neither resolves, the query fails rather than returning nothing. scope:assigned and scope:created resolve 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.

ArgoCD

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.com

server_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.

Who owes the next reply

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.

Kubernetes

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 json

A 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.

The kubeconfig is never modified

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:

  1. the context query param (--context)
  2. the in-process selection from mino context switch or a role binding
  3. plugins.kubectl.context in config
  4. whatever your kubeconfig already says

The signal is read-only end to end: it registers no actions, and every invocation is a kubectl get.