Source script protocol
This document defines the contract between gtdhelper and external source scripts.
Overview
Section titled “Overview”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.
Discovery
Section titled “Discovery”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.
Execution mode
Section titled “Execution mode”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.
Output format
Section titled “Output format”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"}Required fields
Section titled “Required fields”| Field | Type | Description |
|---|---|---|
id | string | Globally unique task identifier. Must be stable across runs for the same logical task (e.g. github:org/repo/#42, trello:abc123). |
title | string | Human-readable task title displayed in the overlay. |
reference | string | Short reference shown alongside the title (e.g. #42, JIRA-123). |
project | string | Project or group name used for grouping in the overlay (e.g. repository name, board name). |
url | string | URL opened in the default browser when the user clicks the task. |
created_at | string | ISO 8601 timestamp of when the task was created. |
updated_at | string | ISO 8601 timestamp of the last activity on the task. Used for sorting. |
Optional fields
Section titled “Optional fields”| Field | Type | Description |
|---|---|---|
type | string | Task type for categorization (e.g. pull_request, issue, email) |
is_draft | boolean | Whether the task is a draft (e.g. draft PR). Affects icon styling. |
is_bot | boolean | Whether the task author is a bot. Used for filtering. |
- The
sourcefield is set by gtdhelper to the script’s filename stem. Anysourcevalue 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
idmust be stable across runs for the same logical task, so gtdhelper can track snooze/archive state.
stdio contract
Section titled “stdio contract”| Stream | Behavior |
|---|---|
| stdin | Piped from parent but unused. Reserved for future commands (see long-running mode). |
| stdout | Captured by gtdhelper. Write JSON Lines here. |
| stderr | Inherited from parent process. Use for logging/diagnostics. |
Signals
Section titled “Signals”| Signal | Behavior |
|---|---|
| SIGINT | Graceful shutdown. Sent on app exit. Scripts should clean up and exit promptly. |
Error handling
Section titled “Error handling”- 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).
Environment variables
Section titled “Environment variables”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.
[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.
Two-tier script discovery
Section titled “Two-tier script discovery”gtdhelper combines scripts from two locations:
- User scripts (
~/.gtdhelper/sources/): Always included. Any executable file here runs on every refresh. - Bundled scripts (bundled with the application): Only activated when a matching config file
<stem>.tomlexists in~/.gtdhelper/. For example, the bundledgithub.pyruns only if~/.gtdhelper/github.tomlexists.
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 script runtime environment
Section titled “Bundled script runtime environment”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.12on first refresh (requiresuv) - 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
.pyscripts 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
uvis not installed,requirements.lockis missing, or the venv cannot be created, bundled.pyscripts are skipped entirely.
Bundled script: github.py
Section titled “Bundled script: github.py”The default github.py script ships with gtdhelper.
Configuration
Section titled “Configuration”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 inowner/repoformattypes: list of"pull_request"and/or"issue"- If
typesis omitted, defaults to["pull_request"]
Bundled script: trello.py
Section titled “Bundled script: trello.py”The trello.py script ships with gtdhelper.
Configuration
Section titled “Configuration”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 fetchedboards: list of board names to include (exact match, case-sensitive). If omitted or empty, no cards are shown — you must opt in per board.
Output
Section titled “Output”Each Trello card produces a task with:
id:trello:<card-id>title: card namereference:#<card-short-id>project: board name (fetched and cached per unique board)url: card short URLtype:cardcreated_at: derived from the card’s ObjectId (embedded creation timestamp)updated_at:dateLastActivityfrom the Trello API
Bundled script: rss.py
Section titled “Bundled script: rss.py”The rss.py script ships with gtdhelper.
Configuration
Section titled “Configuration”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 = 7include = ["release", "stable"]exclude = ["beta", "nightly"]| Field | Required | Default | Description |
|---|---|---|---|
name | yes | — | Human label, used as project in task output |
url | yes | — | RSS or Atom feed URL |
type | no | "rss" | Task type for UI filtering |
max_age_days | no | 7 | Ignore items older than N days |
include | no | [] (accept all) | Case-insensitive keywords, item must match at least one |
exclude | no | [] | Case-insensitive keywords, item is dropped if any match |
Filter evaluation order: max_age first, then exclude, then include (if non-empty).
Output
Section titled “Output”Each feed entry produces a task with:
id:rss:<slugified-name>:<sha256(entry-id-or-link)[:12]>title: entry titlereference: feed nameproject: feed nameurl: entry linktype: value from config (default"rss")created_at: entry’spublishedorupdatedtimestamp, fallback to current timeupdated_at: same ascreated_at