import type { LinearErrorCode, LinearIncludeErrorCode } from '../../shared/linear/agent-access' export class LinearAgentAccessError extends Error { readonly code: LinearErrorCode readonly data?: unknown constructor(code: LinearErrorCode, message: string, data?: unknown) { super(message) this.name = 'LinearAgentAccessError' this.code = code this.data = data } } export function linearError( code: LinearErrorCode, message: string, data?: unknown ): LinearAgentAccessError { return new LinearAgentAccessError(code, message, data) } export function includeErrorCode(error: unknown): LinearIncludeErrorCode { if (error instanceof LinearAgentAccessError) { if ( error.code === 'linear_timeout' || error.code === 'linear_rate_limited' || error.code === 'linear_permission_denied' || error.code === 'linear_auth_expired' || error.code === 'linear_network_error' ) { return error.code } } return 'linear_include_failed' } export function classifyLinearError(error: unknown): LinearErrorCode { const message = linearMessage(error).toLowerCase() if (message.includes('rate limit') || message.includes('429')) { return 'linear_rate_limited' } if (message.includes('timeout') || message.includes('timed out')) { return 'linear_timeout' } if (message.includes('permission') || message.includes('forbidden') || message.includes('403')) { return 'linear_permission_denied' } if ( message.includes('network') || message.includes('econnreset') || message.includes('enotfound') || message.includes('fetch failed') ) { return 'linear_network_error' } return 'linear_network_error' } export function linearMessage(error: unknown): string { const message = error instanceof Error ? error.message : String(error) return sanitizeLinearErrorMessage(message) } export function sanitizeLinearErrorMessage(message: string): string { // Why: provider text is useful in CLI errors, but raw SDK failures can embed secrets or user payloads. return stripLinearStackTrace(message) .replace( /(headers?\s*[:=]\s*)\{[^{}]*(?:authorization|token|api[-_]?key)[^{}]*\}/gi, '$1[REDACTED]' ) .replace(/(authorization\s*[:=]\s*bearer\s+)[^\s]+/gi, '$1[REDACTED]') .replace(/((?:api[-_]?key|token)\s*[:=]\s*)[^\s,}\]]+/gi, '$1[REDACTED]') .replace(/(variables\s*[:=]\s*)\{[\s\S]*?\}/gi, '$1[REDACTED]') .replace(/((?:body|comment|description)\s*[:=]\s*)\{[\s\S]*?\}/gi, '$1[REDACTED]') .replace(/((?:body|comment|description)\s*[:=]\s*)(["']).*?\2/gi, '$1[REDACTED]') .trim() } function stripLinearStackTrace(message: string): string { for (let index = 0; index < message.length; index += 1) { const code = message.charCodeAt(index) if (code !== 10 && code !== 13) { continue } let candidateStart = index + 1 if (code === 13 && message.charCodeAt(candidateStart) === 10) { candidateStart += 1 } while ( candidateStart < message.length && isLinearStackWhitespace(message.charCodeAt(candidateStart)) ) { candidateStart += 1 } if ( message.startsWith('at', candidateStart) && isLinearStackWhitespace(message.charCodeAt(candidateStart + 2)) ) { return message.slice(0, index) } } return message } function isLinearStackWhitespace(code: number): boolean { return code === 32 || (code >= 9 && code <= 13) }