239 lines
7.5 KiB
TypeScript
239 lines
7.5 KiB
TypeScript
import { APIUserAbortError } from '@anthropic-ai/sdk'
|
|
|
|
export class ClaudeError extends Error {
|
|
constructor(message: string) {
|
|
super(message)
|
|
this.name = this.constructor.name
|
|
}
|
|
}
|
|
|
|
export class MalformedCommandError extends Error {}
|
|
|
|
export class AbortError extends Error {
|
|
constructor(message?: string) {
|
|
super(message)
|
|
this.name = 'AbortError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* True iff `e` is any of the abort-shaped errors the codebase encounters:
|
|
* our AbortError class, a DOMException from AbortController.abort()
|
|
* (.name === 'AbortError'), or the SDK's APIUserAbortError. The SDK class
|
|
* is checked via instanceof because minified builds mangle class names —
|
|
* constructor.name becomes something like 'nJT' and the SDK never sets
|
|
* this.name, so string matching silently fails in production.
|
|
*/
|
|
export function isAbortError(e: unknown): boolean {
|
|
return (
|
|
e instanceof AbortError ||
|
|
e instanceof APIUserAbortError ||
|
|
(e instanceof Error && e.name === 'AbortError')
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Custom error class for configuration file parsing errors
|
|
* Includes the file path and the default configuration that should be used
|
|
*/
|
|
export class ConfigParseError extends Error {
|
|
filePath: string
|
|
defaultConfig: unknown
|
|
|
|
constructor(message: string, filePath: string, defaultConfig: unknown) {
|
|
super(message)
|
|
this.name = 'ConfigParseError'
|
|
this.filePath = filePath
|
|
this.defaultConfig = defaultConfig
|
|
}
|
|
}
|
|
|
|
export class ShellError extends Error {
|
|
constructor(
|
|
public readonly stdout: string,
|
|
public readonly stderr: string,
|
|
public readonly code: number,
|
|
public readonly interrupted: boolean,
|
|
) {
|
|
super('Shell command failed')
|
|
this.name = 'ShellError'
|
|
}
|
|
}
|
|
|
|
export class TeleportOperationError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public readonly formattedMessage: string,
|
|
) {
|
|
super(message)
|
|
this.name = 'TeleportOperationError'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Error with a message that is safe to log to telemetry.
|
|
* Use the long name to confirm you've verified the message contains no
|
|
* sensitive data (file paths, URLs, code snippets).
|
|
*
|
|
* Single-arg: same message for user and telemetry
|
|
* Two-arg: different messages (e.g., full message has file path, telemetry doesn't)
|
|
*
|
|
* @example
|
|
* // Same message for both
|
|
* throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
|
|
* 'MCP server "slack" connection timed out'
|
|
* )
|
|
*
|
|
* // Different messages
|
|
* throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
|
|
* `MCP tool timed out after ${ms}ms`, // Full message for logs/user
|
|
* 'MCP tool timed out' // Telemetry message
|
|
* )
|
|
*/
|
|
export class TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS extends Error {
|
|
readonly telemetryMessage: string
|
|
|
|
constructor(message: string, telemetryMessage?: string) {
|
|
super(message)
|
|
this.name = 'TelemetrySafeError'
|
|
this.telemetryMessage = telemetryMessage ?? message
|
|
}
|
|
}
|
|
|
|
export function hasExactErrorMessage(error: unknown, message: string): boolean {
|
|
return error instanceof Error && error.message === message
|
|
}
|
|
|
|
/**
|
|
* Normalize an unknown value into an Error.
|
|
* Use at catch-site boundaries when you need an Error instance.
|
|
*/
|
|
export function toError(e: unknown): Error {
|
|
return e instanceof Error ? e : new Error(String(e))
|
|
}
|
|
|
|
/**
|
|
* Extract a string message from an unknown error-like value.
|
|
* Use when you only need the message (e.g., for logging or display).
|
|
*/
|
|
export function errorMessage(e: unknown): string {
|
|
return e instanceof Error ? e.message : String(e)
|
|
}
|
|
|
|
/**
|
|
* Extract the errno code (e.g., 'ENOENT', 'EACCES') from a caught error.
|
|
* Returns undefined if the error has no code or is not an ErrnoException.
|
|
* Replaces the `(e as NodeJS.ErrnoException).code` cast pattern.
|
|
*/
|
|
export function getErrnoCode(e: unknown): string | undefined {
|
|
if (e && typeof e === 'object' && 'code' in e && typeof e.code === 'string') {
|
|
return e.code
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* True if the error is ENOENT (file or directory does not exist).
|
|
* Replaces `(e as NodeJS.ErrnoException).code === 'ENOENT'`.
|
|
*/
|
|
export function isENOENT(e: unknown): boolean {
|
|
return getErrnoCode(e) === 'ENOENT'
|
|
}
|
|
|
|
/**
|
|
* Extract the errno path (the filesystem path that triggered the error)
|
|
* from a caught error. Returns undefined if the error has no path.
|
|
* Replaces the `(e as NodeJS.ErrnoException).path` cast pattern.
|
|
*/
|
|
export function getErrnoPath(e: unknown): string | undefined {
|
|
if (e && typeof e === 'object' && 'path' in e && typeof e.path === 'string') {
|
|
return e.path
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Extract error message + top N stack frames from an unknown error.
|
|
* Use when the error flows to the model as a tool_result — full stack
|
|
* traces are ~500-2000 chars of mostly-irrelevant internal frames and
|
|
* waste context tokens. Keep the full stack in debug logs instead.
|
|
*/
|
|
export function shortErrorStack(e: unknown, maxFrames = 5): string {
|
|
if (!(e instanceof Error)) return String(e)
|
|
if (!e.stack) return e.message
|
|
// V8/Bun stack format: "Name: message\n at frame1\n at frame2..."
|
|
// First line is the message; subsequent " at " lines are frames.
|
|
const lines = e.stack.split('\n')
|
|
const header = lines[0] ?? e.message
|
|
const frames = lines.slice(1).filter(l => l.trim().startsWith('at '))
|
|
if (frames.length <= maxFrames) return e.stack
|
|
return [header, ...frames.slice(0, maxFrames)].join('\n')
|
|
}
|
|
|
|
/**
|
|
* True if the error means the path is missing, inaccessible, or
|
|
* structurally unreachable — use in catch blocks after fs operations to
|
|
* distinguish expected "nothing there / no access" from unexpected errors.
|
|
*
|
|
* Covers:
|
|
* ENOENT — path does not exist
|
|
* EACCES — permission denied
|
|
* EPERM — operation not permitted
|
|
* ENOTDIR — a path component is not a directory (e.g. a file named
|
|
* `.claude` exists where a directory is expected)
|
|
* ELOOP — too many symlink levels (circular symlinks)
|
|
*/
|
|
export function isFsInaccessible(e: unknown): e is NodeJS.ErrnoException {
|
|
const code = getErrnoCode(e)
|
|
return (
|
|
code === 'ENOENT' ||
|
|
code === 'EACCES' ||
|
|
code === 'EPERM' ||
|
|
code === 'ENOTDIR' ||
|
|
code === 'ELOOP'
|
|
)
|
|
}
|
|
|
|
export type AxiosErrorKind =
|
|
| 'auth' // 401/403 — caller typically sets skipRetry
|
|
| 'timeout' // ECONNABORTED
|
|
| 'network' // ECONNREFUSED/ENOTFOUND
|
|
| 'http' // other axios error (may have status)
|
|
| 'other' // not an axios error
|
|
|
|
/**
|
|
* Classify a caught error from an axios request into one of a few buckets.
|
|
* Replaces the ~20-line isAxiosError → 401/403 → ECONNABORTED → ECONNREFUSED
|
|
* chain duplicated across sync-style services (settingsSync, policyLimits,
|
|
* remoteManagedSettings, teamMemorySync).
|
|
*
|
|
* Checks the `.isAxiosError` marker property directly (same as
|
|
* axios.isAxiosError()) to keep this module dependency-free.
|
|
*/
|
|
export function classifyAxiosError(e: unknown): {
|
|
kind: AxiosErrorKind
|
|
status?: number
|
|
message: string
|
|
} {
|
|
const message = errorMessage(e)
|
|
if (
|
|
!e ||
|
|
typeof e !== 'object' ||
|
|
!('isAxiosError' in e) ||
|
|
!e.isAxiosError
|
|
) {
|
|
return { kind: 'other', message }
|
|
}
|
|
const err = e as {
|
|
response?: { status?: number }
|
|
code?: string
|
|
}
|
|
const status = err.response?.status
|
|
if (status === 401 || status === 403) return { kind: 'auth', status, message }
|
|
if (err.code === 'ECONNABORTED') return { kind: 'timeout', status, message }
|
|
if (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND') {
|
|
return { kind: 'network', status, message }
|
|
}
|
|
return { kind: 'http', status, message }
|
|
}
|