@@ -29,6 +29,11 @@ $script:ConfigPath = if ($IsWindows) {
2929} else {
3030 Join-Path $HOME ' .config/ghostbit.toml'
3131}
32+ $script :HistoryPath = if ($IsWindows ) {
33+ Join-Path $env: LOCALAPPDATA ' ghostbit\history.jsonl'
34+ } else {
35+ Join-Path $HOME ' .local/share/ghostbit/history.jsonl'
36+ }
3237
3338# ── Config ────────────────────────────────────────────────────────────────────
3439
@@ -302,6 +307,21 @@ function New-GhostbitPaste {
302307 }
303308 $fullUrl = " $ ( $response.url ) #$fragment "
304309
310+ # ── Append to local history (best-effort, never blocks) ──
311+ try {
312+ $dir = Split-Path $script :HistoryPath - Parent
313+ if (-not (Test-Path $dir )) { New-Item - ItemType Directory - Path $dir - Force | Out-Null }
314+ $entry = [ordered ]@ {
315+ id = $response.id
316+ url = $response.url
317+ full_url = $fullUrl
318+ created_at = [DateTimeOffset ]::UtcNow.ToUnixTimeSeconds()
319+ language = if ($Language ) { $Language } else { $null }
320+ expires_at = $response.expires_at
321+ }
322+ Add-Content - Path $script :HistoryPath - Value ($entry | ConvertTo-Json - Compress) - Encoding UTF8
323+ } catch { }
324+
305325 if ($AsJson ) {
306326 $response | Add-Member - NotePropertyName full_url - NotePropertyValue $fullUrl - Force
307327 $response | ConvertTo-Json
@@ -476,7 +496,142 @@ function Invoke-GhostbitConfig {
476496 }
477497}
478498
499+ # ── Remove-GhostbitPaste ─────────────────────────────────────────────────────
500+
501+ function Remove-GhostbitPaste {
502+ <#
503+ . SYNOPSIS
504+ Delete a paste using the delete token embedded in its URL.
505+
506+ . PARAMETER Url
507+ Full paste URL including the #fragment (KEY~DELETE_TOKEN or ~DELETE_TOKEN).
508+
509+ . EXAMPLE
510+ Remove-GhostbitPaste "https://paste.example.com/abc123#KEY~TOKEN"
511+ gbd "https://paste.example.com/abc123#KEY~TOKEN"
512+ #>
513+ [CmdletBinding ()]
514+ [Alias (' gbd' )]
515+ param (
516+ [Parameter (Mandatory , Position = 0 )]
517+ [string ]$Url
518+ )
519+
520+ $uri = [System.Uri ]$Url
521+ $fragment = $uri.Fragment.TrimStart (' #' )
522+ $serverUrl = " $ ( $uri.Scheme ) ://$ ( $uri.Authority ) "
523+ $pasteId = $uri.AbsolutePath.Trim (' /' )
524+ $deleteToken = $fragment.Split (' ~' , 2 )[1 ]
525+
526+ if ([string ]::IsNullOrEmpty($deleteToken )) {
527+ Write-Error ' Delete token missing from URL fragment (expected KEY~TOKEN or ~TOKEN).'
528+ return
529+ }
530+
531+ $apiUrl = " $serverUrl /api/v1/pastes/$pasteId "
532+ try {
533+ Invoke-RestMethod - Uri $apiUrl - Method Delete `
534+ - Headers @ { ' User-Agent' = $script :UserAgent ; ' X-Delete-Token' = $deleteToken } | Out-Null
535+ Write-Host " Deleted $pasteId ."
536+ } catch {
537+ $code = $_.Exception.Response.StatusCode.value__
538+ switch ($code ) {
539+ 403 { Write-Error ' Invalid delete token.' }
540+ 404 { Write-Error ' Paste not found (already deleted or expired).' }
541+ default { Write-Error " Error $code `: $ ( $_.ErrorDetails.Message ?? $_.Exception.Message ) " }
542+ }
543+ }
544+ }
545+
546+ # ── Get-GhostbitHistory ───────────────────────────────────────────────────────
547+
548+ function Get-GhostbitHistory {
549+ <#
550+ . SYNOPSIS
551+ List pastes created on this machine, or clear the local history.
552+
553+ . DESCRIPTION
554+ History is stored locally at:
555+ Windows : %LOCALAPPDATA%\ghostbit\history.jsonl
556+ macOS/Linux : ~/.local/share/ghostbit/history.jsonl
557+
558+ Nothing is sent to the server — this file stays on your machine only.
559+
560+ . PARAMETER Clear
561+ Wipe the local history file.
562+
563+ . EXAMPLE
564+ Get-GhostbitHistory
565+ Get-GhostbitHistory -Clear
566+ gbh
567+ #>
568+ [CmdletBinding ()]
569+ [Alias (' gbh' )]
570+ param (
571+ [switch ]$Clear
572+ )
573+
574+ if ($Clear ) {
575+ if (Test-Path $script :HistoryPath ) {
576+ Remove-Item $script :HistoryPath - Force
577+ Write-Host ' History cleared.'
578+ } else {
579+ Write-Host ' No history file found.'
580+ }
581+ return
582+ }
583+
584+ if (-not (Test-Path $script :HistoryPath )) {
585+ Write-Host ' No pastes in local history.' - ForegroundColor DarkGray
586+ Write-Host " History file: $script :HistoryPath " - ForegroundColor DarkGray
587+ return
588+ }
589+
590+ $entries = Get-Content $script :HistoryPath - Encoding UTF8 |
591+ Where-Object { $_ -match ' \S' } |
592+ ForEach-Object { $_ | ConvertFrom-Json }
593+
594+ if (-not $entries ) {
595+ Write-Host ' No pastes in local history.' - ForegroundColor DarkGray
596+ return
597+ }
598+
599+ $now = [DateTimeOffset ]::UtcNow.ToUnixTimeSeconds()
600+
601+ function Format-Age ([long ]$ts ) {
602+ $d = $now - $ts
603+ if ($d -lt 120 ) { return ' just now' }
604+ if ($d -lt 3600 ) { return " $ ( [int ]($d / 60 )) m ago" }
605+ if ($d -lt 86400 ) { return " $ ( [int ]($d / 3600 )) h ago" }
606+ return " $ ( [int ]($d / 86400 )) d ago"
607+ }
608+
609+ function Format-Expiry ($exp ) {
610+ if (-not $exp ) { return ' never' }
611+ $d = $exp - $now
612+ if ($d -le 0 ) { return ' expired' }
613+ if ($d -lt 3600 ) { return " in $ ( [int ]($d / 60 )) m" }
614+ if ($d -lt 86400 ) { return " in $ ( [int ]($d / 3600 )) h" }
615+ return " in $ ( [int ]($d / 86400 )) d"
616+ }
617+
618+ $header = ' {0,-12} {1,-14} {2,-12} {3,-10} {4}' -f ' ID' , ' Lang' , ' Created' , ' Expires' , ' URL'
619+ Write-Host $header
620+ Write-Host (' -' * 80 )
621+
622+ [array ]::Reverse(($entries = @ ($entries )))
623+ foreach ($e in $entries ) {
624+ $id = ([string ]$e.id ).PadRight(12 ).Substring(0 , [Math ]::Min(12 , ([string ]$e.id ).Length)).PadRight(12 )
625+ $lang = ([string ]($e.language ?? ' plain' )).PadRight(14 ).Substring(0 , [Math ]::Min(14 , ([string ]($e.language ?? ' plain' )).Length)).PadRight(14 )
626+ $created = (Format-Age $e.created_at ).PadRight(12 )
627+ $expires = (Format-Expiry $e.expires_at ).PadRight(10 )
628+ $url = $e.full_url ?? $e.url
629+ Write-Host (' {0} {1} {2} {3} {4}' -f $id , $lang , $created , $expires , $url )
630+ }
631+ }
632+
479633# ── Exports ───────────────────────────────────────────────────────────────────
480634
481- Export-ModuleMember - Function New-GhostbitPaste , Get-GhostbitPaste , Invoke-GhostbitConfig `
482- - Alias gb, gbv
635+ Export-ModuleMember - Function New-GhostbitPaste , Get-GhostbitPaste , Remove-GhostbitPaste , `
636+ Get-GhostbitHistory , Invoke-GhostbitConfig `
637+ - Alias gb, gbv, gbd, gbh
0 commit comments