GPT Proto
Home/Skills/claude-to-im

claude-to-im

Bridge THIS Claude Code or Codex session to Telegram, Discord, Feishu/Lark, QQ, or WeChat so the

Download for Windows

supervisor-windows.ps1

<#
.SYNOPSIS
  Windows daemon manager for claude-to-im bridge.

.DESCRIPTION
  Manages the bridge process on Windows.
  Preferred: WinSW or NSSM wrapping as a Windows Service.
  Fallback:  Start-Process with hidden window + PID tracking.

  Usage:
    powershell -File scripts\daemon.ps1 start
    powershell -File scripts\daemon.ps1 stop
    powershell -File scripts\daemon.ps1 status
    powershell -File scripts\daemon.ps1 logs [N]
    powershell -File scripts\daemon.ps1 install-service   # WinSW/NSSM setup
    powershell -File scripts\daemon.ps1 uninstall-service
#>

param(
    [Parameter(Position=0)]
    [ValidateSet('start','stop','status','logs','install-service','uninstall-service','help')]
    [string]$Command = 'help',

    [Parameter(Position=1)]
    [int]$LogLines = 50
)

$ErrorActionPreference = 'Stop'

# ── Paths ──
$CtiHome    = if ($env:CTI_HOME) { $env:CTI_HOME } else { Join-Path $env:USERPROFILE '.claude-to-im' }
$SkillDir   = Split-Path -Parent (Split-Path -Parent $PSCommandPath)
$RuntimeDir = Join-Path $CtiHome 'runtime'
$PidFile    = Join-Path $RuntimeDir 'bridge.pid'
$StatusFile = Join-Path $RuntimeDir 'status.json'
$LogFile    = Join-Path $CtiHome 'logs' 'bridge.log'
$DaemonMjs  = Join-Path $SkillDir 'dist' 'daemon.mjs'

$ServiceName = 'ClaudeToIMBridge'

# ── Helpers ──

function Ensure-Dirs {
    @('data','logs','runtime','data/messages') | ForEach-Object {
        $dir = Join-Path $CtiHome $_
        if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    }
}

function Ensure-Built {
    if (-not (Test-Path $DaemonMjs)) {
        Write-Host "Building daemon bundle..."
        Push-Location $SkillDir
        npm run build
        Pop-Location
    } else {
        $srcFiles = Get-ChildItem -Path (Join-Path $SkillDir 'src') -Filter '*.ts' -Recurse
        $bundleTime = (Get-Item $DaemonMjs).LastWriteTime
        $stale = $srcFiles | Where-Object { $_.LastWriteTime -gt $bundleTime } | Select-Object -First 1
        if ($stale) {
            Write-Host "Rebuilding daemon bundle (source changed)..."
            Push-Location $SkillDir
            npm run build
            Pop-Location
        }
    }
}

function Read-Pid {
    if (Test-Path $PidFile) { return (Get-Content $PidFile -Raw).Trim() }
    return $null
}

function Test-PidAlive {
    param([string]$Pid)
    if (-not $Pid) { return $false }
    try { $null = Get-Process -Id ([int]$Pid) -ErrorAction Stop; return $true }
    catch { return $false }
}

function Test-StatusRunning {
    if (-not (Test-Path $StatusFile)) { return $false }
    $json = Get-Content $StatusFile -Raw | ConvertFrom-Json
    return $json.running -eq $true
}

function Show-LastExitReason {
    if (Test-Path $StatusFile) {
        $json = Get-Content $StatusFile -Raw | ConvertFrom-Json
        if ($json.lastExitReason) {
            Write-Host "Last exit reason: $($json.lastExitReason)"
        }
    }
}

function Show-FailureHelp {
    Write-Host ""
    Write-Host "Recent logs:"
    if (Test-Path $LogFile) {
        Get-Content $LogFile -Tail 20
    } else {
        Write-Host "  (no log file)"
    }
    Write-Host ""
    Write-Host "Next steps:"
    Write-Host "  1. Run diagnostics:  powershell -File `"$SkillDir\scripts\doctor.ps1`""
    Write-Host "  2. Check full logs:  powershell -File `"$SkillDir\scripts\daemon.ps1`" logs 100"
    Write-Host "  3. Rebuild bundle:   cd `"$SkillDir`"; npm run build"
}

function Get-NodePath {
    $nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source
    if (-not $nodePath) {
        Write-Error "Node.js not found in PATH. Install Node.js >= 20."
        exit 1
    }
    return $nodePath
}

# ── WinSW / NSSM detection ──

function Find-ServiceManager {
    # Prefer WinSW, then NSSM
    $winsw = Get-Command 'WinSW.exe' -ErrorAction SilentlyContinue
    if ($winsw) { return @{ type = 'winsw'; path = $winsw.Source } }

    $nssm = Get-Command 'nssm.exe' -ErrorAction SilentlyContinue
    if ($nssm) { return @{ type = 'nssm'; path = $nssm.Source } }

    return $null
}

function Install-WinSWService {
    param([string]$WinSWPath)
    $nodePath = Get-NodePath
    $xmlPath = Join-Path $SkillDir "$ServiceName.xml"

    # Run as current user so the service can access ~/.claude-to-im and Codex login state
    $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    Write-Host "Service will run as: $currentUser"
    $cred = Get-Credential -UserName $currentUser -Message "Enter password for '$currentUser' (required for Windows Service logon)"
    $plainPwd = $cred.GetNetworkCredential().Password

    # Generate WinSW config XML
    @"
<service>
  <id>$ServiceName</id>
  <name>Claude-to-IM Bridge</name>
  <description>Claude-to-IM bridge daemon</description>
  <executable>$nodePath</executable>
  <arguments>$DaemonMjs</arguments>
  <workingdirectory>$SkillDir</workingdirectory>
  <serviceaccount>
    <username>$currentUser</username>
    <password>$([System.Security.SecurityElement]::Escape($plainPwd))</password>
    <allowservicelogon>true</allowservicelogon>
  </serviceaccount>
  <env name="USERPROFILE" value="$env:USERPROFILE"/>
  <env name="APPDATA" value="$env:APPDATA"/>
  <env name="LOCALAPPDATA" value="$env:LOCALAPPDATA"/>
  <env name="PATH" value="$env:PATH"/>
  <env name="CTI_HOME" value="$CtiHome"/>
  <logpath>$(Join-Path $CtiHome 'logs')</logpath>
  <log mode="append">
    <logfile>bridge-service.log</logfile>
  </log>
  <onfailure action="restart" delay="10 sec"/>
  <onfailure action="restart" delay="30 sec"/>
  <onfailure action="none"/>
</service>
"@ | Set-Content -Path $xmlPath -Encoding UTF8

    # Copy WinSW next to the XML with matching name
    $winswCopy = Join-Path $SkillDir "$ServiceName.exe"
    Copy-Item $WinSWPath $winswCopy -Force

    & $winswCopy install
    Write-Host "Service '$ServiceName' installed via WinSW."
    Write-Host "  Service account: $currentUser"
    Write-Host "Start with:  & `"$winswCopy`" start"
    Write-Host "Or:          sc.exe start $ServiceName"
}

function Install-NSSMService {
    param([string]$NSSMPath)
    $nodePath = Get-NodePath

    # Run as current user so the service can access ~/.claude-to-im and Codex login state
    $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    Write-Host "Service will run as: $currentUser"
    $cred = Get-Credential -UserName $currentUser -Message "Enter password for '$currentUser' (required for Windows Service logon)"
    $plainPwd = $cred.GetNetworkCredential().Password

    & $NSSMPath install $ServiceName $nodePath $DaemonMjs
    & $NSSMPath set $ServiceName AppDirectory $SkillDir
    & $NSSMPath set $ServiceName ObjectName $currentUser $plainPwd
    & $NSSMPath set $ServiceName AppStdout $LogFile
    & $NSSMPath set $ServiceName AppStderr $LogFile
    & $NSSMPath set $ServiceName AppStdoutCreationDisposition 4
    & $NSSMPath set $ServiceName AppStderrCreationDisposition 4
    & $NSSMPath set $ServiceName Description "Claude-to-IM bridge daemon"
    & $NSSMPath set $ServiceName AppRestartDelay 10000
    & $NSSMPath set $ServiceName AppEnvironmentExtra "USERPROFILE=$env:USERPROFILE" "APPDATA=$env:APPDATA" "LOCALAPPDATA=$env:LOCALAPPDATA" "CTI_HOME=$CtiHome"

    Write-Host "Service '$ServiceName' installed via NSSM."
    Write-Host "  Service account: $currentUser"
    Write-Host "Start with:  nssm start $ServiceName"
    Write-Host "Or:          sc.exe start $ServiceName"
}

# ── Fallback: Start-Process (no service manager) ──

function Start-Fallback {
    $nodePath = Get-NodePath

    # Clean env
    $envClone = [System.Collections.Hashtable]::new()
    foreach ($key in [System.Environment]::GetEnvironmentVariables().Keys) {
        $envClone[$key] = [System.Environment]::GetEnvironmentVariable($key)
    }
    # Remove CLAUDECODE
    [System.Environment]::SetEnvironmentVariable('CLAUDECODE', $null)

    $proc = Start-Process -FilePath $nodePath `
        -ArgumentList $DaemonMjs `
        -WorkingDirectory $SkillDir `
        -WindowStyle Hidden `
        -RedirectStandardOutput $LogFile `
        -RedirectStandardError $LogFile `
        -PassThru

    # Write initial PID (main.ts will overwrite with real PID)
    Set-Content -Path $PidFile -Value $proc.Id
    return $proc.Id
}

# ── Commands ──

switch ($Command) {
    'start' {
        Ensure-Dirs
        Ensure-Built

        $existingPid = Read-Pid
        if ($existingPid -and (Test-PidAlive $existingPid)) {
            Write-Host "Bridge already running (PID: $existingPid)"
            if (Test-Path $StatusFile) { Get-Content $StatusFile -Raw }
            exit 1
        }

        # Check if registered as Windows Service
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($svc) {
            Write-Host "Starting bridge via Windows Service..."
            Start-Service -Name $ServiceName
            Start-Sleep -Seconds 3

            $newPid = Read-Pid
            if ($newPid -and (Test-PidAlive $newPid) -and (Test-StatusRunning)) {
                Write-Host "Bridge started (PID: $newPid, managed by Windows Service)"
                if (Test-Path $StatusFile) { Get-Content $StatusFile -Raw }
            } else {
                Write-Host "Failed to start bridge via service."
                Show-LastExitReason
                Show-FailureHelp
                exit 1
            }
        } else {
            Write-Host "Starting bridge (background process)..."
            $pid = Start-Fallback
            Start-Sleep -Seconds 3

            $newPid = Read-Pid
            if ($newPid -and (Test-PidAlive $newPid) -and (Test-StatusRunning)) {
                Write-Host "Bridge started (PID: $newPid)"
                if (Test-Path $StatusFile) { Get-Content $StatusFile -Raw }
            } else {
                Write-Host "Failed to start bridge."
                if (-not $newPid -or -not (Test-PidAlive $newPid)) {
                    Write-Host "  Process exited immediately."
                }
                Show-LastExitReason
                Show-FailureHelp
                exit 1
            }
        }
    }

    'stop' {
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($svc -and $svc.Status -eq 'Running') {
            Write-Host "Stopping bridge via Windows Service..."
            Stop-Service -Name $ServiceName -Force
            Write-Host "Bridge stopped"
            if (Test-Path $PidFile) { Remove-Item $PidFile -Force }
        } else {
            $pid = Read-Pid
            if (-not $pid) { Write-Host "No bridge running"; exit 0 }
            if (Test-PidAlive $pid) {
                Stop-Process -Id ([int]$pid) -Force
                Write-Host "Bridge stopped"
            } else {
                Write-Host "Bridge was not running (stale PID file)"
            }
            if (Test-Path $PidFile) { Remove-Item $PidFile -Force }
        }
    }

    'status' {
        $pid = Read-Pid

        # Check Windows Service
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if ($svc) {
            Write-Host "Windows Service '$ServiceName': $($svc.Status)"
        }

        if ($pid -and (Test-PidAlive $pid)) {
            Write-Host "Bridge process is running (PID: $pid)"
            if (Test-StatusRunning) {
                Write-Host "Bridge status: running"
            } else {
                Write-Host "Bridge status: process alive but status.json not reporting running"
            }
            if (Test-Path $StatusFile) { Get-Content $StatusFile -Raw }
        } else {
            Write-Host "Bridge is not running"
            if (Test-Path $PidFile) { Remove-Item $PidFile -Force }
            Show-LastExitReason
        }
    }

    'logs' {
        if (Test-Path $LogFile) {
            Get-Content $LogFile -Tail $LogLines | ForEach-Object {
                $_ -replace '(token|secret|password)(["'']?\s*[:=]\s*["'']?)[^\s"]+', '$1$2*****'
            }
        } else {
            Write-Host "No log file found at $LogFile"
        }
    }

    'install-service' {
        Ensure-Dirs
        Ensure-Built

        $mgr = Find-ServiceManager
        if (-not $mgr) {
            Write-Host "No service manager found. Install one of:"
            Write-Host "  WinSW:  https://github.com/winsw/winsw/releases"
            Write-Host "  NSSM:   https://nssm.cc/download"
            Write-Host ""
            Write-Host "After installing, add it to PATH and re-run:"
            Write-Host "  powershell -File `"$PSCommandPath`" install-service"
            exit 1
        }

        switch ($mgr.type) {
            'winsw' { Install-WinSWService -WinSWPath $mgr.path }
            'nssm'  { Install-NSSMService  -NSSMPath  $mgr.path }
        }
    }

    'uninstall-service' {
        $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
        if (-not $svc) {
            Write-Host "Service '$ServiceName' is not installed."
            exit 0
        }

        if ($svc.Status -eq 'Running') {
            Stop-Service -Name $ServiceName -Force
        }

        $mgr = Find-ServiceManager
        if ($mgr -and $mgr.type -eq 'nssm') {
            & $mgr.path remove $ServiceName confirm
        } else {
            # WinSW or generic
            $winswExe = Join-Path $SkillDir "$ServiceName.exe"
            if (Test-Path $winswExe) {
                & $winswExe uninstall
                Remove-Item $winswExe -Force -ErrorAction SilentlyContinue
                Remove-Item (Join-Path $SkillDir "$ServiceName.xml") -Force -ErrorAction SilentlyContinue
            } else {
                sc.exe delete $ServiceName
            }
        }
        Write-Host "Service '$ServiceName' uninstalled."
    }

    'help' {
        Write-Host "Usage: daemon.ps1 {start|stop|status|logs [N]|install-service|uninstall-service}"
        Write-Host ""
        Write-Host "Commands:"
        Write-Host "  start             Start the bridge daemon"
        Write-Host "  stop              Stop the bridge daemon"
        Write-Host "  status            Show bridge status"
        Write-Host "  logs [N]          Show last N log lines (default 50)"
        Write-Host "  install-service   Install as Windows Service (requires WinSW or NSSM)"
        Write-Host "  uninstall-service Remove the Windows Service"
    }
}