@morphllm/morphsdk 0.2.7 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZQEWQ7LJ.js → chunk-4F6DRRCR.js} +10 -10
- package/dist/chunk-4F6DRRCR.js.map +1 -0
- package/dist/{chunk-G4DJ6VSM.js → chunk-7NG7SI6M.js} +6 -2
- package/dist/{chunk-G4DJ6VSM.js.map → chunk-7NG7SI6M.js.map} +1 -1
- package/dist/{chunk-OI5YYE36.js → chunk-AKVAAKRB.js} +7 -60
- package/dist/chunk-AKVAAKRB.js.map +1 -0
- package/dist/{chunk-5COKN3XD.js → chunk-IUCGXKTX.js} +2 -2
- package/dist/{chunk-5COKN3XD.js.map → chunk-IUCGXKTX.js.map} +1 -1
- package/dist/{chunk-4UVEBIDK.js → chunk-OWXIAZUZ.js} +150 -6
- package/dist/chunk-OWXIAZUZ.js.map +1 -0
- package/dist/{chunk-YWS2GRQC.js → chunk-QFUHDWGY.js} +11 -2
- package/dist/{chunk-YWS2GRQC.js.map → chunk-QFUHDWGY.js.map} +1 -1
- package/dist/client.cjs +151 -64
- package/dist/client.cjs.map +1 -1
- package/dist/client.js +4 -4
- package/dist/index.cjs +151 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +10 -10
- package/dist/{tools/modelrouter → modelrouter}/core.cjs +7 -60
- package/dist/modelrouter/core.cjs.map +1 -0
- package/dist/modelrouter/core.js +13 -0
- package/dist/{tools/modelrouter → modelrouter}/index.cjs +7 -60
- package/dist/modelrouter/index.cjs.map +1 -0
- package/dist/modelrouter/index.js +13 -0
- package/dist/{tools/modelrouter → modelrouter}/types.cjs +1 -1
- package/dist/modelrouter/types.cjs.map +1 -0
- package/dist/tools/browser/anthropic.cjs +9 -3
- package/dist/tools/browser/anthropic.cjs.map +1 -1
- package/dist/tools/browser/anthropic.js +1 -1
- package/dist/tools/browser/core.cjs +145 -5
- package/dist/tools/browser/core.cjs.map +1 -1
- package/dist/tools/browser/core.js +1 -1
- package/dist/tools/browser/index.cjs +153 -5
- package/dist/tools/browser/index.cjs.map +1 -1
- package/dist/tools/browser/index.js +9 -1
- package/dist/tools/browser/openai.cjs +9 -3
- package/dist/tools/browser/openai.cjs.map +1 -1
- package/dist/tools/browser/openai.js +1 -1
- package/dist/tools/browser/types.cjs.map +1 -1
- package/dist/tools/browser/vercel.cjs +9 -3
- package/dist/tools/browser/vercel.cjs.map +1 -1
- package/dist/tools/browser/vercel.js +1 -1
- package/dist/tools/codebase_search/index.js +3 -3
- package/dist/tools/fastapply/anthropic.cjs +5 -1
- package/dist/tools/fastapply/anthropic.cjs.map +1 -1
- package/dist/tools/fastapply/anthropic.js +1 -1
- package/dist/tools/fastapply/index.cjs +16 -3
- package/dist/tools/fastapply/index.cjs.map +1 -1
- package/dist/tools/fastapply/index.js +5 -5
- package/dist/tools/fastapply/openai.cjs +10 -1
- package/dist/tools/fastapply/openai.cjs.map +1 -1
- package/dist/tools/fastapply/openai.js +1 -1
- package/dist/tools/fastapply/types.cjs.map +1 -1
- package/dist/tools/fastapply/vercel.cjs +1 -1
- package/dist/tools/fastapply/vercel.cjs.map +1 -1
- package/dist/tools/fastapply/vercel.js +1 -1
- package/dist/tools/index.cjs +16 -3
- package/dist/tools/index.cjs.map +1 -1
- package/dist/tools/index.js +5 -5
- package/package.json +11 -8
- package/dist/chunk-4UVEBIDK.js.map +0 -1
- package/dist/chunk-OI5YYE36.js.map +0 -1
- package/dist/chunk-ZQEWQ7LJ.js.map +0 -1
- package/dist/tools/modelrouter/core.cjs.map +0 -1
- package/dist/tools/modelrouter/core.js +0 -13
- package/dist/tools/modelrouter/index.cjs.map +0 -1
- package/dist/tools/modelrouter/index.js +0 -13
- package/dist/tools/modelrouter/types.cjs.map +0 -1
- /package/dist/{tools/modelrouter → modelrouter}/core.js.map +0 -0
- /package/dist/{tools/modelrouter → modelrouter}/index.js.map +0 -0
- /package/dist/{tools/modelrouter → modelrouter}/types.js +0 -0
- /package/dist/{tools/modelrouter → modelrouter}/types.js.map +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnthropicRouter,
|
|
3
|
+
GeminiRouter,
|
|
4
|
+
OpenAIRouter
|
|
5
|
+
} from "../chunk-AKVAAKRB.js";
|
|
6
|
+
import "../chunk-4VWJFZVS.js";
|
|
7
|
+
import "../chunk-PZ5AY32C.js";
|
|
8
|
+
export {
|
|
9
|
+
AnthropicRouter,
|
|
10
|
+
GeminiRouter,
|
|
11
|
+
OpenAIRouter
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=core.js.map
|
|
@@ -17,7 +17,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
17
17
|
};
|
|
18
18
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
19
|
|
|
20
|
-
//
|
|
20
|
+
// modelrouter/index.ts
|
|
21
21
|
var modelrouter_exports = {};
|
|
22
22
|
__export(modelrouter_exports, {
|
|
23
23
|
AnthropicRouter: () => AnthropicRouter,
|
|
@@ -100,57 +100,13 @@ function sleep(ms) {
|
|
|
100
100
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
//
|
|
103
|
+
// modelrouter/core.ts
|
|
104
104
|
var DEFAULT_CONFIG = {
|
|
105
105
|
apiUrl: "https://api.morphllm.com",
|
|
106
|
-
timeout:
|
|
107
|
-
//
|
|
106
|
+
timeout: 5e3,
|
|
107
|
+
// 5 seconds (responses typically <500ms)
|
|
108
108
|
debug: false
|
|
109
109
|
};
|
|
110
|
-
var MODEL_MAPPINGS = {
|
|
111
|
-
openai: {
|
|
112
|
-
balanced: {
|
|
113
|
-
easy: "gpt-5-mini",
|
|
114
|
-
medium: "gpt-5-low",
|
|
115
|
-
hard: "gpt-5-medium",
|
|
116
|
-
"needs-info": "gpt-5-mini"
|
|
117
|
-
},
|
|
118
|
-
aggressive: {
|
|
119
|
-
easy: "gpt-5-low",
|
|
120
|
-
medium: "gpt-5-medium",
|
|
121
|
-
hard: "gpt-5-high",
|
|
122
|
-
"needs-info": "gpt-5-mini"
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
anthropic: {
|
|
126
|
-
balanced: {
|
|
127
|
-
easy: "claude-4.5-haiku",
|
|
128
|
-
medium: "claude-4.5-haiku",
|
|
129
|
-
hard: "claude-4.5-sonnet",
|
|
130
|
-
"needs-info": "claude-4.5-haiku"
|
|
131
|
-
},
|
|
132
|
-
aggressive: {
|
|
133
|
-
easy: "claude-4.5-haiku",
|
|
134
|
-
medium: "claude-4.5-sonnet",
|
|
135
|
-
hard: "claude-4.5-sonnet",
|
|
136
|
-
"needs-info": "claude-4.5-haiku"
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
gemini: {
|
|
140
|
-
balanced: {
|
|
141
|
-
easy: "gemini-2.5-flash",
|
|
142
|
-
medium: "gemini-2.5-flash",
|
|
143
|
-
hard: "gemini-2.5-pro",
|
|
144
|
-
"needs-info": "gemini-2.5-flash"
|
|
145
|
-
},
|
|
146
|
-
aggressive: {
|
|
147
|
-
easy: "gemini-2.5-flash",
|
|
148
|
-
medium: "gemini-2.5-pro",
|
|
149
|
-
hard: "gemini-2.5-pro",
|
|
150
|
-
"needs-info": "gemini-2.5-flash"
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
110
|
var BaseRouter = class {
|
|
155
111
|
config;
|
|
156
112
|
provider;
|
|
@@ -164,13 +120,6 @@ var BaseRouter = class {
|
|
|
164
120
|
retryConfig: config.retryConfig
|
|
165
121
|
};
|
|
166
122
|
}
|
|
167
|
-
/**
|
|
168
|
-
* Map backend complexity classification to actual model name
|
|
169
|
-
*/
|
|
170
|
-
mapComplexityToModel(complexity, mode) {
|
|
171
|
-
const mapping = MODEL_MAPPINGS[this.provider][mode];
|
|
172
|
-
return mapping[complexity];
|
|
173
|
-
}
|
|
174
123
|
/**
|
|
175
124
|
* Select the optimal model for a given input and mode
|
|
176
125
|
*/
|
|
@@ -182,7 +131,7 @@ var BaseRouter = class {
|
|
|
182
131
|
"Morph API key is required. Set MORPH_API_KEY environment variable or pass apiKey in config."
|
|
183
132
|
);
|
|
184
133
|
}
|
|
185
|
-
const url = `${this.config.apiUrl}/router/${this.provider}`;
|
|
134
|
+
const url = `${this.config.apiUrl}/v1/router/${this.provider}`;
|
|
186
135
|
const payload = {
|
|
187
136
|
input: input.input,
|
|
188
137
|
mode
|
|
@@ -218,13 +167,11 @@ var BaseRouter = class {
|
|
|
218
167
|
);
|
|
219
168
|
}
|
|
220
169
|
const apiResult = await response.json();
|
|
221
|
-
const actualModel = this.mapComplexityToModel(apiResult.model, mode);
|
|
222
170
|
const result = {
|
|
223
|
-
model:
|
|
224
|
-
reasoning: apiResult.reasoning
|
|
171
|
+
model: apiResult.model
|
|
225
172
|
};
|
|
226
173
|
if (this.config.debug) {
|
|
227
|
-
console.log(`[ModelRouter]
|
|
174
|
+
console.log(`[ModelRouter] Selected model: ${apiResult.model}, Confidence: ${apiResult.confidence?.toFixed(3)}`);
|
|
228
175
|
}
|
|
229
176
|
return result;
|
|
230
177
|
} catch (error) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../modelrouter/index.ts","../../tools/utils/resilience.ts","../../modelrouter/core.ts"],"sourcesContent":["/**\n * Intelligent model routing based on task complexity\n * \n * @module modelrouter\n */\n\nexport { OpenAIRouter, AnthropicRouter, GeminiRouter } from './core.js';\nexport type {\n RouterConfig,\n RouterInput,\n RouterResult,\n ComplexityLevel,\n RouterMode,\n Provider,\n} from './types.js';\n","/**\n * Resilience utilities for retry logic and timeout handling\n */\n\nexport interface RetryConfig {\n maxRetries?: number; // Default: 3\n initialDelay?: number; // Default: 1000ms\n maxDelay?: number; // Default: 30000ms\n backoffMultiplier?: number; // Default: 2\n retryableErrors?: string[]; // Default: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND']\n onRetry?: (attempt: number, error: Error) => void;\n}\n\nconst DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'onRetry'>> = {\n maxRetries: 3,\n initialDelay: 1000,\n maxDelay: 30000,\n backoffMultiplier: 2,\n retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'],\n};\n\n/**\n * Retry a fetch request with exponential backoff\n * \n * @param url - Request URL\n * @param options - Fetch options\n * @param retryConfig - Retry configuration\n * @returns Response from fetch\n * \n * @example\n * ```typescript\n * const response = await fetchWithRetry(\n * 'https://api.example.com/data',\n * { method: 'POST', body: JSON.stringify(data) },\n * { maxRetries: 5, initialDelay: 500 }\n * );\n * ```\n */\nexport async function fetchWithRetry(\n url: string,\n options: RequestInit,\n retryConfig: RetryConfig = {}\n): Promise<Response> {\n const {\n maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,\n initialDelay = DEFAULT_RETRY_CONFIG.initialDelay,\n maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,\n backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,\n retryableErrors = DEFAULT_RETRY_CONFIG.retryableErrors,\n onRetry,\n } = retryConfig;\n\n let lastError: Error | null = null;\n let delay = initialDelay;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, options);\n \n // Retry on 429 (rate limit) or 503 (service unavailable)\n if (response.status === 429 || response.status === 503) {\n if (attempt < maxRetries) {\n // Check for Retry-After header\n const retryAfter = response.headers.get('Retry-After');\n const waitTime = retryAfter \n ? parseInt(retryAfter) * 1000 \n : Math.min(delay, maxDelay);\n \n const error = new Error(`HTTP ${response.status}: Retrying after ${waitTime}ms`);\n if (onRetry) {\n onRetry(attempt + 1, error);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n continue;\n }\n }\n\n return response;\n } catch (error) {\n lastError = error as Error;\n \n // Check if error is retryable\n const isRetryable = retryableErrors.some(errType => \n lastError?.message?.includes(errType)\n );\n\n if (!isRetryable || attempt === maxRetries) {\n throw lastError;\n }\n\n // Exponential backoff\n const waitTime = Math.min(delay, maxDelay);\n if (onRetry) {\n onRetry(attempt + 1, lastError);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n }\n }\n\n throw lastError || new Error('Max retries exceeded');\n}\n\n/**\n * Add timeout to any promise\n * \n * @param promise - Promise to wrap with timeout\n * @param timeoutMs - Timeout in milliseconds\n * @param errorMessage - Optional custom error message\n * @returns Promise that rejects if timeout is reached\n * \n * @example\n * ```typescript\n * const result = await withTimeout(\n * fetchData(),\n * 5000,\n * 'Data fetch timed out'\n * );\n * ```\n */\nexport async function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage?: string\n): Promise<T> {\n let timeoutId: NodeJS.Timeout | number;\n \n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(errorMessage || `Operation timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n try {\n const result = await Promise.race([promise, timeoutPromise]);\n clearTimeout(timeoutId!);\n return result;\n } catch (error) {\n clearTimeout(timeoutId!);\n throw error;\n }\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Unified error type for all tools\n */\nexport class MorphError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public retryable: boolean = false\n ) {\n super(message);\n this.name = 'MorphError';\n }\n}\n\n\n","/**\n * Core implementation for intelligent model routing\n */\n\nimport { fetchWithRetry, withTimeout } from '../tools/utils/resilience.js';\nimport type {\n RouterConfig,\n RouterInput,\n RouterResult,\n ComplexityLevel,\n RouterMode,\n Provider,\n} from './types.js';\n\nconst DEFAULT_CONFIG = {\n apiUrl: 'https://api.morphllm.com',\n timeout: 5000, // 5 seconds (responses typically <500ms)\n debug: false,\n};\n\nabstract class BaseRouter {\n protected config: Required<Omit<RouterConfig, 'apiKey' | 'retryConfig'>> & Pick<RouterConfig, 'apiKey' | 'retryConfig'>;\n protected provider: Provider;\n\n constructor(provider: Provider, config: RouterConfig = {}) {\n this.provider = provider;\n this.config = {\n apiKey: config.apiKey,\n apiUrl: config.apiUrl || DEFAULT_CONFIG.apiUrl,\n timeout: config.timeout || DEFAULT_CONFIG.timeout,\n debug: config.debug || DEFAULT_CONFIG.debug,\n retryConfig: config.retryConfig,\n };\n }\n\n /**\n * Select the optimal model for a given input and mode\n */\n async selectModel(input: RouterInput): Promise<RouterResult> {\n const mode = input.mode || 'balanced';\n const apiKey = this.config.apiKey || process.env.MORPH_API_KEY;\n\n if (!apiKey) {\n throw new Error(\n 'Morph API key is required. Set MORPH_API_KEY environment variable or pass apiKey in config.'\n );\n }\n\n const url = `${this.config.apiUrl}/v1/router/${this.provider}`;\n const payload = {\n input: input.input,\n mode,\n };\n\n if (this.config.debug) {\n console.log(`[ModelRouter] Requesting ${this.provider} model selection:`, {\n mode,\n inputLength: input.input.length,\n });\n }\n\n try {\n const fetchPromise = fetchWithRetry(\n url,\n {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(payload),\n },\n this.config.retryConfig\n );\n\n const response = await withTimeout(\n fetchPromise,\n this.config.timeout,\n `Router API request timed out after ${this.config.timeout}ms`\n );\n\n if (!response.ok) {\n const errorText = await response.text();\n throw new Error(\n `Router API error (${response.status}): ${errorText || response.statusText}`\n );\n }\n\n const apiResult: { model: string; confidence?: number } = await response.json();\n\n const result: RouterResult = {\n model: apiResult.model,\n };\n\n if (this.config.debug) {\n console.log(`[ModelRouter] Selected model: ${apiResult.model}, Confidence: ${apiResult.confidence?.toFixed(3)}`);\n }\n\n return result;\n } catch (error) {\n if (this.config.debug) {\n console.error(`[ModelRouter] Error selecting model:`, error);\n }\n throw error;\n }\n }\n}\n\n/**\n * OpenAI model router for GPT-5 series\n */\nexport class OpenAIRouter extends BaseRouter {\n constructor(config: RouterConfig = {}) {\n super('openai', config);\n }\n\n /**\n * Select optimal GPT-5 model\n * \n * @param input - User input and mode\n * @returns Selected model name (gpt-5-mini | gpt-5-low | gpt-5-medium | gpt-5-high)\n */\n async selectModel(input: RouterInput): Promise<RouterResult> {\n return super.selectModel(input);\n }\n}\n\n/**\n * Anthropic model router for Claude 4.5 series\n */\nexport class AnthropicRouter extends BaseRouter {\n constructor(config: RouterConfig = {}) {\n super('anthropic', config);\n }\n\n /**\n * Select optimal Claude model\n * \n * @param input - User input and mode\n * @returns Selected model name (claude-4.5-haiku | claude-4.5-sonnet)\n */\n async selectModel(input: RouterInput): Promise<RouterResult> {\n return super.selectModel(input);\n }\n}\n\n/**\n * Google Gemini model router\n */\nexport class GeminiRouter extends BaseRouter {\n constructor(config: RouterConfig = {}) {\n super('gemini', config);\n }\n\n /**\n * Select optimal Gemini model\n * \n * @param input - User input and mode\n * @returns Selected model name (gemini-2.5-flash | gemini-2.5-pro)\n */\n async selectModel(input: RouterInput): Promise<RouterResult> {\n return super.selectModel(input);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AnthropicRouter,
|
|
3
|
+
GeminiRouter,
|
|
4
|
+
OpenAIRouter
|
|
5
|
+
} from "../chunk-AKVAAKRB.js";
|
|
6
|
+
import "../chunk-4VWJFZVS.js";
|
|
7
|
+
import "../chunk-PZ5AY32C.js";
|
|
8
|
+
export {
|
|
9
|
+
AnthropicRouter,
|
|
10
|
+
GeminiRouter,
|
|
11
|
+
OpenAIRouter
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -13,7 +13,7 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
13
13
|
};
|
|
14
14
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// modelrouter/types.ts
|
|
17
17
|
var types_exports = {};
|
|
18
18
|
module.exports = __toCommonJS(types_exports);
|
|
19
19
|
//# sourceMappingURL=types.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../modelrouter/types.ts"],"sourcesContent":["/**\n * Model router types for intelligent model selection\n */\n\nexport type ComplexityLevel = 'easy' | 'medium' | 'hard' | 'needs-info';\nexport type RouterMode = 'balanced' | 'aggressive';\nexport type Provider = 'openai' | 'anthropic' | 'gemini';\n\nexport interface RouterConfig {\n /** Morph API key (defaults to MORPH_API_KEY env var) */\n apiKey?: string;\n /** Router API URL (defaults to https://api.morphllm.com) */\n apiUrl?: string;\n /** Request timeout in milliseconds (default: 10000) */\n timeout?: number;\n /** Enable debug logging */\n debug?: boolean;\n /** Retry configuration */\n retryConfig?: {\n maxRetries?: number;\n initialDelay?: number;\n maxDelay?: number;\n backoffMultiplier?: number;\n };\n}\n\nexport interface RouterInput {\n /** User input or task description */\n input: string;\n /** Routing mode: balanced (cheaper models) or aggressive (more capable models) */\n mode?: RouterMode;\n}\n\nexport interface RouterResult {\n /** Selected model name */\n model: string;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|
|
@@ -111,10 +111,16 @@ async function executeBrowserTask(input, config = {}) {
|
|
|
111
111
|
const timeout = config.timeout || DEFAULT_CONFIG.timeout;
|
|
112
112
|
const debug = config.debug || false;
|
|
113
113
|
if (!input.task || input.task.trim().length === 0) {
|
|
114
|
-
return {
|
|
114
|
+
return {
|
|
115
|
+
success: false,
|
|
116
|
+
error: 'Task description is required. Example: "Go to example.com and click the login button"'
|
|
117
|
+
};
|
|
115
118
|
}
|
|
116
119
|
if (input.max_steps !== void 0 && (input.max_steps < 1 || input.max_steps > 50)) {
|
|
117
|
-
return {
|
|
120
|
+
return {
|
|
121
|
+
success: false,
|
|
122
|
+
error: "max_steps must be between 1 and 50. Use more steps for complex multi-page flows."
|
|
123
|
+
};
|
|
118
124
|
}
|
|
119
125
|
if (debug) {
|
|
120
126
|
console.log(`[Browser] Task: "${input.task.slice(0, 60)}..." url=${input.url || "none"} maxSteps=${input.max_steps ?? 10}`);
|
|
@@ -167,7 +173,7 @@ async function executeBrowserTask(input, config = {}) {
|
|
|
167
173
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
|
|
168
174
|
return {
|
|
169
175
|
success: false,
|
|
170
|
-
error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running.`
|
|
176
|
+
error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running and accessible. For local dev, set MORPH_ENVIRONMENT=DEV.`
|
|
171
177
|
};
|
|
172
178
|
}
|
|
173
179
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../tools/browser/anthropic.ts","../../../tools/utils/resilience.ts","../../../tools/browser/core.ts","../../../tools/browser/prompts.ts"],"sourcesContent":["/**\n * Anthropic SDK adapter for browser automation tool\n */\n\nimport type { Tool } from '@anthropic-ai/sdk/resources/messages.mjs';\nimport { executeBrowserTask } from './core.js';\nimport type {\n BrowserConfig,\n BrowserTaskInput,\n BrowserTaskResult,\n} from './types.js';\nimport { BROWSER_TOOL_DESCRIPTION, BROWSER_SYSTEM_PROMPT } from './prompts.js';\n\n/**\n * Anthropic tool definition for browser automation\n */\nexport const browserTool: Tool = {\n name: 'browser_task',\n description: BROWSER_TOOL_DESCRIPTION,\n input_schema: {\n type: 'object',\n properties: {\n task: {\n type: 'string',\n description: 'Natural language description of what to do (e.g., \"Test checkout flow for buying a pineapple\")',\n },\n url: {\n type: 'string',\n description: 'Starting URL (e.g., https://3000-xyz.e2b.dev). Required if navigating to a specific page.',\n },\n max_steps: {\n type: 'number',\n description: 'Maximum number of browser actions to take (1-50). Default: 10. Use 15-30 for complex flows.',\n default: 10,\n },\n region: {\n type: 'string',\n enum: ['sfo', 'lon'],\n description: 'Browserless region: sfo (US West Coast) or lon (Europe). Default: sfo.',\n default: 'sfo',\n },\n },\n required: ['task'],\n },\n};\n\n/**\n * Format browser task result for Anthropic tool result\n * \n * @param result - Browser task result\n * @returns Formatted string for tool result\n */\nfunction formatResult(result: BrowserTaskResult): string {\n if (result.success) {\n const parts = [\n '✅ Browser task completed successfully',\n `Steps taken: ${result.steps_taken ?? 0}`,\n result.execution_time_ms ? `Execution time: ${result.execution_time_ms}ms` : null,\n '',\n 'Result:',\n result.result || 'Task completed',\n ];\n return parts.filter(Boolean).join('\\n');\n }\n\n return `❌ Browser task failed: ${result.error || 'Unknown error'}`;\n}\n\n/**\n * Create a configured browser tool with execute and formatResult methods\n * \n * @param config - Browser worker configuration\n * @returns Tool definition with execute and formatResult methods\n * \n * @example\n * ```typescript\n * import Anthropic from '@anthropic-ai/sdk';\n * import { createBrowserTool } from 'morphsdk/tools/browser/anthropic';\n * \n * const tool = createBrowserTool({\n * apiKey: process.env.MORPH_API_KEY,\n * timeout: 180000\n * });\n * \n * const client = new Anthropic();\n * \n * const response = await client.messages.create({\n * model: 'claude-sonnet-4-5-20250929',\n * tools: [tool], // tool itself is the Tool definition\n * messages: [{\n * role: 'user',\n * content: 'Test the checkout flow at https://3000-abc.e2b.dev'\n * }]\n * });\n * \n * // Execute and format\n * const result = await tool.execute(toolUseBlock.input);\n * const formatted = tool.formatResult(result);\n * ```\n */\nexport function createBrowserTool(config?: BrowserConfig) {\n return Object.assign({}, browserTool, {\n execute: async (input: BrowserTaskInput): Promise<BrowserTaskResult> => {\n return executeBrowserTask(input, config);\n },\n formatResult: (result: BrowserTaskResult): string => {\n return formatResult(result);\n },\n getSystemPrompt: (): string => {\n return BROWSER_SYSTEM_PROMPT;\n },\n });\n}\n\n","/**\n * Resilience utilities for retry logic and timeout handling\n */\n\nexport interface RetryConfig {\n maxRetries?: number; // Default: 3\n initialDelay?: number; // Default: 1000ms\n maxDelay?: number; // Default: 30000ms\n backoffMultiplier?: number; // Default: 2\n retryableErrors?: string[]; // Default: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND']\n onRetry?: (attempt: number, error: Error) => void;\n}\n\nconst DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'onRetry'>> = {\n maxRetries: 3,\n initialDelay: 1000,\n maxDelay: 30000,\n backoffMultiplier: 2,\n retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'],\n};\n\n/**\n * Retry a fetch request with exponential backoff\n * \n * @param url - Request URL\n * @param options - Fetch options\n * @param retryConfig - Retry configuration\n * @returns Response from fetch\n * \n * @example\n * ```typescript\n * const response = await fetchWithRetry(\n * 'https://api.example.com/data',\n * { method: 'POST', body: JSON.stringify(data) },\n * { maxRetries: 5, initialDelay: 500 }\n * );\n * ```\n */\nexport async function fetchWithRetry(\n url: string,\n options: RequestInit,\n retryConfig: RetryConfig = {}\n): Promise<Response> {\n const {\n maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,\n initialDelay = DEFAULT_RETRY_CONFIG.initialDelay,\n maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,\n backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,\n retryableErrors = DEFAULT_RETRY_CONFIG.retryableErrors,\n onRetry,\n } = retryConfig;\n\n let lastError: Error | null = null;\n let delay = initialDelay;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, options);\n \n // Retry on 429 (rate limit) or 503 (service unavailable)\n if (response.status === 429 || response.status === 503) {\n if (attempt < maxRetries) {\n // Check for Retry-After header\n const retryAfter = response.headers.get('Retry-After');\n const waitTime = retryAfter \n ? parseInt(retryAfter) * 1000 \n : Math.min(delay, maxDelay);\n \n const error = new Error(`HTTP ${response.status}: Retrying after ${waitTime}ms`);\n if (onRetry) {\n onRetry(attempt + 1, error);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n continue;\n }\n }\n\n return response;\n } catch (error) {\n lastError = error as Error;\n \n // Check if error is retryable\n const isRetryable = retryableErrors.some(errType => \n lastError?.message?.includes(errType)\n );\n\n if (!isRetryable || attempt === maxRetries) {\n throw lastError;\n }\n\n // Exponential backoff\n const waitTime = Math.min(delay, maxDelay);\n if (onRetry) {\n onRetry(attempt + 1, lastError);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n }\n }\n\n throw lastError || new Error('Max retries exceeded');\n}\n\n/**\n * Add timeout to any promise\n * \n * @param promise - Promise to wrap with timeout\n * @param timeoutMs - Timeout in milliseconds\n * @param errorMessage - Optional custom error message\n * @returns Promise that rejects if timeout is reached\n * \n * @example\n * ```typescript\n * const result = await withTimeout(\n * fetchData(),\n * 5000,\n * 'Data fetch timed out'\n * );\n * ```\n */\nexport async function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage?: string\n): Promise<T> {\n let timeoutId: NodeJS.Timeout | number;\n \n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(errorMessage || `Operation timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n try {\n const result = await Promise.race([promise, timeoutPromise]);\n clearTimeout(timeoutId!);\n return result;\n } catch (error) {\n clearTimeout(timeoutId!);\n throw error;\n }\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Unified error type for all tools\n */\nexport class MorphError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public retryable: boolean = false\n ) {\n super(message);\n this.name = 'MorphError';\n }\n}\n\n\n","/**\n * Core implementation for browser automation tasks\n */\n\nimport { fetchWithRetry, withTimeout } from '../utils/resilience.js';\nimport type {\n BrowserConfig,\n BrowserTaskInput,\n BrowserTaskInputWithSchema,\n BrowserTaskResult,\n BrowserTaskWithPromise,\n BrowserTaskWithPromiseAndSchema,\n RecordingStatus,\n ErrorsResponse,\n} from './types.js';\n\nconst DEFAULT_CONFIG = {\n apiUrl: process.env.MORPH_ENVIRONMENT === 'DEV' \n ? 'http://localhost:8000'\n : 'https://browser.morphllm.com',\n timeout: 120000, // 2 minutes for complex tasks\n debug: false,\n};\n\n/**\n * BrowserClient class for easier usage with instance configuration\n */\nexport class BrowserClient {\n private config: BrowserConfig;\n\n constructor(config: BrowserConfig = {}) {\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n }\n\n /**\n * Execute a browser automation task\n */\n async execute(input: BrowserTaskInput): Promise<BrowserTaskResult> {\n return executeBrowserTask(input, this.config);\n }\n\n async createTask(input: BrowserTaskInput): Promise<BrowserTaskWithPromise>;\n async createTask<T>(input: BrowserTaskInputWithSchema<T>): Promise<BrowserTaskWithPromiseAndSchema<T>>;\n async createTask<T>(\n input: BrowserTaskInput | BrowserTaskInputWithSchema<T>\n ): Promise<BrowserTaskWithPromise | BrowserTaskWithPromiseAndSchema<T>> {\n if ('schema' in input) {\n const taskInput: BrowserTaskInput = {\n ...input,\n structured_output: stringifyStructuredOutput(input.schema),\n };\n const result = await executeBrowserTask(taskInput, this.config);\n return wrapTaskResponseWithSchema(result, this.config, input.schema);\n } else {\n const result = await executeBrowserTask(input, this.config);\n return wrapTaskResponse(result, this.config);\n }\n }\n\n /**\n * Execute task with recording and wait for video to be ready\n */\n async executeWithRecording(\n input: BrowserTaskInput & { record_video: true }\n ): Promise<BrowserTaskResult & { recording?: RecordingStatus }> {\n return executeWithRecording(input, this.config);\n }\n\n /**\n * Get recording status and URLs\n */\n async getRecording(recordingId: string): Promise<RecordingStatus> {\n return getRecording(recordingId, this.config);\n }\n\n /**\n * Wait for recording to complete with automatic polling\n */\n async waitForRecording(\n recordingId: string,\n options?: { timeout?: number; pollInterval?: number }\n ): Promise<RecordingStatus> {\n return waitForRecording(recordingId, this.config, options);\n }\n\n /**\n * Get errors from recording with screenshots\n */\n async getErrors(recordingId: string): Promise<ErrorsResponse> {\n return getErrors(recordingId, this.config);\n }\n\n /**\n * Check if browser worker service is healthy\n */\n async checkHealth(): Promise<{\n ok: boolean;\n google_configured: boolean;\n database_configured: boolean;\n s3_configured: boolean;\n error?: string;\n }> {\n return checkHealth(this.config);\n }\n}\n\n/**\n * Execute a natural language browser automation task\n * \n * @param input - Task parameters\n * @param config - Optional configuration (apiKey, apiUrl to override default)\n * @returns Task result with success status and findings\n * \n * @example\n * ```typescript\n * const result = await executeBrowserTask(\n * {\n * task: \"Test checkout flow for buying a pineapple\",\n * url: \"https://3000-abc.e2b.dev\",\n * max_steps: 20,\n * repo_id: \"my-project\",\n * commit_id: \"uuid-here\"\n * },\n * {\n * apiKey: process.env.MORPH_API_KEY,\n * // apiUrl: 'http://localhost:8001' // Override for local testing\n * }\n * );\n * \n * if (result.success) {\n * console.log('Task completed:', result.result);\n * console.log('Replay:', result.replay_url);\n * }\n * ```\n */\nexport async function executeBrowserTask(\n input: BrowserTaskInput,\n config: BrowserConfig = {}\n): Promise<BrowserTaskResult> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const timeout = config.timeout || DEFAULT_CONFIG.timeout;\n const debug = config.debug || false;\n\n if (!input.task || input.task.trim().length === 0) {\n return { success: false, error: 'Task description is required' };\n }\n\n if (input.max_steps !== undefined && (input.max_steps < 1 || input.max_steps > 50)) {\n return { success: false, error: 'max_steps must be between 1 and 50' };\n }\n\n if (debug) {\n console.log(`[Browser] Task: \"${input.task.slice(0, 60)}...\" url=${input.url || 'none'} maxSteps=${input.max_steps ?? 10}`);\n console.log(`[Browser] Recording: ${input.record_video ? 'yes' : 'no'} | Calling ${apiUrl}/browser-task`);\n }\n\n const startTime = Date.now();\n\n try {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;\n\n const fetchPromise = fetchWithRetry(\n `${apiUrl}/browser-task`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n task: input.task,\n url: input.url,\n max_steps: input.max_steps ?? 10,\n model: input.model ?? 'morph-computer-use-v0',\n viewport_width: input.viewport_width ?? 1280,\n viewport_height: input.viewport_height ?? 720,\n repo_id: input.repo_id,\n commit_id: input.commit_id,\n record_video: input.record_video ?? false,\n video_width: input.video_width ?? input.viewport_width ?? 1280,\n video_height: input.video_height ?? input.viewport_height ?? 720,\n structured_output: input.structured_output,\n }),\n },\n config.retryConfig\n );\n\n const response = await withTimeout(\n fetchPromise,\n timeout,\n `Browser task timed out after ${timeout}ms. Consider increasing timeout or reducing max_steps.`\n );\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const result: BrowserTaskResult = await response.json();\n const elapsed = Date.now() - startTime;\n \n if (debug) {\n console.log(`[Browser] ✅ ${result.success ? 'Success' : 'Failed'} in ${elapsed}ms | steps=${result.steps_taken ?? 0} recordingId=${result.recording_id ?? 'none'}`);\n }\n\n return result;\n\n } catch (error) {\n if (error instanceof Error) {\n // Handle network errors\n if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) {\n return {\n success: false,\n error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running.`,\n };\n }\n\n return {\n success: false,\n error: error.message,\n };\n }\n\n return {\n success: false,\n error: String(error),\n };\n }\n}\n\n/**\n * Get recording status and video URL\n * \n * @param recordingId - Recording UUID from BrowserTaskResult\n * @param config - Configuration with apiKey\n * @returns Recording status with video URL when ready\n * \n * @example\n * ```typescript\n * const status = await getRecording('uuid-here', { apiKey: 'key' });\n * if (status.status === 'COMPLETED' && status.video_url) {\n * console.log('Video ready:', status.video_url);\n * }\n * ```\n */\nexport async function getRecording(\n recordingId: string,\n config: BrowserConfig = {}\n): Promise<RecordingStatus> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (!config.apiKey) {\n throw new Error('API key required for getRecording');\n }\n\n if (debug) console.log(`[Browser] getRecording: ${recordingId}`);\n\n const response = await fetch(`${apiUrl}/recordings/${recordingId}`, {\n method: 'GET',\n headers: { 'Authorization': `Bearer ${config.apiKey}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getRecording error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const recording = await response.json();\n if (debug) console.log(`[Browser] Recording status: ${recording.status}`);\n\n return recording;\n}\n\n/**\n * Wait for recording to complete with automatic polling\n * \n * @param recordingId - Recording UUID\n * @param config - Configuration with apiKey\n * @param options - Polling options\n * @returns Recording status when completed or errored\n * \n * @example\n * ```typescript\n * const result = await executeBrowserTask({ task: '...', record_video: true }, config);\n * if (result.recording_id) {\n * const recording = await waitForRecording(result.recording_id, config, {\n * timeout: 60000, // 1 minute\n * pollInterval: 2000 // Check every 2 seconds\n * });\n * console.log('Video URL:', recording.video_url);\n * }\n * ```\n */\nexport async function waitForRecording(\n recordingId: string,\n config: BrowserConfig = {},\n options: { timeout?: number; pollInterval?: number } = {}\n): Promise<RecordingStatus> {\n const timeout = options.timeout ?? 60000; // Default 1 minute\n const pollInterval = options.pollInterval ?? 2000; // Default 2 seconds\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n const status = await getRecording(recordingId, config);\n \n if (status.status === 'COMPLETED' || status.status === 'ERROR') {\n return status;\n }\n\n // Wait before next poll\n await new Promise(resolve => setTimeout(resolve, pollInterval));\n }\n\n throw new Error(`Recording timeout after ${timeout}ms - status still pending`);\n}\n\n/**\n * Execute task with recording and wait for video to be ready\n * \n * @param input - Task parameters with record_video=true\n * @param config - Configuration with apiKey\n * @returns Task result with ready video URL\n * \n * @example\n * ```typescript\n * const result = await executeWithRecording(\n * {\n * task: \"Test checkout flow\",\n * url: \"https://example.com\",\n * record_video: true,\n * repo_id: \"my-project\"\n * },\n * { apiKey: process.env.MORPH_API_KEY }\n * );\n * \n * console.log('Task result:', result.result);\n * console.log('Video URL:', result.recording?.video_url);\n * ```\n */\nexport async function executeWithRecording(\n input: BrowserTaskInput & { record_video: true },\n config: BrowserConfig = {}\n): Promise<BrowserTaskResult & { recording?: RecordingStatus }> {\n // Execute task with recording\n const taskResult = await executeBrowserTask(input, config);\n\n // If recording was created, wait for it to complete\n if (taskResult.recording_id) {\n try {\n const recording = await waitForRecording(\n taskResult.recording_id,\n config,\n { timeout: 60000, pollInterval: 2000 }\n );\n return {\n ...taskResult,\n recording,\n };\n } catch (error) {\n // Return task result even if recording fails\n return {\n ...taskResult,\n recording: {\n id: taskResult.recording_id,\n status: 'ERROR',\n error: error instanceof Error ? error.message : String(error),\n created_at: new Date().toISOString(),\n },\n };\n }\n }\n\n return taskResult;\n}\n\n/**\n * Get errors from recording with screenshots\n * \n * Screenshots are captured in real-time (500ms after error occurs) during the browser session.\n * \n * @param recordingId - Recording UUID from BrowserTaskResult\n * @param config - Configuration with apiKey\n * @returns Errors with real-time screenshots\n * \n * @example\n * ```typescript\n * const { errors, total_errors } = await getErrors('uuid-here', { apiKey: 'key' });\n * \n * console.log(`Found ${total_errors} errors`);\n * \n * errors.forEach(err => {\n * console.log(`[${err.type}] ${err.message}`);\n * if (err.url) console.log(` URL: ${err.url}`);\n * if (err.screenshot_url) console.log(` Screenshot: ${err.screenshot_url}`);\n * \n * // Download screenshot\n * if (err.screenshot_url) {\n * const response = await fetch(err.screenshot_url);\n * const screenshot = await response.arrayBuffer();\n * // Save or process screenshot\n * }\n * });\n * ```\n */\nexport async function getErrors(\n recordingId: string,\n config: BrowserConfig = {}\n): Promise<ErrorsResponse> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (!config.apiKey) {\n throw new Error('API key required for getErrors');\n }\n\n if (debug) console.log(`[Browser] getErrors: ${recordingId}`);\n\n const response = await fetch(`${apiUrl}/recordings/${recordingId}/errors`, {\n method: 'GET',\n headers: { 'Authorization': `Bearer ${config.apiKey}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getErrors error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const errors = await response.json();\n if (debug) console.log(`[Browser] Found ${errors.total_errors} errors`);\n\n return errors;\n}\n\n/**\n * Helper to serialize Zod schema for API\n */\nfunction stringifyStructuredOutput(schema: any): string {\n try {\n return JSON.stringify({\n type: 'object',\n description: 'Zod schema definition (Zod v3)',\n zodDef: schema._def,\n });\n } catch (error) {\n console.warn('[Browser] Failed to serialize Zod schema:', error);\n return JSON.stringify({\n type: 'object',\n description: 'Schema serialization failed',\n });\n }\n}\n\n/**\n * Parse and validate structured task output\n */\nfunction parseStructuredTaskOutput<T>(\n result: BrowserTaskResult,\n schema: any\n): BrowserTaskResult & { parsed: T | null } {\n if (!result.output) {\n return { ...result, parsed: null };\n }\n\n try {\n const parsed = JSON.parse(result.output);\n const validated = schema.parse(parsed) as T;\n return { ...result, parsed: validated };\n } catch (error) {\n if (error instanceof SyntaxError) {\n return { ...result, parsed: null };\n }\n throw error;\n }\n}\n\n/**\n * Get current task status\n */\nasync function getTaskStatus(\n taskId: string,\n config: BrowserConfig\n): Promise<BrowserTaskResult> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (debug) console.log(`[Browser] getTaskStatus: ${taskId}`);\n\n const headers: Record<string, string> = {};\n if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;\n\n const response = await fetch(`${apiUrl}/tasks/${taskId}`, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getTaskStatus error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const result: BrowserTaskResult = await response.json();\n if (debug) console.log(`[Browser] Task status: ${result.status}`);\n\n return result;\n}\n\n/**\n * Generate live URL for watching task execution in real-time\n */\nfunction generateLiveUrl(taskId: string, config: BrowserConfig): string {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const baseUrl = apiUrl.replace('/api', '');\n return `${baseUrl}/tasks/${taskId}/live`;\n}\n\n/**\n * Poll task until completion\n */\nasync function pollTaskUntilComplete(\n taskId: string,\n config: BrowserConfig,\n pollConfig: { interval?: number; timeout?: number } = {}\n): Promise<BrowserTaskResult> {\n const interval = pollConfig.interval ?? 2000; // 2 seconds\n const timeout = pollConfig.timeout ?? 300000; // 5 minutes\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n const status = await getTaskStatus(taskId, config);\n \n if (status.status === 'completed' || status.status === 'failed') {\n return status;\n }\n\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n\n throw new Error(`Task polling timeout after ${timeout}ms`);\n}\n\n/**\n * Wrap task response with convenience methods\n */\nfunction wrapTaskResponse(\n result: BrowserTaskResult,\n config: BrowserConfig\n): BrowserTaskWithPromise {\n if (!result.task_id) {\n throw new Error('task_id is required to wrap response');\n }\n\n return {\n ...result,\n task_id: result.task_id,\n liveUrl: generateLiveUrl(result.task_id, config),\n complete: async (pollConfig?: { interval?: number; timeout?: number }) => {\n return pollTaskUntilComplete(result.task_id!, config, pollConfig);\n },\n };\n}\n\n/**\n * Wrap task response with schema validation\n */\nfunction wrapTaskResponseWithSchema<T>(\n result: BrowserTaskResult,\n config: BrowserConfig,\n schema: any\n): BrowserTaskWithPromiseAndSchema<T> {\n if (!result.task_id) {\n throw new Error('task_id is required to wrap response');\n }\n\n const parsed = result.output\n ? parseStructuredTaskOutput<T>(result, schema)\n : { ...result, parsed: null };\n\n return {\n ...parsed,\n task_id: result.task_id,\n liveUrl: generateLiveUrl(result.task_id, config),\n complete: async (pollConfig?: { interval?: number; timeout?: number }) => {\n const finalResult = await pollTaskUntilComplete(result.task_id!, config, pollConfig);\n return parseStructuredTaskOutput<T>(finalResult, schema);\n },\n };\n}\n\n/**\n * Check if browser worker service is healthy\n * \n * @param config - Optional configuration\n * @returns Health status\n */\nexport async function checkHealth(config: BrowserConfig = {}): Promise<{\n ok: boolean;\n google_configured: boolean;\n database_configured: boolean;\n s3_configured: boolean;\n error?: string;\n}> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n\n try {\n const response = await fetch(`${apiUrl}/health`, {\n method: 'GET',\n headers: config.apiKey\n ? { 'Authorization': `Bearer ${config.apiKey}` }\n : {},\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return {\n ok: true,\n google_configured: data.google_configured ?? false,\n database_configured: data.database_configured ?? false,\n s3_configured: data.s3_configured ?? false,\n };\n } catch (error) {\n return {\n ok: false,\n google_configured: false,\n database_configured: false,\n s3_configured: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n","/**\n * Tool descriptions and prompts for AI models\n */\n\nexport const BROWSER_TOOL_DESCRIPTION = `Execute natural language browser automation tasks using an AI-powered agent. The agent can navigate websites, interact with elements, fill forms, click buttons, and verify functionality.\n\nUse this tool to:\n- Test web applications end-to-end\n- Verify UI functionality\n- Automate user workflows\n- Extract information from web pages\n\nThe agent uses GPT-4o-mini to interpret your task and execute the necessary browser actions. It runs in a remote browser via Browserless.io, so it can access any publicly accessible URL.\n\nImportant:\n- Provide clear, specific task descriptions\n- Include the starting URL if navigating to a specific page\n- Use remote URLs (e.g., https://3000-xyz.e2b.dev) not localhost\n- Complex tasks may require more max_steps`;\n\nexport const BROWSER_SYSTEM_PROMPT = `You have access to browser automation capabilities. When testing or interacting with web applications:\n\n1. Be specific about what you're testing or verifying\n2. Break complex tasks into clear steps\n3. Always provide the full URL including protocol (https://)\n4. For localhost apps, use the remote tunnel URL (e.g., e2b.dev URLs)\n5. Specify the number of steps needed - simple tasks use 5-10, complex flows use 15-30\n\nExample good tasks:\n- \"Go to https://3000-abc.e2b.dev and verify the landing page loads with a hero section\"\n- \"Test guest checkout: add a pineapple to cart, proceed to checkout, fill shipping info, and verify order summary\"\n- \"Navigate to the dashboard and click on the settings tab, then verify the API keys section is visible\"\n\nExample bad tasks:\n- \"test the app\" (too vague)\n- \"go to localhost:3000\" (use remote URL instead)\n- \"do everything\" (not specific enough)`;\n\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../tools/browser/anthropic.ts","../../../tools/utils/resilience.ts","../../../tools/browser/core.ts","../../../tools/browser/prompts.ts"],"sourcesContent":["/**\n * Anthropic SDK adapter for browser automation tool\n */\n\nimport type { Tool } from '@anthropic-ai/sdk/resources/messages.mjs';\nimport { executeBrowserTask } from './core.js';\nimport type {\n BrowserConfig,\n BrowserTaskInput,\n BrowserTaskResult,\n} from './types.js';\nimport { BROWSER_TOOL_DESCRIPTION, BROWSER_SYSTEM_PROMPT } from './prompts.js';\n\n/**\n * Anthropic tool definition for browser automation\n */\nexport const browserTool: Tool = {\n name: 'browser_task',\n description: BROWSER_TOOL_DESCRIPTION,\n input_schema: {\n type: 'object',\n properties: {\n task: {\n type: 'string',\n description: 'Natural language description of what to do (e.g., \"Test checkout flow for buying a pineapple\")',\n },\n url: {\n type: 'string',\n description: 'Starting URL (e.g., https://3000-xyz.e2b.dev). Required if navigating to a specific page.',\n },\n max_steps: {\n type: 'number',\n description: 'Maximum number of browser actions to take (1-50). Default: 10. Use 15-30 for complex flows.',\n default: 10,\n },\n region: {\n type: 'string',\n enum: ['sfo', 'lon'],\n description: 'Browserless region: sfo (US West Coast) or lon (Europe). Default: sfo.',\n default: 'sfo',\n },\n },\n required: ['task'],\n },\n};\n\n/**\n * Format browser task result for Anthropic tool result\n * \n * @param result - Browser task result\n * @returns Formatted string for tool result\n */\nfunction formatResult(result: BrowserTaskResult): string {\n if (result.success) {\n const parts = [\n '✅ Browser task completed successfully',\n `Steps taken: ${result.steps_taken ?? 0}`,\n result.execution_time_ms ? `Execution time: ${result.execution_time_ms}ms` : null,\n '',\n 'Result:',\n result.result || 'Task completed',\n ];\n return parts.filter(Boolean).join('\\n');\n }\n\n return `❌ Browser task failed: ${result.error || 'Unknown error'}`;\n}\n\n/**\n * Create a configured browser tool with execute and formatResult methods\n * \n * @param config - Browser worker configuration\n * @returns Tool definition with execute and formatResult methods\n * \n * @example\n * ```typescript\n * import Anthropic from '@anthropic-ai/sdk';\n * import { createBrowserTool } from 'morphsdk/tools/browser/anthropic';\n * \n * const tool = createBrowserTool({\n * apiKey: process.env.MORPH_API_KEY,\n * timeout: 180000\n * });\n * \n * const client = new Anthropic();\n * \n * const response = await client.messages.create({\n * model: 'claude-sonnet-4-5-20250929',\n * tools: [tool], // tool itself is the Tool definition\n * messages: [{\n * role: 'user',\n * content: 'Test the checkout flow at https://3000-abc.e2b.dev'\n * }]\n * });\n * \n * // Execute and format\n * const result = await tool.execute(toolUseBlock.input);\n * const formatted = tool.formatResult(result);\n * ```\n */\nexport function createBrowserTool(config?: BrowserConfig) {\n return Object.assign({}, browserTool, {\n execute: async (input: BrowserTaskInput): Promise<BrowserTaskResult> => {\n return executeBrowserTask(input, config);\n },\n formatResult: (result: BrowserTaskResult): string => {\n return formatResult(result);\n },\n getSystemPrompt: (): string => {\n return BROWSER_SYSTEM_PROMPT;\n },\n });\n}\n\n","/**\n * Resilience utilities for retry logic and timeout handling\n */\n\nexport interface RetryConfig {\n maxRetries?: number; // Default: 3\n initialDelay?: number; // Default: 1000ms\n maxDelay?: number; // Default: 30000ms\n backoffMultiplier?: number; // Default: 2\n retryableErrors?: string[]; // Default: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND']\n onRetry?: (attempt: number, error: Error) => void;\n}\n\nconst DEFAULT_RETRY_CONFIG: Required<Omit<RetryConfig, 'onRetry'>> = {\n maxRetries: 3,\n initialDelay: 1000,\n maxDelay: 30000,\n backoffMultiplier: 2,\n retryableErrors: ['ECONNREFUSED', 'ETIMEDOUT', 'ENOTFOUND'],\n};\n\n/**\n * Retry a fetch request with exponential backoff\n * \n * @param url - Request URL\n * @param options - Fetch options\n * @param retryConfig - Retry configuration\n * @returns Response from fetch\n * \n * @example\n * ```typescript\n * const response = await fetchWithRetry(\n * 'https://api.example.com/data',\n * { method: 'POST', body: JSON.stringify(data) },\n * { maxRetries: 5, initialDelay: 500 }\n * );\n * ```\n */\nexport async function fetchWithRetry(\n url: string,\n options: RequestInit,\n retryConfig: RetryConfig = {}\n): Promise<Response> {\n const {\n maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,\n initialDelay = DEFAULT_RETRY_CONFIG.initialDelay,\n maxDelay = DEFAULT_RETRY_CONFIG.maxDelay,\n backoffMultiplier = DEFAULT_RETRY_CONFIG.backoffMultiplier,\n retryableErrors = DEFAULT_RETRY_CONFIG.retryableErrors,\n onRetry,\n } = retryConfig;\n\n let lastError: Error | null = null;\n let delay = initialDelay;\n\n for (let attempt = 0; attempt <= maxRetries; attempt++) {\n try {\n const response = await fetch(url, options);\n \n // Retry on 429 (rate limit) or 503 (service unavailable)\n if (response.status === 429 || response.status === 503) {\n if (attempt < maxRetries) {\n // Check for Retry-After header\n const retryAfter = response.headers.get('Retry-After');\n const waitTime = retryAfter \n ? parseInt(retryAfter) * 1000 \n : Math.min(delay, maxDelay);\n \n const error = new Error(`HTTP ${response.status}: Retrying after ${waitTime}ms`);\n if (onRetry) {\n onRetry(attempt + 1, error);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n continue;\n }\n }\n\n return response;\n } catch (error) {\n lastError = error as Error;\n \n // Check if error is retryable\n const isRetryable = retryableErrors.some(errType => \n lastError?.message?.includes(errType)\n );\n\n if (!isRetryable || attempt === maxRetries) {\n throw lastError;\n }\n\n // Exponential backoff\n const waitTime = Math.min(delay, maxDelay);\n if (onRetry) {\n onRetry(attempt + 1, lastError);\n }\n \n await sleep(waitTime);\n delay *= backoffMultiplier;\n }\n }\n\n throw lastError || new Error('Max retries exceeded');\n}\n\n/**\n * Add timeout to any promise\n * \n * @param promise - Promise to wrap with timeout\n * @param timeoutMs - Timeout in milliseconds\n * @param errorMessage - Optional custom error message\n * @returns Promise that rejects if timeout is reached\n * \n * @example\n * ```typescript\n * const result = await withTimeout(\n * fetchData(),\n * 5000,\n * 'Data fetch timed out'\n * );\n * ```\n */\nexport async function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage?: string\n): Promise<T> {\n let timeoutId: NodeJS.Timeout | number;\n \n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new Error(errorMessage || `Operation timed out after ${timeoutMs}ms`));\n }, timeoutMs);\n });\n\n try {\n const result = await Promise.race([promise, timeoutPromise]);\n clearTimeout(timeoutId!);\n return result;\n } catch (error) {\n clearTimeout(timeoutId!);\n throw error;\n }\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n}\n\n/**\n * Unified error type for all tools\n */\nexport class MorphError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number,\n public retryable: boolean = false\n ) {\n super(message);\n this.name = 'MorphError';\n }\n}\n\n\n","/**\n * Core implementation for browser automation tasks\n */\n\nimport { fetchWithRetry, withTimeout } from '../utils/resilience.js';\nimport type {\n BrowserConfig,\n BrowserTaskInput,\n BrowserTaskInputWithSchema,\n BrowserTaskResult,\n BrowserTaskWithPromise,\n BrowserTaskWithPromiseAndSchema,\n RecordingStatus,\n ErrorsResponse,\n LiveSessionOptions,\n IframeOptions,\n} from './types.js';\nimport { buildLiveUrl, buildLiveIframe, buildEmbedCode, resolvePreset } from './live.js';\n\nconst DEFAULT_CONFIG = {\n apiUrl: process.env.MORPH_ENVIRONMENT === 'DEV' \n ? 'http://localhost:8000'\n : 'https://browser.morphllm.com',\n timeout: 120000, // 2 minutes for complex tasks\n debug: false,\n};\n\n/**\n * BrowserClient class for easier usage with instance configuration\n */\nexport class BrowserClient {\n private config: BrowserConfig;\n\n constructor(config: BrowserConfig = {}) {\n this.config = {\n ...DEFAULT_CONFIG,\n ...config,\n };\n }\n\n /**\n * Execute a browser automation task\n */\n async execute(input: BrowserTaskInput): Promise<BrowserTaskResult> {\n return executeBrowserTask(input, this.config);\n }\n\n async createTask(input: BrowserTaskInput): Promise<BrowserTaskWithPromise>;\n async createTask<T>(input: BrowserTaskInputWithSchema<T>): Promise<BrowserTaskWithPromiseAndSchema<T>>;\n async createTask<T>(\n input: BrowserTaskInput | BrowserTaskInputWithSchema<T>\n ): Promise<BrowserTaskWithPromise | BrowserTaskWithPromiseAndSchema<T>> {\n if ('schema' in input) {\n const taskInput: BrowserTaskInput = {\n ...input,\n structured_output: stringifyStructuredOutput(input.schema),\n };\n const result = await executeBrowserTask(taskInput, this.config);\n return wrapTaskResponseWithSchema(result, this.config, input.schema);\n } else {\n const result = await executeBrowserTask(input, this.config);\n return wrapTaskResponse(result, this.config);\n }\n }\n\n /**\n * Execute task with recording and wait for video to be ready\n */\n async executeWithRecording(\n input: BrowserTaskInput & { record_video: true }\n ): Promise<BrowserTaskResult & { recording?: RecordingStatus }> {\n return executeWithRecording(input, this.config);\n }\n\n /**\n * Get recording status and URLs\n */\n async getRecording(recordingId: string): Promise<RecordingStatus> {\n return getRecording(recordingId, this.config);\n }\n\n /**\n * Wait for recording to complete with automatic polling\n */\n async waitForRecording(\n recordingId: string,\n options?: { timeout?: number; pollInterval?: number }\n ): Promise<RecordingStatus> {\n return waitForRecording(recordingId, this.config, options);\n }\n\n /**\n * Get errors from recording with screenshots\n */\n async getErrors(recordingId: string): Promise<ErrorsResponse> {\n return getErrors(recordingId, this.config);\n }\n\n /**\n * Check if browser worker service is healthy\n */\n async checkHealth(): Promise<{\n ok: boolean;\n google_configured: boolean;\n database_configured: boolean;\n s3_configured: boolean;\n error?: string;\n }> {\n return checkHealth(this.config);\n }\n}\n\n/**\n * Execute a natural language browser automation task\n * \n * @param input - Task parameters\n * @param config - Optional configuration (apiKey, apiUrl to override default)\n * @returns Task result with success status and findings\n * \n * @example\n * ```typescript\n * const result = await executeBrowserTask(\n * {\n * task: \"Test checkout flow for buying a pineapple\",\n * url: \"https://3000-abc.e2b.dev\",\n * max_steps: 20,\n * repo_id: \"my-project\",\n * commit_id: \"uuid-here\"\n * },\n * {\n * apiKey: process.env.MORPH_API_KEY,\n * // apiUrl: 'http://localhost:8001' // Override for local testing\n * }\n * );\n * \n * if (result.success) {\n * console.log('Task completed:', result.result);\n * console.log('Replay:', result.replay_url);\n * }\n * ```\n */\nexport async function executeBrowserTask(\n input: BrowserTaskInput,\n config: BrowserConfig = {}\n): Promise<BrowserTaskResult> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const timeout = config.timeout || DEFAULT_CONFIG.timeout;\n const debug = config.debug || false;\n\n if (!input.task || input.task.trim().length === 0) {\n return { \n success: false, \n error: 'Task description is required. Example: \"Go to example.com and click the login button\"' \n };\n }\n\n if (input.max_steps !== undefined && (input.max_steps < 1 || input.max_steps > 50)) {\n return { \n success: false, \n error: 'max_steps must be between 1 and 50. Use more steps for complex multi-page flows.' \n };\n }\n\n if (debug) {\n console.log(`[Browser] Task: \"${input.task.slice(0, 60)}...\" url=${input.url || 'none'} maxSteps=${input.max_steps ?? 10}`);\n console.log(`[Browser] Recording: ${input.record_video ? 'yes' : 'no'} | Calling ${apiUrl}/browser-task`);\n }\n\n const startTime = Date.now();\n\n try {\n const headers: Record<string, string> = { 'Content-Type': 'application/json' };\n if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;\n\n const fetchPromise = fetchWithRetry(\n `${apiUrl}/browser-task`,\n {\n method: 'POST',\n headers,\n body: JSON.stringify({\n task: input.task,\n url: input.url,\n max_steps: input.max_steps ?? 10,\n model: input.model ?? 'morph-computer-use-v0',\n viewport_width: input.viewport_width ?? 1280,\n viewport_height: input.viewport_height ?? 720,\n repo_id: input.repo_id,\n commit_id: input.commit_id,\n record_video: input.record_video ?? false,\n video_width: input.video_width ?? input.viewport_width ?? 1280,\n video_height: input.video_height ?? input.viewport_height ?? 720,\n structured_output: input.structured_output,\n }),\n },\n config.retryConfig\n );\n\n const response = await withTimeout(\n fetchPromise,\n timeout,\n `Browser task timed out after ${timeout}ms. Consider increasing timeout or reducing max_steps.`\n );\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const result: BrowserTaskResult = await response.json();\n const elapsed = Date.now() - startTime;\n \n if (debug) {\n console.log(`[Browser] ✅ ${result.success ? 'Success' : 'Failed'} in ${elapsed}ms | steps=${result.steps_taken ?? 0} recordingId=${result.recording_id ?? 'none'}`);\n }\n\n return result;\n\n } catch (error) {\n if (error instanceof Error) {\n // Handle network errors\n if (error.message.includes('ECONNREFUSED') || error.message.includes('fetch failed')) {\n return {\n success: false,\n error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running and accessible. For local dev, set MORPH_ENVIRONMENT=DEV.`,\n };\n }\n\n return {\n success: false,\n error: error.message,\n };\n }\n\n return {\n success: false,\n error: String(error),\n };\n }\n}\n\n/**\n * Get recording status and video URL\n * \n * @param recordingId - Recording UUID from BrowserTaskResult\n * @param config - Configuration with apiKey\n * @returns Recording status with video URL when ready\n * \n * @example\n * ```typescript\n * const status = await getRecording('uuid-here', { apiKey: 'key' });\n * if (status.status === 'COMPLETED' && status.video_url) {\n * console.log('Video ready:', status.video_url);\n * }\n * ```\n */\nexport async function getRecording(\n recordingId: string,\n config: BrowserConfig = {}\n): Promise<RecordingStatus> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (!config.apiKey) {\n throw new Error('API key required for getRecording');\n }\n\n if (debug) console.log(`[Browser] getRecording: ${recordingId}`);\n\n const response = await fetch(`${apiUrl}/recordings/${recordingId}`, {\n method: 'GET',\n headers: { 'Authorization': `Bearer ${config.apiKey}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getRecording error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const recording = await response.json();\n if (debug) console.log(`[Browser] Recording status: ${recording.status}`);\n\n return recording;\n}\n\n/**\n * Wait for recording to complete with automatic polling\n * \n * @param recordingId - Recording UUID\n * @param config - Configuration with apiKey\n * @param options - Polling options\n * @returns Recording status when completed or errored\n * \n * @example\n * ```typescript\n * const result = await executeBrowserTask({ task: '...', record_video: true }, config);\n * if (result.recording_id) {\n * const recording = await waitForRecording(result.recording_id, config, {\n * timeout: 60000, // 1 minute\n * pollInterval: 2000 // Check every 2 seconds\n * });\n * console.log('Video URL:', recording.video_url);\n * }\n * ```\n */\nexport async function waitForRecording(\n recordingId: string,\n config: BrowserConfig = {},\n options: { timeout?: number; pollInterval?: number } = {}\n): Promise<RecordingStatus> {\n const timeout = options.timeout ?? 60000; // Default 1 minute\n const pollInterval = options.pollInterval ?? 2000; // Default 2 seconds\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n const status = await getRecording(recordingId, config);\n \n if (status.status === 'COMPLETED' || status.status === 'ERROR') {\n return status;\n }\n\n // Wait before next poll\n await new Promise(resolve => setTimeout(resolve, pollInterval));\n }\n\n throw new Error(`Recording timeout after ${timeout}ms - status still pending`);\n}\n\n/**\n * Execute task with recording and wait for video to be ready\n * \n * @param input - Task parameters with record_video=true\n * @param config - Configuration with apiKey\n * @returns Task result with ready video URL\n * \n * @example\n * ```typescript\n * const result = await executeWithRecording(\n * {\n * task: \"Test checkout flow\",\n * url: \"https://example.com\",\n * record_video: true,\n * repo_id: \"my-project\"\n * },\n * { apiKey: process.env.MORPH_API_KEY }\n * );\n * \n * console.log('Task result:', result.result);\n * console.log('Video URL:', result.recording?.video_url);\n * ```\n */\nexport async function executeWithRecording(\n input: BrowserTaskInput & { record_video: true },\n config: BrowserConfig = {}\n): Promise<BrowserTaskResult & { recording?: RecordingStatus }> {\n // Execute task with recording\n const taskResult = await executeBrowserTask(input, config);\n\n // If recording was created, wait for it to complete\n if (taskResult.recording_id) {\n try {\n const recording = await waitForRecording(\n taskResult.recording_id,\n config,\n { timeout: 60000, pollInterval: 2000 }\n );\n return {\n ...taskResult,\n recording,\n };\n } catch (error) {\n // Return task result even if recording fails\n return {\n ...taskResult,\n recording: {\n id: taskResult.recording_id,\n status: 'ERROR',\n error: error instanceof Error ? error.message : String(error),\n created_at: new Date().toISOString(),\n },\n };\n }\n }\n\n return taskResult;\n}\n\n/**\n * Get errors from recording with screenshots\n * \n * Screenshots are captured in real-time (500ms after error occurs) during the browser session.\n * \n * @param recordingId - Recording UUID from BrowserTaskResult\n * @param config - Configuration with apiKey\n * @returns Errors with real-time screenshots\n * \n * @example\n * ```typescript\n * const { errors, total_errors } = await getErrors('uuid-here', { apiKey: 'key' });\n * \n * console.log(`Found ${total_errors} errors`);\n * \n * errors.forEach(err => {\n * console.log(`[${err.type}] ${err.message}`);\n * if (err.url) console.log(` URL: ${err.url}`);\n * if (err.screenshot_url) console.log(` Screenshot: ${err.screenshot_url}`);\n * \n * // Download screenshot\n * if (err.screenshot_url) {\n * const response = await fetch(err.screenshot_url);\n * const screenshot = await response.arrayBuffer();\n * // Save or process screenshot\n * }\n * });\n * ```\n */\nexport async function getErrors(\n recordingId: string,\n config: BrowserConfig = {}\n): Promise<ErrorsResponse> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (!config.apiKey) {\n throw new Error('API key required for getErrors');\n }\n\n if (debug) console.log(`[Browser] getErrors: ${recordingId}`);\n\n const response = await fetch(`${apiUrl}/recordings/${recordingId}/errors`, {\n method: 'GET',\n headers: { 'Authorization': `Bearer ${config.apiKey}` },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getErrors error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const errors = await response.json();\n if (debug) console.log(`[Browser] Found ${errors.total_errors} errors`);\n\n return errors;\n}\n\n/**\n * Helper to serialize Zod schema for API\n */\nfunction stringifyStructuredOutput(schema: any): string {\n try {\n return JSON.stringify({\n type: 'object',\n description: 'Zod schema definition (Zod v3)',\n zodDef: schema._def,\n });\n } catch (error) {\n console.warn('[Browser] Failed to serialize Zod schema:', error);\n return JSON.stringify({\n type: 'object',\n description: 'Schema serialization failed',\n });\n }\n}\n\n/**\n * Parse and validate structured task output\n */\nfunction parseStructuredTaskOutput<T>(\n result: BrowserTaskResult,\n schema: any\n): BrowserTaskResult & { parsed: T | null } {\n if (!result.output) {\n return { ...result, parsed: null };\n }\n\n try {\n const parsed = JSON.parse(result.output);\n const validated = schema.parse(parsed) as T;\n return { ...result, parsed: validated };\n } catch (error) {\n if (error instanceof SyntaxError) {\n return { ...result, parsed: null };\n }\n throw error;\n }\n}\n\n/**\n * Get current task status\n */\nasync function getTaskStatus(\n taskId: string,\n config: BrowserConfig\n): Promise<BrowserTaskResult> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const debug = config.debug || false;\n\n if (debug) console.log(`[Browser] getTaskStatus: ${taskId}`);\n\n const headers: Record<string, string> = {};\n if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;\n\n const response = await fetch(`${apiUrl}/tasks/${taskId}`, {\n method: 'GET',\n headers,\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => response.statusText);\n if (debug) console.error(`[Browser] getTaskStatus error: ${response.status} - ${errorText}`);\n throw new Error(`HTTP ${response.status}: ${errorText}`);\n }\n\n const result: BrowserTaskResult = await response.json();\n if (debug) console.log(`[Browser] Task status: ${result.status}`);\n\n return result;\n}\n\n/**\n * Generate live URL for watching task execution in real-time\n */\nfunction generateLiveUrl(taskId: string, config: BrowserConfig): string {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n const baseUrl = apiUrl.replace('/api', '');\n return `${baseUrl}/tasks/${taskId}/live`;\n}\n\n/**\n * Poll task until completion\n */\nasync function pollTaskUntilComplete(\n taskId: string,\n config: BrowserConfig,\n pollConfig: { interval?: number; timeout?: number } = {}\n): Promise<BrowserTaskResult> {\n const interval = pollConfig.interval ?? 2000; // 2 seconds\n const timeout = pollConfig.timeout ?? 300000; // 5 minutes\n const startTime = Date.now();\n\n while (Date.now() - startTime < timeout) {\n const status = await getTaskStatus(taskId, config);\n \n if (status.status === 'completed' || status.status === 'failed') {\n return status;\n }\n\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n\n throw new Error(`Task polling timeout after ${timeout}ms`);\n}\n\n/**\n * Wrap task response with convenience methods\n */\nfunction wrapTaskResponse(\n result: BrowserTaskResult,\n config: BrowserConfig\n): BrowserTaskWithPromise {\n if (!result.task_id) {\n throw new Error('task_id is required to wrap response');\n }\n\n const wrapped: BrowserTaskWithPromise = {\n ...result,\n task_id: result.task_id,\n liveUrl: generateLiveUrl(result.task_id, config),\n complete: async (pollConfig?: { interval?: number; timeout?: number }) => {\n return pollTaskUntilComplete(result.task_id!, config, pollConfig);\n },\n };\n\n // Add Steel live session helpers if debugUrl is available\n if (result.debugUrl) {\n wrapped.getLiveUrl = (options?: LiveSessionOptions) => {\n return buildLiveUrl(result.debugUrl!, options);\n };\n\n wrapped.getLiveIframe = (optionsOrPreset?: string | IframeOptions) => {\n const options = resolvePreset(optionsOrPreset);\n return buildLiveIframe(result.debugUrl!, options);\n };\n\n wrapped.getEmbedCode = () => {\n return buildEmbedCode(result.debugUrl!);\n };\n } else {\n // Provide helpful error messages when methods are called without debugUrl\n wrapped.getLiveUrl = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help.'\n );\n };\n\n wrapped.getLiveIframe = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help.'\n );\n };\n\n wrapped.getEmbedCode = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help.'\n );\n };\n }\n\n return wrapped;\n}\n\n/**\n * Wrap task response with schema validation\n */\nfunction wrapTaskResponseWithSchema<T>(\n result: BrowserTaskResult,\n config: BrowserConfig,\n schema: any\n): BrowserTaskWithPromiseAndSchema<T> {\n if (!result.task_id) {\n throw new Error('task_id is required to wrap response');\n }\n\n const parsed = result.output\n ? parseStructuredTaskOutput<T>(result, schema)\n : { ...result, parsed: null };\n\n const wrapped: BrowserTaskWithPromiseAndSchema<T> = {\n ...parsed,\n task_id: result.task_id,\n liveUrl: generateLiveUrl(result.task_id, config),\n complete: async (pollConfig?: { interval?: number; timeout?: number }) => {\n const finalResult = await pollTaskUntilComplete(result.task_id!, config, pollConfig);\n return parseStructuredTaskOutput<T>(finalResult, schema);\n },\n };\n\n // Add Steel live session helpers if debugUrl is available\n if (result.debugUrl) {\n wrapped.getLiveUrl = (options?: LiveSessionOptions) => {\n return buildLiveUrl(result.debugUrl!, options);\n };\n\n wrapped.getLiveIframe = (optionsOrPreset?: string | IframeOptions) => {\n const options = resolvePreset(optionsOrPreset);\n return buildLiveIframe(result.debugUrl!, options);\n };\n\n wrapped.getEmbedCode = () => {\n return buildEmbedCode(result.debugUrl!);\n };\n } else {\n // Provide helpful error messages when methods are called without debugUrl\n wrapped.getLiveUrl = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. ' +\n 'Contact support@morphllm.com if you need help enabling live sessions. '\n );\n };\n\n wrapped.getLiveIframe = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. ' +\n 'Contact support@morphllm.com if you need help enabling live sessions.'\n );\n };\n\n wrapped.getEmbedCode = () => {\n throw new Error(\n 'Live sessions not available. Your backend must return a debugUrl in the response. ' +\n 'Contact support@morphllm.com if you need help enabling live sessions.'\n );\n };\n }\n\n return wrapped;\n}\n\n/**\n * Check if browser worker service is healthy\n * \n * @param config - Optional configuration\n * @returns Health status\n */\nexport async function checkHealth(config: BrowserConfig = {}): Promise<{\n ok: boolean;\n google_configured: boolean;\n database_configured: boolean;\n s3_configured: boolean;\n error?: string;\n}> {\n const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;\n\n try {\n const response = await fetch(`${apiUrl}/health`, {\n method: 'GET',\n headers: config.apiKey\n ? { 'Authorization': `Bearer ${config.apiKey}` }\n : {},\n });\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}`);\n }\n\n const data = await response.json();\n return {\n ok: true,\n google_configured: data.google_configured ?? false,\n database_configured: data.database_configured ?? false,\n s3_configured: data.s3_configured ?? false,\n };\n } catch (error) {\n return {\n ok: false,\n google_configured: false,\n database_configured: false,\n s3_configured: false,\n error: error instanceof Error ? error.message : String(error),\n };\n }\n}\n\n","/**\n * Tool descriptions and prompts for AI models\n */\n\nexport const BROWSER_TOOL_DESCRIPTION = `Execute natural language browser automation tasks using an AI-powered agent. The agent can navigate websites, interact with elements, fill forms, click buttons, and verify functionality.\n\nUse this tool to:\n- Test web applications end-to-end\n- Verify UI functionality\n- Automate user workflows\n- Extract information from web pages\n\nThe agent uses GPT-4o-mini to interpret your task and execute the necessary browser actions. It runs in a remote browser via Browserless.io, so it can access any publicly accessible URL.\n\nImportant:\n- Provide clear, specific task descriptions\n- Include the starting URL if navigating to a specific page\n- Use remote URLs (e.g., https://3000-xyz.e2b.dev) not localhost\n- Complex tasks may require more max_steps`;\n\nexport const BROWSER_SYSTEM_PROMPT = `You have access to browser automation capabilities. When testing or interacting with web applications:\n\n1. Be specific about what you're testing or verifying\n2. Break complex tasks into clear steps\n3. Always provide the full URL including protocol (https://)\n4. For localhost apps, use the remote tunnel URL (e.g., e2b.dev URLs)\n5. Specify the number of steps needed - simple tasks use 5-10, complex flows use 15-30\n\nExample good tasks:\n- \"Go to https://3000-abc.e2b.dev and verify the landing page loads with a hero section\"\n- \"Test guest checkout: add a pineapple to cart, proceed to checkout, fill shipping info, and verify order summary\"\n- \"Navigate to the dashboard and click on the settings tab, then verify the API keys section is visible\"\n\nExample bad tasks:\n- \"test the app\" (too vague)\n- \"go to localhost:3000\" (use remote URL instead)\n- \"do everything\" (not specific enough)`;\n\n"],"mappings":"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","names":[]}
|
|
@@ -104,6 +104,82 @@ function sleep(ms) {
|
|
|
104
104
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// tools/browser/live.ts
|
|
108
|
+
var LIVE_PRESETS = {
|
|
109
|
+
/** Read-only monitoring (no interaction) */
|
|
110
|
+
readonly: { interactive: false },
|
|
111
|
+
/** Interactive control (human-in-the-loop) */
|
|
112
|
+
interactive: { interactive: true },
|
|
113
|
+
/** Watch-only without controls */
|
|
114
|
+
monitoring: { interactive: false, showControls: false }
|
|
115
|
+
};
|
|
116
|
+
function buildLiveUrl(debugUrl, options = {}) {
|
|
117
|
+
if (!debugUrl) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
"debugUrl is required. Ensure your backend returns debugUrl in the task response. Contact support@morphllm.com if you need help."
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const url = new URL(debugUrl);
|
|
123
|
+
if (options.interactive !== void 0) {
|
|
124
|
+
url.searchParams.set("interactive", String(options.interactive));
|
|
125
|
+
}
|
|
126
|
+
if (options.theme) {
|
|
127
|
+
url.searchParams.set("theme", options.theme);
|
|
128
|
+
}
|
|
129
|
+
if (options.showControls !== void 0) {
|
|
130
|
+
url.searchParams.set("showControls", String(options.showControls));
|
|
131
|
+
}
|
|
132
|
+
if (options.pageId) {
|
|
133
|
+
url.searchParams.set("pageId", options.pageId);
|
|
134
|
+
}
|
|
135
|
+
if (options.pageIndex) {
|
|
136
|
+
url.searchParams.set("pageIndex", options.pageIndex);
|
|
137
|
+
}
|
|
138
|
+
return url.toString();
|
|
139
|
+
}
|
|
140
|
+
function buildLiveIframe(debugUrl, options = {}) {
|
|
141
|
+
const {
|
|
142
|
+
width = "100%",
|
|
143
|
+
height = "600px",
|
|
144
|
+
style = "",
|
|
145
|
+
className = "",
|
|
146
|
+
...sessionOptions
|
|
147
|
+
} = options;
|
|
148
|
+
const src = buildLiveUrl(debugUrl, sessionOptions);
|
|
149
|
+
const widthStr = typeof width === "number" ? `${width}px` : width;
|
|
150
|
+
const heightStr = typeof height === "number" ? `${height}px` : height;
|
|
151
|
+
const baseStyle = `width: ${widthStr}; height: ${heightStr}; border: none;`;
|
|
152
|
+
const fullStyle = style ? `${baseStyle} ${style}` : baseStyle;
|
|
153
|
+
const attributes = [
|
|
154
|
+
`src="${src}"`,
|
|
155
|
+
`style="${fullStyle}"`
|
|
156
|
+
];
|
|
157
|
+
if (className) {
|
|
158
|
+
attributes.push(`class="${className}"`);
|
|
159
|
+
}
|
|
160
|
+
return `<iframe ${attributes.join(" ")}></iframe>`;
|
|
161
|
+
}
|
|
162
|
+
function buildEmbedCode(debugUrl, options = {}) {
|
|
163
|
+
const iframe = buildLiveIframe(debugUrl, options);
|
|
164
|
+
return `<!-- Embed Morph Live Session -->
|
|
165
|
+
${iframe}`;
|
|
166
|
+
}
|
|
167
|
+
function resolvePreset(optionsOrPreset) {
|
|
168
|
+
if (!optionsOrPreset) {
|
|
169
|
+
return {};
|
|
170
|
+
}
|
|
171
|
+
if (typeof optionsOrPreset === "string") {
|
|
172
|
+
const preset = LIVE_PRESETS[optionsOrPreset];
|
|
173
|
+
if (!preset) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Unknown preset: ${optionsOrPreset}. Available presets: ${Object.keys(LIVE_PRESETS).join(", ")}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return preset;
|
|
179
|
+
}
|
|
180
|
+
return optionsOrPreset;
|
|
181
|
+
}
|
|
182
|
+
|
|
107
183
|
// tools/browser/core.ts
|
|
108
184
|
var DEFAULT_CONFIG = {
|
|
109
185
|
apiUrl: process.env.MORPH_ENVIRONMENT === "DEV" ? "http://localhost:8000" : "https://browser.morphllm.com",
|
|
@@ -174,10 +250,16 @@ async function executeBrowserTask(input, config = {}) {
|
|
|
174
250
|
const timeout = config.timeout || DEFAULT_CONFIG.timeout;
|
|
175
251
|
const debug = config.debug || false;
|
|
176
252
|
if (!input.task || input.task.trim().length === 0) {
|
|
177
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
success: false,
|
|
255
|
+
error: 'Task description is required. Example: "Go to example.com and click the login button"'
|
|
256
|
+
};
|
|
178
257
|
}
|
|
179
258
|
if (input.max_steps !== void 0 && (input.max_steps < 1 || input.max_steps > 50)) {
|
|
180
|
-
return {
|
|
259
|
+
return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: "max_steps must be between 1 and 50. Use more steps for complex multi-page flows."
|
|
262
|
+
};
|
|
181
263
|
}
|
|
182
264
|
if (debug) {
|
|
183
265
|
console.log(`[Browser] Task: "${input.task.slice(0, 60)}..." url=${input.url || "none"} maxSteps=${input.max_steps ?? 10}`);
|
|
@@ -230,7 +312,7 @@ async function executeBrowserTask(input, config = {}) {
|
|
|
230
312
|
if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
|
|
231
313
|
return {
|
|
232
314
|
success: false,
|
|
233
|
-
error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running.`
|
|
315
|
+
error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running and accessible. For local dev, set MORPH_ENVIRONMENT=DEV.`
|
|
234
316
|
};
|
|
235
317
|
}
|
|
236
318
|
return {
|
|
@@ -395,7 +477,7 @@ function wrapTaskResponse(result, config) {
|
|
|
395
477
|
if (!result.task_id) {
|
|
396
478
|
throw new Error("task_id is required to wrap response");
|
|
397
479
|
}
|
|
398
|
-
|
|
480
|
+
const wrapped = {
|
|
399
481
|
...result,
|
|
400
482
|
task_id: result.task_id,
|
|
401
483
|
liveUrl: generateLiveUrl(result.task_id, config),
|
|
@@ -403,13 +485,42 @@ function wrapTaskResponse(result, config) {
|
|
|
403
485
|
return pollTaskUntilComplete(result.task_id, config, pollConfig);
|
|
404
486
|
}
|
|
405
487
|
};
|
|
488
|
+
if (result.debugUrl) {
|
|
489
|
+
wrapped.getLiveUrl = (options) => {
|
|
490
|
+
return buildLiveUrl(result.debugUrl, options);
|
|
491
|
+
};
|
|
492
|
+
wrapped.getLiveIframe = (optionsOrPreset) => {
|
|
493
|
+
const options = resolvePreset(optionsOrPreset);
|
|
494
|
+
return buildLiveIframe(result.debugUrl, options);
|
|
495
|
+
};
|
|
496
|
+
wrapped.getEmbedCode = () => {
|
|
497
|
+
return buildEmbedCode(result.debugUrl);
|
|
498
|
+
};
|
|
499
|
+
} else {
|
|
500
|
+
wrapped.getLiveUrl = () => {
|
|
501
|
+
throw new Error(
|
|
502
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help."
|
|
503
|
+
);
|
|
504
|
+
};
|
|
505
|
+
wrapped.getLiveIframe = () => {
|
|
506
|
+
throw new Error(
|
|
507
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help."
|
|
508
|
+
);
|
|
509
|
+
};
|
|
510
|
+
wrapped.getEmbedCode = () => {
|
|
511
|
+
throw new Error(
|
|
512
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help."
|
|
513
|
+
);
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return wrapped;
|
|
406
517
|
}
|
|
407
518
|
function wrapTaskResponseWithSchema(result, config, schema) {
|
|
408
519
|
if (!result.task_id) {
|
|
409
520
|
throw new Error("task_id is required to wrap response");
|
|
410
521
|
}
|
|
411
522
|
const parsed = result.output ? parseStructuredTaskOutput(result, schema) : { ...result, parsed: null };
|
|
412
|
-
|
|
523
|
+
const wrapped = {
|
|
413
524
|
...parsed,
|
|
414
525
|
task_id: result.task_id,
|
|
415
526
|
liveUrl: generateLiveUrl(result.task_id, config),
|
|
@@ -418,6 +529,35 @@ function wrapTaskResponseWithSchema(result, config, schema) {
|
|
|
418
529
|
return parseStructuredTaskOutput(finalResult, schema);
|
|
419
530
|
}
|
|
420
531
|
};
|
|
532
|
+
if (result.debugUrl) {
|
|
533
|
+
wrapped.getLiveUrl = (options) => {
|
|
534
|
+
return buildLiveUrl(result.debugUrl, options);
|
|
535
|
+
};
|
|
536
|
+
wrapped.getLiveIframe = (optionsOrPreset) => {
|
|
537
|
+
const options = resolvePreset(optionsOrPreset);
|
|
538
|
+
return buildLiveIframe(result.debugUrl, options);
|
|
539
|
+
};
|
|
540
|
+
wrapped.getEmbedCode = () => {
|
|
541
|
+
return buildEmbedCode(result.debugUrl);
|
|
542
|
+
};
|
|
543
|
+
} else {
|
|
544
|
+
wrapped.getLiveUrl = () => {
|
|
545
|
+
throw new Error(
|
|
546
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help enabling live sessions. "
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
wrapped.getLiveIframe = () => {
|
|
550
|
+
throw new Error(
|
|
551
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help enabling live sessions."
|
|
552
|
+
);
|
|
553
|
+
};
|
|
554
|
+
wrapped.getEmbedCode = () => {
|
|
555
|
+
throw new Error(
|
|
556
|
+
"Live sessions not available. Your backend must return a debugUrl in the response. Contact support@morphllm.com if you need help enabling live sessions."
|
|
557
|
+
);
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return wrapped;
|
|
421
561
|
}
|
|
422
562
|
async function checkHealth(config = {}) {
|
|
423
563
|
const apiUrl = config.apiUrl || DEFAULT_CONFIG.apiUrl;
|