source dump of claude code
at main 238 lines 7.7 kB view raw
1import { APIUserAbortError } from '@anthropic-ai/sdk' 2 3export class ClaudeError extends Error { 4 constructor(message: string) { 5 super(message) 6 this.name = this.constructor.name 7 } 8} 9 10export class MalformedCommandError extends Error {} 11 12export class AbortError extends Error { 13 constructor(message?: string) { 14 super(message) 15 this.name = 'AbortError' 16 } 17} 18 19/** 20 * True iff `e` is any of the abort-shaped errors the codebase encounters: 21 * our AbortError class, a DOMException from AbortController.abort() 22 * (.name === 'AbortError'), or the SDK's APIUserAbortError. The SDK class 23 * is checked via instanceof because minified builds mangle class names — 24 * constructor.name becomes something like 'nJT' and the SDK never sets 25 * this.name, so string matching silently fails in production. 26 */ 27export function isAbortError(e: unknown): boolean { 28 return ( 29 e instanceof AbortError || 30 e instanceof APIUserAbortError || 31 (e instanceof Error && e.name === 'AbortError') 32 ) 33} 34 35/** 36 * Custom error class for configuration file parsing errors 37 * Includes the file path and the default configuration that should be used 38 */ 39export class ConfigParseError extends Error { 40 filePath: string 41 defaultConfig: unknown 42 43 constructor(message: string, filePath: string, defaultConfig: unknown) { 44 super(message) 45 this.name = 'ConfigParseError' 46 this.filePath = filePath 47 this.defaultConfig = defaultConfig 48 } 49} 50 51export class ShellError extends Error { 52 constructor( 53 public readonly stdout: string, 54 public readonly stderr: string, 55 public readonly code: number, 56 public readonly interrupted: boolean, 57 ) { 58 super('Shell command failed') 59 this.name = 'ShellError' 60 } 61} 62 63export class TeleportOperationError extends Error { 64 constructor( 65 message: string, 66 public readonly formattedMessage: string, 67 ) { 68 super(message) 69 this.name = 'TeleportOperationError' 70 } 71} 72 73/** 74 * Error with a message that is safe to log to telemetry. 75 * Use the long name to confirm you've verified the message contains no 76 * sensitive data (file paths, URLs, code snippets). 77 * 78 * Single-arg: same message for user and telemetry 79 * Two-arg: different messages (e.g., full message has file path, telemetry doesn't) 80 * 81 * @example 82 * // Same message for both 83 * throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS( 84 * 'MCP server "slack" connection timed out' 85 * ) 86 * 87 * // Different messages 88 * throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS( 89 * `MCP tool timed out after ${ms}ms`, // Full message for logs/user 90 * 'MCP tool timed out' // Telemetry message 91 * ) 92 */ 93export class TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS extends Error { 94 readonly telemetryMessage: string 95 96 constructor(message: string, telemetryMessage?: string) { 97 super(message) 98 this.name = 'TelemetrySafeError' 99 this.telemetryMessage = telemetryMessage ?? message 100 } 101} 102 103export function hasExactErrorMessage(error: unknown, message: string): boolean { 104 return error instanceof Error && error.message === message 105} 106 107/** 108 * Normalize an unknown value into an Error. 109 * Use at catch-site boundaries when you need an Error instance. 110 */ 111export function toError(e: unknown): Error { 112 return e instanceof Error ? e : new Error(String(e)) 113} 114 115/** 116 * Extract a string message from an unknown error-like value. 117 * Use when you only need the message (e.g., for logging or display). 118 */ 119export function errorMessage(e: unknown): string { 120 return e instanceof Error ? e.message : String(e) 121} 122 123/** 124 * Extract the errno code (e.g., 'ENOENT', 'EACCES') from a caught error. 125 * Returns undefined if the error has no code or is not an ErrnoException. 126 * Replaces the `(e as NodeJS.ErrnoException).code` cast pattern. 127 */ 128export function getErrnoCode(e: unknown): string | undefined { 129 if (e && typeof e === 'object' && 'code' in e && typeof e.code === 'string') { 130 return e.code 131 } 132 return undefined 133} 134 135/** 136 * True if the error is ENOENT (file or directory does not exist). 137 * Replaces `(e as NodeJS.ErrnoException).code === 'ENOENT'`. 138 */ 139export function isENOENT(e: unknown): boolean { 140 return getErrnoCode(e) === 'ENOENT' 141} 142 143/** 144 * Extract the errno path (the filesystem path that triggered the error) 145 * from a caught error. Returns undefined if the error has no path. 146 * Replaces the `(e as NodeJS.ErrnoException).path` cast pattern. 147 */ 148export function getErrnoPath(e: unknown): string | undefined { 149 if (e && typeof e === 'object' && 'path' in e && typeof e.path === 'string') { 150 return e.path 151 } 152 return undefined 153} 154 155/** 156 * Extract error message + top N stack frames from an unknown error. 157 * Use when the error flows to the model as a tool_result — full stack 158 * traces are ~500-2000 chars of mostly-irrelevant internal frames and 159 * waste context tokens. Keep the full stack in debug logs instead. 160 */ 161export function shortErrorStack(e: unknown, maxFrames = 5): string { 162 if (!(e instanceof Error)) return String(e) 163 if (!e.stack) return e.message 164 // V8/Bun stack format: "Name: message\n at frame1\n at frame2..." 165 // First line is the message; subsequent " at " lines are frames. 166 const lines = e.stack.split('\n') 167 const header = lines[0] ?? e.message 168 const frames = lines.slice(1).filter(l => l.trim().startsWith('at ')) 169 if (frames.length <= maxFrames) return e.stack 170 return [header, ...frames.slice(0, maxFrames)].join('\n') 171} 172 173/** 174 * True if the error means the path is missing, inaccessible, or 175 * structurally unreachable — use in catch blocks after fs operations to 176 * distinguish expected "nothing there / no access" from unexpected errors. 177 * 178 * Covers: 179 * ENOENT — path does not exist 180 * EACCES — permission denied 181 * EPERM — operation not permitted 182 * ENOTDIR — a path component is not a directory (e.g. a file named 183 * `.claude` exists where a directory is expected) 184 * ELOOP — too many symlink levels (circular symlinks) 185 */ 186export function isFsInaccessible(e: unknown): e is NodeJS.ErrnoException { 187 const code = getErrnoCode(e) 188 return ( 189 code === 'ENOENT' || 190 code === 'EACCES' || 191 code === 'EPERM' || 192 code === 'ENOTDIR' || 193 code === 'ELOOP' 194 ) 195} 196 197export type AxiosErrorKind = 198 | 'auth' // 401/403 — caller typically sets skipRetry 199 | 'timeout' // ECONNABORTED 200 | 'network' // ECONNREFUSED/ENOTFOUND 201 | 'http' // other axios error (may have status) 202 | 'other' // not an axios error 203 204/** 205 * Classify a caught error from an axios request into one of a few buckets. 206 * Replaces the ~20-line isAxiosError → 401/403 → ECONNABORTED → ECONNREFUSED 207 * chain duplicated across sync-style services (settingsSync, policyLimits, 208 * remoteManagedSettings, teamMemorySync). 209 * 210 * Checks the `.isAxiosError` marker property directly (same as 211 * axios.isAxiosError()) to keep this module dependency-free. 212 */ 213export function classifyAxiosError(e: unknown): { 214 kind: AxiosErrorKind 215 status?: number 216 message: string 217} { 218 const message = errorMessage(e) 219 if ( 220 !e || 221 typeof e !== 'object' || 222 !('isAxiosError' in e) || 223 !e.isAxiosError 224 ) { 225 return { kind: 'other', message } 226 } 227 const err = e as { 228 response?: { status?: number } 229 code?: string 230 } 231 const status = err.response?.status 232 if (status === 401 || status === 403) return { kind: 'auth', status, message } 233 if (err.code === 'ECONNABORTED') return { kind: 'timeout', status, message } 234 if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') { 235 return { kind: 'network', status, message } 236 } 237 return { kind: 'http', status, message } 238}