Compare commits

...

8 Commits

Author SHA1 Message Date
dbd94e7b37 Merge pull request 'Make Claude Code footer stripping more robust' (#25) from probablycorey/fix-claude-footer-strip into main
Some checks failed
CI / test (push) Has been cancelled
Reviewed-on: #25
2026-03-11 21:06:51 +00:00
c22c5c423c Make Claude Code footer stripping more robust
Some checks failed
CI / test (pull_request) Has been cancelled
Updated the regex to match the robot emoji followed by "Claude Code" on the last line, regardless of formatting. Also pulled the regex into a named variable with a comment to clarify intent. This catches variations like "Generated with Claude Code" without square brackets.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-11 13:18:24 -07:00
bb54e1048a Merge pull request 'Add Gitea API error logging' (#24) from probablycorey/spike-error-debug into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #24
2026-03-10 19:18:17 +00:00
829e1ea1dc Add detailed logging for Gitea API errors to diagnose 401 issues.
Some checks failed
CI / test (pull_request) Has been cancelled
Extracts token presence/length, response status, failed URL, and response body (first 500 chars) into structured logs. This will help identify whether token is missing, invalid, or if Gitea is rejecting for another reason.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 12:10:36 -07:00
f31b07caa7 Merge pull request 'Fix log file sorting to handle both timestamp and sha formats' (#23) from probablycorey/fix-log-display into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #23
2026-03-10 18:54:02 +00:00
30a6b0c987 Fix log file sorting to handle both timestamp and sha formats
Some checks failed
CI / test (pull_request) Has been cancelled
Replace naive sort with explicit timestamp extraction that works for both the new timestamp_sha format and legacy sha_timestamp format. Extract log filename parsing into a reusable function for consistency.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 11:53:38 -07:00
d850e00c4e Merge pull request 'Remove Claude Code footer from Discord PR messages' (#22) from probablycorey/remove-claude-footer into main
Some checks are pending
CI / test (push) Waiting to run
Reviewed-on: #22
2026-03-10 18:44:51 +00:00
28cc3d4c94 Remove Claude Code footer from Discord PR messages
Some checks failed
CI / test (pull_request) Has been cancelled
Strip the "🤖 Generated with Claude Code" line from PR bodies before posting them to Discord. This line is added by Claude Code when creating PRs but is unnecessary in the Discord bridge.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-03-10 11:44:05 -07:00
4 changed files with 56 additions and 27 deletions

View File

@ -112,7 +112,11 @@ class PRHandler {
}
static formatPrBody(pullRequest: Gitea.PullRequest, repositoryFullName: string): string {
const body = pullRequest.body || "_empty_"
// Strip the "🤖 Generated with Claude Code" footer that gets appended to PR bodies
const claudeFooter = /\n*🤖.*Claude Code[^\n]*$/
const body = (pullRequest.body || "_empty_")
.replace(claudeFooter, "")
.trim() || "_empty_"
let message = `
> ### [${pullRequest.title}](<${pullRequest.html_url}>)
> **${repositoryFullName}**

View File

@ -1,18 +1,33 @@
import { log } from "../log"
import type { Gitea } from "./types"
const giteaUrl = "https://git.nose.space"
export async function fetchPR(fullname: string, prNumber: number): Promise<Gitea.PullRequest> {
const url = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}`
function tokenPrefix(): string {
const token = process.env.GITEA_API_TOKEN
if (!token) return "MISSING"
return `${token.slice(0, 4)}...${token.slice(-4)} (${token.length} chars)`
}
async function giteaFetch(url: string): Promise<Response> {
const response = await fetch(url, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!response.ok) {
const body = await response.text()
log({ type: "gitea-api-error", status: response.status, url, body: body.slice(0, 500), tokenPrefix: tokenPrefix() })
throw new Error(`Gitea API error: ${response.status} ${response.statusText}`)
}
return response
}
export async function fetchPR(fullname: string, prNumber: number): Promise<Gitea.PullRequest> {
const url = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}`
const response = await giteaFetch(url)
return response.json() as Promise<Gitea.PullRequest>
}
@ -20,16 +35,8 @@ export async function fetchReviewComments(
fullname: string,
prNumber: number
): Promise<Gitea.ReviewComment[]> {
// First, fetch all reviews
const reviewsUrl = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}/reviews`
const reviewsResponse = await fetch(reviewsUrl, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!reviewsResponse.ok) {
throw new Error(`Gitea API error: ${reviewsResponse.status} ${reviewsResponse.statusText}`)
}
const reviewsResponse = await giteaFetch(reviewsUrl)
const reviews = (await reviewsResponse.json()) as Array<{
id: number
@ -40,21 +47,12 @@ export async function fetchReviewComments(
comments_count: number
}>
// For each review, fetch its comments
const allComments: Gitea.ReviewComment[] = []
for (const review of reviews) {
if (review.comments_count === 0) continue
const commentsUrl = `${giteaUrl}/api/v1/repos/${fullname}/pulls/${prNumber}/reviews/${review.id}/comments`
const commentsResponse = await fetch(commentsUrl, {
headers: {
Authorization: `token ${process.env.GITEA_API_TOKEN}`,
},
})
if (!commentsResponse.ok) {
throw new Error(`Gitea API error fetching review ${review.id} comments: ${commentsResponse.status} ${commentsResponse.statusText}`)
}
const commentsResponse = await giteaFetch(commentsUrl)
const comments = (await commentsResponse.json()) as Gitea.ReviewComment[]
allComments.push(...comments)
}

View File

@ -16,6 +16,7 @@ export type LogEvent =
| { type: "crash-log-found" }
| { type: "startup"; detail: string }
| { type: "discord-ready" }
| { type: "gitea-api-error"; status: number; url: string; body: string; tokenPrefix: string }
| { type: "error"; error: unknown; context?: string }
export type StoredLogEvent = LogEvent & { ts: string }
@ -50,11 +51,17 @@ function createLogFile(): string {
return `${logsDir}/${timestamp}_${releaseSha}.jsonl`
}
function extractTimestamp(filename: string): string {
const stem = filename.replace(".jsonl", "")
// New format: timestamp_sha, Old format: sha_timestamp
if (/^\d{4}-/.test(stem)) return stem.split("_").slice(0, -1).join("_")
return stem.split("_").pop() || ""
}
export function listLogFiles(): string[] {
return readdirSync(logsDir)
.filter((f) => f.endsWith(".jsonl"))
.sort()
.reverse()
.sort((a, b) => extractTimestamp(b).localeCompare(extractTimestamp(a)))
.slice(0, 20)
}

View File

@ -108,11 +108,32 @@ function EventRow({ event, index }: { event: StoredLogEvent; index: number }) {
)
}
function parseLogFilename(file: string): { sha: string; isoStr: string } {
const stem = file.replace(".jsonl", "")
// New format: timestamp_sha (e.g. 2026-03-10T18-06-23-561Z_cb10121)
// Old format: sha_timestamp (e.g. cb10121_2026-03-10T18-06-23-561Z)
const isNewFormat = /^\d{4}-/.test(stem)
const parts = stem.split("_")
let sha: string
let rawTimestamp: string
if (isNewFormat) {
sha = parts.pop() || "unknown"
rawTimestamp = parts.join("_")
} else {
rawTimestamp = parts.pop() || ""
sha = parts.join("_") || "unknown"
}
const isoStr = rawTimestamp.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "T$1:$2:$3.$4Z")
return { sha, isoStr }
}
function Sidebar({ files, selectedFile }: { files: string[]; selectedFile?: string }) {
// Group files by sha, sorted by most recent file in each group
const grouped: Record<string, string[]> = {}
for (const file of files) {
const sha = file.replace(".jsonl", "").split("_").pop() || "unknown"
const { sha } = parseLogFilename(file)
const list = grouped[sha] || (grouped[sha] = [])
list.push(file)
}
@ -130,8 +151,7 @@ function Sidebar({ files, selectedFile }: { files: string[]; selectedFile?: stri
<div style="padding: 4px 16px; color: #6b7280; font-size: 11px; font-family: monospace">{sha}</div>
{shaFiles.map((file) => {
const isSelected = file === selectedFile
// Parse ISO timestamp back from filename: 2026-03-10T02-56-59-938Z_sha.jsonl
const isoStr = file.replace(/_[^_]+\.jsonl$/, "").replace(/T(\d{2})-(\d{2})-(\d{2})-(\d+)Z/, "T$1:$2:$3.$4Z")
const { isoStr } = parseLogFilename(file)
const bg = isSelected ? "#2a2a2a" : "transparent"
return (
<a