whisker-ux 0.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.
@@ -0,0 +1,231 @@
1
+ import { chromium } from "playwright";
2
+ function mapKeyNames(key) {
3
+ const keyMap = {
4
+ return: "Enter",
5
+ enter: "Enter",
6
+ ctrl: "Control",
7
+ control: "Control",
8
+ super: "Meta",
9
+ cmd: "Meta",
10
+ command: "Meta",
11
+ alt: "Alt",
12
+ shift: "Shift",
13
+ tab: "Tab",
14
+ escape: "Escape",
15
+ esc: "Escape",
16
+ backspace: "Backspace",
17
+ delete: "Delete",
18
+ space: " ",
19
+ arrowup: "ArrowUp",
20
+ arrowdown: "ArrowDown",
21
+ arrowleft: "ArrowLeft",
22
+ arrowright: "ArrowRight",
23
+ up: "ArrowUp",
24
+ down: "ArrowDown",
25
+ left: "ArrowLeft",
26
+ right: "ArrowRight",
27
+ pageup: "PageUp",
28
+ pagedown: "PageDown",
29
+ page_up: "PageUp",
30
+ page_down: "PageDown",
31
+ home: "Home",
32
+ end: "End",
33
+ };
34
+ return key
35
+ .split("+")
36
+ .map((part) => {
37
+ const lower = part.trim().toLowerCase();
38
+ return keyMap[lower] ?? part.trim();
39
+ })
40
+ .join("+");
41
+ }
42
+ export class BrowserManager {
43
+ config;
44
+ browser = null;
45
+ context = null;
46
+ page = null;
47
+ consoleErrors = [];
48
+ networkFailures = [];
49
+ constructor(config) {
50
+ this.config = config;
51
+ }
52
+ async launch() {
53
+ this.browser = await chromium.launch({
54
+ headless: false,
55
+ args: [
56
+ `--window-size=${this.config.viewport.width},${this.config.viewport.height + 100}`,
57
+ ],
58
+ });
59
+ this.context = await this.browser.newContext({
60
+ viewport: {
61
+ width: this.config.viewport.width,
62
+ height: this.config.viewport.height,
63
+ },
64
+ });
65
+ this.page = await this.context.newPage();
66
+ // Capture console errors
67
+ this.page.on("console", (msg) => {
68
+ if (msg.type() === "error") {
69
+ this.consoleErrors.push(`[${new Date().toISOString()}] ${msg.text()}`);
70
+ }
71
+ });
72
+ // Capture unhandled page errors
73
+ this.page.on("pageerror", (err) => {
74
+ this.consoleErrors.push(`[${new Date().toISOString()}] Uncaught: ${err.message}`);
75
+ });
76
+ // Capture network failures (4xx/5xx)
77
+ this.page.on("response", (response) => {
78
+ if (response.status() >= 400) {
79
+ this.networkFailures.push({
80
+ url: response.url(),
81
+ method: response.request().method(),
82
+ status: response.status(),
83
+ statusText: response.statusText(),
84
+ timestamp: Date.now(),
85
+ });
86
+ }
87
+ });
88
+ // Capture failed requests (network errors)
89
+ this.page.on("requestfailed", (request) => {
90
+ this.networkFailures.push({
91
+ url: request.url(),
92
+ method: request.method(),
93
+ status: 0,
94
+ statusText: request.failure()?.errorText ?? "Request failed",
95
+ timestamp: Date.now(),
96
+ });
97
+ });
98
+ // Handle popups - switch to new page
99
+ this.context.on("page", (newPage) => {
100
+ this.page = newPage;
101
+ newPage.on("console", (msg) => {
102
+ if (msg.type() === "error") {
103
+ this.consoleErrors.push(`[${new Date().toISOString()}] ${msg.text()}`);
104
+ }
105
+ });
106
+ newPage.on("pageerror", (err) => {
107
+ this.consoleErrors.push(`[${new Date().toISOString()}] Uncaught: ${err.message}`);
108
+ });
109
+ });
110
+ await this.page.goto(this.config.url, { waitUntil: "domcontentloaded" });
111
+ }
112
+ async takeScreenshot() {
113
+ if (!this.page)
114
+ throw new Error("Browser not launched");
115
+ const buffer = await this.page.screenshot({
116
+ type: "png",
117
+ fullPage: false,
118
+ });
119
+ return buffer.toString("base64");
120
+ }
121
+ async executeAction(action) {
122
+ if (!this.page)
123
+ throw new Error("Browser not launched");
124
+ const page = this.page;
125
+ switch (action.action) {
126
+ case "screenshot":
127
+ // No-op: screenshot is taken separately by the caller
128
+ break;
129
+ case "left_click":
130
+ if (action.coordinate) {
131
+ await page.mouse.click(action.coordinate[0], action.coordinate[1], {
132
+ button: "left",
133
+ });
134
+ }
135
+ break;
136
+ case "right_click":
137
+ if (action.coordinate) {
138
+ await page.mouse.click(action.coordinate[0], action.coordinate[1], {
139
+ button: "right",
140
+ });
141
+ }
142
+ break;
143
+ case "middle_click":
144
+ if (action.coordinate) {
145
+ await page.mouse.click(action.coordinate[0], action.coordinate[1], {
146
+ button: "middle",
147
+ });
148
+ }
149
+ break;
150
+ case "double_click":
151
+ if (action.coordinate) {
152
+ await page.mouse.dblclick(action.coordinate[0], action.coordinate[1]);
153
+ }
154
+ break;
155
+ case "triple_click":
156
+ if (action.coordinate) {
157
+ await page.mouse.click(action.coordinate[0], action.coordinate[1], {
158
+ clickCount: 3,
159
+ });
160
+ }
161
+ break;
162
+ case "type":
163
+ if (action.text) {
164
+ await page.keyboard.type(action.text, { delay: 30 });
165
+ }
166
+ break;
167
+ case "key":
168
+ if (action.text) {
169
+ const mapped = mapKeyNames(action.text);
170
+ await page.keyboard.press(mapped);
171
+ }
172
+ break;
173
+ case "mouse_move":
174
+ if (action.coordinate) {
175
+ await page.mouse.move(action.coordinate[0], action.coordinate[1]);
176
+ }
177
+ break;
178
+ case "scroll":
179
+ if (action.coordinate && action.scroll_direction) {
180
+ await page.mouse.move(action.coordinate[0], action.coordinate[1]);
181
+ const amount = (action.scroll_amount ?? 3) * 100;
182
+ const deltaX = action.scroll_direction === "left"
183
+ ? -amount
184
+ : action.scroll_direction === "right"
185
+ ? amount
186
+ : 0;
187
+ const deltaY = action.scroll_direction === "down"
188
+ ? amount
189
+ : action.scroll_direction === "up"
190
+ ? -amount
191
+ : 0;
192
+ await page.mouse.wheel(deltaX, deltaY);
193
+ }
194
+ break;
195
+ case "left_click_drag":
196
+ if (action.coordinate) {
197
+ const startX = action.start_coordinate?.[0] ?? 0;
198
+ const startY = action.start_coordinate?.[1] ?? 0;
199
+ await page.mouse.move(startX, startY);
200
+ await page.mouse.down();
201
+ await page.mouse.move(action.coordinate[0], action.coordinate[1]);
202
+ await page.mouse.up();
203
+ }
204
+ break;
205
+ case "wait":
206
+ await new Promise((resolve) => setTimeout(resolve, 2000));
207
+ break;
208
+ default:
209
+ console.warn(` Unknown action type: ${action.action}`);
210
+ }
211
+ // Brief pause after actions to let the page settle
212
+ if (action.action !== "screenshot" && action.action !== "wait") {
213
+ await new Promise((resolve) => setTimeout(resolve, 500));
214
+ }
215
+ }
216
+ async close() {
217
+ if (this.browser) {
218
+ await this.browser.close();
219
+ this.browser = null;
220
+ this.context = null;
221
+ this.page = null;
222
+ }
223
+ }
224
+ getConsoleErrors() {
225
+ return [...this.consoleErrors];
226
+ }
227
+ getNetworkFailures() {
228
+ return [...this.networkFailures];
229
+ }
230
+ }
231
+ //# sourceMappingURL=browser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import path from "path";
4
+ import { runSession } from "./agent.js";
5
+ import { writeReport } from "./report.js";
6
+ import { getApiKey, runSetup, getConfigPath, deleteApiKey } from "./config.js";
7
+ import { printBanner, printConfig, printResults, printError } from "./ui.js";
8
+ const program = new Command();
9
+ program
10
+ .name("whisker")
11
+ .description("AI-powered usability testing CLI using Claude computer use")
12
+ .version("0.1.0");
13
+ // Setup command
14
+ program
15
+ .command("setup")
16
+ .description("Configure your Anthropic API key")
17
+ .action(async () => {
18
+ await runSetup();
19
+ });
20
+ // Logout command
21
+ program
22
+ .command("logout")
23
+ .description("Remove stored API key")
24
+ .action(() => {
25
+ const deleted = deleteApiKey();
26
+ if (deleted) {
27
+ console.log("API key removed from", getConfigPath());
28
+ }
29
+ else {
30
+ console.log("No stored API key found.");
31
+ }
32
+ });
33
+ // Run command (main functionality)
34
+ program
35
+ .command("run", { isDefault: true })
36
+ .description("Run a usability test")
37
+ .argument("<task>", "Task description (e.g., 'Add a product to cart')")
38
+ .requiredOption("-u, --url <url>", "URL of the site to test")
39
+ .option("-p, --persona <persona>", "Persona description for the tester")
40
+ .option("-m, --max-steps <number>", "Maximum number of steps", "50")
41
+ .option("-v, --viewport <WxH>", "Viewport size (e.g., 1280x800)", "1280x800")
42
+ .option("-o, --output <dir>", "Output directory", ".whisker")
43
+ .action(async (task, opts) => {
44
+ // Get API key from config or environment
45
+ const apiKey = getApiKey();
46
+ if (!apiKey) {
47
+ printError(`No Anthropic API key configured.\n\nRun setup: npx tsx src/cli.ts setup\nOr set env: export ANTHROPIC_API_KEY=sk-ant-...\n\nConfig: ${getConfigPath()}`);
48
+ process.exit(1);
49
+ }
50
+ // Set for the Anthropic SDK to pick up
51
+ process.env.ANTHROPIC_API_KEY = apiKey;
52
+ // Parse viewport
53
+ const viewportParts = opts.viewport.split("x");
54
+ if (viewportParts.length !== 2) {
55
+ printError("Invalid viewport format. Use WxH (e.g., 1280x800)");
56
+ process.exit(1);
57
+ }
58
+ const width = parseInt(viewportParts[0], 10);
59
+ const height = parseInt(viewportParts[1], 10);
60
+ if (isNaN(width) || isNaN(height) || width <= 0 || height <= 0) {
61
+ printError("Invalid viewport dimensions. Use positive integers.");
62
+ process.exit(1);
63
+ }
64
+ // Parse max steps
65
+ const maxSteps = parseInt(opts.maxSteps, 10);
66
+ if (isNaN(maxSteps) || maxSteps <= 0) {
67
+ printError("max-steps must be a positive integer");
68
+ process.exit(1);
69
+ }
70
+ const config = {
71
+ task,
72
+ url: opts.url,
73
+ persona: opts.persona,
74
+ maxSteps,
75
+ viewport: { width, height },
76
+ outputDir: opts.output,
77
+ };
78
+ // Print banner and config
79
+ printBanner();
80
+ printConfig(config.task, config.url, config.persona, config.maxSteps, `${config.viewport.width}x${config.viewport.height}`);
81
+ try {
82
+ const { report, sessionLog } = await runSession(config);
83
+ const { markdownPath, jsonPath, screenshotDir } = await writeReport(report, sessionLog, config.outputDir);
84
+ // Print results with the new UI
85
+ const absoluteOutputDir = path.resolve(config.outputDir);
86
+ printResults(report.taskCompleted, report.summary, report.findings, config.outputDir, absoluteOutputDir);
87
+ }
88
+ catch (err) {
89
+ printError(err instanceof Error ? err.message : String(err));
90
+ process.exit(1);
91
+ }
92
+ });
93
+ program.parse();
94
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export declare function getApiKey(): string | undefined;
2
+ export declare function saveApiKey(apiKey: string): void;
3
+ export declare function runSetup(): Promise<void>;
4
+ export declare function getConfigPath(): string;
5
+ export declare function deleteApiKey(): boolean;
package/dist/config.js ADDED
@@ -0,0 +1,104 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import * as readline from "node:readline";
5
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "whisker");
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
+ export function getApiKey() {
8
+ // Environment variable takes precedence
9
+ if (process.env.ANTHROPIC_API_KEY) {
10
+ return process.env.ANTHROPIC_API_KEY;
11
+ }
12
+ // Try reading from config file
13
+ try {
14
+ if (fs.existsSync(CONFIG_FILE)) {
15
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
16
+ const config = JSON.parse(content);
17
+ return config.anthropicApiKey;
18
+ }
19
+ }
20
+ catch {
21
+ // Ignore errors reading config
22
+ }
23
+ return undefined;
24
+ }
25
+ export function saveApiKey(apiKey) {
26
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
27
+ let config = {};
28
+ try {
29
+ if (fs.existsSync(CONFIG_FILE)) {
30
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
31
+ config = JSON.parse(content);
32
+ }
33
+ }
34
+ catch {
35
+ // Start fresh if config is corrupted
36
+ }
37
+ config.anthropicApiKey = apiKey;
38
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
39
+ fs.chmodSync(CONFIG_FILE, 0o600); // Read/write only for owner
40
+ }
41
+ export async function runSetup() {
42
+ const rl = readline.createInterface({
43
+ input: process.stdin,
44
+ output: process.stdout,
45
+ });
46
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
47
+ console.log("");
48
+ console.log("Whisker Setup");
49
+ console.log("─────────────");
50
+ console.log("");
51
+ console.log("Whisker needs an Anthropic API key to use Claude for testing.");
52
+ console.log("Get one at: https://console.anthropic.com/settings/keys");
53
+ console.log("");
54
+ const existingKey = getApiKey();
55
+ if (existingKey) {
56
+ console.log(`Current key: ${existingKey.slice(0, 12)}...${existingKey.slice(-4)}`);
57
+ const overwrite = await question("Replace existing key? (y/N): ");
58
+ if (overwrite.toLowerCase() !== "y") {
59
+ console.log("Setup cancelled.");
60
+ rl.close();
61
+ return;
62
+ }
63
+ }
64
+ const apiKey = await question("Enter your Anthropic API key: ");
65
+ rl.close();
66
+ if (!apiKey.trim()) {
67
+ console.log("No key provided. Setup cancelled.");
68
+ return;
69
+ }
70
+ if (!apiKey.startsWith("sk-ant-")) {
71
+ console.log("Warning: Key doesn't look like an Anthropic API key (should start with sk-ant-)");
72
+ }
73
+ saveApiKey(apiKey.trim());
74
+ console.log("");
75
+ console.log(`API key saved to ${CONFIG_FILE}`);
76
+ console.log("You're ready to use Whisker!");
77
+ }
78
+ export function getConfigPath() {
79
+ return CONFIG_FILE;
80
+ }
81
+ export function deleteApiKey() {
82
+ try {
83
+ if (fs.existsSync(CONFIG_FILE)) {
84
+ const content = fs.readFileSync(CONFIG_FILE, "utf-8");
85
+ const config = JSON.parse(content);
86
+ if (config.anthropicApiKey) {
87
+ delete config.anthropicApiKey;
88
+ // If config is now empty, delete the file
89
+ if (Object.keys(config).length === 0) {
90
+ fs.unlinkSync(CONFIG_FILE);
91
+ }
92
+ else {
93
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
94
+ }
95
+ return true;
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ // Ignore errors
101
+ }
102
+ return false;
103
+ }
104
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { runSession } from "./agent.js";
2
+ export { writeReport } from "./report.js";
3
+ export type { WhiskerConfig, WhiskerReport, Finding, SessionLog, SessionStep, NetworkFailure, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Main exports for programmatic usage
2
+ export { runSession } from "./agent.js";
3
+ export { writeReport } from "./report.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { WhiskerConfig } from "./types.js";
2
+ export declare function getNavigationSystemPrompt(config: WhiskerConfig): string;
3
+ export declare function getReportSystemPrompt(): string;
@@ -0,0 +1,81 @@
1
+ export function getNavigationSystemPrompt(config) {
2
+ const personaSection = config.persona
3
+ ? `\n\n## Persona\nYou are acting as the following persona: ${config.persona}\nConsider this persona's technical ability, familiarity with similar products, and expectations when evaluating the experience.`
4
+ : "";
5
+ return `You are an expert usability tester evaluating a web application. Your job is to complete a specific task while carefully observing and narrating the user experience.
6
+
7
+ ## Your Task
8
+ ${config.task}
9
+
10
+ ## Starting URL
11
+ ${config.url}
12
+ ${personaSection}
13
+
14
+ ## Instructions
15
+
16
+ 1. **Think aloud**: Before each action, describe what you see on screen, what you're trying to do, and any observations about the UX. Be specific and detailed about what you observe.
17
+
18
+ 2. **Act like a real user**: Navigate the site naturally. Don't use shortcuts a normal user wouldn't know about. If something is confusing, say so explicitly.
19
+
20
+ 3. **Watch for issues** as you navigate:
21
+ - **Bugs**: Broken buttons, errors, incorrect behavior, console errors
22
+ - **UX friction**: Confusing labels, unexpected flows, too many steps, missing feedback
23
+ - **Accessibility**: Missing labels, poor contrast, keyboard navigation issues
24
+ - **Performance**: Slow loads, unresponsive elements, layout shifts
25
+ - **Visual issues**: Overlapping elements, misalignment, truncated text, broken layouts
26
+ - **Copy issues**: Confusing or misleading text, typos, jargon
27
+
28
+ 4. **Take a screenshot after each action** to verify the result. If something didn't work as expected, note it clearly and try an alternative approach.
29
+
30
+ 5. **Complete the task or explain why you cannot**. If you get stuck after 3 attempts at the same thing, describe what blocked you and stop.
31
+
32
+ 6. **When finished**, provide a clear final summary of:
33
+ - Whether the task was completed successfully
34
+ - The overall experience quality
35
+ - The most significant issues encountered
36
+
37
+ Start by taking a screenshot to see the current state of the page.`;
38
+ }
39
+ export function getReportSystemPrompt() {
40
+ return `You are an expert usability analyst. You are given the session log and screenshots from an AI-driven usability test of a web application. Your job is to analyze the session and produce a structured JSON report of findings.
41
+
42
+ Carefully examine each screenshot to identify visual issues, layout problems, confusing UI elements, accessibility concerns, and bugs that may not be obvious from the text observations alone.
43
+
44
+ ## Output Format
45
+
46
+ Return ONLY a valid JSON object (no markdown fences, no extra text) matching this exact schema:
47
+
48
+ {
49
+ "summary": "2-3 sentence overview of the test session and overall UX quality",
50
+ "taskCompleted": true or false,
51
+ "taskCompletionNotes": "Explanation of whether and how the task was completed, or what blocked it",
52
+ "findings": [
53
+ {
54
+ "id": "F001",
55
+ "severity": "critical | major | minor | suggestion",
56
+ "category": "bug | ux-friction | accessibility | performance | visual | copy",
57
+ "title": "Short descriptive title",
58
+ "description": "Detailed description of the issue, including what was expected vs what happened",
59
+ "stepsToReproduce": ["Step 1", "Step 2"],
60
+ "screenshotStepNumber": 5,
61
+ "suggestedFix": "Actionable suggestion for how to fix this",
62
+ "grepPatterns": ["patterns to search the codebase for relevant code"]
63
+ }
64
+ ]
65
+ }
66
+
67
+ ## Severity Definitions
68
+ - **critical**: Completely blocks the user from completing the task
69
+ - **major**: Significant friction or confusion that most users would struggle with
70
+ - **minor**: Noticeable issue but doesn't significantly impede the task
71
+ - **suggestion**: Improvement idea that would enhance the experience
72
+
73
+ ## Guidelines
74
+ - Assign IDs sequentially: F001, F002, etc.
75
+ - Be specific about which step number demonstrated each issue (screenshotStepNumber)
76
+ - For grepPatterns, suggest patterns that would help find relevant code: CSS class names, button text, component names, API endpoints
77
+ - Only report genuine issues backed by evidence from the session, not speculative concerns
78
+ - Consolidate duplicate or overlapping observations into single findings
79
+ - Order findings by severity (critical first, then major, minor, suggestion)`;
80
+ }
81
+ //# sourceMappingURL=prompts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../src/prompts.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,yBAAyB,CAAC,MAAqB;IAC7D,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO;QACnC,CAAC,CAAC,4DAA4D,MAAM,CAAC,OAAO,kIAAkI;QAC9M,CAAC,CAAC,EAAE,CAAC;IAEP,OAAO;;;EAGP,MAAM,CAAC,IAAI;;;EAGX,MAAM,CAAC,GAAG;EACV,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;mEAyBmD,CAAC;AACpE,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;6EAuCoE,CAAC;AAC9E,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { WhiskerReport, SessionLog } from "./types.js";
2
+ export declare function writeReport(report: WhiskerReport, sessionLog: SessionLog, outputDir: string): Promise<{
3
+ markdownPath: string;
4
+ jsonPath: string;
5
+ screenshotDir: string;
6
+ }>;