lobster-cli 0.1.0

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 (45) hide show
  1. package/README.md +389 -0
  2. package/dist/agent/core.js +1013 -0
  3. package/dist/agent/core.js.map +1 -0
  4. package/dist/agent/index.js +1027 -0
  5. package/dist/agent/index.js.map +1 -0
  6. package/dist/brain/index.js +60 -0
  7. package/dist/brain/index.js.map +1 -0
  8. package/dist/browser/dom/index.js +1096 -0
  9. package/dist/browser/dom/index.js.map +1 -0
  10. package/dist/browser/index.js +2034 -0
  11. package/dist/browser/index.js.map +1 -0
  12. package/dist/browser/manager.js +86 -0
  13. package/dist/browser/manager.js.map +1 -0
  14. package/dist/browser/page-adapter.js +1345 -0
  15. package/dist/browser/page-adapter.js.map +1 -0
  16. package/dist/cascade/index.js +138 -0
  17. package/dist/cascade/index.js.map +1 -0
  18. package/dist/config/index.js +110 -0
  19. package/dist/config/index.js.map +1 -0
  20. package/dist/config/schema.js +66 -0
  21. package/dist/config/schema.js.map +1 -0
  22. package/dist/discover/index.js +545 -0
  23. package/dist/discover/index.js.map +1 -0
  24. package/dist/index.js +5529 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib.js +4206 -0
  27. package/dist/lib.js.map +1 -0
  28. package/dist/llm/client.js +379 -0
  29. package/dist/llm/client.js.map +1 -0
  30. package/dist/llm/index.js +397 -0
  31. package/dist/llm/index.js.map +1 -0
  32. package/dist/llm/openai-client.js +214 -0
  33. package/dist/llm/openai-client.js.map +1 -0
  34. package/dist/output/index.js +93 -0
  35. package/dist/output/index.js.map +1 -0
  36. package/dist/pipeline/index.js +802 -0
  37. package/dist/pipeline/index.js.map +1 -0
  38. package/dist/router/decision.js +80 -0
  39. package/dist/router/decision.js.map +1 -0
  40. package/dist/router/index.js +3443 -0
  41. package/dist/router/index.js.map +1 -0
  42. package/dist/types/index.js +23 -0
  43. package/dist/types/index.js.map +1 -0
  44. package/logo.svg +11 -0
  45. package/package.json +65 -0
@@ -0,0 +1,1013 @@
1
+ // src/agent/core.ts
2
+ import { readFileSync } from "fs";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ // src/llm/errors.ts
7
+ var InvokeError = class extends Error {
8
+ type;
9
+ retryable;
10
+ rawError;
11
+ rawResponse;
12
+ constructor(type, message, opts) {
13
+ super(message);
14
+ this.name = "InvokeError";
15
+ this.type = type;
16
+ this.retryable = opts?.retryable ?? isRetryable(type);
17
+ this.rawError = opts?.rawError;
18
+ this.rawResponse = opts?.rawResponse;
19
+ }
20
+ };
21
+ function isRetryable(type) {
22
+ switch (type) {
23
+ case "NETWORK_ERROR" /* NETWORK_ERROR */:
24
+ case "RATE_LIMIT" /* RATE_LIMIT */:
25
+ case "SERVER_ERROR" /* SERVER_ERROR */:
26
+ case "NO_TOOL_CALL" /* NO_TOOL_CALL */:
27
+ case "INVALID_TOOL_ARGS" /* INVALID_TOOL_ARGS */:
28
+ case "TOOL_EXECUTION_ERROR" /* TOOL_EXECUTION_ERROR */:
29
+ case "UNKNOWN" /* UNKNOWN */:
30
+ return true;
31
+ case "AUTH_ERROR" /* AUTH_ERROR */:
32
+ case "CONTEXT_LENGTH" /* CONTEXT_LENGTH */:
33
+ case "CONTENT_FILTER" /* CONTENT_FILTER */:
34
+ return false;
35
+ }
36
+ }
37
+
38
+ // src/llm/openai-client.ts
39
+ var OpenAIClient = class {
40
+ config;
41
+ constructor(config) {
42
+ this.config = config;
43
+ }
44
+ /**
45
+ * Build auth headers based on the provider.
46
+ * - OpenAI/Gemini/Ollama: Bearer token
47
+ * - Anthropic: x-api-key header + anthropic-version
48
+ */
49
+ buildHeaders() {
50
+ const headers = {
51
+ "Content-Type": "application/json"
52
+ };
53
+ if (!this.config.apiKey) return headers;
54
+ if (this.config.provider === "anthropic") {
55
+ headers["x-api-key"] = this.config.apiKey;
56
+ headers["anthropic-version"] = "2023-06-01";
57
+ } else {
58
+ headers["Authorization"] = `Bearer ${this.config.apiKey}`;
59
+ }
60
+ return headers;
61
+ }
62
+ /**
63
+ * Build the request body based on provider.
64
+ * Anthropic Messages API is different from OpenAI chat completions.
65
+ */
66
+ buildBody(messages, tools, opts) {
67
+ if (this.config.provider === "anthropic") {
68
+ return this.buildAnthropicBody(messages, tools, opts);
69
+ }
70
+ const body = {
71
+ model: this.config.model,
72
+ messages,
73
+ temperature: this.config.temperature ?? 0.1
74
+ };
75
+ if (tools && tools.length > 0) {
76
+ body.tools = tools;
77
+ body.parallel_tool_calls = false;
78
+ if (opts?.toolChoice) {
79
+ body.tool_choice = typeof opts.toolChoice === "string" ? opts.toolChoice : opts.toolChoice;
80
+ }
81
+ }
82
+ return { url: `${this.config.baseURL}/chat/completions`, body };
83
+ }
84
+ /**
85
+ * Build Anthropic Messages API request.
86
+ * Converts OpenAI-style messages/tools to Anthropic format.
87
+ */
88
+ buildAnthropicBody(messages, tools, opts) {
89
+ let system;
90
+ const anthropicMessages = [];
91
+ for (const msg of messages) {
92
+ if (msg.role === "system") {
93
+ system = msg.content;
94
+ } else {
95
+ anthropicMessages.push({
96
+ role: msg.role === "assistant" ? "assistant" : "user",
97
+ content: msg.content
98
+ });
99
+ }
100
+ }
101
+ const body = {
102
+ model: this.config.model,
103
+ messages: anthropicMessages,
104
+ max_tokens: 4096,
105
+ temperature: this.config.temperature ?? 0.1
106
+ };
107
+ if (system) body.system = system;
108
+ if (tools && tools.length > 0) {
109
+ body.tools = tools.map((t) => {
110
+ const fn = t.function;
111
+ return {
112
+ name: fn.name,
113
+ description: fn.description,
114
+ input_schema: fn.parameters
115
+ };
116
+ });
117
+ if (opts?.toolChoice) {
118
+ if (typeof opts.toolChoice === "string") {
119
+ body.tool_choice = opts.toolChoice === "required" ? { type: "any" } : { type: opts.toolChoice };
120
+ } else {
121
+ body.tool_choice = { type: "tool", name: opts.toolChoice.function.name };
122
+ }
123
+ }
124
+ }
125
+ return { url: `${this.config.baseURL}/messages`, body };
126
+ }
127
+ /**
128
+ * Parse Anthropic response into our unified format.
129
+ */
130
+ parseAnthropicResponse(json) {
131
+ const content = json.content;
132
+ if (!content || !Array.isArray(content)) {
133
+ throw new InvokeError("UNKNOWN" /* UNKNOWN */, "No content in Anthropic response", { rawResponse: json });
134
+ }
135
+ let textContent;
136
+ const toolCalls = [];
137
+ for (const block of content) {
138
+ if (block.type === "text") {
139
+ textContent = block.text;
140
+ } else if (block.type === "tool_use") {
141
+ toolCalls.push({
142
+ id: block.id,
143
+ type: "function",
144
+ function: {
145
+ name: block.name,
146
+ arguments: JSON.stringify(block.input)
147
+ }
148
+ });
149
+ }
150
+ }
151
+ const usage = json.usage;
152
+ return {
153
+ toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
154
+ content: textContent,
155
+ usage: usage ? {
156
+ promptTokens: usage.input_tokens ?? 0,
157
+ completionTokens: usage.output_tokens ?? 0,
158
+ totalTokens: (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0)
159
+ } : void 0
160
+ };
161
+ }
162
+ async chatCompletion(messages, tools, opts) {
163
+ const { url, body } = this.buildBody(messages, tools, opts);
164
+ const headers = this.buildHeaders();
165
+ let response;
166
+ try {
167
+ response = await fetch(url, {
168
+ method: "POST",
169
+ headers,
170
+ body: JSON.stringify(body)
171
+ });
172
+ } catch (err) {
173
+ throw new InvokeError("NETWORK_ERROR" /* NETWORK_ERROR */, `Network error: ${err}`, { rawError: err });
174
+ }
175
+ if (!response.ok) {
176
+ const text = await response.text().catch(() => "");
177
+ if (response.status === 401) {
178
+ throw new InvokeError("AUTH_ERROR" /* AUTH_ERROR */, `Authentication failed: ${text}`, { retryable: false, rawResponse: text });
179
+ }
180
+ if (response.status === 429) {
181
+ throw new InvokeError("RATE_LIMIT" /* RATE_LIMIT */, `Rate limited: ${text}`, { rawResponse: text });
182
+ }
183
+ if (response.status >= 500) {
184
+ throw new InvokeError("SERVER_ERROR" /* SERVER_ERROR */, `Server error ${response.status}: ${text}`, { rawResponse: text });
185
+ }
186
+ throw new InvokeError("UNKNOWN" /* UNKNOWN */, `HTTP ${response.status}: ${text}`, { rawResponse: text });
187
+ }
188
+ const json = await response.json();
189
+ if (this.config.provider === "anthropic") {
190
+ return this.parseAnthropicResponse(json);
191
+ }
192
+ const choice = json.choices?.[0];
193
+ if (!choice) {
194
+ throw new InvokeError("UNKNOWN" /* UNKNOWN */, "No choices in response", { rawResponse: json });
195
+ }
196
+ const message = choice.message;
197
+ const finishReason = choice.finish_reason;
198
+ if (finishReason === "content_filter") {
199
+ throw new InvokeError("CONTENT_FILTER" /* CONTENT_FILTER */, "Content filtered", { retryable: false, rawResponse: json });
200
+ }
201
+ if (finishReason === "length") {
202
+ throw new InvokeError("CONTEXT_LENGTH" /* CONTEXT_LENGTH */, "Context length exceeded", { retryable: false, rawResponse: json });
203
+ }
204
+ const usage = json.usage;
205
+ return {
206
+ toolCalls: message.tool_calls,
207
+ content: message.content,
208
+ usage: usage ? {
209
+ promptTokens: usage.prompt_tokens ?? 0,
210
+ completionTokens: usage.completion_tokens ?? 0,
211
+ totalTokens: usage.total_tokens ?? 0
212
+ } : void 0
213
+ };
214
+ }
215
+ };
216
+
217
+ // src/llm/utils.ts
218
+ function zodToJsonSchema(schema) {
219
+ if ("_def" in schema) {
220
+ const def = schema._def;
221
+ const typeName = def.typeName;
222
+ if (typeName === "ZodObject") {
223
+ const shape = def.shape();
224
+ const properties = {};
225
+ const required = [];
226
+ for (const [key, value] of Object.entries(shape)) {
227
+ properties[key] = zodToJsonSchema(value);
228
+ if (!(value._def?.typeName === "ZodOptional")) {
229
+ required.push(key);
230
+ }
231
+ }
232
+ const result = { type: "object", properties };
233
+ if (required.length > 0) result.required = required;
234
+ if (def.description) result.description = def.description;
235
+ return result;
236
+ }
237
+ if (typeName === "ZodString") {
238
+ const result = { type: "string" };
239
+ if (def.description) result.description = def.description;
240
+ return result;
241
+ }
242
+ if (typeName === "ZodNumber") {
243
+ const result = { type: "number" };
244
+ if (def.description) result.description = def.description;
245
+ return result;
246
+ }
247
+ if (typeName === "ZodBoolean") {
248
+ const result = { type: "boolean" };
249
+ if (def.description) result.description = def.description;
250
+ return result;
251
+ }
252
+ if (typeName === "ZodEnum") {
253
+ return { type: "string", enum: def.values, ...def.description ? { description: def.description } : {} };
254
+ }
255
+ if (typeName === "ZodArray") {
256
+ return { type: "array", items: zodToJsonSchema(def.type), ...def.description ? { description: def.description } : {} };
257
+ }
258
+ if (typeName === "ZodOptional") {
259
+ return zodToJsonSchema(def.innerType);
260
+ }
261
+ if (typeName === "ZodDefault") {
262
+ const inner = zodToJsonSchema(def.innerType);
263
+ return { ...inner, default: def.defaultValue() };
264
+ }
265
+ if (typeName === "ZodUnion") {
266
+ return { oneOf: def.options.map((opt) => zodToJsonSchema(opt)) };
267
+ }
268
+ if (typeName === "ZodRecord") {
269
+ return { type: "object", additionalProperties: zodToJsonSchema(def.valueType) };
270
+ }
271
+ if (typeName === "ZodLiteral") {
272
+ return { const: def.value };
273
+ }
274
+ if (typeName === "ZodAny") {
275
+ return {};
276
+ }
277
+ }
278
+ return { type: "string" };
279
+ }
280
+ function zodToOpenAITool(name, description, schema) {
281
+ return {
282
+ type: "function",
283
+ function: {
284
+ name,
285
+ description,
286
+ parameters: zodToJsonSchema(schema)
287
+ }
288
+ };
289
+ }
290
+
291
+ // src/llm/client.ts
292
+ var LLM = class {
293
+ client;
294
+ config;
295
+ constructor(config) {
296
+ this.config = config;
297
+ this.client = new OpenAIClient({
298
+ baseURL: config.baseURL,
299
+ model: config.model,
300
+ apiKey: config.apiKey,
301
+ temperature: config.temperature,
302
+ provider: config.provider
303
+ });
304
+ }
305
+ async invoke(messages, tool, abortSignal) {
306
+ const openaiTool = zodToOpenAITool(tool.name, tool.description, tool.schema);
307
+ return this.withRetry(async () => {
308
+ if (abortSignal?.aborted) throw new Error("Aborted");
309
+ const response = await this.client.chatCompletion(
310
+ messages,
311
+ [openaiTool],
312
+ { toolChoice: { type: "function", function: { name: tool.name } } }
313
+ );
314
+ const toolCall = response.toolCalls?.[0];
315
+ if (!toolCall) {
316
+ if (response.content) {
317
+ const extracted = extractJsonFromString(response.content);
318
+ if (extracted) {
319
+ const args2 = typeof extracted === "string" ? JSON.parse(extracted) : extracted;
320
+ const result2 = await tool.execute(args2);
321
+ return {
322
+ toolCall: { name: tool.name, args: args2 },
323
+ toolResult: result2,
324
+ usage: response.usage
325
+ };
326
+ }
327
+ }
328
+ throw new InvokeError("NO_TOOL_CALL" /* NO_TOOL_CALL */, "No tool call in response");
329
+ }
330
+ let args;
331
+ try {
332
+ args = JSON.parse(toolCall.function.arguments);
333
+ } catch {
334
+ try {
335
+ args = JSON.parse(JSON.parse(toolCall.function.arguments));
336
+ } catch {
337
+ throw new InvokeError("INVALID_TOOL_ARGS" /* INVALID_TOOL_ARGS */, `Invalid JSON in tool args: ${toolCall.function.arguments}`);
338
+ }
339
+ }
340
+ let result;
341
+ try {
342
+ result = await tool.execute(args);
343
+ } catch (err) {
344
+ throw new InvokeError("TOOL_EXECUTION_ERROR" /* TOOL_EXECUTION_ERROR */, `Tool execution failed: ${err}`, { rawError: err });
345
+ }
346
+ return {
347
+ toolCall: { name: tool.name, args },
348
+ toolResult: result,
349
+ usage: response.usage
350
+ };
351
+ });
352
+ }
353
+ async withRetry(fn) {
354
+ const maxRetries = this.config.maxRetries ?? 3;
355
+ let lastError;
356
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
357
+ try {
358
+ return await fn();
359
+ } catch (err) {
360
+ lastError = err;
361
+ if (err instanceof InvokeError && !err.retryable) throw err;
362
+ if (err instanceof Error && err.name === "AbortError") throw err;
363
+ if (attempt < maxRetries) {
364
+ await new Promise((r) => setTimeout(r, 100 * (attempt + 1)));
365
+ }
366
+ }
367
+ }
368
+ throw lastError;
369
+ }
370
+ };
371
+ function extractJsonFromString(str) {
372
+ const start = str.indexOf("{");
373
+ const end = str.lastIndexOf("}");
374
+ if (start === -1 || end === -1 || end <= start) return null;
375
+ try {
376
+ return JSON.parse(str.slice(start, end + 1));
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+
382
+ // src/agent/tools/click.ts
383
+ import { z } from "zod";
384
+ function createClickTool(page) {
385
+ return {
386
+ description: "Click on an interactive element by its index number from the page content.",
387
+ inputSchema: z.object({
388
+ index: z.number().describe("The index of the element to click")
389
+ }),
390
+ execute: async (args) => {
391
+ await page.click(args.index);
392
+ return `Clicked element [${args.index}]`;
393
+ }
394
+ };
395
+ }
396
+
397
+ // src/agent/tools/type.ts
398
+ import { z as z2 } from "zod";
399
+ function createTypeTool(page) {
400
+ return {
401
+ description: "Type text into an input field identified by its index number.",
402
+ inputSchema: z2.object({
403
+ index: z2.number().describe("The index of the input element"),
404
+ text: z2.string().describe("The text to type")
405
+ }),
406
+ execute: async (args) => {
407
+ await page.typeText(args.index, args.text);
408
+ return `Typed "${args.text}" into element [${args.index}]`;
409
+ }
410
+ };
411
+ }
412
+
413
+ // src/agent/tools/scroll.ts
414
+ import { z as z3 } from "zod";
415
+ function createScrollTool(page) {
416
+ return {
417
+ description: "Scroll the page in a given direction. Use to reveal more content.",
418
+ inputSchema: z3.object({
419
+ direction: z3.enum(["up", "down", "left", "right"]).describe("Scroll direction"),
420
+ amount: z3.number().optional().describe("Pixels to scroll (default 500)")
421
+ }),
422
+ execute: async (args) => {
423
+ await page.scroll(args.direction, args.amount);
424
+ return `Scrolled ${args.direction}${args.amount ? ` ${args.amount}px` : ""}`;
425
+ }
426
+ };
427
+ }
428
+
429
+ // src/agent/tools/select.ts
430
+ import { z as z4 } from "zod";
431
+ function createSelectTool(page) {
432
+ return {
433
+ description: "Select an option from a dropdown/select element by its index.",
434
+ inputSchema: z4.object({
435
+ index: z4.number().describe("The index of the select element"),
436
+ value: z4.string().describe("The option text or value to select")
437
+ }),
438
+ execute: async (args) => {
439
+ await page.selectOption(args.index, args.value);
440
+ return `Selected "${args.value}" in element [${args.index}]`;
441
+ }
442
+ };
443
+ }
444
+
445
+ // src/agent/tools/wait.ts
446
+ import { z as z5 } from "zod";
447
+ function createWaitTool() {
448
+ return {
449
+ description: "Wait for a specified number of seconds before continuing.",
450
+ inputSchema: z5.object({
451
+ seconds: z5.number().min(0.1).max(30).describe("Seconds to wait")
452
+ }),
453
+ execute: async (args) => {
454
+ await new Promise((r) => setTimeout(r, args.seconds * 1e3));
455
+ return `Waited ${args.seconds} seconds`;
456
+ }
457
+ };
458
+ }
459
+
460
+ // src/agent/tools/done.ts
461
+ import { z as z6 } from "zod";
462
+ function createDoneTool() {
463
+ return {
464
+ description: "Signal that the task is complete. Call this when you have finished the task or cannot proceed further.",
465
+ inputSchema: z6.object({
466
+ success: z6.boolean().describe("Whether the task was completed successfully"),
467
+ text: z6.string().describe("Summary of the result or explanation of failure")
468
+ }),
469
+ execute: async (args) => {
470
+ return JSON.stringify({ done: true, success: args.success, text: args.text });
471
+ }
472
+ };
473
+ }
474
+
475
+ // src/agent/tools/ask-user.ts
476
+ import { z as z7 } from "zod";
477
+ import { createInterface } from "readline";
478
+ function createAskUserTool() {
479
+ return {
480
+ description: "Ask the user a question when you need clarification or input to proceed.",
481
+ inputSchema: z7.object({
482
+ question: z7.string().describe("The question to ask the user")
483
+ }),
484
+ execute: async (args) => {
485
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
486
+ return new Promise((resolve) => {
487
+ rl.question(`
488
+ \u{1F916} Agent asks: ${args.question}
489
+ > `, (answer) => {
490
+ rl.close();
491
+ resolve(`User answered: ${answer}`);
492
+ });
493
+ });
494
+ }
495
+ };
496
+ }
497
+
498
+ // src/agent/tools/execute-js.ts
499
+ import { z as z8 } from "zod";
500
+ function createExecuteJsTool(page) {
501
+ return {
502
+ description: "Execute JavaScript code on the current page. Returns the result.",
503
+ inputSchema: z8.object({
504
+ code: z8.string().describe("JavaScript code to execute on the page")
505
+ }),
506
+ execute: async (args) => {
507
+ const result = await page.evaluate(args.code);
508
+ return typeof result === "string" ? result : JSON.stringify(result, null, 2);
509
+ }
510
+ };
511
+ }
512
+
513
+ // src/agent/tools/index.ts
514
+ function createDefaultTools(page) {
515
+ return {
516
+ click_element_by_index: createClickTool(page),
517
+ input_text: createTypeTool(page),
518
+ scroll: createScrollTool(page),
519
+ select_dropdown_option: createSelectTool(page),
520
+ wait: createWaitTool(),
521
+ done: createDoneTool(),
522
+ ask_user: createAskUserTool(),
523
+ execute_javascript: createExecuteJsTool(page)
524
+ };
525
+ }
526
+
527
+ // src/agent/macro-tool.ts
528
+ import { z as z9 } from "zod";
529
+
530
+ // src/agent/auto-fixer.ts
531
+ function normalizeResponse(raw, toolName, availableActions, toolSchemas) {
532
+ let result = { ...raw };
533
+ if (result.type === "function" && result.function) {
534
+ const fn = result.function;
535
+ if (typeof fn.arguments === "string") {
536
+ try {
537
+ result = JSON.parse(fn.arguments);
538
+ } catch {
539
+ }
540
+ } else if (typeof fn.arguments === "object") {
541
+ result = fn.arguments;
542
+ }
543
+ }
544
+ for (const [key, value] of Object.entries(result)) {
545
+ if (typeof value === "string") {
546
+ try {
547
+ const parsed = JSON.parse(value);
548
+ if (typeof parsed === "object" && parsed !== null) {
549
+ result[key] = parsed;
550
+ }
551
+ } catch {
552
+ }
553
+ }
554
+ }
555
+ if (!result.action) {
556
+ for (const actionName of availableActions) {
557
+ if (actionName in result) {
558
+ result = {
559
+ ...result,
560
+ action: { [actionName]: result[actionName] }
561
+ };
562
+ delete result[actionName];
563
+ break;
564
+ }
565
+ }
566
+ }
567
+ if (!result.action) {
568
+ result.action = { wait: { seconds: 1 } };
569
+ }
570
+ if (typeof result.action === "string") {
571
+ if (availableActions.includes(result.action)) {
572
+ result.action = { [result.action]: {} };
573
+ } else {
574
+ result.action = { wait: { seconds: 1 } };
575
+ }
576
+ }
577
+ const action = result.action;
578
+ for (const [name, input] of Object.entries(action)) {
579
+ if (typeof input !== "object" || input === null) {
580
+ if (toolSchemas && toolSchemas[name]) {
581
+ const schema = toolSchemas[name].inputSchema;
582
+ const coerced = coercePrimitiveToSchema(input, schema);
583
+ if (coerced !== null) {
584
+ action[name] = coerced;
585
+ continue;
586
+ }
587
+ }
588
+ if (typeof input === "number") {
589
+ action[name] = { index: input };
590
+ } else if (typeof input === "string") {
591
+ action[name] = { text: input };
592
+ } else {
593
+ action[name] = {};
594
+ }
595
+ }
596
+ if (toolSchemas && toolSchemas[name] && typeof action[name] === "object") {
597
+ const schema = toolSchemas[name].inputSchema;
598
+ const validation = schema.safeParse(action[name]);
599
+ if (!validation.success) {
600
+ const fixed = attemptSchemaFix(action[name], schema, validation.error);
601
+ if (fixed) {
602
+ action[name] = fixed;
603
+ }
604
+ }
605
+ }
606
+ }
607
+ return result;
608
+ }
609
+ function coercePrimitiveToSchema(value, schema) {
610
+ try {
611
+ const def = schema._def;
612
+ if (def?.typeName !== "ZodObject") return null;
613
+ const shape = def.shape();
614
+ const keys = Object.keys(shape);
615
+ const requiredKeys = keys.filter((k) => {
616
+ const fieldDef = shape[k]?._def;
617
+ return fieldDef?.typeName !== "ZodOptional";
618
+ });
619
+ if (requiredKeys.length === 1) {
620
+ return { [requiredKeys[0]]: value };
621
+ }
622
+ const indexField = keys.find((k) => /index|idx|num|number/i.test(k));
623
+ if (indexField && typeof value === "number") {
624
+ return { [indexField]: value };
625
+ }
626
+ const textField = keys.find((k) => /text|value|query|code|question|url/i.test(k));
627
+ if (textField && typeof value === "string") {
628
+ return { [textField]: value };
629
+ }
630
+ } catch {
631
+ }
632
+ return null;
633
+ }
634
+ function attemptSchemaFix(input, schema, error) {
635
+ try {
636
+ const def = schema._def;
637
+ if (def?.typeName !== "ZodObject") return null;
638
+ const shape = def.shape();
639
+ const expectedKeys = Object.keys(shape);
640
+ const inputKeys = Object.keys(input);
641
+ const fixed = { ...input };
642
+ for (const issue of error.issues) {
643
+ if (issue.code === "invalid_type" && issue.path.length === 1) {
644
+ const key = String(issue.path[0]);
645
+ const val = input[key];
646
+ if (issue.expected === "number" && typeof val === "string") {
647
+ const num = Number(val);
648
+ if (!isNaN(num)) fixed[key] = num;
649
+ } else if (issue.expected === "string" && typeof val === "number") {
650
+ fixed[key] = String(val);
651
+ } else if (issue.expected === "boolean" && typeof val === "string") {
652
+ fixed[key] = val === "true";
653
+ }
654
+ }
655
+ if (issue.code === "unrecognized_keys") {
656
+ for (const k of issue.keys || []) {
657
+ delete fixed[k];
658
+ }
659
+ }
660
+ }
661
+ const result = schema.safeParse(fixed);
662
+ if (result.success) return fixed;
663
+ } catch {
664
+ }
665
+ return null;
666
+ }
667
+
668
+ // src/agent/macro-tool.ts
669
+ function packMacroTool(tools) {
670
+ const actionSchemas = [];
671
+ const toolNames = [];
672
+ for (const [name, tool] of Object.entries(tools)) {
673
+ toolNames.push(name);
674
+ actionSchemas.push(
675
+ z9.object({ [name]: tool.inputSchema }).describe(tool.description)
676
+ );
677
+ }
678
+ const actionSchema = actionSchemas.length === 1 ? actionSchemas[0] : z9.union(actionSchemas);
679
+ const macroSchema = z9.object({
680
+ evaluation_previous_goal: z9.string().optional().describe("Evaluate whether the previous goal was achieved"),
681
+ memory: z9.string().optional().describe("Important information to remember for future steps"),
682
+ next_goal: z9.string().optional().describe("The next immediate goal to achieve"),
683
+ action: actionSchema.describe("The action to take")
684
+ });
685
+ return {
686
+ name: "AgentOutput",
687
+ description: "The agent's output containing reflection and action. Must be called every step.",
688
+ schema: macroSchema,
689
+ execute: async (args) => {
690
+ const normalized = normalizeResponse(args, "AgentOutput", toolNames, tools);
691
+ const action = normalized.action;
692
+ const [toolName, toolInput] = Object.entries(action)[0];
693
+ const tool = tools[toolName];
694
+ if (!tool) {
695
+ return `Error: Unknown tool "${toolName}". Available: ${toolNames.join(", ")}`;
696
+ }
697
+ try {
698
+ const result = await tool.execute(toolInput);
699
+ return result;
700
+ } catch (err) {
701
+ return `Error executing ${toolName}: ${err}`;
702
+ }
703
+ }
704
+ };
705
+ }
706
+
707
+ // src/browser/dom/flat-tree.ts
708
+ function flatTreeToString(tree) {
709
+ const lines = [];
710
+ function walk(nodeId, depth) {
711
+ const node = tree.map[nodeId];
712
+ if (!node) return;
713
+ const indent = " ".repeat(depth);
714
+ if (node.tagName === "#text") {
715
+ if (node.text) lines.push(`${indent}${node.text}`);
716
+ return;
717
+ }
718
+ const attrs = node.attributes || {};
719
+ const attrStr = Object.entries(attrs).map(([k, v]) => v === "" ? k : `${k}="${v}"`).join(" ");
720
+ const prefix = node.highlightIndex !== void 0 ? `[${node.highlightIndex}]` : "";
721
+ const scrollInfo = node.scrollable ? ` |scroll: ${Math.round(node.scrollable.top)}px up, ${Math.round(node.scrollable.bottom)}px down|` : "";
722
+ const text = node.text || "";
723
+ const tag = node.tagName;
724
+ if (prefix || text || node.children?.length > 0) {
725
+ const opening = `${indent}${prefix}<${tag}${attrStr ? " " + attrStr : ""}${scrollInfo}>`;
726
+ if (!node.children?.length || node.children.length === 0 && text) {
727
+ lines.push(`${opening}${text}</>`);
728
+ } else {
729
+ lines.push(`${opening}${text}`);
730
+ for (const childId of node.children || []) {
731
+ walk(childId, depth + 1);
732
+ }
733
+ }
734
+ } else {
735
+ for (const childId of node.children || []) {
736
+ walk(childId, depth);
737
+ }
738
+ }
739
+ }
740
+ walk(tree.rootId, 0);
741
+ return lines.join("\n");
742
+ }
743
+
744
+ // src/utils/logger.ts
745
+ import chalk from "chalk";
746
+ var log = {
747
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
748
+ success: (msg) => console.log(chalk.green("\u2713"), msg),
749
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
750
+ error: (msg) => console.error(chalk.red("\u2717"), msg),
751
+ debug: (msg) => {
752
+ if (process.env.LOBSTER_DEBUG) console.log(chalk.gray("\u22EF"), msg);
753
+ },
754
+ step: (n, msg) => console.log(chalk.cyan(`[${n}]`), msg),
755
+ dim: (msg) => console.log(chalk.dim(msg))
756
+ };
757
+
758
+ // src/agent/core.ts
759
+ var __dirname = dirname(fileURLToPath(import.meta.url));
760
+ var AgentCore = class {
761
+ page;
762
+ config;
763
+ llm;
764
+ history = [];
765
+ _status = "idle";
766
+ listeners = /* @__PURE__ */ new Map();
767
+ previousElementHashes = /* @__PURE__ */ new Set();
768
+ totalWaitTime = 0;
769
+ constructor(page, config) {
770
+ this.page = page;
771
+ this.config = config;
772
+ this.llm = new LLM(config.llm);
773
+ }
774
+ // ── Event system ──
775
+ on(event, listener) {
776
+ if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
777
+ this.listeners.get(event).add(listener);
778
+ }
779
+ off(event, listener) {
780
+ this.listeners.get(event)?.delete(listener);
781
+ }
782
+ emit(event) {
783
+ const listeners = this.listeners.get(event.type);
784
+ if (listeners) {
785
+ for (const fn of listeners) {
786
+ try {
787
+ fn(event);
788
+ } catch {
789
+ }
790
+ }
791
+ }
792
+ }
793
+ get status() {
794
+ return this._status;
795
+ }
796
+ setStatus(newStatus) {
797
+ const prev = this._status;
798
+ this._status = newStatus;
799
+ this.emit({ type: "statuschange", status: newStatus, previousStatus: prev });
800
+ }
801
+ pushHistory(event) {
802
+ this.history.push(event);
803
+ this.emit({ type: "historychange", history: this.history });
804
+ }
805
+ async execute(task, abortSignal) {
806
+ this.setStatus("running");
807
+ this.history = [];
808
+ this.previousElementHashes.clear();
809
+ this.totalWaitTime = 0;
810
+ const maxSteps = this.config.maxSteps ?? 40;
811
+ const stepDelay = this.config.stepDelay ?? 0.4;
812
+ const tools = {
813
+ ...createDefaultTools(this.page),
814
+ ...this.config.customTools || {}
815
+ };
816
+ for (const [name, tool] of Object.entries(tools)) {
817
+ if (tool === null) delete tools[name];
818
+ }
819
+ const macroTool = packMacroTool(tools);
820
+ let systemPrompt;
821
+ try {
822
+ systemPrompt = readFileSync(join(__dirname, "prompts", "system.md"), "utf-8");
823
+ } catch {
824
+ systemPrompt = "You are an AI web agent that navigates web pages to complete tasks.";
825
+ }
826
+ if (this.config.instructions?.system) {
827
+ systemPrompt += "\n\n" + this.config.instructions.system;
828
+ }
829
+ let lastURL = "";
830
+ for (let step = 1; step <= maxSteps; step++) {
831
+ if (abortSignal?.aborted) {
832
+ this.setStatus("error");
833
+ return { success: false, data: "Aborted", history: this.history };
834
+ }
835
+ const browserState = await this.page.browserState().catch(() => ({
836
+ url: "",
837
+ title: "",
838
+ viewportWidth: 0,
839
+ viewportHeight: 0,
840
+ pageWidth: 0,
841
+ pageHeight: 0,
842
+ scrollX: 0,
843
+ scrollY: 0,
844
+ scrollPercent: 0,
845
+ pixelsAbove: 0,
846
+ pixelsBelow: 0
847
+ }));
848
+ const flatTree = await this.page.flatTree().catch(() => ({ rootId: "", map: {} }));
849
+ const pageContent = flatTreeToString(flatTree);
850
+ const currentHashes = /* @__PURE__ */ new Set();
851
+ let newElementCount = 0;
852
+ for (const node of Object.values(flatTree.map)) {
853
+ if (node.isInteractive && node.highlightIndex !== void 0) {
854
+ const hash = `${node.tagName}:${node.text || ""}:${JSON.stringify(node.attributes || {})}`;
855
+ currentHashes.add(hash);
856
+ if (!this.previousElementHashes.has(hash)) {
857
+ newElementCount++;
858
+ }
859
+ }
860
+ }
861
+ this.previousElementHashes = currentHashes;
862
+ const observations = [];
863
+ if (browserState.url !== lastURL && lastURL) {
864
+ observations.push(`Navigated to ${browserState.url}`);
865
+ }
866
+ lastURL = browserState.url;
867
+ if (newElementCount > 0 && step > 1) {
868
+ observations.push(`${newElementCount} new interactive element(s) appeared`);
869
+ }
870
+ if (this.totalWaitTime > 3) {
871
+ observations.push(`Total wait time: ${this.totalWaitTime.toFixed(1)}s \u2014 consider if page is still loading`);
872
+ }
873
+ if (step >= maxSteps - 5) {
874
+ observations.push(`Warning: ${maxSteps - step} steps remaining`);
875
+ }
876
+ if (this.config.instructions?.getPageInstructions) {
877
+ try {
878
+ const pi = this.config.instructions.getPageInstructions(browserState.url);
879
+ if (pi) observations.push(`Page instructions: ${pi}`);
880
+ } catch {
881
+ }
882
+ }
883
+ for (const obs of observations) {
884
+ this.pushHistory({ type: "observation", message: obs });
885
+ this.emit({ type: "activity", kind: "observation", message: obs, step });
886
+ }
887
+ const userPrompt = assembleUserPrompt(
888
+ task,
889
+ pageContent,
890
+ browserState,
891
+ this.history,
892
+ step,
893
+ maxSteps
894
+ );
895
+ const messages = [
896
+ { role: "system", content: systemPrompt },
897
+ { role: "user", content: userPrompt }
898
+ ];
899
+ log.step(step, `Thinking... (${browserState.url})`);
900
+ this.emit({ type: "activity", kind: "thinking", message: `Step ${step}: thinking`, step });
901
+ if (this.config.onBeforeStep) await this.config.onBeforeStep(step);
902
+ const startTime = Date.now();
903
+ let result;
904
+ try {
905
+ result = await this.llm.invoke(messages, macroTool, abortSignal);
906
+ } catch (err) {
907
+ log.error(`LLM error at step ${step}: ${err}`);
908
+ this.pushHistory({ type: "error", error: String(err), step });
909
+ this.emit({ type: "activity", kind: "error", message: String(err), step });
910
+ continue;
911
+ }
912
+ const duration = Date.now() - startTime;
913
+ const args = result.toolCall.args;
914
+ const action = args.action || args;
915
+ const [actionName, actionInput] = Object.entries(action)[0] || ["unknown", {}];
916
+ this.emit({ type: "activity", kind: "executing", message: actionName, step });
917
+ if (actionName === "wait") {
918
+ const secs = actionInput?.seconds || 0;
919
+ this.totalWaitTime += secs;
920
+ }
921
+ const stepEvent = {
922
+ type: "step",
923
+ step,
924
+ reflection: {
925
+ evaluation_previous_goal: args.evaluation_previous_goal || "",
926
+ memory: args.memory || "",
927
+ next_goal: args.next_goal || ""
928
+ },
929
+ action: { name: actionName, args: actionInput },
930
+ output: result.toolResult,
931
+ duration
932
+ };
933
+ this.pushHistory(stepEvent);
934
+ log.step(step, `Action: ${actionName} \u2192 ${result.toolResult.slice(0, 100)}`);
935
+ this.emit({ type: "activity", kind: "executed", message: `${actionName}: ${result.toolResult.slice(0, 80)}`, step, duration });
936
+ if (this.config.onAfterStep) await this.config.onAfterStep(this.history);
937
+ if (actionName === "done") {
938
+ try {
939
+ const doneResult = JSON.parse(result.toolResult);
940
+ this.setStatus("completed");
941
+ return { success: doneResult.success, data: doneResult.text || result.toolResult, history: this.history };
942
+ } catch {
943
+ this.setStatus("completed");
944
+ return { success: true, data: result.toolResult, history: this.history };
945
+ }
946
+ }
947
+ if (stepDelay > 0) {
948
+ await new Promise((r) => setTimeout(r, stepDelay * 1e3));
949
+ }
950
+ }
951
+ this.setStatus("error");
952
+ return { success: false, data: `Reached maximum steps (${maxSteps})`, history: this.history };
953
+ }
954
+ };
955
+ function assembleUserPrompt(task, pageContent, state, history, step, maxSteps) {
956
+ let prompt = `# Task
957
+ ${task}
958
+
959
+ `;
960
+ prompt += `# Current Page
961
+ `;
962
+ prompt += `URL: ${state.url}
963
+ `;
964
+ prompt += `Title: ${state.title}
965
+ `;
966
+ prompt += `Viewport: ${state.viewportWidth}x${state.viewportHeight} | Page height: ${state.pageHeight}px
967
+ `;
968
+ prompt += `Scroll: ${state.scrollPercent}%`;
969
+ if (state.pixelsAbove > 50) prompt += ` | ${state.pixelsAbove}px above`;
970
+ if (state.pixelsBelow > 50) prompt += ` | ${state.pixelsBelow}px below`;
971
+ prompt += `
972
+ Step: ${step}/${maxSteps}
973
+
974
+ `;
975
+ prompt += `# Browser State
976
+ ${pageContent}
977
+
978
+ `;
979
+ if (history.length > 0) {
980
+ prompt += `# History
981
+ `;
982
+ const recent = history.slice(-10);
983
+ for (const event of recent) {
984
+ if (event.type === "step") {
985
+ const s = event;
986
+ prompt += `<step_${s.step}>
987
+ `;
988
+ if (s.reflection) {
989
+ prompt += ` eval: ${s.reflection.evaluation_previous_goal}
990
+ `;
991
+ prompt += ` memory: ${s.reflection.memory}
992
+ `;
993
+ prompt += ` goal: ${s.reflection.next_goal}
994
+ `;
995
+ }
996
+ prompt += ` action: ${s.action.name}(${JSON.stringify(s.action.args)})
997
+ `;
998
+ prompt += ` result: ${s.output.slice(0, 200)}
999
+ `;
1000
+ prompt += `</step_${s.step}>
1001
+ `;
1002
+ } else if (event.type === "observation") {
1003
+ prompt += `<sys>${event.message}</sys>
1004
+ `;
1005
+ }
1006
+ }
1007
+ }
1008
+ return prompt;
1009
+ }
1010
+ export {
1011
+ AgentCore
1012
+ };
1013
+ //# sourceMappingURL=core.js.map