Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ The command-line entry point, built with **Clap**.

An **MCP (Model Context Protocol) stdio server** for AI agent integration.

- Exposes **14 tools** covering accounts, posts, schedules, analytics, and media
- Exposes **29 tools** covering accounts, posts, schedules, analytics, and media
- Communicates over stdin/stdout using the MCP protocol
- Delegates all logic to `titen-core`

Expand Down
5 changes: 4 additions & 1 deletion docs/auth-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ Redirect to Threads authorize URL
│ https://threads.net/oauth/authorize
│ ?client_id=...
│ &redirect_uri=.../auth/callback
│ &scope=threads_basic,threads_content_publish
│ &scope=threads_basic,threads_content_publish,
│ threads_manage_replies,threads_manage_mentions,
│ threads_keyword_search,threads_profile_discovery,
│ threads_share_to_instagram,threads_location_tagging
│ &response_type=code
│ &state=<one-time token> ← CSRF protection (#237)
Expand Down
118 changes: 65 additions & 53 deletions docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,14 @@ API_IMAGE=ghcr.io/codecoradev/titen:latest-api
APP_URL=https://titen.yourdomain.com
TITEN_HOST=titen.yourdomain.com

# Generate API key
TITEN_API_KEY=$(openssl rand -hex 24)

# Generate encryption key for tokens at rest
TITEN_ENCRYPTION_KEY=$(openssl rand -hex 32)
# Generate API key (64 hex chars). Compose does NOT evaluate $(...) in .env
# files — generate the value in your shell first and paste the literal:
# openssl rand -hex 32
TITEN_API_KEY=PASTE_GENERATED_64_HEX_CHAR_VALUE_HERE

# Generate encryption key for tokens at rest (same rule: paste the literal):
# openssl rand -hex 32
TITEN_ENCRYPTION_KEY=PASTE_GENERATED_64_HEX_CHAR_VALUE_HERE
TITEN_REQUIRE_ENCRYPTION=true

# HTTPS settings
Expand All @@ -147,10 +150,16 @@ TITEN_CORS_ORIGINS=https://titen.yourdomain.com

```bash
mkdir -p data
chmod 777 data
chmod 755 data
```

SQLite needs write permission to this folder. The `chmod 777` ensures the container user can create and write the database file.
SQLite needs write permission to this folder. The API container runs as the non-root `titen` user — if it cannot write, grant that user ownership instead of loosening permissions:

```bash
# Find the container user's UID (typically 1000):
docker compose exec api id -u titen
sudo chown -R 1000:1000 data
```

### Step 5: Start the Containers

Expand All @@ -165,8 +174,8 @@ docker compose up -d
# Check container status
docker compose ps

# Check health
curl http://localhost:3000/health
# Check health (the web container proxies /health and /api/health to the API)
curl -f http://localhost:3000/api/health

# View logs
docker compose logs -f --tail=50
Expand All @@ -179,7 +188,8 @@ You should see both containers running. The API container should log `titen-api
The `./data/` directory is bind-mounted to `/data` inside the API container and stores:

- **SQLite database** (`/data/titen.db`): all posts, schedules, accounts, analytics
- **Uploaded media** (if stored locally rather than S3)
- **Uploaded media** (media works out of the box on local storage under `/data/media`;
S3-compatible storage is optional — see [section 7](#7-s3-media-storage-optional))

This directory **survives container restarts and updates**. Without it, all data would be lost when containers are recreated.

Expand Down Expand Up @@ -254,9 +264,15 @@ Group=titen
# Binary location
ExecStart=/usr/local/bin/titen serve

# Environment configuration
# Secrets live in a root-only environment file, NOT inline in the unit
# (unit files are world-readable via /etc/systemd/system):
# sudo install -m 600 /dev/null /etc/titen/titen.env then add:
# TITEN_API_KEY=<generate: openssl rand -hex 32>
# TITEN_ENCRYPTION_KEY=<generate: openssl rand -hex 32>
EnvironmentFile=/etc/titen/titen.env

# Non-secret environment configuration
Environment="TITEN_DB_PATH=/var/lib/titen/titen.db"
Environment="TITEN_API_KEY=your-secure-api-key-here"
Environment="TITEN_HOST=0.0.0.0"
Environment="TITEN_PORT=7845"
Environment="TITEN_URL=https://titen.yourdomain.com"
Expand Down Expand Up @@ -309,17 +325,17 @@ Create `/etc/caddy/Caddyfile`:

```caddyfile
titen.yourdomain.com {
reverse_proxy localhost:7845

# Allow large media uploads (images/video)
request_body {
max_size 100MB
}

# Pass real client IP
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
reverse_proxy localhost:7845 {
# Pass real client IP
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto {scheme}
}

# Security headers
header {
Expand Down Expand Up @@ -491,11 +507,11 @@ The built-in scheduler checks token validity every 60 seconds (configurable via
**Manual Refresh (CLI):**

```bash
# Inside the container
docker exec -it titen titen account list
# Inside the API container (name per docker-compose.yml)
docker exec -it titen-api titen account list

# Refresh token manually
docker exec -it titen titen account refresh <account_id>
docker exec -it titen-api titen account refresh <account_id>
```

If a token expires, the Threads API will return `401 Unauthorized`. See [Troubleshooting](#11-troubleshooting).
Expand All @@ -512,7 +528,7 @@ S3-compatible storage is needed when you:
- **Upload videos** (VIDEO post type)
- **Store media locally** is not sufficient for your scale

Without S3 configured, media uploads will fail. TEXT-only posts work without S3.
Media uploads work without any S3 configuration — files are stored locally under `/data/media` (served at `/media`). Configure S3-compatible storage only if you want object storage (e.g. for multi-host deployments or external CDN access).

### Supported Providers

Expand Down Expand Up @@ -631,7 +647,7 @@ This command creates a safe backup without locking the running database:

```bash
# Docker
docker exec titen sqlite3 /data/titen.db ".backup '/data/titen-backup-$(date +%Y%m%d).db'"
docker exec titen-api sqlite3 /data/titen.db ".backup '/data/titen-backup-$(date +%Y%m%d).db'"

# Native
sqlite3 /var/lib/titen/titen.db ".backup '/var/lib/titen/titen-backup-$(date +%Y%m%d).db'"
Expand All @@ -647,17 +663,16 @@ Create a daily cron job:
BACKUP_DIR="/opt/backups/titen"
mkdir -p "$BACKUP_DIR"

# Backup SQLite
docker exec titen sqlite3 /data/titen.db ".backup '/data/backup.db'"
docker cp titen:/data/backup.db "$BACKUP_DIR/titen-$(date +%Y%m%d-%H%M%S).db"
# Backup SQLite (transactionally consistent copy)
docker exec titen-api sqlite3 /data/titen.db ".backup '/data/backup.db'"
docker cp titen-api:/data/backup.db "$BACKUP_DIR/titen-$(date +%Y%m%d-%H%M%S).db"

# Backup full volume
docker run --rm -v titen_titen-data:/data -v "$BACKUP_DIR":/backup \
alpine tar czf "/backup/titen-volume-$(date +%Y%m%d-%H%M%S).tar.gz" /data
# Backup the full data directory (bind-mounted ./data per docker-compose.yml)
tar czf "$BACKUP_DIR/titen-data-$(date +%Y%m%d-%H%M%S).tar.gz" -C . data

# Retain last 30 days
find "$BACKUP_DIR" -name "titen-*.db" -mtime +30 -delete
find "$BACKUP_DIR" -name "titen-volume-*.tar.gz" -mtime +30 -delete
find "$BACKUP_DIR" -name "titen-data-*.tar.gz" -mtime +30 -delete

echo "Backup complete: $(date)"
```
Expand All @@ -676,8 +691,7 @@ Back up the entire data volume (SQLite + media + config):

```bash
# Create backup
docker run --rm -v titen_titen-data:/data -v $(pwd):/backup \
alpine tar czf /backup/titen-volume-$(date +%Y%m%d).tar.gz /data
tar czf "/backup/titen-data-$(date +%Y%m%d).tar.gz" -C . data

# List backups
ls -lh titen-volume-*.tar.gz
Expand All @@ -691,15 +705,14 @@ ls -lh titen-volume-*.tar.gz
# 1. Stop Titen
docker compose down

# 2. Copy backup into the volume
docker run --rm -v titen_titen-data:/data -v $(pwd):/backup \
alpine cp /backup/titen-backup-20260101.db /data/titen.db
# 2. Copy the backup into ./data (bind-mounted to /data per docker-compose.yml)
cp /backup/titen-backup-20260101.db data/titen.db

# 3. Start Titen
docker compose up -d

# 4. Verify
curl http://localhost:7845/api/health
# 4. Verify (web proxies /api/health to the API)
curl -f http://localhost:3000/api/health
```

#### Restore Full Volume
Expand All @@ -708,16 +721,15 @@ curl http://localhost:7845/api/health
# 1. Stop Titen
docker compose down

# 2. Extract volume backup
docker run --rm -v titen_titen-data:/data -v $(pwd):/backup \
alpine tar xzf /backup/titen-volume-20260101.tar.gz -C /
# 2. Extract the data-directory backup
tar xzf /backup/titen-data-20260101.tar.gz -C .

# 3. Start Titen
docker compose up -d

# 4. Verify
docker logs titen
curl http://localhost:7845/api/health
docker compose logs -f --tail=50
curl -f http://localhost:3000/api/health
```

---
Expand All @@ -736,8 +748,8 @@ docker compose pull
docker compose up -d

# Verify the new version is running
docker logs titen
curl http://localhost:7845/api/health
docker compose logs --tail=50
curl -f http://localhost:3000/api/health
```

This preserves your data volume. No data loss.
Expand Down Expand Up @@ -845,7 +857,7 @@ sudo kill -9 <PID>
4. **WAL file corruption**: if persistent, run a checkpoint:

```bash
docker exec titen sqlite3 /data/titen.db "PRAGMA wal_checkpoint(TRUNCATE);"
docker exec titen-api sqlite3 /data/titen.db "PRAGMA wal_checkpoint(TRUNCATE);"
```

---
Expand All @@ -858,18 +870,18 @@ sudo kill -9 <PID>

```bash
# Check account status
docker exec titen titen account list
docker exec titen-api titen account list

# View recent errors in logs
docker logs titen 2>&1 | grep -i "401\|unauthorized\|token"
docker logs titen-api 2>&1 | grep -i "401\|unauthorized\|token"
```

**Fix:**

1. **Manual refresh:**

```bash
docker exec -it titen titen account refresh <account_id>
docker exec -it titen-api titen account refresh <account_id>
```

2. **Re-authenticate if refresh fails** (token fully expired):
Expand All @@ -885,7 +897,7 @@ docker logs titen 2>&1 | grep -i "401\|unauthorized\|token"
3. **Verify the scheduler is running** (handles automatic refresh):

```bash
docker logs titen 2>&1 | grep -i "scheduler\|refresh\|token"
docker logs titen-api 2>&1 | grep -i "scheduler\|refresh\|token"
```

---
Expand All @@ -898,7 +910,7 @@ docker logs titen 2>&1 | grep -i "401\|unauthorized\|token"

```bash
# Check logs for S3 errors
docker logs titen 2>&1 | grep -i "s3\|media\|upload"
docker logs titen-api 2>&1 | grep -i "s3\|media\|upload"
```

**Fix:**
Expand Down Expand Up @@ -967,7 +979,7 @@ docker compose up -d

```bash
# View container logs
docker logs titen
docker logs titen-api

# Check container status
docker compose ps -a
Expand All @@ -988,7 +1000,7 @@ docker inspect titen --format='{{.State.ExitCode}}'

```bash
# 1. Read the full logs
docker logs titen --tail 100
docker logs titen-api --tail 100

# 2. Check if port is in use
docker compose down
Expand All @@ -1012,4 +1024,4 @@ If you're still stuck:

- **GitHub Issues:** [github.com/codecoradev/titen/issues](https://github.com/codecoradev/titen/issues)
- **Documentation:** [github.com/codecoradev/titen/docs](https://github.com/codecoradev/titen/tree/main/docs)
- **Logs:** Always include `docker logs titen` output when reporting issues
- **Logs:** Always include `docker logs titen-api` output when reporting issues
55 changes: 32 additions & 23 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Titen uses the Threads Graph API. You need a Threads account and a Meta for Deve
```bash
# Set your API key (if auth is enabled)
export TITEN_API_KEY=your-key
export TITEN_URL=http://localhost:7845
export TITEN_URL=http://localhost:7845 # API base URL used by the CLI (APP_URL is the separate public site URL)

# Add the account
titen account add mybrand \
Expand Down Expand Up @@ -133,17 +133,10 @@ titen post create mybrand \
--image-url "https://example.com/photo.jpg" \
--text "Check this out"

# Video post
curl -X POST http://localhost:7845/api/posts \
-H "X-API-Key: your-key" \
-H "Content-Type: application/json" \
-d '{
"account_id": 1,
"media_type": "VIDEO",
"video_url": "https://example.com/video.mp4"
}'
```

(The API supports VIDEO posts via `POST /api/posts` — see the next section.)

### Via API

```bash
Expand Down Expand Up @@ -449,23 +442,39 @@ Add to your MCP settings (`Settings → MCP`):
}
```

### Available MCP Tools (14)
### Available MCP Tools (29)

| Tool | Description |
|------|-------------|
| `list_accounts` | List all Threads accounts |
| `get_account` | Get account by ID |
| `create_post` | Create and publish a post |
| `schedule_post` | Schedule a post |
| `list_accounts` | List all Threads accounts managed by titen |
| `get_user_profile` | Fetch a Threads user's profile from the Threads API |
| `get_publishing_limit` | Fetch an account's Threads publishing quota (daily post limit, etc.) |
| `create_post` | Create and publish a Threads post |
| `schedule_post` | Schedule a post for future publishing |
| `list_schedules` | List scheduled posts |
| `cancel_schedule` | Cancel a schedule |
| `fetch_comments` | Fetch comments from Threads |
| `get_post_sentiment` | Analyze comment sentiment |
| `get_post_analytics` | Analytics for a post |
| `get_account_analytics` | Analytics summary per account |
| `upload_media` | Upload media to S3 |
| `refresh_token` | Refresh an account token |
| `check_tokens` | Batch token expiry check |
| `cancel_schedule` | Cancel a scheduled post |
| `refresh_token` | Refresh an account's Threads access token |
| `check_tokens` | Check all accounts' token expiry status and auto-refresh expiring tokens |
| `fetch_comments` | Fetch and store comments from a Threads post via the Threads API |
| `get_post_sentiment` | Get sentiment analysis for a post's comments |
| `get_post_insights` | Fetch post insights (likes, replies, reposts, views, quotes) from the Threads API |
| `get_account_analytics` | Get analytics summary for an account's posts |
| `delete_post` | Delete a post from Threads and the local database |
| `create_container` | Create a Threads container (first step for media posts, carousel, etc.) |
| `publish_container` | Publish a previously created Threads container by container ID |
| `list_posts` | List published/draft posts with optional filtering |
| `get_post` | Get a single post by ID |
| `get_schedule` | Get a single scheduled post by ID |
| `approve_schedule` | Approve a pending scheduled post for publishing (HITL approval) |
| `reject_schedule` | Reject a pending scheduled post with optional reason (HITL rejection) |
| `upload_media` | Upload a media asset (image) to titen storage for use in posts/carousels |
| `list_media` | List media assets stored in titen |
| `fetch_mentions` | Fetch mentions of a managed account from the Threads API and store them |
| `list_mentions` | List stored mentions for an account |
| `search_keyword` | Search Threads for a keyword or trending topic |
| `get_post_trend` | Get time-series engagement trend data for a post (stored analytics snapshots) |
| `reply_to_comment` | Reply to a comment on a Threads post directly from the AI agent |
| `exchange_oauth_code` | Exchange an OAuth authorization code for a long-lived token and add account to titen |

### Example: Ask Claude to Post

Expand Down
Loading