Created
October 29, 2025 13:48
-
-
Save silverqx/02833b3d69701c7489c9bd883ec3ed55 to your computer and use it in GitHub Desktop.
Extract timestamps from ChatGPT backup conversations.json using pwsh and jq
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env pwsh | |
| Set-StrictMode -Version 3.0 | |
| # Script variables section | |
| # --- | |
| Set-Alias NewLine Write-Host -Option ReadOnly -Force | |
| # Functions section | |
| # --- | |
| # Write a Header message to a host | |
| function Write-Header { | |
| [OutputType([void])] | |
| Param( | |
| [Parameter(Position = 0, Mandatory, HelpMessage = 'Writes a header message to the host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Header, | |
| [Parameter(HelpMessage = 'No newline before the header message.')] | |
| [switch] $NoNewlineBefore, | |
| [Parameter(HelpMessage = 'No newline after the header message.')] | |
| [switch] $NoNewlineAfter, | |
| [Parameter(HelpMessage = 'No newlines before and after the header message.')] | |
| [switch] $NoNewlines | |
| ) | |
| if (-not $NoNewlineBefore -and -not $NoNewlines) { | |
| NewLine | |
| } | |
| Write-Host $Header -ForegroundColor DarkBlue | |
| if (-not $NoNewlineAfter -and -not $NoNewlines) { | |
| NewLine | |
| } | |
| } | |
| # Write an info message to a host | |
| function Write-Info { | |
| [OutputType([void])] | |
| Param( | |
| [Parameter(Position = 0, Mandatory, HelpMessage = 'Writes an info message to the host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Message, | |
| [Parameter(HelpMessage = 'No newline is added after the last output string.')] | |
| [switch] $NoNewline | |
| ) | |
| Write-Host $Message -ForegroundColor DarkGreen -NoNewline:$NoNewline | |
| } | |
| # Write a progress message to a host | |
| function Write-Progress { | |
| [OutputType([void])] | |
| Param( | |
| [Parameter(Position = 0, Mandatory, HelpMessage = 'Writes a progress message to the host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Message, | |
| [Parameter(HelpMessage = 'No newline is added after the last output string.')] | |
| [switch] $NoNewline | |
| ) | |
| Write-Host $Message -ForegroundColor DarkYellow -NoNewline:$NoNewline | |
| } | |
| # Write an error message to a host | |
| function Write-Error { | |
| [OutputType([void])] | |
| Param( | |
| [Parameter(Position = 0, Mandatory, HelpMessage = 'Writes an error message to the host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Message | |
| ) | |
| Write-Host $Message -ForegroundColor Red | |
| } | |
| # Write an error message to a host and exit with 1 error code | |
| function Write-ExitError { | |
| [OutputType([void])] | |
| Param( | |
| [Parameter(Position = 0, Mandatory, HelpMessage = 'Writes an error message to the host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Message, | |
| [Parameter(HelpMessage = 'Specifies the exit code.')] | |
| [ValidateNotNull()] | |
| [int] | |
| $ExitCode = 1, | |
| [Parameter(HelpMessage = 'No newline before the header message.')] | |
| [switch] $NoNewlineBefore | |
| ) | |
| if (-not $NoNewlineBefore) { | |
| NewLine | |
| } | |
| Write-Error $Message | |
| exit $ExitCode | |
| } | |
| # Write a label and a value to a host | |
| function Write-LogItem { | |
| param( | |
| [Parameter(Position = 0, Mandatory, | |
| HelpMessage = 'Specifies the label to write to a host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $Label, | |
| [Parameter(Position = 1, HelpMessage = 'Specifies the value to write to a host.')] | |
| [string] $Value, | |
| [Parameter(Position = 2, HelpMessage = 'Specifies additional info to write to a host.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $Info, | |
| [Parameter(HelpMessage = 'Specifies the color of the label.')] | |
| [ConsoleColor] $LabelColor = [ConsoleColor]::DarkGreen, | |
| [Parameter(HelpMessage = 'Specifies the color of the value.')] | |
| [ConsoleColor] $ValueColor, | |
| [Parameter(HelpMessage = 'Specifies the color for special values like <null> or <empty>.')] | |
| [ConsoleColor] $SpecialColor = [ConsoleColor]::DarkGray, | |
| [Parameter(HelpMessage = 'Specifies the color of the info text.')] | |
| [ConsoleColor] $InfoColor = [ConsoleColor]::DarkGray, | |
| [Parameter(HelpMessage = 'No newline is added after the last output string.')] | |
| [switch] $NoNewline | |
| ) | |
| # Normalize the label to always end with ': ' | |
| if (-not $Label.TrimEnd().EndsWith(':')) { | |
| $Label += ': ' | |
| } | |
| # Handle special values | |
| if (-not $PSBoundParameters.ContainsKey('Value')) { | |
| $Value = '<not_provided>' | |
| $ValueColor = $SpecialColor | |
| } | |
| elseif ($Value -eq '') { | |
| $Value = '<empty>' | |
| $ValueColor = $SpecialColor | |
| } | |
| Write-Host "$Label" -NoNewline -ForegroundColor $LabelColor | |
| $hasInfo = $Info.Trim() -ne '' | |
| $NoNewlineForValue = $hasInfo ? $true : $NoNewline | |
| $ValueColor ` | |
| ? (Write-Host $Value -ForegroundColor $ValueColor -NoNewline:$NoNewlineForValue) | |
| : (Write-Host $Value -NoNewline:$NoNewlineForValue) | |
| # Nothing to do, no info passed | |
| if (-not $hasInfo) { | |
| return | |
| } | |
| # Ensure info starts with a space | |
| if (-not $Info.StartsWith(' ')) { | |
| $Info = " $Info" | |
| } | |
| Write-Host "$Info" -ForegroundColor $InfoColor -NoNewline:$NoNewline | |
| } | |
| # Present a dialog allowing the user to choose continue or quit/return | |
| function Approve-Continue { | |
| [CmdletBinding(DefaultParameterSetName = 'Return')] | |
| [OutputType([int], ParameterSetName = 'Return')] | |
| [OutputType([void], ParameterSetName = 'Exit')] | |
| Param( | |
| [Parameter(HelpMessage = 'Specifies the caption to precede or title the prompt.')] | |
| [ValidateNotNull()] | |
| [string] | |
| $Caption = '', | |
| [Parameter(Position = 0, | |
| HelpMessage = 'Specifies a message that describes what the choice is for.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] | |
| $Message = 'Ok to proceed?', | |
| [Parameter(HelpMessage = 'The index of the label in the choices collection element ' + | |
| 'to be presented to the user as the default choice. -1 means "no default". ' + | |
| 'Must be a valid index.')] | |
| [ValidateNotNull()] | |
| [int] | |
| $DefaultChoice = 1, | |
| [Parameter(ParameterSetName = 'Exit', | |
| HelpMessage = 'No newline before the header message.')] | |
| [switch] $Exit, | |
| [Parameter(ParameterSetName = 'Exit', HelpMessage = 'Specifies the exit code.')] | |
| [ValidateNotNull()] | |
| [int] | |
| $ExitCode = 1 | |
| ) | |
| $isExitSet = $PsCmdlet.ParameterSetName -eq 'Exit' | |
| $confirmChoices = [System.Management.Automation.Host.ChoiceDescription[]](@( | |
| New-Object System.Management.Automation.Host.ChoiceDescription('&Yes', 'Continue') | |
| New-Object System.Management.Automation.Host.ChoiceDescription( ` | |
| '&No', ($isExitSet ? 'Quit' : 'Skip') | |
| ) | |
| )) | |
| NewLine | |
| $answer = $Host.Ui.PromptForChoice($Caption, $Message, $confirmChoices, $DefaultChoice) | |
| if ($isExitSet) { | |
| switch ($answer) { | |
| 0 { return } | |
| 1 { Write-ExitError -ExitCode $ExitCode "Quit" } | |
| } | |
| } | |
| return $answer | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env pwsh | |
| Param( | |
| [Parameter(Position = 0, | |
| HelpMessage = 'Specifies the path to the ChatGPT backup conversations.json file.')] | |
| [ValidateNotNullOrEmpty()] | |
| [string] $Path = 'conversations.json', | |
| [Parameter(HelpMessage = 'Specifies the starting timestamp to retrieve messages from.')] | |
| [ValidateNotNull()] | |
| [ValidateRange(0, [double]::MaxValue)] | |
| [double] $FromTimestamp | |
| ) | |
| Set-StrictMode -Version 3.0 | |
| . $PSScriptRoot\private\Common-Host.ps1 | |
| # Script variables section | |
| # --- | |
| $Script:LastTimestampPath = $env:APPDATA + '\flow\ChatGPT\last_backup_timestamp.txt' | |
| $Script:LastTimestampBackupPath = $Script:LastTimestampPath + '.bak' | |
| $Script:LastTimestampBackup1Path = $Script:LastTimestampPath + '.bak1' | |
| # Functions section | |
| # --- | |
| # Read the last timestamp from the file, or return 0.0 if the file doesn't exist | |
| function Read-LastTimestamp { | |
| [OutputType([double])] | |
| Param() | |
| if ($PSBoundParameters.ContainsKey('FromTimestamp') -or | |
| -not (Test-Path $Script:LastTimestampPath) | |
| ) { | |
| return 0.0 | |
| } | |
| [double] $result = Get-Content -Path $Script:LastTimestampPath | |
| # Covers the case when the file is empty or contains invalid data | |
| if ($result -eq 0.0) { | |
| return 0.0 | |
| } | |
| Write-Info 'Using previously saved timestamp: ' -NoNewline | |
| Write-Host $result.ToString([System.Globalization.CultureInfo]::InvariantCulture) | |
| return $result | |
| } | |
| # Backup the last timestamp file to a backup file | |
| function Backup-LastTimestamp { | |
| [OutputType([void])] | |
| Param() | |
| # .bak to .bak1 | |
| if (Test-Path $Script:LastTimestampBackupPath) { | |
| Move-Item ` | |
| -Path $Script:LastTimestampBackupPath ` | |
| -Destination $Script:LastTimestampBackup1Path -Force | |
| } | |
| # Backup the last timestamp file to .bak | |
| if (-not (Test-Path $Script:LastTimestampPath)) { | |
| return | |
| } | |
| Move-Item -Path $Script:LastTimestampPath -Destination $Script:LastTimestampBackupPath -Force | |
| } | |
| # Save the last timestamp to the file if it is greater than the FromTimestamp | |
| function Save-LastTimestamp { | |
| [OutputType([void])] | |
| Param() | |
| [double] $timestampLast = $timestamps[-1] | |
| # Save only if the last timestamp is greater than the FromTimestamp | |
| if ($timestampLast -le $FromTimestamp) { | |
| return | |
| } | |
| Backup-LastTimestamp | |
| # Add a small value to ensure it's greater than FromTimestamp | |
| $timestampLast += 0.000001 | |
| $timestampString = $timestampLast.ToString([System.Globalization.CultureInfo]::InvariantCulture) | |
| Set-Content -Path $Script:LastTimestampPath -Value $timestampString | |
| Write-Info "Saved Latest timestamp: $timestampString" | |
| } | |
| # Main section | |
| # --- | |
| # Test whether the ChatGPT backup file exists | |
| if (-not (Test-Path $Path)) { | |
| Write-ExitError "The ChatGPT backup file '$Path' doesn't exist." | |
| } | |
| $FromTimestamp = Read-LastTimestamp | |
| [array] $timestamps = Get-Content -Path $Path | |
| | jq --argjson timestampMin "$FromTimestamp" --raw-output @' | |
| .[] | |
| | .mapping | |
| | to_entries[] | |
| | select(.key | test("^[0-9a-f-]{36}$")) # only UUID keys | |
| | .value.message as $msg | |
| | select( | |
| $msg.create_time != null | |
| and $msg.create_time > $timestampMin | |
| and ($msg.author.role == "user" or $msg.author.role == "assistant") | |
| and $msg.content.content_type == "text" | |
| and $msg.content.parts[0] != "" | |
| ) | |
| | $msg.create_time | |
| '@ | |
| | Sort-Object | |
| if ($timestamps -eq $null -or $timestamps -isnot [array] -or $timestamps.Count -eq 0) { | |
| Write-ExitError "No messages found in the ChatGPT backup file after the specified timestamp." | |
| } | |
| # Save latest timestamp if available | |
| Save-LastTimestamp | |
| Write-Output $timestamps | |
| # For debugging when I want to see also messages | |
| # | "-------------------------------------------------------------------------------------------------------\n\($msg.create_time)\n----\n\($msg.content.parts[0])" | |
| # | "\($msg.create_time) \t \($msg.author.role): \($msg.content.parts[0] | gsub("\n"; " ") | .[0:80])" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment