pursr 0.6.0 → 0.7.1
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/LICENSE +20 -20
- package/README.md +9 -9
- package/assets/icon.svg +20 -20
- package/assets/logo.svg +28 -28
- package/assets/social-preview.svg +76 -76
- package/bin/pursr-mcp.mjs +10 -9
- package/bin/pursr.mjs +15 -14
- package/package.json +4 -4
- package/plans/m5.4-polish.json +21 -21
- package/plugins/plugin-audit.js +57 -57
- package/plugins/plugin-demo.js +63 -63
- package/src/ai-diff.js +7 -6
- package/src/auth.js +92 -91
- package/src/baseline.js +126 -125
- package/src/ci-output.js +156 -156
- package/src/diff.js +18 -7
- package/src/dom-snapshot.js +192 -192
- package/src/eval.js +17 -17
- package/src/every-viewport.js +51 -51
- package/src/frames.js +33 -33
- package/src/har.js +158 -158
- package/src/hover.js +25 -25
- package/src/index.js +6 -6
- package/src/interact.js +137 -137
- package/src/mcp-resources.js +111 -110
- package/src/mcp.js +436 -435
- package/src/overlays.js +169 -169
- package/src/plugin-audit.js +278 -260
- package/src/plugin.js +120 -120
- package/src/probe.js +19 -19
- package/src/report.js +175 -175
- package/src/runway.js +65 -65
- package/src/selector-heal.js +85 -85
- package/src/selector.js +38 -38
- package/src/shoot.js +73 -73
- package/src/shot.js +17 -17
- package/src/snap.js +128 -128
- package/src/sweep-schema.js +69 -69
- package/src/sweep.js +1 -1
- package/src/util.js +204 -188
- package/src/viewport.js +38 -38
- package/src/watch.js +134 -134
package/src/har.js
CHANGED
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Hooks page.on("request") / page.on("response") / page.on("requestfailed")
|
|
4
|
-
// on an active page and produces a HAR 1.2 spec blob:
|
|
5
|
-
// { log: { version: "1.2", creator: {...}, browser: {...}, pages: [...], entries: [...] } }
|
|
6
|
-
//
|
|
7
|
-
// Use in code:
|
|
8
|
-
// const { startHarCapture, stopHarCapture } = await import("
|
|
9
|
-
// const har = await startHarCapture(page);
|
|
10
|
-
// await page.goto(url);
|
|
11
|
-
// await stopHarCapture(page); // returns the HAR object
|
|
12
|
-
//
|
|
13
|
-
// Or via CLI/sweep:
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// HAR is useful for:
|
|
17
|
-
// - killing flakiness from analytics/ads/CDN by mocking responses later
|
|
18
|
-
// - inspecting what the page actually fetched during a capture
|
|
19
|
-
// - regression diffing along with visual diffs
|
|
20
|
-
|
|
21
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
22
|
-
import { dirname } from "node:path";
|
|
23
|
-
import { nowIso } from "./util.js";
|
|
24
|
-
|
|
25
|
-
const SYM = Symbol.for("
|
|
26
|
-
|
|
27
|
-
function makeEntry(req, resp, startTs, endTs) {
|
|
28
|
-
const url = req.url();
|
|
29
|
-
const u = new URL(url);
|
|
30
|
-
const headersList = (obj) => {
|
|
31
|
-
if (!obj) return [];
|
|
32
|
-
try {
|
|
33
|
-
const out = [];
|
|
34
|
-
for (const [name, value] of Object.entries(obj)) {
|
|
35
|
-
if (Array.isArray(value)) {
|
|
36
|
-
for (const v of value) out.push({ name, value: String(v) });
|
|
37
|
-
} else {
|
|
38
|
-
out.push({ name, value: String(value) });
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return out;
|
|
42
|
-
} catch { return []; }
|
|
43
|
-
};
|
|
44
|
-
const queryString = u.search ? u.search.slice(1).split("&").filter(Boolean).map(kv => {
|
|
45
|
-
const [k, v = ""] = kv.split("=");
|
|
46
|
-
return { name: decodeURIComponent(k), value: decodeURIComponent(v) };
|
|
47
|
-
}) : [];
|
|
48
|
-
const postData = req.postData() ? { mimeType: req.postData() || "application/octet-stream", text: req.postData() } : undefined;
|
|
49
|
-
const entry = {
|
|
50
|
-
pageref: req.frame()?.url?.() || "_top",
|
|
51
|
-
startedDateTime: new Date(startTs).toISOString(),
|
|
52
|
-
time: Math.max(0, endTs - startTs),
|
|
53
|
-
request: {
|
|
54
|
-
method: req.method(),
|
|
55
|
-
url,
|
|
56
|
-
httpVersion: "HTTP/1.1",
|
|
57
|
-
cookies: [],
|
|
58
|
-
headers: headersList(req.headers()),
|
|
59
|
-
queryString,
|
|
60
|
-
headersSize: -1,
|
|
61
|
-
bodySize: postData?.text?.length || 0,
|
|
62
|
-
postData,
|
|
63
|
-
},
|
|
64
|
-
response: resp ? {
|
|
65
|
-
status: resp.status(),
|
|
66
|
-
statusText: resp.statusText() || "",
|
|
67
|
-
httpVersion: "HTTP/1.1",
|
|
68
|
-
cookies: [],
|
|
69
|
-
headers: headersList(resp.headers()),
|
|
70
|
-
content: { size: -1, mimeType: resp.headers()?.["content-type"] || "" },
|
|
71
|
-
redirectURL: resp.headers()?.location || "",
|
|
72
|
-
headersSize: -1,
|
|
73
|
-
bodySize: -1,
|
|
74
|
-
} : { status: 0, statusText: "Failed", httpVersion: "HTTP/1.1", cookies: [], headers: [], content: { size: 0, mimeType: "" }, redirectURL: "", headersSize: -1, bodySize: -1 },
|
|
75
|
-
cache: {},
|
|
76
|
-
timings: { send: 0, wait: Math.max(0, endTs - startTs), receive: 0 },
|
|
77
|
-
serverIPAddress: "",
|
|
78
|
-
connection: "",
|
|
79
|
-
};
|
|
80
|
-
return entry;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function startHarCapture(page, opts = {}) {
|
|
84
|
-
if (!page) throw new Error("startHarCapture: page required");
|
|
85
|
-
if (page[SYM]) return page[SYM]; // idempotent
|
|
86
|
-
const started = Date.now();
|
|
87
|
-
const entries = [];
|
|
88
|
-
const pending = new Map(); // req -> startTs
|
|
89
|
-
const state = {
|
|
90
|
-
started,
|
|
91
|
-
creator: { name: "
|
|
92
|
-
browser: { name: "chromium", version: "playwright-core" },
|
|
93
|
-
pages: [],
|
|
94
|
-
entries,
|
|
95
|
-
pending,
|
|
96
|
-
};
|
|
97
|
-
const onReq = (req) => {
|
|
98
|
-
pending.set(req, Date.now());
|
|
99
|
-
};
|
|
100
|
-
const onResp = async (resp) => {
|
|
101
|
-
try {
|
|
102
|
-
const req = resp.request();
|
|
103
|
-
const startTs = pending.get(req) || Date.now();
|
|
104
|
-
pending.delete(req);
|
|
105
|
-
const endTs = Date.now();
|
|
106
|
-
entries.push(makeEntry(req, resp, startTs, endTs));
|
|
107
|
-
} catch {}
|
|
108
|
-
};
|
|
109
|
-
const onFailed = (req) => {
|
|
110
|
-
try {
|
|
111
|
-
const startTs = pending.get(req) || Date.now();
|
|
112
|
-
pending.delete(req);
|
|
113
|
-
entries.push(makeEntry(req, null, startTs, Date.now()));
|
|
114
|
-
} catch {}
|
|
115
|
-
};
|
|
116
|
-
page.on("request", onReq);
|
|
117
|
-
page.on("response", onResp);
|
|
118
|
-
page.on("requestfailed", onFailed);
|
|
119
|
-
state._teardown = () => {
|
|
120
|
-
try { page.off("request", onReq); } catch {}
|
|
121
|
-
try { page.off("response", onResp); } catch {}
|
|
122
|
-
try { page.off("requestfailed", onFailed); } catch {}
|
|
123
|
-
};
|
|
124
|
-
page[SYM] = state;
|
|
125
|
-
return state;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function stopHarCapture(page) {
|
|
129
|
-
if (!page || !page[SYM]) return null;
|
|
130
|
-
const state = page[SYM];
|
|
131
|
-
try { state._teardown?.(); } catch {}
|
|
132
|
-
delete page[SYM];
|
|
133
|
-
return finalizeHar(state);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function finalizeHar(state) {
|
|
137
|
-
if (!state) return null;
|
|
138
|
-
return {
|
|
139
|
-
log: {
|
|
140
|
-
version: "1.2",
|
|
141
|
-
creator: state.creator,
|
|
142
|
-
browser: state.browser,
|
|
143
|
-
pages: state.pages,
|
|
144
|
-
entries: state.entries,
|
|
145
|
-
},
|
|
146
|
-
_meta: {
|
|
147
|
-
started: state.started,
|
|
148
|
-
finished: Date.now(),
|
|
149
|
-
entryCount: state.entries.length,
|
|
150
|
-
},
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function writeHar(har, file) {
|
|
155
|
-
if (!har || !file) return null;
|
|
156
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
157
|
-
writeFileSync(file, JSON.stringify(har, null, 2), "utf8");
|
|
158
|
-
return file;
|
|
1
|
+
// pursr — HAR (HTTP Archive) capture.
|
|
2
|
+
//
|
|
3
|
+
// Hooks page.on("request") / page.on("response") / page.on("requestfailed")
|
|
4
|
+
// on an active page and produces a HAR 1.2 spec blob:
|
|
5
|
+
// { log: { version: "1.2", creator: {...}, browser: {...}, pages: [...], entries: [...] } }
|
|
6
|
+
//
|
|
7
|
+
// Use in code:
|
|
8
|
+
// const { startHarCapture, stopHarCapture } = await import("pursr/har");
|
|
9
|
+
// const har = await startHarCapture(page);
|
|
10
|
+
// await page.goto(url);
|
|
11
|
+
// await stopHarCapture(page); // returns the HAR object
|
|
12
|
+
//
|
|
13
|
+
// Or via CLI/sweep:
|
|
14
|
+
// pursr shoot <url> --har ./out/req.har.json
|
|
15
|
+
//
|
|
16
|
+
// HAR is useful for:
|
|
17
|
+
// pursr - killing flakiness from analytics/ads/CDN by mocking responses later
|
|
18
|
+
// pursr - inspecting what the page actually fetched during a capture
|
|
19
|
+
// pursr - regression diffing along with visual diffs
|
|
20
|
+
|
|
21
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
22
|
+
import { dirname } from "node:path";
|
|
23
|
+
import { nowIso } from "./util.js";
|
|
24
|
+
|
|
25
|
+
const SYM = Symbol.for("pursr.har.capture");
|
|
26
|
+
|
|
27
|
+
function makeEntry(req, resp, startTs, endTs) {
|
|
28
|
+
const url = req.url();
|
|
29
|
+
const u = new URL(url);
|
|
30
|
+
const headersList = (obj) => {
|
|
31
|
+
if (!obj) return [];
|
|
32
|
+
try {
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const [name, value] of Object.entries(obj)) {
|
|
35
|
+
if (Array.isArray(value)) {
|
|
36
|
+
for (const v of value) out.push({ name, value: String(v) });
|
|
37
|
+
} else {
|
|
38
|
+
out.push({ name, value: String(value) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
} catch { return []; }
|
|
43
|
+
};
|
|
44
|
+
const queryString = u.search ? u.search.slice(1).split("&").filter(Boolean).map(kv => {
|
|
45
|
+
const [k, v = ""] = kv.split("=");
|
|
46
|
+
return { name: decodeURIComponent(k), value: decodeURIComponent(v) };
|
|
47
|
+
}) : [];
|
|
48
|
+
const postData = req.postData() ? { mimeType: req.postData() || "application/octet-stream", text: req.postData() } : undefined;
|
|
49
|
+
const entry = {
|
|
50
|
+
pageref: req.frame()?.url?.() || "_top",
|
|
51
|
+
startedDateTime: new Date(startTs).toISOString(),
|
|
52
|
+
time: Math.max(0, endTs - startTs),
|
|
53
|
+
request: {
|
|
54
|
+
method: req.method(),
|
|
55
|
+
url,
|
|
56
|
+
httpVersion: "HTTP/1.1",
|
|
57
|
+
cookies: [],
|
|
58
|
+
headers: headersList(req.headers()),
|
|
59
|
+
queryString,
|
|
60
|
+
headersSize: -1,
|
|
61
|
+
bodySize: postData?.text?.length || 0,
|
|
62
|
+
postData,
|
|
63
|
+
},
|
|
64
|
+
response: resp ? {
|
|
65
|
+
status: resp.status(),
|
|
66
|
+
statusText: resp.statusText() || "",
|
|
67
|
+
httpVersion: "HTTP/1.1",
|
|
68
|
+
cookies: [],
|
|
69
|
+
headers: headersList(resp.headers()),
|
|
70
|
+
content: { size: -1, mimeType: resp.headers()?.["content-type"] || "" },
|
|
71
|
+
redirectURL: resp.headers()?.location || "",
|
|
72
|
+
headersSize: -1,
|
|
73
|
+
bodySize: -1,
|
|
74
|
+
} : { status: 0, statusText: "Failed", httpVersion: "HTTP/1.1", cookies: [], headers: [], content: { size: 0, mimeType: "" }, redirectURL: "", headersSize: -1, bodySize: -1 },
|
|
75
|
+
cache: {},
|
|
76
|
+
timings: { send: 0, wait: Math.max(0, endTs - startTs), receive: 0 },
|
|
77
|
+
serverIPAddress: "",
|
|
78
|
+
connection: "",
|
|
79
|
+
};
|
|
80
|
+
return entry;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function startHarCapture(page, opts = {}) {
|
|
84
|
+
if (!page) throw new Error("startHarCapture: page required");
|
|
85
|
+
if (page[SYM]) return page[SYM]; // idempotent
|
|
86
|
+
const started = Date.now();
|
|
87
|
+
const entries = [];
|
|
88
|
+
const pending = new Map(); // req -> startTs
|
|
89
|
+
const state = {
|
|
90
|
+
started,
|
|
91
|
+
creator: { name: "pursr", version: opts.version || "0.3.0" },
|
|
92
|
+
browser: { name: "chromium", version: "playwright-core" },
|
|
93
|
+
pages: [],
|
|
94
|
+
entries,
|
|
95
|
+
pending,
|
|
96
|
+
};
|
|
97
|
+
const onReq = (req) => {
|
|
98
|
+
pending.set(req, Date.now());
|
|
99
|
+
};
|
|
100
|
+
const onResp = async (resp) => {
|
|
101
|
+
try {
|
|
102
|
+
const req = resp.request();
|
|
103
|
+
const startTs = pending.get(req) || Date.now();
|
|
104
|
+
pending.delete(req);
|
|
105
|
+
const endTs = Date.now();
|
|
106
|
+
entries.push(makeEntry(req, resp, startTs, endTs));
|
|
107
|
+
} catch {}
|
|
108
|
+
};
|
|
109
|
+
const onFailed = (req) => {
|
|
110
|
+
try {
|
|
111
|
+
const startTs = pending.get(req) || Date.now();
|
|
112
|
+
pending.delete(req);
|
|
113
|
+
entries.push(makeEntry(req, null, startTs, Date.now()));
|
|
114
|
+
} catch {}
|
|
115
|
+
};
|
|
116
|
+
page.on("request", onReq);
|
|
117
|
+
page.on("response", onResp);
|
|
118
|
+
page.on("requestfailed", onFailed);
|
|
119
|
+
state._teardown = () => {
|
|
120
|
+
try { page.off("request", onReq); } catch {}
|
|
121
|
+
try { page.off("response", onResp); } catch {}
|
|
122
|
+
try { page.off("requestfailed", onFailed); } catch {}
|
|
123
|
+
};
|
|
124
|
+
page[SYM] = state;
|
|
125
|
+
return state;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function stopHarCapture(page) {
|
|
129
|
+
if (!page || !page[SYM]) return null;
|
|
130
|
+
const state = page[SYM];
|
|
131
|
+
try { state._teardown?.(); } catch {}
|
|
132
|
+
delete page[SYM];
|
|
133
|
+
return finalizeHar(state);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function finalizeHar(state) {
|
|
137
|
+
if (!state) return null;
|
|
138
|
+
return {
|
|
139
|
+
log: {
|
|
140
|
+
version: "1.2",
|
|
141
|
+
creator: state.creator,
|
|
142
|
+
browser: state.browser,
|
|
143
|
+
pages: state.pages,
|
|
144
|
+
entries: state.entries,
|
|
145
|
+
},
|
|
146
|
+
_meta: {
|
|
147
|
+
started: state.started,
|
|
148
|
+
finished: Date.now(),
|
|
149
|
+
entryCount: state.entries.length,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function writeHar(har, file) {
|
|
155
|
+
if (!har || !file) return null;
|
|
156
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
157
|
+
writeFileSync(file, JSON.stringify(har, null, 2), "utf8");
|
|
158
|
+
return file;
|
|
159
159
|
}
|
package/src/hover.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
// Hover capture: navigate, hover a selector, screenshot.
|
|
2
|
-
|
|
3
|
-
import { launch, newPage } from "./runway.js";
|
|
4
|
-
import { resolveViewport } from "./viewport.js";
|
|
5
|
-
import { gotoOrThrow, settle, CLICK_TIMEOUT_MS } from "./overlays.js";
|
|
6
|
-
import { resolveLocator } from "./selector.js";
|
|
7
|
-
import { asNum, asBool, nowIso, writeSidecar, requireArg } from "./util.js";
|
|
8
|
-
|
|
9
|
-
export async function runHover({ url, selector, out, flags = {} }) {
|
|
10
|
-
requireArg("url", url, "string");
|
|
11
|
-
requireArg("selector", selector, "string");
|
|
12
|
-
const viewport = resolveViewport(flags);
|
|
13
|
-
const browser = await launch();
|
|
14
|
-
try {
|
|
15
|
-
const page = await newPage(browser, viewport);
|
|
16
|
-
const r = await gotoOrThrow(page, url); await settle(page);
|
|
17
|
-
const loc = await resolveLocator(page, selector);
|
|
18
|
-
await loc.first().waitFor({ state: "visible", timeout: CLICK_TIMEOUT_MS });
|
|
19
|
-
await loc.first().hover({ timeout: CLICK_TIMEOUT_MS });
|
|
20
|
-
await page.waitForTimeout(asNum(flags["hover-ms"], 250));
|
|
21
|
-
if (out) await page.screenshot({ path: out, fullPage: asBool(flags.full, false) });
|
|
22
|
-
const meta = { ...r, url, out, selector, viewport, ts: nowIso() };
|
|
23
|
-
if (out) await writeSidecar(meta);
|
|
24
|
-
return meta;
|
|
25
|
-
} finally { try { await browser.close(); } catch {} }
|
|
1
|
+
// Hover capture: navigate, hover a selector, screenshot.
|
|
2
|
+
|
|
3
|
+
import { launch, newPage } from "./runway.js";
|
|
4
|
+
import { resolveViewport } from "./viewport.js";
|
|
5
|
+
import { gotoOrThrow, settle, CLICK_TIMEOUT_MS } from "./overlays.js";
|
|
6
|
+
import { resolveLocator } from "./selector.js";
|
|
7
|
+
import { asNum, asBool, nowIso, writeSidecar, requireArg } from "./util.js";
|
|
8
|
+
|
|
9
|
+
export async function runHover({ url, selector, out, flags = {} }) {
|
|
10
|
+
requireArg("url", url, "string");
|
|
11
|
+
requireArg("selector", selector, "string");
|
|
12
|
+
const viewport = resolveViewport(flags);
|
|
13
|
+
const browser = await launch();
|
|
14
|
+
try {
|
|
15
|
+
const page = await newPage(browser, viewport);
|
|
16
|
+
const r = await gotoOrThrow(page, url); await settle(page);
|
|
17
|
+
const loc = await resolveLocator(page, selector);
|
|
18
|
+
await loc.first().waitFor({ state: "visible", timeout: CLICK_TIMEOUT_MS });
|
|
19
|
+
await loc.first().hover({ timeout: CLICK_TIMEOUT_MS });
|
|
20
|
+
await page.waitForTimeout(asNum(flags["hover-ms"], 250));
|
|
21
|
+
if (out) await page.screenshot({ path: out, fullPage: asBool(flags.full, false) });
|
|
22
|
+
const meta = { ...r, url, out, selector, viewport, ts: nowIso() };
|
|
23
|
+
if (out) await writeSidecar(meta);
|
|
24
|
+
return meta;
|
|
25
|
+
} finally { try { await browser.close(); } catch {} }
|
|
26
26
|
}
|
package/src/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
//
|
|
1
|
+
// pursr — public library API.
|
|
2
2
|
//
|
|
3
|
-
// This is the entry point for consumers who want to embed
|
|
3
|
+
// This is the entry point for consumers who want to embed pursr
|
|
4
4
|
// inside their own scripts (instead of using the CLI). The CLI in
|
|
5
|
-
// bin/
|
|
5
|
+
// bin/pursr.mjs is a thin wrapper around the same exports.
|
|
6
6
|
//
|
|
7
7
|
// All capture / sweep helpers return a `Result` object: the path to the
|
|
8
8
|
// PNG, a sidecar JSON metadata object, and timing info. They never throw
|
|
@@ -32,7 +32,7 @@ import { captureDomSnapshot, captureDomSnapshotSidecar } from "./dom-snapshot.js
|
|
|
32
32
|
import { runAudit } from "./plugin-audit.js";
|
|
33
33
|
import { resolveHealedSelector, healStepAction } from "./selector-heal.js";
|
|
34
34
|
import { writeCiOutput } from "./ci-output.js";
|
|
35
|
-
import {
|
|
35
|
+
import { PursrMCPServer, loadConfig as loadMcpConfig, MCP_VERSION } from "./mcp.js";
|
|
36
36
|
import { createRequire } from "node:module";
|
|
37
37
|
import { saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath } from "./baseline.js";
|
|
38
38
|
import { validateSweepPlan, registerSweepOp } from "./sweep-schema.js";
|
|
@@ -68,7 +68,7 @@ export {
|
|
|
68
68
|
// v3: selector healing, CI output, MCP server
|
|
69
69
|
resolveHealedSelector, healStepAction,
|
|
70
70
|
writeCiOutput,
|
|
71
|
-
|
|
71
|
+
PursrMCPServer, loadMcpConfig, MCP_VERSION,
|
|
72
72
|
// v4: baselines, sweep validation, MCP resources
|
|
73
73
|
saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
|
|
74
74
|
validateSweepPlan, registerSweepOp,
|
|
@@ -98,7 +98,7 @@ export default {
|
|
|
98
98
|
resolveLocator, parseTextSelector,
|
|
99
99
|
resolveHealedSelector, healStepAction,
|
|
100
100
|
writeCiOutput,
|
|
101
|
-
|
|
101
|
+
PursrMCPServer, loadMcpConfig, MCP_VERSION,
|
|
102
102
|
saveBaseline, loadBaseline, listBaselines, approveBaseline, diffKey, resolveBaselinePath,
|
|
103
103
|
validateSweepPlan, registerSweepOp,
|
|
104
104
|
listResources, readResource, recordResource,
|