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.
@@ -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
- if (!["link", "tab", "menu"].includes(e.elementType))
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: 2000,
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 = JSON.parse(stripCodeFences(text));
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})` : ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qazen-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "QAZen CLI — capture authenticated browser sessions for enterprise SSO testing",
5
5
  "license": "MIT",
6
6
  "author": "QAZen",