Skip to content

Instantly share code, notes, and snippets.

@silverqx
Created October 29, 2025 13:48
Show Gist options
  • Select an option

  • Save silverqx/02833b3d69701c7489c9bd883ec3ed55 to your computer and use it in GitHub Desktop.

Select an option

Save silverqx/02833b3d69701c7489c9bd883ec3ed55 to your computer and use it in GitHub Desktop.
Extract timestamps from ChatGPT backup conversations.json using pwsh and jq
#!/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
}
#!/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