spatie/there-there-cli
Command-line client for There There. Authenticate with profiles per workspace, then browse, search, and manage tickets from your terminal—list and filter tickets, view details, reply/forward, add internal notes, and update status or assignee via API-backed commands.
Install globally (run once):
composer global require spatie/there-there-cli
Ensure the global bin directory is in your PATH:
composer global config bin-dir --absolute
Authenticate (run once per workspace):
there-there login
First command (list all tickets):
there-there list-tickets
data: [{id: "ULID", subject: "..."}]).Create a Laravel Artisan command to auto-close stale tickets:
# In `app/Console/Commands/CloseStaleTickets.php`
use Spatie\ThereThereCli\ThereThereCli;
public function handle() {
$cli = new ThereThereCli();
$tickets = $cli->listTickets([
'filter_status' => 'open',
'filter_created_before' => now()->subDays(30)->format('Y-m-d'),
]);
foreach ($tickets['data'] as $ticket) {
$cli->updateTicketStatus($ticket['id'], ['status' => 'closed']);
$this->info("Closed ticket {$ticket['id']}");
}
}
Run it:
php artisan close-stale-tickets
Wrap CLI calls in Laravel commands for reusability:
// app/Console/Commands/ThereThereCommand.php
protected $signature = 'there-there:reply {ticket} {--body= : Reply body}';
public function handle() {
$cli = new \Spatie\ThereThereCli\ThereThereCli();
$cli->replyToTicket($this->argument('ticket'), [
'body' => $this->option('body')
]);
}
Run:
php artisan there-there:reply TICKET_ULID --body="Thanks for your message!"
Trigger Laravel jobs on ticket events (e.g., via a cron job polling for changes):
// app/Listeners/ProcessNewTickets.php
public function handle() {
$cli = new \Spatie\ThereThereCli\ThereThereCli();
$tickets = $cli->listTickets(['filter_status' => 'open', 'filter_created_after' => now()->subMinutes(5)]);
foreach ($tickets['data'] as $ticket) {
ProcessTicketJob::dispatch($ticket);
}
}
Encapsulate CLI logic in a service for testability:
// app/Services/ThereThereService.php
class ThereThereService {
public function __construct(protected ThereThereCli $cli) {}
public function assignTicketToTeam(string $ticketId, string $teamUlid): void {
$this->cli->updateTicketTeam($ticketId, ['team_ulid' => $teamUlid]);
}
}
Use profiles to switch between workspaces dynamically:
// Switch profile in code
$cli = new \Spatie\ThereThereCli\ThereThereCli();
$cli->setProfile('acme-eu'); // Overrides default
$tickets = $cli->listTickets();
Laravel Config Example:
// config/there-there.php
'default_profile' => env('THERE_THERE_PROFILE', 'default'),
'profiles' => [
'acme-us' => ['token' => env('THERE_THERE_US_TOKEN')],
'acme-eu' => ['token' => env('THERE_THERE_EU_TOKEN')],
],
Export tickets to JSON for analytics:
there-there list-tickets --filter-created-after=2024-01-01 > tickets.json
Laravel Example:
// Export tickets to a Laravel collection
$cli = new \Spatie\ThereThereCli\ThereThereCli();
$tickets = collect($cli->listTickets()['data'])
->map(fn ($ticket) => [
'id' => $ticket['id'],
'subject' => $ticket['subject'],
'created_at' => $ticket['created_at'],
]);
Download ticket attachments (e.g., screenshots):
// app/Services/AttachmentService.php
public function downloadAttachments(string $ticketId): array {
$ticket = $cli->showTicket($ticketId);
$attachments = [];
foreach ($ticket['messages'][0]['inline_images'] ?? [] as $image) {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $cli->getToken(),
])->get($image['download_url']);
$attachments[$image['name']] = $response->body();
}
return $attachments;
}
Use the agent skill to generate Laravel code:
# Ask an AI agent to write a script
there-there install-skill
# Then prompt:
# "Write a Laravel script to list all unassigned tickets and assign them to the 'support' team."
X-Workspace-Id header automatically via profiles).
--profile to target the right workspace:
there-there list-tickets --profile=acme-eu
Invalid API token, regenerate the token in There There settings.default). Use explicit names like acme-staging.data key (e.g., {"data": {...}}). Handle this in Laravel:
$ticket = $cli->showTicket($id)['data'];
meta.pagination for total pages:
$totalPages = $cli->listTickets()['meta']['pagination']['total_pages'];
01HX9F3K2M...), not shortened IDs.Y-m-d for filters (e.g., --filter-created-after=2024-01-01).--per-page=100 to reduce API calls:
there-there list-tickets --per-page=100 > all_tickets.json
$tickets = Cache::remember('there_there_tickets', now()->addHour(), function () {
return $cli->listTickets();
});
sleep(1); // Between batches
there-there list-tickets --verbose
there-there show-ticket TICKET_ULID --verbose
there-there profiles
// app/Console/Commands/CustomThereThereCommand.php
use Spatie\ThereThereCli\ThereThereCli;
protected $signature = 'there-there:custom {action}';
public function handle() {
$cli = new ThereThereCli();
// Custom logic...
}
Http::fake([
'api.there-there.app/*' => Http::response(['data' => []]),
]);
~/.there-there/config.json (permissions: 0600)..env:
THERE_THERE_US_TOKEN=your_token_here
THERE_THERE_EU_TOKEN=another_token
How can I help you explore Laravel packages today?