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.
@@ -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 node-screenshots to enumerate windows and match by process ID.
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 node-screenshots to enumerate windows and match by process ID.
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
- // Dynamic import to avoid load-time failures on non-Windows
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
- // node-screenshots Window type doesn't expose pid in current typings,
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}. The process may not have a visible window yet.`);
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
  */
@@ -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
  */
@@ -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: "0.1.0",
38
+ version: pkg.version,
34
39
  status: "ready",
35
40
  capabilities: [
36
41
  "process_lifecycle",
@@ -70,16 +70,14 @@ export function registerNavigateTool(server, sessionManager) {
70
70
  waitUntil: effectiveWaitUntil,
71
71
  timeout: effectiveTimeout,
72
72
  });
73
- // Update PageReference URL in SessionManager for web pages
74
- // so page discovery continues to work with the new URL
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
- const oldRef = sessionManager.getPageRef(sessionId, currentIdentifier);
77
- if (oldRef) {
78
- sessionManager.removePageRef(sessionId, currentIdentifier);
79
- sessionManager.setPageRef(sessionId, url, {
80
- ...oldRef,
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
- await page.goto(url, { waitUntil: "load", timeout: 30000 });
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": "0.1.0",
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"