@xbrowser/cli 1.25.2 → 1.27.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.
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ import {
48
48
  splitCommand,
49
49
  waitForAIResponse,
50
50
  waitForPage
51
- } from "./chunk-DRKL2RFQ.js";
51
+ } from "./chunk-IDXOIXQR.js";
52
52
  import {
53
53
  SessionRecorder
54
54
  } from "./chunk-IR6C24P7.js";
@@ -63,7 +63,7 @@ import {
63
63
  getBrowser,
64
64
  resetForTesting,
65
65
  saveSessionDiskMeta
66
- } from "./chunk-NNI5RT4A.js";
66
+ } from "./chunk-KECYFWJO.js";
67
67
  import "./chunk-JKVUFP3G.js";
68
68
  import {
69
69
  forwardCommandLog,
@@ -120,9 +120,9 @@ import {
120
120
  generateJSScript,
121
121
  generatePythonScript
122
122
  } from "./chunk-ETCO4SNK.js";
123
- import "./chunk-U6GFDVNI.js";
123
+ import "./chunk-J3HXZVSN.js";
124
124
  import "./chunk-R3F4K2SB.js";
125
- import "./chunk-MYH6FIBZ.js";
125
+ import "./chunk-SEEWE5SJ.js";
126
126
  import {
127
127
  errMsg
128
128
  } from "./chunk-GDKLH7ZY.js";
@@ -6127,7 +6127,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
6127
6127
  const targetPage = pages[cmdTabIndex];
6128
6128
  await targetPage.bringToFront().catch(() => {
6129
6129
  });
6130
- const { setActivePage } = await import("./browser-L3TUDYY3.js");
6130
+ const { setActivePage } = await import("./browser-4IYIHZ6R.js");
6131
6131
  setActivePage(session, targetPage);
6132
6132
  }
6133
6133
  }
@@ -6392,7 +6392,7 @@ async function handleRemote(args, options, mode) {
6392
6392
  }
6393
6393
  }
6394
6394
  async function startMcpServer() {
6395
- const { startMcpStdio } = await import("./server-IBQGNDSB.js");
6395
+ const { startMcpStdio } = await import("./server-WDDGIV3C.js");
6396
6396
  startMcpStdio();
6397
6397
  }
6398
6398
 
@@ -9307,7 +9307,7 @@ var DataCollector = class {
9307
9307
  return results;
9308
9308
  }
9309
9309
  async createBrowserContext() {
9310
- const { launch } = await import("./cdp-driver-5YVI5AIS.js");
9310
+ const { launch } = await import("./cdp-driver-EFEW4ZEN.js");
9311
9311
  const { browser } = await launch({
9312
9312
  headless: true,
9313
9313
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
@@ -7,7 +7,7 @@ import {
7
7
  joinCdpHttpUrl,
8
8
  killChrome,
9
9
  launchChrome
10
- } from "./chunk-VCVDILH6.js";
10
+ } from "./chunk-T2AI24A5.js";
11
11
  import "./chunk-3RG5ZIWI.js";
12
12
  export {
13
13
  ANTI_DETECT_ARGS,
@@ -7,7 +7,7 @@ import {
7
7
  joinCdpHttpUrl,
8
8
  killChrome,
9
9
  launchChrome
10
- } from "./chunk-MYH6FIBZ.js";
10
+ } from "./chunk-SEEWE5SJ.js";
11
11
  import "./chunk-GDKLH7ZY.js";
12
12
  import "./chunk-KFQGP6VL.js";
13
13
  export {
@@ -1,20 +1,114 @@
1
1
  import {
2
2
  executeChain,
3
3
  executeCommand
4
- } from "./chunk-DRKL2RFQ.js";
5
- import "./chunk-NNI5RT4A.js";
4
+ } from "./chunk-IDXOIXQR.js";
5
+ import "./chunk-KECYFWJO.js";
6
6
  import "./chunk-JKVUFP3G.js";
7
7
  import "./chunk-3OD76SUE.js";
8
8
  import "./chunk-ZTHE5RBZ.js";
9
9
  import "./chunk-Q2DFJQGS.js";
10
- import "./chunk-U6GFDVNI.js";
10
+ import "./chunk-J3HXZVSN.js";
11
11
  import "./chunk-R3F4K2SB.js";
12
- import "./chunk-MYH6FIBZ.js";
12
+ import "./chunk-SEEWE5SJ.js";
13
13
  import "./chunk-GDKLH7ZY.js";
14
14
  import "./chunk-WYETQ5C7.js";
15
15
  import "./chunk-75DNUI6M.js";
16
16
  import "./chunk-KFQGP6VL.js";
17
17
 
18
+ // src/lib/heal-kb.ts
19
+ import fs from "fs";
20
+ import path from "path";
21
+ import os from "os";
22
+ var HEAL_KB_TTL_DAYS = 30;
23
+ function healKbDir(dir) {
24
+ return dir ?? path.join(os.homedir(), ".xbrowser", "knowledge");
25
+ }
26
+ function domainFile(domain, dir) {
27
+ if (!/^[\w.-]+$/.test(domain)) throw new Error(`invalid domain: ${domain}`);
28
+ return path.join(dir, `heals-${domain}.json`);
29
+ }
30
+ function readHeals(domain, dir) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(domainFile(domain, healKbDir(dir)), "utf8"));
33
+ } catch {
34
+ return {};
35
+ }
36
+ }
37
+ function lookupHeal(domain, primary, dir) {
38
+ if (!primary) return null;
39
+ const e = readHeals(domain, dir)[primary];
40
+ return e ? { ...e } : null;
41
+ }
42
+ function pruneExpiredHeals(domain, dir) {
43
+ const d = healKbDir(dir);
44
+ const file = domainFile(domain, d);
45
+ const data = readHeals(domain, d);
46
+ const cutoff = Date.now() - HEAL_KB_TTL_DAYS * 864e5;
47
+ let pruned = 0;
48
+ for (const k of Object.keys(data)) {
49
+ const ts = Date.parse(data[k]?.lastSeen ?? "");
50
+ if (!(ts >= cutoff)) {
51
+ delete data[k];
52
+ pruned++;
53
+ }
54
+ }
55
+ if (pruned > 0) {
56
+ fs.mkdirSync(d, { recursive: true });
57
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
58
+ }
59
+ return pruned;
60
+ }
61
+ function writeHeal(domain, primary, entry, dir) {
62
+ if (!primary) throw new Error("primary selector is required");
63
+ if (!entry.healed || !entry.strategy) throw new Error("healed and strategy are required");
64
+ const d = healKbDir(dir);
65
+ const file = domainFile(domain, d);
66
+ const data = readHeals(domain, d);
67
+ const prev = data[primary];
68
+ const next = {
69
+ healed: entry.healed,
70
+ strategy: entry.strategy,
71
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
72
+ hits: (prev?.hits ?? 0) + 1
73
+ };
74
+ data[primary] = next;
75
+ fs.mkdirSync(d, { recursive: true });
76
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
77
+ pruneExpiredHeals(domain, d);
78
+ return next;
79
+ }
80
+ function forgetHeal(domain, primary, dir) {
81
+ const d = healKbDir(dir);
82
+ const data = readHeals(domain, d);
83
+ if (!data[primary]) return false;
84
+ delete data[primary];
85
+ fs.writeFileSync(domainFile(domain, d), JSON.stringify(data, null, 2));
86
+ return true;
87
+ }
88
+ function listHealDomains(dir) {
89
+ const d = healKbDir(dir);
90
+ let files = [];
91
+ try {
92
+ files = fs.readdirSync(d).filter((f) => f.startsWith("heals-") && f.endsWith(".json"));
93
+ } catch {
94
+ return [];
95
+ }
96
+ const out = [];
97
+ for (const f of files) {
98
+ const domain = f.slice("heals-".length, -".json".length);
99
+ const data = readHeals(domain, d);
100
+ const entries = Object.values(data);
101
+ if (entries.length === 0) continue;
102
+ out.push({
103
+ domain,
104
+ entries: entries.length,
105
+ hits: entries.reduce((s, e) => s + (e.hits ?? 0), 0),
106
+ lastSeen: entries.reduce((m, e) => e.lastSeen > m ? e.lastSeen : m, "")
107
+ });
108
+ }
109
+ return out.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
110
+ }
111
+
18
112
  // src/mcp/server.ts
19
113
  var TOOLS = [
20
114
  {
@@ -112,6 +206,32 @@ var TOOLS = [
112
206
  },
113
207
  required: ["file"]
114
208
  }
209
+ },
210
+ {
211
+ name: "heal_kb_read",
212
+ description: "Read the self-healing knowledge base (~/.xbrowser/knowledge/heals-{domain}.json). Mappings: broken primary selector \u2192 healed selector + strategy + hits. No args = list all domains; domain only = all entries for that domain; domain+selector = single lookup.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ domain: { type: "string", description: "Site domain, e.g. juejin.cn. Omit to list all domains." },
217
+ selector: { type: "string", description: "Broken primary selector to look up (requires domain)." }
218
+ }
219
+ }
220
+ },
221
+ {
222
+ name: "heal_kb_write",
223
+ description: "Write or remove an entry in the self-healing knowledge base \u2014 lets an external healer agent (selector-healer) persist re-located selectors that the replay engine will pick up as known-heal. Entries TTL-prune after 30 days without a hit.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ domain: { type: "string", description: "Site domain, e.g. juejin.cn" },
228
+ selector: { type: "string", description: "Broken primary selector (the recording-time selector)" },
229
+ healed: { type: "string", description: "The re-located working selector" },
230
+ strategy: { type: "string", description: "How it was healed, e.g. partial-id / text-anchor / label-anchor" },
231
+ remove: { type: "boolean", description: "true = forget this entry instead of writing" }
232
+ },
233
+ required: ["domain", "selector"]
234
+ }
115
235
  }
116
236
  ];
117
237
  var DEFAULT_SESSION = "mcp";
@@ -165,6 +285,28 @@ async function runTool(name, args) {
165
285
  const result = await executeCommand("replay", params, session, {});
166
286
  return text(result);
167
287
  }
288
+ case "heal_kb_read": {
289
+ const domain = args.domain;
290
+ const selector = args.selector;
291
+ if (!domain) return text({ domains: listHealDomains() });
292
+ if (selector) {
293
+ const entry = lookupHeal(domain, selector);
294
+ return text({ domain, selector, entry });
295
+ }
296
+ return text({ domain, entries: readHeals(domain) });
297
+ }
298
+ case "heal_kb_write": {
299
+ const domain = args.domain;
300
+ const selector = args.selector;
301
+ if (args.remove) {
302
+ return text({ domain, selector, removed: forgetHeal(domain, selector) });
303
+ }
304
+ const entry = writeHeal(domain, selector, {
305
+ healed: args.healed,
306
+ strategy: args.strategy
307
+ });
308
+ return text({ domain, selector, written: entry });
309
+ }
168
310
  default:
169
311
  throw new Error(`unknown tool: ${name}`);
170
312
  }
@@ -239,5 +381,6 @@ function startMcpStdio() {
239
381
  });
240
382
  }
241
383
  export {
384
+ runTool,
242
385
  startMcpStdio
243
386
  };
@@ -1,17 +1,111 @@
1
1
  import {
2
2
  executeChain,
3
3
  executeCommand
4
- } from "./chunk-PO27V5UH.js";
4
+ } from "./chunk-YITK42HZ.js";
5
5
  import "./chunk-JKVUFP3G.js";
6
6
  import "./chunk-3OD76SUE.js";
7
7
  import "./chunk-ZTHE5RBZ.js";
8
8
  import "./chunk-Q2DFJQGS.js";
9
- import "./chunk-2EEAXSBA.js";
9
+ import "./chunk-HSYRAV3H.js";
10
10
  import "./chunk-R3F4K2SB.js";
11
- import "./chunk-MYH6FIBZ.js";
11
+ import "./chunk-SEEWE5SJ.js";
12
12
  import "./chunk-GDKLH7ZY.js";
13
13
  import "./chunk-KFQGP6VL.js";
14
14
 
15
+ // src/lib/heal-kb.ts
16
+ import fs from "fs";
17
+ import path from "path";
18
+ import os from "os";
19
+ var HEAL_KB_TTL_DAYS = 30;
20
+ function healKbDir(dir) {
21
+ return dir ?? path.join(os.homedir(), ".xbrowser", "knowledge");
22
+ }
23
+ function domainFile(domain, dir) {
24
+ if (!/^[\w.-]+$/.test(domain)) throw new Error(`invalid domain: ${domain}`);
25
+ return path.join(dir, `heals-${domain}.json`);
26
+ }
27
+ function readHeals(domain, dir) {
28
+ try {
29
+ return JSON.parse(fs.readFileSync(domainFile(domain, healKbDir(dir)), "utf8"));
30
+ } catch {
31
+ return {};
32
+ }
33
+ }
34
+ function lookupHeal(domain, primary, dir) {
35
+ if (!primary) return null;
36
+ const e = readHeals(domain, dir)[primary];
37
+ return e ? { ...e } : null;
38
+ }
39
+ function pruneExpiredHeals(domain, dir) {
40
+ const d = healKbDir(dir);
41
+ const file = domainFile(domain, d);
42
+ const data = readHeals(domain, d);
43
+ const cutoff = Date.now() - HEAL_KB_TTL_DAYS * 864e5;
44
+ let pruned = 0;
45
+ for (const k of Object.keys(data)) {
46
+ const ts = Date.parse(data[k]?.lastSeen ?? "");
47
+ if (!(ts >= cutoff)) {
48
+ delete data[k];
49
+ pruned++;
50
+ }
51
+ }
52
+ if (pruned > 0) {
53
+ fs.mkdirSync(d, { recursive: true });
54
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
55
+ }
56
+ return pruned;
57
+ }
58
+ function writeHeal(domain, primary, entry, dir) {
59
+ if (!primary) throw new Error("primary selector is required");
60
+ if (!entry.healed || !entry.strategy) throw new Error("healed and strategy are required");
61
+ const d = healKbDir(dir);
62
+ const file = domainFile(domain, d);
63
+ const data = readHeals(domain, d);
64
+ const prev = data[primary];
65
+ const next = {
66
+ healed: entry.healed,
67
+ strategy: entry.strategy,
68
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
69
+ hits: (prev?.hits ?? 0) + 1
70
+ };
71
+ data[primary] = next;
72
+ fs.mkdirSync(d, { recursive: true });
73
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
74
+ pruneExpiredHeals(domain, d);
75
+ return next;
76
+ }
77
+ function forgetHeal(domain, primary, dir) {
78
+ const d = healKbDir(dir);
79
+ const data = readHeals(domain, d);
80
+ if (!data[primary]) return false;
81
+ delete data[primary];
82
+ fs.writeFileSync(domainFile(domain, d), JSON.stringify(data, null, 2));
83
+ return true;
84
+ }
85
+ function listHealDomains(dir) {
86
+ const d = healKbDir(dir);
87
+ let files = [];
88
+ try {
89
+ files = fs.readdirSync(d).filter((f) => f.startsWith("heals-") && f.endsWith(".json"));
90
+ } catch {
91
+ return [];
92
+ }
93
+ const out = [];
94
+ for (const f of files) {
95
+ const domain = f.slice("heals-".length, -".json".length);
96
+ const data = readHeals(domain, d);
97
+ const entries = Object.values(data);
98
+ if (entries.length === 0) continue;
99
+ out.push({
100
+ domain,
101
+ entries: entries.length,
102
+ hits: entries.reduce((s, e) => s + (e.hits ?? 0), 0),
103
+ lastSeen: entries.reduce((m, e) => e.lastSeen > m ? e.lastSeen : m, "")
104
+ });
105
+ }
106
+ return out.sort((a, b) => b.lastSeen.localeCompare(a.lastSeen));
107
+ }
108
+
15
109
  // src/mcp/server.ts
16
110
  var TOOLS = [
17
111
  {
@@ -109,6 +203,32 @@ var TOOLS = [
109
203
  },
110
204
  required: ["file"]
111
205
  }
206
+ },
207
+ {
208
+ name: "heal_kb_read",
209
+ description: "Read the self-healing knowledge base (~/.xbrowser/knowledge/heals-{domain}.json). Mappings: broken primary selector \u2192 healed selector + strategy + hits. No args = list all domains; domain only = all entries for that domain; domain+selector = single lookup.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ domain: { type: "string", description: "Site domain, e.g. juejin.cn. Omit to list all domains." },
214
+ selector: { type: "string", description: "Broken primary selector to look up (requires domain)." }
215
+ }
216
+ }
217
+ },
218
+ {
219
+ name: "heal_kb_write",
220
+ description: "Write or remove an entry in the self-healing knowledge base \u2014 lets an external healer agent (selector-healer) persist re-located selectors that the replay engine will pick up as known-heal. Entries TTL-prune after 30 days without a hit.",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: {
224
+ domain: { type: "string", description: "Site domain, e.g. juejin.cn" },
225
+ selector: { type: "string", description: "Broken primary selector (the recording-time selector)" },
226
+ healed: { type: "string", description: "The re-located working selector" },
227
+ strategy: { type: "string", description: "How it was healed, e.g. partial-id / text-anchor / label-anchor" },
228
+ remove: { type: "boolean", description: "true = forget this entry instead of writing" }
229
+ },
230
+ required: ["domain", "selector"]
231
+ }
112
232
  }
113
233
  ];
114
234
  var DEFAULT_SESSION = "mcp";
@@ -162,6 +282,28 @@ async function runTool(name, args) {
162
282
  const result = await executeCommand("replay", params, session, {});
163
283
  return text(result);
164
284
  }
285
+ case "heal_kb_read": {
286
+ const domain = args.domain;
287
+ const selector = args.selector;
288
+ if (!domain) return text({ domains: listHealDomains() });
289
+ if (selector) {
290
+ const entry = lookupHeal(domain, selector);
291
+ return text({ domain, selector, entry });
292
+ }
293
+ return text({ domain, entries: readHeals(domain) });
294
+ }
295
+ case "heal_kb_write": {
296
+ const domain = args.domain;
297
+ const selector = args.selector;
298
+ if (args.remove) {
299
+ return text({ domain, selector, removed: forgetHeal(domain, selector) });
300
+ }
301
+ const entry = writeHeal(domain, selector, {
302
+ healed: args.healed,
303
+ strategy: args.strategy
304
+ });
305
+ return text({ domain, selector, written: entry });
306
+ }
165
307
  default:
166
308
  throw new Error(`unknown tool: ${name}`);
167
309
  }
@@ -236,5 +378,6 @@ function startMcpStdio() {
236
378
  });
237
379
  }
238
380
  export {
381
+ runTool,
239
382
  startMcpStdio
240
383
  };
@@ -5,10 +5,91 @@ import {
5
5
  import "./chunk-KFQGP6VL.js";
6
6
 
7
7
  // src/recorder/session-replayer.ts
8
- import { readFileSync, writeFileSync, mkdirSync } from "fs";
9
8
  import { join } from "path";
10
9
  import { homedir } from "os";
10
+
11
+ // src/lib/heal-kb.ts
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import os from "os";
11
15
  var HEAL_KB_TTL_DAYS = 30;
16
+ function healKbDir(dir) {
17
+ return dir ?? path.join(os.homedir(), ".xbrowser", "knowledge");
18
+ }
19
+ function domainFile(domain, dir) {
20
+ if (!/^[\w.-]+$/.test(domain)) throw new Error(`invalid domain: ${domain}`);
21
+ return path.join(dir, `heals-${domain}.json`);
22
+ }
23
+ function readHeals(domain, dir) {
24
+ try {
25
+ return JSON.parse(fs.readFileSync(domainFile(domain, healKbDir(dir)), "utf8"));
26
+ } catch {
27
+ return {};
28
+ }
29
+ }
30
+ function lookupHeal(domain, primary, dir) {
31
+ if (!primary) return null;
32
+ const e = readHeals(domain, dir)[primary];
33
+ return e ? { ...e } : null;
34
+ }
35
+ function pruneExpiredHeals(domain, dir) {
36
+ const d = healKbDir(dir);
37
+ const file = domainFile(domain, d);
38
+ const data = readHeals(domain, d);
39
+ const cutoff = Date.now() - HEAL_KB_TTL_DAYS * 864e5;
40
+ let pruned = 0;
41
+ for (const k of Object.keys(data)) {
42
+ const ts = Date.parse(data[k]?.lastSeen ?? "");
43
+ if (!(ts >= cutoff)) {
44
+ delete data[k];
45
+ pruned++;
46
+ }
47
+ }
48
+ if (pruned > 0) {
49
+ fs.mkdirSync(d, { recursive: true });
50
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
51
+ }
52
+ return pruned;
53
+ }
54
+ function writeHeal(domain, primary, entry, dir) {
55
+ if (!primary) throw new Error("primary selector is required");
56
+ if (!entry.healed || !entry.strategy) throw new Error("healed and strategy are required");
57
+ const d = healKbDir(dir);
58
+ const file = domainFile(domain, d);
59
+ const data = readHeals(domain, d);
60
+ const prev = data[primary];
61
+ const next = {
62
+ healed: entry.healed,
63
+ strategy: entry.strategy,
64
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
65
+ hits: (prev?.hits ?? 0) + 1
66
+ };
67
+ data[primary] = next;
68
+ fs.mkdirSync(d, { recursive: true });
69
+ fs.writeFileSync(file, JSON.stringify(data, null, 2));
70
+ pruneExpiredHeals(domain, d);
71
+ return next;
72
+ }
73
+ function bumpHeal(domain, primary, dir) {
74
+ const d = healKbDir(dir);
75
+ const data = readHeals(domain, d);
76
+ const e = data[primary];
77
+ if (!e) return false;
78
+ e.hits += 1;
79
+ e.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
80
+ fs.writeFileSync(domainFile(domain, d), JSON.stringify(data, null, 2));
81
+ return true;
82
+ }
83
+ function forgetHeal(domain, primary, dir) {
84
+ const d = healKbDir(dir);
85
+ const data = readHeals(domain, d);
86
+ if (!data[primary]) return false;
87
+ delete data[primary];
88
+ fs.writeFileSync(domainFile(domain, d), JSON.stringify(data, null, 2));
89
+ return true;
90
+ }
91
+
92
+ // src/recorder/session-replayer.ts
12
93
  var NATIVE_VALUE_INJECT_TYPES = /* @__PURE__ */ new Set([
13
94
  "color",
14
95
  "date",
@@ -38,8 +119,8 @@ var SessionReplayer = class {
38
119
  /** Load a recording from a file path or parsed JSON */
39
120
  async load(source) {
40
121
  if (typeof source === "string") {
41
- const fs = await import("fs");
42
- const raw = fs.readFileSync(source, "utf8");
122
+ const fs2 = await import("fs");
123
+ const raw = fs2.readFileSync(source, "utf8");
43
124
  this.recording = JSON.parse(raw);
44
125
  } else {
45
126
  this.recording = source;
@@ -61,7 +142,7 @@ var SessionReplayer = class {
61
142
  if (this.opts.page) {
62
143
  this.page = this.opts.page;
63
144
  } else if (this.opts.cdpUrl) {
64
- const { launch } = await import("./cdp-driver-BYW7WY7E.js");
145
+ const { launch } = await import("./cdp-driver-4X6Q62WI.js");
65
146
  const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
66
147
  let contexts = browser.contexts();
67
148
  for (let i = 0; i < 10 && contexts.length === 0; i++) {
@@ -719,62 +800,26 @@ var SessionReplayer = class {
719
800
  return "unknown";
720
801
  }
721
802
  }
722
- healKnowledgeFile(domain) {
723
- return join(this.opts.healKnowledgeDir, `heals-${domain}.json`);
724
- }
725
- readHealFile(file) {
726
- try {
727
- return JSON.parse(readFileSync(file, "utf8"));
728
- } catch {
729
- return {};
730
- }
731
- }
732
803
  lookupHealKnowledge(domain, primary) {
733
804
  if (!primary || !this.opts.healKnowledgeDir) return null;
734
- const data = this.readHealFile(this.healKnowledgeFile(domain));
735
- const e = data[primary];
736
- return e ? { healed: e.healed, strategy: e.strategy } : null;
805
+ return lookupHeal(domain, primary, this.opts.healKnowledgeDir);
737
806
  }
738
807
  persistHealKnowledge(action, primary, healed) {
739
808
  if (!this.opts.healKnowledgeDir || !primary || healed.strategy === "known-heal") return;
740
809
  try {
741
- const file = this.healKnowledgeFile(this.pageDomain(action));
742
- const data = this.readHealFile(file);
743
- data[primary] = {
744
- healed: healed.selector,
745
- strategy: healed.strategy,
746
- lastSeen: (/* @__PURE__ */ new Date()).toISOString(),
747
- hits: (data[primary]?.hits ?? 0) + 1
748
- };
749
- const cutoff = Date.now() - HEAL_KB_TTL_DAYS * 864e5;
750
- for (const k of Object.keys(data)) {
751
- const ts = Date.parse(data[k]?.lastSeen ?? "");
752
- if (!(ts >= cutoff)) delete data[k];
753
- }
754
- mkdirSync(this.opts.healKnowledgeDir, { recursive: true });
755
- writeFileSync(file, JSON.stringify(data, null, 2));
810
+ writeHeal(this.pageDomain(action), primary, { healed: healed.selector, strategy: healed.strategy }, this.opts.healKnowledgeDir);
756
811
  } catch {
757
812
  }
758
813
  }
759
814
  bumpHealKnowledge(domain, primary) {
760
815
  try {
761
- const file = this.healKnowledgeFile(domain);
762
- const data = this.readHealFile(file);
763
- const e = data[primary];
764
- if (!e) return;
765
- e.hits += 1;
766
- e.lastSeen = (/* @__PURE__ */ new Date()).toISOString();
767
- writeFileSync(file, JSON.stringify(data, null, 2));
816
+ bumpHeal(domain, primary, this.opts.healKnowledgeDir);
768
817
  } catch {
769
818
  }
770
819
  }
771
820
  forgetHealKnowledge(domain, primary) {
772
821
  try {
773
- const file = this.healKnowledgeFile(domain);
774
- const data = this.readHealFile(file);
775
- if (!data[primary]) return;
776
- delete data[primary];
777
- writeFileSync(file, JSON.stringify(data, null, 2));
822
+ forgetHeal(domain, primary, this.opts.healKnowledgeDir);
778
823
  } catch {
779
824
  }
780
825
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.25.2",
3
+ "version": "1.27.0",
4
4
  "description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
5
5
  "type": "module",
6
6
  "bin": {