Skip to content

Source script protocol

This document defines the contract between gtdhelper and external source scripts.

Source scripts are executable files in ~/.gtdhelper/sources/ that produce task data. gtdhelper discovers and runs them on each refresh cycle, parsing their stdout as JSON Lines.

Any executable file in ~/.gtdhelper/sources/ is treated as a source script. The source name is derived from the filename stem (e.g. github.py -> github, jira.sh -> jira, email-checker -> email-checker).

Subdirectories and non-executable files are ignored.

One-off: gtdhelper spawns the script, reads stdout until EOF, then collects the exit code. The script runs once per refresh cycle (every 5 minutes, or on manual refresh).

Scripts have a 30-second timeout. If a script doesn’t exit within 30 seconds, it is killed (SIGKILL) and treated as a failure.

Scripts write JSON Lines to stdout — one JSON object per line:

{"id":"github:org/repo/#42","title":"Fix login flow","reference":"#42","project":"org/repo","url":"https://github.com/org/repo/pull/42","type":"pull_request","created_at":"2025-01-15T10:00:00Z","updated_at":"2025-02-20T14:30:00Z"}
FieldTypeDescription
idstringGlobally unique task identifier. Must be stable across runs for the same logical task (e.g. github:org/repo/#42, trello:abc123).
titlestringHuman-readable task title displayed in the overlay.
referencestringShort reference shown alongside the title (e.g. #42, JIRA-123).
projectstringProject or group name used for grouping in the overlay (e.g. repository name, board name).
urlstringURL opened in the default browser when the user clicks the task.
created_atstringISO 8601 timestamp of when the task was created.
updated_atstringISO 8601 timestamp of the last activity on the task. Used for sorting.
FieldTypeDescription
typestringTask type for categorization (e.g. pull_request, issue, email)
is_draftbooleanWhether the task is a draft (e.g. draft PR). Affects icon styling.
is_botbooleanWhether the task author is a bot. Used for filtering.
  • The source field is set by gtdhelper to the script’s filename stem. Any source value in the JSON is ignored.
  • Lines that fail JSON parsing or lack required fields are silently skipped (with a warning in gtdhelper logs).
  • Empty lines are skipped.
  • The id must be stable across runs for the same logical task, so gtdhelper can track snooze/archive state.
StreamBehavior
stdinPiped from parent but unused. Reserved for future commands (see long-running mode).
stdoutCaptured by gtdhelper. Write JSON Lines here.
stderrInherited from parent process. Use for logging/diagnostics.
SignalBehavior
SIGINTGraceful shutdown. Sent on app exit. Scripts should clean up and exit promptly.
  • Non-zero exit code: Script is removed from the active pool (disabled). Its tasks from the previous run remain in the task list until the next successful run.
  • Reintegration: If a disabled script’s file is modified on disk (mtime change), gtdhelper retries it on the next refresh cycle.
  • Timeout: Treated the same as a non-zero exit (disabled, can be reintegrated).

Scripts can receive environment variables from their TOML config file. If a config file ~/.gtdhelper/<stem>.toml exists for a script (whether bundled or user-provided), any key-value pairs under the [env] section are injected as environment variables before the script runs.

~/.gtdhelper/myscript.toml
[env]
API_BASE_URL = "https://api.example.com"
API_TOKEN = "secret-token"

The script can then read API_BASE_URL and API_TOKEN as standard environment variables. All values must be strings.

The [env] section is optional. Config files can contain other top-level keys (used by the script itself, e.g. [[repos]] for GitHub) alongside the [env] section without conflict.

gtdhelper combines scripts from two locations:

  1. User scripts (~/.gtdhelper/sources/): Always included. Any executable file here runs on every refresh.
  2. Bundled scripts (bundled with the application): Only activated when a matching config file <stem>.toml exists in ~/.gtdhelper/. For example, the bundled github.py runs only if ~/.gtdhelper/github.toml exists.

If a user script has the same stem as a bundled script (e.g. github.sh vs github.py), the user script takes priority and the bundled one is skipped.

No automatic bootstrap — the user must create the config file to activate a bundled script.

Bundled Python (.py) scripts run inside an auto-managed virtualenv. The venv is transparent to the script — gtdhelper handles creation and dependency installation automatically.

  • Venv location: ~/.gtdhelper/env/
  • Creation: uv venv --python >=3.12 on first refresh (requires uv)
  • Dependencies: Synced from a bundled lock file via uv pip sync. The lock file contains pinned versions with SHA256 hashes. Dependencies are only re-synced when the lock file changes.
  • Interpreter injection: Bundled .py scripts are executed as <venv>/bin/python <script> instead of running the script directly.
  • User scripts are NOT affected: Scripts in ~/.gtdhelper/sources/ always run directly (as executable files). They manage their own interpreters and dependencies.
  • Graceful degradation: If uv is not installed, requirements.lock is missing, or the venv cannot be created, bundled .py scripts are skipped entirely.

The default github.py script ships with gtdhelper.

github.py reads its repo list from ~/.gtdhelper/github.toml:

[[repos]]
name = "org/repo"
types = ["pull_request", "issue"]
[[repos]]
name = "other/repo"
types = ["pull_request"]
  • name: GitHub repository in owner/repo format
  • types: list of "pull_request" and/or "issue"
  • If types is omitted, defaults to ["pull_request"]

The trello.py script ships with gtdhelper.

trello.py reads credentials from ~/.gtdhelper/trello.toml:

api_key = "your-api-key"
token = "your-token"
member = "your-trello-username"
boards = ["Product Board", "Engineering"]
  • api_key: Trello API key (get one at https://trello.com/power-ups/admin)
  • token: Trello API token (generated alongside the API key)
  • member: Trello username or member ID — cards assigned to this member are fetched
  • boards: list of board names to include (exact match, case-sensitive). If omitted or empty, no cards are shown — you must opt in per board.

Each Trello card produces a task with:

  • id: trello:<card-id>
  • title: card name
  • reference: #<card-short-id>
  • project: board name (fetched and cached per unique board)
  • url: card short URL
  • type: card
  • created_at: derived from the card’s ObjectId (embedded creation timestamp)
  • updated_at: dateLastActivity from the Trello API

The rss.py script ships with gtdhelper.

rss.py reads its feed list from ~/.gtdhelper/rss.toml:

[[feeds]]
name = "Rust releases"
url = "https://blog.rust-lang.org/feed.xml"
type = "release"
max_age_days = 7
include = ["release", "stable"]
exclude = ["beta", "nightly"]
FieldRequiredDefaultDescription
nameyesHuman label, used as project in task output
urlyesRSS or Atom feed URL
typeno"rss"Task type for UI filtering
max_age_daysno7Ignore items older than N days
includeno[] (accept all)Case-insensitive keywords, item must match at least one
excludeno[]Case-insensitive keywords, item is dropped if any match

Filter evaluation order: max_age first, then exclude, then include (if non-empty).

Each feed entry produces a task with:

  • id: rss:<slugified-name>:<sha256(entry-id-or-link)[:12]>
  • title: entry title
  • reference: feed name
  • project: feed name
  • url: entry link
  • type: value from config (default "rss")
  • created_at: entry’s published or updated timestamp, fallback to current time
  • updated_at: same as created_at