@morphllm/morphsdk 0.2.102 → 0.2.103

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.
Files changed (109) hide show
  1. package/dist/chunk-2MAUPERG.js +129 -0
  2. package/dist/chunk-2MAUPERG.js.map +1 -0
  3. package/dist/{chunk-QH4BSXOD.js → chunk-4CZPXV6R.js} +8 -7
  4. package/dist/chunk-4CZPXV6R.js.map +1 -0
  5. package/dist/{chunk-AIXF4GQC.js → chunk-AFLEE2PO.js} +2 -2
  6. package/dist/{chunk-CP4NZGRY.js → chunk-EMMSRY32.js} +3 -3
  7. package/dist/chunk-EMMSRY32.js.map +1 -0
  8. package/dist/{chunk-QZNGKOCZ.js → chunk-HDRLLCAD.js} +4 -4
  9. package/dist/{chunk-BKIM7SNY.js → chunk-I3IN742Q.js} +4 -4
  10. package/dist/{chunk-7UYDS6OX.js → chunk-KQP6ZPYB.js} +4 -4
  11. package/dist/{chunk-OTPYEYMZ.js → chunk-MY4OU4ON.js} +2 -2
  12. package/dist/{chunk-UBX7QYBD.js → chunk-O5DA5V5S.js} +4 -4
  13. package/dist/{chunk-GJU7UOFL.js → chunk-OUEJ6XEO.js} +4 -4
  14. package/dist/{chunk-L5WXPMCH.js → chunk-QUIGATZE.js} +2 -2
  15. package/dist/chunk-TLC3QKE6.js +114 -0
  16. package/dist/chunk-TLC3QKE6.js.map +1 -0
  17. package/dist/{chunk-4J6NACK2.js → chunk-Y2OTK5WC.js} +15 -15
  18. package/dist/{chunk-4KMBU6T3.js → chunk-YJZP5ZL5.js} +4 -4
  19. package/dist/{chunk-76DJEQEP.js → chunk-ZRLEAPZV.js} +4 -4
  20. package/dist/{chunk-BGEEES52.js → chunk-ZROQPUCQ.js} +7 -7
  21. package/dist/client.cjs +2922 -2840
  22. package/dist/client.cjs.map +1 -1
  23. package/dist/client.d.ts +1 -0
  24. package/dist/client.js +18 -21
  25. package/dist/edge.cjs +437 -0
  26. package/dist/edge.cjs.map +1 -0
  27. package/dist/edge.d.ts +5 -0
  28. package/dist/edge.js +25 -0
  29. package/dist/edge.js.map +1 -0
  30. package/dist/index.cjs +1981 -1888
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.ts +2 -1
  33. package/dist/index.js +28 -26
  34. package/dist/modelrouter/core.cjs +2 -2
  35. package/dist/modelrouter/core.cjs.map +1 -1
  36. package/dist/modelrouter/core.js +1 -1
  37. package/dist/modelrouter/index.cjs +2 -2
  38. package/dist/modelrouter/index.cjs.map +1 -1
  39. package/dist/modelrouter/index.js +1 -1
  40. package/dist/tools/browser/index.js +3 -3
  41. package/dist/tools/codebase_search/anthropic.js +2 -2
  42. package/dist/tools/codebase_search/index.js +6 -6
  43. package/dist/tools/codebase_search/openai.js +2 -2
  44. package/dist/tools/codebase_search/vercel.js +2 -2
  45. package/dist/tools/fastapply/anthropic.cjs +109 -40
  46. package/dist/tools/fastapply/anthropic.cjs.map +1 -1
  47. package/dist/tools/fastapply/anthropic.js +3 -2
  48. package/dist/tools/fastapply/apply.cjs +227 -0
  49. package/dist/tools/fastapply/apply.cjs.map +1 -0
  50. package/dist/tools/fastapply/apply.d.ts +59 -0
  51. package/dist/tools/fastapply/apply.js +15 -0
  52. package/dist/tools/fastapply/apply.js.map +1 -0
  53. package/dist/tools/fastapply/core.cjs +156 -116
  54. package/dist/tools/fastapply/core.cjs.map +1 -1
  55. package/dist/tools/fastapply/core.d.ts +5 -41
  56. package/dist/tools/fastapply/core.js +6 -2
  57. package/dist/tools/fastapply/index.cjs +113 -76
  58. package/dist/tools/fastapply/index.cjs.map +1 -1
  59. package/dist/tools/fastapply/index.d.ts +2 -1
  60. package/dist/tools/fastapply/index.js +11 -9
  61. package/dist/tools/fastapply/openai.cjs +110 -41
  62. package/dist/tools/fastapply/openai.cjs.map +1 -1
  63. package/dist/tools/fastapply/openai.js +3 -2
  64. package/dist/tools/fastapply/vercel.cjs +110 -41
  65. package/dist/tools/fastapply/vercel.cjs.map +1 -1
  66. package/dist/tools/fastapply/vercel.js +3 -2
  67. package/dist/tools/index.cjs +113 -76
  68. package/dist/tools/index.cjs.map +1 -1
  69. package/dist/tools/index.d.ts +2 -1
  70. package/dist/tools/index.js +11 -9
  71. package/dist/tools/warp_grep/agent/runner.js +3 -3
  72. package/dist/tools/warp_grep/anthropic.cjs +549 -500
  73. package/dist/tools/warp_grep/anthropic.cjs.map +1 -1
  74. package/dist/tools/warp_grep/anthropic.js +5 -9
  75. package/dist/tools/warp_grep/client.cjs +550 -501
  76. package/dist/tools/warp_grep/client.cjs.map +1 -1
  77. package/dist/tools/warp_grep/client.js +4 -8
  78. package/dist/tools/warp_grep/gemini.cjs +549 -500
  79. package/dist/tools/warp_grep/gemini.cjs.map +1 -1
  80. package/dist/tools/warp_grep/gemini.js +4 -8
  81. package/dist/tools/warp_grep/gemini.js.map +1 -1
  82. package/dist/tools/warp_grep/harness.js +12 -12
  83. package/dist/tools/warp_grep/index.cjs +559 -501
  84. package/dist/tools/warp_grep/index.cjs.map +1 -1
  85. package/dist/tools/warp_grep/index.js +12 -12
  86. package/dist/tools/warp_grep/openai.cjs +549 -500
  87. package/dist/tools/warp_grep/openai.cjs.map +1 -1
  88. package/dist/tools/warp_grep/openai.js +5 -9
  89. package/dist/tools/warp_grep/providers/local.js +2 -2
  90. package/dist/tools/warp_grep/vercel.cjs +549 -500
  91. package/dist/tools/warp_grep/vercel.cjs.map +1 -1
  92. package/dist/tools/warp_grep/vercel.js +5 -9
  93. package/package.json +7 -2
  94. package/dist/chunk-CKTA4AXM.js +0 -233
  95. package/dist/chunk-CKTA4AXM.js.map +0 -1
  96. package/dist/chunk-CP4NZGRY.js.map +0 -1
  97. package/dist/chunk-QH4BSXOD.js.map +0 -1
  98. /package/dist/{chunk-AIXF4GQC.js.map → chunk-AFLEE2PO.js.map} +0 -0
  99. /package/dist/{chunk-QZNGKOCZ.js.map → chunk-HDRLLCAD.js.map} +0 -0
  100. /package/dist/{chunk-BKIM7SNY.js.map → chunk-I3IN742Q.js.map} +0 -0
  101. /package/dist/{chunk-7UYDS6OX.js.map → chunk-KQP6ZPYB.js.map} +0 -0
  102. /package/dist/{chunk-OTPYEYMZ.js.map → chunk-MY4OU4ON.js.map} +0 -0
  103. /package/dist/{chunk-UBX7QYBD.js.map → chunk-O5DA5V5S.js.map} +0 -0
  104. /package/dist/{chunk-GJU7UOFL.js.map → chunk-OUEJ6XEO.js.map} +0 -0
  105. /package/dist/{chunk-L5WXPMCH.js.map → chunk-QUIGATZE.js.map} +0 -0
  106. /package/dist/{chunk-4J6NACK2.js.map → chunk-Y2OTK5WC.js.map} +0 -0
  107. /package/dist/{chunk-4KMBU6T3.js.map → chunk-YJZP5ZL5.js.map} +0 -0
  108. /package/dist/{chunk-76DJEQEP.js.map → chunk-ZRLEAPZV.js.map} +0 -0
  109. /package/dist/{chunk-BGEEES52.js.map → chunk-ZROQPUCQ.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,40 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
30
- // index.ts
31
- var index_exports = {};
32
- __export(index_exports, {
33
- AnthropicRouter: () => AnthropicRouter,
34
- AnthropicToolFactory: () => AnthropicToolFactory,
35
- BrowserClient: () => BrowserClient,
36
- CodebaseSearchClient: () => CodebaseSearchClient,
37
- FastApplyClient: () => FastApplyClient,
38
- GeminiRouter: () => GeminiRouter,
39
- LocalRipgrepProvider: () => LocalRipgrepProvider,
40
- MorphClient: () => MorphClient,
41
- MorphGit: () => MorphGit,
42
- OpenAIRouter: () => OpenAIRouter,
43
- OpenAIToolFactory: () => OpenAIToolFactory,
44
- RawRouter: () => RawRouter,
45
- VercelToolFactory: () => VercelToolFactory,
46
- WarpGrepClient: () => WarpGrepClient,
47
- applyEdit: () => applyEdit
48
- });
49
- module.exports = __toCommonJS(index_exports);
50
-
51
- // tools/fastapply/core.ts
52
- var import_promises = require("fs/promises");
53
- var import_path = require("path");
54
- var import_diff = require("diff");
55
-
56
33
  // tools/utils/resilience.ts
57
- var DEFAULT_RETRY_CONFIG = {
58
- maxRetries: 3,
59
- initialDelay: 1e3,
60
- maxDelay: 3e4,
61
- backoffMultiplier: 2,
62
- retryableErrors: ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND"]
63
- };
64
34
  async function fetchWithRetry(url, options, retryConfig = {}) {
65
35
  const {
66
36
  maxRetries = DEFAULT_RETRY_CONFIG.maxRetries,
@@ -126,63 +96,28 @@ async function withTimeout(promise, timeoutMs, errorMessage) {
126
96
  function sleep(ms) {
127
97
  return new Promise((resolve2) => setTimeout(resolve2, ms));
128
98
  }
129
-
130
- // tools/fastapply/core.ts
131
- var DEFAULT_CONFIG = {
132
- morphApiUrl: "https://api.morphllm.com",
133
- baseDir: process.cwd(),
134
- generateUdiff: true,
135
- autoWrite: true,
136
- timeout: 3e4,
137
- debug: false
138
- };
139
- var FastApplyClient = class {
140
- config;
141
- constructor(config = {}) {
142
- this.config = {
143
- morphApiKey: config.apiKey,
144
- morphApiUrl: DEFAULT_CONFIG.morphApiUrl,
145
- debug: config.debug,
146
- timeout: config.timeout || DEFAULT_CONFIG.timeout,
147
- retryConfig: config.retryConfig,
148
- generateUdiff: DEFAULT_CONFIG.generateUdiff,
149
- autoWrite: DEFAULT_CONFIG.autoWrite
99
+ var DEFAULT_RETRY_CONFIG;
100
+ var init_resilience = __esm({
101
+ "tools/utils/resilience.ts"() {
102
+ "use strict";
103
+ DEFAULT_RETRY_CONFIG = {
104
+ maxRetries: 3,
105
+ initialDelay: 1e3,
106
+ maxDelay: 3e4,
107
+ backoffMultiplier: 2,
108
+ retryableErrors: ["ECONNREFUSED", "ETIMEDOUT", "ENOTFOUND"]
150
109
  };
151
110
  }
152
- /**
153
- * Execute a file edit operation
154
- *
155
- * @param input - Edit parameters including filepath, instructions, and code_edit
156
- * @param overrides - Optional config overrides for this operation
157
- * @returns Edit result with success status and changes
158
- */
159
- async execute(input, overrides) {
160
- return executeEditFile(input, { ...this.config, ...overrides });
161
- }
162
- /**
163
- * Apply an edit to code directly without file I/O
164
- *
165
- * Useful for sandbox environments or when you manage your own file system.
166
- * Compatible with the earlier OpenAI client API contract.
167
- *
168
- * @param input - Code and edit parameters
169
- * @param overrides - Optional config overrides for this operation
170
- * @returns Result with merged code
171
- *
172
- * @example
173
- * ```typescript
174
- * const result = await client.applyEdit({
175
- * originalCode: 'function hello() { return "world"; }',
176
- * codeEdit: 'function hello() { return "universe"; }',
177
- * instructions: 'Change return value'
178
- * });
179
- * console.log(result.mergedCode);
180
- * ```
181
- */
182
- async applyEdit(input, overrides) {
183
- return applyEdit(input, { ...this.config, ...overrides });
184
- }
185
- };
111
+ });
112
+
113
+ // tools/fastapply/apply.ts
114
+ var apply_exports = {};
115
+ __export(apply_exports, {
116
+ applyEdit: () => applyEdit,
117
+ callMorphAPI: () => callMorphAPI,
118
+ countChanges: () => countChanges,
119
+ generateUdiff: () => generateUdiff
120
+ });
186
121
  function generateUdiff(original, modified, filepath) {
187
122
  return (0, import_diff.createTwoFilesPatch)(
188
123
  filepath,
@@ -213,9 +148,9 @@ function countChanges(original, modified) {
213
148
  };
214
149
  }
215
150
  async function callMorphAPI(originalCode, codeEdit, instructions, filepath, config) {
216
- const apiKey = config.morphApiKey || process.env.MORPH_API_KEY;
217
- const apiUrl = config.morphApiUrl || DEFAULT_CONFIG.morphApiUrl;
218
- const timeout = config.timeout || DEFAULT_CONFIG.timeout;
151
+ const apiKey = config.morphApiKey || (typeof process !== "undefined" ? process.env?.MORPH_API_KEY : void 0);
152
+ const apiUrl = config.morphApiUrl || DEFAULT_API_URL;
153
+ const timeout = config.timeout || DEFAULT_TIMEOUT;
219
154
  const debug = config.debug || false;
220
155
  if (!apiKey) {
221
156
  throw new Error(
@@ -263,54 +198,6 @@ async function callMorphAPI(originalCode, codeEdit, instructions, filepath, conf
263
198
  }
264
199
  return data.choices[0].message.content;
265
200
  }
266
- async function executeEditFile(input, config = {}) {
267
- const baseDir = config.baseDir || DEFAULT_CONFIG.baseDir;
268
- const fullPath = (0, import_path.resolve)((0, import_path.join)(baseDir, input.target_filepath));
269
- const debug = config.debug || false;
270
- const relativePath = (0, import_path.relative)(baseDir, fullPath);
271
- if (relativePath.startsWith("..") || fullPath === baseDir) {
272
- return {
273
- success: false,
274
- filepath: input.target_filepath,
275
- changes: { linesAdded: 0, linesRemoved: 0, linesModified: 0 },
276
- error: `Invalid filepath: '${input.target_filepath}' is outside baseDir`
277
- };
278
- }
279
- try {
280
- if (debug) console.log(`[FastApply] Reading file: ${input.target_filepath}`);
281
- let originalCode = "";
282
- try {
283
- originalCode = await (0, import_promises.readFile)(fullPath, "utf-8");
284
- } catch (error) {
285
- if (error.code !== "ENOENT") {
286
- throw error;
287
- }
288
- if (debug) console.log(`[FastApply] File doesn't exist, will create new file`);
289
- }
290
- const mergedCode = await callMorphAPI(originalCode, input.code_edit, input.instructions, input.target_filepath, config);
291
- const udiff = config.generateUdiff !== false ? generateUdiff(originalCode, mergedCode, input.target_filepath) : void 0;
292
- if (config.autoWrite !== false) {
293
- await (0, import_promises.writeFile)(fullPath, mergedCode, "utf-8");
294
- if (debug) console.log(`[FastApply] Wrote ${mergedCode.length} chars to ${input.target_filepath}`);
295
- }
296
- const changes = countChanges(originalCode, mergedCode);
297
- return {
298
- success: true,
299
- filepath: input.target_filepath,
300
- udiff,
301
- changes
302
- };
303
- } catch (error) {
304
- const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
305
- if (debug) console.error(`[FastApply] Error: ${errorMessage}`);
306
- return {
307
- success: false,
308
- filepath: input.target_filepath,
309
- changes: { linesAdded: 0, linesRemoved: 0, linesModified: 0 },
310
- error: errorMessage
311
- };
312
- }
313
- }
314
201
  async function applyEdit(input, config = {}) {
315
202
  const debug = config.debug || false;
316
203
  const filepath = input.filepath || "file";
@@ -343,1198 +230,1880 @@ async function applyEdit(input, config = {}) {
343
230
  };
344
231
  }
345
232
  }
233
+ var import_diff, DEFAULT_API_URL, DEFAULT_TIMEOUT;
234
+ var init_apply = __esm({
235
+ "tools/fastapply/apply.ts"() {
236
+ "use strict";
237
+ import_diff = require("diff");
238
+ init_resilience();
239
+ DEFAULT_API_URL = "https://api.morphllm.com";
240
+ DEFAULT_TIMEOUT = 3e4;
241
+ }
242
+ });
346
243
 
347
- // tools/codebase_search/core.ts
348
- var CodebaseSearchClient = class {
349
- config;
350
- constructor(config = {}) {
351
- this.config = {
352
- apiKey: config.apiKey,
353
- searchUrl: process.env.MORPH_SEARCH_URL || "https://repos.morphllm.com",
354
- debug: config.debug,
355
- timeout: config.timeout || 3e4,
356
- retryConfig: config.retryConfig
244
+ // tools/warp_grep/agent/config.ts
245
+ var parseEnvTimeout, AGENT_CONFIG, BUILTIN_EXCLUDES, DEFAULT_EXCLUDES, DEFAULT_MODEL;
246
+ var init_config = __esm({
247
+ "tools/warp_grep/agent/config.ts"() {
248
+ "use strict";
249
+ parseEnvTimeout = (envValue, defaultMs) => {
250
+ if (!envValue) return defaultMs;
251
+ const parsed = parseInt(envValue, 10);
252
+ return isNaN(parsed) || parsed <= 0 ? defaultMs : parsed;
357
253
  };
254
+ AGENT_CONFIG = {
255
+ MAX_TURNS: 4,
256
+ /** Default timeout for model calls. Can be overridden via MORPH_WARP_GREP_TIMEOUT env var (in ms) */
257
+ TIMEOUT_MS: parseEnvTimeout(process.env.MORPH_WARP_GREP_TIMEOUT, 3e4),
258
+ MAX_CONTEXT_CHARS: 54e4,
259
+ MAX_OUTPUT_LINES: 200,
260
+ MAX_READ_LINES: 800,
261
+ MAX_LIST_DEPTH: 3,
262
+ LIST_TIMEOUT_MS: 2e3
263
+ };
264
+ BUILTIN_EXCLUDES = [
265
+ // Version control
266
+ ".git",
267
+ ".svn",
268
+ ".hg",
269
+ ".bzr",
270
+ // Dependencies
271
+ "node_modules",
272
+ "bower_components",
273
+ ".pnpm",
274
+ ".yarn",
275
+ "vendor",
276
+ "packages",
277
+ "Pods",
278
+ ".bundle",
279
+ // Python
280
+ "__pycache__",
281
+ ".pytest_cache",
282
+ ".mypy_cache",
283
+ ".ruff_cache",
284
+ ".venv",
285
+ "venv",
286
+ ".tox",
287
+ ".nox",
288
+ ".eggs",
289
+ "*.egg-info",
290
+ // Build outputs
291
+ "dist",
292
+ "build",
293
+ "out",
294
+ "output",
295
+ "target",
296
+ "_build",
297
+ ".next",
298
+ ".nuxt",
299
+ ".output",
300
+ ".vercel",
301
+ ".netlify",
302
+ // Cache directories
303
+ ".cache",
304
+ ".parcel-cache",
305
+ ".turbo",
306
+ ".nx",
307
+ ".gradle",
308
+ // IDE/Editor
309
+ ".idea",
310
+ ".vscode",
311
+ ".vs",
312
+ // Coverage
313
+ "coverage",
314
+ ".coverage",
315
+ "htmlcov",
316
+ ".nyc_output",
317
+ // Temporary
318
+ "tmp",
319
+ "temp",
320
+ ".tmp",
321
+ ".temp",
322
+ // Lock files
323
+ "package-lock.json",
324
+ "yarn.lock",
325
+ "pnpm-lock.yaml",
326
+ "bun.lockb",
327
+ "Cargo.lock",
328
+ "Gemfile.lock",
329
+ "poetry.lock",
330
+ // Binary/minified
331
+ "*.min.js",
332
+ "*.min.css",
333
+ "*.bundle.js",
334
+ "*.wasm",
335
+ "*.so",
336
+ "*.dll",
337
+ "*.pyc",
338
+ "*.map",
339
+ "*.js.map",
340
+ // Hidden directories catch-all
341
+ ".*"
342
+ ];
343
+ DEFAULT_EXCLUDES = (process.env.MORPH_WARP_GREP_EXCLUDE || "").split(",").map((s) => s.trim()).filter(Boolean).concat(BUILTIN_EXCLUDES);
344
+ DEFAULT_MODEL = "morph-warp-grep-v1";
358
345
  }
359
- /**
360
- * Execute a semantic code search
361
- *
362
- * @param input - Search parameters including query, repoId, and target directories
363
- * @param overrides - Optional config overrides for this operation
364
- * @returns Search results with ranked code matches
365
- */
366
- async search(input, overrides) {
367
- return executeCodebaseSearch(
368
- {
369
- query: input.query,
370
- target_directories: input.target_directories,
371
- explanation: input.explanation,
372
- limit: input.limit
373
- },
374
- { ...this.config, repoId: input.repoId, ...overrides }
375
- );
376
- }
377
- };
378
- async function executeCodebaseSearch(input, config) {
379
- const apiKey = config.apiKey || process.env.MORPH_API_KEY;
380
- if (!apiKey) {
381
- throw new Error("MORPH_API_KEY not found. Set environment variable or pass in config");
382
- }
383
- const searchUrl = config.searchUrl || process.env.MORPH_SEARCH_URL || "https://repos.morphllm.com";
384
- const timeout = config.timeout || 3e4;
385
- const debug = config.debug || false;
386
- if (debug) {
387
- console.log(`[CodebaseSearch] Query: "${input.query.slice(0, 60)}..." repo=${config.repoId}`);
388
- console.log(`[CodebaseSearch] URL: ${searchUrl}/v1/codebase_search`);
346
+ });
347
+
348
+ // tools/warp_grep/utils/ripgrep.ts
349
+ function spawnRg(rgBinary, args, opts) {
350
+ return new Promise((resolve2) => {
351
+ const child = (0, import_child_process.spawn)(rgBinary, args, {
352
+ cwd: opts?.cwd,
353
+ env: { ...process.env, ...opts?.env || {} },
354
+ stdio: ["ignore", "pipe", "pipe"]
355
+ });
356
+ let stdout = "";
357
+ let stderr = "";
358
+ child.stdout.on("data", (d) => stdout += d.toString());
359
+ child.stderr.on("data", (d) => stderr += d.toString());
360
+ child.on("close", (code) => {
361
+ resolve2({ stdout, stderr, exitCode: typeof code === "number" ? code : -1 });
362
+ });
363
+ child.on("error", () => {
364
+ resolve2({ stdout: "", stderr: "Failed to spawn ripgrep.", exitCode: -1 });
365
+ });
366
+ });
367
+ }
368
+ function isBinaryFailure(result) {
369
+ if (result.exitCode === -1) return true;
370
+ if (result.stderr.includes("jemalloc") || result.stderr.includes("Unsupported system page size")) return true;
371
+ if (result.exitCode === 134) return true;
372
+ return false;
373
+ }
374
+ async function runRipgrep(args, opts) {
375
+ if (rgPathChecked && resolvedRgPath) {
376
+ return spawnRg(resolvedRgPath, args, opts);
389
377
  }
390
- const startTime = Date.now();
391
- try {
392
- const fetchPromise = fetchWithRetry(
393
- `${searchUrl}/v1/codebase_search`,
394
- {
395
- method: "POST",
396
- headers: {
397
- "Content-Type": "application/json",
398
- "Authorization": `Bearer ${apiKey}`
399
- },
400
- body: JSON.stringify({
401
- query: input.query,
402
- repoId: config.repoId,
403
- targetDirectories: input.target_directories || [],
404
- limit: input.limit || 10,
405
- candidateLimit: 50
406
- })
407
- },
408
- config.retryConfig
409
- );
410
- const response = await withTimeout(fetchPromise, timeout, `Codebase search timed out after ${timeout}ms`);
411
- if (!response.ok) {
412
- const errorText = await response.text();
413
- if (debug) console.error(`[CodebaseSearch] Error: ${response.status} - ${errorText}`);
414
- return {
415
- success: false,
416
- results: [],
417
- stats: { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: 0 },
418
- error: `Search failed (${response.status}): ${errorText}`
419
- };
378
+ if (!rgPathChecked) {
379
+ const result = await spawnRg(import_ripgrep.rgPath, args, opts);
380
+ if (!isBinaryFailure(result)) {
381
+ resolvedRgPath = import_ripgrep.rgPath;
382
+ rgPathChecked = true;
383
+ return result;
420
384
  }
421
- const data = await response.json();
422
- const elapsed = Date.now() - startTime;
423
- if (debug) {
424
- console.log(`[CodebaseSearch] \u2705 ${data.results?.length || 0} results in ${elapsed}ms`);
385
+ const fallbackResult = await spawnRg("rg", args, opts);
386
+ if (!isBinaryFailure(fallbackResult)) {
387
+ resolvedRgPath = "rg";
388
+ rgPathChecked = true;
389
+ return fallbackResult;
425
390
  }
391
+ rgPathChecked = true;
426
392
  return {
427
- success: true,
428
- results: data.results || [],
429
- stats: data.stats || { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: elapsed }
430
- };
431
- } catch (error) {
432
- if (debug) console.error(`[CodebaseSearch] Exception: ${error instanceof Error ? error.message : error}`);
433
- return {
434
- success: false,
435
- results: [],
436
- stats: { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: 0 },
437
- error: error instanceof Error ? error.message : "Unknown error"
393
+ stdout: "",
394
+ stderr: "Failed to spawn ripgrep. Neither bundled nor system rg is available.",
395
+ exitCode: -1
438
396
  };
439
397
  }
398
+ return {
399
+ stdout: "",
400
+ stderr: "Failed to spawn ripgrep. Neither bundled nor system rg is available.",
401
+ exitCode: -1
402
+ };
440
403
  }
441
-
442
- // tools/browser/live.ts
443
- var LIVE_PRESETS = {
444
- /** Read-only monitoring (no interaction) */
445
- readonly: { interactive: false },
446
- /** Interactive control (human-in-the-loop) */
447
- interactive: { interactive: true },
448
- /** Watch-only without controls */
449
- monitoring: { interactive: false, showControls: false }
450
- };
451
- function buildLiveUrl(debugUrl, options = {}) {
452
- if (!debugUrl) {
453
- throw new Error(
454
- "debugUrl is required. Ensure your backend returns debugUrl in the task response. Contact support@morphllm.com if you need help."
455
- );
456
- }
457
- const normalized = normalizeLiveUrl(debugUrl);
458
- const url = new URL(normalized);
459
- if (options.interactive !== void 0) {
460
- url.searchParams.set("interactive", String(options.interactive));
461
- }
462
- if (options.theme) {
463
- url.searchParams.set("theme", options.theme);
464
- }
465
- if (options.showControls !== void 0) {
466
- url.searchParams.set("showControls", String(options.showControls));
467
- }
468
- if (options.pageId) {
469
- url.searchParams.set("pageId", options.pageId);
404
+ var import_child_process, import_ripgrep, resolvedRgPath, rgPathChecked;
405
+ var init_ripgrep = __esm({
406
+ "tools/warp_grep/utils/ripgrep.ts"() {
407
+ "use strict";
408
+ import_child_process = require("child_process");
409
+ import_ripgrep = require("@vscode/ripgrep");
410
+ resolvedRgPath = null;
411
+ rgPathChecked = false;
470
412
  }
471
- if (options.pageIndex) {
472
- url.searchParams.set("pageIndex", options.pageIndex);
473
- }
474
- return url.toString();
413
+ });
414
+
415
+ // tools/warp_grep/utils/paths.ts
416
+ function resolveUnderRepo(repoRoot, targetPath) {
417
+ const absRoot = import_path4.default.resolve(repoRoot);
418
+ const resolved = import_path4.default.resolve(absRoot, targetPath);
419
+ ensureWithinRepo(absRoot, resolved);
420
+ return resolved;
475
421
  }
476
- function normalizeLiveUrl(debugUrl) {
477
- const trimmed = debugUrl.trim();
478
- if (!trimmed) {
479
- return trimmed;
480
- }
481
- if (trimmed.startsWith("wss://") || trimmed.startsWith("ws://")) {
482
- return `https://live.browser-use.com?wss=${encodeURIComponent(trimmed)}`;
422
+ function ensureWithinRepo(repoRoot, absTarget) {
423
+ const rel = import_path4.default.relative(import_path4.default.resolve(repoRoot), import_path4.default.resolve(absTarget));
424
+ if (rel.startsWith("..") || import_path4.default.isAbsolute(rel)) {
425
+ throw new Error(`Path outside repository root: ${absTarget}`);
483
426
  }
484
- let url;
427
+ }
428
+ function toRepoRelative(repoRoot, absPath) {
429
+ return import_path4.default.relative(import_path4.default.resolve(repoRoot), import_path4.default.resolve(absPath));
430
+ }
431
+ function isSymlink(p) {
485
432
  try {
486
- url = new URL(trimmed);
433
+ const st = import_fs.default.lstatSync(p);
434
+ return st.isSymbolicLink();
487
435
  } catch {
488
- return trimmed;
489
- }
490
- if (url.protocol === "wss:" || url.protocol === "ws:") {
491
- return `https://live.browser-use.com?wss=${encodeURIComponent(trimmed)}`;
492
- }
493
- const wssParam = url.searchParams.get("wss");
494
- if (wssParam && (wssParam.startsWith("wss://") || wssParam.startsWith("ws://"))) {
495
- url.searchParams.set("wss", wssParam);
496
- }
497
- return url.toString();
498
- }
499
- function buildLiveIframe(debugUrl, options = {}) {
500
- const {
501
- width = "100%",
502
- height = "600px",
503
- style = "",
504
- className = "",
505
- ...sessionOptions
506
- } = options;
507
- const src = buildLiveUrl(debugUrl, sessionOptions);
508
- const widthStr = typeof width === "number" ? `${width}px` : width;
509
- const heightStr = typeof height === "number" ? `${height}px` : height;
510
- const baseStyle = `width: ${widthStr}; height: ${heightStr}; border: none;`;
511
- const fullStyle = style ? `${baseStyle} ${style}` : baseStyle;
512
- const attributes = [
513
- `src="${src}"`,
514
- `style="${fullStyle}"`
515
- ];
516
- if (className) {
517
- attributes.push(`class="${className}"`);
436
+ return false;
518
437
  }
519
- return `<iframe ${attributes.join(" ")}></iframe>`;
520
438
  }
521
- function buildEmbedCode(debugUrl, options = {}) {
522
- const iframe = buildLiveIframe(debugUrl, options);
523
- return `<!-- Embed Morph Live Session -->
524
- ${iframe}`;
525
- }
526
- function resolvePreset(optionsOrPreset) {
527
- if (!optionsOrPreset) {
528
- return {};
529
- }
530
- if (typeof optionsOrPreset === "string") {
531
- const preset = LIVE_PRESETS[optionsOrPreset];
532
- if (!preset) {
533
- throw new Error(
534
- `Unknown preset: ${optionsOrPreset}. Available presets: ${Object.keys(LIVE_PRESETS).join(", ")}`
535
- );
439
+ function fixPathRepetition(fullPath) {
440
+ const segments = fullPath.split(import_path4.default.sep).filter(Boolean);
441
+ if (segments.length < 2) return null;
442
+ for (let len = Math.floor(segments.length / 2); len >= 1; len--) {
443
+ for (let i = 0; i <= segments.length - 2 * len; i++) {
444
+ const first = segments.slice(i, i + len);
445
+ const second = segments.slice(i + len, i + 2 * len);
446
+ if (first.every((seg, idx) => seg === second[idx])) {
447
+ const fixed = [...segments.slice(0, i), ...segments.slice(i + len)];
448
+ return import_path4.default.sep + fixed.join(import_path4.default.sep);
449
+ }
536
450
  }
537
- return preset;
538
451
  }
539
- return optionsOrPreset;
452
+ return null;
540
453
  }
541
-
542
- // tools/browser/errors.ts
543
- var MorphError = class extends Error {
544
- /** Error code for programmatic handling */
545
- code;
546
- /** Original cause of the error, if any */
547
- cause;
548
- constructor(message, code, cause) {
549
- super(message);
550
- this.name = "MorphError";
551
- this.code = code;
552
- this.cause = cause;
553
- if (Error.captureStackTrace) {
554
- Error.captureStackTrace(this, this.constructor);
454
+ function isTextualFile(filePath, maxBytes = 2e6) {
455
+ try {
456
+ const st = import_fs.default.statSync(filePath);
457
+ if (!st.isFile()) return false;
458
+ if (st.size > maxBytes) return false;
459
+ const fd = import_fs.default.openSync(filePath, "r");
460
+ const buf = Buffer.alloc(512);
461
+ const read = import_fs.default.readSync(fd, buf, 0, buf.length, 0);
462
+ import_fs.default.closeSync(fd);
463
+ for (let i = 0; i < read; i++) {
464
+ const c = buf[i];
465
+ if (c === 0) return false;
555
466
  }
467
+ return true;
468
+ } catch {
469
+ return false;
556
470
  }
557
- /**
558
- * Returns a JSON representation of the error for logging.
559
- */
560
- toJSON() {
561
- return {
562
- name: this.name,
563
- message: this.message,
564
- code: this.code,
565
- cause: this.cause?.message
566
- };
567
- }
568
- };
569
- var MorphValidationError = class extends MorphError {
570
- /** The field that failed validation */
571
- field;
572
- constructor(message, field) {
573
- super(message, "validation_error");
574
- this.name = "MorphValidationError";
575
- this.field = field;
471
+ }
472
+ var import_fs, import_path4;
473
+ var init_paths = __esm({
474
+ "tools/warp_grep/utils/paths.ts"() {
475
+ "use strict";
476
+ import_fs = __toESM(require("fs"), 1);
477
+ import_path4 = __toESM(require("path"), 1);
576
478
  }
577
- toJSON() {
578
- return {
579
- ...super.toJSON(),
580
- field: this.field
581
- };
479
+ });
480
+
481
+ // tools/warp_grep/utils/files.ts
482
+ async function readAllLines(filePath) {
483
+ const content = await import_promises.default.readFile(filePath, "utf8");
484
+ return content.split(/\r?\n/);
485
+ }
486
+ var import_promises;
487
+ var init_files = __esm({
488
+ "tools/warp_grep/utils/files.ts"() {
489
+ "use strict";
490
+ import_promises = __toESM(require("fs/promises"), 1);
582
491
  }
583
- };
584
- var MorphAPIError = class extends MorphError {
585
- /** HTTP status code */
586
- statusCode;
587
- /** Request ID for debugging (if available) */
588
- requestId;
589
- /** Raw response body */
590
- rawResponse;
591
- constructor(message, code, statusCode, options) {
592
- super(message, code, options?.cause);
593
- this.name = "MorphAPIError";
594
- this.statusCode = statusCode;
595
- this.requestId = options?.requestId;
596
- this.rawResponse = options?.rawResponse;
492
+ });
493
+
494
+ // tools/warp_grep/providers/local.ts
495
+ var local_exports = {};
496
+ __export(local_exports, {
497
+ LocalRipgrepProvider: () => LocalRipgrepProvider
498
+ });
499
+ function shouldSkip2(name) {
500
+ if (SKIP_NAMES2.has(name)) return true;
501
+ if (name.startsWith(".")) return true;
502
+ for (const ext of SKIP_EXTENSIONS2) {
503
+ if (name.endsWith(ext)) return true;
597
504
  }
598
- toJSON() {
599
- return {
600
- ...super.toJSON(),
601
- statusCode: this.statusCode,
602
- requestId: this.requestId
505
+ return false;
506
+ }
507
+ var import_promises2, import_path5, SKIP_NAMES2, SKIP_EXTENSIONS2, LocalRipgrepProvider;
508
+ var init_local = __esm({
509
+ "tools/warp_grep/providers/local.ts"() {
510
+ "use strict";
511
+ import_promises2 = __toESM(require("fs/promises"), 1);
512
+ import_path5 = __toESM(require("path"), 1);
513
+ init_ripgrep();
514
+ init_paths();
515
+ init_files();
516
+ init_config();
517
+ SKIP_NAMES2 = /* @__PURE__ */ new Set([
518
+ // Version control
519
+ ".git",
520
+ ".svn",
521
+ ".hg",
522
+ ".bzr",
523
+ // Dependencies
524
+ "node_modules",
525
+ "bower_components",
526
+ ".pnpm",
527
+ ".yarn",
528
+ "vendor",
529
+ "Pods",
530
+ ".bundle",
531
+ // Python
532
+ "__pycache__",
533
+ ".pytest_cache",
534
+ ".mypy_cache",
535
+ ".ruff_cache",
536
+ ".venv",
537
+ "venv",
538
+ ".tox",
539
+ ".nox",
540
+ ".eggs",
541
+ // Build outputs
542
+ "dist",
543
+ "build",
544
+ "out",
545
+ "output",
546
+ "target",
547
+ "_build",
548
+ ".next",
549
+ ".nuxt",
550
+ ".output",
551
+ ".vercel",
552
+ ".netlify",
553
+ // Cache
554
+ ".cache",
555
+ ".parcel-cache",
556
+ ".turbo",
557
+ ".nx",
558
+ ".gradle",
559
+ // IDE
560
+ ".idea",
561
+ ".vscode",
562
+ ".vs",
563
+ // Coverage
564
+ "coverage",
565
+ ".coverage",
566
+ "htmlcov",
567
+ ".nyc_output",
568
+ // Temp
569
+ "tmp",
570
+ "temp",
571
+ ".tmp",
572
+ ".temp",
573
+ // Lock files
574
+ "package-lock.json",
575
+ "yarn.lock",
576
+ "pnpm-lock.yaml",
577
+ "bun.lockb",
578
+ "Cargo.lock",
579
+ "Gemfile.lock",
580
+ "poetry.lock"
581
+ ]);
582
+ SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([
583
+ ".min.js",
584
+ ".min.css",
585
+ ".bundle.js",
586
+ ".wasm",
587
+ ".so",
588
+ ".dll",
589
+ ".pyc",
590
+ ".map",
591
+ ".js.map"
592
+ ]);
593
+ LocalRipgrepProvider = class {
594
+ constructor(repoRoot, excludes = DEFAULT_EXCLUDES) {
595
+ this.repoRoot = repoRoot;
596
+ this.excludes = excludes;
597
+ }
598
+ async grep(params) {
599
+ let abs;
600
+ try {
601
+ abs = resolveUnderRepo(this.repoRoot, params.path);
602
+ } catch (err) {
603
+ return {
604
+ lines: [],
605
+ error: `[PATH ERROR] ${err instanceof Error ? err.message : String(err)}`
606
+ };
607
+ }
608
+ const stat = await import_promises2.default.stat(abs).catch(() => null);
609
+ if (!stat) return { lines: [] };
610
+ const targetArg = abs === import_path5.default.resolve(this.repoRoot) ? "." : toRepoRelative(this.repoRoot, abs);
611
+ const args = [
612
+ "--no-config",
613
+ "--no-heading",
614
+ "--with-filename",
615
+ "--line-number",
616
+ "--color=never",
617
+ "--trim",
618
+ "--max-columns=400",
619
+ "-C",
620
+ "1",
621
+ ...params.glob ? ["--glob", params.glob] : [],
622
+ ...this.excludes.flatMap((e) => ["-g", `!${e}`]),
623
+ params.pattern,
624
+ targetArg || "."
625
+ ];
626
+ const res = await runRipgrep(args, { cwd: this.repoRoot });
627
+ if (res.exitCode === -1) {
628
+ return {
629
+ lines: [],
630
+ error: `[RIPGREP NOT AVAILABLE] ripgrep (rg) is required but failed to execute. Please install it:
631
+ \u2022 macOS: brew install ripgrep
632
+ \u2022 Ubuntu/Debian: apt install ripgrep
633
+ \u2022 Windows: choco install ripgrep
634
+ \u2022 Or visit: https://github.com/BurntSushi/ripgrep#installation
635
+ Exit code: ${res.exitCode}${res.stderr ? `
636
+ Details: ${res.stderr}` : ""}`
637
+ };
638
+ }
639
+ if (res.exitCode !== 0 && res.exitCode !== 1) {
640
+ return {
641
+ lines: [],
642
+ error: `[RIPGREP ERROR] grep failed with exit code ${res.exitCode}${res.stderr ? `: ${res.stderr}` : ""}`
643
+ };
644
+ }
645
+ const lines = (res.stdout || "").trim().split(/\r?\n/).filter((l) => l.length > 0);
646
+ if (lines.length > AGENT_CONFIG.MAX_OUTPUT_LINES) {
647
+ return {
648
+ lines: [],
649
+ error: "query not specific enough, tool tried to return too much context and failed"
650
+ };
651
+ }
652
+ return { lines };
653
+ }
654
+ async read(params) {
655
+ let abs;
656
+ try {
657
+ abs = resolveUnderRepo(this.repoRoot, params.path);
658
+ } catch (err) {
659
+ return {
660
+ lines: [],
661
+ error: `[PATH ERROR] ${err instanceof Error ? err.message : String(err)}`
662
+ };
663
+ }
664
+ let stat = await import_promises2.default.stat(abs).catch(() => null);
665
+ if (!stat || !stat.isFile()) {
666
+ const fixedPath = fixPathRepetition(abs);
667
+ if (fixedPath) {
668
+ const fixedStat = await import_promises2.default.stat(fixedPath).catch(() => null);
669
+ if (fixedStat?.isFile()) {
670
+ abs = fixedPath;
671
+ stat = fixedStat;
672
+ }
673
+ }
674
+ }
675
+ if (!stat || !stat.isFile()) {
676
+ return {
677
+ lines: [],
678
+ error: `[FILE NOT FOUND] You tried to read "${params.path}" but there is no file at this path. Double-check the path exists and is spelled correctly.`
679
+ };
680
+ }
681
+ if (isSymlink(abs)) {
682
+ return {
683
+ lines: [],
684
+ error: `[SYMLINK] You tried to read "${params.path}" but this is a symlink. Try reading the actual file it points to instead.`
685
+ };
686
+ }
687
+ if (!isTextualFile(abs)) {
688
+ return {
689
+ lines: [],
690
+ error: `[UNREADABLE FILE] You tried to read "${params.path}" but this file is either too large or not a text file, so it cannot be read. Try a different file.`
691
+ };
692
+ }
693
+ let lines;
694
+ try {
695
+ lines = await readAllLines(abs);
696
+ } catch (err) {
697
+ return {
698
+ lines: [],
699
+ error: `[READ ERROR] Failed to read "${params.path}": ${err instanceof Error ? err.message : String(err)}`
700
+ };
701
+ }
702
+ const total = lines.length;
703
+ const rawStart = params.start;
704
+ const rawEnd = params.end;
705
+ let s = 1;
706
+ let e = total;
707
+ const startValid = rawStart === void 0 || Number.isFinite(rawStart) && rawStart > 0;
708
+ const endValid = rawEnd === void 0 || Number.isFinite(rawEnd) && rawEnd > 0;
709
+ if (startValid && endValid) {
710
+ s = rawStart ?? 1;
711
+ e = Math.min(rawEnd ?? total, total);
712
+ if (s > total && total > 0 || s > e) {
713
+ s = 1;
714
+ e = total;
715
+ }
716
+ }
717
+ const out = [];
718
+ for (let i = s; i <= e; i += 1) {
719
+ const content = lines[i - 1] ?? "";
720
+ out.push(`${i}|${content}`);
721
+ }
722
+ if (out.length > AGENT_CONFIG.MAX_READ_LINES) {
723
+ const truncated = out.slice(0, AGENT_CONFIG.MAX_READ_LINES);
724
+ truncated.push(`... [truncated: showing ${AGENT_CONFIG.MAX_READ_LINES} of ${out.length} lines]`);
725
+ return { lines: truncated };
726
+ }
727
+ return { lines: out };
728
+ }
729
+ async listDirectory(params) {
730
+ let abs;
731
+ try {
732
+ abs = resolveUnderRepo(this.repoRoot, params.path);
733
+ } catch {
734
+ return [];
735
+ }
736
+ const stat = await import_promises2.default.stat(abs).catch(() => null);
737
+ if (!stat || !stat.isDirectory()) {
738
+ return [];
739
+ }
740
+ const maxResults = params.maxResults ?? AGENT_CONFIG.MAX_OUTPUT_LINES;
741
+ const maxDepth = params.maxDepth ?? AGENT_CONFIG.MAX_LIST_DEPTH;
742
+ const regex = params.pattern ? new RegExp(params.pattern) : null;
743
+ const repoRoot = this.repoRoot;
744
+ const results = [];
745
+ let timedOut = false;
746
+ const startTime = Date.now();
747
+ async function walk(dir, depth) {
748
+ if (Date.now() - startTime > AGENT_CONFIG.LIST_TIMEOUT_MS) {
749
+ timedOut = true;
750
+ return;
751
+ }
752
+ if (depth > maxDepth || results.length >= maxResults) return;
753
+ const entries = await import_promises2.default.readdir(dir, { withFileTypes: true });
754
+ for (const entry of entries) {
755
+ if (timedOut || results.length >= maxResults) break;
756
+ if (shouldSkip2(entry.name)) continue;
757
+ if (regex && !regex.test(entry.name)) continue;
758
+ const full = import_path5.default.join(dir, entry.name);
759
+ const isDir = entry.isDirectory();
760
+ results.push({
761
+ name: entry.name,
762
+ path: toRepoRelative(repoRoot, full),
763
+ type: isDir ? "dir" : "file",
764
+ depth
765
+ });
766
+ if (isDir) {
767
+ await walk(full, depth + 1);
768
+ }
769
+ }
770
+ }
771
+ await walk(abs, 0);
772
+ return results;
773
+ }
603
774
  };
604
775
  }
776
+ });
777
+
778
+ // index.ts
779
+ var index_exports = {};
780
+ __export(index_exports, {
781
+ AnthropicRouter: () => AnthropicRouter,
782
+ AnthropicToolFactory: () => AnthropicToolFactory,
783
+ BrowserClient: () => BrowserClient,
784
+ CodebaseSearchClient: () => CodebaseSearchClient,
785
+ FastApplyClient: () => FastApplyClient,
786
+ GeminiRouter: () => GeminiRouter,
787
+ LocalRipgrepProvider: () => LocalRipgrepProvider,
788
+ MorphClient: () => MorphClient,
789
+ MorphGit: () => MorphGit,
790
+ OpenAIRouter: () => OpenAIRouter,
791
+ OpenAIToolFactory: () => OpenAIToolFactory,
792
+ RawRouter: () => RawRouter,
793
+ VercelToolFactory: () => VercelToolFactory,
794
+ WarpGrepClient: () => WarpGrepClient,
795
+ applyEdit: () => applyEdit
796
+ });
797
+ module.exports = __toCommonJS(index_exports);
798
+
799
+ // tools/fastapply/core.ts
800
+ var import_path = require("path");
801
+ init_apply();
802
+ var DEFAULT_CONFIG = {
803
+ morphApiUrl: "https://api.morphllm.com",
804
+ baseDir: process.cwd(),
805
+ generateUdiff: true,
806
+ autoWrite: true,
807
+ timeout: 3e4,
808
+ debug: false
605
809
  };
606
- var MorphAuthenticationError = class extends MorphAPIError {
607
- constructor(message = "Authentication required. Please provide a valid API key.") {
608
- super(message, "authentication_required", 401);
609
- this.name = "MorphAuthenticationError";
610
- }
611
- };
612
- var MorphRateLimitError = class extends MorphAPIError {
613
- /** When the rate limit resets (Unix timestamp) */
614
- resetAt;
615
- /** Number of seconds until reset */
616
- retryAfter;
617
- constructor(message = "Rate limit exceeded. Please retry later.", options) {
618
- super(message, "rate_limit_exceeded", 429, { requestId: options?.requestId });
619
- this.name = "MorphRateLimitError";
620
- this.resetAt = options?.resetAt;
621
- this.retryAfter = options?.retryAfter;
622
- }
623
- toJSON() {
624
- return {
625
- ...super.toJSON(),
626
- resetAt: this.resetAt,
627
- retryAfter: this.retryAfter
810
+ var FastApplyClient = class {
811
+ config;
812
+ constructor(config = {}) {
813
+ this.config = {
814
+ morphApiKey: config.apiKey,
815
+ morphApiUrl: DEFAULT_CONFIG.morphApiUrl,
816
+ debug: config.debug,
817
+ timeout: config.timeout || DEFAULT_CONFIG.timeout,
818
+ retryConfig: config.retryConfig,
819
+ generateUdiff: DEFAULT_CONFIG.generateUdiff,
820
+ autoWrite: DEFAULT_CONFIG.autoWrite
628
821
  };
629
822
  }
630
- };
631
- var MorphNotFoundError = class extends MorphAPIError {
632
- /** The type of resource that was not found */
633
- resourceType;
634
- /** The ID of the resource that was not found */
635
- resourceId;
636
- constructor(resourceType, resourceId) {
637
- const message = resourceId ? `${resourceType} '${resourceId}' not found` : `${resourceType} not found`;
638
- super(message, "resource_not_found", 404);
639
- this.name = "MorphNotFoundError";
640
- this.resourceType = resourceType;
641
- this.resourceId = resourceId;
823
+ /**
824
+ * Execute a file edit operation
825
+ *
826
+ * @param input - Edit parameters including filepath, instructions, and code_edit
827
+ * @param overrides - Optional config overrides for this operation
828
+ * @returns Edit result with success status and changes
829
+ */
830
+ async execute(input, overrides) {
831
+ return executeEditFile(input, { ...this.config, ...overrides });
642
832
  }
643
- toJSON() {
644
- return {
645
- ...super.toJSON(),
646
- resourceType: this.resourceType,
647
- resourceId: this.resourceId
648
- };
833
+ /**
834
+ * Apply an edit to code directly without file I/O
835
+ *
836
+ * Useful for sandbox environments or when you manage your own file system.
837
+ * Compatible with the earlier OpenAI client API contract.
838
+ *
839
+ * @param input - Code and edit parameters
840
+ * @param overrides - Optional config overrides for this operation
841
+ * @returns Result with merged code
842
+ *
843
+ * @example
844
+ * ```typescript
845
+ * const result = await client.applyEdit({
846
+ * originalCode: 'function hello() { return "world"; }',
847
+ * codeEdit: 'function hello() { return "universe"; }',
848
+ * instructions: 'Change return value'
849
+ * });
850
+ * console.log(result.mergedCode);
851
+ * ```
852
+ */
853
+ async applyEdit(input, overrides) {
854
+ const { applyEdit: applyEdit2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
855
+ return applyEdit2(input, { ...this.config, ...overrides });
649
856
  }
650
857
  };
651
- var MorphProfileLimitError = class extends MorphAPIError {
652
- /** Current number of profiles */
653
- currentCount;
654
- /** Maximum allowed profiles for the plan */
655
- maxAllowed;
656
- constructor(message = "Profile limit exceeded for your plan.", options) {
657
- super(message, "profile_limit_exceeded", 403, { requestId: options?.requestId });
658
- this.name = "MorphProfileLimitError";
659
- this.currentCount = options?.currentCount;
660
- this.maxAllowed = options?.maxAllowed;
661
- }
662
- toJSON() {
858
+ async function executeEditFile(input, config = {}) {
859
+ const baseDir = config.baseDir || DEFAULT_CONFIG.baseDir;
860
+ const fullPath = (0, import_path.resolve)((0, import_path.join)(baseDir, input.target_filepath));
861
+ const debug = config.debug || false;
862
+ const relativePath = (0, import_path.relative)(baseDir, fullPath);
863
+ if (relativePath.startsWith("..") || fullPath === baseDir) {
663
864
  return {
664
- ...super.toJSON(),
665
- currentCount: this.currentCount,
666
- maxAllowed: this.maxAllowed
865
+ success: false,
866
+ filepath: input.target_filepath,
867
+ changes: { linesAdded: 0, linesRemoved: 0, linesModified: 0 },
868
+ error: `Invalid filepath: '${input.target_filepath}' is outside baseDir`
667
869
  };
668
870
  }
669
- };
670
- function parseAPIError(statusCode, responseText, requestId) {
671
- let errorData = {};
672
871
  try {
673
- errorData = JSON.parse(responseText);
674
- } catch {
675
- }
676
- const message = errorData.detail || errorData.message || responseText || "Unknown error";
677
- const code = errorData.code;
678
- switch (statusCode) {
679
- case 401:
680
- return new MorphAuthenticationError(message);
681
- case 403:
682
- if (code === "profile_limit_exceeded" || message.toLowerCase().includes("limit")) {
683
- return new MorphProfileLimitError(message, { requestId });
684
- }
685
- return new MorphAPIError(message, "insufficient_permissions", statusCode, { requestId, rawResponse: responseText });
686
- case 404:
687
- if (message.toLowerCase().includes("profile")) {
688
- return new MorphNotFoundError("Profile", void 0);
689
- }
690
- if (message.toLowerCase().includes("session")) {
691
- return new MorphNotFoundError("Session", void 0);
872
+ if (debug) console.log(`[FastApply] Reading file: ${input.target_filepath}`);
873
+ const { readFile, writeFile } = await import("fs/promises");
874
+ const { callMorphAPI: callMorphAPI2, generateUdiff: generateUdiff2, countChanges: countChanges2 } = await Promise.resolve().then(() => (init_apply(), apply_exports));
875
+ let originalCode = "";
876
+ try {
877
+ originalCode = await readFile(fullPath, "utf-8");
878
+ } catch (error) {
879
+ if (error.code !== "ENOENT") {
880
+ throw error;
692
881
  }
693
- return new MorphAPIError(message, "resource_not_found", statusCode, { requestId, rawResponse: responseText });
694
- case 429:
695
- return new MorphRateLimitError(message, { requestId });
696
- case 422:
697
- return new MorphAPIError(message, "validation_error", statusCode, { requestId, rawResponse: responseText });
698
- case 500:
699
- case 502:
700
- case 503:
701
- case 504:
702
- return new MorphAPIError(message, "service_unavailable", statusCode, { requestId, rawResponse: responseText });
703
- default:
704
- return new MorphAPIError(message, "network_error", statusCode, { requestId, rawResponse: responseText });
882
+ if (debug) console.log(`[FastApply] File doesn't exist, will create new file`);
883
+ }
884
+ const mergedCode = await callMorphAPI2(originalCode, input.code_edit, input.instructions, input.target_filepath, config);
885
+ const udiff = config.generateUdiff !== false ? generateUdiff2(originalCode, mergedCode, input.target_filepath) : void 0;
886
+ if (config.autoWrite !== false) {
887
+ await writeFile(fullPath, mergedCode, "utf-8");
888
+ if (debug) console.log(`[FastApply] Wrote ${mergedCode.length} chars to ${input.target_filepath}`);
889
+ }
890
+ const changes = countChanges2(originalCode, mergedCode);
891
+ return {
892
+ success: true,
893
+ filepath: input.target_filepath,
894
+ udiff,
895
+ changes
896
+ };
897
+ } catch (error) {
898
+ const errorMessage = error instanceof Error ? error.message : "Unknown error occurred";
899
+ if (debug) console.error(`[FastApply] Error: ${errorMessage}`);
900
+ return {
901
+ success: false,
902
+ filepath: input.target_filepath,
903
+ changes: { linesAdded: 0, linesRemoved: 0, linesModified: 0 },
904
+ error: errorMessage
905
+ };
705
906
  }
706
907
  }
707
908
 
708
- // tools/browser/profiles/types.ts
709
- function transformProfile(api) {
710
- return {
711
- id: api.id,
712
- name: api.name,
713
- repoId: api.repo_id,
714
- cookieDomains: api.cookie_domains,
715
- lastUsedAt: api.last_used_at,
716
- createdAt: api.created_at,
717
- updatedAt: api.updated_at
718
- };
719
- }
720
- function transformCreateInput(input) {
721
- return {
722
- name: input.name,
723
- repo_id: input.repoId
724
- };
725
- }
726
- function transformSession(api) {
727
- return {
728
- sessionId: api.session_id,
729
- debugUrl: api.debug_url || ""
730
- };
731
- }
732
- function transformSaveInput(input) {
733
- return {
734
- session_id: input.sessionId,
735
- profile_id: input.profileId
736
- };
737
- }
738
- function transformStateResponse(api) {
739
- return {
740
- profileId: api.profile_id,
741
- stateUrl: api.state_url,
742
- expiresIn: api.expires_in
743
- };
744
- }
745
-
746
- // tools/browser/profiles/core.ts
747
- var DEFAULT_API_URL = process.env.MORPH_ENVIRONMENT === "DEV" ? "http://localhost:8000" : "https://browser.morphllm.com";
748
- var ProfilesClient = class {
909
+ // tools/codebase_search/core.ts
910
+ init_resilience();
911
+ var CodebaseSearchClient = class {
749
912
  config;
750
- constructor(config) {
751
- this.config = config;
913
+ constructor(config = {}) {
914
+ this.config = {
915
+ apiKey: config.apiKey,
916
+ searchUrl: process.env.MORPH_SEARCH_URL || "https://repos.morphllm.com",
917
+ debug: config.debug,
918
+ timeout: config.timeout || 3e4,
919
+ retryConfig: config.retryConfig
920
+ };
752
921
  }
753
922
  /**
754
- * Create a new browser profile and immediately start a live session.
755
- *
756
- * @param input - Profile creation parameters
757
- * @returns Profile setup handle with live URL + save()
758
- * @throws {MorphValidationError} If input validation fails
759
- * @throws {MorphProfileLimitError} If profile limit is exceeded
760
- * @throws {MorphAuthenticationError} If API key is missing or invalid
761
- *
762
- * @example
763
- * ```typescript
764
- * const setup = await morph.browser.profiles.createProfile({
765
- * name: 'LinkedIn Production',
766
- * repoId: 'owner/repo'
767
- * });
768
- * console.log(setup.session.debugUrl);
769
- * await setup.save();
770
- * ```
771
- */
772
- async createProfile(input) {
773
- return createProfile(input, this.config);
774
- }
775
- /**
776
- * List all profiles for the authenticated user.
777
- *
778
- * @param repoId - Optional repository ID to filter by
779
- * @returns Array of profiles
780
- *
781
- * @example
782
- * ```typescript
783
- * // List all profiles
784
- * const allProfiles = await morph.browser.profiles.listProfiles();
785
- *
786
- * // List profiles for a specific repo
787
- * const repoProfiles = await morph.browser.profiles.listProfiles('owner/repo');
788
- * ```
923
+ * Execute a semantic code search
924
+ *
925
+ * @param input - Search parameters including query, repoId, and target directories
926
+ * @param overrides - Optional config overrides for this operation
927
+ * @returns Search results with ranked code matches
789
928
  */
790
- async listProfiles(repoId) {
791
- return listProfiles(this.config, repoId);
929
+ async search(input, overrides) {
930
+ return executeCodebaseSearch(
931
+ {
932
+ query: input.query,
933
+ target_directories: input.target_directories,
934
+ explanation: input.explanation,
935
+ limit: input.limit
936
+ },
937
+ { ...this.config, repoId: input.repoId, ...overrides }
938
+ );
792
939
  }
793
- /**
794
- * Get a profile by ID with convenience methods.
795
- *
796
- * @param id - Profile ID
797
- * @returns Profile with attached methods
798
- * @throws {MorphNotFoundError} If profile is not found
799
- *
800
- * @example
801
- * ```typescript
802
- * const profile = await morph.browser.profiles.getProfile('profile-id');
803
- * const state = await profile.getState();
804
- * await profile.delete();
805
- * ```
806
- */
807
- async getProfile(id) {
808
- return getProfile(id, this.config);
940
+ };
941
+ async function executeCodebaseSearch(input, config) {
942
+ const apiKey = config.apiKey || process.env.MORPH_API_KEY;
943
+ if (!apiKey) {
944
+ throw new Error("MORPH_API_KEY not found. Set environment variable or pass in config");
809
945
  }
810
- /**
811
- * Update a profile by opening a live session (no rename).
812
- *
813
- * @param id - Profile ID
814
- * @returns Profile setup handle with live URL + save()
815
- */
816
- async updateProfile(id) {
817
- return updateProfile(id, this.config);
946
+ const searchUrl = config.searchUrl || process.env.MORPH_SEARCH_URL || "https://repos.morphllm.com";
947
+ const timeout = config.timeout || 3e4;
948
+ const debug = config.debug || false;
949
+ if (debug) {
950
+ console.log(`[CodebaseSearch] Query: "${input.query.slice(0, 60)}..." repo=${config.repoId}`);
951
+ console.log(`[CodebaseSearch] URL: ${searchUrl}/v1/codebase_search`);
818
952
  }
819
- /**
820
- * Delete a profile.
821
- *
822
- * @param id - Profile ID
823
- * @throws {MorphNotFoundError} If profile is not found
824
- */
825
- async deleteProfile(id) {
826
- return deleteProfile(id, this.config);
953
+ const startTime = Date.now();
954
+ try {
955
+ const fetchPromise = fetchWithRetry(
956
+ `${searchUrl}/v1/codebase_search`,
957
+ {
958
+ method: "POST",
959
+ headers: {
960
+ "Content-Type": "application/json",
961
+ "Authorization": `Bearer ${apiKey}`
962
+ },
963
+ body: JSON.stringify({
964
+ query: input.query,
965
+ repoId: config.repoId,
966
+ targetDirectories: input.target_directories || [],
967
+ limit: input.limit || 10,
968
+ candidateLimit: 50
969
+ })
970
+ },
971
+ config.retryConfig
972
+ );
973
+ const response = await withTimeout(fetchPromise, timeout, `Codebase search timed out after ${timeout}ms`);
974
+ if (!response.ok) {
975
+ const errorText = await response.text();
976
+ if (debug) console.error(`[CodebaseSearch] Error: ${response.status} - ${errorText}`);
977
+ return {
978
+ success: false,
979
+ results: [],
980
+ stats: { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: 0 },
981
+ error: `Search failed (${response.status}): ${errorText}`
982
+ };
983
+ }
984
+ const data = await response.json();
985
+ const elapsed = Date.now() - startTime;
986
+ if (debug) {
987
+ console.log(`[CodebaseSearch] \u2705 ${data.results?.length || 0} results in ${elapsed}ms`);
988
+ }
989
+ return {
990
+ success: true,
991
+ results: data.results || [],
992
+ stats: data.stats || { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: elapsed }
993
+ };
994
+ } catch (error) {
995
+ if (debug) console.error(`[CodebaseSearch] Exception: ${error instanceof Error ? error.message : error}`);
996
+ return {
997
+ success: false,
998
+ results: [],
999
+ stats: { totalResults: 0, candidatesRetrieved: 0, searchTimeMs: 0 },
1000
+ error: error instanceof Error ? error.message : "Unknown error"
1001
+ };
827
1002
  }
828
- /**
829
- * Start a browser session for profile setup.
830
- *
831
- * Returns a live URL where the user can sign into accounts.
832
- * After signing in, call `saveSession` to persist the state.
833
- *
834
- * @param input - Optional session parameters
835
- * @returns Session with debug URL
836
- *
837
- * @example
838
- * ```typescript
839
- * const session = await morph.browser.profiles.startSession();
840
- * console.log('Sign in at:', session.debugUrl);
841
- * // Open debugUrl in browser, user signs in...
842
- * await morph.browser.profiles.saveSession(session.sessionId, profile.id);
843
- * ```
844
- */
845
- async startSession(input) {
846
- return startProfileSession(this.config, input);
1003
+ }
1004
+
1005
+ // tools/browser/core.ts
1006
+ init_resilience();
1007
+
1008
+ // tools/browser/live.ts
1009
+ var LIVE_PRESETS = {
1010
+ /** Read-only monitoring (no interaction) */
1011
+ readonly: { interactive: false },
1012
+ /** Interactive control (human-in-the-loop) */
1013
+ interactive: { interactive: true },
1014
+ /** Watch-only without controls */
1015
+ monitoring: { interactive: false, showControls: false }
1016
+ };
1017
+ function buildLiveUrl(debugUrl, options = {}) {
1018
+ if (!debugUrl) {
1019
+ throw new Error(
1020
+ "debugUrl is required. Ensure your backend returns debugUrl in the task response. Contact support@morphllm.com if you need help."
1021
+ );
847
1022
  }
848
- /**
849
- * Save browser state from a session to a profile.
850
- *
851
- * Call this after the user is done signing into accounts.
852
- * Extracts cookies, localStorage, and sessionStorage.
853
- *
854
- * @param sessionId - Browser session ID from startSession
855
- * @param profileId - Profile ID to save state to
856
- * @returns Updated profile with cookie domains
857
- */
858
- async saveSession(sessionId, profileId) {
859
- return saveProfileSession({ sessionId, profileId }, this.config);
1023
+ const normalized = normalizeLiveUrl(debugUrl);
1024
+ const url = new URL(normalized);
1025
+ if (options.interactive !== void 0) {
1026
+ url.searchParams.set("interactive", String(options.interactive));
860
1027
  }
861
- /**
862
- * List available repo IDs (discovery).
863
- *
864
- * @returns Repo summaries with profile counts
865
- */
866
- async listRepos() {
867
- return listRepos(this.config);
1028
+ if (options.theme) {
1029
+ url.searchParams.set("theme", options.theme);
868
1030
  }
869
- /**
870
- * Get the presigned URL for a profile's state.
871
- *
872
- * Use this to download the raw state JSON for debugging
873
- * or to restore state manually.
874
- *
875
- * @param profileId - Profile ID
876
- * @returns State URL with expiry information
877
- */
878
- async getProfileState(profileId) {
879
- return getProfileState(profileId, this.config);
1031
+ if (options.showControls !== void 0) {
1032
+ url.searchParams.set("showControls", String(options.showControls));
880
1033
  }
881
- };
882
- function validateCreateInput(input) {
883
- if (!input.name || typeof input.name !== "string") {
884
- throw new MorphValidationError("name is required", "name");
1034
+ if (options.pageId) {
1035
+ url.searchParams.set("pageId", options.pageId);
885
1036
  }
886
- const trimmedName = input.name.trim();
887
- if (trimmedName.length === 0) {
888
- throw new MorphValidationError("name cannot be empty", "name");
1037
+ if (options.pageIndex) {
1038
+ url.searchParams.set("pageIndex", options.pageIndex);
889
1039
  }
890
- if (trimmedName.length > 100) {
891
- throw new MorphValidationError("name must be 100 characters or less", "name");
1040
+ return url.toString();
1041
+ }
1042
+ function normalizeLiveUrl(debugUrl) {
1043
+ const trimmed = debugUrl.trim();
1044
+ if (!trimmed) {
1045
+ return trimmed;
892
1046
  }
893
- if (!input.repoId || typeof input.repoId !== "string") {
894
- throw new MorphValidationError("repoId is required", "repoId");
1047
+ if (trimmed.startsWith("wss://") || trimmed.startsWith("ws://")) {
1048
+ return `https://live.browser-use.com?wss=${encodeURIComponent(trimmed)}`;
895
1049
  }
896
- if (input.repoId.trim().length === 0) {
897
- throw new MorphValidationError("repoId cannot be empty", "repoId");
1050
+ let url;
1051
+ try {
1052
+ url = new URL(trimmed);
1053
+ } catch {
1054
+ return trimmed;
898
1055
  }
899
- }
900
- function validateId(id, fieldName) {
901
- if (!id || typeof id !== "string") {
902
- throw new MorphValidationError(`${fieldName} is required`, fieldName);
1056
+ if (url.protocol === "wss:" || url.protocol === "ws:") {
1057
+ return `https://live.browser-use.com?wss=${encodeURIComponent(trimmed)}`;
903
1058
  }
904
- if (id.trim().length === 0) {
905
- throw new MorphValidationError(`${fieldName} cannot be empty`, fieldName);
1059
+ const wssParam = url.searchParams.get("wss");
1060
+ if (wssParam && (wssParam.startsWith("wss://") || wssParam.startsWith("ws://"))) {
1061
+ url.searchParams.set("wss", wssParam);
906
1062
  }
1063
+ return url.toString();
907
1064
  }
908
- async function createProfile(input, config = {}) {
909
- validateCreateInput(input);
910
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
911
- const headers = buildHeaders(config);
912
- const response = await fetchWithRetry(
913
- `${apiUrl}/profiles`,
914
- {
915
- method: "POST",
916
- headers,
917
- body: JSON.stringify(transformCreateInput(input))
918
- },
919
- config.retryConfig
920
- );
921
- if (!response.ok) {
922
- const errorText = await response.text().catch(() => response.statusText);
923
- const requestId = response.headers.get("x-request-id") || void 0;
924
- throw parseAPIError(response.status, errorText, requestId);
1065
+ function buildLiveIframe(debugUrl, options = {}) {
1066
+ const {
1067
+ width = "100%",
1068
+ height = "600px",
1069
+ style = "",
1070
+ className = "",
1071
+ ...sessionOptions
1072
+ } = options;
1073
+ const src = buildLiveUrl(debugUrl, sessionOptions);
1074
+ const widthStr = typeof width === "number" ? `${width}px` : width;
1075
+ const heightStr = typeof height === "number" ? `${height}px` : height;
1076
+ const baseStyle = `width: ${widthStr}; height: ${heightStr}; border: none;`;
1077
+ const fullStyle = style ? `${baseStyle} ${style}` : baseStyle;
1078
+ const attributes = [
1079
+ `src="${src}"`,
1080
+ `style="${fullStyle}"`
1081
+ ];
1082
+ if (className) {
1083
+ attributes.push(`class="${className}"`);
925
1084
  }
926
- const apiProfile = await response.json();
927
- const profile = transformProfile(apiProfile);
928
- const session = await startProfileSession(config, { profileId: profile.id });
929
- return buildProfileSetup(profile, session, config);
1085
+ return `<iframe ${attributes.join(" ")}></iframe>`;
930
1086
  }
931
- async function listProfiles(config = {}, repoId) {
932
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
933
- const headers = buildHeaders(config);
934
- const url = repoId ? `${apiUrl}/profiles?repo_id=${encodeURIComponent(repoId)}` : `${apiUrl}/profiles`;
935
- const response = await fetchWithRetry(
936
- url,
937
- { method: "GET", headers },
938
- config.retryConfig
939
- );
940
- if (!response.ok) {
941
- const errorText = await response.text().catch(() => response.statusText);
942
- const requestId = response.headers.get("x-request-id") || void 0;
943
- throw parseAPIError(response.status, errorText, requestId);
944
- }
945
- const data = await response.json();
946
- return data.profiles.map(transformProfile);
1087
+ function buildEmbedCode(debugUrl, options = {}) {
1088
+ const iframe = buildLiveIframe(debugUrl, options);
1089
+ return `<!-- Embed Morph Live Session -->
1090
+ ${iframe}`;
947
1091
  }
948
- async function getProfile(id, config = {}) {
949
- validateId(id, "id");
950
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
951
- const headers = buildHeaders(config);
952
- const response = await fetchWithRetry(
953
- `${apiUrl}/profiles/${encodeURIComponent(id)}`,
954
- { method: "GET", headers },
955
- config.retryConfig
956
- );
957
- if (!response.ok) {
958
- const errorText = await response.text().catch(() => response.statusText);
959
- const requestId = response.headers.get("x-request-id") || void 0;
960
- throw parseAPIError(response.status, errorText, requestId);
1092
+ function resolvePreset(optionsOrPreset) {
1093
+ if (!optionsOrPreset) {
1094
+ return {};
961
1095
  }
962
- const apiProfile = await response.json();
963
- const profile = transformProfile(apiProfile);
964
- return {
965
- ...profile,
966
- getState: () => getProfileState(id, config),
967
- delete: () => deleteProfile(id, config)
968
- };
969
- }
970
- async function updateProfile(id, config = {}) {
971
- validateId(id, "id");
972
- const profile = await fetchProfile(id, config);
973
- const session = await startProfileSession(config, { profileId: profile.id });
974
- return buildProfileSetup(profile, session, config);
975
- }
976
- async function deleteProfile(id, config = {}) {
977
- validateId(id, "id");
978
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
979
- const headers = buildHeaders(config);
980
- const response = await fetchWithRetry(
981
- `${apiUrl}/profiles/${encodeURIComponent(id)}`,
982
- { method: "DELETE", headers },
983
- config.retryConfig
984
- );
985
- if (!response.ok) {
986
- const errorText = await response.text().catch(() => response.statusText);
987
- const requestId = response.headers.get("x-request-id") || void 0;
988
- throw parseAPIError(response.status, errorText, requestId);
1096
+ if (typeof optionsOrPreset === "string") {
1097
+ const preset = LIVE_PRESETS[optionsOrPreset];
1098
+ if (!preset) {
1099
+ throw new Error(
1100
+ `Unknown preset: ${optionsOrPreset}. Available presets: ${Object.keys(LIVE_PRESETS).join(", ")}`
1101
+ );
1102
+ }
1103
+ return preset;
989
1104
  }
1105
+ return optionsOrPreset;
990
1106
  }
991
- async function startProfileSession(config = {}, input) {
992
- if (!config.apiKey) {
993
- throw new MorphAuthenticationError();
1107
+
1108
+ // tools/browser/profiles/core.ts
1109
+ init_resilience();
1110
+
1111
+ // tools/browser/errors.ts
1112
+ var MorphError = class extends Error {
1113
+ /** Error code for programmatic handling */
1114
+ code;
1115
+ /** Original cause of the error, if any */
1116
+ cause;
1117
+ constructor(message, code, cause) {
1118
+ super(message);
1119
+ this.name = "MorphError";
1120
+ this.code = code;
1121
+ this.cause = cause;
1122
+ if (Error.captureStackTrace) {
1123
+ Error.captureStackTrace(this, this.constructor);
1124
+ }
994
1125
  }
995
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
996
- const headers = buildHeaders(config);
997
- const body = input?.profileId ? { profile_id: input.profileId } : {};
998
- const response = await fetchWithRetry(
999
- `${apiUrl}/profiles/session/start`,
1000
- {
1001
- method: "POST",
1002
- headers,
1003
- body: JSON.stringify(body)
1004
- },
1005
- config.retryConfig
1006
- );
1007
- if (!response.ok) {
1008
- const errorText = await response.text().catch(() => response.statusText);
1009
- const requestId = response.headers.get("x-request-id") || void 0;
1010
- throw parseAPIError(response.status, errorText, requestId);
1126
+ /**
1127
+ * Returns a JSON representation of the error for logging.
1128
+ */
1129
+ toJSON() {
1130
+ return {
1131
+ name: this.name,
1132
+ message: this.message,
1133
+ code: this.code,
1134
+ cause: this.cause?.message
1135
+ };
1011
1136
  }
1012
- const apiSession = await response.json();
1013
- return transformSession(apiSession);
1014
- }
1015
- async function saveProfileSession(input, config = {}) {
1016
- validateId(input.sessionId, "sessionId");
1017
- validateId(input.profileId, "profileId");
1018
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
1019
- const headers = buildHeaders(config);
1020
- const response = await fetchWithRetry(
1021
- `${apiUrl}/profiles/session/save`,
1022
- {
1023
- method: "POST",
1024
- headers,
1025
- body: JSON.stringify(transformSaveInput(input))
1026
- },
1027
- config.retryConfig
1028
- );
1029
- if (!response.ok) {
1030
- const errorText = await response.text().catch(() => response.statusText);
1031
- const requestId = response.headers.get("x-request-id") || void 0;
1032
- throw parseAPIError(response.status, errorText, requestId);
1137
+ };
1138
+ var MorphValidationError = class extends MorphError {
1139
+ /** The field that failed validation */
1140
+ field;
1141
+ constructor(message, field) {
1142
+ super(message, "validation_error");
1143
+ this.name = "MorphValidationError";
1144
+ this.field = field;
1033
1145
  }
1034
- const apiProfile = await response.json();
1035
- return transformProfile(apiProfile);
1036
- }
1037
- async function listRepos(config = {}) {
1038
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
1039
- const headers = buildHeaders(config);
1040
- const response = await fetchWithRetry(
1041
- `${apiUrl}/repos`,
1042
- { method: "GET", headers },
1043
- config.retryConfig
1044
- );
1045
- if (!response.ok) {
1046
- const errorText = await response.text().catch(() => response.statusText);
1047
- const requestId = response.headers.get("x-request-id") || void 0;
1048
- throw parseAPIError(response.status, errorText, requestId);
1146
+ toJSON() {
1147
+ return {
1148
+ ...super.toJSON(),
1149
+ field: this.field
1150
+ };
1049
1151
  }
1050
- const data = await response.json();
1051
- const repos = Array.isArray(data?.repos) ? data.repos : [];
1052
- return repos.map((repo) => ({
1053
- repoId: repo.repo_id,
1054
- repoFullName: repo.repo_full_name,
1055
- profileCount: repo.profile_count ?? 0
1056
- }));
1057
- }
1058
- async function getProfileState(profileId, config = {}) {
1059
- validateId(profileId, "profileId");
1060
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
1061
- const headers = buildHeaders(config);
1062
- const response = await fetchWithRetry(
1063
- `${apiUrl}/profiles/${encodeURIComponent(profileId)}/state`,
1064
- { method: "GET", headers },
1065
- config.retryConfig
1066
- );
1067
- if (!response.ok) {
1068
- const errorText = await response.text().catch(() => response.statusText);
1069
- const requestId = response.headers.get("x-request-id") || void 0;
1070
- throw parseAPIError(response.status, errorText, requestId);
1152
+ };
1153
+ var MorphAPIError = class extends MorphError {
1154
+ /** HTTP status code */
1155
+ statusCode;
1156
+ /** Request ID for debugging (if available) */
1157
+ requestId;
1158
+ /** Raw response body */
1159
+ rawResponse;
1160
+ constructor(message, code, statusCode, options) {
1161
+ super(message, code, options?.cause);
1162
+ this.name = "MorphAPIError";
1163
+ this.statusCode = statusCode;
1164
+ this.requestId = options?.requestId;
1165
+ this.rawResponse = options?.rawResponse;
1071
1166
  }
1072
- const apiState = await response.json();
1073
- return transformStateResponse(apiState);
1074
- }
1075
- function buildHeaders(config) {
1076
- const headers = { "Content-Type": "application/json" };
1077
- if (config.apiKey) {
1078
- headers["Authorization"] = `Bearer ${config.apiKey}`;
1167
+ toJSON() {
1168
+ return {
1169
+ ...super.toJSON(),
1170
+ statusCode: this.statusCode,
1171
+ requestId: this.requestId
1172
+ };
1079
1173
  }
1080
- return headers;
1081
- }
1082
- async function fetchProfile(id, config) {
1083
- const apiUrl = config.apiUrl || DEFAULT_API_URL;
1084
- const headers = buildHeaders(config);
1085
- const response = await fetchWithRetry(
1086
- `${apiUrl}/profiles/${encodeURIComponent(id)}`,
1087
- { method: "GET", headers },
1088
- config.retryConfig
1089
- );
1090
- if (!response.ok) {
1091
- const errorText = await response.text().catch(() => response.statusText);
1092
- const requestId = response.headers.get("x-request-id") || void 0;
1093
- throw parseAPIError(response.status, errorText, requestId);
1174
+ };
1175
+ var MorphAuthenticationError = class extends MorphAPIError {
1176
+ constructor(message = "Authentication required. Please provide a valid API key.") {
1177
+ super(message, "authentication_required", 401);
1178
+ this.name = "MorphAuthenticationError";
1179
+ }
1180
+ };
1181
+ var MorphRateLimitError = class extends MorphAPIError {
1182
+ /** When the rate limit resets (Unix timestamp) */
1183
+ resetAt;
1184
+ /** Number of seconds until reset */
1185
+ retryAfter;
1186
+ constructor(message = "Rate limit exceeded. Please retry later.", options) {
1187
+ super(message, "rate_limit_exceeded", 429, { requestId: options?.requestId });
1188
+ this.name = "MorphRateLimitError";
1189
+ this.resetAt = options?.resetAt;
1190
+ this.retryAfter = options?.retryAfter;
1191
+ }
1192
+ toJSON() {
1193
+ return {
1194
+ ...super.toJSON(),
1195
+ resetAt: this.resetAt,
1196
+ retryAfter: this.retryAfter
1197
+ };
1198
+ }
1199
+ };
1200
+ var MorphNotFoundError = class extends MorphAPIError {
1201
+ /** The type of resource that was not found */
1202
+ resourceType;
1203
+ /** The ID of the resource that was not found */
1204
+ resourceId;
1205
+ constructor(resourceType, resourceId) {
1206
+ const message = resourceId ? `${resourceType} '${resourceId}' not found` : `${resourceType} not found`;
1207
+ super(message, "resource_not_found", 404);
1208
+ this.name = "MorphNotFoundError";
1209
+ this.resourceType = resourceType;
1210
+ this.resourceId = resourceId;
1211
+ }
1212
+ toJSON() {
1213
+ return {
1214
+ ...super.toJSON(),
1215
+ resourceType: this.resourceType,
1216
+ resourceId: this.resourceId
1217
+ };
1218
+ }
1219
+ };
1220
+ var MorphProfileLimitError = class extends MorphAPIError {
1221
+ /** Current number of profiles */
1222
+ currentCount;
1223
+ /** Maximum allowed profiles for the plan */
1224
+ maxAllowed;
1225
+ constructor(message = "Profile limit exceeded for your plan.", options) {
1226
+ super(message, "profile_limit_exceeded", 403, { requestId: options?.requestId });
1227
+ this.name = "MorphProfileLimitError";
1228
+ this.currentCount = options?.currentCount;
1229
+ this.maxAllowed = options?.maxAllowed;
1230
+ }
1231
+ toJSON() {
1232
+ return {
1233
+ ...super.toJSON(),
1234
+ currentCount: this.currentCount,
1235
+ maxAllowed: this.maxAllowed
1236
+ };
1237
+ }
1238
+ };
1239
+ function parseAPIError(statusCode, responseText, requestId) {
1240
+ let errorData = {};
1241
+ try {
1242
+ errorData = JSON.parse(responseText);
1243
+ } catch {
1244
+ }
1245
+ const message = errorData.detail || errorData.message || responseText || "Unknown error";
1246
+ const code = errorData.code;
1247
+ switch (statusCode) {
1248
+ case 401:
1249
+ return new MorphAuthenticationError(message);
1250
+ case 403:
1251
+ if (code === "profile_limit_exceeded" || message.toLowerCase().includes("limit")) {
1252
+ return new MorphProfileLimitError(message, { requestId });
1253
+ }
1254
+ return new MorphAPIError(message, "insufficient_permissions", statusCode, { requestId, rawResponse: responseText });
1255
+ case 404:
1256
+ if (message.toLowerCase().includes("profile")) {
1257
+ return new MorphNotFoundError("Profile", void 0);
1258
+ }
1259
+ if (message.toLowerCase().includes("session")) {
1260
+ return new MorphNotFoundError("Session", void 0);
1261
+ }
1262
+ return new MorphAPIError(message, "resource_not_found", statusCode, { requestId, rawResponse: responseText });
1263
+ case 429:
1264
+ return new MorphRateLimitError(message, { requestId });
1265
+ case 422:
1266
+ return new MorphAPIError(message, "validation_error", statusCode, { requestId, rawResponse: responseText });
1267
+ case 500:
1268
+ case 502:
1269
+ case 503:
1270
+ case 504:
1271
+ return new MorphAPIError(message, "service_unavailable", statusCode, { requestId, rawResponse: responseText });
1272
+ default:
1273
+ return new MorphAPIError(message, "network_error", statusCode, { requestId, rawResponse: responseText });
1094
1274
  }
1095
- const apiProfile = await response.json();
1096
- return transformProfile(apiProfile);
1097
1275
  }
1098
- function buildProfileSetup(profile, session, config) {
1276
+
1277
+ // tools/browser/profiles/types.ts
1278
+ function transformProfile(api) {
1099
1279
  return {
1100
- profile,
1101
- session,
1102
- save: () => saveProfileSession({ sessionId: session.sessionId, profileId: profile.id }, config)
1280
+ id: api.id,
1281
+ name: api.name,
1282
+ repoId: api.repo_id,
1283
+ cookieDomains: api.cookie_domains,
1284
+ lastUsedAt: api.last_used_at,
1285
+ createdAt: api.created_at,
1286
+ updatedAt: api.updated_at
1287
+ };
1288
+ }
1289
+ function transformCreateInput(input) {
1290
+ return {
1291
+ name: input.name,
1292
+ repo_id: input.repoId
1293
+ };
1294
+ }
1295
+ function transformSession(api) {
1296
+ return {
1297
+ sessionId: api.session_id,
1298
+ debugUrl: api.debug_url || ""
1299
+ };
1300
+ }
1301
+ function transformSaveInput(input) {
1302
+ return {
1303
+ session_id: input.sessionId,
1304
+ profile_id: input.profileId
1305
+ };
1306
+ }
1307
+ function transformStateResponse(api) {
1308
+ return {
1309
+ profileId: api.profile_id,
1310
+ stateUrl: api.state_url,
1311
+ expiresIn: api.expires_in
1103
1312
  };
1104
1313
  }
1105
1314
 
1106
- // tools/browser/core.ts
1107
- var DEFAULT_CONFIG2 = {
1108
- apiUrl: process.env.MORPH_ENVIRONMENT === "DEV" ? "http://localhost:8000" : "https://browser.morphllm.com",
1109
- timeout: 1e6,
1110
- // 10 minutes for complex tasks
1111
- debug: false
1112
- };
1113
- var BrowserClient = class {
1315
+ // tools/browser/profiles/core.ts
1316
+ var DEFAULT_API_URL2 = process.env.MORPH_ENVIRONMENT === "DEV" ? "http://localhost:8000" : "https://browser.morphllm.com";
1317
+ var ProfilesClient = class {
1114
1318
  config;
1319
+ constructor(config) {
1320
+ this.config = config;
1321
+ }
1115
1322
  /**
1116
- * Profile management - create and manage browser profiles for storing login state.
1323
+ * Create a new browser profile and immediately start a live session.
1324
+ *
1325
+ * @param input - Profile creation parameters
1326
+ * @returns Profile setup handle with live URL + save()
1327
+ * @throws {MorphValidationError} If input validation fails
1328
+ * @throws {MorphProfileLimitError} If profile limit is exceeded
1329
+ * @throws {MorphAuthenticationError} If API key is missing or invalid
1330
+ *
1331
+ * @example
1332
+ * ```typescript
1333
+ * const setup = await morph.browser.profiles.createProfile({
1334
+ * name: 'LinkedIn Production',
1335
+ * repoId: 'owner/repo'
1336
+ * });
1337
+ * console.log(setup.session.debugUrl);
1338
+ * await setup.save();
1339
+ * ```
1340
+ */
1341
+ async createProfile(input) {
1342
+ return createProfile(input, this.config);
1343
+ }
1344
+ /**
1345
+ * List all profiles for the authenticated user.
1346
+ *
1347
+ * @param repoId - Optional repository ID to filter by
1348
+ * @returns Array of profiles
1349
+ *
1350
+ * @example
1351
+ * ```typescript
1352
+ * // List all profiles
1353
+ * const allProfiles = await morph.browser.profiles.listProfiles();
1354
+ *
1355
+ * // List profiles for a specific repo
1356
+ * const repoProfiles = await morph.browser.profiles.listProfiles('owner/repo');
1357
+ * ```
1117
1358
  */
1118
- profiles;
1119
- constructor(config = {}) {
1120
- this.config = {
1121
- ...DEFAULT_CONFIG2,
1122
- ...config
1123
- };
1124
- this.profiles = new ProfilesClient(this.config);
1359
+ async listProfiles(repoId) {
1360
+ return listProfiles(this.config, repoId);
1125
1361
  }
1126
1362
  /**
1127
- * Execute a browser automation task
1363
+ * Get a profile by ID with convenience methods.
1364
+ *
1365
+ * @param id - Profile ID
1366
+ * @returns Profile with attached methods
1367
+ * @throws {MorphNotFoundError} If profile is not found
1368
+ *
1369
+ * @example
1370
+ * ```typescript
1371
+ * const profile = await morph.browser.profiles.getProfile('profile-id');
1372
+ * const state = await profile.getState();
1373
+ * await profile.delete();
1374
+ * ```
1128
1375
  */
1129
- async execute(input) {
1130
- return executeBrowserTask(input, this.config);
1131
- }
1132
- async createTask(input) {
1133
- const apiUrl = this.config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1134
- const debug = this.config.debug || false;
1135
- const hasTask = typeof input.task === "string" && input.task.trim().length > 0;
1136
- const hasDiff = typeof input.diff === "string" && input.diff.trim().length > 0;
1137
- if (!hasTask && !hasDiff) {
1138
- throw new Error('Browser task requires either "task" (natural language) or "diff" (PR-review planning)');
1139
- }
1140
- if (debug) {
1141
- const preview = (input.task ?? "").slice(0, 60);
1142
- console.log(`[Browser] createTask: "${preview}..." url=${input.url || "none"}`);
1143
- console.log(`[Browser] Calling async endpoint: ${apiUrl}/browser-task/async`);
1144
- }
1145
- const headers = { "Content-Type": "application/json" };
1146
- if (this.config.apiKey) headers["Authorization"] = `Bearer ${this.config.apiKey}`;
1147
- const response = await fetch(`${apiUrl}/browser-task/async`, {
1148
- method: "POST",
1149
- headers,
1150
- body: JSON.stringify({
1151
- task: input.task,
1152
- diff: input.diff,
1153
- url: input.url,
1154
- max_steps: input.maxSteps ?? 10,
1155
- model: input.model ?? "morph-computer-use-v0",
1156
- viewport_width: input.viewportWidth ?? 1280,
1157
- viewport_height: input.viewportHeight ?? 720,
1158
- external_id: input.externalId,
1159
- repo_id: input.repoId,
1160
- repo_full_name: input.repoFullName,
1161
- commit_id: input.commitId,
1162
- record_video: input.recordVideo ?? false,
1163
- video_width: input.videoWidth ?? input.viewportWidth ?? 1280,
1164
- video_height: input.videoHeight ?? input.viewportHeight ?? 720,
1165
- allow_resizing: input.allowResizing ?? false,
1166
- structured_output: "schema" in input ? stringifyStructuredOutput(input.schema) : void 0,
1167
- auth: input.auth,
1168
- profile_id: input.profileId,
1169
- force_query_params: input.forceQueryParams
1170
- })
1171
- });
1172
- if (!response.ok) {
1173
- const errorText = await response.text().catch(() => response.statusText);
1174
- if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);
1175
- throw new Error(`HTTP ${response.status}: ${errorText}`);
1176
- }
1177
- const result = mapTaskResult(await response.json());
1178
- if (debug) {
1179
- const debugUrl = result.debugUrl;
1180
- console.log(`[Browser] \u2705 Task created: recordingId=${result.recordingId ?? "none"} debugUrl=${debugUrl ? "available" : "none"}`);
1181
- }
1182
- if ("schema" in input) {
1183
- return wrapTaskResponseWithSchema(result, this.config, input.schema);
1184
- } else {
1185
- return wrapTaskResponse(result, this.config);
1186
- }
1376
+ async getProfile(id) {
1377
+ return getProfile(id, this.config);
1187
1378
  }
1188
1379
  /**
1189
- * Execute task with recording and wait for video to be ready
1380
+ * Update a profile by opening a live session (no rename).
1381
+ *
1382
+ * @param id - Profile ID
1383
+ * @returns Profile setup handle with live URL + save()
1190
1384
  */
1191
- async executeWithRecording(input) {
1192
- return executeWithRecording(input, this.config);
1385
+ async updateProfile(id) {
1386
+ return updateProfile(id, this.config);
1193
1387
  }
1194
1388
  /**
1195
- * Get recording status and URLs
1389
+ * Delete a profile.
1390
+ *
1391
+ * @param id - Profile ID
1392
+ * @throws {MorphNotFoundError} If profile is not found
1196
1393
  */
1197
- async getRecording(recordingId) {
1198
- return getRecording(recordingId, this.config);
1394
+ async deleteProfile(id) {
1395
+ return deleteProfile(id, this.config);
1199
1396
  }
1200
1397
  /**
1201
- * Wait for recording to complete with automatic polling
1398
+ * Start a browser session for profile setup.
1399
+ *
1400
+ * Returns a live URL where the user can sign into accounts.
1401
+ * After signing in, call `saveSession` to persist the state.
1402
+ *
1403
+ * @param input - Optional session parameters
1404
+ * @returns Session with debug URL
1405
+ *
1406
+ * @example
1407
+ * ```typescript
1408
+ * const session = await morph.browser.profiles.startSession();
1409
+ * console.log('Sign in at:', session.debugUrl);
1410
+ * // Open debugUrl in browser, user signs in...
1411
+ * await morph.browser.profiles.saveSession(session.sessionId, profile.id);
1412
+ * ```
1202
1413
  */
1203
- async waitForRecording(recordingId, options) {
1204
- return waitForRecording(recordingId, this.config, options);
1414
+ async startSession(input) {
1415
+ return startProfileSession(this.config, input);
1205
1416
  }
1206
1417
  /**
1207
- * Get errors from recording with screenshots
1418
+ * Save browser state from a session to a profile.
1419
+ *
1420
+ * Call this after the user is done signing into accounts.
1421
+ * Extracts cookies, localStorage, and sessionStorage.
1422
+ *
1423
+ * @param sessionId - Browser session ID from startSession
1424
+ * @param profileId - Profile ID to save state to
1425
+ * @returns Updated profile with cookie domains
1208
1426
  */
1209
- async getErrors(recordingId) {
1210
- return getErrors(recordingId, this.config);
1427
+ async saveSession(sessionId, profileId) {
1428
+ return saveProfileSession({ sessionId, profileId }, this.config);
1211
1429
  }
1212
1430
  /**
1213
- * Get animated WebP preview of recording
1431
+ * List available repo IDs (discovery).
1432
+ *
1433
+ * @returns Repo summaries with profile counts
1214
1434
  */
1215
- async getWebp(recordingId, options) {
1216
- return getWebp(recordingId, this.config, options);
1435
+ async listRepos() {
1436
+ return listRepos(this.config);
1217
1437
  }
1218
1438
  /**
1219
- * Check if browser worker service is healthy
1439
+ * Get the presigned URL for a profile's state.
1440
+ *
1441
+ * Use this to download the raw state JSON for debugging
1442
+ * or to restore state manually.
1443
+ *
1444
+ * @param profileId - Profile ID
1445
+ * @returns State URL with expiry information
1220
1446
  */
1221
- async checkHealth() {
1222
- return checkHealth(this.config);
1447
+ async getProfileState(profileId) {
1448
+ return getProfileState(profileId, this.config);
1223
1449
  }
1224
1450
  };
1225
- async function executeBrowserTask(input, config = {}) {
1226
- const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1227
- const timeout = config.timeout || DEFAULT_CONFIG2.timeout;
1228
- const debug = config.debug || false;
1229
- if (!input.task || input.task.trim().length === 0) {
1230
- return {
1231
- success: false,
1232
- error: 'Task description is required. Example: "Go to example.com and click the login button"'
1233
- };
1451
+ function validateCreateInput(input) {
1452
+ if (!input.name || typeof input.name !== "string") {
1453
+ throw new MorphValidationError("name is required", "name");
1234
1454
  }
1235
- if (input.maxSteps !== void 0 && (input.maxSteps < 1 || input.maxSteps > 50)) {
1236
- return {
1237
- success: false,
1238
- error: "maxSteps must be between 1 and 50. Use more steps for complex multi-page flows."
1239
- };
1455
+ const trimmedName = input.name.trim();
1456
+ if (trimmedName.length === 0) {
1457
+ throw new MorphValidationError("name cannot be empty", "name");
1240
1458
  }
1241
- if (debug) {
1242
- console.log(`[Browser] Task: "${input.task.slice(0, 60)}..." url=${input.url || "none"} maxSteps=${input.maxSteps ?? 10}`);
1243
- console.log(`[Browser] Recording: ${input.recordVideo ? "yes" : "no"} | Calling ${apiUrl}/browser-task`);
1459
+ if (trimmedName.length > 100) {
1460
+ throw new MorphValidationError("name must be 100 characters or less", "name");
1244
1461
  }
1245
- const startTime = Date.now();
1246
- try {
1247
- const headers = { "Content-Type": "application/json" };
1248
- if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1249
- const fetchPromise = fetchWithRetry(
1250
- `${apiUrl}/browser-task`,
1251
- {
1252
- method: "POST",
1253
- headers,
1254
- body: JSON.stringify({
1255
- task: input.task,
1256
- url: input.url,
1257
- max_steps: input.maxSteps ?? 10,
1258
- model: input.model ?? "morph-computer-use-v0",
1259
- viewport_width: input.viewportWidth ?? 1280,
1260
- viewport_height: input.viewportHeight ?? 720,
1261
- external_id: input.externalId,
1262
- repo_id: input.repoId,
1263
- commit_id: input.commitId,
1264
- record_video: input.recordVideo ?? false,
1265
- video_width: input.videoWidth ?? input.viewportWidth ?? 1280,
1266
- video_height: input.videoHeight ?? input.viewportHeight ?? 720,
1267
- allow_resizing: input.allowResizing ?? false,
1268
- structured_output: input.structuredOutput,
1269
- auth: input.auth,
1270
- profile_id: input.profileId,
1271
- force_query_params: input.forceQueryParams
1272
- })
1273
- },
1274
- config.retryConfig
1275
- );
1276
- const response = await withTimeout(
1277
- fetchPromise,
1278
- timeout,
1279
- `Browser task timed out after ${timeout}ms. Consider increasing timeout or reducing maxSteps.`
1280
- );
1281
- if (!response.ok) {
1282
- const errorText = await response.text().catch(() => response.statusText);
1283
- if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);
1284
- throw new Error(`HTTP ${response.status}: ${errorText}`);
1285
- }
1286
- const result = mapTaskResult(await response.json());
1287
- const elapsed = Date.now() - startTime;
1288
- if (debug) {
1289
- console.log(`[Browser] \u2705 ${result.success ? "Success" : "Failed"} in ${elapsed}ms | steps=${result.stepsTaken ?? 0} recordingId=${result.recordingId ?? "none"}`);
1290
- }
1291
- return result;
1292
- } catch (error) {
1293
- if (error instanceof Error) {
1294
- if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
1295
- return {
1296
- success: false,
1297
- error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running and accessible. For local dev, set MORPH_ENVIRONMENT=DEV.`
1298
- };
1299
- }
1300
- return {
1301
- success: false,
1302
- error: error.message
1303
- };
1304
- }
1305
- return {
1306
- success: false,
1307
- error: String(error)
1308
- };
1462
+ if (!input.repoId || typeof input.repoId !== "string") {
1463
+ throw new MorphValidationError("repoId is required", "repoId");
1464
+ }
1465
+ if (input.repoId.trim().length === 0) {
1466
+ throw new MorphValidationError("repoId cannot be empty", "repoId");
1309
1467
  }
1310
1468
  }
1311
- async function getRecording(recordingId, config = {}) {
1312
- const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1313
- const debug = config.debug || false;
1314
- if (!config.apiKey) {
1315
- throw new Error("API key required for getRecording");
1469
+ function validateId(id, fieldName) {
1470
+ if (!id || typeof id !== "string") {
1471
+ throw new MorphValidationError(`${fieldName} is required`, fieldName);
1316
1472
  }
1317
- if (debug) console.log(`[Browser] getRecording: ${recordingId}`);
1318
- const response = await fetch(`${apiUrl}/recordings/${recordingId}`, {
1319
- method: "GET",
1320
- headers: { "Authorization": `Bearer ${config.apiKey}` }
1321
- });
1473
+ if (id.trim().length === 0) {
1474
+ throw new MorphValidationError(`${fieldName} cannot be empty`, fieldName);
1475
+ }
1476
+ }
1477
+ async function createProfile(input, config = {}) {
1478
+ validateCreateInput(input);
1479
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1480
+ const headers = buildHeaders(config);
1481
+ const response = await fetchWithRetry(
1482
+ `${apiUrl}/profiles`,
1483
+ {
1484
+ method: "POST",
1485
+ headers,
1486
+ body: JSON.stringify(transformCreateInput(input))
1487
+ },
1488
+ config.retryConfig
1489
+ );
1322
1490
  if (!response.ok) {
1323
1491
  const errorText = await response.text().catch(() => response.statusText);
1324
- if (debug) console.error(`[Browser] getRecording error: ${response.status} - ${errorText}`);
1325
- throw new Error(`HTTP ${response.status}: ${errorText}`);
1492
+ const requestId = response.headers.get("x-request-id") || void 0;
1493
+ throw parseAPIError(response.status, errorText, requestId);
1326
1494
  }
1327
- const data = mapRecordingStatus(await response.json());
1328
- if (debug) console.log(`[Browser] Recording status: ${data.status}`);
1329
- return {
1330
- ...data,
1331
- getWebp: (options) => getWebp(recordingId, config, options),
1332
- getErrors: () => getErrors(recordingId, config)
1333
- };
1495
+ const apiProfile = await response.json();
1496
+ const profile = transformProfile(apiProfile);
1497
+ const session = await startProfileSession(config, { profileId: profile.id });
1498
+ return buildProfileSetup(profile, session, config);
1334
1499
  }
1335
- async function waitForRecording(recordingId, config = {}, options = {}) {
1336
- const timeout = options.timeout ?? 6e4;
1337
- const pollInterval = options.pollInterval ?? 2e3;
1338
- const startTime = Date.now();
1339
- while (Date.now() - startTime < timeout) {
1340
- const status = await getRecording(recordingId, config);
1341
- if (status.status === "COMPLETED" || status.status === "ERROR") {
1342
- return status;
1343
- }
1344
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
1500
+ async function listProfiles(config = {}, repoId) {
1501
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1502
+ const headers = buildHeaders(config);
1503
+ const url = repoId ? `${apiUrl}/profiles?repo_id=${encodeURIComponent(repoId)}` : `${apiUrl}/profiles`;
1504
+ const response = await fetchWithRetry(
1505
+ url,
1506
+ { method: "GET", headers },
1507
+ config.retryConfig
1508
+ );
1509
+ if (!response.ok) {
1510
+ const errorText = await response.text().catch(() => response.statusText);
1511
+ const requestId = response.headers.get("x-request-id") || void 0;
1512
+ throw parseAPIError(response.status, errorText, requestId);
1345
1513
  }
1346
- throw new Error(`Recording timeout after ${timeout}ms - status still pending`);
1514
+ const data = await response.json();
1515
+ return data.profiles.map(transformProfile);
1347
1516
  }
1348
- async function executeWithRecording(input, config = {}) {
1349
- const taskResult = await executeBrowserTask(input, config);
1350
- if (taskResult.recordingId) {
1351
- try {
1352
- const recording = await waitForRecording(
1353
- taskResult.recordingId,
1354
- config,
1355
- { timeout: 6e4, pollInterval: 2e3 }
1356
- );
1357
- return {
1358
- ...taskResult,
1359
- recording
1360
- };
1361
- } catch (error) {
1362
- const errorRecording = {
1363
- id: taskResult.recordingId,
1364
- status: "ERROR",
1365
- error: error instanceof Error ? error.message : String(error),
1366
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1367
- getWebp: (options) => getWebp(taskResult.recordingId, config, options),
1368
- getErrors: () => getErrors(taskResult.recordingId, config)
1369
- };
1370
- return {
1371
- ...taskResult,
1372
- recording: errorRecording
1373
- };
1374
- }
1517
+ async function getProfile(id, config = {}) {
1518
+ validateId(id, "id");
1519
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1520
+ const headers = buildHeaders(config);
1521
+ const response = await fetchWithRetry(
1522
+ `${apiUrl}/profiles/${encodeURIComponent(id)}`,
1523
+ { method: "GET", headers },
1524
+ config.retryConfig
1525
+ );
1526
+ if (!response.ok) {
1527
+ const errorText = await response.text().catch(() => response.statusText);
1528
+ const requestId = response.headers.get("x-request-id") || void 0;
1529
+ throw parseAPIError(response.status, errorText, requestId);
1375
1530
  }
1376
- return taskResult;
1531
+ const apiProfile = await response.json();
1532
+ const profile = transformProfile(apiProfile);
1533
+ return {
1534
+ ...profile,
1535
+ getState: () => getProfileState(id, config),
1536
+ delete: () => deleteProfile(id, config)
1537
+ };
1377
1538
  }
1378
- async function getErrors(recordingId, config = {}) {
1379
- const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1380
- const debug = config.debug || false;
1539
+ async function updateProfile(id, config = {}) {
1540
+ validateId(id, "id");
1541
+ const profile = await fetchProfile(id, config);
1542
+ const session = await startProfileSession(config, { profileId: profile.id });
1543
+ return buildProfileSetup(profile, session, config);
1544
+ }
1545
+ async function deleteProfile(id, config = {}) {
1546
+ validateId(id, "id");
1547
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1548
+ const headers = buildHeaders(config);
1549
+ const response = await fetchWithRetry(
1550
+ `${apiUrl}/profiles/${encodeURIComponent(id)}`,
1551
+ { method: "DELETE", headers },
1552
+ config.retryConfig
1553
+ );
1554
+ if (!response.ok) {
1555
+ const errorText = await response.text().catch(() => response.statusText);
1556
+ const requestId = response.headers.get("x-request-id") || void 0;
1557
+ throw parseAPIError(response.status, errorText, requestId);
1558
+ }
1559
+ }
1560
+ async function startProfileSession(config = {}, input) {
1381
1561
  if (!config.apiKey) {
1382
- throw new Error("API key required for getErrors");
1562
+ throw new MorphAuthenticationError();
1383
1563
  }
1384
- if (debug) console.log(`[Browser] getErrors: ${recordingId}`);
1385
- const response = await fetch(`${apiUrl}/recordings/${recordingId}/errors`, {
1386
- method: "GET",
1387
- headers: { "Authorization": `Bearer ${config.apiKey}` }
1388
- });
1564
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1565
+ const headers = buildHeaders(config);
1566
+ const body = input?.profileId ? { profile_id: input.profileId } : {};
1567
+ const response = await fetchWithRetry(
1568
+ `${apiUrl}/profiles/session/start`,
1569
+ {
1570
+ method: "POST",
1571
+ headers,
1572
+ body: JSON.stringify(body)
1573
+ },
1574
+ config.retryConfig
1575
+ );
1389
1576
  if (!response.ok) {
1390
1577
  const errorText = await response.text().catch(() => response.statusText);
1391
- if (debug) console.error(`[Browser] getErrors error: ${response.status} - ${errorText}`);
1392
- throw new Error(`HTTP ${response.status}: ${errorText}`);
1578
+ const requestId = response.headers.get("x-request-id") || void 0;
1579
+ throw parseAPIError(response.status, errorText, requestId);
1393
1580
  }
1394
- const errors = mapErrorsResponse(await response.json());
1395
- if (debug) console.log(`[Browser] Found ${errors.totalErrors} errors`);
1396
- return errors;
1581
+ const apiSession = await response.json();
1582
+ return transformSession(apiSession);
1397
1583
  }
1398
- function stringifyStructuredOutput(schema) {
1399
- try {
1400
- return JSON.stringify({
1401
- type: "object",
1402
- description: "Zod schema definition (Zod v3)",
1403
- zodDef: schema._def
1404
- });
1405
- } catch (error) {
1406
- console.warn("[Browser] Failed to serialize Zod schema:", error);
1407
- return JSON.stringify({
1408
- type: "object",
1409
- description: "Schema serialization failed"
1410
- });
1584
+ async function saveProfileSession(input, config = {}) {
1585
+ validateId(input.sessionId, "sessionId");
1586
+ validateId(input.profileId, "profileId");
1587
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1588
+ const headers = buildHeaders(config);
1589
+ const response = await fetchWithRetry(
1590
+ `${apiUrl}/profiles/session/save`,
1591
+ {
1592
+ method: "POST",
1593
+ headers,
1594
+ body: JSON.stringify(transformSaveInput(input))
1595
+ },
1596
+ config.retryConfig
1597
+ );
1598
+ if (!response.ok) {
1599
+ const errorText = await response.text().catch(() => response.statusText);
1600
+ const requestId = response.headers.get("x-request-id") || void 0;
1601
+ throw parseAPIError(response.status, errorText, requestId);
1411
1602
  }
1603
+ const apiProfile = await response.json();
1604
+ return transformProfile(apiProfile);
1412
1605
  }
1413
- function parseStructuredTaskOutput(result, schema) {
1414
- if (!result.output) {
1415
- return { ...result, parsed: null };
1416
- }
1417
- try {
1418
- const parsed = JSON.parse(result.output);
1419
- const validated = schema.parse(parsed);
1420
- return { ...result, parsed: validated };
1421
- } catch (error) {
1422
- if (error instanceof SyntaxError) {
1423
- return { ...result, parsed: null };
1424
- }
1425
- throw error;
1606
+ async function listRepos(config = {}) {
1607
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1608
+ const headers = buildHeaders(config);
1609
+ const response = await fetchWithRetry(
1610
+ `${apiUrl}/repos`,
1611
+ { method: "GET", headers },
1612
+ config.retryConfig
1613
+ );
1614
+ if (!response.ok) {
1615
+ const errorText = await response.text().catch(() => response.statusText);
1616
+ const requestId = response.headers.get("x-request-id") || void 0;
1617
+ throw parseAPIError(response.status, errorText, requestId);
1426
1618
  }
1619
+ const data = await response.json();
1620
+ const repos = Array.isArray(data?.repos) ? data.repos : [];
1621
+ return repos.map((repo) => ({
1622
+ repoId: repo.repo_id,
1623
+ repoFullName: repo.repo_full_name,
1624
+ profileCount: repo.profile_count ?? 0
1625
+ }));
1427
1626
  }
1428
- function mapTaskResult(api) {
1429
- if (!api || typeof api !== "object") {
1430
- return api;
1627
+ async function getProfileState(profileId, config = {}) {
1628
+ validateId(profileId, "profileId");
1629
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1630
+ const headers = buildHeaders(config);
1631
+ const response = await fetchWithRetry(
1632
+ `${apiUrl}/profiles/${encodeURIComponent(profileId)}/state`,
1633
+ { method: "GET", headers },
1634
+ config.retryConfig
1635
+ );
1636
+ if (!response.ok) {
1637
+ const errorText = await response.text().catch(() => response.statusText);
1638
+ const requestId = response.headers.get("x-request-id") || void 0;
1639
+ throw parseAPIError(response.status, errorText, requestId);
1431
1640
  }
1432
- return {
1433
- success: api.success,
1434
- result: api.result,
1435
- error: api.error,
1436
- stepsTaken: api.steps_taken,
1437
- executionTimeMs: api.execution_time_ms,
1438
- urls: api.urls,
1439
- actionNames: api.action_names,
1440
- errors: api.errors,
1441
- modelActions: api.model_actions,
1442
- isDone: api.is_done,
1443
- actionHistory: api.action_history,
1444
- actionResults: api.action_results,
1445
- hasErrors: api.has_errors,
1446
- numberOfSteps: api.number_of_steps,
1447
- judgement: api.judgement,
1448
- isValidated: api.is_validated,
1449
- replayId: api.replay_id,
1450
- replayUrl: api.replay_url,
1451
- recordingId: api.recording_id,
1452
- recordingStatus: api.recording_status,
1453
- taskId: api.task_id,
1454
- status: api.status,
1455
- output: api.output,
1456
- debugUrl: api.debug_url
1457
- };
1641
+ const apiState = await response.json();
1642
+ return transformStateResponse(apiState);
1458
1643
  }
1459
- function mapRecordingStatus(api) {
1460
- return {
1461
- id: api.id,
1462
- status: api.status,
1463
- replayUrl: api.replay_url,
1464
- networkUrl: api.network_url,
1465
- consoleUrl: api.console_url,
1466
- videoUrl: api.video_url,
1467
- result: api.result,
1468
- totalEvents: api.total_events,
1469
- fileSize: api.file_size,
1470
- duration: api.duration,
1471
- error: api.error,
1472
- createdAt: api.created_at
1473
- };
1644
+ function buildHeaders(config) {
1645
+ const headers = { "Content-Type": "application/json" };
1646
+ if (config.apiKey) {
1647
+ headers["Authorization"] = `Bearer ${config.apiKey}`;
1648
+ }
1649
+ return headers;
1474
1650
  }
1475
- function mapBrowserError(api) {
1476
- return {
1477
- type: api.type,
1478
- message: api.message,
1479
- url: api.url,
1480
- timestamp: api.timestamp,
1481
- screenshotUrl: api.screenshot_url,
1482
- capturedAt: api.captured_at,
1483
- status: api.status
1484
- };
1651
+ async function fetchProfile(id, config) {
1652
+ const apiUrl = config.apiUrl || DEFAULT_API_URL2;
1653
+ const headers = buildHeaders(config);
1654
+ const response = await fetchWithRetry(
1655
+ `${apiUrl}/profiles/${encodeURIComponent(id)}`,
1656
+ { method: "GET", headers },
1657
+ config.retryConfig
1658
+ );
1659
+ if (!response.ok) {
1660
+ const errorText = await response.text().catch(() => response.statusText);
1661
+ const requestId = response.headers.get("x-request-id") || void 0;
1662
+ throw parseAPIError(response.status, errorText, requestId);
1663
+ }
1664
+ const apiProfile = await response.json();
1665
+ return transformProfile(apiProfile);
1485
1666
  }
1486
- function mapErrorsResponse(api) {
1667
+ function buildProfileSetup(profile, session, config) {
1487
1668
  return {
1488
- recordingId: api.recording_id,
1489
- totalErrors: api.total_errors,
1490
- errors: Array.isArray(api.errors) ? api.errors.map(mapBrowserError) : []
1669
+ profile,
1670
+ session,
1671
+ save: () => saveProfileSession({ sessionId: session.sessionId, profileId: profile.id }, config)
1491
1672
  };
1492
1673
  }
1493
- function mapWebpResponse(api) {
1494
- return {
1495
- webpUrl: api.webp_url,
1496
- cached: api.cached,
1497
- width: api.width,
1498
- fps: api.fps,
1499
- maxDuration: api.max_duration,
1500
- fileSize: api.file_size,
1501
- maxSizeMb: api.max_size_mb,
1502
- budgetMet: api.budget_met,
1503
- qualityUsed: api.quality_used,
1504
- attempts: api.attempts
1505
- };
1674
+
1675
+ // tools/browser/core.ts
1676
+ var DEFAULT_CONFIG2 = {
1677
+ apiUrl: process.env.MORPH_ENVIRONMENT === "DEV" ? "http://localhost:8000" : "https://browser.morphllm.com",
1678
+ timeout: 1e6,
1679
+ // 10 minutes for complex tasks
1680
+ debug: false
1681
+ };
1682
+ var BrowserClient = class {
1683
+ config;
1684
+ /**
1685
+ * Profile management - create and manage browser profiles for storing login state.
1686
+ */
1687
+ profiles;
1688
+ constructor(config = {}) {
1689
+ this.config = {
1690
+ ...DEFAULT_CONFIG2,
1691
+ ...config
1692
+ };
1693
+ this.profiles = new ProfilesClient(this.config);
1694
+ }
1695
+ /**
1696
+ * Execute a browser automation task
1697
+ */
1698
+ async execute(input) {
1699
+ return executeBrowserTask(input, this.config);
1700
+ }
1701
+ async createTask(input) {
1702
+ const apiUrl = this.config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1703
+ const debug = this.config.debug || false;
1704
+ const hasTask = typeof input.task === "string" && input.task.trim().length > 0;
1705
+ const hasDiff = typeof input.diff === "string" && input.diff.trim().length > 0;
1706
+ if (!hasTask && !hasDiff) {
1707
+ throw new Error('Browser task requires either "task" (natural language) or "diff" (PR-review planning)');
1708
+ }
1709
+ if (debug) {
1710
+ const preview = (input.task ?? "").slice(0, 60);
1711
+ console.log(`[Browser] createTask: "${preview}..." url=${input.url || "none"}`);
1712
+ console.log(`[Browser] Calling async endpoint: ${apiUrl}/browser-task/async`);
1713
+ }
1714
+ const headers = { "Content-Type": "application/json" };
1715
+ if (this.config.apiKey) headers["Authorization"] = `Bearer ${this.config.apiKey}`;
1716
+ const response = await fetch(`${apiUrl}/browser-task/async`, {
1717
+ method: "POST",
1718
+ headers,
1719
+ body: JSON.stringify({
1720
+ task: input.task,
1721
+ diff: input.diff,
1722
+ url: input.url,
1723
+ max_steps: input.maxSteps ?? 10,
1724
+ model: input.model ?? "morph-computer-use-v0",
1725
+ viewport_width: input.viewportWidth ?? 1280,
1726
+ viewport_height: input.viewportHeight ?? 720,
1727
+ external_id: input.externalId,
1728
+ repo_id: input.repoId,
1729
+ repo_full_name: input.repoFullName,
1730
+ commit_id: input.commitId,
1731
+ record_video: input.recordVideo ?? false,
1732
+ video_width: input.videoWidth ?? input.viewportWidth ?? 1280,
1733
+ video_height: input.videoHeight ?? input.viewportHeight ?? 720,
1734
+ allow_resizing: input.allowResizing ?? false,
1735
+ structured_output: "schema" in input ? stringifyStructuredOutput(input.schema) : void 0,
1736
+ auth: input.auth,
1737
+ profile_id: input.profileId,
1738
+ force_query_params: input.forceQueryParams
1739
+ })
1740
+ });
1741
+ if (!response.ok) {
1742
+ const errorText = await response.text().catch(() => response.statusText);
1743
+ if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);
1744
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
1745
+ }
1746
+ const result = mapTaskResult(await response.json());
1747
+ if (debug) {
1748
+ const debugUrl = result.debugUrl;
1749
+ console.log(`[Browser] \u2705 Task created: recordingId=${result.recordingId ?? "none"} debugUrl=${debugUrl ? "available" : "none"}`);
1750
+ }
1751
+ if ("schema" in input) {
1752
+ return wrapTaskResponseWithSchema(result, this.config, input.schema);
1753
+ } else {
1754
+ return wrapTaskResponse(result, this.config);
1755
+ }
1756
+ }
1757
+ /**
1758
+ * Execute task with recording and wait for video to be ready
1759
+ */
1760
+ async executeWithRecording(input) {
1761
+ return executeWithRecording(input, this.config);
1762
+ }
1763
+ /**
1764
+ * Get recording status and URLs
1765
+ */
1766
+ async getRecording(recordingId) {
1767
+ return getRecording(recordingId, this.config);
1768
+ }
1769
+ /**
1770
+ * Wait for recording to complete with automatic polling
1771
+ */
1772
+ async waitForRecording(recordingId, options) {
1773
+ return waitForRecording(recordingId, this.config, options);
1774
+ }
1775
+ /**
1776
+ * Get errors from recording with screenshots
1777
+ */
1778
+ async getErrors(recordingId) {
1779
+ return getErrors(recordingId, this.config);
1780
+ }
1781
+ /**
1782
+ * Get animated WebP preview of recording
1783
+ */
1784
+ async getWebp(recordingId, options) {
1785
+ return getWebp(recordingId, this.config, options);
1786
+ }
1787
+ /**
1788
+ * Check if browser worker service is healthy
1789
+ */
1790
+ async checkHealth() {
1791
+ return checkHealth(this.config);
1792
+ }
1793
+ };
1794
+ async function executeBrowserTask(input, config = {}) {
1795
+ const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1796
+ const timeout = config.timeout || DEFAULT_CONFIG2.timeout;
1797
+ const debug = config.debug || false;
1798
+ if (!input.task || input.task.trim().length === 0) {
1799
+ return {
1800
+ success: false,
1801
+ error: 'Task description is required. Example: "Go to example.com and click the login button"'
1802
+ };
1803
+ }
1804
+ if (input.maxSteps !== void 0 && (input.maxSteps < 1 || input.maxSteps > 50)) {
1805
+ return {
1806
+ success: false,
1807
+ error: "maxSteps must be between 1 and 50. Use more steps for complex multi-page flows."
1808
+ };
1809
+ }
1810
+ if (debug) {
1811
+ console.log(`[Browser] Task: "${input.task.slice(0, 60)}..." url=${input.url || "none"} maxSteps=${input.maxSteps ?? 10}`);
1812
+ console.log(`[Browser] Recording: ${input.recordVideo ? "yes" : "no"} | Calling ${apiUrl}/browser-task`);
1813
+ }
1814
+ const startTime = Date.now();
1815
+ try {
1816
+ const headers = { "Content-Type": "application/json" };
1817
+ if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1818
+ const fetchPromise = fetchWithRetry(
1819
+ `${apiUrl}/browser-task`,
1820
+ {
1821
+ method: "POST",
1822
+ headers,
1823
+ body: JSON.stringify({
1824
+ task: input.task,
1825
+ url: input.url,
1826
+ max_steps: input.maxSteps ?? 10,
1827
+ model: input.model ?? "morph-computer-use-v0",
1828
+ viewport_width: input.viewportWidth ?? 1280,
1829
+ viewport_height: input.viewportHeight ?? 720,
1830
+ external_id: input.externalId,
1831
+ repo_id: input.repoId,
1832
+ commit_id: input.commitId,
1833
+ record_video: input.recordVideo ?? false,
1834
+ video_width: input.videoWidth ?? input.viewportWidth ?? 1280,
1835
+ video_height: input.videoHeight ?? input.viewportHeight ?? 720,
1836
+ allow_resizing: input.allowResizing ?? false,
1837
+ structured_output: input.structuredOutput,
1838
+ auth: input.auth,
1839
+ profile_id: input.profileId,
1840
+ force_query_params: input.forceQueryParams
1841
+ })
1842
+ },
1843
+ config.retryConfig
1844
+ );
1845
+ const response = await withTimeout(
1846
+ fetchPromise,
1847
+ timeout,
1848
+ `Browser task timed out after ${timeout}ms. Consider increasing timeout or reducing maxSteps.`
1849
+ );
1850
+ if (!response.ok) {
1851
+ const errorText = await response.text().catch(() => response.statusText);
1852
+ if (debug) console.error(`[Browser] Error: ${response.status} - ${errorText}`);
1853
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
1854
+ }
1855
+ const result = mapTaskResult(await response.json());
1856
+ const elapsed = Date.now() - startTime;
1857
+ if (debug) {
1858
+ console.log(`[Browser] \u2705 ${result.success ? "Success" : "Failed"} in ${elapsed}ms | steps=${result.stepsTaken ?? 0} recordingId=${result.recordingId ?? "none"}`);
1859
+ }
1860
+ return result;
1861
+ } catch (error) {
1862
+ if (error instanceof Error) {
1863
+ if (error.message.includes("ECONNREFUSED") || error.message.includes("fetch failed")) {
1864
+ return {
1865
+ success: false,
1866
+ error: `Cannot connect to browser worker at ${apiUrl}. Ensure the service is running and accessible. For local dev, set MORPH_ENVIRONMENT=DEV.`
1867
+ };
1868
+ }
1869
+ return {
1870
+ success: false,
1871
+ error: error.message
1872
+ };
1873
+ }
1874
+ return {
1875
+ success: false,
1876
+ error: String(error)
1877
+ };
1878
+ }
1506
1879
  }
1507
- async function getTaskStatus(taskId, config) {
1880
+ async function getRecording(recordingId, config = {}) {
1508
1881
  const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1509
1882
  const debug = config.debug || false;
1510
- if (debug) console.log(`[Browser] getTaskStatus: ${taskId}`);
1511
- const headers = {};
1512
- if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
1513
- const response = await fetch(`${apiUrl}/tasks/${taskId}`, {
1883
+ if (!config.apiKey) {
1884
+ throw new Error("API key required for getRecording");
1885
+ }
1886
+ if (debug) console.log(`[Browser] getRecording: ${recordingId}`);
1887
+ const response = await fetch(`${apiUrl}/recordings/${recordingId}`, {
1514
1888
  method: "GET",
1515
- headers
1889
+ headers: { "Authorization": `Bearer ${config.apiKey}` }
1516
1890
  });
1517
1891
  if (!response.ok) {
1518
1892
  const errorText = await response.text().catch(() => response.statusText);
1519
- if (debug) console.error(`[Browser] getTaskStatus error: ${response.status} - ${errorText}`);
1893
+ if (debug) console.error(`[Browser] getRecording error: ${response.status} - ${errorText}`);
1520
1894
  throw new Error(`HTTP ${response.status}: ${errorText}`);
1521
1895
  }
1522
- const result = mapTaskResult(await response.json());
1523
- if (debug) console.log(`[Browser] Task status: ${result.status}`);
1524
- return result;
1525
- }
1526
- function generateLiveUrl(taskId, config) {
1527
- const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1528
- const baseUrl = apiUrl.replace("/api", "");
1529
- return `${baseUrl}/tasks/${taskId}/live`;
1896
+ const data = mapRecordingStatus(await response.json());
1897
+ if (debug) console.log(`[Browser] Recording status: ${data.status}`);
1898
+ return {
1899
+ ...data,
1900
+ getWebp: (options) => getWebp(recordingId, config, options),
1901
+ getErrors: () => getErrors(recordingId, config)
1902
+ };
1530
1903
  }
1531
- async function pollTaskUntilComplete(taskId, config, pollConfig = {}) {
1532
- const interval = pollConfig.interval ?? 2e3;
1533
- const timeout = pollConfig.timeout ?? 3e5;
1904
+ async function waitForRecording(recordingId, config = {}, options = {}) {
1905
+ const timeout = options.timeout ?? 6e4;
1906
+ const pollInterval = options.pollInterval ?? 2e3;
1534
1907
  const startTime = Date.now();
1535
1908
  while (Date.now() - startTime < timeout) {
1536
- const status = await getTaskStatus(taskId, config);
1537
- if (status.status === "completed" || status.status === "failed") {
1909
+ const status = await getRecording(recordingId, config);
1910
+ if (status.status === "COMPLETED" || status.status === "ERROR") {
1911
+ return status;
1912
+ }
1913
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
1914
+ }
1915
+ throw new Error(`Recording timeout after ${timeout}ms - status still pending`);
1916
+ }
1917
+ async function executeWithRecording(input, config = {}) {
1918
+ const taskResult = await executeBrowserTask(input, config);
1919
+ if (taskResult.recordingId) {
1920
+ try {
1921
+ const recording = await waitForRecording(
1922
+ taskResult.recordingId,
1923
+ config,
1924
+ { timeout: 6e4, pollInterval: 2e3 }
1925
+ );
1926
+ return {
1927
+ ...taskResult,
1928
+ recording
1929
+ };
1930
+ } catch (error) {
1931
+ const errorRecording = {
1932
+ id: taskResult.recordingId,
1933
+ status: "ERROR",
1934
+ error: error instanceof Error ? error.message : String(error),
1935
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1936
+ getWebp: (options) => getWebp(taskResult.recordingId, config, options),
1937
+ getErrors: () => getErrors(taskResult.recordingId, config)
1938
+ };
1939
+ return {
1940
+ ...taskResult,
1941
+ recording: errorRecording
1942
+ };
1943
+ }
1944
+ }
1945
+ return taskResult;
1946
+ }
1947
+ async function getErrors(recordingId, config = {}) {
1948
+ const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
1949
+ const debug = config.debug || false;
1950
+ if (!config.apiKey) {
1951
+ throw new Error("API key required for getErrors");
1952
+ }
1953
+ if (debug) console.log(`[Browser] getErrors: ${recordingId}`);
1954
+ const response = await fetch(`${apiUrl}/recordings/${recordingId}/errors`, {
1955
+ method: "GET",
1956
+ headers: { "Authorization": `Bearer ${config.apiKey}` }
1957
+ });
1958
+ if (!response.ok) {
1959
+ const errorText = await response.text().catch(() => response.statusText);
1960
+ if (debug) console.error(`[Browser] getErrors error: ${response.status} - ${errorText}`);
1961
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
1962
+ }
1963
+ const errors = mapErrorsResponse(await response.json());
1964
+ if (debug) console.log(`[Browser] Found ${errors.totalErrors} errors`);
1965
+ return errors;
1966
+ }
1967
+ function stringifyStructuredOutput(schema) {
1968
+ try {
1969
+ return JSON.stringify({
1970
+ type: "object",
1971
+ description: "Zod schema definition (Zod v3)",
1972
+ zodDef: schema._def
1973
+ });
1974
+ } catch (error) {
1975
+ console.warn("[Browser] Failed to serialize Zod schema:", error);
1976
+ return JSON.stringify({
1977
+ type: "object",
1978
+ description: "Schema serialization failed"
1979
+ });
1980
+ }
1981
+ }
1982
+ function parseStructuredTaskOutput(result, schema) {
1983
+ if (!result.output) {
1984
+ return { ...result, parsed: null };
1985
+ }
1986
+ try {
1987
+ const parsed = JSON.parse(result.output);
1988
+ const validated = schema.parse(parsed);
1989
+ return { ...result, parsed: validated };
1990
+ } catch (error) {
1991
+ if (error instanceof SyntaxError) {
1992
+ return { ...result, parsed: null };
1993
+ }
1994
+ throw error;
1995
+ }
1996
+ }
1997
+ function mapTaskResult(api) {
1998
+ if (!api || typeof api !== "object") {
1999
+ return api;
2000
+ }
2001
+ return {
2002
+ success: api.success,
2003
+ result: api.result,
2004
+ error: api.error,
2005
+ stepsTaken: api.steps_taken,
2006
+ executionTimeMs: api.execution_time_ms,
2007
+ urls: api.urls,
2008
+ actionNames: api.action_names,
2009
+ errors: api.errors,
2010
+ modelActions: api.model_actions,
2011
+ isDone: api.is_done,
2012
+ actionHistory: api.action_history,
2013
+ actionResults: api.action_results,
2014
+ hasErrors: api.has_errors,
2015
+ numberOfSteps: api.number_of_steps,
2016
+ judgement: api.judgement,
2017
+ isValidated: api.is_validated,
2018
+ replayId: api.replay_id,
2019
+ replayUrl: api.replay_url,
2020
+ recordingId: api.recording_id,
2021
+ recordingStatus: api.recording_status,
2022
+ taskId: api.task_id,
2023
+ status: api.status,
2024
+ output: api.output,
2025
+ debugUrl: api.debug_url
2026
+ };
2027
+ }
2028
+ function mapRecordingStatus(api) {
2029
+ return {
2030
+ id: api.id,
2031
+ status: api.status,
2032
+ replayUrl: api.replay_url,
2033
+ networkUrl: api.network_url,
2034
+ consoleUrl: api.console_url,
2035
+ videoUrl: api.video_url,
2036
+ result: api.result,
2037
+ totalEvents: api.total_events,
2038
+ fileSize: api.file_size,
2039
+ duration: api.duration,
2040
+ error: api.error,
2041
+ createdAt: api.created_at
2042
+ };
2043
+ }
2044
+ function mapBrowserError(api) {
2045
+ return {
2046
+ type: api.type,
2047
+ message: api.message,
2048
+ url: api.url,
2049
+ timestamp: api.timestamp,
2050
+ screenshotUrl: api.screenshot_url,
2051
+ capturedAt: api.captured_at,
2052
+ status: api.status
2053
+ };
2054
+ }
2055
+ function mapErrorsResponse(api) {
2056
+ return {
2057
+ recordingId: api.recording_id,
2058
+ totalErrors: api.total_errors,
2059
+ errors: Array.isArray(api.errors) ? api.errors.map(mapBrowserError) : []
2060
+ };
2061
+ }
2062
+ function mapWebpResponse(api) {
2063
+ return {
2064
+ webpUrl: api.webp_url,
2065
+ cached: api.cached,
2066
+ width: api.width,
2067
+ fps: api.fps,
2068
+ maxDuration: api.max_duration,
2069
+ fileSize: api.file_size,
2070
+ maxSizeMb: api.max_size_mb,
2071
+ budgetMet: api.budget_met,
2072
+ qualityUsed: api.quality_used,
2073
+ attempts: api.attempts
2074
+ };
2075
+ }
2076
+ async function getTaskStatus(taskId, config) {
2077
+ const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
2078
+ const debug = config.debug || false;
2079
+ if (debug) console.log(`[Browser] getTaskStatus: ${taskId}`);
2080
+ const headers = {};
2081
+ if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
2082
+ const response = await fetch(`${apiUrl}/tasks/${taskId}`, {
2083
+ method: "GET",
2084
+ headers
2085
+ });
2086
+ if (!response.ok) {
2087
+ const errorText = await response.text().catch(() => response.statusText);
2088
+ if (debug) console.error(`[Browser] getTaskStatus error: ${response.status} - ${errorText}`);
2089
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
2090
+ }
2091
+ const result = mapTaskResult(await response.json());
2092
+ if (debug) console.log(`[Browser] Task status: ${result.status}`);
2093
+ return result;
2094
+ }
2095
+ function generateLiveUrl(taskId, config) {
2096
+ const apiUrl = config.apiUrl || DEFAULT_CONFIG2.apiUrl;
2097
+ const baseUrl = apiUrl.replace("/api", "");
2098
+ return `${baseUrl}/tasks/${taskId}/live`;
2099
+ }
2100
+ async function pollTaskUntilComplete(taskId, config, pollConfig = {}) {
2101
+ const interval = pollConfig.interval ?? 2e3;
2102
+ const timeout = pollConfig.timeout ?? 3e5;
2103
+ const startTime = Date.now();
2104
+ while (Date.now() - startTime < timeout) {
2105
+ const status = await getTaskStatus(taskId, config);
2106
+ if (status.status === "completed" || status.status === "failed") {
1538
2107
  return status;
1539
2108
  }
1540
2109
  await new Promise((resolve2) => setTimeout(resolve2, interval));
@@ -1690,103 +2259,8 @@ async function checkHealth(config = {}) {
1690
2259
  }
1691
2260
  }
1692
2261
 
1693
- // tools/warp_grep/agent/config.ts
1694
- var parseEnvTimeout = (envValue, defaultMs) => {
1695
- if (!envValue) return defaultMs;
1696
- const parsed = parseInt(envValue, 10);
1697
- return isNaN(parsed) || parsed <= 0 ? defaultMs : parsed;
1698
- };
1699
- var AGENT_CONFIG = {
1700
- MAX_TURNS: 4,
1701
- /** Default timeout for model calls. Can be overridden via MORPH_WARP_GREP_TIMEOUT env var (in ms) */
1702
- TIMEOUT_MS: parseEnvTimeout(process.env.MORPH_WARP_GREP_TIMEOUT, 3e4),
1703
- MAX_CONTEXT_CHARS: 54e4,
1704
- MAX_OUTPUT_LINES: 200,
1705
- MAX_READ_LINES: 800,
1706
- MAX_LIST_DEPTH: 3,
1707
- LIST_TIMEOUT_MS: 2e3
1708
- };
1709
- var BUILTIN_EXCLUDES = [
1710
- // Version control
1711
- ".git",
1712
- ".svn",
1713
- ".hg",
1714
- ".bzr",
1715
- // Dependencies
1716
- "node_modules",
1717
- "bower_components",
1718
- ".pnpm",
1719
- ".yarn",
1720
- "vendor",
1721
- "packages",
1722
- "Pods",
1723
- ".bundle",
1724
- // Python
1725
- "__pycache__",
1726
- ".pytest_cache",
1727
- ".mypy_cache",
1728
- ".ruff_cache",
1729
- ".venv",
1730
- "venv",
1731
- ".tox",
1732
- ".nox",
1733
- ".eggs",
1734
- "*.egg-info",
1735
- // Build outputs
1736
- "dist",
1737
- "build",
1738
- "out",
1739
- "output",
1740
- "target",
1741
- "_build",
1742
- ".next",
1743
- ".nuxt",
1744
- ".output",
1745
- ".vercel",
1746
- ".netlify",
1747
- // Cache directories
1748
- ".cache",
1749
- ".parcel-cache",
1750
- ".turbo",
1751
- ".nx",
1752
- ".gradle",
1753
- // IDE/Editor
1754
- ".idea",
1755
- ".vscode",
1756
- ".vs",
1757
- // Coverage
1758
- "coverage",
1759
- ".coverage",
1760
- "htmlcov",
1761
- ".nyc_output",
1762
- // Temporary
1763
- "tmp",
1764
- "temp",
1765
- ".tmp",
1766
- ".temp",
1767
- // Lock files
1768
- "package-lock.json",
1769
- "yarn.lock",
1770
- "pnpm-lock.yaml",
1771
- "bun.lockb",
1772
- "Cargo.lock",
1773
- "Gemfile.lock",
1774
- "poetry.lock",
1775
- // Binary/minified
1776
- "*.min.js",
1777
- "*.min.css",
1778
- "*.bundle.js",
1779
- "*.wasm",
1780
- "*.so",
1781
- "*.dll",
1782
- "*.pyc",
1783
- "*.map",
1784
- "*.js.map",
1785
- // Hidden directories catch-all
1786
- ".*"
1787
- ];
1788
- var DEFAULT_EXCLUDES = (process.env.MORPH_WARP_GREP_EXCLUDE || "").split(",").map((s) => s.trim()).filter(Boolean).concat(BUILTIN_EXCLUDES);
1789
- var DEFAULT_MODEL = "morph-warp-grep-v1";
2262
+ // tools/warp_grep/agent/runner.ts
2263
+ init_config();
1790
2264
 
1791
2265
  // tools/warp_grep/agent/prompt.ts
1792
2266
  var SYSTEM_PROMPT = `You are a code search agent. Your task is to find all relevant code for a given search_string.
@@ -2302,6 +2776,7 @@ async function toolRead(provider, args) {
2302
2776
  }
2303
2777
 
2304
2778
  // tools/warp_grep/agent/tools/list_directory.ts
2779
+ init_config();
2305
2780
  async function toolListDirectory(provider, args) {
2306
2781
  const maxResults = args.maxResults ?? AGENT_CONFIG.MAX_OUTPUT_LINES;
2307
2782
  const initialDepth = args.maxDepth ?? AGENT_CONFIG.MAX_LIST_DEPTH;
@@ -2476,6 +2951,7 @@ function formatAgentToolOutput(toolName, args, output, options = {}) {
2476
2951
 
2477
2952
  // tools/warp_grep/agent/helpers.ts
2478
2953
  var import_path2 = __toESM(require("path"), 1);
2954
+ init_config();
2479
2955
  var TRUNCATED_MARKER = "[truncated for context limit]";
2480
2956
  function formatTurnMessage(turnsUsed, maxTurns) {
2481
2957
  const turnsRemaining = maxTurns - turnsUsed;
@@ -2558,9 +3034,9 @@ function enforceContextLimit(messages, maxChars = AGENT_CONFIG.MAX_CONTEXT_CHARS
2558
3034
  var import_openai = __toESM(require("openai"), 1);
2559
3035
  var import_path3 = __toESM(require("path"), 1);
2560
3036
  var parser = new LLMResponseParser();
2561
- var DEFAULT_API_URL2 = "https://api.morphllm.com";
3037
+ var DEFAULT_API_URL3 = "https://api.morphllm.com";
2562
3038
  async function callModel(messages, model, options = {}) {
2563
- const baseUrl = options.morphApiUrl || DEFAULT_API_URL2;
3039
+ const baseUrl = options.morphApiUrl || DEFAULT_API_URL3;
2564
3040
  const apiKey = options.morphApiKey || process.env.MORPH_API_KEY || "";
2565
3041
  const timeoutMs = options.timeout ?? AGENT_CONFIG.TIMEOUT_MS;
2566
3042
  const client = new import_openai.default({
@@ -2669,624 +3145,225 @@ async function runWarpGrep(config) {
2669
3145
  toolRead(provider, args).then(
2670
3146
  (p) => formatAgentToolOutput("read", args, p, { isError: false }),
2671
3147
  (err) => formatAgentToolOutput("read", args, String(err), { isError: true })
2672
- )
2673
- );
2674
- }
2675
- const toolExecStart = Date.now();
2676
- const allResults = await Promise.all(allPromises);
2677
- turnMetrics.local_tools_ms = Date.now() - toolExecStart;
2678
- for (const result of allResults) {
2679
- formatted.push(result);
2680
- }
2681
- if (formatted.length > 0) {
2682
- const turnMessage = formatTurnMessage(turn, maxTurns);
2683
- const contextBudget = calculateContextBudget(messages);
2684
- messages.push({ role: "user", content: formatted.join("\n") + turnMessage + "\n" + contextBudget });
2685
- }
2686
- timings.turns.push(turnMetrics);
2687
- if (finishCalls.length) {
2688
- const fc = finishCalls[0];
2689
- const files = fc.arguments?.files ?? [];
2690
- finishMeta = { files };
2691
- terminationReason = "completed";
2692
- break;
2693
- }
2694
- }
2695
- if (terminationReason !== "completed" || !finishMeta) {
2696
- timings.total_ms = Date.now() - totalStart;
2697
- return { terminationReason, messages, errors, timings };
2698
- }
2699
- const parts = ["Relevant context found:"];
2700
- for (const f of finishMeta.files) {
2701
- const ranges = f.lines === "*" ? "*" : Array.isArray(f.lines) ? f.lines.map(([s, e]) => `${s}-${e}`).join(", ") : "*";
2702
- parts.push(`- ${f.path}: ${ranges}`);
2703
- }
2704
- const payload = parts.join("\n");
2705
- const finishResolutionStart = Date.now();
2706
- const fileReadErrors = [];
2707
- const resolved = await readFinishFiles(
2708
- repoRoot,
2709
- finishMeta.files,
2710
- async (p, s, e) => {
2711
- try {
2712
- const rr = await provider.read({ path: p, start: s, end: e });
2713
- return rr.lines.map((l) => {
2714
- const idx = l.indexOf("|");
2715
- return idx >= 0 ? l.slice(idx + 1) : l;
2716
- });
2717
- } catch (err) {
2718
- const errorMsg = err instanceof Error ? err.message : String(err);
2719
- fileReadErrors.push({ path: p, error: errorMsg });
2720
- console.error(`[warp_grep] Failed to read file: ${p} - ${errorMsg}`);
2721
- return [`[couldn't find: ${p}]`];
2722
- }
2723
- }
2724
- );
2725
- timings.finish_resolution_ms = Date.now() - finishResolutionStart;
2726
- if (fileReadErrors.length > 0) {
2727
- errors.push(...fileReadErrors.map((e) => ({ message: `File read error: ${e.path} - ${e.error}` })));
2728
- }
2729
- timings.total_ms = Date.now() - totalStart;
2730
- return {
2731
- terminationReason: "completed",
2732
- messages,
2733
- finish: { payload, metadata: finishMeta, resolved },
2734
- timings
2735
- };
2736
- }
2737
- async function* runWarpGrepStreaming(config) {
2738
- const totalStart = Date.now();
2739
- const timeoutMs = config.timeout ?? AGENT_CONFIG.TIMEOUT_MS;
2740
- const timings = { turns: [], timeout_ms: timeoutMs };
2741
- const repoRoot = import_path3.default.resolve(config.repoRoot || process.cwd());
2742
- const messages = [];
2743
- messages.push({ role: "system", content: getSystemPrompt() });
2744
- const initialStateStart = Date.now();
2745
- const initialState = await buildInitialState(repoRoot, config.query, config.provider);
2746
- timings.initial_state_ms = Date.now() - initialStateStart;
2747
- messages.push({ role: "user", content: initialState });
2748
- const maxTurns = AGENT_CONFIG.MAX_TURNS;
2749
- const model = config.model || DEFAULT_MODEL;
2750
- const provider = config.provider;
2751
- const errors = [];
2752
- let finishMeta;
2753
- let terminationReason = "terminated";
2754
- for (let turn = 1; turn <= maxTurns; turn += 1) {
2755
- const turnMetrics = { turn, morph_api_ms: 0, local_tools_ms: 0 };
2756
- enforceContextLimit(messages);
2757
- const modelCallStart = Date.now();
2758
- const assistantContent = await callModel(messages, model, {
2759
- morphApiKey: config.morphApiKey,
2760
- morphApiUrl: config.morphApiUrl,
2761
- retryConfig: config.retryConfig,
2762
- timeout: timeoutMs
2763
- }).catch((e) => {
2764
- errors.push({ message: e instanceof Error ? e.message : String(e) });
2765
- return "";
2766
- });
2767
- turnMetrics.morph_api_ms = Date.now() - modelCallStart;
2768
- if (!assistantContent) {
2769
- timings.turns.push(turnMetrics);
2770
- break;
2771
- }
2772
- messages.push({ role: "assistant", content: assistantContent });
2773
- const toolCalls = parser.parse(assistantContent);
2774
- if (toolCalls.length === 0) {
2775
- errors.push({ message: "No tool calls produced by the model. Your MCP is likely out of date! Update it by running: rm -rf ~/.npm/_npx && npm cache clean --force && npx -y @morphllm/morphmcp@latest" });
2776
- terminationReason = "terminated";
2777
- timings.turns.push(turnMetrics);
2778
- break;
2779
- }
2780
- yield {
2781
- turn,
2782
- toolCalls: toolCalls.map((c) => ({
2783
- name: c.name,
2784
- arguments: c.arguments ?? {}
2785
- }))
2786
- };
2787
- const finishCalls = toolCalls.filter((c) => c.name === "finish");
2788
- const grepCalls = toolCalls.filter((c) => c.name === "grep");
2789
- const listDirCalls = toolCalls.filter((c) => c.name === "list_directory");
2790
- const readCalls = toolCalls.filter((c) => c.name === "read");
2791
- const skipCalls = toolCalls.filter((c) => c.name === "_skip");
2792
- const formatted = [];
2793
- for (const c of skipCalls) {
2794
- const msg = c.arguments?.message || "Command skipped due to parsing error";
2795
- formatted.push(msg);
2796
- }
2797
- const allPromises = [];
2798
- for (const c of grepCalls) {
2799
- const args = c.arguments ?? {};
2800
- allPromises.push(
2801
- toolGrep(provider, args).then(
2802
- ({ output }) => formatAgentToolOutput("grep", args, output, { isError: false }),
2803
- (err) => formatAgentToolOutput("grep", args, String(err), { isError: true })
2804
- )
2805
- );
2806
- }
2807
- for (const c of listDirCalls) {
2808
- const args = c.arguments ?? {};
2809
- allPromises.push(
2810
- toolListDirectory(provider, args).then(
2811
- (p) => formatAgentToolOutput("list_directory", args, p, { isError: false }),
2812
- (err) => formatAgentToolOutput("list_directory", args, String(err), { isError: true })
2813
- )
2814
- );
2815
- }
2816
- for (const c of readCalls) {
2817
- const args = c.arguments ?? {};
2818
- allPromises.push(
2819
- toolRead(provider, args).then(
2820
- (p) => formatAgentToolOutput("read", args, p, { isError: false }),
2821
- (err) => formatAgentToolOutput("read", args, String(err), { isError: true })
2822
- )
2823
- );
2824
- }
2825
- const toolExecStart = Date.now();
2826
- const allResults = await Promise.all(allPromises);
2827
- turnMetrics.local_tools_ms = Date.now() - toolExecStart;
2828
- for (const result of allResults) {
2829
- formatted.push(result);
2830
- }
2831
- if (formatted.length > 0) {
2832
- const turnMessage = formatTurnMessage(turn, maxTurns);
2833
- const contextBudget = calculateContextBudget(messages);
2834
- messages.push({ role: "user", content: formatted.join("\n") + turnMessage + "\n" + contextBudget });
2835
- }
2836
- timings.turns.push(turnMetrics);
2837
- if (finishCalls.length) {
2838
- const fc = finishCalls[0];
2839
- const files = fc.arguments?.files ?? [];
2840
- finishMeta = { files };
2841
- terminationReason = "completed";
2842
- break;
2843
- }
2844
- }
2845
- if (terminationReason !== "completed" || !finishMeta) {
2846
- timings.total_ms = Date.now() - totalStart;
2847
- return { terminationReason, messages, errors, timings };
2848
- }
2849
- const parts = ["Relevant context found:"];
2850
- for (const f of finishMeta.files) {
2851
- const ranges = f.lines === "*" ? "*" : Array.isArray(f.lines) ? f.lines.map(([s, e]) => `${s}-${e}`).join(", ") : "*";
2852
- parts.push(`- ${f.path}: ${ranges}`);
2853
- }
2854
- const payload = parts.join("\n");
2855
- const finishResolutionStart = Date.now();
2856
- const fileReadErrors = [];
2857
- const resolved = await readFinishFiles(
2858
- repoRoot,
2859
- finishMeta.files,
2860
- async (p, s, e) => {
2861
- try {
2862
- const rr = await provider.read({ path: p, start: s, end: e });
2863
- return rr.lines.map((l) => {
2864
- const idx = l.indexOf("|");
2865
- return idx >= 0 ? l.slice(idx + 1) : l;
2866
- });
2867
- } catch (err) {
2868
- const errorMsg = err instanceof Error ? err.message : String(err);
2869
- fileReadErrors.push({ path: p, error: errorMsg });
2870
- console.error(`[warp_grep] Failed to read file: ${p} - ${errorMsg}`);
2871
- return [`[couldn't find: ${p}]`];
2872
- }
2873
- }
2874
- );
2875
- timings.finish_resolution_ms = Date.now() - finishResolutionStart;
2876
- if (fileReadErrors.length > 0) {
2877
- errors.push(...fileReadErrors.map((e) => ({ message: `File read error: ${e.path} - ${e.error}` })));
2878
- }
2879
- timings.total_ms = Date.now() - totalStart;
2880
- return {
2881
- terminationReason: "completed",
2882
- messages,
2883
- finish: { payload, metadata: finishMeta, resolved },
2884
- timings
2885
- };
2886
- }
2887
-
2888
- // tools/warp_grep/providers/local.ts
2889
- var import_promises3 = __toESM(require("fs/promises"), 1);
2890
- var import_path5 = __toESM(require("path"), 1);
2891
-
2892
- // tools/warp_grep/utils/ripgrep.ts
2893
- var import_child_process = require("child_process");
2894
- var import_ripgrep = require("@vscode/ripgrep");
2895
- var resolvedRgPath = null;
2896
- var rgPathChecked = false;
2897
- function spawnRg(rgBinary, args, opts) {
2898
- return new Promise((resolve2) => {
2899
- const child = (0, import_child_process.spawn)(rgBinary, args, {
2900
- cwd: opts?.cwd,
2901
- env: { ...process.env, ...opts?.env || {} },
2902
- stdio: ["ignore", "pipe", "pipe"]
2903
- });
2904
- let stdout = "";
2905
- let stderr = "";
2906
- child.stdout.on("data", (d) => stdout += d.toString());
2907
- child.stderr.on("data", (d) => stderr += d.toString());
2908
- child.on("close", (code) => {
2909
- resolve2({ stdout, stderr, exitCode: typeof code === "number" ? code : -1 });
2910
- });
2911
- child.on("error", () => {
2912
- resolve2({ stdout: "", stderr: "Failed to spawn ripgrep.", exitCode: -1 });
2913
- });
2914
- });
2915
- }
2916
- function isBinaryFailure(result) {
2917
- if (result.exitCode === -1) return true;
2918
- if (result.stderr.includes("jemalloc") || result.stderr.includes("Unsupported system page size")) return true;
2919
- if (result.exitCode === 134) return true;
2920
- return false;
2921
- }
2922
- async function runRipgrep(args, opts) {
2923
- if (rgPathChecked && resolvedRgPath) {
2924
- return spawnRg(resolvedRgPath, args, opts);
2925
- }
2926
- if (!rgPathChecked) {
2927
- const result = await spawnRg(import_ripgrep.rgPath, args, opts);
2928
- if (!isBinaryFailure(result)) {
2929
- resolvedRgPath = import_ripgrep.rgPath;
2930
- rgPathChecked = true;
2931
- return result;
2932
- }
2933
- const fallbackResult = await spawnRg("rg", args, opts);
2934
- if (!isBinaryFailure(fallbackResult)) {
2935
- resolvedRgPath = "rg";
2936
- rgPathChecked = true;
2937
- return fallbackResult;
2938
- }
2939
- rgPathChecked = true;
2940
- return {
2941
- stdout: "",
2942
- stderr: "Failed to spawn ripgrep. Neither bundled nor system rg is available.",
2943
- exitCode: -1
2944
- };
2945
- }
2946
- return {
2947
- stdout: "",
2948
- stderr: "Failed to spawn ripgrep. Neither bundled nor system rg is available.",
2949
- exitCode: -1
2950
- };
2951
- }
2952
-
2953
- // tools/warp_grep/utils/paths.ts
2954
- var import_fs = __toESM(require("fs"), 1);
2955
- var import_path4 = __toESM(require("path"), 1);
2956
- function resolveUnderRepo(repoRoot, targetPath) {
2957
- const absRoot = import_path4.default.resolve(repoRoot);
2958
- const resolved = import_path4.default.resolve(absRoot, targetPath);
2959
- ensureWithinRepo(absRoot, resolved);
2960
- return resolved;
2961
- }
2962
- function ensureWithinRepo(repoRoot, absTarget) {
2963
- const rel = import_path4.default.relative(import_path4.default.resolve(repoRoot), import_path4.default.resolve(absTarget));
2964
- if (rel.startsWith("..") || import_path4.default.isAbsolute(rel)) {
2965
- throw new Error(`Path outside repository root: ${absTarget}`);
2966
- }
2967
- }
2968
- function toRepoRelative(repoRoot, absPath) {
2969
- return import_path4.default.relative(import_path4.default.resolve(repoRoot), import_path4.default.resolve(absPath));
2970
- }
2971
- function isSymlink(p) {
2972
- try {
2973
- const st = import_fs.default.lstatSync(p);
2974
- return st.isSymbolicLink();
2975
- } catch {
2976
- return false;
2977
- }
2978
- }
2979
- function fixPathRepetition(fullPath) {
2980
- const segments = fullPath.split(import_path4.default.sep).filter(Boolean);
2981
- if (segments.length < 2) return null;
2982
- for (let len = Math.floor(segments.length / 2); len >= 1; len--) {
2983
- for (let i = 0; i <= segments.length - 2 * len; i++) {
2984
- const first = segments.slice(i, i + len);
2985
- const second = segments.slice(i + len, i + 2 * len);
2986
- if (first.every((seg, idx) => seg === second[idx])) {
2987
- const fixed = [...segments.slice(0, i), ...segments.slice(i + len)];
2988
- return import_path4.default.sep + fixed.join(import_path4.default.sep);
2989
- }
2990
- }
2991
- }
2992
- return null;
2993
- }
2994
- function isTextualFile(filePath, maxBytes = 2e6) {
2995
- try {
2996
- const st = import_fs.default.statSync(filePath);
2997
- if (!st.isFile()) return false;
2998
- if (st.size > maxBytes) return false;
2999
- const fd = import_fs.default.openSync(filePath, "r");
3000
- const buf = Buffer.alloc(512);
3001
- const read = import_fs.default.readSync(fd, buf, 0, buf.length, 0);
3002
- import_fs.default.closeSync(fd);
3003
- for (let i = 0; i < read; i++) {
3004
- const c = buf[i];
3005
- if (c === 0) return false;
3006
- }
3007
- return true;
3008
- } catch {
3009
- return false;
3010
- }
3011
- }
3012
-
3013
- // tools/warp_grep/utils/files.ts
3014
- var import_promises2 = __toESM(require("fs/promises"), 1);
3015
- async function readAllLines(filePath) {
3016
- const content = await import_promises2.default.readFile(filePath, "utf8");
3017
- return content.split(/\r?\n/);
3018
- }
3019
-
3020
- // tools/warp_grep/providers/local.ts
3021
- var SKIP_NAMES = /* @__PURE__ */ new Set([
3022
- // Version control
3023
- ".git",
3024
- ".svn",
3025
- ".hg",
3026
- ".bzr",
3027
- // Dependencies
3028
- "node_modules",
3029
- "bower_components",
3030
- ".pnpm",
3031
- ".yarn",
3032
- "vendor",
3033
- "Pods",
3034
- ".bundle",
3035
- // Python
3036
- "__pycache__",
3037
- ".pytest_cache",
3038
- ".mypy_cache",
3039
- ".ruff_cache",
3040
- ".venv",
3041
- "venv",
3042
- ".tox",
3043
- ".nox",
3044
- ".eggs",
3045
- // Build outputs
3046
- "dist",
3047
- "build",
3048
- "out",
3049
- "output",
3050
- "target",
3051
- "_build",
3052
- ".next",
3053
- ".nuxt",
3054
- ".output",
3055
- ".vercel",
3056
- ".netlify",
3057
- // Cache
3058
- ".cache",
3059
- ".parcel-cache",
3060
- ".turbo",
3061
- ".nx",
3062
- ".gradle",
3063
- // IDE
3064
- ".idea",
3065
- ".vscode",
3066
- ".vs",
3067
- // Coverage
3068
- "coverage",
3069
- ".coverage",
3070
- "htmlcov",
3071
- ".nyc_output",
3072
- // Temp
3073
- "tmp",
3074
- "temp",
3075
- ".tmp",
3076
- ".temp",
3077
- // Lock files
3078
- "package-lock.json",
3079
- "yarn.lock",
3080
- "pnpm-lock.yaml",
3081
- "bun.lockb",
3082
- "Cargo.lock",
3083
- "Gemfile.lock",
3084
- "poetry.lock"
3085
- ]);
3086
- var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
3087
- ".min.js",
3088
- ".min.css",
3089
- ".bundle.js",
3090
- ".wasm",
3091
- ".so",
3092
- ".dll",
3093
- ".pyc",
3094
- ".map",
3095
- ".js.map"
3096
- ]);
3097
- function shouldSkip(name) {
3098
- if (SKIP_NAMES.has(name)) return true;
3099
- if (name.startsWith(".")) return true;
3100
- for (const ext of SKIP_EXTENSIONS) {
3101
- if (name.endsWith(ext)) return true;
3102
- }
3103
- return false;
3104
- }
3105
- var LocalRipgrepProvider = class {
3106
- constructor(repoRoot, excludes = DEFAULT_EXCLUDES) {
3107
- this.repoRoot = repoRoot;
3108
- this.excludes = excludes;
3109
- }
3110
- async grep(params) {
3111
- let abs;
3112
- try {
3113
- abs = resolveUnderRepo(this.repoRoot, params.path);
3114
- } catch (err) {
3115
- return {
3116
- lines: [],
3117
- error: `[PATH ERROR] ${err instanceof Error ? err.message : String(err)}`
3118
- };
3119
- }
3120
- const stat = await import_promises3.default.stat(abs).catch(() => null);
3121
- if (!stat) return { lines: [] };
3122
- const targetArg = abs === import_path5.default.resolve(this.repoRoot) ? "." : toRepoRelative(this.repoRoot, abs);
3123
- const args = [
3124
- "--no-config",
3125
- "--no-heading",
3126
- "--with-filename",
3127
- "--line-number",
3128
- "--color=never",
3129
- "--trim",
3130
- "--max-columns=400",
3131
- "-C",
3132
- "1",
3133
- ...params.glob ? ["--glob", params.glob] : [],
3134
- ...this.excludes.flatMap((e) => ["-g", `!${e}`]),
3135
- params.pattern,
3136
- targetArg || "."
3137
- ];
3138
- const res = await runRipgrep(args, { cwd: this.repoRoot });
3139
- if (res.exitCode === -1) {
3140
- return {
3141
- lines: [],
3142
- error: `[RIPGREP NOT AVAILABLE] ripgrep (rg) is required but failed to execute. Please install it:
3143
- \u2022 macOS: brew install ripgrep
3144
- \u2022 Ubuntu/Debian: apt install ripgrep
3145
- \u2022 Windows: choco install ripgrep
3146
- \u2022 Or visit: https://github.com/BurntSushi/ripgrep#installation
3147
- Exit code: ${res.exitCode}${res.stderr ? `
3148
- Details: ${res.stderr}` : ""}`
3149
- };
3148
+ )
3149
+ );
3150
3150
  }
3151
- if (res.exitCode !== 0 && res.exitCode !== 1) {
3152
- return {
3153
- lines: [],
3154
- error: `[RIPGREP ERROR] grep failed with exit code ${res.exitCode}${res.stderr ? `: ${res.stderr}` : ""}`
3155
- };
3151
+ const toolExecStart = Date.now();
3152
+ const allResults = await Promise.all(allPromises);
3153
+ turnMetrics.local_tools_ms = Date.now() - toolExecStart;
3154
+ for (const result of allResults) {
3155
+ formatted.push(result);
3156
3156
  }
3157
- const lines = (res.stdout || "").trim().split(/\r?\n/).filter((l) => l.length > 0);
3158
- if (lines.length > AGENT_CONFIG.MAX_OUTPUT_LINES) {
3159
- return {
3160
- lines: [],
3161
- error: "query not specific enough, tool tried to return too much context and failed"
3162
- };
3157
+ if (formatted.length > 0) {
3158
+ const turnMessage = formatTurnMessage(turn, maxTurns);
3159
+ const contextBudget = calculateContextBudget(messages);
3160
+ messages.push({ role: "user", content: formatted.join("\n") + turnMessage + "\n" + contextBudget });
3163
3161
  }
3164
- return { lines };
3165
- }
3166
- async read(params) {
3167
- let abs;
3168
- try {
3169
- abs = resolveUnderRepo(this.repoRoot, params.path);
3170
- } catch (err) {
3171
- return {
3172
- lines: [],
3173
- error: `[PATH ERROR] ${err instanceof Error ? err.message : String(err)}`
3174
- };
3162
+ timings.turns.push(turnMetrics);
3163
+ if (finishCalls.length) {
3164
+ const fc = finishCalls[0];
3165
+ const files = fc.arguments?.files ?? [];
3166
+ finishMeta = { files };
3167
+ terminationReason = "completed";
3168
+ break;
3175
3169
  }
3176
- let stat = await import_promises3.default.stat(abs).catch(() => null);
3177
- if (!stat || !stat.isFile()) {
3178
- const fixedPath = fixPathRepetition(abs);
3179
- if (fixedPath) {
3180
- const fixedStat = await import_promises3.default.stat(fixedPath).catch(() => null);
3181
- if (fixedStat?.isFile()) {
3182
- abs = fixedPath;
3183
- stat = fixedStat;
3184
- }
3170
+ }
3171
+ if (terminationReason !== "completed" || !finishMeta) {
3172
+ timings.total_ms = Date.now() - totalStart;
3173
+ return { terminationReason, messages, errors, timings };
3174
+ }
3175
+ const parts = ["Relevant context found:"];
3176
+ for (const f of finishMeta.files) {
3177
+ const ranges = f.lines === "*" ? "*" : Array.isArray(f.lines) ? f.lines.map(([s, e]) => `${s}-${e}`).join(", ") : "*";
3178
+ parts.push(`- ${f.path}: ${ranges}`);
3179
+ }
3180
+ const payload = parts.join("\n");
3181
+ const finishResolutionStart = Date.now();
3182
+ const fileReadErrors = [];
3183
+ const resolved = await readFinishFiles(
3184
+ repoRoot,
3185
+ finishMeta.files,
3186
+ async (p, s, e) => {
3187
+ try {
3188
+ const rr = await provider.read({ path: p, start: s, end: e });
3189
+ return rr.lines.map((l) => {
3190
+ const idx = l.indexOf("|");
3191
+ return idx >= 0 ? l.slice(idx + 1) : l;
3192
+ });
3193
+ } catch (err) {
3194
+ const errorMsg = err instanceof Error ? err.message : String(err);
3195
+ fileReadErrors.push({ path: p, error: errorMsg });
3196
+ console.error(`[warp_grep] Failed to read file: ${p} - ${errorMsg}`);
3197
+ return [`[couldn't find: ${p}]`];
3185
3198
  }
3186
3199
  }
3187
- if (!stat || !stat.isFile()) {
3188
- return {
3189
- lines: [],
3190
- error: `[FILE NOT FOUND] You tried to read "${params.path}" but there is no file at this path. Double-check the path exists and is spelled correctly.`
3191
- };
3200
+ );
3201
+ timings.finish_resolution_ms = Date.now() - finishResolutionStart;
3202
+ if (fileReadErrors.length > 0) {
3203
+ errors.push(...fileReadErrors.map((e) => ({ message: `File read error: ${e.path} - ${e.error}` })));
3204
+ }
3205
+ timings.total_ms = Date.now() - totalStart;
3206
+ return {
3207
+ terminationReason: "completed",
3208
+ messages,
3209
+ finish: { payload, metadata: finishMeta, resolved },
3210
+ timings
3211
+ };
3212
+ }
3213
+ async function* runWarpGrepStreaming(config) {
3214
+ const totalStart = Date.now();
3215
+ const timeoutMs = config.timeout ?? AGENT_CONFIG.TIMEOUT_MS;
3216
+ const timings = { turns: [], timeout_ms: timeoutMs };
3217
+ const repoRoot = import_path3.default.resolve(config.repoRoot || process.cwd());
3218
+ const messages = [];
3219
+ messages.push({ role: "system", content: getSystemPrompt() });
3220
+ const initialStateStart = Date.now();
3221
+ const initialState = await buildInitialState(repoRoot, config.query, config.provider);
3222
+ timings.initial_state_ms = Date.now() - initialStateStart;
3223
+ messages.push({ role: "user", content: initialState });
3224
+ const maxTurns = AGENT_CONFIG.MAX_TURNS;
3225
+ const model = config.model || DEFAULT_MODEL;
3226
+ const provider = config.provider;
3227
+ const errors = [];
3228
+ let finishMeta;
3229
+ let terminationReason = "terminated";
3230
+ for (let turn = 1; turn <= maxTurns; turn += 1) {
3231
+ const turnMetrics = { turn, morph_api_ms: 0, local_tools_ms: 0 };
3232
+ enforceContextLimit(messages);
3233
+ const modelCallStart = Date.now();
3234
+ const assistantContent = await callModel(messages, model, {
3235
+ morphApiKey: config.morphApiKey,
3236
+ morphApiUrl: config.morphApiUrl,
3237
+ retryConfig: config.retryConfig,
3238
+ timeout: timeoutMs
3239
+ }).catch((e) => {
3240
+ errors.push({ message: e instanceof Error ? e.message : String(e) });
3241
+ return "";
3242
+ });
3243
+ turnMetrics.morph_api_ms = Date.now() - modelCallStart;
3244
+ if (!assistantContent) {
3245
+ timings.turns.push(turnMetrics);
3246
+ break;
3192
3247
  }
3193
- if (isSymlink(abs)) {
3194
- return {
3195
- lines: [],
3196
- error: `[SYMLINK] You tried to read "${params.path}" but this is a symlink. Try reading the actual file it points to instead.`
3197
- };
3248
+ messages.push({ role: "assistant", content: assistantContent });
3249
+ const toolCalls = parser.parse(assistantContent);
3250
+ if (toolCalls.length === 0) {
3251
+ errors.push({ message: "No tool calls produced by the model. Your MCP is likely out of date! Update it by running: rm -rf ~/.npm/_npx && npm cache clean --force && npx -y @morphllm/morphmcp@latest" });
3252
+ terminationReason = "terminated";
3253
+ timings.turns.push(turnMetrics);
3254
+ break;
3198
3255
  }
3199
- if (!isTextualFile(abs)) {
3200
- return {
3201
- lines: [],
3202
- error: `[UNREADABLE FILE] You tried to read "${params.path}" but this file is either too large or not a text file, so it cannot be read. Try a different file.`
3203
- };
3256
+ yield {
3257
+ turn,
3258
+ toolCalls: toolCalls.map((c) => ({
3259
+ name: c.name,
3260
+ arguments: c.arguments ?? {}
3261
+ }))
3262
+ };
3263
+ const finishCalls = toolCalls.filter((c) => c.name === "finish");
3264
+ const grepCalls = toolCalls.filter((c) => c.name === "grep");
3265
+ const listDirCalls = toolCalls.filter((c) => c.name === "list_directory");
3266
+ const readCalls = toolCalls.filter((c) => c.name === "read");
3267
+ const skipCalls = toolCalls.filter((c) => c.name === "_skip");
3268
+ const formatted = [];
3269
+ for (const c of skipCalls) {
3270
+ const msg = c.arguments?.message || "Command skipped due to parsing error";
3271
+ formatted.push(msg);
3204
3272
  }
3205
- let lines;
3206
- try {
3207
- lines = await readAllLines(abs);
3208
- } catch (err) {
3209
- return {
3210
- lines: [],
3211
- error: `[READ ERROR] Failed to read "${params.path}": ${err instanceof Error ? err.message : String(err)}`
3212
- };
3273
+ const allPromises = [];
3274
+ for (const c of grepCalls) {
3275
+ const args = c.arguments ?? {};
3276
+ allPromises.push(
3277
+ toolGrep(provider, args).then(
3278
+ ({ output }) => formatAgentToolOutput("grep", args, output, { isError: false }),
3279
+ (err) => formatAgentToolOutput("grep", args, String(err), { isError: true })
3280
+ )
3281
+ );
3213
3282
  }
3214
- const total = lines.length;
3215
- const rawStart = params.start;
3216
- const rawEnd = params.end;
3217
- let s = 1;
3218
- let e = total;
3219
- const startValid = rawStart === void 0 || Number.isFinite(rawStart) && rawStart > 0;
3220
- const endValid = rawEnd === void 0 || Number.isFinite(rawEnd) && rawEnd > 0;
3221
- if (startValid && endValid) {
3222
- s = rawStart ?? 1;
3223
- e = Math.min(rawEnd ?? total, total);
3224
- if (s > total && total > 0 || s > e) {
3225
- s = 1;
3226
- e = total;
3227
- }
3283
+ for (const c of listDirCalls) {
3284
+ const args = c.arguments ?? {};
3285
+ allPromises.push(
3286
+ toolListDirectory(provider, args).then(
3287
+ (p) => formatAgentToolOutput("list_directory", args, p, { isError: false }),
3288
+ (err) => formatAgentToolOutput("list_directory", args, String(err), { isError: true })
3289
+ )
3290
+ );
3228
3291
  }
3229
- const out = [];
3230
- for (let i = s; i <= e; i += 1) {
3231
- const content = lines[i - 1] ?? "";
3232
- out.push(`${i}|${content}`);
3292
+ for (const c of readCalls) {
3293
+ const args = c.arguments ?? {};
3294
+ allPromises.push(
3295
+ toolRead(provider, args).then(
3296
+ (p) => formatAgentToolOutput("read", args, p, { isError: false }),
3297
+ (err) => formatAgentToolOutput("read", args, String(err), { isError: true })
3298
+ )
3299
+ );
3233
3300
  }
3234
- if (out.length > AGENT_CONFIG.MAX_READ_LINES) {
3235
- const truncated = out.slice(0, AGENT_CONFIG.MAX_READ_LINES);
3236
- truncated.push(`... [truncated: showing ${AGENT_CONFIG.MAX_READ_LINES} of ${out.length} lines]`);
3237
- return { lines: truncated };
3301
+ const toolExecStart = Date.now();
3302
+ const allResults = await Promise.all(allPromises);
3303
+ turnMetrics.local_tools_ms = Date.now() - toolExecStart;
3304
+ for (const result of allResults) {
3305
+ formatted.push(result);
3238
3306
  }
3239
- return { lines: out };
3240
- }
3241
- async listDirectory(params) {
3242
- let abs;
3243
- try {
3244
- abs = resolveUnderRepo(this.repoRoot, params.path);
3245
- } catch {
3246
- return [];
3307
+ if (formatted.length > 0) {
3308
+ const turnMessage = formatTurnMessage(turn, maxTurns);
3309
+ const contextBudget = calculateContextBudget(messages);
3310
+ messages.push({ role: "user", content: formatted.join("\n") + turnMessage + "\n" + contextBudget });
3247
3311
  }
3248
- const stat = await import_promises3.default.stat(abs).catch(() => null);
3249
- if (!stat || !stat.isDirectory()) {
3250
- return [];
3312
+ timings.turns.push(turnMetrics);
3313
+ if (finishCalls.length) {
3314
+ const fc = finishCalls[0];
3315
+ const files = fc.arguments?.files ?? [];
3316
+ finishMeta = { files };
3317
+ terminationReason = "completed";
3318
+ break;
3251
3319
  }
3252
- const maxResults = params.maxResults ?? AGENT_CONFIG.MAX_OUTPUT_LINES;
3253
- const maxDepth = params.maxDepth ?? AGENT_CONFIG.MAX_LIST_DEPTH;
3254
- const regex = params.pattern ? new RegExp(params.pattern) : null;
3255
- const repoRoot = this.repoRoot;
3256
- const results = [];
3257
- let timedOut = false;
3258
- const startTime = Date.now();
3259
- async function walk(dir, depth) {
3260
- if (Date.now() - startTime > AGENT_CONFIG.LIST_TIMEOUT_MS) {
3261
- timedOut = true;
3262
- return;
3263
- }
3264
- if (depth > maxDepth || results.length >= maxResults) return;
3265
- const entries = await import_promises3.default.readdir(dir, { withFileTypes: true });
3266
- for (const entry of entries) {
3267
- if (timedOut || results.length >= maxResults) break;
3268
- if (shouldSkip(entry.name)) continue;
3269
- if (regex && !regex.test(entry.name)) continue;
3270
- const full = import_path5.default.join(dir, entry.name);
3271
- const isDir = entry.isDirectory();
3272
- results.push({
3273
- name: entry.name,
3274
- path: toRepoRelative(repoRoot, full),
3275
- type: isDir ? "dir" : "file",
3276
- depth
3320
+ }
3321
+ if (terminationReason !== "completed" || !finishMeta) {
3322
+ timings.total_ms = Date.now() - totalStart;
3323
+ return { terminationReason, messages, errors, timings };
3324
+ }
3325
+ const parts = ["Relevant context found:"];
3326
+ for (const f of finishMeta.files) {
3327
+ const ranges = f.lines === "*" ? "*" : Array.isArray(f.lines) ? f.lines.map(([s, e]) => `${s}-${e}`).join(", ") : "*";
3328
+ parts.push(`- ${f.path}: ${ranges}`);
3329
+ }
3330
+ const payload = parts.join("\n");
3331
+ const finishResolutionStart = Date.now();
3332
+ const fileReadErrors = [];
3333
+ const resolved = await readFinishFiles(
3334
+ repoRoot,
3335
+ finishMeta.files,
3336
+ async (p, s, e) => {
3337
+ try {
3338
+ const rr = await provider.read({ path: p, start: s, end: e });
3339
+ return rr.lines.map((l) => {
3340
+ const idx = l.indexOf("|");
3341
+ return idx >= 0 ? l.slice(idx + 1) : l;
3277
3342
  });
3278
- if (isDir) {
3279
- await walk(full, depth + 1);
3280
- }
3343
+ } catch (err) {
3344
+ const errorMsg = err instanceof Error ? err.message : String(err);
3345
+ fileReadErrors.push({ path: p, error: errorMsg });
3346
+ console.error(`[warp_grep] Failed to read file: ${p} - ${errorMsg}`);
3347
+ return [`[couldn't find: ${p}]`];
3281
3348
  }
3282
3349
  }
3283
- await walk(abs, 0);
3284
- return results;
3350
+ );
3351
+ timings.finish_resolution_ms = Date.now() - finishResolutionStart;
3352
+ if (fileReadErrors.length > 0) {
3353
+ errors.push(...fileReadErrors.map((e) => ({ message: `File read error: ${e.path} - ${e.error}` })));
3285
3354
  }
3286
- };
3355
+ timings.total_ms = Date.now() - totalStart;
3356
+ return {
3357
+ terminationReason: "completed",
3358
+ messages,
3359
+ finish: { payload, metadata: finishMeta, resolved },
3360
+ timings
3361
+ };
3362
+ }
3287
3363
 
3288
3364
  // tools/warp_grep/providers/remote.ts
3289
- var SKIP_NAMES2 = /* @__PURE__ */ new Set([
3365
+ init_config();
3366
+ var SKIP_NAMES = /* @__PURE__ */ new Set([
3290
3367
  // Version control
3291
3368
  ".git",
3292
3369
  ".svn",
@@ -3351,7 +3428,7 @@ var SKIP_NAMES2 = /* @__PURE__ */ new Set([
3351
3428
  "Gemfile.lock",
3352
3429
  "poetry.lock"
3353
3430
  ]);
3354
- var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([
3431
+ var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
3355
3432
  ".min.js",
3356
3433
  ".min.css",
3357
3434
  ".bundle.js",
@@ -3362,10 +3439,10 @@ var SKIP_EXTENSIONS2 = /* @__PURE__ */ new Set([
3362
3439
  ".map",
3363
3440
  ".js.map"
3364
3441
  ]);
3365
- function shouldSkip2(name) {
3366
- if (SKIP_NAMES2.has(name)) return true;
3442
+ function shouldSkip(name) {
3443
+ if (SKIP_NAMES.has(name)) return true;
3367
3444
  if (name.startsWith(".")) return true;
3368
- for (const ext of SKIP_EXTENSIONS2) {
3445
+ for (const ext of SKIP_EXTENSIONS) {
3369
3446
  if (name.endsWith(ext)) return true;
3370
3447
  }
3371
3448
  return false;
@@ -3441,7 +3518,7 @@ var RemoteCommandsProvider = class {
3441
3518
  for (const fullPath of paths) {
3442
3519
  if (fullPath === params.path || fullPath === this.repoRoot) continue;
3443
3520
  const name = fullPath.split("/").pop() || "";
3444
- if (shouldSkip2(name)) continue;
3521
+ if (shouldSkip(name)) continue;
3445
3522
  if (regex && !regex.test(name)) continue;
3446
3523
  let relativePath = fullPath;
3447
3524
  if (fullPath.startsWith(this.repoRoot)) {
@@ -3466,6 +3543,10 @@ var RemoteCommandsProvider = class {
3466
3543
  };
3467
3544
 
3468
3545
  // tools/warp_grep/client.ts
3546
+ async function getLocalProvider(repoRoot, excludes) {
3547
+ const { LocalRipgrepProvider: LocalRipgrepProvider2 } = await Promise.resolve().then(() => (init_local(), local_exports));
3548
+ return new LocalRipgrepProvider2(repoRoot, excludes);
3549
+ }
3469
3550
  var WarpGrepClient = class {
3470
3551
  config;
3471
3552
  constructor(config = {}) {
@@ -3498,7 +3579,7 @@ var WarpGrepClient = class {
3498
3579
  };
3499
3580
  async function executeToolCall(input, config) {
3500
3581
  const parsed = typeof input === "string" ? JSON.parse(input) : input;
3501
- const provider = config.remoteCommands ? new RemoteCommandsProvider(config.repoRoot, config.remoteCommands) : config.provider ?? new LocalRipgrepProvider(config.repoRoot, config.excludes);
3582
+ const provider = config.remoteCommands ? new RemoteCommandsProvider(config.repoRoot, config.remoteCommands) : config.provider ?? await getLocalProvider(config.repoRoot, config.excludes);
3502
3583
  const result = await runWarpGrep({
3503
3584
  query: parsed.query,
3504
3585
  repoRoot: config.repoRoot,
@@ -3534,7 +3615,7 @@ function processAgentResult(result) {
3534
3615
  }
3535
3616
  async function* executeToolCallStreaming(input, config) {
3536
3617
  const parsed = typeof input === "string" ? JSON.parse(input) : input;
3537
- const provider = config.remoteCommands ? new RemoteCommandsProvider(config.repoRoot, config.remoteCommands) : config.provider ?? new LocalRipgrepProvider(config.repoRoot, config.excludes);
3618
+ const provider = config.remoteCommands ? new RemoteCommandsProvider(config.repoRoot, config.remoteCommands) : config.provider ?? await getLocalProvider(config.repoRoot, config.excludes);
3538
3619
  const generator = runWarpGrepStreaming({
3539
3620
  query: parsed.query,
3540
3621
  repoRoot: config.repoRoot,
@@ -4126,6 +4207,7 @@ var import_isomorphic_git2 = __toESM(require("isomorphic-git"), 1);
4126
4207
  var import_node2 = __toESM(require("isomorphic-git/http/node"), 1);
4127
4208
 
4128
4209
  // modelrouter/core.ts
4210
+ init_resilience();
4129
4211
  var DEFAULT_CONFIG3 = {
4130
4212
  apiUrl: "https://api.morphllm.com",
4131
4213
  timeout: 5e3,
@@ -4150,7 +4232,7 @@ var BaseRouter = class {
4150
4232
  */
4151
4233
  async selectModel(input) {
4152
4234
  const mode = input.mode || "balanced";
4153
- const apiKey = this.config.apiKey || process.env.MORPH_API_KEY;
4235
+ const apiKey = this.config.apiKey || (typeof process !== "undefined" ? process.env?.MORPH_API_KEY : void 0);
4154
4236
  if (!apiKey) {
4155
4237
  throw new Error(
4156
4238
  "Morph API key is required. Set MORPH_API_KEY environment variable or pass apiKey in config."
@@ -4261,7 +4343,7 @@ var RawRouter = class extends BaseRouter {
4261
4343
  */
4262
4344
  async classify(input) {
4263
4345
  const mode = input.mode || "balanced";
4264
- const apiKey = this.config.apiKey || process.env.MORPH_API_KEY;
4346
+ const apiKey = this.config.apiKey || (typeof process !== "undefined" ? process.env?.MORPH_API_KEY : void 0);
4265
4347
  if (!apiKey) {
4266
4348
  throw new Error(
4267
4349
  "Morph API key is required. Set MORPH_API_KEY environment variable or pass apiKey in config."
@@ -5060,6 +5142,17 @@ var MorphClient = class {
5060
5142
 
5061
5143
  // tools/warp_grep/index.ts
5062
5144
  var import_zod4 = require("zod");
5145
+
5146
+ // tools/warp_grep/providers/index.ts
5147
+ init_local();
5148
+
5149
+ // tools/warp_grep/index.ts
5150
+ init_paths();
5151
+
5152
+ // tools/warp_grep/agent/index.ts
5153
+ init_config();
5154
+
5155
+ // tools/warp_grep/index.ts
5063
5156
  var warpGrepInputSchema = import_zod4.z.object({
5064
5157
  query: import_zod4.z.string().describe("Free-form repository question")
5065
5158
  });