diff --git a/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/info.plist b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/info.plist
new file mode 100644
index 0000000..b41dfea
--- /dev/null
+++ b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/info.plist
@@ -0,0 +1,296 @@
+
+
+
+
+ bundleid
+ com.tlockney.task-manager
+ category
+ Productivity
+ connections
+
+ 11111111-1111-1111-1111-111111111111
+
+
+ destinationuid
+ 22222222-2222-2222-2222-222222222222
+ modifiers
+ 0
+ modifiersubtext
+
+ vitowards
+
+
+
+ 33333333-3333-3333-3333-333333333333
+
+
+ destinationuid
+ 44444444-4444-4444-4444-444444444444
+ modifiers
+ 0
+ modifiersubtext
+
+ vitowards
+
+
+
+ destinationuid
+ 55555555-5555-5555-5555-555555555555
+ modifiers
+ 1048576
+ modifiersubtext
+ Open in Notion
+ vitowards
+
+
+
+ 22222222-2222-2222-2222-222222222222
+
+
+ destinationuid
+ 66666666-6666-6666-6666-666666666666
+ modifiers
+ 0
+ modifiersubtext
+
+ vitowards
+
+
+
+
+ createdby
+ Thomas Lockney
+ description
+ Quick capture and manage tasks in Notion
+ disabled
+
+ name
+ Task Manager
+ objects
+
+
+ config
+
+ argumenttype
+ 0
+ keyword
+ task
+ subtext
+ Quick capture a new task
+ text
+ Add Task
+ withspace
+
+
+ type
+ alfred.workflow.input.keyword
+ uid
+ 11111111-1111-1111-1111-111111111111
+ version
+ 1
+
+
+ config
+
+ concurrently
+
+ escaping
+ 102
+ script
+ ./task_add.sh "{query}"
+ scriptargtype
+ 1
+ scriptfile
+
+ type
+ 0
+
+ type
+ alfred.workflow.action.script
+ uid
+ 22222222-2222-2222-2222-222222222222
+ version
+ 2
+
+
+ config
+
+ alfredfiltersresults
+
+ alfredfiltersresultsmatchmode
+ 0
+ argumenttreatemptyqueryasnil
+
+ argumenttrimmode
+ 0
+ argumenttype
+ 1
+ escaping
+ 102
+ keyword
+ tasks
+ queuedelaycustom
+ 3
+ queuedelayimmediatelyalinitially
+
+ queuedelaymode
+ 0
+ queuemode
+ 1
+ runningsubtext
+ Loading tasks...
+ script
+ ./task_list.sh "{query}"
+ scriptargtype
+ 1
+ scriptfile
+
+ subtext
+ View and manage tasks
+ title
+ Tasks
+ type
+ 0
+ withspace
+
+
+ type
+ alfred.workflow.input.scriptfilter
+ uid
+ 33333333-3333-3333-3333-333333333333
+ version
+ 3
+
+
+ config
+
+ concurrently
+
+ escaping
+ 102
+ script
+ ./task_complete.sh "{query}"
+ scriptargtype
+ 1
+ scriptfile
+
+ type
+ 0
+
+ type
+ alfred.workflow.action.script
+ uid
+ 44444444-4444-4444-4444-444444444444
+ version
+ 2
+
+
+ config
+
+ browser
+
+ spaces
+
+ url
+ {query}
+ utf8
+
+
+ type
+ alfred.workflow.action.openurl
+ uid
+ 55555555-5555-5555-5555-555555555555
+ version
+ 1
+
+
+ config
+
+ lastpathcomponent
+
+ onlyshowifquerypopulated
+
+ removeextension
+
+ text
+ ✅ Task added: {query}
+ title
+ Task Manager
+
+ type
+ alfred.workflow.output.notification
+ uid
+ 66666666-6666-6666-6666-666666666666
+ version
+ 1
+
+
+ readme
+ Task Manager - Quick capture and manage tasks in Notion
+
+Keywords:
+- task <title> - Quick capture a new task
+- tasks - View active tasks (Ready + In Focus)
+
+Actions:
+- Enter: Complete the selected task
+- Cmd+Enter: Open task in Notion
+
+Filters for 'tasks' command:
+- tasks - Show active tasks (Ready + In Focus)
+- tasks all - Show all non-complete tasks
+- tasks ready - Show Ready tasks only
+- tasks focus - Show In Focus tasks only
+- tasks triage - Show Triage tasks only
+- tasks open - Open Notion database
+ uidata
+
+ 11111111-1111-1111-1111-111111111111
+
+ xpos
+ 100
+ ypos
+ 50
+
+ 22222222-2222-2222-2222-222222222222
+
+ xpos
+ 300
+ ypos
+ 50
+
+ 33333333-3333-3333-3333-333333333333
+
+ xpos
+ 100
+ ypos
+ 200
+
+ 44444444-4444-4444-4444-444444444444
+
+ xpos
+ 300
+ ypos
+ 180
+
+ 55555555-5555-5555-5555-555555555555
+
+ xpos
+ 300
+ ypos
+ 260
+
+ 66666666-6666-6666-6666-666666666666
+
+ xpos
+ 500
+ ypos
+ 50
+
+
+ version
+ 1.0
+ webaddress
+
+
+
diff --git a/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_add.sh b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_add.sh
new file mode 100755
index 0000000..de48887
--- /dev/null
+++ b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_add.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+#
+# Alfred script: Add a new task
+#
+
+QUERY="$1"
+
+if [[ -z "$QUERY" ]]; then
+ echo "No task title provided"
+ exit 1
+fi
+
+# Call the task-manager script
+~/bin/task-manager add "$QUERY" >/dev/null 2>&1
+
+# Output the title for the notification
+echo "$QUERY"
diff --git a/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_complete.sh b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_complete.sh
new file mode 100755
index 0000000..2ce2194
--- /dev/null
+++ b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_complete.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+#
+# Alfred script: Complete a task
+#
+
+TASK_ID="$1"
+
+if [[ -z "$TASK_ID" ]]; then
+ echo "No task ID provided"
+ exit 1
+fi
+
+# Call the task-manager script
+~/bin/task-manager complete "$TASK_ID" >/dev/null 2>&1
+
+exit 0
diff --git a/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_list.sh b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_list.sh
new file mode 100755
index 0000000..bbccb22
--- /dev/null
+++ b/.config/Alfred.alfredpreferences/workflows/user.workflow.A1B2C3D4-E5F6-7890-ABCD-TASK12345678/task_list.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+#
+# Alfred script: List tasks
+#
+
+QUERY="${1:-}"
+
+# Handle special "open" command
+if [[ "$QUERY" == "open" ]]; then
+ ~/bin/task-manager open
+ # Return empty results since we're opening Notion directly
+ echo '{"items":[]}'
+ exit 0
+fi
+
+# Determine filter based on query
+FILTER="active"
+case "$QUERY" in
+ all|ready|focus|triage)
+ FILTER="$QUERY"
+ ;;
+ "")
+ FILTER="active"
+ ;;
+ *)
+ # If query doesn't match a filter, use active and let Alfred filter
+ FILTER="active"
+ ;;
+esac
+
+# Get tasks in JSON format
+RESULT=$(~/bin/task-manager --json list "$FILTER" 2>/dev/null)
+
+if [[ $? -ne 0 ]] || [[ -z "$RESULT" ]]; then
+ # Return error item
+ echo '{"items":[{"title":"Error loading tasks","subtitle":"Check your Notion API token and configuration","valid":false}]}'
+ exit 0
+fi
+
+# Check if we have any results
+ITEM_COUNT=$(echo "$RESULT" | jq -r '.items | length')
+
+if [[ "$ITEM_COUNT" == "0" ]]; then
+ # Add helpful message when no tasks
+ echo '{"items":[{"title":"No tasks found","subtitle":"Try: tasks all, tasks triage, or task
to add one","valid":false}]}'
+ exit 0
+fi
+
+# Output the result
+echo "$RESULT"
diff --git a/.config/task-manager/README.md b/.config/task-manager/README.md
new file mode 100644
index 0000000..b069c3e
--- /dev/null
+++ b/.config/task-manager/README.md
@@ -0,0 +1,170 @@
+# Task Manager
+
+CLI and Alfred workflow for managing tasks in Notion.
+
+## Setup
+
+### 1. Notion Integration
+
+1. Create an integration at [notion.so/my-integrations](https://www.notion.so/my-integrations)
+2. Copy the "Internal Integration Secret" (starts with `ntn_`)
+3. Share your Task Manager database with the integration
+
+### 2. Store Token in 1Password
+
+Store the Notion API token in 1Password. The default item name is "Notion API Token" with the token in a field labeled "credential".
+
+To use a different item name, update `op_token_item` in `config.json`.
+
+### 3. Configuration
+
+Edit `config.json` in this directory:
+
+```json
+{
+ "database_id": "your-database-id-here",
+ "notion_url": "https://www.notion.so/your-database-url",
+ "op_token_item": "Notion API Token"
+}
+```
+
+The `database_id` is the 32-character ID from your database URL.
+
+## Usage
+
+### CLI
+
+```bash
+# Quick capture (most common)
+task "Buy groceries"
+
+# List active tasks (Ready + In Focus)
+task-manager list
+
+# List with filters
+task-manager list ready # Ready tasks only
+task-manager list focus # In Focus tasks only
+task-manager list triage # Triage tasks only
+task-manager list all # All non-complete tasks
+
+# Complete a task
+task-manager complete
+
+# Open database in browser (prints URL if in SSH session)
+task-manager open
+```
+
+### Alfred
+
+- `task ` - Quick capture a new task
+- `tasks` - View active tasks (Ready + In Focus)
+- `tasks all` - View all non-complete tasks
+- `tasks ready` - View Ready tasks
+- `tasks focus` - View In Focus tasks
+- `tasks triage` - View Triage tasks
+- `tasks open` - Open Notion database
+
+**Actions on task list:**
+- `Enter` - Complete the selected task
+- `Cmd+Enter` - Open task in Notion
+
+### Environment Variable Override
+
+For shell use, you can set the token directly:
+
+```bash
+export NOTION_TOKEN="ntn_..."
+task "My task"
+```
+
+This takes precedence over 1Password lookup.
+
+## Conventions for Other Clients
+
+When implementing task capture on other platforms (iOS Shortcuts, widgets, etc.), follow these conventions for consistency:
+
+### Creating Tasks
+
+**API Endpoint:** `POST https://api.notion.com/v1/pages`
+
+**Required Headers:**
+```
+Authorization: Bearer
+Content-Type: application/json
+Notion-Version: 2022-06-28
+```
+
+**Minimal Payload (quick capture):**
+```json
+{
+ "parent": { "database_id": "" },
+ "properties": {
+ "Title": {
+ "title": [{ "text": { "content": "Task title here" } }]
+ },
+ "Status": {
+ "status": { "name": "📥 Triage" }
+ }
+ }
+}
+```
+
+### Default Values
+
+| Field | Default | Notes |
+|-------|---------|-------|
+| Status | 📥 Triage | New tasks land here for processing |
+| Context | (none) | Optional: "Work" or "Personal" |
+| Energy/Interest | (none) | Optional |
+| Time Estimate | (none) | Optional |
+| Due Date | (none) | Optional |
+
+### Status Values
+
+| Key | Value | Group |
+|-----|-------|-------|
+| triage | 📥 Triage | To Do |
+| ready | 🎯 Ready | To Do |
+| waiting | ⏳ Waiting | To Do |
+| focus | 🔥 In Focus | In Progress |
+| complete | ✅ Complete | Complete |
+
+### Context Values
+
+- `Work` (blue)
+- `Personal` (green)
+
+### Energy/Interest Values
+
+- `5 - Excited` (green)
+- `4 - Interested` (blue)
+- `3 - Neutral` (gray)
+- `2 - Resistant` (yellow)
+- `1 - Dreading` (red)
+
+### Time Estimate Values
+
+- `5 min` (green)
+- `15 min` (green)
+- `30 min` (yellow)
+- `1 hour` (orange)
+- `2+ hours` (red)
+
+## Files
+
+```
+~/.config/task-manager/
+├── config.json # Configuration (database ID, 1Password item)
+└── README.md # This file
+
+~/bin/
+├── task-manager # Main CLI script
+└── task # Quick capture wrapper
+
+~/.config/Alfred.alfredpreferences/workflows/
+└── user.workflow.A1B2C3D4-.../
+ ├── info.plist # Workflow definition
+ ├── task_add.sh # Quick capture script
+ ├── task_list.sh # List tasks script
+ └── task_complete.sh # Complete task script
+```
diff --git a/.config/task-manager/config.json b/.config/task-manager/config.json
new file mode 100644
index 0000000..03c5695
--- /dev/null
+++ b/.config/task-manager/config.json
@@ -0,0 +1,24 @@
+{
+ "database_id": "daaac64904a445f99bb1f54f315360c6",
+ "notion_url": "https://www.notion.so/dc4a2ae3aded4534bbde450f27248b2a",
+ "op_token_item": "Notion API Token",
+ "defaults": {
+ "status": "📥 Triage"
+ },
+ "status_values": {
+ "triage": "📥 Triage",
+ "ready": "🎯 Ready",
+ "waiting": "⏳ Waiting",
+ "focus": "🔥 In Focus",
+ "complete": "✅ Complete"
+ },
+ "context_values": ["Work", "Personal"],
+ "energy_values": [
+ "5 - Excited",
+ "4 - Interested",
+ "3 - Neutral",
+ "2 - Resistant",
+ "1 - Dreading"
+ ],
+ "time_estimate_values": ["5 min", "15 min", "30 min", "1 hour", "2+ hours"]
+}
diff --git a/bin/task b/bin/task
new file mode 100755
index 0000000..ee32c77
--- /dev/null
+++ b/bin/task
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+#
+# task - Quick capture for task-manager
+#
+# Usage: task
+#
+# Creates a new task in Notion with the given title.
+# The task lands in Triage status for later processing.
+#
+
+exec "$(dirname "$0")/task-manager" add "$@"
diff --git a/bin/task-manager b/bin/task-manager
new file mode 100755
index 0000000..681c458
--- /dev/null
+++ b/bin/task-manager
@@ -0,0 +1,368 @@
+#!/usr/bin/env bash
+#
+# task-manager - CLI for managing tasks in Notion
+#
+# Usage:
+# task-manager add Add a new task (lands in Triage)
+# task-manager list [filter] List tasks (ready, focus, all)
+# task-manager complete Mark a task as complete
+# task-manager open Open database (or print URL if in SSH)
+#
+# Options:
+# --json Output in Alfred-compatible JSON format
+# -h, --help Show this help message
+#
+
+set -euo pipefail
+
+# Configuration
+CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/task-manager"
+CONFIG_FILE="$CONFIG_DIR/config.json"
+
+# Notion API
+NOTION_API_URL="https://api.notion.com/v1"
+NOTION_API_VERSION="2022-06-28"
+
+# Flags
+JSON_OUTPUT=false
+
+#------------------------------------------------------------------------------
+# Helper functions
+#------------------------------------------------------------------------------
+
+die() {
+ echo "Error: $1" >&2
+ exit 1
+}
+
+usage() {
+ sed -n '3,13p' "$0" | sed 's/^# \?//'
+ exit 0
+}
+
+load_config() {
+ if [[ ! -f "$CONFIG_FILE" ]]; then
+ die "Config file not found: $CONFIG_FILE"
+ fi
+
+ DATABASE_ID=$(jq -r '.database_id' "$CONFIG_FILE")
+ NOTION_URL=$(jq -r '.notion_url' "$CONFIG_FILE")
+ OP_TOKEN_ITEM=$(jq -r '.op_token_item' "$CONFIG_FILE")
+ DEFAULT_STATUS=$(jq -r '.defaults.status' "$CONFIG_FILE")
+
+ if [[ -z "$DATABASE_ID" || "$DATABASE_ID" == "null" ]]; then
+ die "database_id not set in config"
+ fi
+}
+
+get_notion_token() {
+ # Priority: environment variable, then 1Password
+ if [[ -n "${NOTION_TOKEN:-}" ]]; then
+ echo "$NOTION_TOKEN"
+ return
+ fi
+
+ if ! command -v op &>/dev/null; then
+ die "NOTION_TOKEN not set and 1Password CLI (op) not found"
+ fi
+
+ local token
+ token=$(op item get "$OP_TOKEN_ITEM" --fields label=credential 2>/dev/null) || \
+ die "Failed to get Notion token from 1Password item: $OP_TOKEN_ITEM"
+
+ echo "$token"
+}
+
+notion_api() {
+ local method="$1"
+ local endpoint="$2"
+ local data="${3:-}"
+
+ local token
+ token=$(get_notion_token)
+
+ local args=(
+ -s
+ -X "$method"
+ -H "Authorization: Bearer $token"
+ -H "Content-Type: application/json"
+ -H "Notion-Version: $NOTION_API_VERSION"
+ )
+
+ if [[ -n "$data" ]]; then
+ args+=(-d "$data")
+ fi
+
+ curl "${args[@]}" "${NOTION_API_URL}${endpoint}"
+}
+
+#------------------------------------------------------------------------------
+# Commands
+#------------------------------------------------------------------------------
+
+cmd_add() {
+ local title="$1"
+
+ if [[ -z "$title" ]]; then
+ die "Task title is required"
+ fi
+
+ local payload
+ payload=$(jq -n \
+ --arg db_id "$DATABASE_ID" \
+ --arg title "$title" \
+ --arg status "$DEFAULT_STATUS" \
+ '{
+ parent: { database_id: $db_id },
+ properties: {
+ Title: {
+ title: [{ text: { content: $title } }]
+ },
+ Status: {
+ status: { name: $status }
+ }
+ }
+ }')
+
+ local response
+ response=$(notion_api POST "/pages" "$payload")
+
+ # Check for errors
+ if echo "$response" | jq -e '.object == "error"' &>/dev/null; then
+ local msg
+ msg=$(echo "$response" | jq -r '.message')
+ die "Notion API error: $msg"
+ fi
+
+ local page_id page_url
+ page_id=$(echo "$response" | jq -r '.id')
+ page_url=$(echo "$response" | jq -r '.url')
+
+ if $JSON_OUTPUT; then
+ jq -n --arg id "$page_id" --arg url "$page_url" --arg title "$title" \
+ '{ success: true, id: $id, url: $url, title: $title }'
+ else
+ echo "Created: $title"
+ echo "URL: $page_url"
+ fi
+}
+
+cmd_list() {
+ local filter="${1:-active}"
+
+ local filter_payload
+ case "$filter" in
+ ready)
+ filter_payload='{"property": "Status", "status": {"equals": "🎯 Ready"}}'
+ ;;
+ focus)
+ filter_payload='{"property": "Status", "status": {"equals": "🔥 In Focus"}}'
+ ;;
+ triage)
+ filter_payload='{"property": "Status", "status": {"equals": "📥 Triage"}}'
+ ;;
+ active)
+ # Ready or In Focus
+ filter_payload='{
+ "or": [
+ {"property": "Status", "status": {"equals": "🎯 Ready"}},
+ {"property": "Status", "status": {"equals": "🔥 In Focus"}}
+ ]
+ }'
+ ;;
+ all)
+ filter_payload='{
+ "or": [
+ {"property": "Status", "status": {"equals": "📥 Triage"}},
+ {"property": "Status", "status": {"equals": "🎯 Ready"}},
+ {"property": "Status", "status": {"equals": "⏳ Waiting"}},
+ {"property": "Status", "status": {"equals": "🔥 In Focus"}}
+ ]
+ }'
+ ;;
+ *)
+ die "Unknown filter: $filter (valid: ready, focus, triage, active, all)"
+ ;;
+ esac
+
+ local payload
+ payload=$(jq -n \
+ --argjson filter "$filter_payload" \
+ '{
+ filter: $filter,
+ sorts: [
+ { property: "Status", direction: "ascending" },
+ { property: "Created", direction: "descending" }
+ ]
+ }')
+
+ local response
+ response=$(notion_api POST "/databases/$DATABASE_ID/query" "$payload")
+
+ # Check for errors
+ if echo "$response" | jq -e '.object == "error"' &>/dev/null; then
+ local msg
+ msg=$(echo "$response" | jq -r '.message')
+ die "Notion API error: $msg"
+ fi
+
+ if $JSON_OUTPUT; then
+ # Alfred-compatible JSON output
+ echo "$response" | jq '{
+ items: [.results[] | {
+ uid: .id,
+ title: (.properties.Title.title[0].text.content // "Untitled"),
+ subtitle: (
+ (.properties.Status.status.name // "") +
+ (if .properties.Context.select.name then " [" + .properties.Context.select.name + "]" else "" end) +
+ (if .properties["Time Estimate"].select.name then " " + .properties["Time Estimate"].select.name else "" end) +
+ (if .properties["Due Date"].date.start then " Due: " + .properties["Due Date"].date.start else "" end)
+ ),
+ arg: .id,
+ mods: {
+ cmd: {
+ subtitle: "Open in Notion",
+ arg: .url
+ }
+ },
+ text: {
+ copy: .url,
+ largetype: (.properties.Title.title[0].text.content // "Untitled")
+ }
+ }]
+ }'
+ else
+ # Human-readable output
+ echo "$response" | jq -r '.results[] |
+ (.properties.Status.status.name // "?") + " " +
+ (.properties.Title.title[0].text.content // "Untitled") +
+ (if .properties.Context.select.name then " [" + .properties.Context.select.name + "]" else "" end) +
+ (if .properties["Time Estimate"].select.name then " (" + .properties["Time Estimate"].select.name + ")" else "" end) +
+ "\n " + .url'
+ fi
+}
+
+cmd_complete() {
+ local page_id="$1"
+
+ if [[ -z "$page_id" ]]; then
+ die "Task ID is required"
+ fi
+
+ local status_complete
+ status_complete=$(jq -r '.status_values.complete' "$CONFIG_FILE")
+
+ local payload
+ payload=$(jq -n \
+ --arg status "$status_complete" \
+ '{
+ properties: {
+ Status: {
+ status: { name: $status }
+ },
+ Completed: {
+ date: { start: (now | strftime("%Y-%m-%d")) }
+ }
+ }
+ }')
+
+ local response
+ response=$(notion_api PATCH "/pages/$page_id" "$payload")
+
+ # Check for errors
+ if echo "$response" | jq -e '.object == "error"' &>/dev/null; then
+ local msg
+ msg=$(echo "$response" | jq -r '.message')
+ die "Notion API error: $msg"
+ fi
+
+ local title
+ title=$(echo "$response" | jq -r '.properties.Title.title[0].text.content // "Untitled"')
+
+ if $JSON_OUTPUT; then
+ jq -n --arg id "$page_id" --arg title "$title" \
+ '{ success: true, id: $id, title: $title, status: "complete" }'
+ else
+ echo "Completed: $title"
+ fi
+}
+
+cmd_open() {
+ local url="$NOTION_URL"
+
+ # In SSH session, just print the URL
+ if [[ -n "${SSH_CLIENT:-}" || -n "${SSH_TTY:-}" ]]; then
+ echo "$url"
+ return
+ fi
+
+ case "$(uname -s)" in
+ Darwin)
+ open "$url"
+ ;;
+ Linux)
+ if command -v xdg-open &>/dev/null; then
+ xdg-open "$url"
+ else
+ echo "$url"
+ fi
+ ;;
+ *)
+ echo "$url"
+ ;;
+ esac
+}
+
+#------------------------------------------------------------------------------
+# Main
+#------------------------------------------------------------------------------
+
+main() {
+ # Parse global options
+ while [[ $# -gt 0 ]]; do
+ case "$1" in
+ --json)
+ JSON_OUTPUT=true
+ shift
+ ;;
+ -h|--help)
+ usage
+ ;;
+ -*)
+ die "Unknown option: $1"
+ ;;
+ *)
+ break
+ ;;
+ esac
+ done
+
+ if [[ $# -eq 0 ]]; then
+ usage
+ fi
+
+ local command="$1"
+ shift
+
+ load_config
+
+ case "$command" in
+ add)
+ cmd_add "${*:-}"
+ ;;
+ list|ls)
+ cmd_list "${1:-active}"
+ ;;
+ complete|done)
+ cmd_complete "${1:-}"
+ ;;
+ open)
+ cmd_open
+ ;;
+ *)
+ die "Unknown command: $command"
+ ;;
+ esac
+}
+
+main "$@"