qazen-cli 0.2.0 → 0.2.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/lib/visionNavigator.js +104 -4
- package/package.json +1 -1
|
@@ -10,6 +10,76 @@ function firstTextBlock(content) {
|
|
|
10
10
|
const block = content[0];
|
|
11
11
|
return block && block.type === "text" ? block.text : "";
|
|
12
12
|
}
|
|
13
|
+
function salvageJson(text) {
|
|
14
|
+
const cleaned = stripCodeFences(text);
|
|
15
|
+
// First try clean parse
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(cleaned);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Fall through to salvage
|
|
21
|
+
}
|
|
22
|
+
// Salvage truncated JSON: extract metadata + every complete {...} element block.
|
|
23
|
+
try {
|
|
24
|
+
const descMatch = cleaned.match(/"pageDescription"\s*:\s*"([^"]+)"/);
|
|
25
|
+
const workflowMatch = cleaned.match(/"workflow"\s*:\s*"([^"]+)"/);
|
|
26
|
+
const pageTypeMatch = cleaned.match(/"pageType"\s*:\s*"([^"]+)"/);
|
|
27
|
+
const elements = [];
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let start = -1;
|
|
30
|
+
let inString = false;
|
|
31
|
+
let escape = false;
|
|
32
|
+
for (let i = 0; i < cleaned.length; i++) {
|
|
33
|
+
const ch = cleaned[i];
|
|
34
|
+
if (escape) {
|
|
35
|
+
escape = false;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (ch === "\\") {
|
|
39
|
+
escape = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ch === '"') {
|
|
43
|
+
inString = !inString;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (inString)
|
|
47
|
+
continue;
|
|
48
|
+
if (ch === "{") {
|
|
49
|
+
if (depth === 0)
|
|
50
|
+
start = i;
|
|
51
|
+
depth++;
|
|
52
|
+
}
|
|
53
|
+
else if (ch === "}") {
|
|
54
|
+
depth--;
|
|
55
|
+
if (depth === 0 && start !== -1) {
|
|
56
|
+
try {
|
|
57
|
+
const obj = JSON.parse(cleaned.substring(start, i + 1));
|
|
58
|
+
if (typeof obj.description === "string" &&
|
|
59
|
+
typeof obj.elementType === "string") {
|
|
60
|
+
elements.push(obj);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// skip incomplete object
|
|
65
|
+
}
|
|
66
|
+
start = -1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (elements.length === 0)
|
|
71
|
+
return null;
|
|
72
|
+
return {
|
|
73
|
+
pageDescription: descMatch?.[1] ?? "Page analysis (partial)",
|
|
74
|
+
workflow: workflowMatch?.[1] ?? "Unknown",
|
|
75
|
+
pageType: pageTypeMatch?.[1] ?? "other",
|
|
76
|
+
elements,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
13
83
|
export class VisionNavigator {
|
|
14
84
|
anthropic;
|
|
15
85
|
baseUrl;
|
|
@@ -152,11 +222,31 @@ export class VisionNavigator {
|
|
|
152
222
|
});
|
|
153
223
|
}
|
|
154
224
|
}
|
|
225
|
+
// Debug: log every elementType:priority Claude returned, so we can see
|
|
226
|
+
// why the highPriority filter may be excluding everything.
|
|
227
|
+
const elementTypes = analysis.elements
|
|
228
|
+
.map((e) => `${e.elementType}:${e.priority}`)
|
|
229
|
+
.join(", ");
|
|
230
|
+
onEvent({
|
|
231
|
+
type: "vision_analysis",
|
|
232
|
+
message: `Element types found: ${elementTypes}`,
|
|
233
|
+
});
|
|
234
|
+
const NAV_TYPES = ["link", "tab", "menu"];
|
|
235
|
+
const NAV_TYPES_EXPANDED = [...NAV_TYPES, "button", "other"];
|
|
155
236
|
const highPriority = analysis.elements
|
|
156
237
|
.filter((e) => {
|
|
157
238
|
if (e.priority !== "high")
|
|
158
239
|
return false;
|
|
159
|
-
|
|
240
|
+
const desc = e.description.toLowerCase();
|
|
241
|
+
const isNavElement = desc.includes("navigation") ||
|
|
242
|
+
desc.includes("menu") ||
|
|
243
|
+
desc.includes("nav link") ||
|
|
244
|
+
desc.includes("sidebar");
|
|
245
|
+
// Allow strict nav types, OR an expanded type set when the
|
|
246
|
+
// description itself signals navigation intent.
|
|
247
|
+
if (!isNavElement && !NAV_TYPES.includes(e.elementType))
|
|
248
|
+
return false;
|
|
249
|
+
if (!NAV_TYPES_EXPANDED.includes(e.elementType))
|
|
160
250
|
return false;
|
|
161
251
|
// Skip if this action URL is already visited
|
|
162
252
|
if (e.action && e.action.startsWith("http")) {
|
|
@@ -165,7 +255,6 @@ export class VisionNavigator {
|
|
|
165
255
|
return false;
|
|
166
256
|
}
|
|
167
257
|
// Skip if element description indicates it's the current page
|
|
168
|
-
const desc = e.description.toLowerCase();
|
|
169
258
|
if (desc.includes("currently active") ||
|
|
170
259
|
desc.includes("current page") ||
|
|
171
260
|
desc.includes("(active)")) {
|
|
@@ -174,6 +263,10 @@ export class VisionNavigator {
|
|
|
174
263
|
return true;
|
|
175
264
|
})
|
|
176
265
|
.slice(0, 4);
|
|
266
|
+
onEvent({
|
|
267
|
+
type: "vision_analysis",
|
|
268
|
+
message: `Navigation candidates: ${highPriority.length} (of ${analysis.elements.length})`,
|
|
269
|
+
});
|
|
177
270
|
for (const element of highPriority) {
|
|
178
271
|
try {
|
|
179
272
|
const newUrl = await this.clickElement(element, pageMap, onEvent);
|
|
@@ -221,7 +314,7 @@ export class VisionNavigator {
|
|
|
221
314
|
try {
|
|
222
315
|
const response = await this.anthropic.messages.create({
|
|
223
316
|
model: MODEL,
|
|
224
|
-
max_tokens:
|
|
317
|
+
max_tokens: 4000,
|
|
225
318
|
messages: [
|
|
226
319
|
{
|
|
227
320
|
role: "user",
|
|
@@ -342,7 +435,14 @@ Return ONLY valid JSON — no markdown, no explanation:
|
|
|
342
435
|
],
|
|
343
436
|
});
|
|
344
437
|
const text = firstTextBlock(response.content);
|
|
345
|
-
const parsed =
|
|
438
|
+
const parsed = salvageJson(text);
|
|
439
|
+
if (!parsed) {
|
|
440
|
+
onEvent({
|
|
441
|
+
type: "error",
|
|
442
|
+
message: "Vision analysis failed: could not parse or salvage JSON response",
|
|
443
|
+
});
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
346
446
|
onEvent({
|
|
347
447
|
type: "vision_analysis",
|
|
348
448
|
message: parsed.pageDescription + (parsed.workflow ? ` (${parsed.workflow})` : ""),
|