agentic-browser 2.7.1 → 2.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -4
- package/dist/cli/index.mjs +33 -16
- package/dist/index.d.mts +87 -4
- package/dist/index.mjs +1 -1
- package/dist/mcp/index.mjs +33 -71
- package/dist/{runtime-CnexYwrf.mjs → runtime-CgX6XW9u.mjs} +311 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -128,7 +128,8 @@ agentic-browser agent elements
|
|
|
128
128
|
agentic-browser agent elements --roles button,link --limit 20
|
|
129
129
|
agentic-browser agent elements --href /news/ --text "gta"
|
|
130
130
|
agentic-browser agent screenshot --out ./page.png --full-page
|
|
131
|
-
agentic-browser agent
|
|
131
|
+
agentic-browser agent click e3 --intent "open top story"
|
|
132
|
+
agentic-browser agent memory-search "open the top story" --domain example.com
|
|
132
133
|
agentic-browser agent stop
|
|
133
134
|
agentic-browser agent cleanup --dry-run --max-age-days 7
|
|
134
135
|
```
|
|
@@ -143,7 +144,7 @@ agentic-browser agent elements --roles button,link,input --visible-only --limit
|
|
|
143
144
|
agentic-browser agent elements --selector "#main-content"
|
|
144
145
|
```
|
|
145
146
|
|
|
146
|
-
Returns a JSON array of elements with selectors and fallback selectors usable in the typed agent commands like `agent click`, `agent type`, and `agent select
|
|
147
|
+
Returns a JSON array of elements with selectors and fallback selectors usable in the typed agent commands like `agent click`, `agent type`, and `agent select`. Main-content elements come first; header, nav and footer links come last. Each element also gets a short ref (`e1`, `e2`, ...): `agent click e3` resolves it to the stored selector. Refs stay valid while the page URL is unchanged; after navigating, run `agent elements` again:
|
|
147
148
|
|
|
148
149
|
```json
|
|
149
150
|
{
|
|
@@ -274,9 +275,9 @@ agentic-browser memory:stats
|
|
|
274
275
|
|
|
275
276
|
1. `agent start` — launch Chrome and persist session.
|
|
276
277
|
2. `agent elements` — discover what's on the page.
|
|
277
|
-
3. `agent navigate`, `agent click`, `agent type`, `agent select`, and related typed commands — execute actions using
|
|
278
|
+
3. `agent navigate`, `agent click`, `agent type`, `agent select`, and related typed commands — execute actions using element refs or selectors. Pass `--intent "<task>"` (an `agent`-level option, accepted before or after the subcommand) to group the steps of one task in memory. Consecutive steps with the same intent form one run; a new run replaces the stored recipe, so it always holds the latest run's steps in order.
|
|
278
279
|
4. `agent content` — read page content after actions.
|
|
279
|
-
5. `agent memory-search` —
|
|
280
|
+
5. `agent memory-search "<task>"` — matching is fuzzy; results include a `recipe` with the steps that worked last time (`needsInput` marks typed text that was redacted).
|
|
280
281
|
6. `agent stop` — terminate when done.
|
|
281
282
|
|
|
282
283
|
## Anti-Detection
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { c as writePrivateFileSync, o as isConnectionError, r as createCliRuntime, s as ensurePrivateDir } from "../runtime-CgX6XW9u.mjs";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import crypto from "node:crypto";
|
|
@@ -87,7 +87,8 @@ async function executeAgentCommand(runtime, input) {
|
|
|
87
87
|
const attempt = () => runtime.api.executeCommand(sessionId, {
|
|
88
88
|
commandId: nextCommandId(),
|
|
89
89
|
type: input.type,
|
|
90
|
-
payload: input.payload
|
|
90
|
+
payload: input.payload,
|
|
91
|
+
intent: input.intent
|
|
91
92
|
});
|
|
92
93
|
const retryAfterRestart = async () => {
|
|
93
94
|
await runtime.api.restartSession(sessionId);
|
|
@@ -111,12 +112,12 @@ async function executeAgentCommand(runtime, input) {
|
|
|
111
112
|
async function agentNavigate(runtime, input) {
|
|
112
113
|
const result = await executeAgentCommand(runtime, {
|
|
113
114
|
type: "navigate",
|
|
114
|
-
payload: { url: input.url }
|
|
115
|
+
payload: { url: input.url },
|
|
116
|
+
intent: input.intent
|
|
115
117
|
});
|
|
116
118
|
let cookieBanner;
|
|
117
119
|
if (result.resultStatus === "success") try {
|
|
118
|
-
const
|
|
119
|
-
const dismissed = await runtime.api.dismissCookieBanner(sessionId);
|
|
120
|
+
const dismissed = await runtime.api.dismissCookieBanner(result.sessionId);
|
|
120
121
|
if (dismissed.dismissed) cookieBanner = dismissed;
|
|
121
122
|
} catch {}
|
|
122
123
|
const page = await tryPageState(runtime, result.sessionId);
|
|
@@ -128,10 +129,13 @@ async function agentNavigate(runtime, input) {
|
|
|
128
129
|
cookieBanner
|
|
129
130
|
};
|
|
130
131
|
}
|
|
132
|
+
/** `selector` may be a ref ("e3") from `agent elements`; the core resolves it. */
|
|
131
133
|
async function agentInteract(runtime, input) {
|
|
134
|
+
const { intent, ...payload } = input;
|
|
132
135
|
const result = await executeAgentCommand(runtime, {
|
|
133
136
|
type: "interact",
|
|
134
|
-
payload
|
|
137
|
+
payload,
|
|
138
|
+
intent
|
|
135
139
|
});
|
|
136
140
|
const page = await tryPageState(runtime, result.sessionId);
|
|
137
141
|
return {
|
|
@@ -473,6 +477,7 @@ async function runMemoryStats(runtime) {
|
|
|
473
477
|
// src/cli/index.ts
|
|
474
478
|
const collect = (value, previous = []) => [...previous, value];
|
|
475
479
|
const FRAME_HELP = "iframe selector to run in (cross-origin frames supported)";
|
|
480
|
+
const INTENT_HELP = "task this step belongs to, e.g. \"log in\"; memory groups steps by it (see agent memory-search)";
|
|
476
481
|
/** Resolves `<selector>` or `--text` (substring; `--exact` for exact match) to one locator. */
|
|
477
482
|
function resolveTarget(selector, options) {
|
|
478
483
|
if (selector && options.text) throw new Error("Pass either a selector or --text, not both");
|
|
@@ -554,7 +559,9 @@ async function main() {
|
|
|
554
559
|
const result = await runMemoryStats(runtime);
|
|
555
560
|
console.log(JSON.stringify(result));
|
|
556
561
|
});
|
|
557
|
-
const agent = program.command("agent").description("Stateful agent wrapper with session persistence and auto-retry");
|
|
562
|
+
const agent = program.command("agent").description("Stateful agent wrapper with session persistence and auto-retry").option("--intent <task>", INTENT_HELP);
|
|
563
|
+
/** --intent of the current agent step command; memory groups steps by it. */
|
|
564
|
+
const stepIntent = () => agent.opts().intent;
|
|
558
565
|
agent.command("start").option("--cdp-url <url>", "connect to existing Chrome via CDP endpoint URL").option("--user-profile <path>", "use 'default' for system Chrome profile or an absolute path").option("--headless", "run Chrome in headless mode (no visible window)").option("--user-agent <string>", "override the browser user-agent string").action(async (options) => {
|
|
559
566
|
if (options.cdpUrl) runtime.context.config.cdpUrl = options.cdpUrl;
|
|
560
567
|
if (options.userProfile) runtime.context.config.userProfileDir = options.userProfile === "true" || options.userProfile === "default" ? "default" : options.userProfile;
|
|
@@ -572,19 +579,23 @@ async function main() {
|
|
|
572
579
|
console.log(JSON.stringify(result));
|
|
573
580
|
});
|
|
574
581
|
agent.command("navigate").argument("<url>").action(async (url) => {
|
|
575
|
-
const result = await agentNavigate(runtime, {
|
|
582
|
+
const result = await agentNavigate(runtime, {
|
|
583
|
+
url,
|
|
584
|
+
intent: stepIntent()
|
|
585
|
+
});
|
|
576
586
|
console.log(JSON.stringify(result));
|
|
577
587
|
});
|
|
578
|
-
agent.command("click").argument("[selector]", "CSS selector or composed locator (a >>> b)").option("--text <text>", "click the element with this visible text instead of a selector").option("--exact", "with --text: require an exact text match").option("--frame <selector>", FRAME_HELP).option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
|
|
588
|
+
agent.command("click").argument("[selector]", "element ref (e3) from agent elements, CSS selector, or composed locator (a >>> b)").option("--text <text>", "click the element with this visible text instead of a selector").option("--exact", "with --text: require an exact text match").option("--frame <selector>", FRAME_HELP).option("--fallback <selector>", "backup CSS selector", collect, []).action(async (selector, options) => {
|
|
579
589
|
const result = await agentInteract(runtime, {
|
|
580
590
|
action: "click",
|
|
581
591
|
selector: resolveTarget(selector, options),
|
|
582
592
|
frame: options.frame,
|
|
583
|
-
fallbackSelectors: options.fallback
|
|
593
|
+
fallbackSelectors: options.fallback,
|
|
594
|
+
intent: stepIntent()
|
|
584
595
|
});
|
|
585
596
|
console.log(JSON.stringify(result));
|
|
586
597
|
});
|
|
587
|
-
agent.command("type").argument("<selectorOrText>", "CSS selector, or the text to type when --into-text is used").argument("[text]").option("--into-text <label>", "target the field by its visible text/label/placeholder").option("--frame <selector>", FRAME_HELP).option("--fallback <selector>", "backup CSS selector", collect, []).action(async (first, second, options) => {
|
|
598
|
+
agent.command("type").argument("<selectorOrText>", "element ref (e3), CSS selector, or the text to type when --into-text is used").argument("[text]").option("--into-text <label>", "target the field by its visible text/label/placeholder").option("--frame <selector>", FRAME_HELP).option("--fallback <selector>", "backup CSS selector", collect, []).action(async (first, second, options) => {
|
|
588
599
|
const selector = options.intoText ? `text=${options.intoText}` : first;
|
|
589
600
|
const text = options.intoText ? first : second;
|
|
590
601
|
if (text === void 0) throw new Error("Missing text to type");
|
|
@@ -593,14 +604,16 @@ async function main() {
|
|
|
593
604
|
selector,
|
|
594
605
|
text,
|
|
595
606
|
frame: options.frame,
|
|
596
|
-
fallbackSelectors: options.fallback
|
|
607
|
+
fallbackSelectors: options.fallback,
|
|
608
|
+
intent: stepIntent()
|
|
597
609
|
});
|
|
598
610
|
console.log(JSON.stringify(result));
|
|
599
611
|
});
|
|
600
612
|
agent.command("press").argument("<key>").action(async (key) => {
|
|
601
613
|
const result = await agentInteract(runtime, {
|
|
602
614
|
action: "press",
|
|
603
|
-
key
|
|
615
|
+
key,
|
|
616
|
+
intent: stepIntent()
|
|
604
617
|
});
|
|
605
618
|
console.log(JSON.stringify(result));
|
|
606
619
|
});
|
|
@@ -620,7 +633,8 @@ async function main() {
|
|
|
620
633
|
action: "hover",
|
|
621
634
|
selector: resolveTarget(selector, options),
|
|
622
635
|
frame: options.frame,
|
|
623
|
-
fallbackSelectors: options.fallback
|
|
636
|
+
fallbackSelectors: options.fallback,
|
|
637
|
+
intent: stepIntent()
|
|
624
638
|
});
|
|
625
639
|
console.log(JSON.stringify(result));
|
|
626
640
|
});
|
|
@@ -630,7 +644,8 @@ async function main() {
|
|
|
630
644
|
selector,
|
|
631
645
|
value,
|
|
632
646
|
frame: options.frame,
|
|
633
|
-
fallbackSelectors: options.fallback
|
|
647
|
+
fallbackSelectors: options.fallback,
|
|
648
|
+
intent: stepIntent()
|
|
634
649
|
});
|
|
635
650
|
console.log(JSON.stringify(result));
|
|
636
651
|
});
|
|
@@ -639,7 +654,8 @@ async function main() {
|
|
|
639
654
|
action: "toggle",
|
|
640
655
|
selector,
|
|
641
656
|
frame: options.frame,
|
|
642
|
-
fallbackSelectors: options.fallback
|
|
657
|
+
fallbackSelectors: options.fallback,
|
|
658
|
+
intent: stepIntent()
|
|
643
659
|
});
|
|
644
660
|
console.log(JSON.stringify(result));
|
|
645
661
|
});
|
|
@@ -648,6 +664,7 @@ async function main() {
|
|
|
648
664
|
action: "scroll",
|
|
649
665
|
selector: options.selector,
|
|
650
666
|
fallbackSelectors: options.fallback,
|
|
667
|
+
intent: stepIntent(),
|
|
651
668
|
scrollX: Number.parseInt(options.x, 10),
|
|
652
669
|
scrollY: Number.parseInt(options.y, 10)
|
|
653
670
|
});
|
package/dist/index.d.mts
CHANGED
|
@@ -49,6 +49,8 @@ interface InteractiveElement {
|
|
|
49
49
|
ariaLabel?: string;
|
|
50
50
|
placeholder?: string;
|
|
51
51
|
currentValue?: string;
|
|
52
|
+
/** Short handle ("e3") usable in place of the selector until the page changes. */
|
|
53
|
+
ref?: string;
|
|
52
54
|
}
|
|
53
55
|
interface InteractiveElementsOptions {
|
|
54
56
|
roles?: InteractiveElementRole[];
|
|
@@ -171,6 +173,7 @@ declare const TaskInsightSchema: z.ZodObject<{
|
|
|
171
173
|
selector: z.ZodOptional<z.ZodString>;
|
|
172
174
|
payload: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
173
175
|
}, z.core.$strip>>;
|
|
176
|
+
recipeRunId: z.ZodOptional<z.ZodString>;
|
|
174
177
|
expectedOutcome: z.ZodString;
|
|
175
178
|
confidence: z.ZodNumber;
|
|
176
179
|
successCount: z.ZodNumber;
|
|
@@ -255,6 +258,14 @@ interface SiteProfileSummary {
|
|
|
255
258
|
};
|
|
256
259
|
visitCount: number;
|
|
257
260
|
}
|
|
261
|
+
/** One replayable step of a remembered task. */
|
|
262
|
+
interface RecipeStepSummary {
|
|
263
|
+
action: string;
|
|
264
|
+
selector?: string;
|
|
265
|
+
url?: string;
|
|
266
|
+
/** Typed text or a selected value was not stored; supply it again on replay. */
|
|
267
|
+
needsInput?: true;
|
|
268
|
+
}
|
|
258
269
|
interface MemorySearchResult {
|
|
259
270
|
insightId: string;
|
|
260
271
|
taskIntent: string;
|
|
@@ -262,6 +273,8 @@ interface MemorySearchResult {
|
|
|
262
273
|
confidence: number;
|
|
263
274
|
freshness: InsightFreshness;
|
|
264
275
|
lastVerifiedAt: string;
|
|
276
|
+
/** The steps that completed this task last time, in order. */
|
|
277
|
+
recipe: RecipeStepSummary[];
|
|
265
278
|
selectorHints: string[];
|
|
266
279
|
selectorAliases: SelectorAlias[];
|
|
267
280
|
score: number;
|
|
@@ -398,6 +411,11 @@ interface RecordExecutionInput {
|
|
|
398
411
|
expectedOutcome: string;
|
|
399
412
|
url?: string;
|
|
400
413
|
selector?: string;
|
|
414
|
+
/**
|
|
415
|
+
* Run this step belongs to. A step from a new run starts the recipe over, so
|
|
416
|
+
* the recipe is always the latest run's steps in order. Defaults to commandId.
|
|
417
|
+
*/
|
|
418
|
+
runId?: string;
|
|
401
419
|
}
|
|
402
420
|
declare class MemoryService {
|
|
403
421
|
private readonly store;
|
|
@@ -451,6 +469,7 @@ declare class MemoryService {
|
|
|
451
469
|
private createEvidence;
|
|
452
470
|
private maybeGenerateAliases;
|
|
453
471
|
private deriveAliasName;
|
|
472
|
+
/** Same run: append the step. New run: its first step replaces the old recipe. */
|
|
454
473
|
private mergeRecipe;
|
|
455
474
|
}
|
|
456
475
|
interface AppContext {
|
|
@@ -460,12 +479,43 @@ interface AppContext {
|
|
|
460
479
|
tokenService: SessionTokenService;
|
|
461
480
|
memoryService: MemoryService;
|
|
462
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Short, stable handles ("e1", "e2", …) an agent can point at instead of
|
|
484
|
+
* authoring CSS selectors — the way a human points at what they see rather
|
|
485
|
+
* than reasoning about the DOM.
|
|
486
|
+
*
|
|
487
|
+
* Refs are ephemeral per "look": each element listing replaces the session's
|
|
488
|
+
* ref set, and a ref only resolves while the page URL is the one it was taken
|
|
489
|
+
* on. SessionManager persists them on the session record, so the CLI (one
|
|
490
|
+
* process per command) and the MCP server share the same mechanism.
|
|
491
|
+
*/
|
|
492
|
+
interface ResolvedRef {
|
|
493
|
+
selector: string;
|
|
494
|
+
fallbackSelectors?: string[];
|
|
495
|
+
/** Iframe the element was discovered in, so the ref acts inside it. */
|
|
496
|
+
frame?: string;
|
|
497
|
+
}
|
|
498
|
+
interface StoredRefs {
|
|
499
|
+
/** Page the refs were taken on; on any other page they are stale. */
|
|
500
|
+
url?: string;
|
|
501
|
+
byRef: Record<string, ResolvedRef>;
|
|
502
|
+
}
|
|
463
503
|
interface StoredSessionRecord {
|
|
464
504
|
session: Session;
|
|
465
505
|
cdpUrl: string;
|
|
466
506
|
targetWsUrl: string;
|
|
467
507
|
pid: number;
|
|
468
508
|
lastUrl?: string;
|
|
509
|
+
/** Element refs from the last element listing. */
|
|
510
|
+
refs?: StoredRefs;
|
|
511
|
+
/**
|
|
512
|
+
* The task run in progress: consecutive commands with the same explicit
|
|
513
|
+
* intent share `runId`, so memory can rebuild that task's recipe per run.
|
|
514
|
+
*/
|
|
515
|
+
intentRun?: {
|
|
516
|
+
intent: string;
|
|
517
|
+
runId: string;
|
|
518
|
+
};
|
|
469
519
|
}
|
|
470
520
|
interface CreateSessionInput {
|
|
471
521
|
browser: "chrome";
|
|
@@ -474,6 +524,8 @@ interface ExecuteCommandInput$1 {
|
|
|
474
524
|
commandId: string;
|
|
475
525
|
type: CommandType;
|
|
476
526
|
payload: Record<string, unknown>;
|
|
527
|
+
/** Task this step belongs to (e.g. "log in"); memory groups steps by it. */
|
|
528
|
+
intent?: string;
|
|
477
529
|
}
|
|
478
530
|
interface CleanupInput {
|
|
479
531
|
maxAgeDays?: number;
|
|
@@ -494,9 +546,15 @@ declare class SessionManager {
|
|
|
494
546
|
getSession(sessionId: string): Session;
|
|
495
547
|
/** Return a healthy session, recovering automatically if needed. */
|
|
496
548
|
ensureSession(sessionId: string): Promise<StoredSessionRecord>;
|
|
497
|
-
executeCommand(sessionId: string,
|
|
549
|
+
executeCommand(sessionId: string, rawInput: ExecuteCommandInput$1): Promise<Command>;
|
|
498
550
|
getContent(sessionId: string, options: PageContentOptions): Promise<PageContentResult>;
|
|
499
|
-
|
|
551
|
+
/** Lists interactive elements and assigns fresh refs (e1, e2, …) for this page. */
|
|
552
|
+
getInteractiveElements(sessionId: string, options: InteractiveElementsOptions): Promise<{
|
|
553
|
+
totalFound: number;
|
|
554
|
+
truncated: boolean;
|
|
555
|
+
crossOriginFrames?: CrossOriginFrameInfo[];
|
|
556
|
+
elements: InteractiveElement[];
|
|
557
|
+
}>;
|
|
500
558
|
screenshot(sessionId: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
501
559
|
getCurrentUrl(sessionId: string): Promise<string>;
|
|
502
560
|
getPageState(sessionId: string): Promise<PageState>;
|
|
@@ -534,6 +592,7 @@ declare class SessionManager {
|
|
|
534
592
|
selector?: string | undefined;
|
|
535
593
|
payload: Record<string, unknown>;
|
|
536
594
|
}[];
|
|
595
|
+
recipeRunId?: string | undefined;
|
|
537
596
|
expectedOutcome: string;
|
|
538
597
|
confidence: number;
|
|
539
598
|
successCount: number;
|
|
@@ -571,6 +630,7 @@ declare class SessionManager {
|
|
|
571
630
|
selector?: string | undefined;
|
|
572
631
|
payload: Record<string, unknown>;
|
|
573
632
|
}[];
|
|
633
|
+
recipeRunId?: string | undefined;
|
|
574
634
|
expectedOutcome: string;
|
|
575
635
|
confidence: number;
|
|
576
636
|
successCount: number;
|
|
@@ -615,6 +675,12 @@ declare class SessionManager {
|
|
|
615
675
|
private mustGetRecord;
|
|
616
676
|
private recordEvent;
|
|
617
677
|
private buildMemoryContext;
|
|
678
|
+
private resolveRef;
|
|
679
|
+
/**
|
|
680
|
+
* Consecutive commands with the same explicit intent form one run; any other
|
|
681
|
+
* command ends it. Commands without an intent are each their own run.
|
|
682
|
+
*/
|
|
683
|
+
private trackIntentRun;
|
|
618
684
|
private parseUrl;
|
|
619
685
|
}
|
|
620
686
|
declare class ControlApi {
|
|
@@ -643,6 +709,7 @@ declare class ControlApi {
|
|
|
643
709
|
commandId: string;
|
|
644
710
|
type: "navigate" | "interact" | "restart" | "terminate";
|
|
645
711
|
payload: Record<string, unknown>;
|
|
712
|
+
intent?: string;
|
|
646
713
|
}): Promise<{
|
|
647
714
|
commandId: string;
|
|
648
715
|
sessionId: string;
|
|
@@ -667,7 +734,12 @@ declare class ControlApi {
|
|
|
667
734
|
selector?: string;
|
|
668
735
|
maxChars?: number;
|
|
669
736
|
}): Promise<PageContentResult>;
|
|
670
|
-
getInteractiveElements(sessionId: string, options: InteractiveElementsOptions): Promise<
|
|
737
|
+
getInteractiveElements(sessionId: string, options: InteractiveElementsOptions): Promise<{
|
|
738
|
+
totalFound: number;
|
|
739
|
+
truncated: boolean;
|
|
740
|
+
crossOriginFrames?: CrossOriginFrameInfo[];
|
|
741
|
+
elements: InteractiveElement[];
|
|
742
|
+
}>;
|
|
671
743
|
dismissCookieBanner(sessionId: string): Promise<DismissCookieBannerResult>;
|
|
672
744
|
screenshot(sessionId: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
673
745
|
getCurrentUrl(sessionId: string): Promise<string>;
|
|
@@ -709,6 +781,7 @@ declare class ControlApi {
|
|
|
709
781
|
selector?: string | undefined;
|
|
710
782
|
payload: Record<string, unknown>;
|
|
711
783
|
}[];
|
|
784
|
+
recipeRunId?: string | undefined;
|
|
712
785
|
expectedOutcome: string;
|
|
713
786
|
confidence: number;
|
|
714
787
|
successCount: number;
|
|
@@ -746,6 +819,7 @@ declare class ControlApi {
|
|
|
746
819
|
selector?: string | undefined;
|
|
747
820
|
payload: Record<string, unknown>;
|
|
748
821
|
}[];
|
|
822
|
+
recipeRunId?: string | undefined;
|
|
749
823
|
expectedOutcome: string;
|
|
750
824
|
confidence: number;
|
|
751
825
|
successCount: number;
|
|
@@ -796,6 +870,8 @@ interface ExecuteCommandInput {
|
|
|
796
870
|
commandId?: string;
|
|
797
871
|
type: CommandType;
|
|
798
872
|
payload: Record<string, unknown>;
|
|
873
|
+
/** Task this step belongs to (e.g. "log in"); memory groups steps by it. */
|
|
874
|
+
intent?: string;
|
|
799
875
|
}
|
|
800
876
|
export declare class AgenticBrowserCore {
|
|
801
877
|
readonly context: AppContext;
|
|
@@ -853,7 +929,12 @@ export declare class AgenticBrowserCore {
|
|
|
853
929
|
href?: string;
|
|
854
930
|
text?: string;
|
|
855
931
|
frame?: string;
|
|
856
|
-
}): Promise<
|
|
932
|
+
}): Promise<{
|
|
933
|
+
totalFound: number;
|
|
934
|
+
truncated: boolean;
|
|
935
|
+
crossOriginFrames?: CrossOriginFrameInfo[];
|
|
936
|
+
elements: InteractiveElement[];
|
|
937
|
+
}>;
|
|
857
938
|
dismissCookieBanner(sessionId: string): Promise<DismissCookieBannerResult>;
|
|
858
939
|
screenshot(sessionId: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
|
|
859
940
|
getCurrentUrl(sessionId: string): Promise<string>;
|
|
@@ -895,6 +976,7 @@ export declare class AgenticBrowserCore {
|
|
|
895
976
|
selector?: string | undefined;
|
|
896
977
|
payload: Record<string, unknown>;
|
|
897
978
|
}[];
|
|
979
|
+
recipeRunId?: string | undefined;
|
|
898
980
|
expectedOutcome: string;
|
|
899
981
|
confidence: number;
|
|
900
982
|
successCount: number;
|
|
@@ -932,6 +1014,7 @@ export declare class AgenticBrowserCore {
|
|
|
932
1014
|
selector?: string | undefined;
|
|
933
1015
|
payload: Record<string, unknown>;
|
|
934
1016
|
}[];
|
|
1017
|
+
recipeRunId?: string | undefined;
|
|
935
1018
|
expectedOutcome: string;
|
|
936
1019
|
confidence: number;
|
|
937
1020
|
successCount: number;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { t as AgenticBrowserCore, n as createAgenticBrowserCore, i as createMockAgenticBrowserCore } from "./runtime-
|
|
2
|
+
export { t as AgenticBrowserCore, n as createAgenticBrowserCore, i as createMockAgenticBrowserCore } from "./runtime-CgX6XW9u.mjs";
|
package/dist/mcp/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as createAgenticBrowserCore } from "../runtime-
|
|
2
|
+
import { a as StaleRefError, n as createAgenticBrowserCore } from "../runtime-CgX6XW9u.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -204,46 +204,6 @@ function compactMemoryResults(results) {
|
|
|
204
204
|
return compact;
|
|
205
205
|
}) };
|
|
206
206
|
}
|
|
207
|
-
// src/mcp/element-refs.ts
|
|
208
|
-
var ElementRefRegistry = class {
|
|
209
|
-
bySession = /* @__PURE__ */ new Map();
|
|
210
|
-
/**
|
|
211
|
-
* Assign fresh refs to a freshly-discovered element list, replacing any prior
|
|
212
|
-
* refs for the session. Elements without a selector (nothing to act on) are
|
|
213
|
-
* still returned but get no ref.
|
|
214
|
-
*/
|
|
215
|
-
assign(sessionId, elements) {
|
|
216
|
-
const map = /* @__PURE__ */ new Map();
|
|
217
|
-
let n = 0;
|
|
218
|
-
const annotated = elements.map((el) => {
|
|
219
|
-
if (!el.selector) return {
|
|
220
|
-
...el,
|
|
221
|
-
ref: ""
|
|
222
|
-
};
|
|
223
|
-
const ref = `e${++n}`;
|
|
224
|
-
map.set(ref, {
|
|
225
|
-
selector: el.selector,
|
|
226
|
-
fallbackSelectors: el.fallbackSelectors,
|
|
227
|
-
text: el.text,
|
|
228
|
-
role: el.role
|
|
229
|
-
});
|
|
230
|
-
return {
|
|
231
|
-
...el,
|
|
232
|
-
ref
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
this.bySession.set(sessionId, map);
|
|
236
|
-
return { elements: annotated };
|
|
237
|
-
}
|
|
238
|
-
/** Resolve a ref to its selector target, or undefined if unknown/stale. */
|
|
239
|
-
resolve(sessionId, ref) {
|
|
240
|
-
return this.bySession.get(sessionId)?.get(ref);
|
|
241
|
-
}
|
|
242
|
-
/** Drop a session's refs (on stop/restart) so stale handles can't resolve. */
|
|
243
|
-
clear(sessionId) {
|
|
244
|
-
this.bySession.delete(sessionId);
|
|
245
|
-
}
|
|
246
|
-
};
|
|
247
207
|
// src/mcp/session-resolver.ts
|
|
248
208
|
/**
|
|
249
209
|
* Tracks the active session and auto-starts one on demand. Concurrent callers
|
|
@@ -318,7 +278,6 @@ function getCore() {
|
|
|
318
278
|
function genId(prefix) {
|
|
319
279
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
320
280
|
}
|
|
321
|
-
const refs = new ElementRefRegistry();
|
|
322
281
|
/**
|
|
323
282
|
* Discover interactive elements, assign fresh refs for the session, and return
|
|
324
283
|
* the compact agent-facing view (refs + role/text, no raw selectors). This is
|
|
@@ -334,8 +293,7 @@ async function snapshotElements(sessionId, opts = {}) {
|
|
|
334
293
|
selector: opts.selector,
|
|
335
294
|
roles: opts.roles
|
|
336
295
|
});
|
|
337
|
-
|
|
338
|
-
return compactInteractiveElementsResult(elements, {
|
|
296
|
+
return compactInteractiveElementsResult(result.elements, {
|
|
339
297
|
totalFound: result.totalFound,
|
|
340
298
|
truncated: result.truncated
|
|
341
299
|
}, visibleOnly);
|
|
@@ -347,6 +305,7 @@ async function snapshotElements(sessionId, opts = {}) {
|
|
|
347
305
|
async function resolveSession(sessionId) {
|
|
348
306
|
return sessionResolver.resolve(sessionId);
|
|
349
307
|
}
|
|
308
|
+
const intentParam = z.string().optional().describe("Task this step belongs to, in your own words (e.g. \"log in\", \"search products\"). Steps sharing an intent are remembered together and returned as a recipe by browser_search_memory");
|
|
350
309
|
const server = new McpServer({
|
|
351
310
|
name: "agentic-browser",
|
|
352
311
|
version: readPackageVersion()
|
|
@@ -362,14 +321,16 @@ server.tool("browser_start_session", "Start a new Chrome browser session (or ret
|
|
|
362
321
|
});
|
|
363
322
|
server.tool("browser_navigate", "Navigate the browser to a URL. A session is auto-started if needed.", {
|
|
364
323
|
url: z.string().describe("The URL to navigate to"),
|
|
324
|
+
intent: intentParam,
|
|
365
325
|
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
366
|
-
}, async ({ url, sessionId }) => {
|
|
326
|
+
}, async ({ url, intent, sessionId }) => {
|
|
367
327
|
const sid = await resolveSession(sessionId);
|
|
368
328
|
const result = await getCore().runCommand({
|
|
369
329
|
sessionId: sid,
|
|
370
330
|
commandId: genId("nav"),
|
|
371
331
|
type: "navigate",
|
|
372
|
-
payload: { url }
|
|
332
|
+
payload: { url },
|
|
333
|
+
intent
|
|
373
334
|
});
|
|
374
335
|
let cookieBanner;
|
|
375
336
|
if (result.resultStatus === "success") try {
|
|
@@ -416,38 +377,40 @@ server.tool("browser_interact", "Interact with a page element or perform browser
|
|
|
416
377
|
scrollX: z.number().optional().describe("Horizontal scroll delta in pixels (for \"scroll\" action)"),
|
|
417
378
|
scrollY: z.number().optional().describe("Vertical scroll delta in pixels (for \"scroll\" action, positive = down)"),
|
|
418
379
|
timeoutMs: z.number().optional().describe("Timeout in milliseconds (for \"waitFor\" action, default 4000)"),
|
|
380
|
+
intent: intentParam,
|
|
419
381
|
sessionId: z.string().optional().describe("Session ID (auto-resolved if omitted)")
|
|
420
|
-
}, async ({ action, ref, selector, fallbackSelectors, text, key, value, scrollX, scrollY, timeoutMs, sessionId }) => {
|
|
382
|
+
}, async ({ action, ref, selector, fallbackSelectors, text, key, value, scrollX, scrollY, timeoutMs, intent, sessionId }) => {
|
|
421
383
|
const sid = await resolveSession(sessionId);
|
|
422
384
|
const payload = { action };
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
385
|
+
const target = selector ?? ref;
|
|
386
|
+
if (target) payload.selector = target;
|
|
387
|
+
if (fallbackSelectors) payload.fallbackSelectors = fallbackSelectors;
|
|
388
|
+
if (text) payload.text = text;
|
|
389
|
+
if (key) payload.key = key;
|
|
390
|
+
if (value) payload.value = value;
|
|
391
|
+
if (scrollX !== void 0) payload.scrollX = scrollX;
|
|
392
|
+
if (scrollY !== void 0) payload.scrollY = scrollY;
|
|
393
|
+
if (timeoutMs) payload.timeoutMs = timeoutMs;
|
|
394
|
+
let result;
|
|
395
|
+
try {
|
|
396
|
+
result = await getCore().runCommand({
|
|
397
|
+
sessionId: sid,
|
|
398
|
+
commandId: genId("int"),
|
|
399
|
+
type: "interact",
|
|
400
|
+
payload,
|
|
401
|
+
intent
|
|
402
|
+
});
|
|
403
|
+
} catch (error) {
|
|
404
|
+
if (!(error instanceof StaleRefError)) throw error;
|
|
405
|
+
return { content: [{
|
|
426
406
|
type: "text",
|
|
427
407
|
text: JSON.stringify({
|
|
428
408
|
resultStatus: "failed",
|
|
429
|
-
resultMessage:
|
|
409
|
+
resultMessage: error.message,
|
|
430
410
|
errorCode: "SELECTOR_NOT_FOUND"
|
|
431
411
|
})
|
|
432
412
|
}] };
|
|
433
|
-
payload.selector = resolved.selector;
|
|
434
|
-
if (resolved.fallbackSelectors) payload.fallbackSelectors = resolved.fallbackSelectors;
|
|
435
|
-
} else {
|
|
436
|
-
if (selector) payload.selector = selector;
|
|
437
|
-
if (fallbackSelectors) payload.fallbackSelectors = fallbackSelectors;
|
|
438
413
|
}
|
|
439
|
-
if (text) payload.text = text;
|
|
440
|
-
if (key) payload.key = key;
|
|
441
|
-
if (value) payload.value = value;
|
|
442
|
-
if (scrollX !== void 0) payload.scrollX = scrollX;
|
|
443
|
-
if (scrollY !== void 0) payload.scrollY = scrollY;
|
|
444
|
-
if (timeoutMs) payload.timeoutMs = timeoutMs;
|
|
445
|
-
const result = await getCore().runCommand({
|
|
446
|
-
sessionId: sid,
|
|
447
|
-
commandId: genId("int"),
|
|
448
|
-
type: "interact",
|
|
449
|
-
payload
|
|
450
|
-
});
|
|
451
414
|
let currentUrl;
|
|
452
415
|
try {
|
|
453
416
|
currentUrl = await getCore().getCurrentUrl(sid);
|
|
@@ -523,8 +486,8 @@ server.tool("browser_get_elements", "Discover interactive elements on the curren
|
|
|
523
486
|
text: JSON.stringify(snapshot)
|
|
524
487
|
}] };
|
|
525
488
|
});
|
|
526
|
-
server.tool("browser_search_memory", "Search task memory for previously learned selectors, selector aliases, and interaction patterns. Results include selectorHints (proven selectors) and selectorAliases (human-readable names mapped to selectors with fallbacks). Use this before interacting with a known site to reuse proven selectors instead of rediscovering them.", {
|
|
527
|
-
taskIntent: z.string().describe("What you want to do, e.g. \"
|
|
489
|
+
server.tool("browser_search_memory", "Search task memory for previously learned selectors, selector aliases, and interaction patterns. Matching is fuzzy, so a rephrased task still finds earlier runs. Results include recipe (the ordered steps that worked; needsInput marks steps whose typed text was redacted), selectorHints (proven selectors) and selectorAliases (human-readable names mapped to selectors with fallbacks). Use this before interacting with a known site to reuse proven selectors instead of rediscovering them.", {
|
|
490
|
+
taskIntent: z.string().describe("What you want to do, e.g. \"log in\" or \"search products\". Use the same wording you passed as intent to navigate/interact"),
|
|
528
491
|
siteDomain: z.string().optional().describe("Domain to scope the search"),
|
|
529
492
|
limit: z.number().default(5).describe("Maximum number of results")
|
|
530
493
|
}, async ({ taskIntent, siteDomain, limit }) => {
|
|
@@ -574,7 +537,6 @@ server.tool("browser_stop_session", "Stop the browser session and terminate Chro
|
|
|
574
537
|
})
|
|
575
538
|
}] };
|
|
576
539
|
await getCore().stopSession(sid);
|
|
577
|
-
refs.clear(sid);
|
|
578
540
|
if (sessionResolver.current() === sid) sessionResolver.clear();
|
|
579
541
|
return { content: [{
|
|
580
542
|
type: "text",
|
|
@@ -454,6 +454,22 @@ function resolveDefaultProfileDir() {
|
|
|
454
454
|
return path.join(home, ".config", "google-chrome");
|
|
455
455
|
}
|
|
456
456
|
const CONTENT_TRUNCATION_HINT = "Use a CSS selector to scope the content, or use mode=\"summary\" for a lower-token overview.";
|
|
457
|
+
/**
|
|
458
|
+
* In-page visibility check shared by element discovery and cookie dismissal.
|
|
459
|
+
* Uses the element's own window so it also works inside same-origin iframes.
|
|
460
|
+
*/
|
|
461
|
+
const IN_PAGE_IS_VISIBLE = String.raw`
|
|
462
|
+
function isVisible(el) {
|
|
463
|
+
if (!el) return false;
|
|
464
|
+
const view = (el.ownerDocument && el.ownerDocument.defaultView) || window;
|
|
465
|
+
const style = view.getComputedStyle(el);
|
|
466
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
const rect = el.getBoundingClientRect();
|
|
470
|
+
return rect.width > 0 && rect.height > 0;
|
|
471
|
+
}
|
|
472
|
+
`;
|
|
457
473
|
const LOCATOR_RUNTIME_HELPERS = String.raw`
|
|
458
474
|
const LOCATOR_SEPARATOR = ' >>> ';
|
|
459
475
|
|
|
@@ -2234,7 +2250,24 @@ var ChromeCdpBrowserController = class {
|
|
|
2234
2250
|
}
|
|
2235
2251
|
current = parent;
|
|
2236
2252
|
}
|
|
2237
|
-
|
|
2253
|
+
// Prefer the shortest unique tail of the path: long nth-of-type chains
|
|
2254
|
+
// cost tokens and break on the slightest layout change. A longer tail
|
|
2255
|
+
// matches a subset of a shorter one, so uniqueness is monotonic in the
|
|
2256
|
+
// tail length and a binary search finds the shortest unique tail.
|
|
2257
|
+
let best = parts.join(' > ');
|
|
2258
|
+
let lo = 1;
|
|
2259
|
+
let hi = parts.length - 1;
|
|
2260
|
+
while (lo <= hi) {
|
|
2261
|
+
const size = (lo + hi) >> 1;
|
|
2262
|
+
const candidate = tryUniqueSelector(root, parts.slice(-size).join(' > '));
|
|
2263
|
+
if (candidate) {
|
|
2264
|
+
best = candidate;
|
|
2265
|
+
hi = size - 1;
|
|
2266
|
+
} else {
|
|
2267
|
+
lo = size + 1;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
return best;
|
|
2238
2271
|
}
|
|
2239
2272
|
|
|
2240
2273
|
function buildFallbackSelectors(root, path, el, primarySelector) {
|
|
@@ -2328,18 +2361,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2328
2361
|
}
|
|
2329
2362
|
}
|
|
2330
2363
|
|
|
2331
|
-
|
|
2332
|
-
return el.ownerDocument && el.ownerDocument.defaultView ? el.ownerDocument.defaultView : window;
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
function isVisible(el) {
|
|
2336
|
-
const style = viewForElement(el).getComputedStyle(el);
|
|
2337
|
-
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') {
|
|
2338
|
-
return false;
|
|
2339
|
-
}
|
|
2340
|
-
const rect = el.getBoundingClientRect();
|
|
2341
|
-
return rect.width > 0 && rect.height > 0;
|
|
2342
|
-
}
|
|
2364
|
+
${IN_PAGE_IS_VISIBLE}
|
|
2343
2365
|
|
|
2344
2366
|
function getText(el) {
|
|
2345
2367
|
const ariaLabel = el.getAttribute('aria-label');
|
|
@@ -2392,7 +2414,7 @@ var ChromeCdpBrowserController = class {
|
|
|
2392
2414
|
return [root];
|
|
2393
2415
|
}
|
|
2394
2416
|
|
|
2395
|
-
const
|
|
2417
|
+
const candidates = [];
|
|
2396
2418
|
const crossOriginFrames = [];
|
|
2397
2419
|
let totalFound = 0;
|
|
2398
2420
|
|
|
@@ -2431,8 +2453,38 @@ var ChromeCdpBrowserController = class {
|
|
|
2431
2453
|
}
|
|
2432
2454
|
|
|
2433
2455
|
totalFound += 1;
|
|
2434
|
-
|
|
2456
|
+
candidates.push({ root, path, el, role, visible, rank: rankElement(el, visible) });
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
// Lower rank sorts first: main content, then the rest of the page, then
|
|
2460
|
+
// site chrome (header, nav, footer), then hidden elements. Ties keep
|
|
2461
|
+
// document order.
|
|
2462
|
+
// Region flags per ancestor, memoized so siblings share the walk up the
|
|
2463
|
+
// tree instead of each element running three closest() scans.
|
|
2464
|
+
const FOOTER = 1, MAIN = 2, CHROME = 4;
|
|
2465
|
+
const regionMemo = new Map();
|
|
2466
|
+
function regionFlags(el) {
|
|
2467
|
+
if (!el) return 0;
|
|
2468
|
+
let flags = regionMemo.get(el);
|
|
2469
|
+
if (flags !== undefined) return flags;
|
|
2470
|
+
flags = regionFlags(el.parentElement);
|
|
2471
|
+
if (el.matches('footer, [role="contentinfo"]')) flags |= FOOTER;
|
|
2472
|
+
if (el.matches('main, [role="main"], article')) flags |= MAIN;
|
|
2473
|
+
if (el.matches('header, nav, [role="banner"], [role="navigation"]')) flags |= CHROME;
|
|
2474
|
+
regionMemo.set(el, flags);
|
|
2475
|
+
return flags;
|
|
2476
|
+
}
|
|
2477
|
+
function rankElement(el, visible) {
|
|
2478
|
+
if (!visible) return 3;
|
|
2479
|
+
const flags = regionFlags(el);
|
|
2480
|
+
if (flags & FOOTER) return 2;
|
|
2481
|
+
if (flags & MAIN) return 0;
|
|
2482
|
+
if (flags & CHROME) return 2;
|
|
2483
|
+
return 1;
|
|
2484
|
+
}
|
|
2435
2485
|
|
|
2486
|
+
function buildEntry(candidate) {
|
|
2487
|
+
const { root, path, el, role, visible } = candidate;
|
|
2436
2488
|
const selector = composeLocator(path, buildSelector(root, el));
|
|
2437
2489
|
const entry = {
|
|
2438
2490
|
selector,
|
|
@@ -2457,13 +2509,15 @@ var ChromeCdpBrowserController = class {
|
|
|
2457
2509
|
entry.currentValue = el.value.slice(0, 200);
|
|
2458
2510
|
}
|
|
2459
2511
|
|
|
2460
|
-
|
|
2512
|
+
return entry;
|
|
2461
2513
|
}
|
|
2462
2514
|
|
|
2515
|
+
// Depth-first in document order, so a capped listing follows reading
|
|
2516
|
+
// order instead of favouring shallow page chrome.
|
|
2463
2517
|
function walkRoot(root, path) {
|
|
2464
|
-
const
|
|
2465
|
-
while (
|
|
2466
|
-
const current =
|
|
2518
|
+
const stack = rootEntries(root).slice().reverse();
|
|
2519
|
+
while (stack.length) {
|
|
2520
|
+
const current = stack.pop();
|
|
2467
2521
|
if (!current || current.nodeType !== 1) continue;
|
|
2468
2522
|
|
|
2469
2523
|
maybeRecordElement(root, path, current);
|
|
@@ -2491,13 +2545,17 @@ var ChromeCdpBrowserController = class {
|
|
|
2491
2545
|
}
|
|
2492
2546
|
}
|
|
2493
2547
|
|
|
2494
|
-
|
|
2495
|
-
|
|
2548
|
+
const children = current.children;
|
|
2549
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
2550
|
+
stack.push(children[i]);
|
|
2496
2551
|
}
|
|
2497
2552
|
}
|
|
2498
2553
|
}
|
|
2499
2554
|
|
|
2500
2555
|
walkRoot(document, []);
|
|
2556
|
+
// Array sort is stable, so ties keep document order.
|
|
2557
|
+
candidates.sort((a, b) => a.rank - b.rank);
|
|
2558
|
+
const results = candidates.slice(0, limit).map(buildEntry);
|
|
2501
2559
|
const output = { elements: results, totalFound, truncated: totalFound > results.length };
|
|
2502
2560
|
if (crossOriginFrames.length) output.crossOriginFrames = crossOriginFrames;
|
|
2503
2561
|
return output;
|
|
@@ -2577,16 +2635,12 @@ var ChromeCdpBrowserController = class {
|
|
|
2577
2635
|
}
|
|
2578
2636
|
return {
|
|
2579
2637
|
dismissed: false,
|
|
2580
|
-
detail: notes.size ? [...notes].join("; ") : "no consent banner
|
|
2638
|
+
detail: notes.size ? [...notes].join("; ") : "no consent banner found on the page, in consent iframes or the a11y tree (it may already be dismissed)"
|
|
2581
2639
|
};
|
|
2582
2640
|
}
|
|
2583
2641
|
async dismissViaDom(targetWsUrl) {
|
|
2584
2642
|
const expression = `(() => {
|
|
2585
|
-
|
|
2586
|
-
if (!el) return false;
|
|
2587
|
-
const style = getComputedStyle(el);
|
|
2588
|
-
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && el.offsetWidth > 0;
|
|
2589
|
-
}
|
|
2643
|
+
${IN_PAGE_IS_VISIBLE}
|
|
2590
2644
|
|
|
2591
2645
|
// Comprehensive CMP selectors (ordered by specificity/popularity)
|
|
2592
2646
|
const selectors = [
|
|
@@ -2889,10 +2943,49 @@ var ChromeCdpBrowserController = class {
|
|
|
2889
2943
|
'iframe[src*="consentmanager"]',
|
|
2890
2944
|
];
|
|
2891
2945
|
|
|
2946
|
+
// Consent prompts overlay the page: the frame or a close ancestor is
|
|
2947
|
+
// fixed/sticky or stacked high. In-flow frames (ads, embeds) are skipped.
|
|
2948
|
+
function isOverlay(iframe) {
|
|
2949
|
+
let el = iframe;
|
|
2950
|
+
for (let depth = 0; el && el !== document.body && depth < 10; depth++) {
|
|
2951
|
+
const style = getComputedStyle(el);
|
|
2952
|
+
if (style.position === 'fixed' || style.position === 'sticky') return true;
|
|
2953
|
+
if ((parseInt(style.zIndex, 10) || 0) >= 1000) return true;
|
|
2954
|
+
el = el.parentElement;
|
|
2955
|
+
}
|
|
2956
|
+
return false;
|
|
2957
|
+
}
|
|
2958
|
+
function isShown(iframe) {
|
|
2959
|
+
const rect = iframe.getBoundingClientRect();
|
|
2960
|
+
if (rect.width < 2 || rect.height < 2) return false;
|
|
2961
|
+
const style = getComputedStyle(iframe);
|
|
2962
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
2963
|
+
}
|
|
2964
|
+
// A src keyword must sit in host or path: ad and tracking frames often
|
|
2965
|
+
// carry "cookie=" or "privacy=" in their query string.
|
|
2966
|
+
function srcKeywordMatches(iframe, sel) {
|
|
2967
|
+
const prefix = 'iframe[src*="';
|
|
2968
|
+
if (!sel.startsWith(prefix)) return true;
|
|
2969
|
+
const keyword = sel.slice(prefix.length, sel.indexOf('"', prefix.length));
|
|
2970
|
+
try {
|
|
2971
|
+
const url = new URL(iframe.src, location.href);
|
|
2972
|
+
return (url.hostname + url.pathname).toLowerCase().includes(keyword);
|
|
2973
|
+
} catch {
|
|
2974
|
+
return false;
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
function findFrame(sel) {
|
|
2978
|
+
for (const iframe of document.querySelectorAll(sel)) {
|
|
2979
|
+
if (!isShown(iframe) || !srcKeywordMatches(iframe, sel) || !isOverlay(iframe)) continue;
|
|
2980
|
+
return iframe;
|
|
2981
|
+
}
|
|
2982
|
+
return null;
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2892
2985
|
// Cross-origin consent frames are handed back so the caller can attach to them.
|
|
2893
2986
|
const crossOrigin = [];
|
|
2894
2987
|
for (const sel of iframeSelectors) {
|
|
2895
|
-
const iframe =
|
|
2988
|
+
const iframe = findFrame(sel);
|
|
2896
2989
|
if (!iframe) continue;
|
|
2897
2990
|
let doc = null;
|
|
2898
2991
|
try {
|
|
@@ -3354,6 +3447,40 @@ z.object({
|
|
|
3354
3447
|
createdAt: z.string().datetime(),
|
|
3355
3448
|
severity: EventSeveritySchema
|
|
3356
3449
|
});
|
|
3450
|
+
// src/session/element-refs.ts
|
|
3451
|
+
/** Matches "e3" or "@e3"; group 1 is the bare ref. */
|
|
3452
|
+
const REF_PATTERN = /^@?(e\d+)$/;
|
|
3453
|
+
/**
|
|
3454
|
+
* Number elements e1, e2, … Elements without a selector (nothing to act on)
|
|
3455
|
+
* are still returned but get no ref.
|
|
3456
|
+
*/
|
|
3457
|
+
function assignRefs(elements, frame) {
|
|
3458
|
+
const byRef = {};
|
|
3459
|
+
let n = 0;
|
|
3460
|
+
return {
|
|
3461
|
+
elements: elements.map((el) => {
|
|
3462
|
+
if (!el.selector) return el;
|
|
3463
|
+
const ref = `e${++n}`;
|
|
3464
|
+
byRef[ref] = {
|
|
3465
|
+
selector: el.selector,
|
|
3466
|
+
fallbackSelectors: el.fallbackSelectors,
|
|
3467
|
+
frame
|
|
3468
|
+
};
|
|
3469
|
+
return {
|
|
3470
|
+
...el,
|
|
3471
|
+
ref
|
|
3472
|
+
};
|
|
3473
|
+
}),
|
|
3474
|
+
byRef
|
|
3475
|
+
};
|
|
3476
|
+
}
|
|
3477
|
+
/** A ref that was never assigned, or was taken on a different page. */
|
|
3478
|
+
var StaleRefError = class extends Error {
|
|
3479
|
+
constructor(ref) {
|
|
3480
|
+
super(`Unknown or stale ref "${ref}". List the page's elements again (agent elements / browser_get_elements) to get fresh refs.`);
|
|
3481
|
+
this.name = "StaleRefError";
|
|
3482
|
+
}
|
|
3483
|
+
};
|
|
3357
3484
|
// src/session/session-store.ts
|
|
3358
3485
|
var SessionStore = class {
|
|
3359
3486
|
filePath;
|
|
@@ -3398,12 +3525,16 @@ var SessionStore = class {
|
|
|
3398
3525
|
}
|
|
3399
3526
|
}
|
|
3400
3527
|
setLastUrl(sessionId, lastUrl) {
|
|
3528
|
+
this.update(sessionId, { lastUrl });
|
|
3529
|
+
}
|
|
3530
|
+
/** Merge fields into a stored record; `undefined` values clear the field. */
|
|
3531
|
+
update(sessionId, patch) {
|
|
3401
3532
|
const state = this.read();
|
|
3402
3533
|
const existing = state.sessions[sessionId];
|
|
3403
3534
|
if (!existing) throw new Error("Session not found in store");
|
|
3404
3535
|
state.sessions[sessionId] = {
|
|
3405
3536
|
...existing,
|
|
3406
|
-
|
|
3537
|
+
...patch
|
|
3407
3538
|
};
|
|
3408
3539
|
this.write(state);
|
|
3409
3540
|
}
|
|
@@ -3566,8 +3697,13 @@ var SessionManager = class {
|
|
|
3566
3697
|
}
|
|
3567
3698
|
throw new Error("Session is terminated. Start a new session.");
|
|
3568
3699
|
}
|
|
3569
|
-
async executeCommand(sessionId,
|
|
3700
|
+
async executeCommand(sessionId, rawInput) {
|
|
3570
3701
|
const record = await this.ensureSession(sessionId);
|
|
3702
|
+
const input = rawInput.type === "interact" ? {
|
|
3703
|
+
...rawInput,
|
|
3704
|
+
payload: await this.resolveRef(record, rawInput.payload)
|
|
3705
|
+
} : rawInput;
|
|
3706
|
+
const runId = this.trackIntentRun(sessionId, record, input);
|
|
3571
3707
|
const command = {
|
|
3572
3708
|
commandId: input.commandId,
|
|
3573
3709
|
sessionId,
|
|
@@ -3596,6 +3732,8 @@ var SessionManager = class {
|
|
|
3596
3732
|
const parsedPayload = InteractPayloadSchema.safeParse(input.payload);
|
|
3597
3733
|
if (!parsedPayload.success) throw new Error(`Invalid interact payload: ${parsedPayload.error.message}`);
|
|
3598
3734
|
resultMessage = `Interaction result: ${await this.browser.interact(record.targetWsUrl, parsedPayload.data)}`;
|
|
3735
|
+
const currentUrl = await this.browser.getCurrentUrl(record.targetWsUrl).catch(() => void 0);
|
|
3736
|
+
if (currentUrl) this.store.setLastUrl(sessionId, currentUrl);
|
|
3599
3737
|
} else if (input.type === "restart") {
|
|
3600
3738
|
await this.restartSession(sessionId);
|
|
3601
3739
|
resultMessage = "Session restarted";
|
|
@@ -3622,7 +3760,8 @@ var SessionManager = class {
|
|
|
3622
3760
|
expectedOutcome: resultMessage,
|
|
3623
3761
|
step: memoryContext.step,
|
|
3624
3762
|
selector: memoryContext.selector,
|
|
3625
|
-
url: memoryContext.url
|
|
3763
|
+
url: memoryContext.url,
|
|
3764
|
+
runId
|
|
3626
3765
|
});
|
|
3627
3766
|
else this.ctx.memoryService.recordFailure({
|
|
3628
3767
|
commandId: input.commandId,
|
|
@@ -3652,9 +3791,20 @@ var SessionManager = class {
|
|
|
3652
3791
|
}
|
|
3653
3792
|
return result;
|
|
3654
3793
|
}
|
|
3794
|
+
/** Lists interactive elements and assigns fresh refs (e1, e2, …) for this page. */
|
|
3655
3795
|
async getInteractiveElements(sessionId, options) {
|
|
3656
3796
|
const record = await this.ensureSession(sessionId);
|
|
3657
|
-
|
|
3797
|
+
const result = await this.browser.getInteractiveElements(record.targetWsUrl, options);
|
|
3798
|
+
const { elements, byRef } = assignRefs(result.elements, options.frame);
|
|
3799
|
+
const url = await this.browser.getCurrentUrl(record.targetWsUrl).catch(() => void 0);
|
|
3800
|
+
this.store.update(sessionId, { refs: {
|
|
3801
|
+
url,
|
|
3802
|
+
byRef
|
|
3803
|
+
} });
|
|
3804
|
+
return {
|
|
3805
|
+
...result,
|
|
3806
|
+
elements
|
|
3807
|
+
};
|
|
3658
3808
|
}
|
|
3659
3809
|
async screenshot(sessionId, options) {
|
|
3660
3810
|
const record = await this.ensureSession(sessionId);
|
|
@@ -3789,7 +3939,8 @@ var SessionManager = class {
|
|
|
3789
3939
|
session: restarted,
|
|
3790
3940
|
cdpUrl: relaunched.cdpUrl,
|
|
3791
3941
|
targetWsUrl: relaunched.targetWsUrl,
|
|
3792
|
-
pid: relaunched.pid
|
|
3942
|
+
pid: relaunched.pid,
|
|
3943
|
+
intentRun: record.intentRun
|
|
3793
3944
|
});
|
|
3794
3945
|
this.recordEvent(sessionId, "lifecycle", "info", "Session restarted");
|
|
3795
3946
|
return restarted;
|
|
@@ -3933,7 +4084,7 @@ var SessionManager = class {
|
|
|
3933
4084
|
const parsed = this.parseUrl(inputUrl);
|
|
3934
4085
|
const action = typeof input.payload.action === "string" ? input.payload.action : void 0;
|
|
3935
4086
|
const defaultIntent = input.type === "navigate" ? `navigate:${parsed.domain}` : input.type === "interact" ? `interact:${action ?? "action"}:${parsed.domain}` : `${input.type}:${parsed.domain}`;
|
|
3936
|
-
const taskIntent =
|
|
4087
|
+
const taskIntent = explicitIntent(input) ?? defaultIntent;
|
|
3937
4088
|
const expectedOutcome = typeof input.payload.expectedOutcome === "string" && input.payload.expectedOutcome.trim() ? input.payload.expectedOutcome : `${input.type} command succeeds`;
|
|
3938
4089
|
return {
|
|
3939
4090
|
taskIntent,
|
|
@@ -3950,6 +4101,36 @@ var SessionManager = class {
|
|
|
3950
4101
|
url: inputUrl
|
|
3951
4102
|
};
|
|
3952
4103
|
}
|
|
4104
|
+
async resolveRef(record, payload) {
|
|
4105
|
+
const selector = typeof payload.selector === "string" ? payload.selector : void 0;
|
|
4106
|
+
const ref = selector ? REF_PATTERN.exec(selector)?.[1] : void 0;
|
|
4107
|
+
if (!selector || !ref) return payload;
|
|
4108
|
+
const { refs } = record;
|
|
4109
|
+
const stored = refs?.byRef[ref];
|
|
4110
|
+
const onSamePage = !refs?.url || await this.browser.getCurrentUrl(record.targetWsUrl).catch(() => void 0) === refs.url;
|
|
4111
|
+
if (!stored || !onSamePage) throw new StaleRefError(selector);
|
|
4112
|
+
const extra = Array.isArray(payload.fallbackSelectors) ? payload.fallbackSelectors : [];
|
|
4113
|
+
return {
|
|
4114
|
+
...payload,
|
|
4115
|
+
selector: stored.selector,
|
|
4116
|
+
fallbackSelectors: [...stored.fallbackSelectors ?? [], ...extra],
|
|
4117
|
+
frame: payload.frame ?? stored.frame
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
/**
|
|
4121
|
+
* Consecutive commands with the same explicit intent form one run; any other
|
|
4122
|
+
* command ends it. Commands without an intent are each their own run.
|
|
4123
|
+
*/
|
|
4124
|
+
trackIntentRun(sessionId, record, input) {
|
|
4125
|
+
const intent = explicitIntent(input);
|
|
4126
|
+
if (intent && record.intentRun?.intent === intent) return record.intentRun.runId;
|
|
4127
|
+
const intentRun = intent ? {
|
|
4128
|
+
intent,
|
|
4129
|
+
runId: input.commandId
|
|
4130
|
+
} : void 0;
|
|
4131
|
+
if (intentRun || record.intentRun) this.store.update(sessionId, { intentRun });
|
|
4132
|
+
return input.commandId;
|
|
4133
|
+
}
|
|
3953
4134
|
parseUrl(rawUrl) {
|
|
3954
4135
|
if (!rawUrl) return {
|
|
3955
4136
|
domain: "unknown",
|
|
@@ -3960,7 +4141,7 @@ var SessionManager = class {
|
|
|
3960
4141
|
const firstSegment = parsed.pathname.split("/").filter(Boolean)[0];
|
|
3961
4142
|
const pathPattern = firstSegment ? `/${firstSegment}/*` : "/";
|
|
3962
4143
|
return {
|
|
3963
|
-
domain: parsed.hostname.toLowerCase(),
|
|
4144
|
+
domain: parsed.hostname.toLowerCase() || parsed.protocol.replace(/:$/, ""),
|
|
3964
4145
|
pathPattern
|
|
3965
4146
|
};
|
|
3966
4147
|
} catch {
|
|
@@ -3971,6 +4152,11 @@ var SessionManager = class {
|
|
|
3971
4152
|
}
|
|
3972
4153
|
}
|
|
3973
4154
|
};
|
|
4155
|
+
/** The caller's task intent, if any. payload.intent is the older spelling, still honored. */
|
|
4156
|
+
function explicitIntent(input) {
|
|
4157
|
+
const raw = input.intent ?? input.payload.intent;
|
|
4158
|
+
return typeof raw === "string" && raw.trim() ? raw.trim() : void 0;
|
|
4159
|
+
}
|
|
3974
4160
|
// src/transport/control-api.ts
|
|
3975
4161
|
var ControlApi = class {
|
|
3976
4162
|
sessions;
|
|
@@ -4210,6 +4396,61 @@ var SessionTokenService = class {
|
|
|
4210
4396
|
function normalize(value) {
|
|
4211
4397
|
return value.trim().toLowerCase();
|
|
4212
4398
|
}
|
|
4399
|
+
const STOPWORDS = /* @__PURE__ */ new Set([
|
|
4400
|
+
"a",
|
|
4401
|
+
"an",
|
|
4402
|
+
"the",
|
|
4403
|
+
"to",
|
|
4404
|
+
"on",
|
|
4405
|
+
"in",
|
|
4406
|
+
"of",
|
|
4407
|
+
"and",
|
|
4408
|
+
"or",
|
|
4409
|
+
"for",
|
|
4410
|
+
"with",
|
|
4411
|
+
"at",
|
|
4412
|
+
"my",
|
|
4413
|
+
"from",
|
|
4414
|
+
"by",
|
|
4415
|
+
"into"
|
|
4416
|
+
]);
|
|
4417
|
+
/**
|
|
4418
|
+
* Words of an intent, minus the site domain and filler words, with a trailing
|
|
4419
|
+
* plural "s" dropped — so "open the newest stories" and "newest story" share
|
|
4420
|
+
* tokens.
|
|
4421
|
+
*/
|
|
4422
|
+
function intentTokens(value, domain) {
|
|
4423
|
+
let text = normalize(value);
|
|
4424
|
+
if (domain) text = text.split(normalize(domain)).join(" ");
|
|
4425
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
4426
|
+
for (const raw of text.split(/[^\p{L}\p{N}]+/u)) {
|
|
4427
|
+
if (raw.length < 2 || STOPWORDS.has(raw)) continue;
|
|
4428
|
+
tokens.add(raw.length > 3 && raw.endsWith("s") ? raw.slice(0, -1) : raw);
|
|
4429
|
+
}
|
|
4430
|
+
return tokens;
|
|
4431
|
+
}
|
|
4432
|
+
/** 0 = unrelated; 1 = identical intent. Word overlap covers rephrased queries. */
|
|
4433
|
+
function intentSimilarity(insight, normalizedIntent, query) {
|
|
4434
|
+
const insightIntent = normalize(insight.taskIntent);
|
|
4435
|
+
if (insightIntent === normalizedIntent) return 1;
|
|
4436
|
+
if (insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent)) return .65;
|
|
4437
|
+
if (!query.size) return 0;
|
|
4438
|
+
const stored = intentTokens(insightIntent, insight.siteDomain);
|
|
4439
|
+
let shared = 0;
|
|
4440
|
+
for (const token of query) if (stored.has(token)) shared += 1;
|
|
4441
|
+
if (!shared) return 0;
|
|
4442
|
+
return .3 + .5 * (shared / query.size);
|
|
4443
|
+
}
|
|
4444
|
+
function buildRecipe(insight) {
|
|
4445
|
+
return insight.actionRecipe.map((step) => {
|
|
4446
|
+
const payload = step.payload ?? {};
|
|
4447
|
+
const summary = { action: typeof payload.action === "string" ? payload.action : step.type };
|
|
4448
|
+
if (step.selector) summary.selector = step.selector;
|
|
4449
|
+
if (typeof payload.url === "string") summary.url = payload.url;
|
|
4450
|
+
if (payload.redacted === true) summary.needsInput = true;
|
|
4451
|
+
return summary;
|
|
4452
|
+
});
|
|
4453
|
+
}
|
|
4213
4454
|
function freshnessWeight(freshness) {
|
|
4214
4455
|
if (freshness === "fresh") return 1;
|
|
4215
4456
|
if (freshness === "suspect") return .65;
|
|
@@ -4242,15 +4483,19 @@ function selectorSignal(insight) {
|
|
|
4242
4483
|
const evidenceStrength = Math.min(selectorEvidence / 5, 1);
|
|
4243
4484
|
return .7 * recipeCoverage + .3 * evidenceStrength;
|
|
4244
4485
|
}
|
|
4245
|
-
function scoreInsight(insight, normalizedIntent,
|
|
4246
|
-
const
|
|
4247
|
-
const intentMatch = insightIntent === normalizedIntent ? 1 : 0;
|
|
4248
|
-
const intentPartial = intentMatch === 1 || insightIntent.includes(normalizedIntent) || normalizedIntent.includes(insightIntent) ? .65 : 0;
|
|
4249
|
-
const intentSignal = Math.max(intentMatch, intentPartial);
|
|
4486
|
+
function scoreInsight(insight, normalizedIntent, queryTokens, normalizedDomain) {
|
|
4487
|
+
const intentSignal = intentSimilarity(insight, normalizedIntent, queryTokens);
|
|
4250
4488
|
const domainMatch = normalizedDomain && normalize(insight.siteDomain) === normalizedDomain ? 1 : normalizedDomain ? 0 : .6;
|
|
4251
4489
|
const reliability = .6 * confidenceFromCounts(insight.successCount, insight.failureCount) + .4 * freshnessWeight(insight.freshness);
|
|
4252
4490
|
const selectorQuality = selectorSignal(insight);
|
|
4253
|
-
|
|
4491
|
+
return {
|
|
4492
|
+
insight,
|
|
4493
|
+
score: .5 * intentSignal + .2 * domainMatch + .15 * reliability + .15 * selectorQuality,
|
|
4494
|
+
intentSignal
|
|
4495
|
+
};
|
|
4496
|
+
}
|
|
4497
|
+
/** Builds the full result (recipe, hints, profile) — only for insights that are returned. */
|
|
4498
|
+
function toSearchResult({ insight, score }, profileLookup) {
|
|
4254
4499
|
const result = {
|
|
4255
4500
|
insightId: insight.insightId,
|
|
4256
4501
|
taskIntent: insight.taskIntent,
|
|
@@ -4258,6 +4503,7 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
4258
4503
|
confidence: insight.confidence,
|
|
4259
4504
|
freshness: insight.freshness,
|
|
4260
4505
|
lastVerifiedAt: insight.lastVerifiedAt,
|
|
4506
|
+
recipe: buildRecipe(insight),
|
|
4261
4507
|
selectorHints: buildSelectorHints(insight),
|
|
4262
4508
|
selectorAliases: buildSelectorAliases(insight),
|
|
4263
4509
|
score
|
|
@@ -4269,10 +4515,7 @@ function scoreInsight(insight, normalizedIntent, normalizedDomain, profileLookup
|
|
|
4269
4515
|
cookieBanner: profile.cookieBanner,
|
|
4270
4516
|
visitCount: profile.visitCount
|
|
4271
4517
|
};
|
|
4272
|
-
return
|
|
4273
|
-
result,
|
|
4274
|
-
intentSignal
|
|
4275
|
-
};
|
|
4518
|
+
return result;
|
|
4276
4519
|
}
|
|
4277
4520
|
var MemoryIndex = class {
|
|
4278
4521
|
/** Domain → insights index, rebuilt lazily when insight list changes. */
|
|
@@ -4295,6 +4538,7 @@ var MemoryIndex = class {
|
|
|
4295
4538
|
}
|
|
4296
4539
|
search(insights, input, profileLookup) {
|
|
4297
4540
|
const normalizedIntent = normalize(input.taskIntent);
|
|
4541
|
+
const queryTokens = intentTokens(normalizedIntent);
|
|
4298
4542
|
const normalizedDomain = input.siteDomain ? normalize(input.siteDomain) : void 0;
|
|
4299
4543
|
const limit = input.limit ?? 10;
|
|
4300
4544
|
this.ensureIndex(insights);
|
|
@@ -4303,16 +4547,16 @@ var MemoryIndex = class {
|
|
|
4303
4547
|
if (limit === 1 && normalizedDomain) {
|
|
4304
4548
|
let best;
|
|
4305
4549
|
for (const insight of candidates) {
|
|
4306
|
-
const scored = scoreInsight(insight, normalizedIntent,
|
|
4550
|
+
const scored = scoreInsight(insight, normalizedIntent, queryTokens, normalizedDomain);
|
|
4307
4551
|
if (!relevant(scored)) continue;
|
|
4308
|
-
if (!best || scored.
|
|
4552
|
+
if (!best || scored.score > best.score) {
|
|
4309
4553
|
best = scored;
|
|
4310
|
-
if (best.
|
|
4554
|
+
if (best.score >= 1) break;
|
|
4311
4555
|
}
|
|
4312
4556
|
}
|
|
4313
|
-
return best ? [best
|
|
4557
|
+
return best ? [toSearchResult(best, profileLookup)] : [];
|
|
4314
4558
|
}
|
|
4315
|
-
return candidates.map((insight) => scoreInsight(insight, normalizedIntent,
|
|
4559
|
+
return candidates.map((insight) => scoreInsight(insight, normalizedIntent, queryTokens, normalizedDomain)).filter(relevant).toSorted((a, b) => b.score - a.score).slice(0, limit).map((scored) => toSearchResult(scored, profileLookup));
|
|
4316
4560
|
}
|
|
4317
4561
|
};
|
|
4318
4562
|
// src/memory/staleness-detector.ts
|
|
@@ -4456,6 +4700,8 @@ const TaskInsightSchema = z.object({
|
|
|
4456
4700
|
siteDomain: z.string().min(1),
|
|
4457
4701
|
sitePathPattern: z.string().min(1),
|
|
4458
4702
|
actionRecipe: z.array(TaskStepSchema),
|
|
4703
|
+
/** Run that produced actionRecipe; steps from another run start it over. */
|
|
4704
|
+
recipeRunId: z.string().optional(),
|
|
4459
4705
|
expectedOutcome: z.string().min(1),
|
|
4460
4706
|
confidence: z.number().min(0).max(1),
|
|
4461
4707
|
successCount: z.number().int().nonnegative(),
|
|
@@ -4870,6 +5116,7 @@ var MemoryService = class {
|
|
|
4870
5116
|
siteDomain: input.siteDomain,
|
|
4871
5117
|
sitePathPattern: input.sitePathPattern,
|
|
4872
5118
|
actionRecipe: [input.step],
|
|
5119
|
+
recipeRunId: input.runId ?? input.commandId,
|
|
4873
5120
|
expectedOutcome: input.expectedOutcome,
|
|
4874
5121
|
confidence: 1,
|
|
4875
5122
|
successCount: 1,
|
|
@@ -4892,7 +5139,7 @@ var MemoryService = class {
|
|
|
4892
5139
|
...matched,
|
|
4893
5140
|
useCount: matched.useCount + 1,
|
|
4894
5141
|
evidence: [...matched.evidence.slice(-49), evidence],
|
|
4895
|
-
|
|
5142
|
+
...this.mergeRecipe(matched, input.step, input.runId ?? input.commandId),
|
|
4896
5143
|
expectedOutcome: input.expectedOutcome
|
|
4897
5144
|
});
|
|
4898
5145
|
if (matched.freshness === "suspect" || matched.freshness === "stale") {
|
|
@@ -5101,9 +5348,16 @@ var MemoryService = class {
|
|
|
5101
5348
|
const testMatch = selector.match(/\[data-test="([^"]+)"\]/);
|
|
5102
5349
|
if (testMatch) return testMatch[1];
|
|
5103
5350
|
}
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5351
|
+
/** Same run: append the step. New run: its first step replaces the old recipe. */
|
|
5352
|
+
mergeRecipe(insight, step, runId) {
|
|
5353
|
+
if (insight.recipeRunId !== runId) return {
|
|
5354
|
+
actionRecipe: [step],
|
|
5355
|
+
recipeRunId: runId
|
|
5356
|
+
};
|
|
5357
|
+
return {
|
|
5358
|
+
actionRecipe: [...insight.actionRecipe, step].slice(-8),
|
|
5359
|
+
recipeRunId: runId
|
|
5360
|
+
};
|
|
5107
5361
|
}
|
|
5108
5362
|
};
|
|
5109
5363
|
// src/cli/app.ts
|
|
@@ -5153,7 +5407,8 @@ var AgenticBrowserCore = class {
|
|
|
5153
5407
|
return await this.api.executeCommand(input.sessionId, {
|
|
5154
5408
|
commandId,
|
|
5155
5409
|
type: input.type,
|
|
5156
|
-
payload: input.payload
|
|
5410
|
+
payload: input.payload,
|
|
5411
|
+
intent: input.intent
|
|
5157
5412
|
});
|
|
5158
5413
|
}
|
|
5159
5414
|
async getPageContent(input) {
|
|
@@ -5230,4 +5485,4 @@ function createMockAgenticBrowserCore(env) {
|
|
|
5230
5485
|
function createCliRuntime() {
|
|
5231
5486
|
return createAgenticBrowserCore();
|
|
5232
5487
|
}
|
|
5233
|
-
export {
|
|
5488
|
+
export { StaleRefError as a, writePrivateFileSync as c, createMockAgenticBrowserCore as i, createAgenticBrowserCore as n, isConnectionError as o, createCliRuntime as r, ensurePrivateDir as s, AgenticBrowserCore as t };
|