Skip to content
← Public packages

@kentcdodds/friction-log

Qualify and file friction papercuts to GitHub (any owner/repo) or wake Patch for Kody packages; daily sweep of kody-hq/kody friction issues.

FollowFork

src/shared.ts

216 lines · 6.0 KB · TypeScript
Raw
export const SELF_KODY_ID = 'friction-log'
export const owner = 'kody-hq'
export const repo = 'kody'
export const repoSlug = `${owner}/${repo}`
export const repository = `https://github.com/${repoSlug}`
export const ref = 'main'
export const timezone = 'America/Denver'
export const frictionLabel = 'friction'
export const skippedLabel = 'friction-skipped'
export const skippedLabelDescription =
	'Daily sweep will not re-investigate until this label is removed.'
export const skippedLabelColor = '6B5D52'
export const maxIssuesInPrompt = 10

export const agentModel = {
	id: 'grok-4.6',
	params: [
		{ id: 'effort', value: 'high' },
		{ id: 'fast', value: 'true' },
	],
} as const

export type IssueComment = {
	id: number
	userLogin: string
	createdAt: string
	body: string
}

export type FrictionIssue = {
	number: number
	title: string
	body: string
	htmlUrl: string
	userLogin: string
	createdAt: string
	updatedAt: string
	labels: Array<string>
	comments: Array<IssueComment>
}

export type EligibilityReason = 'open' | 'skipped'

export type Eligibility = {
	eligible: boolean
	reason: EligibilityReason
}

export type SweepRecord = {
	day: string
	scanned_at: string
	eligible_count: number
	issue_numbers_json: string
	spawned: number
	agent_id: string | null
	agent_url: string | null
	skipped: string | null
	dry_run: number
}

export type OutcomeRecord = {
	agent_id: string
	day: string
	outcome: string
	summary: string
	pr_url: string | null
	completed_at: string
}

export function truncate(text: unknown, maxLength: number) {
	const value = String(text ?? '')
	return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}…`
}

/**
 * Neutralize untrusted text before it goes into an outbound Cursor prompt.
 *
 * Kody expands `{{secret:…}}` placeholders in outbound request bodies at fetch
 * time, so a literal placeholder quoted in an issue would otherwise trigger a
 * host-approval error for api.cursor.com (or, if approved, leak that secret
 * into the agent prompt). Breaking the double brace keeps the text readable
 * without being a placeholder.
 */
export function sanitizeForPrompt(text: unknown) {
	return String(text ?? '')
		.replaceAll('`', 'ˋ')
		.replaceAll('«', '<')
		.replaceAll('»', '>')
		.replace(/\{\{(\s*secret\s*:)/gi, '{ {$1')
}

export function agentUrlOf(agent: { id?: string; url?: string } | null) {
	if (!agent) return null
	return agent.url || (agent.id ? `https://cursor.com/agents/${agent.id}` : null)
}

export function denverDayKey(now = new Date()) {
	return new Intl.DateTimeFormat('en-CA', {
		timeZone: timezone,
		year: 'numeric',
		month: '2-digit',
		day: '2-digit',
	}).format(now)
}

export function normalizeFrictionTitle(title: string) {
	const cleaned = String(title ?? '').trim().replace(/\s+/g, " ")
	if (!cleaned) return ''
	if (/^friction:\s*/i.test(cleaned)) {
		return `Friction: ${cleaned.replace(/^friction:\s*/i, "")}`
	}
	return `Friction: ${cleaned}`
}

export const preliminaryInvestigationMaxChars = 1200
export const relevantFilesMaxCount = 20

export function normalizeRelevantFiles(
	files: Array<string> | undefined,
): Array<string> {
	if (!Array.isArray(files)) return []
	const seen = new Set<string>()
	const out: Array<string> = []
	for (const entry of files) {
		const path = String(entry ?? '').trim()
		if (!path || seen.has(path)) continue
		seen.add(path)
		out.push(path)
		if (out.length >= relevantFilesMaxCount) break
	}
	return out
}

/**
 * Build the friction issue / reuse-comment body from structured fields.
 * When `body` is set, it wins as-is. Otherwise sections are joined; include a
 * short preliminary investigation (what was already looked at) and optional
 * relevant files. Do not invent a root cause. Omit the files list when empty.
 */
export function buildFrictionBody(input: {
	body?: string
	whatHappened?: string
	whatYouWanted?: string
	howToReproduce?: string
	cost?: string
	preliminaryInvestigation?: string
	relevantFiles?: Array<string>
}) {
	const provided = String(input.body ?? '').trim()
	if (provided) return provided
	const parts: string[] = []
	const sections = [
		['What happened', input.whatHappened],
		['What you wanted', input.whatYouWanted],
		['How to reproduce', input.howToReproduce],
		['Cost', input.cost],
	] as const
	for (const [heading, value] of sections) {
		const text = String(value ?? '').trim()
		if (!text) continue
		parts.push(`## ${heading}\n\n${text}`)
	}
	const investigation = truncate(
		String(input.preliminaryInvestigation ?? '').trim(),
		preliminaryInvestigationMaxChars,
	).trim()
	const files = normalizeRelevantFiles(input.relevantFiles)
	if (investigation || files.length > 0) {
		const block = ['## Preliminary investigation', '']
		if (investigation) block.push(investigation)
		if (files.length > 0) {
			if (investigation) block.push('')
			block.push('Relevant files:')
			for (const file of files) {
				block.push(`- \`${file}\``)
			}
		}
		parts.push(block.join('\n'))
	}
	return parts.join('\n\n')
}

function normalizeBodyForCompare(text: unknown) {
	return String(text ?? '')
		.replace(/\r\n/g, '\n')
		.replace(/\s+/g, ' ')
		.trim()
}

/**
 * True when a reuse comment would only repeat text the issue already has
 * (same body as the issue itself). Re-filing the identical papercut adds no
 * new information, so `./file` skips the "Seen again" comment.
 */
export function isSameFrictionBody(newBody: unknown, existingBody: unknown) {
	const next = normalizeBodyForCompare(newBody)
	if (!next) return false
	return next === normalizeBodyForCompare(existingBody)
}

export type ReuseCommentSkipped = 'same-as-issue-body' | 'disabled' | 'empty-body'

/**
 * Decide whether a reuse should skip its "Seen again" comment. Returns the
 * reason to skip, or undefined to post the comment.
 */
export function reuseCommentSkipReason(input: {
	commentOnReuse: boolean
	body: string
	sameBodyAsExisting: boolean
}): ReuseCommentSkipped | undefined {
	if (!input.commentOnReuse) return 'disabled'
	if (!String(input.body ?? '').trim()) return 'empty-body'
	if (input.sameBodyAsExisting) return 'same-as-issue-body'
	return undefined
}