cbrowser 20.1.0 → 20.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analysis/persona-comparison.d.ts.map +1 -1
- package/dist/analysis/persona-comparison.js +3 -2
- package/dist/analysis/persona-comparison.js.map +1 -1
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +39 -4
- package/dist/cli-args.js.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +1 -1
- package/dist/cognitive/index.d.ts +42 -0
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +122 -25
- package/dist/cognitive/index.js.map +1 -1
- package/dist/cognitive/journey-dialog.d.ts +7 -0
- package/dist/cognitive/journey-dialog.d.ts.map +1 -1
- package/dist/cognitive/journey-dialog.js +14 -1
- package/dist/cognitive/journey-dialog.js.map +1 -1
- package/dist/cognitive/journey-trace.d.ts +2 -0
- package/dist/cognitive/journey-trace.d.ts.map +1 -1
- package/dist/cognitive/journey-trace.js.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/browser-state-tools.js +10 -1
- package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +28 -2
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/journey-sessions.d.ts +28 -0
- package/dist/mcp-tools/journey-sessions.d.ts.map +1 -0
- package/dist/mcp-tools/journey-sessions.js +38 -0
- package/dist/mcp-tools/journey-sessions.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
* Copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com
|
|
4
4
|
* Learn more at https://cbrowser.ai - MIT License
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Cognitive Journey API Module
|
|
8
|
+
*
|
|
9
|
+
* Provides cognitive user simulation with two modes:
|
|
10
|
+
*
|
|
11
|
+
* ## Mode 1: Claude Code Session (Recommended when inside Claude Code)
|
|
12
|
+
* When running via MCP inside Claude Code, use the step-by-step tools:
|
|
13
|
+
* - `cognitive_journey_init` - Initialize journey with persona
|
|
14
|
+
* - `cognitive_journey_update_state` - Track cognitive state after each action
|
|
15
|
+
* No API key needed - Claude Code itself is the reasoning engine.
|
|
16
|
+
*
|
|
17
|
+
* ## Mode 2: Standalone (API Key Required)
|
|
18
|
+
* When running standalone (CLI, CI/CD, scripts), uses Anthropic API directly:
|
|
19
|
+
* - `npx cbrowser cognitive-journey --persona X --start URL --goal "..."
|
|
20
|
+
* Requires: `npx cbrowser config set-api-key YOUR_KEY`
|
|
21
|
+
*
|
|
22
|
+
* @module cognitive
|
|
23
|
+
*/
|
|
24
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
6
25
|
import { type AutoCaptureSetting } from "../recording/auto-capture.js";
|
|
7
26
|
import type { CognitiveState, CognitiveJourneyResult, CognitiveTraits } from "../types.js";
|
|
8
27
|
import type { DecisionProvider } from "./journey-trace.js";
|
|
@@ -39,6 +58,8 @@ export declare function isCognitiveAvailable(): boolean;
|
|
|
39
58
|
export interface CognitiveJourneyOptions {
|
|
40
59
|
/** Persona name or description */
|
|
41
60
|
persona: string;
|
|
61
|
+
/** A label for this run (the CLI's first word). Names its trace and capture files and is returned on the result. */
|
|
62
|
+
name?: string;
|
|
42
63
|
/** Goal statement for the journey */
|
|
43
64
|
goal: string;
|
|
44
65
|
/** Starting URL */
|
|
@@ -177,6 +198,8 @@ export interface CognitiveStep {
|
|
|
177
198
|
* not anything the model said.
|
|
178
199
|
*/
|
|
179
200
|
replyReadable: boolean;
|
|
201
|
+
/** Why the reply could not be read, when replyReadable is false. */
|
|
202
|
+
replyProblem?: string;
|
|
180
203
|
state: CognitiveState;
|
|
181
204
|
/** Step-aligned screenshot. Captured only on initial page load + scroll>100. */
|
|
182
205
|
screenshotPath?: string;
|
|
@@ -193,6 +216,25 @@ export interface CognitiveStep {
|
|
|
193
216
|
url?: string;
|
|
194
217
|
}
|
|
195
218
|
export declare function runCognitiveJourney(options: CognitiveJourneyOptions): Promise<CognitiveJourneyResult>;
|
|
219
|
+
/**
|
|
220
|
+
* The live per-step decision: one messages.create call, its text blocks read
|
|
221
|
+
* together, and one more ask for the JSON alone when the reply does not hold
|
|
222
|
+
* a JSON object. describeLastReply says why the last reply was unreadable.
|
|
223
|
+
* Exported for tests, which pass a fake client.
|
|
224
|
+
*/
|
|
225
|
+
export declare function createLiveDecisionProvider(anthropic: Pick<Anthropic, "messages"> | null, model: string): DecisionProvider;
|
|
226
|
+
/** A journey or persona name as a file-name part: letters, digits, - and _ only. */
|
|
227
|
+
export declare function journeySlug(name: string): string;
|
|
228
|
+
/**
|
|
229
|
+
* The JSON object in a model reply, or why there is none. Tried in order: the
|
|
230
|
+
* reply with any code fences removed, each balanced {...} in it (string-aware,
|
|
231
|
+
* so a reply with prose after its JSON still reads), then the old greedy
|
|
232
|
+
* first-"{"-to-last-"}" match; the first that parses to an object wins.
|
|
233
|
+
*/
|
|
234
|
+
export declare function readReplyJson(reply: string): {
|
|
235
|
+
value?: Record<string, unknown>;
|
|
236
|
+
problem?: string;
|
|
237
|
+
};
|
|
196
238
|
export { createInitialEmotionalState, createEmotionalConfig, applyEmotionalTrigger, decayEmotions, calculateAbandonmentModifier, calculateExplorationTendency, calculateDecisionSpeedModifier, describeEmotionalState, shouldConsiderAbandonment, } from "./emotions.js";
|
|
197
239
|
export { GoalDecomposer } from "./goal-decomposer.js";
|
|
198
240
|
export type { ParsedGoal, GoalPlan, SubGoal, Strategy, PlannedAction, GoalResult, ExecutionOptions, } from "./goal-types.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cognitive/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cognitive/index.ts"],"names":[],"mappings":"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"}
|
package/dist/cognitive/index.js
CHANGED
|
@@ -693,25 +693,16 @@ export async function runCognitiveJourney(options) {
|
|
|
693
693
|
// Decision-provider seam (v18.68.0): the per-step LLM call sits behind a
|
|
694
694
|
// DecisionProvider so a recorded trace (or an injected fake) can stand in
|
|
695
695
|
// for the live API. The default path is the same anthropic.messages.create
|
|
696
|
-
// call as before
|
|
696
|
+
// call as before, reading every text block (20.1.2). The post-journey
|
|
697
697
|
// goal evaluation goes through the same provider (FINAL_EVAL_STEP), so a
|
|
698
698
|
// replay reproduces the recorded verdict with no model call.
|
|
699
|
-
const liveLLMProvider =
|
|
700
|
-
decide: async (ctx) => {
|
|
701
|
-
if (!anthropic)
|
|
702
|
-
throw new Error("Anthropic API key not configured.");
|
|
703
|
-
const response = await anthropic.messages.create({
|
|
704
|
-
model,
|
|
705
|
-
max_tokens: 2000,
|
|
706
|
-
...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
|
|
707
|
-
messages: ctx.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
708
|
-
});
|
|
709
|
-
return response.content[0].type === "text" ? response.content[0].text : "";
|
|
710
|
-
},
|
|
711
|
-
};
|
|
699
|
+
const liveLLMProvider = createLiveDecisionProvider(anthropic, model);
|
|
712
700
|
const decisionProvider = options.decisionProvider ??
|
|
713
701
|
(options.replayTrace ? new TraceReplayProvider(options.replayTrace) : liveLLMProvider);
|
|
714
|
-
const
|
|
702
|
+
const nameSlug = options.name ? journeySlug(options.name) : undefined;
|
|
703
|
+
const traceWriter = options.trace
|
|
704
|
+
? new JourneyTraceWriter(nameSlug ? { journeyId: `journey-${nameSlug}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` } : undefined)
|
|
705
|
+
: null;
|
|
715
706
|
// Get or create persona
|
|
716
707
|
const existingPersona = getPersona(options.persona);
|
|
717
708
|
let personaObj;
|
|
@@ -990,7 +981,7 @@ export async function runCognitiveJourney(options) {
|
|
|
990
981
|
// Auto-capture (v18.70.0): recording starts once the journey's start URL is
|
|
991
982
|
// loaded, so frame 0 is the page the persona actually opens on, and stops
|
|
992
983
|
// just before the browser closes. Capture never fails the journey.
|
|
993
|
-
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${options.persona
|
|
984
|
+
const captureSession = await startAutoCapture(await browser.getPage(), options.capture, `journey-${nameSlug ?? journeySlug(options.persona)}-${Date.now()}`);
|
|
994
985
|
// Apply geolocation at runtime as well (ensures permission is granted after context exists)
|
|
995
986
|
if (effectiveLocation.geolocation) {
|
|
996
987
|
await browser.setGeolocationRuntime(effectiveLocation.geolocation);
|
|
@@ -1326,6 +1317,9 @@ Right now it's ${temporalState.description}.`;
|
|
|
1326
1317
|
// action); say so rather than let it pass as an "evaluate" step.
|
|
1327
1318
|
const parsed = parseCognitiveResponse(assistantMessage);
|
|
1328
1319
|
const replyReadable = Object.keys(parsed).length > 0;
|
|
1320
|
+
const replyProblem = replyReadable
|
|
1321
|
+
? undefined
|
|
1322
|
+
: decisionProvider.describeLastReply?.() ?? readReplyJson(assistantMessage).problem;
|
|
1329
1323
|
// Capture URL BEFORE we apply any action. We use the previous URL +
|
|
1330
1324
|
// the post-action URL to gate goal progress on real evidence — Claude's
|
|
1331
1325
|
// self-reported progress is taken as a *signal*, but only allowed to
|
|
@@ -1549,6 +1543,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1549
1543
|
pageTitle: pageTitle,
|
|
1550
1544
|
phase: parsed.phase || "evaluate",
|
|
1551
1545
|
replyReadable,
|
|
1546
|
+
...(replyProblem ? { replyProblem } : {}),
|
|
1552
1547
|
action: parsed.action || null,
|
|
1553
1548
|
actionTarget: parsed.actionTarget || null,
|
|
1554
1549
|
focusedElement: parsed.focusedElement || null,
|
|
@@ -1586,6 +1581,7 @@ Right now it's ${temporalState.description}.`;
|
|
|
1586
1581
|
action: parsed.action,
|
|
1587
1582
|
actionTarget: parsed.actionTarget || undefined,
|
|
1588
1583
|
replyReadable,
|
|
1584
|
+
...(replyProblem ? { replyProblem } : {}),
|
|
1589
1585
|
state: { ...state },
|
|
1590
1586
|
screenshotPath,
|
|
1591
1587
|
attentionHeatmapBase64,
|
|
@@ -1926,7 +1922,7 @@ Answer with JSON:
|
|
|
1926
1922
|
});
|
|
1927
1923
|
}
|
|
1928
1924
|
try {
|
|
1929
|
-
const evalJson =
|
|
1925
|
+
const evalJson = (readReplyJson(evalText).value ?? {});
|
|
1930
1926
|
if (evalJson.achieved) {
|
|
1931
1927
|
goalAchieved = true;
|
|
1932
1928
|
goalEvidence = `[FINAL EVAL] ${evalJson.evidence || finalPageText.substring(0, 200)}`;
|
|
@@ -1960,6 +1956,7 @@ Answer with JSON:
|
|
|
1960
1956
|
: state.confusionLevel;
|
|
1961
1957
|
// Build result
|
|
1962
1958
|
return {
|
|
1959
|
+
...(options.name ? { name: options.name } : {}),
|
|
1963
1960
|
persona: personaObj.name,
|
|
1964
1961
|
goal: options.goal,
|
|
1965
1962
|
goalAchieved,
|
|
@@ -2230,18 +2227,118 @@ STRATEGY:
|
|
|
2230
2227
|
|
|
2231
2228
|
Respond in JSON format.`;
|
|
2232
2229
|
}
|
|
2230
|
+
/**
|
|
2231
|
+
* The live per-step decision: one messages.create call, its text blocks read
|
|
2232
|
+
* together, and one more ask for the JSON alone when the reply does not hold
|
|
2233
|
+
* a JSON object. describeLastReply says why the last reply was unreadable.
|
|
2234
|
+
* Exported for tests, which pass a fake client.
|
|
2235
|
+
*/
|
|
2236
|
+
export function createLiveDecisionProvider(anthropic, model) {
|
|
2237
|
+
// Why the last live reply could not be read; the step reports it as
|
|
2238
|
+
// replyProblem. Undefined after a readable reply.
|
|
2239
|
+
let lastLiveProblem;
|
|
2240
|
+
return {
|
|
2241
|
+
decide: async (ctx) => {
|
|
2242
|
+
if (!anthropic)
|
|
2243
|
+
throw new Error("Anthropic API key not configured.");
|
|
2244
|
+
const ask = (messages, maxTokens = 2000) => anthropic.messages.create({
|
|
2245
|
+
model,
|
|
2246
|
+
max_tokens: maxTokens,
|
|
2247
|
+
...(ctx.systemPrompt ? { system: ctx.systemPrompt } : {}),
|
|
2248
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
2249
|
+
});
|
|
2250
|
+
const textOf = (r) => r.content.map((b) => (b.type === "text" ? b.text : "")).join("");
|
|
2251
|
+
const whyUnreadable = (r, text) => text.trim()
|
|
2252
|
+
? readReplyJson(text).problem
|
|
2253
|
+
: `the model returned no text (stop_reason ${r.stop_reason ?? "none"}; content blocks: ${r.content.map((b) => b.type).join(", ") || "none"})`;
|
|
2254
|
+
lastLiveProblem = undefined;
|
|
2255
|
+
const first = await ask(ctx.messages);
|
|
2256
|
+
const firstText = textOf(first);
|
|
2257
|
+
if (readReplyJson(firstText).value)
|
|
2258
|
+
return firstText;
|
|
2259
|
+
// Asked once more, for the JSON alone. Inside the provider, so a trace
|
|
2260
|
+
// records one decision per step and still replays.
|
|
2261
|
+
// Thinking counts against max_tokens, so a reply cut off there may hold
|
|
2262
|
+
// no text at all; the second ask gets room for both.
|
|
2263
|
+
const firstProblem = whyUnreadable(first, firstText);
|
|
2264
|
+
const retry = await ask([
|
|
2265
|
+
...ctx.messages,
|
|
2266
|
+
{ role: "assistant", content: firstText.trim() || "(no text)" },
|
|
2267
|
+
{ role: "user", content: "Your reply could not be read as the JSON object the response format asks for. Reply again with only that JSON object, nothing before or after it." },
|
|
2268
|
+
], first.stop_reason === "max_tokens" ? 8000 : 2000);
|
|
2269
|
+
const retryText = textOf(retry);
|
|
2270
|
+
if (readReplyJson(retryText).value)
|
|
2271
|
+
return retryText;
|
|
2272
|
+
lastLiveProblem = `${firstProblem}; asked once more, and ${whyUnreadable(retry, retryText)}`;
|
|
2273
|
+
return retryText;
|
|
2274
|
+
},
|
|
2275
|
+
describeLastReply: () => lastLiveProblem,
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
/** A journey or persona name as a file-name part: letters, digits, - and _ only. */
|
|
2279
|
+
export function journeySlug(name) {
|
|
2280
|
+
return name.replace(/[^a-zA-Z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "journey";
|
|
2281
|
+
}
|
|
2233
2282
|
function parseCognitiveResponse(response) {
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2283
|
+
return (readReplyJson(response).value ?? {});
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* The JSON object in a model reply, or why there is none. Tried in order: the
|
|
2287
|
+
* reply with any code fences removed, each balanced {...} in it (string-aware,
|
|
2288
|
+
* so a reply with prose after its JSON still reads), then the old greedy
|
|
2289
|
+
* first-"{"-to-last-"}" match; the first that parses to an object wins.
|
|
2290
|
+
*/
|
|
2291
|
+
export function readReplyJson(reply) {
|
|
2292
|
+
const text = reply.trim();
|
|
2293
|
+
if (!text)
|
|
2294
|
+
return { problem: "the model returned no text" };
|
|
2295
|
+
const unfenced = text.replace(/```(?:json)?/gi, "").trim();
|
|
2296
|
+
const candidates = [unfenced];
|
|
2297
|
+
for (let i = 0; i < unfenced.length; i++) {
|
|
2298
|
+
if (unfenced[i] !== "{")
|
|
2299
|
+
continue;
|
|
2300
|
+
let depth = 0;
|
|
2301
|
+
let inString = false;
|
|
2302
|
+
let escaped = false;
|
|
2303
|
+
for (let j = i; j < unfenced.length; j++) {
|
|
2304
|
+
const c = unfenced[j];
|
|
2305
|
+
if (inString) {
|
|
2306
|
+
if (escaped)
|
|
2307
|
+
escaped = false;
|
|
2308
|
+
else if (c === "\\")
|
|
2309
|
+
escaped = true;
|
|
2310
|
+
else if (c === '"')
|
|
2311
|
+
inString = false;
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (c === '"')
|
|
2315
|
+
inString = true;
|
|
2316
|
+
else if (c === "{")
|
|
2317
|
+
depth++;
|
|
2318
|
+
else if (c === "}" && --depth === 0) {
|
|
2319
|
+
candidates.push(unfenced.slice(i, j + 1));
|
|
2320
|
+
i = j;
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2239
2323
|
}
|
|
2240
2324
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2325
|
+
const greedy = unfenced.match(/\{[\s\S]*\}/);
|
|
2326
|
+
if (greedy)
|
|
2327
|
+
candidates.push(greedy[0]);
|
|
2328
|
+
let firstError = "";
|
|
2329
|
+
for (const candidate of candidates) {
|
|
2330
|
+
try {
|
|
2331
|
+
const value = JSON.parse(candidate);
|
|
2332
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
2333
|
+
return { value };
|
|
2334
|
+
}
|
|
2335
|
+
catch (e) {
|
|
2336
|
+
if (!firstError)
|
|
2337
|
+
firstError = e.message;
|
|
2338
|
+
}
|
|
2243
2339
|
}
|
|
2244
|
-
|
|
2340
|
+
const start = text.slice(0, 80).replace(/\s+/g, " ");
|
|
2341
|
+
return { problem: `the reply was not a JSON object (${firstError || "no {...} in it"}); it began: ${start}${text.length > 80 ? "..." : ""}` };
|
|
2245
2342
|
}
|
|
2246
2343
|
function checkAbandonmentTriggers(state, thresholds) {
|
|
2247
2344
|
if (state.patienceRemaining < thresholds.patienceMin) {
|