auto-feedback 0.1.0 → 1.1.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/build/screenshot/capture.d.ts +2 -4
- package/build/screenshot/capture.js +33 -19
- package/build/session-manager.d.ts +7 -0
- package/build/session-manager.js +30 -0
- package/build/tools/index.js +6 -1
- package/build/tools/navigate.js +7 -9
- package/build/tools/screenshot-web.js +4 -3
- package/package.json +10 -3
|
@@ -12,10 +12,8 @@ export declare function capturePlaywrightPage(page: Page, options?: {
|
|
|
12
12
|
}): Promise<Buffer>;
|
|
13
13
|
/**
|
|
14
14
|
* Capture a screenshot of a Windows desktop window by PID.
|
|
15
|
-
* Uses
|
|
15
|
+
* Uses tasklist to resolve PID to process name, then node-screenshots
|
|
16
|
+
* to enumerate windows and match by appName.
|
|
16
17
|
* Returns raw PNG buffer for further optimization.
|
|
17
|
-
*
|
|
18
|
-
* Note: node-screenshots Window objects may not expose PID in all versions.
|
|
19
|
-
* Falls back to matching by window title if PID is unavailable.
|
|
20
18
|
*/
|
|
21
19
|
export declare function captureDesktopWindow(pid: number): Promise<Buffer>;
|
|
@@ -14,34 +14,48 @@ export async function capturePlaywrightPage(page, options) {
|
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
16
|
* Capture a screenshot of a Windows desktop window by PID.
|
|
17
|
-
* Uses
|
|
17
|
+
* Uses tasklist to resolve PID to process name, then node-screenshots
|
|
18
|
+
* to enumerate windows and match by appName.
|
|
18
19
|
* Returns raw PNG buffer for further optimization.
|
|
19
|
-
*
|
|
20
|
-
* Note: node-screenshots Window objects may not expose PID in all versions.
|
|
21
|
-
* Falls back to matching by window title if PID is unavailable.
|
|
22
20
|
*/
|
|
23
21
|
export async function captureDesktopWindow(pid) {
|
|
24
|
-
|
|
22
|
+
const { execSync } = await import("child_process");
|
|
25
23
|
const { Window } = await import("node-screenshots");
|
|
24
|
+
// Resolve PID to process name via tasklist
|
|
25
|
+
let processName;
|
|
26
|
+
try {
|
|
27
|
+
const output = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: "utf-8" }).trim();
|
|
28
|
+
// tasklist returns "INFO: No tasks are running..." when PID not found
|
|
29
|
+
if (output.includes("INFO:") || !output.startsWith('"')) {
|
|
30
|
+
throw new Error(`Could not find process name for PID ${pid}. The process may have exited.`);
|
|
31
|
+
}
|
|
32
|
+
// CSV format: "processname.exe","1234","Console","1","12,345 K"
|
|
33
|
+
const match = output.match(/^"([^"]+)"/);
|
|
34
|
+
if (!match) {
|
|
35
|
+
throw new Error(`Could not find process name for PID ${pid}. The process may have exited.`);
|
|
36
|
+
}
|
|
37
|
+
// Strip .exe suffix for matching against appName
|
|
38
|
+
processName = match[1].replace(/\.exe$/i, "");
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
if (error instanceof Error && error.message.includes("Could not find")) {
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Could not find process name for PID ${pid}. The process may have exited.`);
|
|
45
|
+
}
|
|
46
|
+
console.error(`[captureDesktopWindow] PID ${pid} resolved to process name: ${processName}`);
|
|
47
|
+
// Find matching window by appName
|
|
26
48
|
const windows = Window.all();
|
|
27
|
-
|
|
28
|
-
// but some builds include it at runtime. Use any cast for defensive check.
|
|
29
|
-
const target = windows.find((w) => {
|
|
30
|
-
const wAny = w;
|
|
31
|
-
if (typeof wAny.processId === "number")
|
|
32
|
-
return wAny.processId === pid;
|
|
33
|
-
if (typeof wAny.pid === "number")
|
|
34
|
-
return wAny.pid === pid;
|
|
35
|
-
if (typeof wAny.pid === "function")
|
|
36
|
-
return wAny.pid() === pid;
|
|
37
|
-
return false;
|
|
38
|
-
});
|
|
49
|
+
const target = windows.find((w) => w.appName.toLowerCase().includes(processName.toLowerCase()));
|
|
39
50
|
if (!target) {
|
|
40
|
-
throw new Error(`No window found for PID ${pid}.
|
|
51
|
+
throw new Error(`No window found for process "${processName}" (PID ${pid}). ` +
|
|
52
|
+
`The process may not have a visible window yet. ` +
|
|
53
|
+
`Available windows: ${windows.map((w) => w.appName).join(", ")}`);
|
|
41
54
|
}
|
|
42
55
|
if (target.isMinimized) {
|
|
43
|
-
throw new Error(`Window for PID ${pid} is minimized. Restore it before capturing.`);
|
|
56
|
+
throw new Error(`Window for "${processName}" (PID ${pid}) is minimized. Restore it before capturing.`);
|
|
44
57
|
}
|
|
58
|
+
console.error(`[captureDesktopWindow] Capturing window: "${target.appName}" (${target.width}x${target.height})`);
|
|
45
59
|
const image = await target.captureImage();
|
|
46
60
|
const pngData = await image.toPng();
|
|
47
61
|
return Buffer.from(pngData);
|
|
@@ -50,6 +50,13 @@ export declare class SessionManager {
|
|
|
50
50
|
* Remove a specific page reference
|
|
51
51
|
*/
|
|
52
52
|
removePageRef(sessionId: string, identifier: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Re-key all Maps associated with a page identifier.
|
|
55
|
+
* Used by navigate when goto changes the page URL.
|
|
56
|
+
* Updates: pageRefs, consoleCollectors, errorCollectors, networkCollectors, processCollectors.
|
|
57
|
+
* No-op for entries that don't exist under the old key.
|
|
58
|
+
*/
|
|
59
|
+
rekeyIdentifier(sessionId: string, oldIdentifier: string, newIdentifier: string): void;
|
|
53
60
|
/**
|
|
54
61
|
* Store the latest auto-captured screenshot for a session
|
|
55
62
|
*/
|
package/build/session-manager.js
CHANGED
|
@@ -87,6 +87,36 @@ export class SessionManager {
|
|
|
87
87
|
removePageRef(sessionId, identifier) {
|
|
88
88
|
this.pageRefs.delete(`${sessionId}:${identifier}`);
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Re-key all Maps associated with a page identifier.
|
|
92
|
+
* Used by navigate when goto changes the page URL.
|
|
93
|
+
* Updates: pageRefs, consoleCollectors, errorCollectors, networkCollectors, processCollectors.
|
|
94
|
+
* No-op for entries that don't exist under the old key.
|
|
95
|
+
*/
|
|
96
|
+
rekeyIdentifier(sessionId, oldIdentifier, newIdentifier) {
|
|
97
|
+
const oldKey = `${sessionId}:${oldIdentifier}`;
|
|
98
|
+
const newKey = `${sessionId}:${newIdentifier}`;
|
|
99
|
+
// Re-key page ref
|
|
100
|
+
const pageRef = this.pageRefs.get(oldKey);
|
|
101
|
+
if (pageRef) {
|
|
102
|
+
this.pageRefs.delete(oldKey);
|
|
103
|
+
this.pageRefs.set(newKey, pageRef);
|
|
104
|
+
}
|
|
105
|
+
// Re-key all collector maps (silently skip if old key not present)
|
|
106
|
+
const maps = [
|
|
107
|
+
this.consoleCollectors,
|
|
108
|
+
this.errorCollectors,
|
|
109
|
+
this.networkCollectors,
|
|
110
|
+
this.processCollectors,
|
|
111
|
+
];
|
|
112
|
+
for (const map of maps) {
|
|
113
|
+
const value = map.get(oldKey);
|
|
114
|
+
if (value) {
|
|
115
|
+
map.delete(oldKey);
|
|
116
|
+
map.set(newKey, value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
90
120
|
/**
|
|
91
121
|
* Store the latest auto-captured screenshot for a session
|
|
92
122
|
*/
|
package/build/tools/index.js
CHANGED
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
* MCP tool registration
|
|
3
3
|
*/
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { resolve, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
5
8
|
import { createToolError, createToolResult } from "../utils/errors.js";
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../../package.json"), "utf-8"));
|
|
6
11
|
import { registerCheckPortTool } from "./check-port.js";
|
|
7
12
|
import { registerLaunchWebServerTool } from "./launch-web-server.js";
|
|
8
13
|
import { registerLaunchElectronTool } from "./launch-electron.js";
|
|
@@ -30,7 +35,7 @@ export function registerTools(server, sessionManager) {
|
|
|
30
35
|
server.tool("get_version", "Get server version and capabilities. Use to verify the server is running and responsive.", {}, async () => {
|
|
31
36
|
return createToolResult({
|
|
32
37
|
name: "feedback",
|
|
33
|
-
version:
|
|
38
|
+
version: pkg.version,
|
|
34
39
|
status: "ready",
|
|
35
40
|
capabilities: [
|
|
36
41
|
"process_lifecycle",
|
package/build/tools/navigate.js
CHANGED
|
@@ -70,16 +70,14 @@ export function registerNavigateTool(server, sessionManager) {
|
|
|
70
70
|
waitUntil: effectiveWaitUntil,
|
|
71
71
|
timeout: effectiveTimeout,
|
|
72
72
|
});
|
|
73
|
-
//
|
|
74
|
-
// so page discovery
|
|
73
|
+
// Re-key page ref AND all collector maps atomically
|
|
74
|
+
// so page discovery and diagnostic lookups work with the new URL
|
|
75
75
|
if (pageType === "web" && currentIdentifier !== "electron") {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
url: url,
|
|
82
|
-
});
|
|
76
|
+
sessionManager.rekeyIdentifier(sessionId, currentIdentifier, url);
|
|
77
|
+
// Update the URL field in the re-keyed page ref
|
|
78
|
+
const updatedRef = sessionManager.getPageRef(sessionId, url);
|
|
79
|
+
if (updatedRef) {
|
|
80
|
+
updatedRef.url = url;
|
|
83
81
|
}
|
|
84
82
|
}
|
|
85
83
|
}
|
|
@@ -60,8 +60,7 @@ export function registerScreenshotWebTool(server, sessionManager) {
|
|
|
60
60
|
viewport: { width: 1280, height: 720 },
|
|
61
61
|
});
|
|
62
62
|
const page = await context.newPage();
|
|
63
|
-
|
|
64
|
-
// Store page reference for reuse
|
|
63
|
+
// Store page reference before goto (URL is known from parameter)
|
|
65
64
|
pageRef = {
|
|
66
65
|
type: "web",
|
|
67
66
|
page,
|
|
@@ -72,7 +71,7 @@ export function registerScreenshotWebTool(server, sessionManager) {
|
|
|
72
71
|
sessionManager.setPageRef(sessionId, url, pageRef);
|
|
73
72
|
// Attach auto-capture on navigation
|
|
74
73
|
setupAutoCapture(page, sessionId, sessionManager);
|
|
75
|
-
// Attach diagnostic collectors
|
|
74
|
+
// Attach diagnostic collectors BEFORE goto so initial load events are captured
|
|
76
75
|
const consoleCollector = attachConsoleCollector(page);
|
|
77
76
|
const errorCollector = attachErrorCollector(page);
|
|
78
77
|
const networkCollector = attachNetworkCollector(page);
|
|
@@ -87,6 +86,8 @@ export function registerScreenshotWebTool(server, sessionManager) {
|
|
|
87
86
|
await browser.close().catch(() => { });
|
|
88
87
|
},
|
|
89
88
|
});
|
|
89
|
+
// Navigate AFTER collectors are attached to capture all initial load events
|
|
90
|
+
await page.goto(url, { waitUntil: "load", timeout: 30000 });
|
|
90
91
|
console.error(`[screenshot_web] Browser ready for ${url}`);
|
|
91
92
|
}
|
|
92
93
|
else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auto-feedback",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for GUI testing and feedback - gives Claude Code eyes and hands",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"dev": "tsx src/index.ts",
|
|
15
|
-
"prepublishOnly": "npm run build"
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"test:unit": "vitest run tests/unit",
|
|
19
|
+
"test:integration": "vitest run tests/integration"
|
|
16
20
|
},
|
|
17
21
|
"dependencies": {
|
|
18
22
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -27,8 +31,11 @@
|
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@types/node": "^20.0.0",
|
|
29
33
|
"@types/wait-on": "^5.3.4",
|
|
34
|
+
"electron": "^40.2.1",
|
|
30
35
|
"tsx": "^4.0.0",
|
|
31
|
-
"typescript": "^5.0.0"
|
|
36
|
+
"typescript": "^5.0.0",
|
|
37
|
+
"vite": "^7.3.1",
|
|
38
|
+
"vitest": "^4.0.18"
|
|
32
39
|
},
|
|
33
40
|
"engines": {
|
|
34
41
|
"node": ">=18.0.0"
|