3480 lines
97 KiB
JavaScript
3480 lines
97 KiB
JavaScript
|
|
// src/_internal/transport/subprocess-cli.ts
|
||
|
|
import { execa } from "execa";
|
||
|
|
import which from "which";
|
||
|
|
import { createInterface } from "readline";
|
||
|
|
import { platform } from "os";
|
||
|
|
import { join } from "path";
|
||
|
|
import { homedir } from "os";
|
||
|
|
import { access, constants } from "fs/promises";
|
||
|
|
|
||
|
|
// src/types/base-error.ts
|
||
|
|
var BaseSDKError = class _BaseSDKError extends Error {
|
||
|
|
constructor(message) {
|
||
|
|
super(message);
|
||
|
|
this.name = "BaseSDKError";
|
||
|
|
Object.setPrototypeOf(this, _BaseSDKError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/types/enhanced-errors.ts
|
||
|
|
var APIError = class _APIError extends BaseSDKError {
|
||
|
|
constructor(message, statusCode, headers) {
|
||
|
|
super(message);
|
||
|
|
this.statusCode = statusCode;
|
||
|
|
this.headers = headers;
|
||
|
|
this.name = "APIError";
|
||
|
|
Object.setPrototypeOf(this, _APIError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var RateLimitError = class _RateLimitError extends APIError {
|
||
|
|
constructor(message, retryAfter, limit, remaining, resetAt) {
|
||
|
|
super(message, 429);
|
||
|
|
this.retryAfter = retryAfter;
|
||
|
|
this.limit = limit;
|
||
|
|
this.remaining = remaining;
|
||
|
|
this.resetAt = resetAt;
|
||
|
|
this.name = "RateLimitError";
|
||
|
|
Object.setPrototypeOf(this, _RateLimitError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var AuthenticationError = class _AuthenticationError extends APIError {
|
||
|
|
constructor(message, authMethod, requiredAction) {
|
||
|
|
super(message, 401);
|
||
|
|
this.authMethod = authMethod;
|
||
|
|
this.requiredAction = requiredAction;
|
||
|
|
this.name = "AuthenticationError";
|
||
|
|
Object.setPrototypeOf(this, _AuthenticationError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ModelNotAvailableError = class _ModelNotAvailableError extends APIError {
|
||
|
|
constructor(model, availableModels, reason) {
|
||
|
|
super(`Model not available: ${model}`, 404);
|
||
|
|
this.model = model;
|
||
|
|
this.availableModels = availableModels;
|
||
|
|
this.reason = reason;
|
||
|
|
this.name = "ModelNotAvailableError";
|
||
|
|
Object.setPrototypeOf(this, _ModelNotAvailableError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ContextLengthExceededError = class _ContextLengthExceededError extends APIError {
|
||
|
|
constructor(currentTokens, maxTokens, truncationStrategy) {
|
||
|
|
super(`Context length exceeded: ${currentTokens} > ${maxTokens} tokens`, 413);
|
||
|
|
this.currentTokens = currentTokens;
|
||
|
|
this.maxTokens = maxTokens;
|
||
|
|
this.truncationStrategy = truncationStrategy;
|
||
|
|
this.name = "ContextLengthExceededError";
|
||
|
|
Object.setPrototypeOf(this, _ContextLengthExceededError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var PermissionError = class _PermissionError extends BaseSDKError {
|
||
|
|
constructor(message, resource, action) {
|
||
|
|
super(message);
|
||
|
|
this.resource = resource;
|
||
|
|
this.action = action;
|
||
|
|
this.name = "PermissionError";
|
||
|
|
Object.setPrototypeOf(this, _PermissionError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ToolPermissionError = class _ToolPermissionError extends PermissionError {
|
||
|
|
constructor(tool, permission, reason, context) {
|
||
|
|
super(`Tool permission denied: ${tool}`, tool, "execute");
|
||
|
|
this.tool = tool;
|
||
|
|
this.permission = permission;
|
||
|
|
this.reason = reason;
|
||
|
|
this.context = context;
|
||
|
|
this.name = "ToolPermissionError";
|
||
|
|
Object.setPrototypeOf(this, _ToolPermissionError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var MCPServerPermissionError = class _MCPServerPermissionError extends PermissionError {
|
||
|
|
constructor(serverName, permission, requestedTools) {
|
||
|
|
super(`MCP server permission denied: ${serverName}`, serverName, "connect");
|
||
|
|
this.serverName = serverName;
|
||
|
|
this.permission = permission;
|
||
|
|
this.requestedTools = requestedTools;
|
||
|
|
this.name = "MCPServerPermissionError";
|
||
|
|
Object.setPrototypeOf(this, _MCPServerPermissionError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var NetworkError = class _NetworkError extends BaseSDKError {
|
||
|
|
constructor(message, code, syscall) {
|
||
|
|
super(message);
|
||
|
|
this.code = code;
|
||
|
|
this.syscall = syscall;
|
||
|
|
this.name = "NetworkError";
|
||
|
|
Object.setPrototypeOf(this, _NetworkError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ConnectionTimeoutError = class _ConnectionTimeoutError extends NetworkError {
|
||
|
|
constructor(timeout, operation) {
|
||
|
|
super(`Connection timeout after ${timeout}ms`, "ETIMEDOUT");
|
||
|
|
this.timeout = timeout;
|
||
|
|
this.operation = operation;
|
||
|
|
this.name = "ConnectionTimeoutError";
|
||
|
|
Object.setPrototypeOf(this, _ConnectionTimeoutError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ConnectionRefusedError = class _ConnectionRefusedError extends NetworkError {
|
||
|
|
constructor(host, port) {
|
||
|
|
super(`Connection refused${host ? ` to ${host}:${port}` : ""}`, "ECONNREFUSED");
|
||
|
|
this.host = host;
|
||
|
|
this.port = port;
|
||
|
|
this.name = "ConnectionRefusedError";
|
||
|
|
Object.setPrototypeOf(this, _ConnectionRefusedError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var StreamingError = class _StreamingError extends BaseSDKError {
|
||
|
|
constructor(message, partialData, bytesReceived) {
|
||
|
|
super(message);
|
||
|
|
this.partialData = partialData;
|
||
|
|
this.bytesReceived = bytesReceived;
|
||
|
|
this.name = "StreamingError";
|
||
|
|
Object.setPrototypeOf(this, _StreamingError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var StreamAbortedError = class _StreamAbortedError extends StreamingError {
|
||
|
|
constructor(reason, abortedAt, partialData) {
|
||
|
|
super(`Stream aborted${reason ? `: ${reason}` : ""}`, partialData);
|
||
|
|
this.reason = reason;
|
||
|
|
this.abortedAt = abortedAt;
|
||
|
|
this.name = "StreamAbortedError";
|
||
|
|
Object.setPrototypeOf(this, _StreamAbortedError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var StreamPausedError = class _StreamPausedError extends StreamingError {
|
||
|
|
constructor(pausedAt, canResume = true) {
|
||
|
|
super("Stream is paused");
|
||
|
|
this.pausedAt = pausedAt;
|
||
|
|
this.canResume = canResume;
|
||
|
|
this.name = "StreamPausedError";
|
||
|
|
Object.setPrototypeOf(this, _StreamPausedError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var MaxRetriesExceededError = class _MaxRetriesExceededError extends BaseSDKError {
|
||
|
|
constructor(lastError, attempts, totalDelay) {
|
||
|
|
super(`Max retries exceeded after ${attempts} attempts: ${lastError.message}`);
|
||
|
|
this.lastError = lastError;
|
||
|
|
this.attempts = attempts;
|
||
|
|
this.totalDelay = totalDelay;
|
||
|
|
this.name = "MaxRetriesExceededError";
|
||
|
|
Object.setPrototypeOf(this, _MaxRetriesExceededError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var CircuitOpenError = class _CircuitOpenError extends BaseSDKError {
|
||
|
|
constructor(openedAt, failureCount, nextRetryAt) {
|
||
|
|
super("Circuit breaker is open");
|
||
|
|
this.openedAt = openedAt;
|
||
|
|
this.failureCount = failureCount;
|
||
|
|
this.nextRetryAt = nextRetryAt;
|
||
|
|
this.name = "CircuitOpenError";
|
||
|
|
Object.setPrototypeOf(this, _CircuitOpenError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function isRateLimitError(error) {
|
||
|
|
return error instanceof RateLimitError;
|
||
|
|
}
|
||
|
|
function isAuthenticationError(error) {
|
||
|
|
return error instanceof AuthenticationError;
|
||
|
|
}
|
||
|
|
function isToolPermissionError(error) {
|
||
|
|
return error instanceof ToolPermissionError;
|
||
|
|
}
|
||
|
|
function isStreamAbortedError(error) {
|
||
|
|
return error instanceof StreamAbortedError;
|
||
|
|
}
|
||
|
|
function isNetworkError(error) {
|
||
|
|
return error instanceof NetworkError;
|
||
|
|
}
|
||
|
|
function isTimeoutError(error) {
|
||
|
|
return error instanceof TimeoutError;
|
||
|
|
}
|
||
|
|
function isValidationError(error) {
|
||
|
|
return error instanceof ValidationError;
|
||
|
|
}
|
||
|
|
function isAPIError(error) {
|
||
|
|
return error instanceof APIError;
|
||
|
|
}
|
||
|
|
function isRetryableError(error) {
|
||
|
|
return error instanceof RateLimitError || error instanceof NetworkError || error instanceof ConnectionTimeoutError || error instanceof APIError && error.statusCode !== void 0 && error.statusCode >= 500;
|
||
|
|
}
|
||
|
|
var TimeoutError = class _TimeoutError extends NetworkError {
|
||
|
|
constructor(message, _timeout) {
|
||
|
|
super(message, "ETIMEDOUT");
|
||
|
|
this.name = "TimeoutError";
|
||
|
|
Object.setPrototypeOf(this, _TimeoutError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ValidationError = class _ValidationError extends BaseSDKError {
|
||
|
|
constructor(message, field, value) {
|
||
|
|
super(message);
|
||
|
|
this.field = field;
|
||
|
|
this.value = value;
|
||
|
|
this.name = "ValidationError";
|
||
|
|
Object.setPrototypeOf(this, _ValidationError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ErrorDetectionPatterns = {
|
||
|
|
rate_limit_error: [
|
||
|
|
/rate[_\s]?limit[_\s]?exceeded/i,
|
||
|
|
/exceeded.*rate[_\s]?limit/i,
|
||
|
|
/429/,
|
||
|
|
/too many requests/i
|
||
|
|
],
|
||
|
|
authentication_error: [
|
||
|
|
/authentication[_\s]?failed/i,
|
||
|
|
/401/,
|
||
|
|
/unauthorized/i,
|
||
|
|
/please[_\s]?login/i
|
||
|
|
],
|
||
|
|
model_not_available_error: [
|
||
|
|
/model[_\s]?not[_\s]?found/i,
|
||
|
|
/invalid[_\s]?model/i,
|
||
|
|
/no such model/i
|
||
|
|
],
|
||
|
|
context_length_exceeded_error: [
|
||
|
|
/context[_\s]?length[_\s]?exceeded/i,
|
||
|
|
/maximum[_\s]?tokens/i,
|
||
|
|
/token[_\s]?limit/i
|
||
|
|
],
|
||
|
|
tool_permission_error: [
|
||
|
|
/tool[_\s]?permission[_\s]?denied/i,
|
||
|
|
/tool[_\s]?not[_\s]?allowed/i,
|
||
|
|
/permission[_\s]?denied[_\s]?for[_\s]?tool/i
|
||
|
|
],
|
||
|
|
network_error: [
|
||
|
|
/network[_\s]?error/i,
|
||
|
|
/ENETWORK/i,
|
||
|
|
/EHOSTUNREACH/i
|
||
|
|
],
|
||
|
|
timeout_error: [
|
||
|
|
/timeout/i,
|
||
|
|
/timed[_\s]?out/i,
|
||
|
|
/ETIMEDOUT/i
|
||
|
|
],
|
||
|
|
connection_refused_error: [
|
||
|
|
/connection[_\s]?refused/i,
|
||
|
|
/ECONNREFUSED/i
|
||
|
|
],
|
||
|
|
stream_aborted_error: [
|
||
|
|
/stream[_\s]?aborted/i,
|
||
|
|
/aborted/i
|
||
|
|
],
|
||
|
|
validation_error: [
|
||
|
|
/validation[_\s]?error/i,
|
||
|
|
/validation[_\s]?failed/i,
|
||
|
|
/invalid[_\s]?parameter/i,
|
||
|
|
/invalid[_\s]?argument/i,
|
||
|
|
/invalid[_\s]?request/i
|
||
|
|
],
|
||
|
|
api_error: []
|
||
|
|
// Catch-all, no specific patterns
|
||
|
|
};
|
||
|
|
var ERROR_PATTERNS = [
|
||
|
|
{
|
||
|
|
pattern: /rate[_\s]?limit[_\s]?exceeded|429|too many requests/i,
|
||
|
|
errorFactory: (_match, output) => {
|
||
|
|
const retryAfterMatch = output.match(/retry[_\s]?after[:\s]+(\d+)/i);
|
||
|
|
const retryAfter = retryAfterMatch?.[1] ? parseInt(retryAfterMatch[1]) : 60;
|
||
|
|
return new RateLimitError("Rate limit exceeded", retryAfter);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /authentication[_\s]?failed|401|unauthorized|please[_\s]?login/i,
|
||
|
|
errorFactory: () => new AuthenticationError("Authentication failed", "cli", "Please run: claude login")
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /model[_\s]?not[_\s]?found|invalid[_\s]?model|no such model/i,
|
||
|
|
errorFactory: (_match, output) => {
|
||
|
|
const modelMatch = output.match(/model[:\s]+"?(\w+)"?/i);
|
||
|
|
const model = modelMatch?.[1] || "unknown";
|
||
|
|
return new ModelNotAvailableError(model);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /context[_\s]?length[_\s]?exceeded|maximum[_\s]?tokens|token[_\s]?limit/i,
|
||
|
|
errorFactory: (_match, output) => {
|
||
|
|
const currentMatch = output.match(/current[:\s]+(\d+)/i);
|
||
|
|
const maxMatch = output.match(/max(?:imum)?[:\s]+(\d+)/i);
|
||
|
|
const current = currentMatch?.[1] ? parseInt(currentMatch[1]) : 0;
|
||
|
|
const max = maxMatch?.[1] ? parseInt(maxMatch[1]) : 0;
|
||
|
|
return new ContextLengthExceededError(current, max);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /tool[_\s]?permission[_\s]?denied|tool[_\s]?not[_\s]?allowed|permission[_\s]?denied[_\s]?for[_\s]?tool/i,
|
||
|
|
errorFactory: (_match, output) => {
|
||
|
|
const toolMatch = output.match(/tool[:\s]+"?(\w+)"?|for[_\s]?tool[:\s]+"?(\w+)"?/i);
|
||
|
|
const tool = toolMatch?.[1] || toolMatch?.[2] || "unknown";
|
||
|
|
return new ToolPermissionError(tool, "deny");
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /connection[_\s]?timeout|ETIMEDOUT|request[_\s]?timeout/i,
|
||
|
|
errorFactory: (_match, output) => {
|
||
|
|
const timeoutMatch = output.match(/timeout[:\s]+(\d+)|after[_\s]?(\d+)ms/i);
|
||
|
|
const timeoutStr = timeoutMatch?.[1] || timeoutMatch?.[2];
|
||
|
|
const timeout = timeoutStr ? parseInt(timeoutStr) : 3e4;
|
||
|
|
return new ConnectionTimeoutError(timeout);
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
pattern: /connection[_\s]?refused|ECONNREFUSED/i,
|
||
|
|
errorFactory: () => new ConnectionRefusedError()
|
||
|
|
}
|
||
|
|
];
|
||
|
|
|
||
|
|
// src/errors.ts
|
||
|
|
var ClaudeSDKError = class _ClaudeSDKError extends Error {
|
||
|
|
constructor(message, code) {
|
||
|
|
super(message);
|
||
|
|
this.code = code;
|
||
|
|
this.name = "ClaudeSDKError";
|
||
|
|
Object.setPrototypeOf(this, _ClaudeSDKError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var CLIConnectionError = class _CLIConnectionError extends ClaudeSDKError {
|
||
|
|
constructor(message) {
|
||
|
|
super(message);
|
||
|
|
this.name = "CLIConnectionError";
|
||
|
|
Object.setPrototypeOf(this, _CLIConnectionError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var CLINotFoundError = class _CLINotFoundError extends ClaudeSDKError {
|
||
|
|
constructor(message = "Claude Code CLI not found. Please install it from https://github.com/anthropics/claude-code") {
|
||
|
|
super(message);
|
||
|
|
this.name = "CLINotFoundError";
|
||
|
|
Object.setPrototypeOf(this, _CLINotFoundError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ProcessError = class _ProcessError extends ClaudeSDKError {
|
||
|
|
constructor(message, exitCode, signal) {
|
||
|
|
super(message);
|
||
|
|
this.exitCode = exitCode;
|
||
|
|
this.signal = signal;
|
||
|
|
this.name = "ProcessError";
|
||
|
|
Object.setPrototypeOf(this, _ProcessError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var AbortError = class _AbortError extends ClaudeSDKError {
|
||
|
|
constructor(message = "Operation was aborted") {
|
||
|
|
super(message, "ABORT_ERROR");
|
||
|
|
this.name = "AbortError";
|
||
|
|
Object.setPrototypeOf(this, _AbortError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var CLIJSONDecodeError = class _CLIJSONDecodeError extends ClaudeSDKError {
|
||
|
|
constructor(message, rawOutput) {
|
||
|
|
super(message);
|
||
|
|
this.rawOutput = rawOutput;
|
||
|
|
this.name = "CLIJSONDecodeError";
|
||
|
|
Object.setPrototypeOf(this, _CLIJSONDecodeError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var ConfigValidationError = class _ConfigValidationError extends ClaudeSDKError {
|
||
|
|
constructor(message) {
|
||
|
|
super(message);
|
||
|
|
this.name = "ConfigValidationError";
|
||
|
|
Object.setPrototypeOf(this, _ConfigValidationError.prototype);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function detectErrorType(message) {
|
||
|
|
for (const [errorType, patterns] of Object.entries(ErrorDetectionPatterns)) {
|
||
|
|
for (const pattern of patterns) {
|
||
|
|
if (pattern.test(message)) {
|
||
|
|
return errorType;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return "api_error";
|
||
|
|
}
|
||
|
|
function createTypedError(errorType, message, _originalError) {
|
||
|
|
switch (errorType) {
|
||
|
|
case "rate_limit_error": {
|
||
|
|
const retryMatch = message.match(/retry.?after[:\s]+(\d+)/i);
|
||
|
|
const retryAfter = retryMatch?.[1] ? parseInt(retryMatch[1]) : 60;
|
||
|
|
return new RateLimitError(message, retryAfter);
|
||
|
|
}
|
||
|
|
case "authentication_error":
|
||
|
|
return new AuthenticationError(message);
|
||
|
|
case "tool_permission_error": {
|
||
|
|
const toolMatch = message.match(/tool[:\s]+([\w]+)/i);
|
||
|
|
const toolName = toolMatch?.[1] || "unknown";
|
||
|
|
return new ToolPermissionError(toolName, "deny", message);
|
||
|
|
}
|
||
|
|
case "network_error":
|
||
|
|
return new NetworkError(message);
|
||
|
|
case "timeout_error":
|
||
|
|
return new TimeoutError(message);
|
||
|
|
case "validation_error":
|
||
|
|
return new ValidationError(message);
|
||
|
|
case "stream_aborted_error":
|
||
|
|
return new StreamAbortedError(message);
|
||
|
|
case "api_error":
|
||
|
|
default: {
|
||
|
|
const statusMatch = message.match(/\b(4\d{2}|5\d{2})\b/);
|
||
|
|
const statusCode = statusMatch?.[1] ? parseInt(statusMatch[1]) : void 0;
|
||
|
|
return new APIError(message, statusCode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/_internal/transport/subprocess-abort-handler.ts
|
||
|
|
var SubprocessAbortHandler = class {
|
||
|
|
constructor(process2, signal) {
|
||
|
|
this.process = process2;
|
||
|
|
this.signal = signal;
|
||
|
|
}
|
||
|
|
cleanupHandler;
|
||
|
|
timeoutId;
|
||
|
|
/**
|
||
|
|
* Sets up abort handling with proper cleanup.
|
||
|
|
* Returns a cleanup function that should be called in finally blocks.
|
||
|
|
*/
|
||
|
|
setup() {
|
||
|
|
if (!this.signal) {
|
||
|
|
return () => {
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (this.signal.aborted) {
|
||
|
|
this.process.cancel();
|
||
|
|
throw new AbortError("Operation aborted before starting");
|
||
|
|
}
|
||
|
|
this.cleanupHandler = () => {
|
||
|
|
this.process.cancel();
|
||
|
|
this.timeoutId = setTimeout(() => {
|
||
|
|
if (!this.process.killed) {
|
||
|
|
this.process.kill("SIGKILL");
|
||
|
|
}
|
||
|
|
}, 5e3);
|
||
|
|
};
|
||
|
|
this.signal.addEventListener("abort", this.cleanupHandler, { once: true });
|
||
|
|
const errorHandler = (error) => {
|
||
|
|
if (error.name === "CancelError" || this.signal?.aborted) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
};
|
||
|
|
this.process.on("error", errorHandler);
|
||
|
|
return () => {
|
||
|
|
if (this.cleanupHandler) {
|
||
|
|
this.signal?.removeEventListener("abort", this.cleanupHandler);
|
||
|
|
}
|
||
|
|
if (this.timeoutId) {
|
||
|
|
clearTimeout(this.timeoutId);
|
||
|
|
}
|
||
|
|
this.process.removeListener("error", errorHandler);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Checks if the process was aborted
|
||
|
|
*/
|
||
|
|
wasAborted() {
|
||
|
|
return this.signal?.aborted ?? false;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/_internal/transport/subprocess-cli.ts
|
||
|
|
var SubprocessCLITransport = class {
|
||
|
|
process;
|
||
|
|
options;
|
||
|
|
prompt;
|
||
|
|
abortHandler;
|
||
|
|
cleanupAbort;
|
||
|
|
constructor(prompt, options = {}) {
|
||
|
|
this.prompt = prompt;
|
||
|
|
this.options = options;
|
||
|
|
}
|
||
|
|
async findCLI() {
|
||
|
|
const localPaths = [
|
||
|
|
join(homedir(), ".claude", "local", "claude"),
|
||
|
|
join(homedir(), ".claude", "bin", "claude")
|
||
|
|
];
|
||
|
|
for (const path3 of localPaths) {
|
||
|
|
try {
|
||
|
|
await access(path3, constants.X_OK);
|
||
|
|
return path3;
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return await which("claude");
|
||
|
|
} catch {
|
||
|
|
try {
|
||
|
|
return await which("claude-code");
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const paths = [];
|
||
|
|
const isWindows = platform() === "win32";
|
||
|
|
const home = homedir();
|
||
|
|
if (isWindows) {
|
||
|
|
paths.push(
|
||
|
|
join(home, "AppData", "Local", "Programs", "claude", "claude.exe"),
|
||
|
|
join(home, "AppData", "Local", "Programs", "claude-code", "claude-code.exe"),
|
||
|
|
"C:\\Program Files\\claude\\claude.exe",
|
||
|
|
"C:\\Program Files\\claude-code\\claude-code.exe"
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
paths.push(
|
||
|
|
"/usr/local/bin/claude",
|
||
|
|
"/usr/local/bin/claude-code",
|
||
|
|
"/usr/bin/claude",
|
||
|
|
"/usr/bin/claude-code",
|
||
|
|
"/opt/homebrew/bin/claude",
|
||
|
|
"/opt/homebrew/bin/claude-code",
|
||
|
|
join(home, ".local", "bin", "claude"),
|
||
|
|
join(home, ".local", "bin", "claude-code"),
|
||
|
|
join(home, "bin", "claude"),
|
||
|
|
join(home, "bin", "claude-code"),
|
||
|
|
join(home, ".claude", "local", "claude")
|
||
|
|
// Claude's custom installation path
|
||
|
|
);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const { stdout: npmPrefix } = await execa("npm", ["config", "get", "prefix"]);
|
||
|
|
if (npmPrefix) {
|
||
|
|
paths.push(
|
||
|
|
join(npmPrefix.trim(), "bin", "claude"),
|
||
|
|
join(npmPrefix.trim(), "bin", "claude-code")
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
for (const path3 of paths) {
|
||
|
|
try {
|
||
|
|
await execa(path3, ["--version"]);
|
||
|
|
return path3;
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw new CLINotFoundError();
|
||
|
|
}
|
||
|
|
buildCommand() {
|
||
|
|
const args = ["--output-format", "stream-json", "--verbose"];
|
||
|
|
if (this.options.model) args.push("--model", this.options.model);
|
||
|
|
if (this.options.sessionId) {
|
||
|
|
args.push("--resume", this.options.sessionId);
|
||
|
|
}
|
||
|
|
if (this.options.allowedTools && this.options.allowedTools.length > 0) {
|
||
|
|
args.push("--allowedTools", this.options.allowedTools.join(","));
|
||
|
|
}
|
||
|
|
if (this.options.deniedTools && this.options.deniedTools.length > 0) {
|
||
|
|
args.push("--disallowedTools", this.options.deniedTools.join(","));
|
||
|
|
}
|
||
|
|
if (this.options.permissionMode === "bypassPermissions") {
|
||
|
|
args.push("--dangerously-skip-permissions");
|
||
|
|
}
|
||
|
|
if (this.options.mcpServers && this.options.mcpServers.length > 0) {
|
||
|
|
const mcpConfig = {
|
||
|
|
mcpServers: this.options.mcpServers
|
||
|
|
};
|
||
|
|
args.push("--mcp-config", JSON.stringify(mcpConfig));
|
||
|
|
}
|
||
|
|
if (this.options.mcpServerPermissions && Object.keys(this.options.mcpServerPermissions).length > 0) {
|
||
|
|
args.push("--mcp-server-permissions", JSON.stringify(this.options.mcpServerPermissions));
|
||
|
|
}
|
||
|
|
if (this.options.configFile) {
|
||
|
|
args.push("--config-file", this.options.configFile);
|
||
|
|
}
|
||
|
|
if (this.options.role) {
|
||
|
|
args.push("--role", this.options.role);
|
||
|
|
}
|
||
|
|
if (this.options.context && this.options.context.length > 0) {
|
||
|
|
args.push("--context", ...this.options.context);
|
||
|
|
}
|
||
|
|
if (this.options.temperature !== void 0) {
|
||
|
|
args.push("--temperature", this.options.temperature.toString());
|
||
|
|
}
|
||
|
|
if (this.options.maxTokens !== void 0) {
|
||
|
|
args.push("--max-tokens", this.options.maxTokens.toString());
|
||
|
|
}
|
||
|
|
if (this.options.addDirectories && this.options.addDirectories.length > 0) {
|
||
|
|
args.push("--add-dir", this.options.addDirectories.join(" "));
|
||
|
|
}
|
||
|
|
args.push("--print");
|
||
|
|
return args;
|
||
|
|
}
|
||
|
|
async connect() {
|
||
|
|
const cliPath = await this.findCLI();
|
||
|
|
const args = this.buildCommand();
|
||
|
|
const env = {
|
||
|
|
...process.env,
|
||
|
|
...this.options.env,
|
||
|
|
CLAUDE_CODE_ENTRYPOINT: "sdk-ts"
|
||
|
|
};
|
||
|
|
if (this.options.debug) {
|
||
|
|
console.error("DEBUG: Running command:", cliPath, args.join(" "));
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
this.process = execa(cliPath, args, {
|
||
|
|
env,
|
||
|
|
cwd: this.options.cwd,
|
||
|
|
stdin: "pipe",
|
||
|
|
stdout: "pipe",
|
||
|
|
stderr: "pipe",
|
||
|
|
buffer: false
|
||
|
|
// Remove signal from here - we'll handle it manually
|
||
|
|
});
|
||
|
|
this.abortHandler = new SubprocessAbortHandler(this.process, this.options.signal);
|
||
|
|
this.cleanupAbort = this.abortHandler.setup();
|
||
|
|
if (this.process.stdin) {
|
||
|
|
this.process.stdin.write(this.prompt);
|
||
|
|
this.process.stdin.end();
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
throw new CLIConnectionError(`Failed to start Claude Code CLI: ${error}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async *receiveMessages() {
|
||
|
|
if (!this.process || !this.process.stdout) {
|
||
|
|
throw new CLIConnectionError("Not connected to CLI");
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
if (this.process.stderr) {
|
||
|
|
const stderrRl = createInterface({
|
||
|
|
input: this.process.stderr,
|
||
|
|
crlfDelay: Infinity
|
||
|
|
});
|
||
|
|
stderrRl.on("line", (line) => {
|
||
|
|
if (this.options.debug) {
|
||
|
|
console.error("DEBUG stderr:", line);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
const rl = createInterface({
|
||
|
|
input: this.process.stdout,
|
||
|
|
crlfDelay: Infinity
|
||
|
|
});
|
||
|
|
for await (const line of rl) {
|
||
|
|
const trimmedLine = line.trim();
|
||
|
|
if (!trimmedLine) continue;
|
||
|
|
if (this.options.debug) {
|
||
|
|
console.error("DEBUG stdout:", trimmedLine);
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
const parsed = JSON.parse(trimmedLine);
|
||
|
|
yield parsed;
|
||
|
|
} catch (error) {
|
||
|
|
if (trimmedLine.startsWith("{") || trimmedLine.startsWith("[")) {
|
||
|
|
throw new CLIJSONDecodeError(
|
||
|
|
`Failed to parse CLI output: ${error}`,
|
||
|
|
trimmedLine
|
||
|
|
);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
await this.process;
|
||
|
|
} catch (error) {
|
||
|
|
if (error.isCanceled || error.name === "CancelError" || this.abortHandler?.wasAborted()) {
|
||
|
|
throw new AbortError("Query was aborted via AbortSignal");
|
||
|
|
}
|
||
|
|
const execError = error;
|
||
|
|
if (execError.exitCode !== 0) {
|
||
|
|
throw new ProcessError(
|
||
|
|
`Claude Code CLI exited with code ${execError.exitCode}`,
|
||
|
|
execError.exitCode,
|
||
|
|
execError.signal
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
if (this.cleanupAbort) {
|
||
|
|
this.cleanupAbort();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
async disconnect() {
|
||
|
|
if (this.cleanupAbort) {
|
||
|
|
this.cleanupAbort();
|
||
|
|
this.cleanupAbort = void 0;
|
||
|
|
}
|
||
|
|
if (this.process) {
|
||
|
|
if (!this.process.killed) {
|
||
|
|
this.process.kill();
|
||
|
|
}
|
||
|
|
this.process = void 0;
|
||
|
|
}
|
||
|
|
this.abortHandler = void 0;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/environment.ts
|
||
|
|
function parseBoolean(value) {
|
||
|
|
if (value === void 0) return void 0;
|
||
|
|
const normalized = value.toLowerCase().trim();
|
||
|
|
if (normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on") {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
if (normalized === "false" || normalized === "0" || normalized === "no" || normalized === "off") {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return void 0;
|
||
|
|
}
|
||
|
|
function parseLogLevel(value) {
|
||
|
|
if (value === void 0) return void 0;
|
||
|
|
const level = parseInt(value, 10);
|
||
|
|
if (isNaN(level) || level < 0 || level > 4) {
|
||
|
|
return void 0;
|
||
|
|
}
|
||
|
|
return level;
|
||
|
|
}
|
||
|
|
function loadSafeEnvironmentOptions() {
|
||
|
|
const options = {};
|
||
|
|
const debug = parseBoolean(process.env.DEBUG);
|
||
|
|
if (debug !== void 0) {
|
||
|
|
options.debug = debug;
|
||
|
|
}
|
||
|
|
const verbose = parseBoolean(process.env.VERBOSE);
|
||
|
|
if (verbose !== void 0) {
|
||
|
|
options.verbose = verbose;
|
||
|
|
}
|
||
|
|
const logLevel = parseLogLevel(process.env.LOG_LEVEL);
|
||
|
|
if (logLevel !== void 0) {
|
||
|
|
options.logLevel = logLevel;
|
||
|
|
}
|
||
|
|
if (process.env.NODE_ENV) {
|
||
|
|
options.nodeEnv = process.env.NODE_ENV;
|
||
|
|
}
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
var API_KEY_SAFETY_WARNING = `
|
||
|
|
IMPORTANT: API keys are not automatically loaded from environment variables.
|
||
|
|
This is a safety measure to prevent accidental billing charges.
|
||
|
|
|
||
|
|
If you need to use an API key, you must explicitly provide it in your code:
|
||
|
|
const result = await query('Your prompt', { apiKey: 'your-api-key' });
|
||
|
|
|
||
|
|
If you understand the risks and want to allow API key from environment:
|
||
|
|
const result = await query('Your prompt', {
|
||
|
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
||
|
|
allowApiKeyFromEnv: true // Explicit opt-in
|
||
|
|
});
|
||
|
|
`.trim();
|
||
|
|
|
||
|
|
// src/_internal/options-merger.ts
|
||
|
|
function applyEnvironmentOptions(userOptions, envOptions) {
|
||
|
|
const merged = { ...userOptions };
|
||
|
|
if (merged.debug === void 0 && envOptions.debug !== void 0) {
|
||
|
|
merged.debug = envOptions.debug;
|
||
|
|
}
|
||
|
|
if (!("verbose" in merged) && envOptions.verbose !== void 0) {
|
||
|
|
merged.verbose = envOptions.verbose;
|
||
|
|
}
|
||
|
|
if (!("logLevel" in merged) && envOptions.logLevel !== void 0) {
|
||
|
|
merged.logLevel = envOptions.logLevel;
|
||
|
|
}
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/_internal/client.ts
|
||
|
|
var InternalClient = class {
|
||
|
|
options;
|
||
|
|
prompt;
|
||
|
|
constructor(prompt, options = {}) {
|
||
|
|
this.prompt = prompt;
|
||
|
|
const envOptions = loadSafeEnvironmentOptions();
|
||
|
|
this.options = applyEnvironmentOptions(options, envOptions);
|
||
|
|
}
|
||
|
|
async *processQuery() {
|
||
|
|
const transport = new SubprocessCLITransport(this.prompt, this.options);
|
||
|
|
try {
|
||
|
|
await transport.connect();
|
||
|
|
for await (const output of transport.receiveMessages()) {
|
||
|
|
const message = this.parseMessage(output);
|
||
|
|
if (message) {
|
||
|
|
yield message;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} finally {
|
||
|
|
await transport.disconnect();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
parseMessage(output) {
|
||
|
|
switch (output.type) {
|
||
|
|
case "assistant": {
|
||
|
|
const assistantMsg = output;
|
||
|
|
if (assistantMsg.message) {
|
||
|
|
return {
|
||
|
|
type: "assistant",
|
||
|
|
content: assistantMsg.message.content,
|
||
|
|
session_id: assistantMsg.session_id
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
type: "assistant",
|
||
|
|
content: [],
|
||
|
|
session_id: assistantMsg.session_id
|
||
|
|
};
|
||
|
|
}
|
||
|
|
case "system":
|
||
|
|
return null;
|
||
|
|
case "result": {
|
||
|
|
const resultMsg = output;
|
||
|
|
return {
|
||
|
|
type: "result",
|
||
|
|
subtype: resultMsg.subtype,
|
||
|
|
content: resultMsg.content || "",
|
||
|
|
session_id: resultMsg.session_id,
|
||
|
|
usage: resultMsg.usage,
|
||
|
|
cost: {
|
||
|
|
total_cost: resultMsg.cost?.total_cost_usd
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
case "error": {
|
||
|
|
const errorOutput = output;
|
||
|
|
const errorMessage = errorOutput.error?.message || "Unknown error";
|
||
|
|
const errorType = detectErrorType(errorMessage);
|
||
|
|
throw createTypedError(errorType, errorMessage, errorOutput.error);
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/types/telemetry.ts
|
||
|
|
var BUILTIN_METRICS = {
|
||
|
|
queries: {
|
||
|
|
name: "claude_sdk_queries_total",
|
||
|
|
type: "counter",
|
||
|
|
description: "Total number of queries",
|
||
|
|
unit: "query"
|
||
|
|
},
|
||
|
|
queryDuration: {
|
||
|
|
name: "claude_sdk_query_duration",
|
||
|
|
type: "histogram",
|
||
|
|
description: "Query duration",
|
||
|
|
unit: "ms",
|
||
|
|
boundaries: [10, 50, 100, 500, 1e3, 5e3, 1e4, 3e4, 6e4]
|
||
|
|
},
|
||
|
|
tokens: {
|
||
|
|
name: "claude_sdk_tokens_total",
|
||
|
|
type: "counter",
|
||
|
|
description: "Total tokens used",
|
||
|
|
unit: "token"
|
||
|
|
},
|
||
|
|
toolExecutions: {
|
||
|
|
name: "claude_sdk_tool_executions_total",
|
||
|
|
type: "counter",
|
||
|
|
description: "Total tool executions",
|
||
|
|
unit: "execution"
|
||
|
|
},
|
||
|
|
toolDuration: {
|
||
|
|
name: "claude_sdk_tool_duration",
|
||
|
|
type: "histogram",
|
||
|
|
description: "Tool execution duration",
|
||
|
|
unit: "ms",
|
||
|
|
boundaries: [1, 5, 10, 50, 100, 500, 1e3, 5e3]
|
||
|
|
},
|
||
|
|
errors: {
|
||
|
|
name: "claude_sdk_errors_total",
|
||
|
|
type: "counter",
|
||
|
|
description: "Total errors",
|
||
|
|
unit: "error"
|
||
|
|
},
|
||
|
|
retries: {
|
||
|
|
name: "claude_sdk_retries_total",
|
||
|
|
type: "counter",
|
||
|
|
description: "Total retry attempts",
|
||
|
|
unit: "retry"
|
||
|
|
},
|
||
|
|
activeStreams: {
|
||
|
|
name: "claude_sdk_active_streams",
|
||
|
|
type: "gauge",
|
||
|
|
description: "Number of active streams",
|
||
|
|
unit: "stream"
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/types/retry.ts
|
||
|
|
var ExponentialBackoffStrategy = class {
|
||
|
|
constructor(options = {}) {
|
||
|
|
this.options = options;
|
||
|
|
}
|
||
|
|
calculateDelay(attempt, baseDelay) {
|
||
|
|
const multiplier = this.options.multiplier || 2;
|
||
|
|
const base = this.options.base || multiplier;
|
||
|
|
const maxDelay = this.options.maxDelay || 6e4;
|
||
|
|
let delay = baseDelay * Math.pow(base, attempt - 1);
|
||
|
|
delay = Math.min(delay, maxDelay);
|
||
|
|
if (this.options.jitter) {
|
||
|
|
const jitterFactor = this.options.jitterFactor || 0.1;
|
||
|
|
const jitterRange = delay * jitterFactor;
|
||
|
|
const jitter = (Math.random() - 0.5) * 2 * jitterRange;
|
||
|
|
delay = Math.max(0, delay + jitter);
|
||
|
|
}
|
||
|
|
return Math.round(delay);
|
||
|
|
}
|
||
|
|
shouldRetry(_error, _attempt) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
reset() {
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var LinearBackoffStrategy = class {
|
||
|
|
constructor(options = {}) {
|
||
|
|
this.options = options;
|
||
|
|
}
|
||
|
|
calculateDelay(attempt, baseDelay) {
|
||
|
|
const increment = this.options.increment || baseDelay;
|
||
|
|
const maxDelay = this.options.maxDelay || 6e4;
|
||
|
|
let delay = baseDelay + increment * (attempt - 1);
|
||
|
|
delay = Math.min(delay, maxDelay);
|
||
|
|
if (this.options.jitter) {
|
||
|
|
const jitter = (Math.random() - 0.5) * 0.2 * delay;
|
||
|
|
delay = Math.max(0, delay + jitter);
|
||
|
|
}
|
||
|
|
return Math.round(delay);
|
||
|
|
}
|
||
|
|
shouldRetry(_error, _attempt) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
reset() {
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var FibonacciBackoffStrategy = class {
|
||
|
|
constructor(options = {}) {
|
||
|
|
this.options = options;
|
||
|
|
}
|
||
|
|
sequence = [1, 1];
|
||
|
|
calculateDelay(attempt, baseDelay) {
|
||
|
|
const maxDelay = this.options.maxDelay || 6e4;
|
||
|
|
while (this.sequence.length < attempt) {
|
||
|
|
const len = this.sequence.length;
|
||
|
|
if (len >= 2) {
|
||
|
|
const next = this.sequence[len - 1] + this.sequence[len - 2];
|
||
|
|
this.sequence.push(next);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const fibValue = this.sequence[attempt - 1] ?? 1;
|
||
|
|
let delay = baseDelay * fibValue;
|
||
|
|
delay = Math.min(delay, maxDelay);
|
||
|
|
if (this.options.jitter) {
|
||
|
|
const jitter = (Math.random() - 0.5) * 0.2 * delay;
|
||
|
|
delay = Math.max(0, delay + jitter);
|
||
|
|
}
|
||
|
|
return Math.round(delay);
|
||
|
|
}
|
||
|
|
shouldRetry(_error, _attempt) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
reset() {
|
||
|
|
this.sequence = [1, 1];
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var RetryUtils = class {
|
||
|
|
/** Default retryable errors */
|
||
|
|
static DEFAULT_RETRYABLE_ERRORS = [
|
||
|
|
"NetworkError",
|
||
|
|
"TimeoutError",
|
||
|
|
"RateLimitError",
|
||
|
|
"ServiceUnavailableError"
|
||
|
|
];
|
||
|
|
/** Check if error is retryable by default */
|
||
|
|
static isRetryableError(error) {
|
||
|
|
if (this.DEFAULT_RETRYABLE_ERRORS.includes(error.name)) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
const retryablePatterns = [
|
||
|
|
/timeout/i,
|
||
|
|
/rate.?limit/i,
|
||
|
|
/too.?many.?requests/i,
|
||
|
|
/service.?unavailable/i,
|
||
|
|
/gateway.?timeout/i,
|
||
|
|
/ECONNREFUSED/,
|
||
|
|
/ETIMEDOUT/,
|
||
|
|
/ENOTFOUND/
|
||
|
|
];
|
||
|
|
return retryablePatterns.some((pattern) => pattern.test(error.message));
|
||
|
|
}
|
||
|
|
/** Sleep for specified milliseconds */
|
||
|
|
static sleep(ms) {
|
||
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
|
|
}
|
||
|
|
/** Create retry function with defaults */
|
||
|
|
static withRetry(fn, options = {}) {
|
||
|
|
return async () => {
|
||
|
|
const executor = new SimpleRetryExecutor();
|
||
|
|
return executor.execute(fn, options);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var SimpleRetryExecutor = class {
|
||
|
|
defaults = {
|
||
|
|
maxAttempts: 3,
|
||
|
|
initialDelay: 1e3,
|
||
|
|
maxDelay: 3e4,
|
||
|
|
multiplier: 2,
|
||
|
|
jitter: true
|
||
|
|
};
|
||
|
|
stats = {
|
||
|
|
totalExecutions: 0,
|
||
|
|
successfulFirstAttempts: 0,
|
||
|
|
successfulRetries: 0,
|
||
|
|
totalFailures: 0,
|
||
|
|
totalRetryAttempts: 0,
|
||
|
|
averageAttempts: 0,
|
||
|
|
maxAttempts: 0
|
||
|
|
};
|
||
|
|
async execute(fn, options) {
|
||
|
|
const result = await this.executeWithResult(fn, options);
|
||
|
|
return result.value;
|
||
|
|
}
|
||
|
|
async executeWithResult(fn, options) {
|
||
|
|
const opts = { ...this.defaults, ...options };
|
||
|
|
const errors = [];
|
||
|
|
const startTime = Date.now();
|
||
|
|
this.stats.totalExecutions++;
|
||
|
|
for (let attempt = 1; attempt <= (opts.maxAttempts || 3); attempt++) {
|
||
|
|
try {
|
||
|
|
const value = await fn();
|
||
|
|
if (attempt === 1) {
|
||
|
|
this.stats.successfulFirstAttempts++;
|
||
|
|
} else {
|
||
|
|
this.stats.successfulRetries++;
|
||
|
|
}
|
||
|
|
this.updateStats(attempt);
|
||
|
|
return {
|
||
|
|
value,
|
||
|
|
attempts: attempt,
|
||
|
|
totalDuration: Date.now() - startTime,
|
||
|
|
errors
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
errors.push(error);
|
||
|
|
const shouldRetry = this.shouldRetry(error, attempt, opts);
|
||
|
|
if (!shouldRetry || attempt === opts.maxAttempts) {
|
||
|
|
this.stats.totalFailures++;
|
||
|
|
this.updateStats(attempt);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
const delay = this.calculateDelay(attempt, opts);
|
||
|
|
if (opts.onRetry) {
|
||
|
|
await opts.onRetry(attempt, error, delay);
|
||
|
|
}
|
||
|
|
await RetryUtils.sleep(delay);
|
||
|
|
this.stats.totalRetryAttempts++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw errors[errors.length - 1];
|
||
|
|
}
|
||
|
|
setDefaults(options) {
|
||
|
|
this.defaults = { ...this.defaults, ...options };
|
||
|
|
}
|
||
|
|
getStats() {
|
||
|
|
return { ...this.stats };
|
||
|
|
}
|
||
|
|
resetStats() {
|
||
|
|
this.stats = {
|
||
|
|
totalExecutions: 0,
|
||
|
|
successfulFirstAttempts: 0,
|
||
|
|
successfulRetries: 0,
|
||
|
|
totalFailures: 0,
|
||
|
|
totalRetryAttempts: 0,
|
||
|
|
averageAttempts: 0,
|
||
|
|
maxAttempts: 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
shouldRetry(error, attempt, options) {
|
||
|
|
if (options.shouldRetry) {
|
||
|
|
return options.shouldRetry(error, attempt);
|
||
|
|
}
|
||
|
|
if (options.retryableErrors) {
|
||
|
|
return options.retryableErrors.some((ErrorClass) => error instanceof ErrorClass);
|
||
|
|
}
|
||
|
|
return RetryUtils.isRetryableError(error);
|
||
|
|
}
|
||
|
|
calculateDelay(attempt, options) {
|
||
|
|
const strategy = new ExponentialBackoffStrategy({
|
||
|
|
multiplier: options.multiplier,
|
||
|
|
maxDelay: options.maxDelay,
|
||
|
|
jitter: options.jitter,
|
||
|
|
jitterFactor: options.jitterFactor
|
||
|
|
});
|
||
|
|
return strategy.calculateDelay(attempt, options.initialDelay || 1e3);
|
||
|
|
}
|
||
|
|
updateStats(attempts) {
|
||
|
|
this.stats.maxAttempts = Math.max(this.stats.maxAttempts, attempts);
|
||
|
|
const totalAttempts = this.stats.successfulFirstAttempts + this.stats.successfulRetries * 2 + // At least 2 attempts
|
||
|
|
this.stats.totalFailures * (this.defaults.maxAttempts || 3);
|
||
|
|
this.stats.averageAttempts = totalAttempts / this.stats.totalExecutions;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/types/environment.ts
|
||
|
|
function isEnhancedError(error) {
|
||
|
|
return error instanceof Error && "category" in error && typeof error.category === "string";
|
||
|
|
}
|
||
|
|
function hasResolution(error) {
|
||
|
|
return isEnhancedError(error) && !!error.resolution && typeof error.resolution === "string";
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/parser.ts
|
||
|
|
var ResponseParser = class {
|
||
|
|
constructor(generator, handlers = [], logger) {
|
||
|
|
this.generator = generator;
|
||
|
|
this.handlers = handlers;
|
||
|
|
this.logger = logger;
|
||
|
|
}
|
||
|
|
messages = [];
|
||
|
|
consumed = false;
|
||
|
|
/**
|
||
|
|
* Get all messages as an array (consumes the generator)
|
||
|
|
*/
|
||
|
|
async asArray() {
|
||
|
|
await this.consume();
|
||
|
|
return this.messages;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get only the text content from assistant messages
|
||
|
|
*/
|
||
|
|
async asText() {
|
||
|
|
await this.consume();
|
||
|
|
const texts = [];
|
||
|
|
for (const msg of this.messages) {
|
||
|
|
if (msg.type === "assistant") {
|
||
|
|
for (const block of msg.content) {
|
||
|
|
if (block.type === "text") {
|
||
|
|
texts.push(block.text);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return texts.join("\n");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the final result message content
|
||
|
|
*/
|
||
|
|
async asResult() {
|
||
|
|
await this.consume();
|
||
|
|
const resultMsg = this.messages.findLast((msg) => msg.type === "result");
|
||
|
|
return resultMsg?.content ?? null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all tool uses with their results
|
||
|
|
*/
|
||
|
|
async asToolExecutions() {
|
||
|
|
await this.consume();
|
||
|
|
const executions = [];
|
||
|
|
const toolUses = /* @__PURE__ */ new Map();
|
||
|
|
for (const msg of this.messages) {
|
||
|
|
if (msg.type === "assistant") {
|
||
|
|
for (const block of msg.content) {
|
||
|
|
if (block.type === "tool_use") {
|
||
|
|
toolUses.set(block.id, block);
|
||
|
|
} else if (block.type === "tool_result") {
|
||
|
|
const toolUse = toolUses.get(block.tool_use_id);
|
||
|
|
if (toolUse) {
|
||
|
|
executions.push({
|
||
|
|
tool: toolUse.name,
|
||
|
|
input: toolUse.input,
|
||
|
|
result: block.content,
|
||
|
|
isError: block.is_error ?? false
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return executions;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Find all tool results for a specific tool
|
||
|
|
*/
|
||
|
|
async findToolResults(toolName) {
|
||
|
|
const executions = await this.asToolExecutions();
|
||
|
|
return executions.filter((exec) => exec.tool === toolName && !exec.isError).map((exec) => exec.result);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get first tool result for a specific tool
|
||
|
|
*/
|
||
|
|
async findToolResult(toolName) {
|
||
|
|
const results = await this.findToolResults(toolName);
|
||
|
|
return results[0] ?? null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Extract structured data from the response
|
||
|
|
*/
|
||
|
|
async asJSON() {
|
||
|
|
const text = await this.asText();
|
||
|
|
const codeBlockMatch = text.match(/```(?:json)?\n([\s\S]*?)\n```/);
|
||
|
|
if (codeBlockMatch) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(codeBlockMatch[1] ?? "");
|
||
|
|
} catch (e) {
|
||
|
|
this.logger?.warn("Failed to parse JSON from code block", { error: e });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
return JSON.parse(text);
|
||
|
|
} catch {
|
||
|
|
const jsonMatch = text.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
|
||
|
|
if (jsonMatch) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(jsonMatch[0]);
|
||
|
|
} catch (e) {
|
||
|
|
this.logger?.warn("Failed to parse JSON from text", { error: e });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get usage statistics
|
||
|
|
*/
|
||
|
|
async getUsage() {
|
||
|
|
await this.consume();
|
||
|
|
const resultMsg = this.messages.findLast((msg) => msg.type === "result");
|
||
|
|
if (!resultMsg?.usage) return null;
|
||
|
|
return {
|
||
|
|
inputTokens: resultMsg.usage.input_tokens ?? 0,
|
||
|
|
outputTokens: resultMsg.usage.output_tokens ?? 0,
|
||
|
|
cacheCreationTokens: resultMsg.usage.cache_creation_input_tokens ?? 0,
|
||
|
|
cacheReadTokens: resultMsg.usage.cache_read_input_tokens ?? 0,
|
||
|
|
totalTokens: (resultMsg.usage.input_tokens ?? 0) + (resultMsg.usage.output_tokens ?? 0),
|
||
|
|
totalCost: resultMsg.cost?.total_cost ?? 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the session ID if available
|
||
|
|
*/
|
||
|
|
async getSessionId() {
|
||
|
|
await this.consume();
|
||
|
|
for (const msg of this.messages) {
|
||
|
|
if ("session_id" in msg && msg.session_id) {
|
||
|
|
return msg.session_id;
|
||
|
|
}
|
||
|
|
if (msg.type === "system" && msg.data && typeof msg.data === "object" && "session_id" in msg.data) {
|
||
|
|
return String(msg.data.session_id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Stream messages with a callback (doesn't consume for other methods)
|
||
|
|
*/
|
||
|
|
async stream(callback) {
|
||
|
|
for await (const message of this.generator) {
|
||
|
|
for (const handler of this.handlers) {
|
||
|
|
try {
|
||
|
|
handler(message);
|
||
|
|
} catch (error) {
|
||
|
|
this.logger?.error("Message handler error", { error });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.messages.push(message);
|
||
|
|
await callback(message);
|
||
|
|
}
|
||
|
|
this.consumed = true;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Wait for completion and return success status
|
||
|
|
*/
|
||
|
|
async succeeded() {
|
||
|
|
await this.consume();
|
||
|
|
const resultMsg = this.messages.findLast((msg) => msg.type === "result");
|
||
|
|
if (!resultMsg) return false;
|
||
|
|
const executions = await this.asToolExecutions();
|
||
|
|
const hasErrors = executions.some((exec) => exec.isError);
|
||
|
|
return !hasErrors;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all error messages
|
||
|
|
*/
|
||
|
|
async getErrors() {
|
||
|
|
await this.consume();
|
||
|
|
const errors = [];
|
||
|
|
for (const msg of this.messages) {
|
||
|
|
if (msg.type === "system" && msg.subtype === "error") {
|
||
|
|
const errorMessage = msg.data && typeof msg.data === "object" && "message" in msg.data ? String(msg.data.message) : "Unknown error";
|
||
|
|
errors.push(errorMessage);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const executions = await this.asToolExecutions();
|
||
|
|
for (const exec of executions) {
|
||
|
|
if (exec.isError) {
|
||
|
|
errors.push(`Tool ${exec.tool} failed: ${exec.result}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return errors;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Transform messages using a custom transformer
|
||
|
|
*/
|
||
|
|
async transform(transformer) {
|
||
|
|
await this.consume();
|
||
|
|
return transformer(this.messages);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Consume the generator if not already consumed
|
||
|
|
*/
|
||
|
|
async consume() {
|
||
|
|
if (this.consumed) return;
|
||
|
|
this.logger?.debug("Consuming message generator");
|
||
|
|
for await (const message of this.generator) {
|
||
|
|
this.logger?.debug("Received message", { type: message.type });
|
||
|
|
for (const handler of this.handlers) {
|
||
|
|
try {
|
||
|
|
handler(message);
|
||
|
|
} catch (error) {
|
||
|
|
this.logger?.error("Message handler error", { error });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.messages.push(message);
|
||
|
|
}
|
||
|
|
this.consumed = true;
|
||
|
|
this.logger?.debug("Message generator consumed", { messageCount: this.messages.length });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/permissions/manager.ts
|
||
|
|
var PermissionManager = class _PermissionManager {
|
||
|
|
mcpServerPermissions;
|
||
|
|
constructor() {
|
||
|
|
this.mcpServerPermissions = /* @__PURE__ */ new Map();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set permission for an MCP server
|
||
|
|
*/
|
||
|
|
setMCPServerPermission(serverName, permission) {
|
||
|
|
if (!serverName) {
|
||
|
|
throw new Error("Server name cannot be empty");
|
||
|
|
}
|
||
|
|
if (!["whitelist", "blacklist", "ask"].includes(permission)) {
|
||
|
|
throw new Error(`Invalid permission value: ${permission}`);
|
||
|
|
}
|
||
|
|
this.mcpServerPermissions.set(serverName, permission);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get permission for an MCP server
|
||
|
|
*/
|
||
|
|
getMCPServerPermission(serverName) {
|
||
|
|
return this.mcpServerPermissions.get(serverName);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set multiple MCP server permissions at once
|
||
|
|
*/
|
||
|
|
setMCPServerPermissions(permissions) {
|
||
|
|
Object.entries(permissions).forEach(([serverName, permission]) => {
|
||
|
|
this.setMCPServerPermission(serverName, permission);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear all MCP server permissions
|
||
|
|
*/
|
||
|
|
clearMCPServerPermissions() {
|
||
|
|
this.mcpServerPermissions.clear();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Export current configuration
|
||
|
|
*/
|
||
|
|
exportConfig() {
|
||
|
|
const config = {};
|
||
|
|
this.mcpServerPermissions.forEach((permission, serverName) => {
|
||
|
|
config[serverName] = permission;
|
||
|
|
});
|
||
|
|
return config;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resolve tool permission based on MCP server permission
|
||
|
|
* Maps MCP server permissions to tool-level permissions
|
||
|
|
*/
|
||
|
|
resolveToolPermission(toolName, serverName) {
|
||
|
|
const serverPermission = this.getMCPServerPermission(serverName);
|
||
|
|
if (!serverPermission) {
|
||
|
|
return void 0;
|
||
|
|
}
|
||
|
|
const permissionMap = {
|
||
|
|
"whitelist": "allow",
|
||
|
|
"blacklist": "deny",
|
||
|
|
"ask": "ask"
|
||
|
|
};
|
||
|
|
return permissionMap[serverPermission];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply permissions to ClaudeCodeOptions
|
||
|
|
*/
|
||
|
|
applyToOptions(options, merge = true) {
|
||
|
|
const exportedConfig = this.exportConfig();
|
||
|
|
if (Object.keys(exportedConfig).length === 0) {
|
||
|
|
return options;
|
||
|
|
}
|
||
|
|
if (merge && options.mcpServerPermissions) {
|
||
|
|
return {
|
||
|
|
...options,
|
||
|
|
mcpServerPermissions: {
|
||
|
|
...options.mcpServerPermissions,
|
||
|
|
...exportedConfig
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
...options,
|
||
|
|
mcpServerPermissions: exportedConfig
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Serialize to JSON string
|
||
|
|
*/
|
||
|
|
toJSON() {
|
||
|
|
return JSON.stringify(this.exportConfig());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Deserialize from JSON string
|
||
|
|
*/
|
||
|
|
fromJSON(json) {
|
||
|
|
try {
|
||
|
|
const config = JSON.parse(json);
|
||
|
|
this.clearMCPServerPermissions();
|
||
|
|
this.setMCPServerPermissions(config);
|
||
|
|
} catch (error) {
|
||
|
|
throw new Error("Invalid JSON");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clone the permission manager
|
||
|
|
*/
|
||
|
|
clone() {
|
||
|
|
const cloned = new _PermissionManager();
|
||
|
|
cloned.setMCPServerPermissions(this.exportConfig());
|
||
|
|
return cloned;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a server has any permission set
|
||
|
|
*/
|
||
|
|
hasPermission(serverName) {
|
||
|
|
return this.mcpServerPermissions.has(serverName);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get all server names with permissions
|
||
|
|
*/
|
||
|
|
getServerNames() {
|
||
|
|
return Array.from(this.mcpServerPermissions.keys());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get count of permissions
|
||
|
|
*/
|
||
|
|
get size() {
|
||
|
|
return this.mcpServerPermissions.size;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if empty
|
||
|
|
*/
|
||
|
|
get isEmpty() {
|
||
|
|
return this.mcpServerPermissions.size === 0;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/config/loader.ts
|
||
|
|
import { promises as fs } from "fs";
|
||
|
|
import path from "path";
|
||
|
|
var ConfigLoader = class {
|
||
|
|
loadedConfigs = /* @__PURE__ */ new Map();
|
||
|
|
/**
|
||
|
|
* Detect file format based on extension
|
||
|
|
*/
|
||
|
|
detectFormat(filePath) {
|
||
|
|
const ext = path.extname(filePath).toLowerCase();
|
||
|
|
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
||
|
|
return "json";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load configuration from file
|
||
|
|
*/
|
||
|
|
async loadFromFile(filePath, options) {
|
||
|
|
const absolutePath = path.resolve(filePath);
|
||
|
|
try {
|
||
|
|
await fs.access(absolutePath);
|
||
|
|
} catch (error) {
|
||
|
|
if (error.code === "ENOENT") {
|
||
|
|
throw new Error(`Failed to read configuration file: ${filePath}`);
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
if (this.loadedConfigs.has(absolutePath)) {
|
||
|
|
const cached = this.loadedConfigs.get(absolutePath);
|
||
|
|
if (Object.keys(cached).length === 0) {
|
||
|
|
throw new Error("Circular inheritance detected");
|
||
|
|
}
|
||
|
|
return cached;
|
||
|
|
}
|
||
|
|
this.loadedConfigs.set(absolutePath, {});
|
||
|
|
try {
|
||
|
|
const content = await fs.readFile(absolutePath, "utf-8");
|
||
|
|
const format = options?.format || this.detectFormat(filePath);
|
||
|
|
let config;
|
||
|
|
if (format === "yaml") {
|
||
|
|
config = await this.parseYAML(content, options);
|
||
|
|
} else {
|
||
|
|
config = this.parseJSON(content, filePath);
|
||
|
|
}
|
||
|
|
if (config && config.extends) {
|
||
|
|
const baseConfigPath = path.resolve(path.dirname(absolutePath), config.extends);
|
||
|
|
const baseConfig = await this.loadFromFile(baseConfigPath, options);
|
||
|
|
delete config.extends;
|
||
|
|
const merged = this.mergeConfigs(baseConfig, config);
|
||
|
|
this.loadedConfigs.set(absolutePath, merged);
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
this.validateConfig(config);
|
||
|
|
this.loadedConfigs.set(absolutePath, config);
|
||
|
|
return config;
|
||
|
|
} catch (error) {
|
||
|
|
this.loadedConfigs.delete(absolutePath);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse JSON content
|
||
|
|
*/
|
||
|
|
parseJSON(content, filePath) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(content);
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof SyntaxError) {
|
||
|
|
throw new Error(`Invalid JSON in configuration file: ${filePath}`);
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse YAML content
|
||
|
|
*/
|
||
|
|
async parseYAML(content, options) {
|
||
|
|
try {
|
||
|
|
const yaml = await import("js-yaml");
|
||
|
|
return yaml.load(content, {
|
||
|
|
strict: options?.strict ?? true,
|
||
|
|
schema: yaml.JSON_SCHEMA
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
throw new ConfigValidationError(`Invalid YAML: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Validate configuration against schema
|
||
|
|
*/
|
||
|
|
validateConfig(config) {
|
||
|
|
if (!config || typeof config !== "object") {
|
||
|
|
throw new Error("Configuration must be an object");
|
||
|
|
}
|
||
|
|
const cfg = config;
|
||
|
|
if (!cfg.version) {
|
||
|
|
throw new Error("Configuration must have a version field");
|
||
|
|
}
|
||
|
|
if (cfg.version !== "1.0") {
|
||
|
|
throw new Error(`Unsupported configuration version: ${cfg.version}`);
|
||
|
|
}
|
||
|
|
if (cfg.mcpServers) {
|
||
|
|
Object.entries(cfg.mcpServers).forEach(([serverName, serverConfig]) => {
|
||
|
|
this.validateMCPServerConfig(serverName, serverConfig);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
if (cfg.globalSettings) {
|
||
|
|
this.validateGlobalSettings(cfg.globalSettings);
|
||
|
|
}
|
||
|
|
if (cfg.tools) {
|
||
|
|
this.validateTools(cfg.tools);
|
||
|
|
}
|
||
|
|
return cfg;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Validate MCP server configuration
|
||
|
|
*/
|
||
|
|
validateMCPServerConfig(serverName, config) {
|
||
|
|
const validPermissions = ["allow", "deny", "ask"];
|
||
|
|
if (!validPermissions.includes(config.defaultPermission)) {
|
||
|
|
throw new Error(
|
||
|
|
`Invalid permission value '${config.defaultPermission}' at mcpServers.${serverName}.defaultPermission`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if (config.tools) {
|
||
|
|
Object.entries(config.tools).forEach(([toolName, permission]) => {
|
||
|
|
const permValue = typeof permission === "object" && permission !== null ? permission.permission : permission;
|
||
|
|
if (permValue && !validPermissions.includes(permValue)) {
|
||
|
|
throw new Error(
|
||
|
|
`Invalid permission value '${permValue}' at mcpServers.${serverName}.tools.${toolName}`
|
||
|
|
);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Validate global settings
|
||
|
|
*/
|
||
|
|
validateGlobalSettings(settings) {
|
||
|
|
if (!settings || typeof settings !== "object") {
|
||
|
|
throw new Error("Global settings must be an object");
|
||
|
|
}
|
||
|
|
const gs = settings;
|
||
|
|
if (gs.defaultToolPermission) {
|
||
|
|
const validPermissions = ["allow", "deny", "ask"];
|
||
|
|
if (!validPermissions.includes(gs.defaultToolPermission)) {
|
||
|
|
throw new Error(`Invalid defaultToolPermission: ${gs.defaultToolPermission}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (gs.permissionMode) {
|
||
|
|
const validModes = ["default", "acceptEdits", "bypassPermissions", "ask"];
|
||
|
|
if (!validModes.includes(gs.permissionMode)) {
|
||
|
|
throw new Error(`Invalid permissionMode: ${gs.permissionMode}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (gs.timeout && (typeof gs.timeout !== "number" || gs.timeout <= 0)) {
|
||
|
|
throw new Error("Timeout must be a positive number");
|
||
|
|
}
|
||
|
|
if (gs.temperature !== void 0) {
|
||
|
|
if (typeof gs.temperature !== "number" || gs.temperature < 0 || gs.temperature > 1) {
|
||
|
|
throw new Error("Temperature must be between 0 and 1");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (gs.maxTokens !== void 0) {
|
||
|
|
const maxTokens = typeof gs.maxTokens === "string" ? parseInt(gs.maxTokens, 10) : gs.maxTokens;
|
||
|
|
if (typeof maxTokens !== "number" || isNaN(maxTokens) || maxTokens <= 0) {
|
||
|
|
throw new Error("maxTokens must be a positive number");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Validate tools configuration
|
||
|
|
*/
|
||
|
|
validateTools(tools) {
|
||
|
|
if (!tools || typeof tools !== "object") {
|
||
|
|
throw new Error("Tools configuration must be an object");
|
||
|
|
}
|
||
|
|
const toolsConfig = tools;
|
||
|
|
if (toolsConfig.allowed && !Array.isArray(toolsConfig.allowed)) {
|
||
|
|
throw new Error("tools.allowed must be an array");
|
||
|
|
}
|
||
|
|
if (toolsConfig.denied && !Array.isArray(toolsConfig.denied)) {
|
||
|
|
throw new Error("tools.denied must be an array");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Merge configuration with options
|
||
|
|
*/
|
||
|
|
mergeWithOptions(config, options, mergeOptions) {
|
||
|
|
const merged = { ...options };
|
||
|
|
const configPrecedence = mergeOptions?.configPrecedence ?? true;
|
||
|
|
if (config.globalSettings) {
|
||
|
|
const gs = config.globalSettings;
|
||
|
|
if (configPrecedence) {
|
||
|
|
if (gs.model !== void 0) merged.model = gs.model;
|
||
|
|
if (gs.timeout !== void 0) merged.timeout = gs.timeout;
|
||
|
|
if (gs.cwd !== void 0) merged.cwd = gs.cwd;
|
||
|
|
if (gs.permissionMode !== void 0) merged.permissionMode = gs.permissionMode;
|
||
|
|
if (gs.env !== void 0) merged.env = { ...merged.env, ...gs.env };
|
||
|
|
if (gs.temperature !== void 0) merged.temperature = gs.temperature;
|
||
|
|
if (gs.maxTokens !== void 0) merged.maxTokens = gs.maxTokens;
|
||
|
|
} else {
|
||
|
|
merged.model = merged.model ?? gs.model;
|
||
|
|
merged.timeout = merged.timeout ?? gs.timeout;
|
||
|
|
merged.cwd = merged.cwd ?? gs.cwd;
|
||
|
|
merged.permissionMode = merged.permissionMode ?? gs.permissionMode;
|
||
|
|
merged.env = { ...gs.env, ...merged.env };
|
||
|
|
merged.temperature = merged.temperature ?? gs.temperature;
|
||
|
|
merged.maxTokens = merged.maxTokens ?? gs.maxTokens;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (config.mcpServers) {
|
||
|
|
const mcpServerPermissions = {};
|
||
|
|
Object.entries(config.mcpServers).forEach(([serverName, serverConfig]) => {
|
||
|
|
const permissionMap = {
|
||
|
|
"allow": "whitelist",
|
||
|
|
"deny": "blacklist",
|
||
|
|
"ask": "ask"
|
||
|
|
};
|
||
|
|
mcpServerPermissions[serverName] = permissionMap[serverConfig.defaultPermission];
|
||
|
|
});
|
||
|
|
merged.mcpServerPermissions = {
|
||
|
|
...merged.mcpServerPermissions,
|
||
|
|
...mcpServerPermissions
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (config.tools) {
|
||
|
|
if (config.tools.allowed) {
|
||
|
|
merged.allowedTools = [
|
||
|
|
...config.tools.allowed,
|
||
|
|
...merged.allowedTools || []
|
||
|
|
];
|
||
|
|
}
|
||
|
|
if (config.tools.denied) {
|
||
|
|
merged.deniedTools = [
|
||
|
|
...config.tools.denied,
|
||
|
|
...merged.deniedTools || []
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Expand environment variables in configuration
|
||
|
|
*/
|
||
|
|
expandEnvironmentVariables(config) {
|
||
|
|
const expanded = JSON.parse(JSON.stringify(config));
|
||
|
|
const expandValue = (value) => {
|
||
|
|
if (typeof value === "string") {
|
||
|
|
return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
||
|
|
const envValue = process.env[varName];
|
||
|
|
if (envValue === void 0) {
|
||
|
|
throw new Error(`Environment variable ${varName} not found`);
|
||
|
|
}
|
||
|
|
return envValue;
|
||
|
|
});
|
||
|
|
} else if (typeof value === "object" && value !== null) {
|
||
|
|
Object.keys(value).forEach((key) => {
|
||
|
|
value[key] = expandValue(value[key]);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return value;
|
||
|
|
};
|
||
|
|
return expandValue(expanded);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Merge two configurations
|
||
|
|
*/
|
||
|
|
mergeConfigs(base, override) {
|
||
|
|
const merged = {
|
||
|
|
version: override.version || base.version
|
||
|
|
};
|
||
|
|
if (base.globalSettings || override.globalSettings) {
|
||
|
|
merged.globalSettings = {
|
||
|
|
...base.globalSettings,
|
||
|
|
...override.globalSettings
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (base.mcpServers || override.mcpServers) {
|
||
|
|
merged.mcpServers = {
|
||
|
|
...base.mcpServers,
|
||
|
|
...override.mcpServers
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (base.tools || override.tools) {
|
||
|
|
merged.tools = {
|
||
|
|
allowed: [
|
||
|
|
...base.tools?.allowed || [],
|
||
|
|
...override.tools?.allowed || []
|
||
|
|
],
|
||
|
|
denied: [
|
||
|
|
...base.tools?.denied || [],
|
||
|
|
...override.tools?.denied || []
|
||
|
|
]
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear loaded configurations cache
|
||
|
|
*/
|
||
|
|
clearCache() {
|
||
|
|
this.loadedConfigs.clear();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get a cached configuration
|
||
|
|
*/
|
||
|
|
getCached(filePath) {
|
||
|
|
return this.loadedConfigs.get(filePath);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/roles/manager.ts
|
||
|
|
import { promises as fs2 } from "fs";
|
||
|
|
import path2 from "path";
|
||
|
|
var RoleManager = class {
|
||
|
|
roles = /* @__PURE__ */ new Map();
|
||
|
|
defaultRole;
|
||
|
|
/**
|
||
|
|
* Add a role definition
|
||
|
|
*/
|
||
|
|
addRole(role) {
|
||
|
|
const validation = this.validateRole(role);
|
||
|
|
if (!validation.valid) {
|
||
|
|
throw new Error(`Invalid role definition: ${validation.errors?.join(", ")}`);
|
||
|
|
}
|
||
|
|
this.roles.set(role.name, role);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get a role by name
|
||
|
|
*/
|
||
|
|
getRole(name) {
|
||
|
|
return this.roles.get(name);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a role exists
|
||
|
|
*/
|
||
|
|
hasRole(name) {
|
||
|
|
return this.roles.has(name);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* List all role names
|
||
|
|
*/
|
||
|
|
listRoles() {
|
||
|
|
return Array.from(this.roles.keys());
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set the default role
|
||
|
|
*/
|
||
|
|
setDefaultRole(name) {
|
||
|
|
if (!this.roles.has(name)) {
|
||
|
|
throw new Error(`Role '${name}' not found`);
|
||
|
|
}
|
||
|
|
this.defaultRole = name;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get the default role
|
||
|
|
*/
|
||
|
|
getDefaultRole() {
|
||
|
|
return this.defaultRole;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Detect file format based on extension
|
||
|
|
*/
|
||
|
|
detectFormat(filePath) {
|
||
|
|
const ext = path2.extname(filePath).toLowerCase();
|
||
|
|
if (ext === ".yaml" || ext === ".yml") return "yaml";
|
||
|
|
return "json";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse JSON content
|
||
|
|
*/
|
||
|
|
parseJSON(content, filePath) {
|
||
|
|
try {
|
||
|
|
return JSON.parse(content);
|
||
|
|
} catch (error) {
|
||
|
|
if (error instanceof SyntaxError) {
|
||
|
|
throw new Error(`Invalid JSON in roles file: ${filePath}`);
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Parse YAML content
|
||
|
|
*/
|
||
|
|
async parseYAML(content, options) {
|
||
|
|
try {
|
||
|
|
const yaml = await import("js-yaml");
|
||
|
|
return yaml.load(content, {
|
||
|
|
strict: options?.strict ?? true,
|
||
|
|
schema: yaml.JSON_SCHEMA
|
||
|
|
});
|
||
|
|
} catch (error) {
|
||
|
|
throw new ConfigValidationError(`Invalid YAML: ${error.message}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load roles from a configuration file
|
||
|
|
*/
|
||
|
|
async loadFromFile(filePath, options) {
|
||
|
|
try {
|
||
|
|
await fs2.access(filePath);
|
||
|
|
} catch (error) {
|
||
|
|
if (error.code === "ENOENT") {
|
||
|
|
throw new Error(`Failed to read roles file: ${filePath}`);
|
||
|
|
}
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
const content = await fs2.readFile(filePath, "utf-8");
|
||
|
|
const format = options?.format || this.detectFormat(filePath);
|
||
|
|
let config;
|
||
|
|
if (format === "yaml") {
|
||
|
|
config = await this.parseYAML(content, options);
|
||
|
|
} else {
|
||
|
|
config = this.parseJSON(content, filePath);
|
||
|
|
}
|
||
|
|
this.loadFromConfig(config);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load roles from configuration object
|
||
|
|
*/
|
||
|
|
loadFromConfig(config) {
|
||
|
|
if (config.version !== "1.0") {
|
||
|
|
throw new Error(`Unsupported roles configuration version: ${config.version}`);
|
||
|
|
}
|
||
|
|
this.roles.clear();
|
||
|
|
Object.entries(config.roles).forEach(([name, roleConfig]) => {
|
||
|
|
const role = {
|
||
|
|
name,
|
||
|
|
...roleConfig
|
||
|
|
};
|
||
|
|
this.addRole(role);
|
||
|
|
});
|
||
|
|
if (config.defaultRole) {
|
||
|
|
this.setDefaultRole(config.defaultRole);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply a role to options
|
||
|
|
*/
|
||
|
|
applyRole(roleName, options, applicationOptions) {
|
||
|
|
const role = this.getRole(roleName);
|
||
|
|
if (!role) {
|
||
|
|
throw new Error(`Role '${roleName}' not found`);
|
||
|
|
}
|
||
|
|
const resolvedRole = this.resolveInheritance(role);
|
||
|
|
return this.applyRoleToOptions(resolvedRole, options, applicationOptions);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resolve role inheritance
|
||
|
|
*/
|
||
|
|
resolveInheritance(role) {
|
||
|
|
const chain = this.getInheritanceChain(role);
|
||
|
|
if (chain.hasCircularDependency) {
|
||
|
|
throw new Error("Circular inheritance detected");
|
||
|
|
}
|
||
|
|
let resolved = {
|
||
|
|
name: role.name,
|
||
|
|
model: role.model,
|
||
|
|
permissions: {}
|
||
|
|
};
|
||
|
|
for (let i = chain.chain.length - 1; i >= 0; i--) {
|
||
|
|
const currentRole = this.getRole(chain.chain[i]);
|
||
|
|
if (!currentRole) continue;
|
||
|
|
resolved = this.mergeRoles(resolved, currentRole);
|
||
|
|
}
|
||
|
|
return resolved;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get inheritance chain for a role
|
||
|
|
*/
|
||
|
|
getInheritanceChain(role, visited = /* @__PURE__ */ new Set()) {
|
||
|
|
const chain = [role.name];
|
||
|
|
if (visited.has(role.name)) {
|
||
|
|
return { chain, hasCircularDependency: true };
|
||
|
|
}
|
||
|
|
visited.add(role.name);
|
||
|
|
if (role.extends) {
|
||
|
|
const parent = this.getRole(role.extends);
|
||
|
|
if (!parent) {
|
||
|
|
throw new Error(`Parent role '${role.extends}' not found`);
|
||
|
|
}
|
||
|
|
const parentChain = this.getInheritanceChain(parent, visited);
|
||
|
|
chain.push(...parentChain.chain);
|
||
|
|
if (parentChain.hasCircularDependency) {
|
||
|
|
return { chain, hasCircularDependency: true };
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { chain, hasCircularDependency: false };
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Merge two roles (child overrides parent)
|
||
|
|
*/
|
||
|
|
mergeRoles(parent, child) {
|
||
|
|
const merged = {
|
||
|
|
name: child.name,
|
||
|
|
model: child.model || parent.model,
|
||
|
|
permissions: {
|
||
|
|
...parent.permissions || {},
|
||
|
|
mode: child.permissions?.mode || parent.permissions?.mode,
|
||
|
|
mcpServers: {
|
||
|
|
...parent.permissions?.mcpServers || {},
|
||
|
|
...child.permissions?.mcpServers || {}
|
||
|
|
},
|
||
|
|
tools: {
|
||
|
|
allowed: [
|
||
|
|
...parent.permissions?.tools?.allowed || [],
|
||
|
|
...child.permissions?.tools?.allowed || []
|
||
|
|
],
|
||
|
|
denied: [
|
||
|
|
...parent.permissions?.tools?.denied || [],
|
||
|
|
...child.permissions?.tools?.denied || []
|
||
|
|
]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
if (parent.description || child.description) {
|
||
|
|
merged.description = child.description || parent.description;
|
||
|
|
}
|
||
|
|
if (parent.promptingTemplate || child.promptingTemplate) {
|
||
|
|
merged.promptingTemplate = child.promptingTemplate || parent.promptingTemplate;
|
||
|
|
}
|
||
|
|
if (parent.systemPrompt || child.systemPrompt) {
|
||
|
|
merged.systemPrompt = child.systemPrompt || parent.systemPrompt;
|
||
|
|
}
|
||
|
|
if (parent.context || child.context) {
|
||
|
|
merged.context = {
|
||
|
|
...parent.context,
|
||
|
|
...child.context
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (parent.metadata || child.metadata) {
|
||
|
|
merged.metadata = {
|
||
|
|
...parent.metadata,
|
||
|
|
...child.metadata
|
||
|
|
};
|
||
|
|
}
|
||
|
|
return merged;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply role definition to options
|
||
|
|
*/
|
||
|
|
applyRoleToOptions(role, options, applicationOptions) {
|
||
|
|
const applied = { ...options };
|
||
|
|
const override = applicationOptions?.override ?? true;
|
||
|
|
if (override || !applied.model) {
|
||
|
|
applied.model = role.model;
|
||
|
|
}
|
||
|
|
if (role.permissions.mode && (override || !applied.permissionMode)) {
|
||
|
|
applied.permissionMode = role.permissions.mode;
|
||
|
|
}
|
||
|
|
if (role.permissions.mcpServers && Object.keys(role.permissions.mcpServers).length > 0) {
|
||
|
|
applied.mcpServerPermissions = {
|
||
|
|
...applied.mcpServerPermissions || {},
|
||
|
|
...role.permissions.mcpServers
|
||
|
|
};
|
||
|
|
}
|
||
|
|
if (role.permissions.tools?.allowed) {
|
||
|
|
if (applicationOptions?.mergeArrays) {
|
||
|
|
applied.allowedTools = [
|
||
|
|
...applied.allowedTools || [],
|
||
|
|
...role.permissions.tools.allowed
|
||
|
|
];
|
||
|
|
} else {
|
||
|
|
applied.allowedTools = role.permissions.tools.allowed;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.permissions.tools?.denied) {
|
||
|
|
if (applicationOptions?.mergeArrays) {
|
||
|
|
applied.deniedTools = [
|
||
|
|
...applied.deniedTools || [],
|
||
|
|
...role.permissions.tools.denied
|
||
|
|
];
|
||
|
|
} else {
|
||
|
|
applied.deniedTools = role.permissions.tools.denied;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.context) {
|
||
|
|
if (role.context.maxTokens && (override || !applied.maxTokens)) {
|
||
|
|
applied.maxTokens = role.context.maxTokens;
|
||
|
|
}
|
||
|
|
if (role.context.temperature !== void 0 && (override || applied.temperature === void 0)) {
|
||
|
|
applied.temperature = role.context.temperature;
|
||
|
|
}
|
||
|
|
if (role.context.additionalContext) {
|
||
|
|
applied.context = [
|
||
|
|
...applied.context || [],
|
||
|
|
...role.context.additionalContext
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.systemPrompt && (override || !applied.systemPrompt)) {
|
||
|
|
applied.systemPrompt = role.systemPrompt;
|
||
|
|
}
|
||
|
|
return applied;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Validate a role definition
|
||
|
|
*/
|
||
|
|
validateRole(role) {
|
||
|
|
const errors = [];
|
||
|
|
const warnings = [];
|
||
|
|
if (!role.name) {
|
||
|
|
errors.push("Role must have a name");
|
||
|
|
}
|
||
|
|
if (!role.model) {
|
||
|
|
errors.push('Role "' + role.name + '" must have a model');
|
||
|
|
} else {
|
||
|
|
const validModels = ["opus", "sonnet", "haiku", "claude-3-opus", "claude-3-sonnet", "claude-3-haiku"];
|
||
|
|
if (!validModels.some((m) => role.model.includes(m))) {
|
||
|
|
errors.push(`Invalid model '${role.model}'`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.permissions?.mode) {
|
||
|
|
const validModes = ["default", "acceptEdits", "bypassPermissions"];
|
||
|
|
if (!validModes.includes(role.permissions.mode)) {
|
||
|
|
errors.push("Invalid permission mode");
|
||
|
|
}
|
||
|
|
if (role.permissions.mode === "bypassPermissions") {
|
||
|
|
warnings.push("bypassPermissions mode may pose security risks");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.context) {
|
||
|
|
if (role.context.temperature !== void 0) {
|
||
|
|
if (role.context.temperature < 0 || role.context.temperature > 1) {
|
||
|
|
errors.push("Temperature must be between 0 and 1");
|
||
|
|
} else if (role.context.temperature > 0.9) {
|
||
|
|
warnings.push(`High temperature (${role.context.temperature}) may produce inconsistent results`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (role.context.maxTokens !== void 0 && role.context.maxTokens <= 0) {
|
||
|
|
errors.push("Max tokens must be positive");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
valid: errors.length === 0,
|
||
|
|
errors: errors.length > 0 ? errors : void 0,
|
||
|
|
warnings: warnings.length > 0 ? warnings : void 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get prompting template with variable interpolation
|
||
|
|
*/
|
||
|
|
getPromptingTemplate(roleName, variables) {
|
||
|
|
const role = this.getRole(roleName);
|
||
|
|
if (!role || !role.promptingTemplate) {
|
||
|
|
throw new Error(`No prompting template found for role '${roleName}'`);
|
||
|
|
}
|
||
|
|
return this.interpolateTemplate(role.promptingTemplate, variables);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get full prompt including system prompt and template
|
||
|
|
*/
|
||
|
|
getFullPrompt(roleName, variables, userPrompt) {
|
||
|
|
const role = this.getRole(roleName);
|
||
|
|
if (!role) {
|
||
|
|
throw new Error(`Role '${roleName}' not found`);
|
||
|
|
}
|
||
|
|
const parts = [];
|
||
|
|
if (role.systemPrompt) {
|
||
|
|
parts.push(role.systemPrompt);
|
||
|
|
}
|
||
|
|
if (role.promptingTemplate) {
|
||
|
|
parts.push(this.interpolateTemplate(role.promptingTemplate, variables));
|
||
|
|
}
|
||
|
|
parts.push(userPrompt);
|
||
|
|
return parts.join("\n\n");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Interpolate template variables
|
||
|
|
*/
|
||
|
|
interpolateTemplate(template, variables) {
|
||
|
|
return template.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
||
|
|
if (!(varName in variables)) {
|
||
|
|
throw new Error(`Missing template variable: ${varName}`);
|
||
|
|
}
|
||
|
|
return variables[varName];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Export configuration
|
||
|
|
*/
|
||
|
|
exportConfig() {
|
||
|
|
const config = {
|
||
|
|
version: "1.0",
|
||
|
|
roles: {}
|
||
|
|
};
|
||
|
|
this.roles.forEach((role, name) => {
|
||
|
|
const { name: _, ...roleConfig } = role;
|
||
|
|
config.roles[name] = roleConfig;
|
||
|
|
});
|
||
|
|
if (this.defaultRole) {
|
||
|
|
config.defaultRole = this.defaultRole;
|
||
|
|
}
|
||
|
|
return config;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear all roles
|
||
|
|
*/
|
||
|
|
clear() {
|
||
|
|
this.roles.clear();
|
||
|
|
this.defaultRole = void 0;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get count of roles
|
||
|
|
*/
|
||
|
|
get size() {
|
||
|
|
return this.roles.size;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/logger.ts
|
||
|
|
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
|
||
|
|
LogLevel2[LogLevel2["ERROR"] = 0] = "ERROR";
|
||
|
|
LogLevel2[LogLevel2["WARN"] = 1] = "WARN";
|
||
|
|
LogLevel2[LogLevel2["INFO"] = 2] = "INFO";
|
||
|
|
LogLevel2[LogLevel2["DEBUG"] = 3] = "DEBUG";
|
||
|
|
LogLevel2[LogLevel2["TRACE"] = 4] = "TRACE";
|
||
|
|
return LogLevel2;
|
||
|
|
})(LogLevel || {});
|
||
|
|
var ConsoleLogger = class {
|
||
|
|
constructor(minLevel = 2 /* INFO */, prefix = "[Claude SDK]") {
|
||
|
|
this.minLevel = minLevel;
|
||
|
|
this.prefix = prefix;
|
||
|
|
}
|
||
|
|
log(entry) {
|
||
|
|
if (entry.level > this.minLevel) return;
|
||
|
|
const timestamp = entry.timestamp.toISOString();
|
||
|
|
const level = LogLevel[entry.level];
|
||
|
|
const prefix = `${timestamp} ${this.prefix} ${level}`;
|
||
|
|
const args = [`${prefix}: ${entry.message}`];
|
||
|
|
if (entry.context && Object.keys(entry.context).length > 0) {
|
||
|
|
args.push(JSON.stringify(entry.context, null, 2));
|
||
|
|
}
|
||
|
|
if (entry.error) {
|
||
|
|
args.push(entry.error);
|
||
|
|
}
|
||
|
|
switch (entry.level) {
|
||
|
|
case 0 /* ERROR */:
|
||
|
|
console.error(...args);
|
||
|
|
break;
|
||
|
|
case 1 /* WARN */:
|
||
|
|
console.warn(...args);
|
||
|
|
break;
|
||
|
|
case 2 /* INFO */:
|
||
|
|
console.info(...args);
|
||
|
|
break;
|
||
|
|
case 3 /* DEBUG */:
|
||
|
|
case 4 /* TRACE */:
|
||
|
|
console.log(...args);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
error(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 0 /* ERROR */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context,
|
||
|
|
error: context?.error instanceof Error ? context.error : void 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
warn(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 1 /* WARN */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
info(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 2 /* INFO */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
debug(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 3 /* DEBUG */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
trace(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 4 /* TRACE */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var JSONLogger = class {
|
||
|
|
constructor(minLevel = 2 /* INFO */, output = console.log) {
|
||
|
|
this.minLevel = minLevel;
|
||
|
|
this.output = output;
|
||
|
|
}
|
||
|
|
log(entry) {
|
||
|
|
if (entry.level > this.minLevel) return;
|
||
|
|
const logObject = {
|
||
|
|
level: LogLevel[entry.level],
|
||
|
|
message: entry.message,
|
||
|
|
timestamp: entry.timestamp.toISOString(),
|
||
|
|
context: entry.context,
|
||
|
|
...entry.error && {
|
||
|
|
error: {
|
||
|
|
message: entry.error.message,
|
||
|
|
stack: entry.error.stack,
|
||
|
|
name: entry.error.name
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
this.output(JSON.stringify(logObject));
|
||
|
|
}
|
||
|
|
error(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 0 /* ERROR */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context,
|
||
|
|
error: context?.error instanceof Error ? context.error : void 0
|
||
|
|
});
|
||
|
|
}
|
||
|
|
warn(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 1 /* WARN */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
info(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 2 /* INFO */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
debug(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 3 /* DEBUG */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
trace(message, context) {
|
||
|
|
this.log({
|
||
|
|
level: 4 /* TRACE */,
|
||
|
|
message,
|
||
|
|
timestamp: /* @__PURE__ */ new Date(),
|
||
|
|
context
|
||
|
|
});
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var MultiLogger = class {
|
||
|
|
constructor(loggers) {
|
||
|
|
this.loggers = loggers;
|
||
|
|
}
|
||
|
|
log(entry) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.log(entry);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
error(message, context) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.error(message, context);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
warn(message, context) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.warn(message, context);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
info(message, context) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.info(message, context);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
debug(message, context) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.debug(message, context);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
trace(message, context) {
|
||
|
|
for (const logger of this.loggers) {
|
||
|
|
try {
|
||
|
|
logger.trace(message, context);
|
||
|
|
} catch {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var NullLogger = class {
|
||
|
|
log(_entry) {
|
||
|
|
}
|
||
|
|
error(_message, _context) {
|
||
|
|
}
|
||
|
|
warn(_message, _context) {
|
||
|
|
}
|
||
|
|
info(_message, _context) {
|
||
|
|
}
|
||
|
|
debug(_message, _context) {
|
||
|
|
}
|
||
|
|
trace(_message, _context) {
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/fluent.ts
|
||
|
|
var QueryBuilder = class _QueryBuilder {
|
||
|
|
options = {};
|
||
|
|
messageHandlers = [];
|
||
|
|
logger;
|
||
|
|
permissionManager;
|
||
|
|
configLoader;
|
||
|
|
roleManager;
|
||
|
|
rolePromptingTemplate;
|
||
|
|
roleTemplateVariables;
|
||
|
|
constructor() {
|
||
|
|
this.permissionManager = new PermissionManager();
|
||
|
|
this.configLoader = new ConfigLoader();
|
||
|
|
this.roleManager = new RoleManager();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set the model to use
|
||
|
|
*/
|
||
|
|
withModel(model) {
|
||
|
|
this.options.model = model;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set allowed tools
|
||
|
|
* Use allowTools() with no arguments to enforce read-only mode (denies all tools)
|
||
|
|
*/
|
||
|
|
allowTools(...tools) {
|
||
|
|
if (tools.length === 0) {
|
||
|
|
const allTools = [
|
||
|
|
"Read",
|
||
|
|
"Write",
|
||
|
|
"Edit",
|
||
|
|
"Bash",
|
||
|
|
"Grep",
|
||
|
|
"Glob",
|
||
|
|
"LS",
|
||
|
|
"MultiEdit",
|
||
|
|
"NotebookRead",
|
||
|
|
"NotebookEdit",
|
||
|
|
"WebFetch",
|
||
|
|
"TodoRead",
|
||
|
|
"TodoWrite",
|
||
|
|
"WebSearch",
|
||
|
|
"Task",
|
||
|
|
"MCPTool"
|
||
|
|
];
|
||
|
|
this.options.deniedTools = allTools;
|
||
|
|
this.options.allowedTools = [];
|
||
|
|
} else {
|
||
|
|
this.options.allowedTools = tools;
|
||
|
|
}
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set denied tools
|
||
|
|
*/
|
||
|
|
denyTools(...tools) {
|
||
|
|
this.options.deniedTools = tools;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set permission mode
|
||
|
|
*/
|
||
|
|
withPermissions(mode) {
|
||
|
|
this.options.permissionMode = mode;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Skip all permissions (shorthand for bypassPermissions)
|
||
|
|
*/
|
||
|
|
skipPermissions() {
|
||
|
|
this.options.permissionMode = "bypassPermissions";
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Accept all edits automatically
|
||
|
|
*/
|
||
|
|
acceptEdits() {
|
||
|
|
this.options.permissionMode = "acceptEdits";
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set working directory
|
||
|
|
*/
|
||
|
|
inDirectory(cwd) {
|
||
|
|
this.options.cwd = cwd;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set environment variables
|
||
|
|
*/
|
||
|
|
withEnv(env) {
|
||
|
|
this.options.env = { ...this.options.env, ...env };
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set timeout in milliseconds
|
||
|
|
*/
|
||
|
|
withTimeout(ms) {
|
||
|
|
this.options.timeout = ms;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set AbortSignal for cancellation
|
||
|
|
*/
|
||
|
|
withSignal(signal) {
|
||
|
|
this.options.signal = signal;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set session ID for continuing an existing conversation
|
||
|
|
*/
|
||
|
|
withSessionId(sessionId) {
|
||
|
|
this.options.sessionId = sessionId;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Enable debug mode
|
||
|
|
*/
|
||
|
|
debug(enabled = true) {
|
||
|
|
this.options.debug = enabled;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add MCP servers
|
||
|
|
*/
|
||
|
|
withMCP(...servers) {
|
||
|
|
this.options.mcpServers = [...this.options.mcpServers || [], ...servers];
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add directory(-ies) to include in the context
|
||
|
|
*/
|
||
|
|
addDirectory(directories) {
|
||
|
|
if (!this.options.addDirectories) {
|
||
|
|
this.options.addDirectories = [];
|
||
|
|
}
|
||
|
|
const dirsToAdd = Array.isArray(directories) ? directories : [directories];
|
||
|
|
this.options.addDirectories.push(...dirsToAdd);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set logger
|
||
|
|
*/
|
||
|
|
withLogger(logger) {
|
||
|
|
this.logger = logger;
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add message handler
|
||
|
|
*/
|
||
|
|
onMessage(handler) {
|
||
|
|
this.messageHandlers.push(handler);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add handler for specific message type
|
||
|
|
*/
|
||
|
|
onAssistant(handler) {
|
||
|
|
this.messageHandlers.push((msg) => {
|
||
|
|
if (msg.type === "assistant") {
|
||
|
|
handler(msg.content);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Add handler for tool usage
|
||
|
|
*/
|
||
|
|
onToolUse(handler) {
|
||
|
|
this.messageHandlers.push((msg) => {
|
||
|
|
if (msg.type === "assistant") {
|
||
|
|
for (const block of msg.content) {
|
||
|
|
if (block.type === "tool_use") {
|
||
|
|
handler({ name: block.name, input: block.input });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set MCP server permission
|
||
|
|
*/
|
||
|
|
withMCPServerPermission(serverName, permission) {
|
||
|
|
this.permissionManager.setMCPServerPermission(serverName, permission);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Set multiple MCP server permissions
|
||
|
|
*/
|
||
|
|
withMCPServerPermissions(permissions) {
|
||
|
|
this.permissionManager.setMCPServerPermissions(permissions);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load configuration from file
|
||
|
|
*/
|
||
|
|
async withConfigFile(filePath) {
|
||
|
|
const config = await this.configLoader.loadFromFile(filePath);
|
||
|
|
this.applyConfig(config);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply configuration object
|
||
|
|
*/
|
||
|
|
withConfig(config) {
|
||
|
|
this.configLoader.validateConfig(config);
|
||
|
|
this.applyConfig(config);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Load roles from file
|
||
|
|
*/
|
||
|
|
async withRolesFile(filePath) {
|
||
|
|
await this.roleManager.loadFromFile(filePath);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
withRole(roleOrName, templateVariables) {
|
||
|
|
if (typeof roleOrName === "string") {
|
||
|
|
const options = this.roleManager.applyRole(roleOrName, this.options);
|
||
|
|
this.options = options;
|
||
|
|
const role = this.roleManager.getRole(roleOrName);
|
||
|
|
if (role?.promptingTemplate) {
|
||
|
|
this.rolePromptingTemplate = role.promptingTemplate;
|
||
|
|
}
|
||
|
|
if (role?.systemPrompt) {
|
||
|
|
this.options.systemPrompt = role.systemPrompt;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
this.roleManager.addRole(roleOrName);
|
||
|
|
const options = this.roleManager.applyRole(roleOrName.name, this.options);
|
||
|
|
this.options = options;
|
||
|
|
if (roleOrName.promptingTemplate) {
|
||
|
|
this.rolePromptingTemplate = roleOrName.promptingTemplate;
|
||
|
|
this.roleTemplateVariables = templateVariables;
|
||
|
|
}
|
||
|
|
if (roleOrName.systemPrompt) {
|
||
|
|
this.options.systemPrompt = roleOrName.systemPrompt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Apply configuration to options
|
||
|
|
*/
|
||
|
|
applyConfig(config) {
|
||
|
|
this.options = this.configLoader.mergeWithOptions(config, this.options);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Execute query and return response parser
|
||
|
|
*/
|
||
|
|
query(prompt) {
|
||
|
|
const finalOptions = this.permissionManager.applyToOptions(this.options);
|
||
|
|
let finalPrompt = prompt;
|
||
|
|
if (this.rolePromptingTemplate && this.roleTemplateVariables) {
|
||
|
|
const templatedPrompt = this.rolePromptingTemplate.replace(
|
||
|
|
/\$\{([^}]+)\}/g,
|
||
|
|
(match, varName) => this.roleTemplateVariables[varName] || match
|
||
|
|
);
|
||
|
|
if (finalOptions.systemPrompt) {
|
||
|
|
finalPrompt = `${finalOptions.systemPrompt}
|
||
|
|
|
||
|
|
${templatedPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
} else {
|
||
|
|
finalPrompt = `${templatedPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
}
|
||
|
|
} else if (finalOptions.systemPrompt) {
|
||
|
|
finalPrompt = `${finalOptions.systemPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
}
|
||
|
|
const parser = new ResponseParser(
|
||
|
|
query(finalPrompt, finalOptions),
|
||
|
|
this.messageHandlers,
|
||
|
|
this.logger
|
||
|
|
);
|
||
|
|
return parser;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Execute query and return raw async generator (for backward compatibility)
|
||
|
|
*/
|
||
|
|
async *queryRaw(prompt) {
|
||
|
|
const finalOptions = this.permissionManager.applyToOptions(this.options);
|
||
|
|
let finalPrompt = prompt;
|
||
|
|
if (this.rolePromptingTemplate && this.roleTemplateVariables) {
|
||
|
|
const templatedPrompt = this.rolePromptingTemplate.replace(
|
||
|
|
/\$\{([^}]+)\}/g,
|
||
|
|
(match, varName) => this.roleTemplateVariables[varName] || match
|
||
|
|
);
|
||
|
|
if (finalOptions.systemPrompt) {
|
||
|
|
finalPrompt = `${finalOptions.systemPrompt}
|
||
|
|
|
||
|
|
${templatedPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
} else {
|
||
|
|
finalPrompt = `${templatedPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
}
|
||
|
|
} else if (finalOptions.systemPrompt) {
|
||
|
|
finalPrompt = `${finalOptions.systemPrompt}
|
||
|
|
|
||
|
|
${prompt}`;
|
||
|
|
}
|
||
|
|
this.logger?.info("Starting query", { prompt: finalPrompt, options: finalOptions });
|
||
|
|
for await (const message of query(finalPrompt, finalOptions)) {
|
||
|
|
this.logger?.debug("Received message", { type: message.type });
|
||
|
|
for (const handler of this.messageHandlers) {
|
||
|
|
try {
|
||
|
|
handler(message);
|
||
|
|
} catch (error) {
|
||
|
|
this.logger?.error("Message handler error", { error });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
yield message;
|
||
|
|
}
|
||
|
|
this.logger?.info("Query completed");
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Static factory method for cleaner syntax
|
||
|
|
*/
|
||
|
|
static create() {
|
||
|
|
return new _QueryBuilder();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function claude() {
|
||
|
|
return new QueryBuilder();
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/streaming/token-stream.ts
|
||
|
|
var TokenStreamImpl = class {
|
||
|
|
controller;
|
||
|
|
snapshot = [];
|
||
|
|
metrics;
|
||
|
|
messageGenerator;
|
||
|
|
tokenGenerator;
|
||
|
|
completionPromise;
|
||
|
|
completionResolve;
|
||
|
|
completionReject;
|
||
|
|
eventHandlers = /* @__PURE__ */ new Map();
|
||
|
|
startTime;
|
||
|
|
constructor(messageGenerator) {
|
||
|
|
this.messageGenerator = messageGenerator;
|
||
|
|
this.controller = new StreamControllerImpl();
|
||
|
|
this.startTime = Date.now();
|
||
|
|
this.metrics = {
|
||
|
|
tokensEmitted: 0,
|
||
|
|
duration: 0,
|
||
|
|
state: "active",
|
||
|
|
averageTokensPerSecond: 0,
|
||
|
|
bytesReceived: 0,
|
||
|
|
lastTokenTime: void 0,
|
||
|
|
pauseCount: 0,
|
||
|
|
totalPauseDuration: 0
|
||
|
|
};
|
||
|
|
this.completionPromise = new Promise((resolve, reject) => {
|
||
|
|
this.completionResolve = resolve;
|
||
|
|
this.completionReject = reject;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async *tokens() {
|
||
|
|
if (this.tokenGenerator) {
|
||
|
|
yield* this.tokenGenerator;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
this.tokenGenerator = this.createTokenGenerator();
|
||
|
|
yield* this.tokenGenerator;
|
||
|
|
}
|
||
|
|
async *createTokenGenerator() {
|
||
|
|
try {
|
||
|
|
let currentTextBlock = "";
|
||
|
|
for await (const message of this.messageGenerator) {
|
||
|
|
await this.controller.checkPause();
|
||
|
|
if (this.controller.isAborted) {
|
||
|
|
throw new Error("Stream aborted");
|
||
|
|
}
|
||
|
|
if (message.type === "assistant") {
|
||
|
|
const assistantMessage = message;
|
||
|
|
for (const block of assistantMessage.content) {
|
||
|
|
await this.controller.checkPause();
|
||
|
|
if (this.controller.isAborted) {
|
||
|
|
throw new Error("Stream aborted");
|
||
|
|
}
|
||
|
|
if (block.type === "text") {
|
||
|
|
const textBlock = block;
|
||
|
|
const text = textBlock.text;
|
||
|
|
const tokens = this.tokenizeText(text, currentTextBlock);
|
||
|
|
for (const token of tokens) {
|
||
|
|
await this.controller.checkPause();
|
||
|
|
if (this.controller.isAborted) {
|
||
|
|
throw new Error("Stream aborted");
|
||
|
|
}
|
||
|
|
const chunk = {
|
||
|
|
token,
|
||
|
|
timestamp: Date.now(),
|
||
|
|
metadata: {
|
||
|
|
messageId: `msg-${Date.now()}`,
|
||
|
|
blockIndex: 0,
|
||
|
|
position: this.metrics.tokensEmitted
|
||
|
|
}
|
||
|
|
};
|
||
|
|
this.snapshot.push(chunk);
|
||
|
|
this.metrics.tokensEmitted++;
|
||
|
|
this.metrics.bytesReceived = (this.metrics.bytesReceived || 0) + new TextEncoder().encode(token).length;
|
||
|
|
this.metrics.lastTokenTime = Date.now();
|
||
|
|
this.updateMetrics();
|
||
|
|
this.emit("token", chunk);
|
||
|
|
yield chunk;
|
||
|
|
}
|
||
|
|
currentTextBlock = text;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
this.metrics.state = "completed";
|
||
|
|
this.updateMetrics();
|
||
|
|
this.emit("complete", this.metrics.state);
|
||
|
|
if (this.completionResolve) this.completionResolve();
|
||
|
|
} catch (error) {
|
||
|
|
this.metrics.state = "error";
|
||
|
|
this.updateMetrics();
|
||
|
|
this.emit("error", error);
|
||
|
|
if (this.completionReject) this.completionReject(error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
tokenizeText(text, previousText) {
|
||
|
|
const tokens = [];
|
||
|
|
if (previousText && text.startsWith(previousText)) {
|
||
|
|
const newText = text.substring(previousText.length);
|
||
|
|
if (newText) {
|
||
|
|
const parts = newText.split(/(\s+|[.,!?;:])/);
|
||
|
|
for (const part of parts) {
|
||
|
|
if (part) {
|
||
|
|
tokens.push(part);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
const parts = text.split(/(\s+|[.,!?;:])/);
|
||
|
|
for (const part of parts) {
|
||
|
|
if (part) {
|
||
|
|
tokens.push(part);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return tokens;
|
||
|
|
}
|
||
|
|
updateMetrics() {
|
||
|
|
this.metrics.duration = Date.now() - this.startTime;
|
||
|
|
if (this.metrics.tokensEmitted > 0 && this.metrics.duration > 0) {
|
||
|
|
this.metrics.averageTokensPerSecond = this.metrics.tokensEmitted / (this.metrics.duration / 1e3);
|
||
|
|
}
|
||
|
|
this.metrics.pauseCount = this.controller.getPauseCount();
|
||
|
|
this.metrics.totalPauseDuration = this.controller.getTotalPauseDuration();
|
||
|
|
}
|
||
|
|
getController() {
|
||
|
|
return this.controller;
|
||
|
|
}
|
||
|
|
getSnapshot() {
|
||
|
|
return [...this.snapshot];
|
||
|
|
}
|
||
|
|
getMetrics() {
|
||
|
|
this.updateMetrics();
|
||
|
|
return { ...this.metrics };
|
||
|
|
}
|
||
|
|
async waitForCompletion() {
|
||
|
|
return this.completionPromise;
|
||
|
|
}
|
||
|
|
on(event, handler) {
|
||
|
|
if (!this.eventHandlers.has(event)) {
|
||
|
|
this.eventHandlers.set(event, /* @__PURE__ */ new Set());
|
||
|
|
}
|
||
|
|
this.eventHandlers.get(event).add(handler);
|
||
|
|
}
|
||
|
|
off(event, handler) {
|
||
|
|
this.eventHandlers.get(event)?.delete(handler);
|
||
|
|
}
|
||
|
|
emit(event, data) {
|
||
|
|
const handlers = this.eventHandlers.get(event);
|
||
|
|
if (handlers) {
|
||
|
|
for (const handler of handlers) {
|
||
|
|
try {
|
||
|
|
handler(data);
|
||
|
|
} catch (error) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (["token", "pause", "resume", "complete", "error"].includes(event)) {
|
||
|
|
this.emit("metrics", this.getMetrics());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
var StreamControllerImpl = class {
|
||
|
|
state = "active";
|
||
|
|
pausePromise;
|
||
|
|
pauseResolve;
|
||
|
|
listeners = /* @__PURE__ */ new Map();
|
||
|
|
pauseStartTime;
|
||
|
|
_abortReason;
|
||
|
|
pauseCount = 0;
|
||
|
|
totalPauseDuration = 0;
|
||
|
|
pause() {
|
||
|
|
if (this.state === "active") {
|
||
|
|
this.state = "paused";
|
||
|
|
this.pauseStartTime = Date.now();
|
||
|
|
this.pauseCount++;
|
||
|
|
this.pausePromise = new Promise((resolve) => {
|
||
|
|
this.pauseResolve = resolve;
|
||
|
|
});
|
||
|
|
this.emit("pause");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
resume() {
|
||
|
|
if (this.state === "paused" && this.pauseResolve) {
|
||
|
|
this.state = "active";
|
||
|
|
this.pauseResolve();
|
||
|
|
this.pausePromise = void 0;
|
||
|
|
this.pauseResolve = void 0;
|
||
|
|
if (this.pauseStartTime) {
|
||
|
|
const pauseDuration = Date.now() - this.pauseStartTime;
|
||
|
|
this.totalPauseDuration += pauseDuration;
|
||
|
|
this.pauseStartTime = void 0;
|
||
|
|
this.emit("resume", { pauseDuration });
|
||
|
|
} else {
|
||
|
|
this.emit("resume");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
abort(reason) {
|
||
|
|
this.state = "aborted";
|
||
|
|
this._abortReason = reason;
|
||
|
|
if (this.pauseResolve) {
|
||
|
|
this.pauseResolve();
|
||
|
|
}
|
||
|
|
this.emit("abort");
|
||
|
|
}
|
||
|
|
getState() {
|
||
|
|
return this.state;
|
||
|
|
}
|
||
|
|
get isPaused() {
|
||
|
|
return this.state === "paused";
|
||
|
|
}
|
||
|
|
get isAborted() {
|
||
|
|
return this.state === "aborted";
|
||
|
|
}
|
||
|
|
get abortReason() {
|
||
|
|
return this._abortReason;
|
||
|
|
}
|
||
|
|
getPauseCount() {
|
||
|
|
return this.pauseCount;
|
||
|
|
}
|
||
|
|
getTotalPauseDuration() {
|
||
|
|
if (this.state === "paused" && this.pauseStartTime) {
|
||
|
|
return this.totalPauseDuration + (Date.now() - this.pauseStartTime);
|
||
|
|
}
|
||
|
|
return this.totalPauseDuration;
|
||
|
|
}
|
||
|
|
async checkPause() {
|
||
|
|
if (this.pausePromise) {
|
||
|
|
await this.pausePromise;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
on(event, listener) {
|
||
|
|
if (!this.listeners.has(event)) {
|
||
|
|
this.listeners.set(event, /* @__PURE__ */ new Set());
|
||
|
|
}
|
||
|
|
this.listeners.get(event).add(listener);
|
||
|
|
}
|
||
|
|
off(event, listener) {
|
||
|
|
this.listeners.get(event)?.delete(listener);
|
||
|
|
}
|
||
|
|
emit(event, _data) {
|
||
|
|
const listeners = this.listeners.get(event);
|
||
|
|
if (listeners) {
|
||
|
|
for (const listener of listeners) {
|
||
|
|
try {
|
||
|
|
listener();
|
||
|
|
} catch (error) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function createTokenStream(messageGenerator) {
|
||
|
|
return new TokenStreamImpl(messageGenerator);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/permissions/tool-permissions.ts
|
||
|
|
var ToolPermissionManager2 = class {
|
||
|
|
globalPermissions;
|
||
|
|
rolePermissions;
|
||
|
|
resolutionLog = [];
|
||
|
|
constructor(options, rolePermissions) {
|
||
|
|
this.globalPermissions = this.initializeGlobalPermissions(options);
|
||
|
|
this.rolePermissions = new Map(
|
||
|
|
rolePermissions ? Object.entries(rolePermissions) : []
|
||
|
|
);
|
||
|
|
}
|
||
|
|
initializeGlobalPermissions(options) {
|
||
|
|
const permissions = /* @__PURE__ */ new Map();
|
||
|
|
if (options.allowedTools) {
|
||
|
|
for (const tool of options.allowedTools) {
|
||
|
|
permissions.set(tool, "allow");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (options.deniedTools) {
|
||
|
|
for (const tool of options.deniedTools) {
|
||
|
|
permissions.set(tool, "deny");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (options.tools) {
|
||
|
|
for (const tool of options.tools) {
|
||
|
|
if (!permissions.has(tool)) {
|
||
|
|
permissions.set(tool, "allow");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return permissions;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Resolve permissions for a specific tool with optional overrides
|
||
|
|
*/
|
||
|
|
async resolvePermission(tool, context, overrides) {
|
||
|
|
const resolution = {
|
||
|
|
tool,
|
||
|
|
permission: "allow",
|
||
|
|
// Default to allow
|
||
|
|
source: "default",
|
||
|
|
context,
|
||
|
|
timestamp: Date.now()
|
||
|
|
};
|
||
|
|
if (overrides) {
|
||
|
|
const overridePermission = await this.checkOverrides(tool, context, overrides);
|
||
|
|
if (overridePermission !== void 0) {
|
||
|
|
resolution.permission = overridePermission;
|
||
|
|
resolution.source = "query";
|
||
|
|
resolution.override = overrides;
|
||
|
|
this.resolutionLog.push(resolution);
|
||
|
|
return resolution;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (overrides?.dynamicPermissions) {
|
||
|
|
const dynamicPermission = await this.checkDynamicPermissions(
|
||
|
|
tool,
|
||
|
|
context,
|
||
|
|
overrides.dynamicPermissions
|
||
|
|
);
|
||
|
|
if (dynamicPermission !== void 0) {
|
||
|
|
resolution.permission = dynamicPermission;
|
||
|
|
resolution.source = "dynamic";
|
||
|
|
resolution.override = overrides;
|
||
|
|
this.resolutionLog.push(resolution);
|
||
|
|
return resolution;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (this.rolePermissions.has(tool)) {
|
||
|
|
resolution.permission = this.rolePermissions.get(tool);
|
||
|
|
resolution.source = "role";
|
||
|
|
this.resolutionLog.push(resolution);
|
||
|
|
return resolution;
|
||
|
|
}
|
||
|
|
if (this.globalPermissions.has(tool)) {
|
||
|
|
resolution.permission = this.globalPermissions.get(tool);
|
||
|
|
resolution.source = "global";
|
||
|
|
this.resolutionLog.push(resolution);
|
||
|
|
return resolution;
|
||
|
|
}
|
||
|
|
this.resolutionLog.push(resolution);
|
||
|
|
return resolution;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check query-level overrides
|
||
|
|
*/
|
||
|
|
async checkOverrides(tool, _context, overrides) {
|
||
|
|
if (overrides.deny?.includes(tool)) {
|
||
|
|
return "deny";
|
||
|
|
}
|
||
|
|
if (overrides.allow?.includes(tool)) {
|
||
|
|
return "allow";
|
||
|
|
}
|
||
|
|
if (overrides.permissions?.[tool]) {
|
||
|
|
return overrides.permissions[tool];
|
||
|
|
}
|
||
|
|
return void 0;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check dynamic permissions
|
||
|
|
*/
|
||
|
|
async checkDynamicPermissions(tool, context, dynamicPermissions) {
|
||
|
|
const dynamicFn = dynamicPermissions[tool];
|
||
|
|
if (dynamicFn) {
|
||
|
|
try {
|
||
|
|
return await dynamicFn(context);
|
||
|
|
} catch (error) {
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return void 0;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get permission resolution history
|
||
|
|
*/
|
||
|
|
getResolutionHistory() {
|
||
|
|
return [...this.resolutionLog];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear resolution history
|
||
|
|
*/
|
||
|
|
clearHistory() {
|
||
|
|
this.resolutionLog = [];
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Update role permissions
|
||
|
|
*/
|
||
|
|
updateRolePermissions(permissions) {
|
||
|
|
this.rolePermissions = new Map(Object.entries(permissions));
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a tool is allowed with current configuration
|
||
|
|
*/
|
||
|
|
async isToolAllowed(tool, context, overrides) {
|
||
|
|
const resolution = await this.resolvePermission(tool, context, overrides);
|
||
|
|
return resolution.permission === "allow";
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get effective permissions for a context
|
||
|
|
*/
|
||
|
|
async getEffectivePermissions(context, overrides) {
|
||
|
|
const allTools = [
|
||
|
|
"Read",
|
||
|
|
"Write",
|
||
|
|
"Edit",
|
||
|
|
"Bash",
|
||
|
|
"Grep",
|
||
|
|
"Glob",
|
||
|
|
"LS",
|
||
|
|
"MultiEdit",
|
||
|
|
"NotebookRead",
|
||
|
|
"NotebookEdit",
|
||
|
|
"WebFetch",
|
||
|
|
"TodoRead",
|
||
|
|
"TodoWrite",
|
||
|
|
"WebSearch",
|
||
|
|
"Task",
|
||
|
|
"MCPTool"
|
||
|
|
];
|
||
|
|
const effectivePermissions = /* @__PURE__ */ new Map();
|
||
|
|
for (const tool of allTools) {
|
||
|
|
const resolution = await this.resolvePermission(tool, context, overrides);
|
||
|
|
effectivePermissions.set(tool, resolution);
|
||
|
|
}
|
||
|
|
return effectivePermissions;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function createPermissionManager(options, rolePermissions) {
|
||
|
|
return new ToolPermissionManager2(options, rolePermissions);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/telemetry/provider-simple.ts
|
||
|
|
var ClaudeTelemetryProvider = class {
|
||
|
|
async initialize(_config) {
|
||
|
|
}
|
||
|
|
getLogger(_name) {
|
||
|
|
throw new Error("Telemetry provider not fully implemented yet");
|
||
|
|
}
|
||
|
|
async shutdown() {
|
||
|
|
}
|
||
|
|
async forceFlush() {
|
||
|
|
}
|
||
|
|
getQueryMetrics() {
|
||
|
|
return {
|
||
|
|
totalQueries: 0,
|
||
|
|
successfulQueries: 0,
|
||
|
|
failedQueries: 0,
|
||
|
|
totalTokens: 0,
|
||
|
|
inputTokens: 0,
|
||
|
|
outputTokens: 0,
|
||
|
|
cacheHits: 0,
|
||
|
|
cacheMisses: 0,
|
||
|
|
averageQueryDuration: 0,
|
||
|
|
p95QueryDuration: 0,
|
||
|
|
p99QueryDuration: 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
getToolMetrics() {
|
||
|
|
return /* @__PURE__ */ new Map();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function createTelemetryProvider() {
|
||
|
|
return new ClaudeTelemetryProvider();
|
||
|
|
}
|
||
|
|
var TelemetryUtils = class {
|
||
|
|
static extractTraceContext(_headers) {
|
||
|
|
return {};
|
||
|
|
}
|
||
|
|
static injectTraceContext(_context, _headers) {
|
||
|
|
}
|
||
|
|
static createNoOpProvider() {
|
||
|
|
return new ClaudeTelemetryProvider();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// src/retry/executor.ts
|
||
|
|
var ClaudeRetryExecutor = class {
|
||
|
|
defaults = {
|
||
|
|
maxAttempts: 3,
|
||
|
|
initialDelay: 1e3,
|
||
|
|
maxDelay: 3e4,
|
||
|
|
multiplier: 2,
|
||
|
|
jitter: true,
|
||
|
|
jitterFactor: 0.1
|
||
|
|
};
|
||
|
|
stats = {
|
||
|
|
totalExecutions: 0,
|
||
|
|
successfulFirstAttempts: 0,
|
||
|
|
successfulRetries: 0,
|
||
|
|
totalFailures: 0,
|
||
|
|
totalRetryAttempts: 0,
|
||
|
|
averageAttempts: 0,
|
||
|
|
maxAttempts: 0
|
||
|
|
};
|
||
|
|
strategy;
|
||
|
|
constructor(options, strategy) {
|
||
|
|
if (options) {
|
||
|
|
this.defaults = { ...this.defaults, ...options };
|
||
|
|
}
|
||
|
|
this.strategy = strategy || new ExponentialBackoffStrategy({
|
||
|
|
multiplier: this.defaults.multiplier,
|
||
|
|
maxDelay: this.defaults.maxDelay,
|
||
|
|
jitter: this.defaults.jitter,
|
||
|
|
jitterFactor: this.defaults.jitterFactor
|
||
|
|
});
|
||
|
|
}
|
||
|
|
async execute(fn, options) {
|
||
|
|
const result = await this.executeWithResult(fn, options);
|
||
|
|
return result.value;
|
||
|
|
}
|
||
|
|
async executeWithResult(fn, options) {
|
||
|
|
const opts = { ...this.defaults, ...options };
|
||
|
|
const errors = [];
|
||
|
|
const startTime = Date.now();
|
||
|
|
this.stats.totalExecutions++;
|
||
|
|
if (opts.signal?.aborted) {
|
||
|
|
throw new Error("Operation aborted before execution");
|
||
|
|
}
|
||
|
|
let totalTimeoutId;
|
||
|
|
let totalTimeoutPromise;
|
||
|
|
if (opts.totalTimeout) {
|
||
|
|
totalTimeoutPromise = new Promise((_, reject) => {
|
||
|
|
totalTimeoutId = setTimeout(() => {
|
||
|
|
reject(new Error(`Total timeout of ${opts.totalTimeout}ms exceeded`));
|
||
|
|
}, opts.totalTimeout);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
try {
|
||
|
|
for (let attempt = 1; attempt <= (opts.maxAttempts || 3); attempt++) {
|
||
|
|
try {
|
||
|
|
if (opts.signal?.aborted) {
|
||
|
|
throw new Error("Operation aborted");
|
||
|
|
}
|
||
|
|
let attemptPromise = fn();
|
||
|
|
if (opts.attemptTimeout) {
|
||
|
|
const timeoutPromise = new Promise((_, reject) => {
|
||
|
|
setTimeout(() => {
|
||
|
|
reject(new Error(`Attempt timeout of ${opts.attemptTimeout}ms exceeded`));
|
||
|
|
}, opts.attemptTimeout);
|
||
|
|
});
|
||
|
|
attemptPromise = Promise.race([attemptPromise, timeoutPromise]);
|
||
|
|
}
|
||
|
|
const value = await (totalTimeoutPromise ? Promise.race([attemptPromise, totalTimeoutPromise]) : attemptPromise);
|
||
|
|
if (attempt === 1) {
|
||
|
|
this.stats.successfulFirstAttempts++;
|
||
|
|
} else {
|
||
|
|
this.stats.successfulRetries++;
|
||
|
|
}
|
||
|
|
this.updateStats(attempt);
|
||
|
|
return {
|
||
|
|
value,
|
||
|
|
attempts: attempt,
|
||
|
|
totalDuration: Date.now() - startTime,
|
||
|
|
errors
|
||
|
|
};
|
||
|
|
} catch (error) {
|
||
|
|
errors.push(error);
|
||
|
|
const shouldRetry = await this.shouldRetry(error, attempt, opts);
|
||
|
|
if (!shouldRetry || attempt === opts.maxAttempts) {
|
||
|
|
this.stats.totalFailures++;
|
||
|
|
this.updateStats(attempt);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
const delay = this.strategy.calculateDelay(attempt, opts.initialDelay || 1e3);
|
||
|
|
if (opts.onRetry) {
|
||
|
|
await opts.onRetry(attempt, error, delay);
|
||
|
|
}
|
||
|
|
await this.sleep(delay, opts.signal);
|
||
|
|
this.stats.totalRetryAttempts++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw errors[errors.length - 1];
|
||
|
|
} finally {
|
||
|
|
if (totalTimeoutId) {
|
||
|
|
clearTimeout(totalTimeoutId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setDefaults(options) {
|
||
|
|
this.defaults = { ...this.defaults, ...options };
|
||
|
|
if (options.multiplier || options.maxDelay || options.jitter || options.jitterFactor) {
|
||
|
|
this.strategy = new ExponentialBackoffStrategy({
|
||
|
|
multiplier: options.multiplier || this.defaults.multiplier,
|
||
|
|
maxDelay: options.maxDelay || this.defaults.maxDelay,
|
||
|
|
jitter: options.jitter ?? this.defaults.jitter,
|
||
|
|
jitterFactor: options.jitterFactor || this.defaults.jitterFactor
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
getStats() {
|
||
|
|
return { ...this.stats };
|
||
|
|
}
|
||
|
|
resetStats() {
|
||
|
|
this.stats = {
|
||
|
|
totalExecutions: 0,
|
||
|
|
successfulFirstAttempts: 0,
|
||
|
|
successfulRetries: 0,
|
||
|
|
totalFailures: 0,
|
||
|
|
totalRetryAttempts: 0,
|
||
|
|
averageAttempts: 0,
|
||
|
|
maxAttempts: 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
async shouldRetry(error, attempt, options) {
|
||
|
|
if (options.shouldRetry) {
|
||
|
|
return options.shouldRetry(error, attempt);
|
||
|
|
}
|
||
|
|
if (!this.strategy.shouldRetry(error, attempt)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
if (options.retryableErrors) {
|
||
|
|
return options.retryableErrors.some((ErrorClass) => error instanceof ErrorClass);
|
||
|
|
}
|
||
|
|
return RetryUtils.isRetryableError(error);
|
||
|
|
}
|
||
|
|
async sleep(ms, signal) {
|
||
|
|
return new Promise((resolve, reject) => {
|
||
|
|
const timeoutId = setTimeout(() => {
|
||
|
|
cleanup();
|
||
|
|
resolve();
|
||
|
|
}, ms);
|
||
|
|
const cleanup = () => {
|
||
|
|
clearTimeout(timeoutId);
|
||
|
|
signal?.removeEventListener("abort", onAbort);
|
||
|
|
};
|
||
|
|
const onAbort = () => {
|
||
|
|
cleanup();
|
||
|
|
reject(new Error("Sleep aborted"));
|
||
|
|
};
|
||
|
|
if (signal?.aborted) {
|
||
|
|
reject(new Error("Sleep aborted"));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
signal?.addEventListener("abort", onAbort);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
updateStats(attempts) {
|
||
|
|
this.stats.maxAttempts = Math.max(this.stats.maxAttempts, attempts);
|
||
|
|
const totalAttempts = this.stats.successfulFirstAttempts + this.stats.successfulRetries * 2 + // At least 2 attempts for retries
|
||
|
|
this.stats.totalFailures * (this.defaults.maxAttempts || 3);
|
||
|
|
this.stats.averageAttempts = totalAttempts / this.stats.totalExecutions;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
function createRetryExecutor(options) {
|
||
|
|
return new ClaudeRetryExecutor(options);
|
||
|
|
}
|
||
|
|
function createExponentialRetryExecutor(options) {
|
||
|
|
return new ClaudeRetryExecutor(options, new ExponentialBackoffStrategy({
|
||
|
|
multiplier: options?.multiplier,
|
||
|
|
maxDelay: options?.maxDelay,
|
||
|
|
jitter: options?.jitter,
|
||
|
|
jitterFactor: options?.jitterFactor
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
function createLinearRetryExecutor(options) {
|
||
|
|
return new ClaudeRetryExecutor(options, new LinearBackoffStrategy({
|
||
|
|
increment: options?.increment,
|
||
|
|
maxDelay: options?.maxDelay,
|
||
|
|
jitter: options?.jitter
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
function createFibonacciRetryExecutor(options) {
|
||
|
|
return new ClaudeRetryExecutor(options, new FibonacciBackoffStrategy({
|
||
|
|
maxDelay: options?.maxDelay,
|
||
|
|
jitter: options?.jitter
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
function withRetry(fn, options) {
|
||
|
|
const executor = createRetryExecutor(options);
|
||
|
|
return () => executor.execute(fn);
|
||
|
|
}
|
||
|
|
|
||
|
|
// src/index.ts
|
||
|
|
async function* query(prompt, options) {
|
||
|
|
const client = new InternalClient(prompt, options);
|
||
|
|
yield* client.processQuery();
|
||
|
|
}
|
||
|
|
export {
|
||
|
|
APIError,
|
||
|
|
API_KEY_SAFETY_WARNING,
|
||
|
|
AbortError,
|
||
|
|
AuthenticationError,
|
||
|
|
BUILTIN_METRICS,
|
||
|
|
CLIConnectionError,
|
||
|
|
CLIJSONDecodeError,
|
||
|
|
CLINotFoundError,
|
||
|
|
CircuitOpenError,
|
||
|
|
ClaudeRetryExecutor,
|
||
|
|
ClaudeSDKError,
|
||
|
|
ClaudeTelemetryProvider,
|
||
|
|
ConfigValidationError,
|
||
|
|
ConnectionRefusedError,
|
||
|
|
ConnectionTimeoutError,
|
||
|
|
ConsoleLogger,
|
||
|
|
ContextLengthExceededError,
|
||
|
|
ERROR_PATTERNS,
|
||
|
|
ErrorDetectionPatterns,
|
||
|
|
ExponentialBackoffStrategy,
|
||
|
|
FibonacciBackoffStrategy,
|
||
|
|
JSONLogger,
|
||
|
|
LinearBackoffStrategy,
|
||
|
|
LogLevel,
|
||
|
|
MCPServerPermissionError,
|
||
|
|
MaxRetriesExceededError,
|
||
|
|
ModelNotAvailableError,
|
||
|
|
MultiLogger,
|
||
|
|
NetworkError,
|
||
|
|
NullLogger,
|
||
|
|
PermissionError,
|
||
|
|
ProcessError,
|
||
|
|
QueryBuilder,
|
||
|
|
RateLimitError,
|
||
|
|
ResponseParser,
|
||
|
|
RetryUtils,
|
||
|
|
SimpleRetryExecutor,
|
||
|
|
StreamAbortedError,
|
||
|
|
StreamPausedError,
|
||
|
|
StreamingError,
|
||
|
|
TelemetryUtils,
|
||
|
|
TimeoutError,
|
||
|
|
TokenStreamImpl,
|
||
|
|
ToolPermissionError,
|
||
|
|
ToolPermissionManager2 as ToolPermissionManager,
|
||
|
|
ValidationError,
|
||
|
|
claude,
|
||
|
|
createExponentialRetryExecutor,
|
||
|
|
createFibonacciRetryExecutor,
|
||
|
|
createLinearRetryExecutor,
|
||
|
|
createPermissionManager,
|
||
|
|
createRetryExecutor,
|
||
|
|
createTelemetryProvider,
|
||
|
|
createTokenStream,
|
||
|
|
createTypedError,
|
||
|
|
detectErrorType,
|
||
|
|
hasResolution,
|
||
|
|
isAPIError,
|
||
|
|
isAuthenticationError,
|
||
|
|
isEnhancedError,
|
||
|
|
isNetworkError,
|
||
|
|
isRateLimitError,
|
||
|
|
isRetryableError,
|
||
|
|
isStreamAbortedError,
|
||
|
|
isTimeoutError,
|
||
|
|
isToolPermissionError,
|
||
|
|
isValidationError,
|
||
|
|
query,
|
||
|
|
withRetry
|
||
|
|
};
|
||
|
|
//# sourceMappingURL=index.js.map
|