@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/{browser-L3TUDYY3.js → browser-4IYIHZ6R.js} +3 -3
- package/dist/{browser-33QQWEDI.js → browser-NYYCR4O7.js} +2 -2
- package/dist/{browser-4LOJSMWV.js → browser-ZD53IMUJ.js} +3 -3
- package/dist/{cdp-driver-BYW7WY7E.js → cdp-driver-4X6Q62WI.js} +2 -2
- package/dist/{cdp-driver-5YVI5AIS.js → cdp-driver-EFEW4ZEN.js} +2 -2
- package/dist/{cdp-driver-MEK5EDEK.js → cdp-driver-HB744QB4.js} +3 -3
- package/dist/{chunk-ZOYM7QMS.js → chunk-3PUOISKE.js} +1 -1
- package/dist/{chunk-2EEAXSBA.js → chunk-HSYRAV3H.js} +3 -3
- package/dist/{chunk-DRKL2RFQ.js → chunk-IDXOIXQR.js} +320 -144
- package/dist/{chunk-U6GFDVNI.js → chunk-J3HXZVSN.js} +3 -3
- package/dist/{chunk-NNI5RT4A.js → chunk-KECYFWJO.js} +1 -1
- package/dist/{chunk-CGNHGAIJ.js → chunk-ME5FO5YV.js} +3 -3
- package/dist/{chunk-MYH6FIBZ.js → chunk-SEEWE5SJ.js} +5 -1
- package/dist/{chunk-VCVDILH6.js → chunk-T2AI24A5.js} +5 -1
- package/dist/{chunk-PO27V5UH.js → chunk-YITK42HZ.js} +320 -144
- package/dist/cli.js +6 -6
- package/dist/daemon-main.js +326 -150
- package/dist/index.js +7 -7
- package/dist/{launcher-LUOMMGZC.js → launcher-SJ44X25U.js} +1 -1
- package/dist/{launcher-IK2FKW6T.js → launcher-XHKIC4GP.js} +1 -1
- package/dist/{server-IBQGNDSB.js → server-WDDGIV3C.js} +147 -4
- package/dist/{server-EZJW6IHU.js → server-YKMRFDEM.js} +146 -3
- package/dist/{session-replayer-PJIUH7KI.js → session-replayer-TXTNTG33.js} +89 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
splitCommand,
|
|
49
49
|
waitForAIResponse,
|
|
50
50
|
waitForPage
|
|
51
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
123
|
+
import "./chunk-J3HXZVSN.js";
|
|
124
124
|
import "./chunk-R3F4K2SB.js";
|
|
125
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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"]
|
|
@@ -1,20 +1,114 @@
|
|
|
1
1
|
import {
|
|
2
2
|
executeChain,
|
|
3
3
|
executeCommand
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
10
|
+
import "./chunk-J3HXZVSN.js";
|
|
11
11
|
import "./chunk-R3F4K2SB.js";
|
|
12
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
9
|
+
import "./chunk-HSYRAV3H.js";
|
|
10
10
|
import "./chunk-R3F4K2SB.js";
|
|
11
|
-
import "./chunk-
|
|
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
|
|
42
|
-
const raw =
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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": {
|