devharness 0.9.2 → 0.9.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devharness",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "MCP server that connects AI assistants to Chrome DevTools Protocol for runtime debugging - set breakpoints, inspect variables, monitor network traffic, and automate browser interactions",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
@@ -2,194 +2,109 @@
2
2
  name: devharness
3
3
  description: Drive and debug a running app via the devharness MCP server - launch or attach to Chrome and Node.js, set breakpoints and logpoints, inspect call stacks and variables, watch console and network, manage dev servers, replay any earlier tool call by its history index, and record reproduction sequences that verify a fix. Use whenever a task involves running or debugging a live app, reproducing or verifying a bug, re-driving setup you already did (relaunching, re-logging in, refilling a form), or the user mentions breakpoints, Chrome DevTools, CDP, replay sequences, or devharness tools (launchChrome, navigate, breakpoint, inspect, replay, server, issues, etc.).
4
4
  compatibility: Requires the devharness MCP server to be connected (tools such as launchChrome, breakpoint, inspect, replay, server, issues). Previously published as cdp-tools-mcp.
5
- version: 0.9.2
5
+ version: 0.9.3
6
6
  ---
7
7
 
8
8
  # devharness
9
9
 
10
- Chrome DevTools Protocol debugging for JavaScript/TypeScript in Chrome, Node.js, or CDP-compatible environments.
10
+ CDP debugging for JS/TS in Chrome, Node.js, or any CDP target.
11
11
 
12
- ## Quick Start
12
+ ## Quick start
13
13
 
14
- **Web apps (most common):**
15
- ```
16
- 1. launchChrome({ reference: "your-descriptive-name" }) # Auto-connects, ready immediately
17
- 2. navigate({ action: 'goto', connectionReason: "your-descriptive-name", url: "..." })
18
- # Navigation automatically caches interactive elements (links, buttons, inputs) for the page
19
- 3. content({ action: 'findInteractive', connectionReason: "your-descriptive-name" })
20
- # Shows summary of all interactive elements. Use search/types to filter
21
- 4. content({ action: 'extractText', mode: 'outline' }) # Read page content (preferred over screenshot)
22
- 5. Use other tools as needed with connectionReason parameter
23
- ```
14
+ Every tool takes `connectionReason` — the name you gave the connection.
24
15
 
25
- **Alternative (rename later):**
26
16
  ```
27
- 1. launchChrome() # Uses default "unnamed-connection-default"
28
- 2. tab({ action: 'rename', reference: "unnamed-connection-default", newReference: "your-name" })
29
- 3. Use other tools with connectionReason: "your-name"
17
+ launchChrome({ reference: "app" }) # launches AND connects; do NOT then call connectDebugger
18
+ navigate({ action: 'goto', connectionReason: "app", url }) # caches interactive elements
19
+ content({ action: 'findInteractive' }) # summary; filter with search/types
20
+ content({ action: 'extractText', mode: 'outline' }) # prefer over screenshot
30
21
  ```
31
22
 
32
- **Node.js debugging:**
33
- ```
34
- 1. Start app: node --inspect=9229 app.js
35
- 2. connectDebugger({ reference: "my-app-debug", port: 9229 })
36
- 3. breakpoint({ action: 'set', connectionReason: "my-app-debug", ... })
37
- ```
23
+ Node: `node --inspect=9229 app.js` → `connectDebugger({ reference: "api", port: 9229 })`.
24
+ `connectDebugger` is only for existing Node/remote debuggers.
25
+
26
+ Launched without a reference? `tab({ action: 'rename', reference: "unnamed-connection-default", newReference: "app" })`.
27
+
28
+ Paused: `inspect({ action: 'getCallStack' })` → `getVariables` → `evaluateExpression`.
29
+ Watch: `console({ action: 'list' })`, `network({ action: 'list' })` (needs `network({ action: 'enable' })` first).
30
+
31
+ ## `.devharness/` must be git-ignored
32
+
33
+ State lands in `.devharness/` — config, server claims, logs, sequences, issues. Machine-local; carries pids, ports, and local paths into what may be a public repo.
34
+
35
+ Before the first tool that writes there (`server`, `replay` record, `issues`, `setDebugLogging`) in a git repo:
38
36
 
39
- ## Basic Workflow
40
-
41
- 1. **Connect**:
42
- - `launchChrome({ reference: "name" })` - Launches AND auto-connects (ready immediately, don't call connectDebugger)
43
- - `connectDebugger({ reference: "name" })` - Only for existing Node.js/remote debuggers
44
- 2. **Navigate & interact**: Use connectionReason in all tool calls
45
- - `navigate({ action: 'goto', connectionReason: "name", url: "..." })`
46
- - `input({ action: 'click', connectionReason: "name", selector: "..." })`
47
- 3. **Debug**: `breakpoint({ action: 'set', connectionReason: "name", ... })`
48
- 4. **Inspect when paused**: `inspect({ action: 'getCallStack', ... })` → `inspect({ action: 'getVariables', ... })`
49
- 5. **Monitor**: `console({ action: 'list', connectionReason: "name" })`, `network({ action: 'list', connectionReason: "name" })`
50
-
51
- ## Key Practices
52
-
53
- **Breakpoints:**
54
- - Use conditional: `breakpoint({ action: 'set', condition: "userId === '123'" })`
55
- - Prefer `breakpoint({ action: 'setLogpoint' })` for loops/high-frequency code
56
- - Clean up with `breakpoint({ action: 'remove' })` or check `breakpoint({ action: 'list' })`
57
-
58
- **DOM/Event/XHR Breakpoints (Chrome only):**
59
- - `breakpoint({ action: 'setDOMBreakpoint' })`: Pause when element changes
60
- - `subtree-modified`: Children added/removed
61
- - `attribute-modified`: Attributes changed (class, style, etc.)
62
- - `node-removed`: Element deleted from DOM
63
- - `breakpoint({ action: 'setEventBreakpoint' })`: Pause when events fire (click, submit, input, keydown, etc.)
64
- - `breakpoint({ action: 'setXHRBreakpoint' })`: Pause when XHR/Fetch URL contains pattern
65
- - Example: `breakpoint({ action: 'setDOMBreakpoint', selector: '.todo-list', domBreakpointType: 'subtree-modified' })`
66
- - Note: DOM breakpoints use nodeIds which are invalidated on page reload
67
-
68
- **Code search:**
69
- - `inspect({ action: 'searchCode' })`: Find patterns
70
- - `inspect({ action: 'searchFunctions' })`: Locate definitions
71
- - `getSourceCode`: View context
72
-
73
- **Modal handling:**
74
- - Use `handleModals: true` on `input({ action: 'click' | 'type' | 'hover' })`
75
- - Strategies: `auto` (smart), `accept`, `reject`, `close`, `remove`
76
- - Example: `input({ action: 'click', selector: '.btn', handleModals: true, dismissStrategy: 'auto' })`
77
- - Limitation: English-only, no Shadow DOM/iframes
78
-
79
- **Multiple connections:**
80
- - `listConnections` → `switchConnection`
81
- - Each connection = separate tab/process
82
-
83
- **Re-running work you already did:**
84
- - Every tool response carries its own history index in the footer:
85
- `**Repeat:** replay({ action: 'repeat', indices: [58] })`. That is not only
86
- for failures - it is on every call, all the time. (The `replay` tool's own
87
- responses are the exception: replay calls are not recorded into history)
88
- - `indices` takes a **list**, so a whole stretch of work replays in one call:
89
- `replay({ action: 'repeat', indices: [58, 59, 60, 61] })` re-runs those four
90
- steps in order
91
- - Reach for this whenever you are about to redo something you already did -
92
- a browser relaunch, re-logging in, retyping a form, getting back to the
93
- screen where a bug appears. Re-issuing the calls by hand is slower, and
94
- retyped arguments drift from what actually ran
95
- - `replay({ action: 'history' })` lists the indices when they have scrolled
96
- out of view
97
- - Each repeated call replays against the connection it was recorded with, so a
98
- batch spanning two browsers stays on both. Pass `connectionReason` on every
99
- call while driving multiple browsers and this holds; drive one implicitly and
100
- those calls have no connection to replay against
101
- - If the stretch is worth keeping, turn it into a sequence:
102
- `replay({ action: 'create', name: '...', indices: [58, 59, 60, 61] })`
103
-
104
- ## Common Patterns
105
-
106
- **Bug debugging:**
107
- 1. `launchChrome` → `navigate({ action: 'goto' })`
108
- 2. `inspect({ action: 'searchCode' | 'searchFunctions' })`
109
- 3. `breakpoint({ action: 'set' | 'setLogpoint' })`
110
- 4. Trigger bug
111
- 5. `inspect({ action: 'getCallStack' })` + `inspect({ action: 'getVariables' })`
112
- 6. `inspect({ action: 'evaluateExpression' })`
113
-
114
- **Performance:**
115
- 1. `network({ action: 'enable' })`
116
- 2. `navigate({ action: 'goto' })`
117
- 3. `network({ action: 'search' })` (find slow)
118
- 4. `network({ action: 'get' })` (timing)
119
- 5. `breakpoint({ action: 'setLogpoint' })` in slow paths
120
-
121
- **Frontend state:**
122
- 1. `dom({ action: 'querySelector' })` + `dom({ action: 'getProperties' })`
123
- 2. `storage({ action: 'getLocalStorage' })` + `storage({ action: 'getCookies' })`
124
- 3. `inspect({ action: 'evaluateExpression' })`
125
- 4. `dom({ action: 'snapshot' })`
126
-
127
- **UI verification:**
128
- 1. `content({ action: 'verify' })` - Run all default checks
129
- 2. Reports: dead buttons, small touch targets, overflow clipping, dead links, viewport issues
130
- 3. Filter checks: `checks: ['handlers', 'touch']` for specific issues
131
- 4. Available checks: `handlers`, `viewport`, `touch`, `overflow`, `clickability`, `links`, `scroll`
132
-
133
- ## Important Notes
134
-
135
- - **After `launchChrome()`**: You are ALREADY connected. Do NOT call `connectDebugger()`. Use the `reference` parameter when launching, or rename later with `tab({ action: 'rename' })`
136
- - **Interactive elements cache**: Navigation (goto, reload, back, forward) automatically caches all interactive elements. Cache expires after 5 minutes. `findInteractive` shows a summary by default; use `search` or `types` parameters to filter elements
137
- - **Logpoint limits**: Default 20 executions. Use `breakpoint({ action: 'resetCounter' })` or adjust `maxExecutions`
138
- - **Expression failures**: Wrapped in try-catch, shows `[Error: message]`. Search: `console({ action: 'search', pattern: "Logpoint Error" })`
139
- - **CDP line mapping**: May map to nearest valid line. Use `breakpoint({ action: 'validate' })` first
140
- - **Source maps**: Auto-handled. Use `loadSourceMaps` for manual
141
- - **File paths**: Full URLs (`http://localhost:3000/app.js`) or `file://`
142
- - **Network monitoring**: Must enable with `network({ action: 'enable' })`
143
- - **Working an issue**: `comment` on it as you go - once when you start (what you're about to change and why) and once when you finish (what you actually changed, files touched, tests added, and anything that contradicts the issue as written). The issue is the durable record; someone reviewing later reads the timeline, not your diff
144
- - **Closing an issue**: `issues({ action: 'resolve' })` waits on a browser overlay only a human can click - don't call it unattended, use `issues({ action: 'comment' })` to record findings instead
145
-
146
- ## Recovering from a failed tool call
147
-
148
- Two different mechanisms fix two different failure points - don't confuse them.
149
-
150
- **1. Missing/invalid parameters -> `continuationToken` (fix and resubmit, cheaply)**
151
-
152
- If a call fails validation (`code: 'MISSING_PARAMETERS'` or `'INVALID_PARAMS'`), the error includes a `continuationToken` and a `missingParameters` list (name/type/description/enum). Don't resend the whole call - retry with just:
153
37
  ```
154
- { continuationToken: '<token>', <only the missing/bad field(s)> }
38
+ git check-ignore -q .devharness && echo ignored || echo NOT ignored
39
+ git ls-files .devharness # already tracked?
155
40
  ```
156
- The server merges this with what you already sent and re-validates. Repeat (same token) until it succeeds. The token expires after 5 minutes. This only applies to calls that never passed validation in the first place - it has nothing to do with guard blocks below.
157
41
 
158
- **2. A validated call gets blocked by a guard (port failure, dead server, breakpoint pause, etc.) -> `replay`**
42
+ Not ignored → **ask** before adding it to `.gitignore`. Already tracked say so; `git rm -r --cached .devharness` untracks, but anything pushed stays in history.
43
+
44
+ ## Repeat instead of retyping
45
+
46
+ Every response footer carries its own index: `**Repeat:** replay({ action: 'repeat', indices: [58] })`. On every call, not just failures (`replay`'s own calls aren't recorded).
159
47
 
160
- Once a call passes validation, devharness records it (even if a guard then blocks it before the handler runs) and every response footer includes a hint like:
48
+ - `indices` takes a list: `[58, 59, 60, 61]` re-runs four steps in order
49
+ - Use it for anything you already did — relaunch, re-login, refilling a form, getting back to the bug. Retyped arguments drift from what actually ran
50
+ - `replay({ action: 'history' })` when indices scrolled away
51
+ - Each call replays on the connection it was recorded with, so pass `connectionReason` explicitly when driving several browsers — implicit calls have no connection to replay against
52
+ - Worth keeping: `replay({ action: 'create', name, indices })`
53
+
54
+ ## Recovering from a failed call
55
+
56
+ Two mechanisms, two different failure points — don't mix them.
57
+
58
+ **Validation failed** (`MISSING_PARAMETERS`, `INVALID_PARAMS`) → the error carries a `continuationToken` and `missingParameters`. Resend only what was missing:
161
59
  ```
162
- **Repeat:** `replay({ action: 'repeat', indices: [N] })`
60
+ { continuationToken: '<token>', <missing field(s)> }
163
61
  ```
164
- Acknowledge whatever blocked it (e.g. `server({ action: 'acknowledgePort' })`, `server({ action: 'acknowledgeStartup' })`), then use that `replay` hint to resume the exact same call - do not reconstruct the arguments by hand, and do not try to reuse a `continuationToken` for this case (that mechanism is for fixing bad input, not for retrying a call that was already valid).
62
+ Server merges and re-validates. Same token, repeat until it passes. Expires in 5 min.
165
63
 
166
- Note this is only one use of `repeat`. The footer hint is on every response, not just blocked ones, and `indices` takes a list - see "Re-running work you already did" above.
64
+ **Guard blocked a valid call** (dead port, breakpoint pause) it was already recorded. Acknowledge (`server({ action: 'acknowledgePort' })`, `acknowledgeStartup`), then use the footer's `replay` hint. Don't rebuild the arguments; don't use a `continuationToken` here.
167
65
 
168
66
  ## Restarting devharness
169
67
 
170
- If devharness itself seems stuck or broken (not the target app), restart it yourself rather than asking the user to reconnect - don't wait to be told to.
68
+ If devharness itself is stuck (not the target app), restart it don't wait to be asked.
69
+
70
+ - `config({ action: 'restart' })` — SIGUSR2s the supervisor via `.devharness/mcp-supervisor.pid`; it replays the `initialize` handshake so the host never reconnects
71
+ - `CONFIG_RESTART_NOT_SUPERVISED` → `kill -USR2 $(cat .devharness/mcp-supervisor.pid)`
72
+ - `CONFIG_RESTART_STALE_PID` → supervisor died dirty; ask the user to run `/mcp`
73
+
74
+ **The triggering call returns an error — that's normal.** You get `MCP error -32000: MCP server is restarting...` instead of `CONFIG_RESTART_REQUESTED`, because the old process dies before flushing. Retry; the next call hits the new process. Expect a new PID in footers, and acknowledged port failures to reset.
75
+
76
+ `config({ action: 'status' })` reports version, entry file, its timestamp, and pids — check the timestamp before believing a rebuild landed; a build signals the supervisor in its own project's pidfile, not always this session's.
77
+
78
+ Restart kills Chrome instances this session launched (relaunch with `launchChrome`); managed servers survive and reattach. `config({ action: 'reload' })` hot-applies most config edits — restart is only needed for `tools.enabled`/`tools.disabled` or a genuinely stuck process.
79
+
80
+ ## Practices
81
+
82
+ **Breakpoints** — conditional: `condition: "userId === '123'"`. Loops/hot paths: `setLogpoint` (20 executions default; `resetCounter` or `maxExecutions`). Clean up with `remove`, audit with `list`. CDP may snap to the nearest line — `validate` first. Source maps auto-load; `loadSourceMaps` to force. Paths are full URLs (`http://localhost:3000/app.js`) or `file://`.
83
+
84
+ **DOM/Event/XHR breakpoints** (Chrome only) — `setDOMBreakpoint` (`subtree-modified`, `attribute-modified`, `node-removed`), `setEventBreakpoint` (click, submit, input, keydown…), `setXHRBreakpoint` (URL substring). Example: `breakpoint({ action: 'setDOMBreakpoint', selector: '.todo-list', domBreakpointType: 'subtree-modified' })`. nodeIds die on reload.
171
85
 
172
- - **Preferred**: `config({ action: 'restart' })`. Under the hood this reads `.devharness/mcp-supervisor.pid` and sends the running mcp-supervisor process a `SIGUSR2`, the same signal `npm run build`'s postbuild hook sends automatically after a rebuild. The supervisor replays the original MCP `initialize` handshake to the freshly spawned child, so the host session never needs to reconnect.
173
- - If that returns `CONFIG_RESTART_NOT_SUPERVISED` (this server isn't running through the supervisor - e.g. a bare `node build/index.js`), fall back to Bash: `kill -USR2 $(cat .devharness/mcp-supervisor.pid)`.
174
- - If it returns `CONFIG_RESTART_STALE_PID`, the supervisor died without cleaning up its pidfile - ask the user to run `/mcp` to reconnect.
86
+ **Expressions** wrapped in try-catch, surface as `[Error: message]`. Find them: `console({ action: 'search', pattern: "Logpoint Error" })`.
175
87
 
176
- **Expect the triggering call itself to come back as an error - that's normal, not a failure.** In practice `config({ action: 'restart' })` almost never returns its own `CONFIG_RESTART_REQUESTED` success message: the old process gets torn down before it can flush that response, so the supervisor's restart-coordinator answers with a synthesized `MCP error -32000: MCP server is restarting; this request will not receive a response from the previous process. Please retry.` instead. Just retry the next call - it'll hit the freshly restarted (and by then ready) process. Two things to expect on that next call: it runs against a new PID (visible in tool response footers), and any acknowledged monitored-port failures (`server({ action: 'acknowledgePort' })`) reset and may need re-acknowledging, since that state lived in the process that just got replaced.
88
+ **Element cache** populated by goto/reload/back/forward, expires after 5 min.
177
89
 
178
- `config({ action: 'status' })` says which build is answering: the version, the entry file it loaded, that file's timestamp, and the server/supervisor pids. If you have just rebuilt devharness and the behaviour still looks old, check that timestamp before believing the code - a build signals the supervisor named in its own project's pidfile, which is not always the one serving this session.
90
+ **Modals** `handleModals: true` on `input` click/type/hover, `dismissStrategy`: `auto` | `accept` | `reject` | `close` | `remove`. English-only, no Shadow DOM or iframes.
179
91
 
180
- Either way, a restart kills any Chrome instances this session launched (relaunch with `launchChrome`), but managed dev servers (`server` tool) survive and reattach automatically. `config({ action: 'reload' })` is different and lighter-weight - it hot-applies most `config.json` edits without a restart; a restart is only needed for `tools.enabled`/`tools.disabled` changes (the tool list is frozen at server startup) or when the process itself is actually stuck.
92
+ **Code search** `inspect({ action: 'searchCode' | 'searchFunctions' })`, then `getSourceCode`.
181
93
 
182
- ## Tool Categories
94
+ **Connections** `listConnections` → `switchConnection`. One connection per tab/process.
183
95
 
184
- The full list of tools grouped by category (connection, tab, breakpoint, execution, inspection, source, console, network, page, DOM, content, screenshot, input, modal, storage, HTTP/assertions, issues, server, replay, dashboard, config) is not needed for most tasks. Load it only when you need to look up a specific tool name or action:
96
+ **Issues** `comment` when you start (what you're changing, why) and when you finish (what changed, files, tests, anything contradicting the issue). The timeline is the durable record, not your diff. `resolve` waits on a browser overlay only a human can click never call it unattended; `comment` instead.
185
97
 
186
- [references/tool-categories.md](references/tool-categories.md)
98
+ ## Patterns
187
99
 
188
- ## Replay Sequences
100
+ | Task | Sequence |
101
+ |---|---|
102
+ | Bug | `launchChrome` → `goto` → `searchCode`/`searchFunctions` → `set`/`setLogpoint` → trigger → `getCallStack` + `getVariables` → `evaluateExpression` |
103
+ | Performance | `network enable` → `goto` → `network search` → `network get` (timing) → `setLogpoint` in slow paths |
104
+ | Frontend state | `dom querySelector` + `getProperties` → `storage getLocalStorage`/`getCookies` → `evaluateExpression` → `dom snapshot` |
105
+ | UI audit | `content({ action: 'verify' })` — dead buttons, touch targets, overflow, dead links, viewport. Filter: `checks: ['handlers','touch']` from `handlers`, `viewport`, `touch`, `overflow`, `clickability`, `links`, `scroll` |
189
106
 
190
- Recording or replaying a sequence - to build a repro, a regression test, or a
191
- multi-device flow - has its own workflow: capturing values mid-run with
192
- `saveAs`, per-step `connectionReason`, conditionals, and verifying an issue's
193
- fix. Load it when the task involves sequences:
107
+ ## Load on demand
194
108
 
195
- [references/sequences.md](references/sequences.md)
109
+ - Full tool/action catalogue: [references/tool-categories.md](references/tool-categories.md)
110
+ - Recording/replaying sequences — `saveAs`, per-step `connectionReason`, conditionals, verifying an issue fix: [references/sequences.md](references/sequences.md)