Eigenverft.Manifested.Codex.MetadataInstallAndSessions.ps1

<#
    Eigenverft.Manifested.Codex.MetadataInstallAndSessions
#>


function Get-CodexLocalRoot {
    [CmdletBinding()]
    param(
        [string]$LocalRoot = (Join-Path $env:LOCALAPPDATA 'CodexSlots')
    )

    return [System.IO.Path]::GetFullPath($LocalRoot)
}

function Get-CodexSessionStorePath {
    [CmdletBinding()]
    param(
        [string]$LocalRoot = (Get-CodexLocalRoot)
    )

    return (Join-Path (Join-Path $LocalRoot 'sessions') 'named-sessions.json')
}

function Get-CodexSessionKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$SessionName
    )

    return ($SessionName.Trim() -replace '\|', '_')
}

function Read-CodexSessionMap {
    [CmdletBinding()]
    param(
        [string]$SessionStorePath = (Get-CodexSessionStorePath)
    )

    $sessionMap = @{}
    if (-not (Test-Path -LiteralPath $SessionStorePath)) {
        return $sessionMap
    }

    try {
        $raw = Get-Content -LiteralPath $SessionStorePath -Raw
        if (-not [string]::IsNullOrWhiteSpace($raw)) {
            $obj = $raw | ConvertFrom-Json
            foreach ($property in $obj.PSObject.Properties) {
                $sessionMap[$property.Name] = $property.Value
            }
        }
    }
    catch {
        throw "Failed to read session store: $SessionStorePath"
    }

    return $sessionMap
}

function Write-CodexSessionMap {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$SessionMap,

        [string]$SessionStorePath = (Get-CodexSessionStorePath)
    )

    $sessionStoreRoot = Split-Path -Parent $SessionStorePath
    if (-not (Test-Path -LiteralPath $sessionStoreRoot)) {
        New-Item -ItemType Directory -Path $sessionStoreRoot -Force | Out-Null
    }

    ($SessionMap | ConvertTo-Json -Depth 6) | Set-Content -LiteralPath $SessionStorePath -Encoding UTF8
}

function Resolve-CodexCommandPath {
    [CmdletBinding()]
    param()

    $resolvedCodex = Get-Command codex -ErrorAction SilentlyContinue
    if (-not $resolvedCodex) {
        $resolvedCodex = Get-Command codex.cmd -ErrorAction SilentlyContinue
    }

    if (-not $resolvedCodex) {
        throw 'codex was not found on PATH. Install the Codex CLI or add it to PATH before using Eigenverft.Manifested.Codex.'
    }

    if ($resolvedCodex.PSObject.Properties['Path'] -and $resolvedCodex.Path) {
        return $resolvedCodex.Path
    }

    return $resolvedCodex.Source
}

function Resolve-CodexDirectory {
    [CmdletBinding()]
    param(
        [string]$Directory = (Get-Location).ProviderPath
    )

    $resolvedPaths = @(Resolve-Path -LiteralPath $Directory -ErrorAction Stop)

    if ($resolvedPaths.Count -ne 1) {
        throw "Directory path '$Directory' resolved to multiple locations."
    }

    $path = $resolvedPaths[0].ProviderPath
    if (-not $path) {
        $path = $resolvedPaths[0].Path
    }

    if (-not (Test-Path -LiteralPath $path -PathType Container)) {
        throw "Directory '$Directory' does not exist or is not a directory."
    }

    return [System.IO.Path]::GetFullPath($path)
}

function Get-CodexState {
    [CmdletBinding()]
    param()

    $localRoot = Get-CodexLocalRoot
    $sessionStorePath = Get-CodexSessionStorePath -LocalRoot $localRoot
    $sessionStoreExists = Test-Path -LiteralPath $sessionStorePath
    $sessionCount = 0

    if ($sessionStoreExists) {
        $sessionCount = @((Read-CodexSessionMap -SessionStorePath $sessionStorePath).Keys).Count
    }

    $codexCommandPath = $null
    try {
        $codexCommandPath = Resolve-CodexCommandPath
    }
    catch {
        $codexCommandPath = $null
    }

    [pscustomobject]@{
        LocalRoot          = $localRoot
        SessionStorePath   = $sessionStorePath
        SessionStoreExists = $sessionStoreExists
        SessionCount       = $sessionCount
        CodexCommandPath   = $codexCommandPath
        CodexAvailable     = [bool]$codexCommandPath
        ReadyToRun         = [bool]$codexCommandPath
    }
}

function Get-CodexSession {
<#
.SYNOPSIS
Gets one or more stored Codex wrapper sessions.
 
.DESCRIPTION
Reads the local named session store used by the Codex PowerShell wrapper.
 
If SessionName is supplied, returns that single session if present.
If SessionName is omitted, returns all stored sessions.
 
.PARAMETER SessionName
Optional session name to fetch.
Alias: Session
#>

    [CmdletBinding()]
    param(
        [Alias('Session')]
        [string]$SessionName
    )

    $sessionStorePath = Get-CodexSessionStorePath
    if (-not (Test-Path -LiteralPath $sessionStorePath)) {
        if ($PSBoundParameters.ContainsKey('SessionName')) {
            return $null
        }

        return @()
    }

    $sessionMap = Read-CodexSessionMap -SessionStorePath $sessionStorePath

    if ($PSBoundParameters.ContainsKey('SessionName')) {
        $sessionKey = Get-CodexSessionKey -SessionName $SessionName

        if (-not $sessionMap.ContainsKey($sessionKey)) {
            return $null
        }

        $value = $sessionMap[$sessionKey]

        return [pscustomobject]@{
            SessionName   = [string]$value.SessionName
            ThreadId      = [string]$value.ThreadId
            LastDirectory = [string]$value.LastDirectory
            UpdatedUtc    = [string]$value.UpdatedUtc
        }
    }

    $result = foreach ($key in ($sessionMap.Keys | Sort-Object)) {
        $value = $sessionMap[$key]

        [pscustomobject]@{
            SessionName   = [string]$value.SessionName
            ThreadId      = [string]$value.ThreadId
            LastDirectory = [string]$value.LastDirectory
            UpdatedUtc    = [string]$value.UpdatedUtc
        }
    }

    return @($result)
}

function Remove-CodexSession {
<#
.SYNOPSIS
Removes a stored Codex wrapper session.
 
.DESCRIPTION
Deletes a named session from the local session store.
 
This only removes the wrapper-side session mapping.
It does not delete any Codex-internal session history.
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Alias('Session')]
        [Parameter(Mandatory = $true)]
        [string]$SessionName,

        [switch]$Force
    )

    if (-not $Force) {
        throw "Pass -Force to remove session '$SessionName'."
    }

    $sessionStorePath = Get-CodexSessionStorePath
    if (-not (Test-Path -LiteralPath $sessionStorePath)) {
        return $false
    }

    $sessionMap = Read-CodexSessionMap -SessionStorePath $sessionStorePath
    $sessionKey = Get-CodexSessionKey -SessionName $SessionName

    if (-not $sessionMap.ContainsKey($sessionKey)) {
        return $false
    }

    if ($PSCmdlet.ShouldProcess($sessionKey, 'Remove stored Codex session')) {
        [void]$sessionMap.Remove($sessionKey)
        Write-CodexSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
        return $true
    }

    return $false
}

function Set-CodexSessionDirectory {
<#
.SYNOPSIS
Updates the stored last directory for a Codex wrapper session.
 
.DESCRIPTION
Sets LastDirectory for an existing named session in the local session store.
 
This does not change Codex-internal session state directly.
It only changes the wrapper's remembered working directory.
#>

    [CmdletBinding()]
    param(
        [Alias('Session')]
        [Parameter(Mandatory = $true)]
        [string]$SessionName,

        [Parameter(Mandatory = $true)]
        [string]$Directory
    )

    $sessionStorePath = Get-CodexSessionStorePath
    $resolvedDirectory = Resolve-CodexDirectory -Directory $Directory

    if (-not (Test-Path -LiteralPath $sessionStorePath)) {
        throw "Session store was not found: $sessionStorePath"
    }

    $sessionMap = Read-CodexSessionMap -SessionStorePath $sessionStorePath
    $sessionKey = Get-CodexSessionKey -SessionName $SessionName

    if (-not $sessionMap.ContainsKey($sessionKey)) {
        throw "Session '$SessionName' was not found."
    }

    $existing = $sessionMap[$sessionKey]

    $sessionMap[$sessionKey] = @{
        SessionName   = [string]$existing.SessionName
        ThreadId      = [string]$existing.ThreadId
        LastDirectory = $resolvedDirectory
        UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
    }

    Write-CodexSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath

    return [pscustomobject]@{
        SessionName   = [string]$sessionMap[$sessionKey].SessionName
        ThreadId      = [string]$sessionMap[$sessionKey].ThreadId
        LastDirectory = [string]$sessionMap[$sessionKey].LastDirectory
        UpdatedUtc    = [string]$sessionMap[$sessionKey].UpdatedUtc
    }
}

function Clear-CodexSessions {
<#
.SYNOPSIS
Clears all stored Codex wrapper sessions.
 
.DESCRIPTION
Deletes the local session store file used by the Codex PowerShell wrapper.
 
This only removes wrapper-side mappings.
It does not delete Codex-internal session history.
#>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [switch]$Force
    )

    if (-not $Force) {
        throw 'Pass -Force to clear all stored sessions.'
    }

    $sessionStorePath = Get-CodexSessionStorePath
    if (-not (Test-Path -LiteralPath $sessionStorePath)) {
        return $false
    }

    if ($PSCmdlet.ShouldProcess($sessionStorePath, 'Remove all stored Codex sessions')) {
        Remove-Item -LiteralPath $sessionStorePath -Force
        return $true
    }

    return $false
}

function Invoke-CodexTask {
<#
.SYNOPSIS
Runs a Codex non-interactive task and maintains wrapper-level named session state.
 
.DESCRIPTION
Thin PowerShell wrapper around:
 
- codex exec
- codex exec resume
 
Session continuity is based on the stored thread id only.
 
Stored session record:
- SessionName
- ThreadId
- LastDirectory
- UpdatedUtc
 
Directory behavior:
- If SessionName is supplied and Directory is supplied:
  - use Directory
  - store/update LastDirectory
- If SessionName is supplied and Directory is omitted:
  - use stored LastDirectory if present
  - otherwise use current shell directory
- If SessionName is omitted:
  - use Directory if provided
  - otherwise use current shell directory
 
Important current assumption:
- initial run uses `codex exec --cd <DIR> ...`
- resume uses `codex exec resume ...`
- because `codex exec resume --help` does not show `--cd`,
  this wrapper temporarily changes the PowerShell working directory
  with Push-Location / Pop-Location for resume runs.
 
Repo check behavior:
- default is relaxed
- wrapper adds --skip-git-repo-check
- use -EnforceRepoCheck to disable that behavior
 
OutputLastMessage behavior:
- this wrapper does NOT pass --output-last-message to Codex
- when JSON output is available, it extracts the last agent message itself
  and writes it to a local file
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Prompt,

        [Alias('Path')]
        [string]$Directory,

        [Alias('Session')]
        [string]$SessionName,

        [bool]$AllowDangerous = $true,

        [ValidateSet('read-only', 'workspace-write', 'danger-full-access')]
        [string]$Sandbox = 'danger-full-access',

        [ValidateSet('untrusted', 'on-request', 'never')]
        [string]$AskForApproval = 'never',

        [switch]$EnforceRepoCheck,

        [bool]$Json = $true,

        [string]$OutputLastMessage,

        [ValidateSet('always', 'never', 'auto')]
        [string]$Color = 'never',

        [Nullable[bool]]$Ephemeral,

        [string]$Model,

        [string[]]$AddDir
    )

    $codexCmd = Resolve-CodexCommandPath

    $currentDirectory = Resolve-CodexDirectory -Directory ((Get-Location).ProviderPath)
    $directoryProvided = $PSBoundParameters.ContainsKey('Directory')
    $requestedDirectory = $null

    if ($directoryProvided) {
        $requestedDirectory = Resolve-CodexDirectory -Directory $Directory
    }

    if ($null -eq $Ephemeral) {
        $Ephemeral = [string]::IsNullOrWhiteSpace($SessionName)
    }

    $sessionStorePath = Get-CodexSessionStorePath
    $sessionStoreRoot = Split-Path -Parent $sessionStorePath

    if (-not (Test-Path -LiteralPath $sessionStoreRoot)) {
        New-Item -ItemType Directory -Path $sessionStoreRoot -Force | Out-Null
    }

    $sessionMap = @{}
    if (Test-Path -LiteralPath $sessionStorePath) {
        try {
            $raw = Get-Content -LiteralPath $sessionStorePath -Raw
            if (-not [string]::IsNullOrWhiteSpace($raw)) {
                $obj = $raw | ConvertFrom-Json
                foreach ($property in $obj.PSObject.Properties) {
                    $sessionMap[$property.Name] = $property.Value
                }
            }
        }
        catch {
            $sessionMap = @{}
        }
    }

    $sessionKey = $null
    $existingSession = $null
    $effectiveDirectory = $currentDirectory

    if (-not [string]::IsNullOrWhiteSpace($SessionName)) {
        $sessionKey = Get-CodexSessionKey -SessionName $SessionName

        if ($sessionMap.ContainsKey($sessionKey)) {
            $existingSession = $sessionMap[$sessionKey]
        }

        if ($directoryProvided) {
            $effectiveDirectory = $requestedDirectory
        }
        elseif ($existingSession -and $existingSession.LastDirectory) {
            $effectiveDirectory = [string]$existingSession.LastDirectory
        }
        else {
            $effectiveDirectory = $currentDirectory
        }
    }
    elseif ($directoryProvided) {
        $effectiveDirectory = $requestedDirectory
    }

    $canResume = [bool](
        $existingSession -and
        $existingSession.ThreadId
    )

    $effectiveJson =
        if (-not [string]::IsNullOrWhiteSpace($SessionName)) {
            $true
        }
        else {
            $Json
        }

    if ([string]::IsNullOrWhiteSpace($OutputLastMessage) -and $effectiveJson) {
        $safeDirName = ([IO.Path]::GetFileName($effectiveDirectory)).Trim()
        if ([string]::IsNullOrWhiteSpace($safeDirName)) {
            $safeDirName = 'workspace'
        }

        $safeDirName = ($safeDirName -replace '[^A-Za-z0-9._-]', '_')

        if ([string]::IsNullOrWhiteSpace($SessionName)) {
            $OutputLastMessage = Join-Path $env:TEMP ("codex-last-message-{0}-{1}.txt" -f $safeDirName, ([Guid]::NewGuid().ToString('N')))
        }
        else {
            $safeSessionFile = ($SessionName -replace '[^A-Za-z0-9._-]', '_')
            $OutputLastMessage = Join-Path $env:TEMP ("codex-last-message-{0}-{1}.txt" -f $safeDirName, $safeSessionFile)
        }
    }

    $cargs = New-Object System.Collections.Generic.List[string]

    if ($canResume) {
        [void]$cargs.Add('exec')
        [void]$cargs.Add('resume')

        if (-not [string]::IsNullOrWhiteSpace($Model)) {
            [void]$cargs.Add('--model')
            [void]$cargs.Add($Model)
        }

        if ($AllowDangerous) {
            [void]$cargs.Add('--dangerously-bypass-approvals-and-sandbox')
        }

        if (-not $EnforceRepoCheck) {
            [void]$cargs.Add('--skip-git-repo-check')
        }

        if ($Ephemeral) {
            [void]$cargs.Add('--ephemeral')
        }

        if ($effectiveJson) {
            [void]$cargs.Add('--json')
        }

        [void]$cargs.Add([string]$existingSession.ThreadId)
        [void]$cargs.Add($Prompt)
    }
    else {
        [void]$cargs.Add('exec')

        [void]$cargs.Add('--cd')
        [void]$cargs.Add($effectiveDirectory)

        if (-not [string]::IsNullOrWhiteSpace($Model)) {
            [void]$cargs.Add('--model')
            [void]$cargs.Add($Model)
        }

        if ($AllowDangerous) {
            [void]$cargs.Add('--dangerously-bypass-approvals-and-sandbox')
        }
        else {
            [void]$cargs.Add('--sandbox')
            [void]$cargs.Add($Sandbox)
        }

        if (-not $EnforceRepoCheck) {
            [void]$cargs.Add('--skip-git-repo-check')
        }

        foreach ($dir in @($AddDir)) {
            if (-not [string]::IsNullOrWhiteSpace($dir)) {
                [void]$cargs.Add('--add-dir')
                [void]$cargs.Add((Resolve-CodexDirectory -Directory $dir))
            }
        }

        if ($Ephemeral) {
            [void]$cargs.Add('--ephemeral')
        }

        if ($effectiveJson) {
            [void]$cargs.Add('--json')
        }

        if (-not [string]::IsNullOrWhiteSpace($Color)) {
            [void]$cargs.Add('--color')
            [void]$cargs.Add($Color)
        }

        [void]$cargs.Add($Prompt)
    }

    $argArray = $cargs.ToArray()
    $lastAgentMessage = $null

    try {
        if ($canResume) {
            Push-Location -LiteralPath $effectiveDirectory
        }

        if ($effectiveJson) {
            $outputLines = @(& $codexCmd @argArray 2>&1)
            $exitCode = $LASTEXITCODE

            foreach ($line in $outputLines) {
                $text = [string]$line
                Write-Host $text

                try {
                    $evt = $text | ConvertFrom-Json

                    if (-not $canResume -and $evt.type -eq 'thread.started' -and $evt.thread_id -and -not [string]::IsNullOrWhiteSpace($SessionName)) {
                        $sessionMap[$sessionKey] = @{
                            SessionName   = $SessionName
                            ThreadId      = [string]$evt.thread_id
                            LastDirectory = $effectiveDirectory
                            UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
                        }

                        Write-CodexSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
                        $existingSession = $sessionMap[$sessionKey]
                    }

                    if ($evt.type -eq 'item.completed' -and $evt.item -and $evt.item.type -eq 'agent_message' -and $evt.item.text) {
                        $lastAgentMessage = [string]$evt.item.text
                    }
                }
                catch {
                    # Ignore non-JSON lines.
                }
            }

            if ($canResume -and -not [string]::IsNullOrWhiteSpace($SessionName)) {
                $sessionMap[$sessionKey] = @{
                    SessionName   = $SessionName
                    ThreadId      = [string]$existingSession.ThreadId
                    LastDirectory = $effectiveDirectory
                    UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
                }

                Write-CodexSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
                $existingSession = $sessionMap[$sessionKey]
            }

            if (-not [string]::IsNullOrWhiteSpace($OutputLastMessage) -and -not [string]::IsNullOrWhiteSpace($lastAgentMessage)) {
                Set-Content -LiteralPath $OutputLastMessage -Value $lastAgentMessage -Encoding UTF8
            }
        }
        else {
            & $codexCmd @argArray
            $exitCode = $LASTEXITCODE

            if ($canResume -and -not [string]::IsNullOrWhiteSpace($SessionName)) {
                $sessionMap[$sessionKey] = @{
                    SessionName   = $SessionName
                    ThreadId      = [string]$existingSession.ThreadId
                    LastDirectory = $effectiveDirectory
                    UpdatedUtc    = [DateTime]::UtcNow.ToString('o')
                }

                Write-CodexSessionMap -SessionMap $sessionMap -SessionStorePath $sessionStorePath
                $existingSession = $sessionMap[$sessionKey]
            }
        }
    }
    finally {
        if ($canResume) {
            Pop-Location
        }
    }

    if ($exitCode -ne 0) {
        throw "codex command failed with exit code $exitCode."
    }

    [pscustomobject]@{
        CommandPath       = $codexCmd
        Directory         = $effectiveDirectory
        SessionName       = $SessionName
        ThreadId          = if ($existingSession) { $existingSession.ThreadId } else { $null }
        Prompt            = $Prompt
        AllowDangerous    = [bool]$AllowDangerous
        Json              = [bool]$effectiveJson
        Ephemeral         = [bool]$Ephemeral
        OutputLastMessage = $OutputLastMessage
        LastAgentMessage  = $lastAgentMessage
        ExitCode          = $exitCode
        Resumed           = $canResume
        EffectiveArgs     = $argArray
    }
}