@kontextmind/kxm 0.7.4 → 0.7.5
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/.claude-plugin/marketplace.json +1 -1
- package/docs/README.md +1 -0
- package/docs/adr/ADR-0002-browser-automation-steel-doks.md +103 -0
- package/docs/agent-skills.md +14 -0
- package/docs/browser-automation.md +116 -0
- package/docs/kb/how-credentials-retrieved-safely.md +31 -0
- package/docs/kb/how-to-capture-and-annotate-section.md +60 -0
- package/docs/kb/how-to-connect-playwright-to-steel.md +54 -0
- package/docs/kb/how-to-recover-expired-session-or-orphan.md +54 -0
- package/docs/kb/how-to-resume-after-mfa.md +28 -0
- package/docs/kb/how-to-take-over-session.md +32 -0
- package/docs/kb/why-authentication-disappeared.md +32 -0
- package/docs/kb/why-automation-opened-different-browser.md +32 -0
- package/docs/kb/why-session-viewer-cannot-control.md +31 -0
- package/docs/prompts/browser-annotate-feedback.md +41 -0
- package/docs/prompts/browser-diagnose-recover.md +38 -0
- package/docs/prompts/browser-explore.md +42 -0
- package/docs/prompts/browser-repro-fix.md +48 -0
- package/docs/prompts/browser-start.md +41 -0
- package/docs/prompts/browser-takeover.md +50 -0
- package/package.json +1 -1
- package/plugins/kxm/.claude-plugin/plugin.json +1 -1
- package/plugins/kxm/dist/mcp-server.js +1 -1
- package/plugins/kxm/dist/runtime.js +432 -0
- package/plugins/kxm/package.json +1 -1
- package/plugins/kxm/skills/hints.json +30 -0
- package/plugins/kxm/skills/kxm-browser-annotate/SKILL.md +90 -0
- package/plugins/kxm/skills/kxm-browser-auth/SKILL.md +47 -0
- package/plugins/kxm/skills/kxm-browser-diagnostics/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-explore/SKILL.md +48 -0
- package/plugins/kxm/skills/kxm-browser-session/SKILL.md +94 -0
- package/plugins/kxm/skills/kxm-browser-takeover/SKILL.md +87 -0
- package/plugins/kxm/skills/kxm-browser-verify/SKILL.md +71 -0
- package/plugins/kxm/src/browser.ts +603 -0
- package/plugins/kxm/src/mcp-server.ts +1 -1
- package/plugins/kxm/src/runtime.ts +1 -0
|
@@ -27750,6 +27750,429 @@ function evaluatePromotionPolicy(candidate, policy = "manual_pr", options) {
|
|
|
27750
27750
|
reason: `Unknown promotion policy: ${String(policy)}`
|
|
27751
27751
|
};
|
|
27752
27752
|
}
|
|
27753
|
+
|
|
27754
|
+
// plugins/kxm/src/browser.ts
|
|
27755
|
+
import { execSync } from "node:child_process";
|
|
27756
|
+
var VIEWPORT_PRESETS = Object.freeze({
|
|
27757
|
+
"mobile-sm": { name: "Mobile Small (SE)", category: "mobile", width: 375, height: 667, isMobile: true, hasTouch: true },
|
|
27758
|
+
"mobile": { name: "Mobile (iPhone 16 / 15 Pro)", category: "mobile", width: 393, height: 852, isMobile: true, hasTouch: true },
|
|
27759
|
+
"mobile-lg": { name: "Mobile Large (Pro Max)", category: "mobile", width: 430, height: 932, isMobile: true, hasTouch: true },
|
|
27760
|
+
"pixel": { name: "Google Pixel 8/9", category: "mobile", width: 412, height: 924, isMobile: true, hasTouch: true },
|
|
27761
|
+
"galaxy": { name: "Samsung Galaxy S24", category: "mobile", width: 360, height: 780, isMobile: true, hasTouch: true },
|
|
27762
|
+
"tablet": { name: "Tablet (iPad Air / Mini)", category: "tablet", width: 820, height: 1180, isMobile: true, hasTouch: true },
|
|
27763
|
+
"tablet-lg": { name: "Tablet Large (iPad Pro 12.9)", category: "tablet", width: 1024, height: 1366, isMobile: true, hasTouch: true },
|
|
27764
|
+
"laptop": { name: "Standard Laptop", category: "laptop", width: 1366, height: 768 },
|
|
27765
|
+
"macbook-13": { name: "MacBook Air 13", category: "laptop", width: 1440, height: 900 },
|
|
27766
|
+
"macbook-16": { name: "MacBook Pro 16", category: "laptop", width: 1728, height: 1117 },
|
|
27767
|
+
"desktop": { name: "Desktop FHD (1080p)", category: "desktop", width: 1920, height: 1080 },
|
|
27768
|
+
"desktop-2k": { name: "Desktop QHD (1440p)", category: "desktop", width: 2560, height: 1440 },
|
|
27769
|
+
"desktop-4k": { name: "Desktop 4K UHD", category: "desktop", width: 3840, height: 2160 }
|
|
27770
|
+
});
|
|
27771
|
+
function resolveViewportDimensions(presetOrDims) {
|
|
27772
|
+
if (!presetOrDims) {
|
|
27773
|
+
return { width: 1920, height: 1080 };
|
|
27774
|
+
}
|
|
27775
|
+
if (typeof presetOrDims === "string") {
|
|
27776
|
+
const matched = VIEWPORT_PRESETS[presetOrDims.toLowerCase()];
|
|
27777
|
+
if (matched) {
|
|
27778
|
+
return { width: matched.width, height: matched.height };
|
|
27779
|
+
}
|
|
27780
|
+
return { width: 1920, height: 1080 };
|
|
27781
|
+
}
|
|
27782
|
+
return presetOrDims;
|
|
27783
|
+
}
|
|
27784
|
+
function resolvePassCliApiKey(execFn = (cmd) => execSync(cmd, { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"], timeout: 5e3 })) {
|
|
27785
|
+
if (typeof process === "undefined" || process.env.USE_PASS_CLI === "false") {
|
|
27786
|
+
return void 0;
|
|
27787
|
+
}
|
|
27788
|
+
if (process.env.PASS_CLI_OUTPUT_MOCK) {
|
|
27789
|
+
try {
|
|
27790
|
+
const parsed = JSON.parse(process.env.PASS_CLI_OUTPUT_MOCK);
|
|
27791
|
+
const extraFields = parsed?.item?.content?.extra_fields || [];
|
|
27792
|
+
const customSections = parsed?.item?.content?.content?.Custom?.sections || [];
|
|
27793
|
+
const sectionFields = customSections.flatMap((s) => s.section_fields || []);
|
|
27794
|
+
const allFields = [...extraFields, ...sectionFields];
|
|
27795
|
+
return allFields.find((f) => f.name === "STEEL_API_KEY")?.content?.Hidden;
|
|
27796
|
+
} catch {
|
|
27797
|
+
return void 0;
|
|
27798
|
+
}
|
|
27799
|
+
}
|
|
27800
|
+
try {
|
|
27801
|
+
const output = execFn(
|
|
27802
|
+
'pass-cli item view --vault-name "AI Provider Keys" --item-title "Steel Browser (KontextMind DOKS)" --output json'
|
|
27803
|
+
);
|
|
27804
|
+
const parsed = JSON.parse(output);
|
|
27805
|
+
const extraFields = parsed?.item?.content?.extra_fields || [];
|
|
27806
|
+
const customSections = parsed?.item?.content?.content?.Custom?.sections || [];
|
|
27807
|
+
const sectionFields = customSections.flatMap((s) => s.section_fields || []);
|
|
27808
|
+
const allFields = [...extraFields, ...sectionFields];
|
|
27809
|
+
return allFields.find((f) => f.name === "STEEL_API_KEY")?.content?.Hidden;
|
|
27810
|
+
} catch {
|
|
27811
|
+
return void 0;
|
|
27812
|
+
}
|
|
27813
|
+
}
|
|
27814
|
+
function resolveSteelConfig(overrides) {
|
|
27815
|
+
const apiUrl = overrides?.apiUrl || process.env.STEEL_API_URL || "https://steel.kontextmind.com";
|
|
27816
|
+
const apiKey = overrides?.apiKey || process.env.STEEL_API_KEY || resolvePassCliApiKey();
|
|
27817
|
+
const uiUrl = overrides?.uiUrl || (overrides?.apiUrl ? `${overrides.apiUrl.replace(/\/$/, "")}/ui` : void 0) || process.env.STEEL_UI_URL || `${apiUrl.replace(/\/$/, "")}/ui`;
|
|
27818
|
+
return {
|
|
27819
|
+
apiUrl: apiUrl.replace(/\/$/, ""),
|
|
27820
|
+
apiKey,
|
|
27821
|
+
uiUrl,
|
|
27822
|
+
timeoutMs: overrides?.timeoutMs || 3e5
|
|
27823
|
+
// 5 minutes default
|
|
27824
|
+
};
|
|
27825
|
+
}
|
|
27826
|
+
function formatCDPEndpoint(session, config) {
|
|
27827
|
+
const baseApi = config.apiUrl;
|
|
27828
|
+
const urlObj = new URL(baseApi);
|
|
27829
|
+
const isSecure = urlObj.protocol === "https:";
|
|
27830
|
+
const wsProtocol = isSecure ? "wss:" : "ws:";
|
|
27831
|
+
const host = urlObj.host;
|
|
27832
|
+
const searchParams = new URLSearchParams();
|
|
27833
|
+
searchParams.set("sessionId", session.id);
|
|
27834
|
+
if (config.apiKey) {
|
|
27835
|
+
searchParams.set("apiKey", config.apiKey);
|
|
27836
|
+
}
|
|
27837
|
+
return `${wsProtocol}//${host}/v1/devtools?${searchParams.toString()}`;
|
|
27838
|
+
}
|
|
27839
|
+
function sanitizeLogOutput(input) {
|
|
27840
|
+
if (typeof input === "string") {
|
|
27841
|
+
return input.replace(/apiKey=[^&]+/g, "apiKey=[REDACTED]").replace(/steel_[a-f0-9]+/g, "steel_[REDACTED]");
|
|
27842
|
+
}
|
|
27843
|
+
if (Array.isArray(input)) {
|
|
27844
|
+
return input.map(sanitizeLogOutput);
|
|
27845
|
+
}
|
|
27846
|
+
if (input !== null && typeof input === "object") {
|
|
27847
|
+
const copy = {};
|
|
27848
|
+
for (const [k, v] of Object.entries(input)) {
|
|
27849
|
+
if (/key|secret|token|auth|password/i.test(k) && typeof v === "string") {
|
|
27850
|
+
copy[k] = "[REDACTED]";
|
|
27851
|
+
} else {
|
|
27852
|
+
copy[k] = sanitizeLogOutput(v);
|
|
27853
|
+
}
|
|
27854
|
+
}
|
|
27855
|
+
return copy;
|
|
27856
|
+
}
|
|
27857
|
+
return input;
|
|
27858
|
+
}
|
|
27859
|
+
function createAnnotationFeedback(feedback) {
|
|
27860
|
+
return {
|
|
27861
|
+
...feedback,
|
|
27862
|
+
capturedAt: feedback.capturedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
27863
|
+
};
|
|
27864
|
+
}
|
|
27865
|
+
function formatAnnotationFeedbackPrompt(feedback) {
|
|
27866
|
+
let output = `## Visual Feedback & Section Annotation
|
|
27867
|
+
|
|
27868
|
+
`;
|
|
27869
|
+
output += `- **Target URL**: ${feedback.url}
|
|
27870
|
+
`;
|
|
27871
|
+
if (feedback.sessionId) {
|
|
27872
|
+
output += `- **Session ID**: ${feedback.sessionId}
|
|
27873
|
+
`;
|
|
27874
|
+
}
|
|
27875
|
+
if (feedback.sectionSelector) {
|
|
27876
|
+
output += `- **Section Target Selector**: \`${feedback.sectionSelector}\`
|
|
27877
|
+
`;
|
|
27878
|
+
}
|
|
27879
|
+
if (feedback.screenshotPath) {
|
|
27880
|
+
output += `- **Screenshot Artifact**: \`${feedback.screenshotPath}\`
|
|
27881
|
+
`;
|
|
27882
|
+
}
|
|
27883
|
+
output += `- **Captured At**: ${feedback.capturedAt}
|
|
27884
|
+
|
|
27885
|
+
`;
|
|
27886
|
+
output += `### Summary
|
|
27887
|
+
${feedback.overallSummary}
|
|
27888
|
+
|
|
27889
|
+
`;
|
|
27890
|
+
if (feedback.annotations.length > 0) {
|
|
27891
|
+
output += `### Annotated Elements & Notes
|
|
27892
|
+
|
|
27893
|
+
`;
|
|
27894
|
+
feedback.annotations.forEach((item, idx) => {
|
|
27895
|
+
output += `${idx + 1}. **${item.label}**`;
|
|
27896
|
+
if (item.severity) {
|
|
27897
|
+
output += ` [${item.severity.toUpperCase()}]`;
|
|
27898
|
+
}
|
|
27899
|
+
output += `
|
|
27900
|
+
- **Note**: ${item.note}
|
|
27901
|
+
`;
|
|
27902
|
+
if (item.selector) {
|
|
27903
|
+
output += ` - **Selector**: \`${item.selector}\`
|
|
27904
|
+
`;
|
|
27905
|
+
}
|
|
27906
|
+
if (item.boundingBox) {
|
|
27907
|
+
output += ` - **Region (Box)**: x=${item.boundingBox.x}, y=${item.boundingBox.y}, w=${item.boundingBox.width}, h=${item.boundingBox.height}
|
|
27908
|
+
`;
|
|
27909
|
+
}
|
|
27910
|
+
});
|
|
27911
|
+
output += `
|
|
27912
|
+
`;
|
|
27913
|
+
}
|
|
27914
|
+
if (feedback.requestedChanges.length > 0) {
|
|
27915
|
+
output += `### Actionable Change List
|
|
27916
|
+
|
|
27917
|
+
`;
|
|
27918
|
+
feedback.requestedChanges.forEach((change, idx) => {
|
|
27919
|
+
output += `- [ ] ${change}
|
|
27920
|
+
`;
|
|
27921
|
+
});
|
|
27922
|
+
}
|
|
27923
|
+
return output;
|
|
27924
|
+
}
|
|
27925
|
+
var SteelClient = class {
|
|
27926
|
+
config;
|
|
27927
|
+
activeSessions = /* @__PURE__ */ new Map();
|
|
27928
|
+
constructor(config) {
|
|
27929
|
+
this.config = resolveSteelConfig(config);
|
|
27930
|
+
}
|
|
27931
|
+
getConfig() {
|
|
27932
|
+
return { ...this.config };
|
|
27933
|
+
}
|
|
27934
|
+
headers() {
|
|
27935
|
+
const h = {
|
|
27936
|
+
"Content-Type": "application/json"
|
|
27937
|
+
};
|
|
27938
|
+
if (this.config.apiKey) {
|
|
27939
|
+
h["x-steel-api-key"] = this.config.apiKey;
|
|
27940
|
+
}
|
|
27941
|
+
return h;
|
|
27942
|
+
}
|
|
27943
|
+
/**
|
|
27944
|
+
* Launch a new Steel browser session on DOKS.
|
|
27945
|
+
*/
|
|
27946
|
+
async createSession(options) {
|
|
27947
|
+
const timeoutMs = options?.timeoutMs ?? this.config.timeoutMs ?? 3e5;
|
|
27948
|
+
const body = {
|
|
27949
|
+
timeout: timeoutMs
|
|
27950
|
+
};
|
|
27951
|
+
const dimensions = options?.dimensions || (options?.viewportPreset ? resolveViewportDimensions(options.viewportPreset) : void 0);
|
|
27952
|
+
if (dimensions) {
|
|
27953
|
+
body.dimensions = dimensions;
|
|
27954
|
+
}
|
|
27955
|
+
if (options?.userAgent) {
|
|
27956
|
+
body.userAgent = options.userAgent;
|
|
27957
|
+
}
|
|
27958
|
+
if (options?.proxy) {
|
|
27959
|
+
body.proxy = options.proxy;
|
|
27960
|
+
}
|
|
27961
|
+
const res = await fetch(`${this.config.apiUrl}/v1/sessions`, {
|
|
27962
|
+
method: "POST",
|
|
27963
|
+
headers: this.headers(),
|
|
27964
|
+
body: JSON.stringify(body)
|
|
27965
|
+
});
|
|
27966
|
+
if (!res.ok) {
|
|
27967
|
+
const errText = await res.text();
|
|
27968
|
+
throw new Error(`Failed to create Steel session (${res.status}): ${errText}`);
|
|
27969
|
+
}
|
|
27970
|
+
const data = await res.json();
|
|
27971
|
+
const session = {
|
|
27972
|
+
id: data.id,
|
|
27973
|
+
createdAt: data.createdAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
27974
|
+
status: "live",
|
|
27975
|
+
state: "AGENT_CONTROL",
|
|
27976
|
+
websocketUrl: data.websocketUrl || "",
|
|
27977
|
+
debugUrl: data.debugUrl || "",
|
|
27978
|
+
debuggerUrl: data.debuggerUrl || "",
|
|
27979
|
+
sessionViewerUrl: data.sessionViewerUrl || `${this.config.uiUrl}?sessionId=${data.id}`,
|
|
27980
|
+
timeoutMs,
|
|
27981
|
+
lastActiveAt: Date.now(),
|
|
27982
|
+
activeController: "agent",
|
|
27983
|
+
userAgent: data.userAgent
|
|
27984
|
+
};
|
|
27985
|
+
this.activeSessions.set(session.id, session);
|
|
27986
|
+
return session;
|
|
27987
|
+
}
|
|
27988
|
+
/**
|
|
27989
|
+
* Get details of an existing session.
|
|
27990
|
+
*/
|
|
27991
|
+
async getSession(sessionId) {
|
|
27992
|
+
const res = await fetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}`, {
|
|
27993
|
+
method: "GET",
|
|
27994
|
+
headers: this.headers()
|
|
27995
|
+
});
|
|
27996
|
+
if (res.status === 404) {
|
|
27997
|
+
const cached = this.activeSessions.get(sessionId);
|
|
27998
|
+
if (cached) {
|
|
27999
|
+
cached.state = "EXPIRED";
|
|
28000
|
+
cached.status = "released";
|
|
28001
|
+
cached.activeController = "none";
|
|
28002
|
+
}
|
|
28003
|
+
return null;
|
|
28004
|
+
}
|
|
28005
|
+
if (!res.ok) {
|
|
28006
|
+
throw new Error(`Failed to fetch Steel session (${res.status})`);
|
|
28007
|
+
}
|
|
28008
|
+
const data = await res.json();
|
|
28009
|
+
const existing = this.activeSessions.get(sessionId);
|
|
28010
|
+
const session = {
|
|
28011
|
+
id: data.id,
|
|
28012
|
+
createdAt: data.createdAt,
|
|
28013
|
+
status: data.status,
|
|
28014
|
+
state: existing?.state ?? (data.status === "live" ? "AGENT_CONTROL" : "RELEASED"),
|
|
28015
|
+
websocketUrl: data.websocketUrl || existing?.websocketUrl || "",
|
|
28016
|
+
debugUrl: data.debugUrl || existing?.debugUrl || "",
|
|
28017
|
+
debuggerUrl: data.debuggerUrl || existing?.debuggerUrl || "",
|
|
28018
|
+
sessionViewerUrl: existing?.sessionViewerUrl || `${this.config.uiUrl}?sessionId=${data.id}`,
|
|
28019
|
+
timeoutMs: data.timeout || existing?.timeoutMs || 3e5,
|
|
28020
|
+
lastActiveAt: Date.now(),
|
|
28021
|
+
activeController: existing?.activeController ?? (data.status === "live" ? "agent" : "none"),
|
|
28022
|
+
userAgent: data.userAgent
|
|
28023
|
+
};
|
|
28024
|
+
this.activeSessions.set(session.id, session);
|
|
28025
|
+
return session;
|
|
28026
|
+
}
|
|
28027
|
+
/**
|
|
28028
|
+
* Request human takeover for MFA, login, or consent.
|
|
28029
|
+
* Pauses agent automation and sets state to HUMAN_CONTROL.
|
|
28030
|
+
*/
|
|
28031
|
+
requestHumanTakeover(sessionId, reason) {
|
|
28032
|
+
const session = this.activeSessions.get(sessionId);
|
|
28033
|
+
if (!session) {
|
|
28034
|
+
throw new Error(`Session ${sessionId} not tracked or already released`);
|
|
28035
|
+
}
|
|
28036
|
+
if (session.state === "RELEASED" || session.state === "EXPIRED" || session.state === "FAILED") {
|
|
28037
|
+
throw new Error(`Cannot initiate takeover on session in state ${session.state}`);
|
|
28038
|
+
}
|
|
28039
|
+
session.state = "HUMAN_CONTROL";
|
|
28040
|
+
session.activeController = "human";
|
|
28041
|
+
session.takeoverReason = reason;
|
|
28042
|
+
session.lastActiveAt = Date.now();
|
|
28043
|
+
const takeoverUrl = `${this.config.uiUrl}?sessionId=${encodeURIComponent(sessionId)}`;
|
|
28044
|
+
const instructions = `[HUMAN TAKEOVER REQUIRED]
|
|
28045
|
+
Reason: ${reason}
|
|
28046
|
+
Session ID: ${session.id}
|
|
28047
|
+
Takeover URL: ${takeoverUrl}
|
|
28048
|
+
|
|
28049
|
+
Instructions for Operator:
|
|
28050
|
+
1. Open the URL above to access the session UI.
|
|
28051
|
+
2. Perform the required authentication / MFA / consent action.
|
|
28052
|
+
3. Return to the terminal and signal completion. Automation is paused until you confirm.`;
|
|
28053
|
+
return {
|
|
28054
|
+
session,
|
|
28055
|
+
takeoverUrl,
|
|
28056
|
+
instructions
|
|
28057
|
+
};
|
|
28058
|
+
}
|
|
28059
|
+
/**
|
|
28060
|
+
* Signal that human takeover is complete.
|
|
28061
|
+
* Moves state to VERIFY_AUTHENTICATION before transitioning back to AGENT_CONTROL.
|
|
28062
|
+
*/
|
|
28063
|
+
signalHumanComplete(sessionId) {
|
|
28064
|
+
const session = this.activeSessions.get(sessionId);
|
|
28065
|
+
if (!session) {
|
|
28066
|
+
throw new Error(`Session ${sessionId} not tracked or already released`);
|
|
28067
|
+
}
|
|
28068
|
+
if (session.state !== "HUMAN_CONTROL") {
|
|
28069
|
+
throw new Error(`Session ${sessionId} is not in HUMAN_CONTROL state (currently ${session.state})`);
|
|
28070
|
+
}
|
|
28071
|
+
session.state = "VERIFY_AUTHENTICATION";
|
|
28072
|
+
session.activeController = "agent";
|
|
28073
|
+
session.lastActiveAt = Date.now();
|
|
28074
|
+
return {
|
|
28075
|
+
session,
|
|
28076
|
+
state: session.state
|
|
28077
|
+
};
|
|
28078
|
+
}
|
|
28079
|
+
/**
|
|
28080
|
+
* Confirm authentication verification passed and restore AGENT_CONTROL.
|
|
28081
|
+
*/
|
|
28082
|
+
confirmAuthenticationVerified(sessionId) {
|
|
28083
|
+
const session = this.activeSessions.get(sessionId);
|
|
28084
|
+
if (!session) {
|
|
28085
|
+
throw new Error(`Session ${sessionId} not tracked or already released`);
|
|
28086
|
+
}
|
|
28087
|
+
if (session.state !== "VERIFY_AUTHENTICATION") {
|
|
28088
|
+
throw new Error(`Session ${sessionId} is not in VERIFY_AUTHENTICATION state (currently ${session.state})`);
|
|
28089
|
+
}
|
|
28090
|
+
session.state = "AGENT_CONTROL";
|
|
28091
|
+
session.activeController = "agent";
|
|
28092
|
+
session.takeoverReason = void 0;
|
|
28093
|
+
session.lastActiveAt = Date.now();
|
|
28094
|
+
return session;
|
|
28095
|
+
}
|
|
28096
|
+
/**
|
|
28097
|
+
* Gracefully release a session.
|
|
28098
|
+
*/
|
|
28099
|
+
async releaseSession(sessionId) {
|
|
28100
|
+
try {
|
|
28101
|
+
const res = await fetch(`${this.config.apiUrl}/v1/sessions/${encodeURIComponent(sessionId)}/release`, {
|
|
28102
|
+
method: "POST",
|
|
28103
|
+
headers: this.headers()
|
|
28104
|
+
});
|
|
28105
|
+
const session = this.activeSessions.get(sessionId);
|
|
28106
|
+
if (session) {
|
|
28107
|
+
session.status = "released";
|
|
28108
|
+
session.state = "RELEASED";
|
|
28109
|
+
session.activeController = "none";
|
|
28110
|
+
}
|
|
28111
|
+
this.activeSessions.delete(sessionId);
|
|
28112
|
+
return res.ok;
|
|
28113
|
+
} catch {
|
|
28114
|
+
this.activeSessions.delete(sessionId);
|
|
28115
|
+
return false;
|
|
28116
|
+
}
|
|
28117
|
+
}
|
|
28118
|
+
/**
|
|
28119
|
+
* Perform a direct stateless scrape without manual session management.
|
|
28120
|
+
*/
|
|
28121
|
+
async scrape(url) {
|
|
28122
|
+
const res = await fetch(`${this.config.apiUrl}/v1/scrape`, {
|
|
28123
|
+
method: "POST",
|
|
28124
|
+
headers: this.headers(),
|
|
28125
|
+
body: JSON.stringify({ url })
|
|
28126
|
+
});
|
|
28127
|
+
if (!res.ok) {
|
|
28128
|
+
const err = await res.text();
|
|
28129
|
+
throw new Error(`Scrape failed (${res.status}): ${err}`);
|
|
28130
|
+
}
|
|
28131
|
+
return res.json();
|
|
28132
|
+
}
|
|
28133
|
+
/**
|
|
28134
|
+
* Perform a direct screenshot action.
|
|
28135
|
+
*/
|
|
28136
|
+
async screenshot(url, fullPage = false) {
|
|
28137
|
+
const res = await fetch(`${this.config.apiUrl}/v1/screenshot`, {
|
|
28138
|
+
method: "POST",
|
|
28139
|
+
headers: this.headers(),
|
|
28140
|
+
body: JSON.stringify({ url, fullPage })
|
|
28141
|
+
});
|
|
28142
|
+
if (!res.ok) {
|
|
28143
|
+
const err = await res.text();
|
|
28144
|
+
throw new Error(`Screenshot failed (${res.status}): ${err}`);
|
|
28145
|
+
}
|
|
28146
|
+
return res.json();
|
|
28147
|
+
}
|
|
28148
|
+
/**
|
|
28149
|
+
* Detect and list orphaned or timed-out active sessions.
|
|
28150
|
+
*/
|
|
28151
|
+
async checkOrphanedSessions(maxIdleMs = 6e5) {
|
|
28152
|
+
const res = await fetch(`${this.config.apiUrl}/v1/sessions`, {
|
|
28153
|
+
method: "GET",
|
|
28154
|
+
headers: this.headers()
|
|
28155
|
+
});
|
|
28156
|
+
if (!res.ok) {
|
|
28157
|
+
return [];
|
|
28158
|
+
}
|
|
28159
|
+
const data = await res.json();
|
|
28160
|
+
const remoteSessions = data.sessions || [];
|
|
28161
|
+
const now = Date.now();
|
|
28162
|
+
const orphaned = [];
|
|
28163
|
+
for (const rs of remoteSessions) {
|
|
28164
|
+
if (rs.status === "live" || rs.status === "idle") {
|
|
28165
|
+
const tracked = this.activeSessions.get(rs.id);
|
|
28166
|
+
if (!tracked && rs.duration > maxIdleMs) {
|
|
28167
|
+
orphaned.push(rs.id);
|
|
28168
|
+
} else if (tracked && now - tracked.lastActiveAt > maxIdleMs && tracked.state !== "HUMAN_CONTROL") {
|
|
28169
|
+
orphaned.push(rs.id);
|
|
28170
|
+
}
|
|
28171
|
+
}
|
|
28172
|
+
}
|
|
28173
|
+
return orphaned;
|
|
28174
|
+
}
|
|
28175
|
+
};
|
|
27753
28176
|
export {
|
|
27754
28177
|
BUILTIN_HARNESSES,
|
|
27755
28178
|
BUILTIN_HARNESS_IDS,
|
|
@@ -27765,6 +28188,8 @@ export {
|
|
|
27765
28188
|
PI_NATIVE_BRAKE_PROVIDERS,
|
|
27766
28189
|
PiSession,
|
|
27767
28190
|
SAFE_HARNESS_COMMAND_ID,
|
|
28191
|
+
SteelClient,
|
|
28192
|
+
VIEWPORT_PRESETS,
|
|
27768
28193
|
VNEXT_ABSENT_MEMORY_REVISION,
|
|
27769
28194
|
VNEXT_EVENT_STORE_SCHEMA_VERSION,
|
|
27770
28195
|
VNEXT_REGISTRY_SCHEMA_VERSION,
|
|
@@ -27786,6 +28211,7 @@ export {
|
|
|
27786
28211
|
closeVnextRuntimeContext,
|
|
27787
28212
|
computeGateEvidenceOutcome,
|
|
27788
28213
|
computeVnextMemoryRevision,
|
|
28214
|
+
createAnnotationFeedback,
|
|
27789
28215
|
createBackup,
|
|
27790
28216
|
createLogger,
|
|
27791
28217
|
createVnextOneShotProducer,
|
|
@@ -27800,6 +28226,8 @@ export {
|
|
|
27800
28226
|
fileSha256,
|
|
27801
28227
|
findWinNpmInnerExe,
|
|
27802
28228
|
foldStoredVnextRun,
|
|
28229
|
+
formatAnnotationFeedbackPrompt,
|
|
28230
|
+
formatCDPEndpoint,
|
|
27803
28231
|
formatHarnessInventory,
|
|
27804
28232
|
formatHarnessUpdate,
|
|
27805
28233
|
formatImprovementReport,
|
|
@@ -27844,11 +28272,15 @@ export {
|
|
|
27844
28272
|
redactLogValue,
|
|
27845
28273
|
registerVnextRuntimeCloseHook,
|
|
27846
28274
|
resolveDispatchStatus,
|
|
28275
|
+
resolvePassCliApiKey,
|
|
28276
|
+
resolveSteelConfig,
|
|
28277
|
+
resolveViewportDimensions,
|
|
27847
28278
|
restoreBackup,
|
|
27848
28279
|
restoreDatabaseFile,
|
|
27849
28280
|
rotateLogFiles,
|
|
27850
28281
|
runHarnessUpdate,
|
|
27851
28282
|
runtimeError,
|
|
28283
|
+
sanitizeLogOutput,
|
|
27852
28284
|
startVnextRuntimeSupervisor,
|
|
27853
28285
|
tableColumns,
|
|
27854
28286
|
userTables,
|
package/plugins/kxm/package.json
CHANGED
|
@@ -68,6 +68,36 @@
|
|
|
68
68
|
"argumentHint": "[trailers|trust|gates|authz|webhooks]",
|
|
69
69
|
"triggers": ["KM-Session", "trust mode", "secret gate", "RLS", "protocol"],
|
|
70
70
|
"complete": ["trailers", "trust", "gates", "authz", "webhooks"]
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "kxm-browser-session",
|
|
74
|
+
"argumentHint": "[create|inspect|release|cdp]",
|
|
75
|
+
"triggers": ["browser session", "steel session", "remote browser", "launch browser", "cdp endpoint"],
|
|
76
|
+
"complete": ["create session", "inspect session", "release session", "format cdp"]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
"name": "kxm-browser-takeover",
|
|
80
|
+
"argumentHint": "[request|signal|verify]",
|
|
81
|
+
"triggers": ["human takeover", "mfa login", "takeover url", "browser auth required", "resume browser"],
|
|
82
|
+
"complete": ["request takeover", "signal complete", "verify auth"]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"name": "kxm-browser-explore",
|
|
86
|
+
"argumentHint": "[open|snapshot|get|screenshot]",
|
|
87
|
+
"triggers": ["agent-browser", "explore web", "scrape page", "dom snapshot"],
|
|
88
|
+
"complete": ["agent-browser open", "agent-browser snapshot", "agent-browser get", "agent-browser screenshot"]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"name": "kxm-browser-verify",
|
|
92
|
+
"argumentHint": "[reproduce|test|assert|trace]",
|
|
93
|
+
"triggers": ["playwright test", "ui reproduction", "regression test", "e2e verify"],
|
|
94
|
+
"complete": ["playwright test", "reproduce issue", "save trace"]
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"name": "kxm-browser-diagnostics",
|
|
98
|
+
"argumentHint": "[check|cleanup|recover]",
|
|
99
|
+
"triggers": ["browser diagnostics", "cdp failure", "session timeout", "orphaned browser"],
|
|
100
|
+
"complete": ["check health", "cleanup orphans", "recover session"]
|
|
71
101
|
}
|
|
72
102
|
]
|
|
73
103
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kxm-browser-annotate
|
|
3
|
+
description: Capture a visual DOM section or element, attach structured annotations and change requests, and send them back to the agent.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# KXM Browser Section Capture & Visual Annotation Feedback
|
|
7
|
+
|
|
8
|
+
Use this skill to capture visual screenshots of specific UI sections or elements from a Steel browser session, record structured design/code annotations, and feed actionable change requests directly back to an AI coding agent.
|
|
9
|
+
|
|
10
|
+
## Purpose & Scope
|
|
11
|
+
|
|
12
|
+
- Enable human operators and critic agents to visually review web interfaces.
|
|
13
|
+
- Crop or capture specific DOM elements, cards, modals, or viewport bounding boxes.
|
|
14
|
+
- Attach structured notes (e.g. alignment issues, color contrast, missing data, layout bugs) with severity ratings.
|
|
15
|
+
- Provide a standardized Markdown/JSON feedback payload that an agent can parse and implement immediately.
|
|
16
|
+
|
|
17
|
+
## Workflow
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
1. CAPTURE SECTION
|
|
21
|
+
└─ Use Playwright element.screenshot() or agent-browser screenshot to isolate the target component.
|
|
22
|
+
|
|
23
|
+
2. ATTACH ANNOTATIONS
|
|
24
|
+
└─ Record bounding box / element selector, defect note, and severity rating.
|
|
25
|
+
|
|
26
|
+
3. ASSEMBLE FEEDBACK PACKAGE
|
|
27
|
+
└─ Package screenshot artifact, element selectors, notes, and concrete change list.
|
|
28
|
+
|
|
29
|
+
4. HANDOFF TO AGENT
|
|
30
|
+
└─ Inject formatted visual feedback into agent context or KXM workflow run.
|
|
31
|
+
|
|
32
|
+
5. AGENT IMPLEMENTS FIX
|
|
33
|
+
└─ Agent modifies code, re-captures the section, and verifies the change visually and with Playwright.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Capturing a Specific Section with Playwright
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { chromium } from "playwright";
|
|
40
|
+
import { resolveSteelConfig, formatCDPEndpoint } from "@kontextmind/kxm/runtime";
|
|
41
|
+
|
|
42
|
+
async function captureSection(sessionId: string, selector: string, outputPath: string) {
|
|
43
|
+
const config = resolveSteelConfig();
|
|
44
|
+
const cdpUrl = formatCDPEndpoint({ id: sessionId, websocketUrl: "" }, config);
|
|
45
|
+
|
|
46
|
+
const browser = await chromium.connectOverCDP(cdpUrl);
|
|
47
|
+
const context = browser.contexts()[0] || await browser.newContext();
|
|
48
|
+
const page = context.pages()[0] || await context.newPage();
|
|
49
|
+
|
|
50
|
+
const element = page.locator(selector);
|
|
51
|
+
await element.screenshot({ path: outputPath });
|
|
52
|
+
|
|
53
|
+
await browser.close();
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Structured Feedback Schema
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
{
|
|
61
|
+
"sessionId": "sess_12345",
|
|
62
|
+
"url": "https://app.example.com/settings/billing",
|
|
63
|
+
"sectionSelector": "[data-testid='subscription-card']",
|
|
64
|
+
"screenshotPath": ".kxm/artifacts/browser/billing-card.png",
|
|
65
|
+
"overallSummary": "Pricing tier badge overflows card boundary on narrow screens",
|
|
66
|
+
"annotations": [
|
|
67
|
+
{
|
|
68
|
+
"label": "Badge Overflow",
|
|
69
|
+
"selector": ".badge-tier",
|
|
70
|
+
"note": "Text overflows container when tier name is 'Enterprise Plus'",
|
|
71
|
+
"severity": "fix"
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"label": "Button Padding",
|
|
75
|
+
"selector": "button.upgrade-btn",
|
|
76
|
+
"note": "Increase vertical padding from 8px to 12px for touch target compliance",
|
|
77
|
+
"severity": "suggestion"
|
|
78
|
+
}
|
|
79
|
+
],
|
|
80
|
+
"requestedChanges": [
|
|
81
|
+
"Add `overflow: hidden` or `flex-wrap: wrap` to the subscription header container",
|
|
82
|
+
"Update `.badge-tier` CSS to support dynamic text wrapping",
|
|
83
|
+
"Adjust `button.upgrade-btn` padding to `py-3 px-4`"
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Formatting for the Agent
|
|
89
|
+
|
|
90
|
+
Use `formatAnnotationFeedbackPrompt()` from `@kontextmind/kxm/runtime` to render a clean, checklist-driven prompt that the agent executes step-by-step.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kxm-browser-auth
|
|
3
|
+
description: Retrieve application credentials and manage authenticated browser profiles safely via pass-cli without secret exposure.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# KXM Browser Credentials & Authenticated Profiles
|
|
7
|
+
|
|
8
|
+
Use this skill to retrieve target application credentials and manage browser session state securely using `pass-cli` as the sole authoritative store.
|
|
9
|
+
|
|
10
|
+
## Purpose & Scope
|
|
11
|
+
|
|
12
|
+
- Enforce `pass-cli` as the single source of truth for credentials and API keys.
|
|
13
|
+
- Prevent secrets from leaking into git repositories, logs, prompts, or model-visible tool outputs.
|
|
14
|
+
- Support safe storage and retrieval of session storage state and authenticated profiles.
|
|
15
|
+
|
|
16
|
+
## Credential Retrieval Guidelines
|
|
17
|
+
|
|
18
|
+
### 1. Authoritative Tool: pass-cli
|
|
19
|
+
|
|
20
|
+
Always retrieve credentials and API keys directly from `pass-cli`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Retrieve target login password into an environment variable or piping mechanism
|
|
24
|
+
pass-cli item view --vault-name "<vault>" --item-title "<title>" --field password
|
|
25
|
+
|
|
26
|
+
# Retrieve Steel infrastructure API key
|
|
27
|
+
pass-cli item view --vault-name "AI Provider Keys" --item-title "Steel Browser (KontextMind DOKS)" --field STEEL_API_KEY
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### 2. Secret Redaction Invariants
|
|
31
|
+
|
|
32
|
+
- **Never** write plain passwords, session tokens, or API keys into markdown docs, commit messages, or prompts.
|
|
33
|
+
- **Never** pass plain credentials as unredacted command line arguments in shared logs.
|
|
34
|
+
- Use environment variable injection (`pass-cli run`) or direct in-memory pipes.
|
|
35
|
+
|
|
36
|
+
### 3. Profile & Storage State Management
|
|
37
|
+
|
|
38
|
+
When an authenticated session state (cookies, local storage) needs to be preserved for subsequent test runs:
|
|
39
|
+
|
|
40
|
+
1. **Extract State**:
|
|
41
|
+
Extract storage state from Playwright via `context.storageState({ path: 'state.json' })` or from Steel via `GET /v1/sessions/:id/context`.
|
|
42
|
+
2. **Encrypt / Store Privately**:
|
|
43
|
+
Store sensitive storage state in git-ignored, private locations (e.g. `.kxm/state/browser/` or as an encrypted secret).
|
|
44
|
+
3. **Session Expiration**:
|
|
45
|
+
Treat cookies as transient. When expired, trigger the `kxm-browser-takeover` flow instead of failing silently.
|
|
46
|
+
4. **Account & Profile Separation**:
|
|
47
|
+
Maintain separate storage states per environment (e.g., `dev`, `staging`, `prod`) and per user role (e.g., `admin`, `viewer`). Never mix profiles across concurrent test runs.
|