spatie/there-there-cli
There There CLI lets you interact with the There There support API from your terminal. Authenticate with workspace profiles, list and search tickets, view details, reply/forward, add notes, and update status or assignee via simple commands.
Install the CLI globally:
composer global require spatie/there-there-cli
Ensure the global bin directory is in your PATH:
composer global config bin-dir --absolute
Authenticate:
there-there login
First Use Case: List all open tickets:
there-there list-tickets --filter-status=open
Output is JSON-formatted, ready for parsing in Laravel scripts.
--profile=NAME to manage multiple workspaces.show-ticket and reply-to-ticket for basic workflows.there-there install-skill
Use Laravel’s Artisan::call() to trigger CLI commands from within your app:
// app/Console/Commands/ProcessTickets.php
public function handle() {
$exitCode = Artisan::call('there-there list-tickets --filter-status=open --json');
$tickets = json_decode(Artisan::output(), true);
foreach ($tickets['data'] as $ticket) {
// Process tickets (e.g., update Laravel DB, trigger notifications)
}
}
Pro Tip: Pipe JSON output to Laravel’s json_decode() for structured data.
Pair with Laravel Events to react to ticket changes:
// app/Listeners/UpdateTicketStatus.php
public function handle(ThereThereTicketUpdated $event) {
$ticketUlid = $event->ticketUlid;
Artisan::call("there-there update-ticket-status --ticket={$ticketUlid} --field status=closed");
}
Switch Profiles Dynamically:
# In Laravel, use environment variables to switch profiles
$profile = env('THERE_THERE_PROFILE', 'default');
Artisan::call("there-there list-tickets --profile={$profile}");
Profile-Based Scripts:
# Example: Run a script against a specific workspace
./scripts/process-tickets.sh --profile=acme-eu
Export Tickets to JSON:
there-there list-tickets --filter-created-after=2024-01-01 --json > tickets.json
Import into Laravel:
$tickets = json_decode(file_get_contents('tickets.json'), true);
Ticket::insert($tickets['data']);
Download Ticket Attachments:
$ticket = json_decode(Artisan::call("there-there show-ticket --ticket={$ulid} --json"));
foreach ($ticket->inline_images as $image) {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . config('there-there.token'),
])->get($image->download_url);
Storage::put("attachments/{$image->id}.{$image->mime_type}", $response->body());
}
Use the agent skill to generate Laravel code:
# Ask the agent to create a script for "list unassigned tickets"
there-there install-skill
# Then use your AI tool to prompt:
# "Generate a Laravel script to list all unassigned There There tickets from the last 7 days."
Token Expiry:
401 Unauthorized, re-authenticate:
there-there logout --all
there-there login
.env and use config('there-there.token') to avoid CLI prompts.Workspace Mismatch:
X-Workspace-Id header is set. The CLI auto-handles this, but verify with:
there-there get-me
Pagination Handling:
--page and --per-page for large datasets, but process in batches to avoid memory issues:
for ($page = 1; $page <= 10; $page++) {
$tickets = json_decode(Artisan::call("there-there list-tickets --page={$page} --per-page=50 --json"));
// Process $tickets['data']
}
Field Updates:
--field for updates (e.g., --field status=closed). Always validate the field name against the There There API docs.assignee_ulid) require ULIDs, not names. Use there-there list-members to fetch ULIDs.JSON Output:
--json for machine-readable output. Human-readable output may break parsing:
there-there list-tickets --json > tickets.json
Rate Limiting:
use Illuminate\Bus\Queueable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Bus;
Bus::dispatch(new ProcessTickets)->delay(now()->addMinute());
Caching:
$tickets = Cache::remember("there_there_tickets_{$profile}", now()->addHours(1), function () {
return json_decode(Artisan::call("there-there list-tickets --profile={$profile} --json"), true);
});
Verbose Output:
there-there list-tickets --verbose
Error Handling:
try-catch:
try {
$exitCode = Artisan::call('there-there show-ticket --ticket=' . $ulid);
if ($exitCode !== 0) {
throw new \RuntimeException("Failed to fetch ticket");
}
} catch (\Exception $e) {
Log::error("There There CLI error: " . $e->getMessage());
}
Network Issues:
localhost).--insecure (not recommended for production).Custom Commands:
there-there calls:
// app/Console/Commands/CloseOldTickets.php
public function handle() {
$tickets = json_decode(Artisan::call("there-there list-tickets --filter-status=open --filter-created-before=2024-01-01 --json"));
foreach ($tickets['data'] as $ticket) {
Artisan::call("there-there update-ticket-status --ticket={$ticket['ulid']} --field status=closed");
}
}
Webhook Integration:
queue:work to poll for changes and trigger webhooks:
// app/Console/Commands/PollTickets.php
public function handle() {
$newTickets = $this->fetchNewTickets();
foreach ($newTickets as $ticket) {
event(new ThereThereTicketCreated($ticket));
}
}
Testing:
Artisan::shouldReceive():
Artisan::shouldReceive('call')
->with('there-there list-tickets --json')
->andReturn(0)
->once();
Token Storage:
.env:
THERE_THERE_TOKEN=your_token_here
~/.there-there/config.json to 0600.Profile Isolation:
Logging:
Log::info("Updated ticket {$ticketUlid}", ['action' => 'status_update', 'status' => $new
How can I help you explore Laravel packages today?