# Gitea issue wrapper for the Grok /loop session (Windows). The allowlist permits # only this script, not arbitrary PowerShell, so the session cannot read the # token or run other commands through it. ASCII only (PowerShell 5.1 reads # BOM-less scripts as ANSI); Chinese label names arrive as arguments. # gitea.ps1 whoami # gitea.ps1 issues open issues carrying ALL labels, oldest first (JSON) # gitea.ps1 issue number, title, body (body only if written by a trusted account) # gitea.ps1 labels label names, one per line # gitea.ps1 add-label # gitea.ps1 remove-label # gitea.ps1 comments trusted accounts' comments only (JSON: author, created_at, body) # gitea.ps1 comment post a UTF-8 file as a comment # gitea.ps1 create-pr <bodyfile> open a PR from <branch> into the repo's default branch; prints its number # gitea.ps1 pr-for-branch <branch> open PR number for <branch> (empty if none) # -Repo selects the repository (default root/Jyotisha), e.g. -Repo root/copse. param([Parameter(Mandatory = $true)][string]$Cmd, [string]$A1, [string]$A2, [string]$A3, [string]$Repo = 'root/Jyotisha') $ErrorActionPreference = 'Stop' [Console]::OutputEncoding = [Text.Encoding]::UTF8 $Utf8 = New-Object Text.UTF8Encoding $false $Base = 'https://git.copse.top/api/v1' if ($Repo -notmatch '^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$') { throw "bad -Repo: $Repo" } $Api = "$Base/repos/$Repo" # Accounts whose issue text and comments are trusted: the product owner and the two agents. $Trusted = @('jesse', 'Claude', 'Grok') if ($env:GITEA_TRUSTED_USERS) { $Trusted = $env:GITEA_TRUSTED_USERS -split ',' } $Token = ([IO.File]::ReadAllText((Join-Path $HOME '.config\jyotisha-agent\gitea-token'))).Trim() function Call([string]$Method, [string]$Url, $Body = $null) { $p = @{ Uri = $Url; Method = $Method; UseBasicParsing = $true; Headers = @{ Authorization = "token $Token" } } if ($null -ne $Body) { $p.Body = $Utf8.GetBytes(($Body | ConvertTo-Json -Depth 10 -Compress)) $p.ContentType = 'application/json; charset=utf-8' } $text = $Utf8.GetString((Invoke-WebRequest @p).RawContentStream.ToArray()) if ($text) { $text | ConvertFrom-Json } else { $null } } function Owner { (Call GET "$Base/user").login } function LabelId([string]$name) { (@(Call GET "$Api/labels?limit=100") | Where-Object { $_.name -eq $name } | Select-Object -First 1).id } function Json($o) { $o | ConvertTo-Json -Depth 10 } switch ($Cmd) { 'whoami' { Owner } 'issues' { Json @(@(Call GET "$Api/issues?state=open&type=issues&limit=50&labels=$([uri]::EscapeDataString($A1))") | Sort-Object created_at | ForEach-Object { [ordered]@{ number = $_.number; title = $_.title; labels = @($_.labels | ForEach-Object { $_.name }) } }) } 'issue' { $i = Call GET "$Api/issues/$A1" Json ([ordered]@{ number = $i.number; title = $i.title; body = $(if ($Trusted -contains $i.user.login) { $i.body } else { '' }) }) } 'labels' { @(Call GET "$Api/issues/$A1/labels") | ForEach-Object { $_.name } } 'add-label' { Call POST "$Api/issues/$A1/labels" @{ labels = @($A2) } | Out-Null; "added $A2" } 'remove-label' { $id = LabelId $A2; if ($id -and (@(Call GET "$Api/issues/$A1/labels") | Where-Object { $_.id -eq $id })) { Call DELETE "$Api/issues/$A1/labels/$id" | Out-Null }; "removed $A2" } 'comments' { Json @(@(Call GET "$Api/issues/$A1/comments") | Where-Object { $Trusted -contains $_.user.login } | ForEach-Object { [ordered]@{ author = $_.user.login; created_at = $_.created_at; body = $_.body } }) } 'comment' { Call POST "$Api/issues/$A1/comments" @{ body = [IO.File]::ReadAllText($A2, [Text.Encoding]::UTF8) } | Out-Null; "commented on #$A1" } 'create-pr' { if ($A1 -notmatch '^codex/') { throw 'PR head must be a codex/* branch' } $base = (Call GET $Api).default_branch (Call POST "$Api/pulls" @{ head = $A1; base = $base; title = $A2; body = [IO.File]::ReadAllText($A3, [Text.Encoding]::UTF8) }).number } 'pr-for-branch' { (@(Call GET "$Api/pulls?state=open&limit=50") | Where-Object { $_.head.ref -eq $A1 } | Select-Object -First 1).number } default { throw "unknown command: $Cmd" } }