devharness 0.9.7 → 0.9.9
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/README.md +15 -0
- package/build/block-events.d.ts +3 -5
- package/build/block-events.d.ts.map +1 -1
- package/build/block-events.js +6 -24
- package/build/block-events.js.map +1 -1
- package/build/block-events.test.js +10 -7
- package/build/block-events.test.js.map +1 -1
- package/build/cdp-manager.d.ts +7 -0
- package/build/cdp-manager.d.ts.map +1 -1
- package/build/cdp-manager.js +9 -0
- package/build/cdp-manager.js.map +1 -1
- package/build/cli/index.d.ts +26 -0
- package/build/cli/index.d.ts.map +1 -0
- package/build/cli/index.js +290 -0
- package/build/cli/index.js.map +1 -0
- package/build/cli/process-tree.d.ts +21 -0
- package/build/cli/process-tree.d.ts.map +1 -0
- package/build/cli/process-tree.js +51 -0
- package/build/cli/process-tree.js.map +1 -0
- package/build/cli/session-match.d.ts +37 -0
- package/build/cli/session-match.d.ts.map +1 -0
- package/build/cli/session-match.js +55 -0
- package/build/cli/session-match.js.map +1 -0
- package/build/cli/session-match.test.d.ts +17 -0
- package/build/cli/session-match.test.d.ts.map +1 -0
- package/build/cli/session-match.test.js +125 -0
- package/build/cli/session-match.test.js.map +1 -0
- package/build/config.d.ts +1 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +1 -0
- package/build/config.js.map +1 -1
- package/build/connection-manager.d.ts.map +1 -1
- package/build/connection-manager.js +6 -1
- package/build/connection-manager.js.map +1 -1
- package/build/index.js +57 -4
- package/build/index.js.map +1 -1
- package/build/mcp-supervisor.js +43 -0
- package/build/mcp-supervisor.js.map +1 -1
- package/build/network-monitor.d.ts +13 -2
- package/build/network-monitor.d.ts.map +1 -1
- package/build/network-monitor.js +54 -23
- package/build/network-monitor.js.map +1 -1
- package/build/network-monitor.test.d.ts +11 -0
- package/build/network-monitor.test.d.ts.map +1 -0
- package/build/network-monitor.test.js +151 -0
- package/build/network-monitor.test.js.map +1 -0
- package/build/plugin-session-start-hook.test.d.ts +10 -0
- package/build/plugin-session-start-hook.test.d.ts.map +1 -0
- package/build/plugin-session-start-hook.test.js +187 -0
- package/build/plugin-session-start-hook.test.js.map +1 -0
- package/build/session-endpoint.d.ts +72 -0
- package/build/session-endpoint.d.ts.map +1 -0
- package/build/session-endpoint.js +220 -0
- package/build/session-endpoint.js.map +1 -0
- package/build/session-endpoint.test.d.ts +10 -0
- package/build/session-endpoint.test.d.ts.map +1 -0
- package/build/session-endpoint.test.js +202 -0
- package/build/session-endpoint.test.js.map +1 -0
- package/build/session-events.d.ts +28 -0
- package/build/session-events.d.ts.map +1 -0
- package/build/session-events.js +38 -0
- package/build/session-events.js.map +1 -0
- package/build/session-events.test.d.ts +8 -0
- package/build/session-events.test.d.ts.map +1 -0
- package/build/session-events.test.js +56 -0
- package/build/session-events.test.js.map +1 -0
- package/build/session-identity.d.ts +56 -0
- package/build/session-identity.d.ts.map +1 -0
- package/build/session-identity.js +96 -0
- package/build/session-identity.js.map +1 -0
- package/build/session-identity.test.d.ts +9 -0
- package/build/session-identity.test.d.ts.map +1 -0
- package/build/session-identity.test.js +126 -0
- package/build/session-identity.test.js.map +1 -0
- package/build/session-messages.d.ts +70 -0
- package/build/session-messages.d.ts.map +1 -0
- package/build/session-messages.js +139 -0
- package/build/session-messages.js.map +1 -0
- package/build/tool-response.d.ts +40 -1
- package/build/tool-response.d.ts.map +1 -1
- package/build/tool-response.js.map +1 -1
- package/build/tools/console-tools.d.ts +4 -0
- package/build/tools/console-tools.d.ts.map +1 -1
- package/build/tools/console-tools.js +55 -1
- package/build/tools/console-tools.js.map +1 -1
- package/build/tools/inspect-worker-meta.test.d.ts +9 -0
- package/build/tools/inspect-worker-meta.test.d.ts.map +1 -0
- package/build/tools/inspect-worker-meta.test.js +122 -0
- package/build/tools/inspect-worker-meta.test.js.map +1 -0
- package/build/tools/inspection-tools.d.ts +12 -5
- package/build/tools/inspection-tools.d.ts.map +1 -1
- package/build/tools/inspection-tools.js +86 -5
- package/build/tools/inspection-tools.js.map +1 -1
- package/build/tools/issues-tools.d.ts +3 -3
- package/build/tools/message-tools.d.ts +51 -0
- package/build/tools/message-tools.d.ts.map +1 -0
- package/build/tools/message-tools.js +250 -0
- package/build/tools/message-tools.js.map +1 -0
- package/build/tools/message-tools.test.d.ts +9 -0
- package/build/tools/message-tools.test.d.ts.map +1 -0
- package/build/tools/message-tools.test.js +332 -0
- package/build/tools/message-tools.test.js.map +1 -0
- package/build/tools/network-tools.js +1 -1
- package/build/tools/network-tools.js.map +1 -1
- package/build/tools/replay-tools.d.ts +3 -3
- package/build/tools/server-tools.d.ts +3 -3
- package/build/worker-targets.d.ts +75 -0
- package/build/worker-targets.d.ts.map +1 -0
- package/build/worker-targets.js +171 -0
- package/build/worker-targets.js.map +1 -0
- package/build/worker-targets.test.d.ts +8 -0
- package/build/worker-targets.test.d.ts.map +1 -0
- package/build/worker-targets.test.js +118 -0
- package/build/worker-targets.test.js.map +1 -0
- package/docs/instructions.md +23 -12
- package/docs/messages.md +150 -0
- package/package.json +3 -1
- package/plugin/skills/devharness/SKILL.md +36 -8
- package/plugin/skills/devharness/references/tool-categories.md +8 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker targets - service workers, dedicated workers, shared workers.
|
|
3
|
+
*
|
|
4
|
+
* Every debugging tool runs against the page target, so code inside a worker
|
|
5
|
+
* has been unreachable: no evaluation, no console. A worker is its own CDP
|
|
6
|
+
* target with its own websocket, listed by the browser alongside the page, so
|
|
7
|
+
* a client per worker reaches it without touching the page's debugger state.
|
|
8
|
+
*/
|
|
9
|
+
import CDP from 'chrome-remote-interface';
|
|
10
|
+
/** The three target types that run script outside the page. */
|
|
11
|
+
const WORKER_TYPES = ['service_worker', 'worker', 'shared_worker'];
|
|
12
|
+
export class WorkerTargetNotFoundError extends Error {
|
|
13
|
+
ref;
|
|
14
|
+
available;
|
|
15
|
+
constructor(ref, available) {
|
|
16
|
+
super(`No worker target matches "${ref}"`);
|
|
17
|
+
this.ref = ref;
|
|
18
|
+
this.available = available;
|
|
19
|
+
this.name = 'WorkerTargetNotFoundError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class WorkerTargetAmbiguousError extends Error {
|
|
23
|
+
ref;
|
|
24
|
+
matches;
|
|
25
|
+
constructor(ref, matches) {
|
|
26
|
+
super(`"${ref}" matches ${matches.length} worker targets`);
|
|
27
|
+
this.ref = ref;
|
|
28
|
+
this.matches = matches;
|
|
29
|
+
this.name = 'WorkerTargetAmbiguousError';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class WorkerEvaluateError extends Error {
|
|
33
|
+
details;
|
|
34
|
+
constructor(message, details) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.details = details;
|
|
37
|
+
this.name = 'WorkerEvaluateError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Console output is capped per target; a chatty worker cannot grow unbounded. */
|
|
41
|
+
const MAX_MESSAGES = 200;
|
|
42
|
+
/**
|
|
43
|
+
* One browser's worker targets. Keyed by host and port, since that pair is
|
|
44
|
+
* what locates a target's websocket.
|
|
45
|
+
*/
|
|
46
|
+
export class WorkerTargetRegistry {
|
|
47
|
+
host;
|
|
48
|
+
port;
|
|
49
|
+
listTargets;
|
|
50
|
+
connect;
|
|
51
|
+
attached = new Map();
|
|
52
|
+
constructor(host, port, listTargets = CDP.List, connect = CDP) {
|
|
53
|
+
this.host = host;
|
|
54
|
+
this.port = port;
|
|
55
|
+
this.listTargets = listTargets;
|
|
56
|
+
this.connect = connect;
|
|
57
|
+
}
|
|
58
|
+
async list() {
|
|
59
|
+
const targets = await this.listTargets({ host: this.host, port: this.port });
|
|
60
|
+
return targets
|
|
61
|
+
.filter((t) => WORKER_TYPES.includes(t.type))
|
|
62
|
+
.map((t) => ({
|
|
63
|
+
targetId: t.id,
|
|
64
|
+
type: t.type,
|
|
65
|
+
url: t.url,
|
|
66
|
+
attached: this.attached.has(t.id),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* A target id, or a substring of a target's URL. A substring matching more
|
|
71
|
+
* than one target is refused with both named - picking the first would
|
|
72
|
+
* evaluate in a worker the caller did not choose.
|
|
73
|
+
*/
|
|
74
|
+
async resolve(ref) {
|
|
75
|
+
const targets = await this.list();
|
|
76
|
+
const byId = targets.find((t) => t.targetId === ref);
|
|
77
|
+
if (byId)
|
|
78
|
+
return byId;
|
|
79
|
+
const matches = targets.filter((t) => t.url.includes(ref));
|
|
80
|
+
if (matches.length === 1)
|
|
81
|
+
return matches[0];
|
|
82
|
+
if (matches.length === 0)
|
|
83
|
+
throw new WorkerTargetNotFoundError(ref, targets);
|
|
84
|
+
throw new WorkerTargetAmbiguousError(ref, matches);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Evaluate inside the worker. Console output from the target is recorded
|
|
88
|
+
* from the moment of first attach, so a log emitted before that is not held.
|
|
89
|
+
*/
|
|
90
|
+
async evaluate(ref, expression, awaitPromise = true) {
|
|
91
|
+
const target = await this.resolve(ref);
|
|
92
|
+
const worker = await this.attach(target);
|
|
93
|
+
const result = await worker.client.Runtime.evaluate({
|
|
94
|
+
expression,
|
|
95
|
+
returnByValue: true,
|
|
96
|
+
awaitPromise,
|
|
97
|
+
});
|
|
98
|
+
if (result.exceptionDetails) {
|
|
99
|
+
const thrown = result.exceptionDetails.exception;
|
|
100
|
+
throw new WorkerEvaluateError(thrown?.description || thrown?.value || result.exceptionDetails.text, result.exceptionDetails.text);
|
|
101
|
+
}
|
|
102
|
+
return result.result?.value;
|
|
103
|
+
}
|
|
104
|
+
/** Console output recorded since this target was first attached. */
|
|
105
|
+
async messages(ref) {
|
|
106
|
+
const target = await this.resolve(ref);
|
|
107
|
+
const worker = await this.attach(target);
|
|
108
|
+
return [...worker.messages];
|
|
109
|
+
}
|
|
110
|
+
async attach(target) {
|
|
111
|
+
const existing = this.attached.get(target.targetId);
|
|
112
|
+
if (existing)
|
|
113
|
+
return existing;
|
|
114
|
+
const client = await this.connect({ host: this.host, port: this.port, target: target.targetId });
|
|
115
|
+
const worker = { client, messages: [] };
|
|
116
|
+
const record = (type, text) => {
|
|
117
|
+
worker.messages.push({ type, text, timestamp: Date.now() });
|
|
118
|
+
if (worker.messages.length > MAX_MESSAGES)
|
|
119
|
+
worker.messages.shift();
|
|
120
|
+
};
|
|
121
|
+
client.Runtime.consoleAPICalled((e) => {
|
|
122
|
+
record(e.type, (e.args || []).map(describeArg).join(' '));
|
|
123
|
+
});
|
|
124
|
+
client.Runtime.exceptionThrown((e) => {
|
|
125
|
+
const thrown = e.exceptionDetails?.exception;
|
|
126
|
+
record('error', thrown?.description || e.exceptionDetails?.text || 'exception');
|
|
127
|
+
});
|
|
128
|
+
await client.Runtime.enable();
|
|
129
|
+
this.attached.set(target.targetId, worker);
|
|
130
|
+
return worker;
|
|
131
|
+
}
|
|
132
|
+
/** Closes every client. A client left open holds its worker alive. */
|
|
133
|
+
async dispose() {
|
|
134
|
+
const workers = [...this.attached.values()];
|
|
135
|
+
this.attached.clear();
|
|
136
|
+
for (const worker of workers) {
|
|
137
|
+
try {
|
|
138
|
+
await worker.client.close();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// Its target is already gone; the socket went with it.
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function describeArg(arg) {
|
|
147
|
+
if (arg.value !== undefined)
|
|
148
|
+
return typeof arg.value === 'string' ? arg.value : JSON.stringify(arg.value);
|
|
149
|
+
return arg.description || arg.className || arg.type || '';
|
|
150
|
+
}
|
|
151
|
+
const registries = new Map();
|
|
152
|
+
/** One registry per browser, so a second call reuses the open clients. */
|
|
153
|
+
export function getWorkerTargetRegistry(host, port) {
|
|
154
|
+
const key = `${host}:${port}`;
|
|
155
|
+
const existing = registries.get(key);
|
|
156
|
+
if (existing)
|
|
157
|
+
return existing;
|
|
158
|
+
const registry = new WorkerTargetRegistry(host, port);
|
|
159
|
+
registries.set(key, registry);
|
|
160
|
+
return registry;
|
|
161
|
+
}
|
|
162
|
+
/** Called when a connection closes: its browser's clients go with it. */
|
|
163
|
+
export async function disposeWorkerTargetRegistry(host, port) {
|
|
164
|
+
const key = `${host}:${port}`;
|
|
165
|
+
const registry = registries.get(key);
|
|
166
|
+
if (!registry)
|
|
167
|
+
return;
|
|
168
|
+
registries.delete(key);
|
|
169
|
+
await registry.dispose();
|
|
170
|
+
}
|
|
171
|
+
//# sourceMappingURL=worker-targets.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-targets.js","sourceRoot":"","sources":["../src/worker-targets.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A worker's script runs in its own target, so a reference has to name exactly
|
|
3
|
+
* one. A substring matching two targets resolved to the first would evaluate
|
|
4
|
+
* inside a worker the caller did not choose, and a client left open holds its
|
|
5
|
+
* worker alive past the browser that owned it - these pin both.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=worker-targets.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-targets.test.d.ts","sourceRoot":"","sources":["../src/worker-targets.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A worker's script runs in its own target, so a reference has to name exactly
|
|
3
|
+
* one. A substring matching two targets resolved to the first would evaluate
|
|
4
|
+
* inside a worker the caller did not choose, and a client left open holds its
|
|
5
|
+
* worker alive past the browser that owned it - these pin both.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { WorkerTargetRegistry, WorkerTargetNotFoundError, WorkerTargetAmbiguousError, WorkerEvaluateError, } from './worker-targets.js';
|
|
9
|
+
const TARGETS = [
|
|
10
|
+
{ id: 'page-1', type: 'page', url: 'http://localhost:5173/' },
|
|
11
|
+
{ id: 'sw-1', type: 'service_worker', url: 'http://localhost:5173/sw.js' },
|
|
12
|
+
{ id: 'w-1', type: 'worker', url: 'http://localhost:5173/worker.js' },
|
|
13
|
+
{ id: 'w-2', type: 'worker', url: 'http://localhost:5173/second-worker.js' },
|
|
14
|
+
];
|
|
15
|
+
function fakeClient(evaluate) {
|
|
16
|
+
const client = {
|
|
17
|
+
closed: false,
|
|
18
|
+
handlers: {},
|
|
19
|
+
Runtime: {
|
|
20
|
+
enable: async () => { },
|
|
21
|
+
evaluate: async (params) => (evaluate ? evaluate(params) : { result: { value: 'ok' } }),
|
|
22
|
+
consoleAPICalled: (h) => { client.handlers.console = h; },
|
|
23
|
+
exceptionThrown: (h) => { client.handlers.exception = h; },
|
|
24
|
+
},
|
|
25
|
+
close: async () => { client.closed = true; },
|
|
26
|
+
};
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
function registry(options = {}) {
|
|
30
|
+
const clients = [];
|
|
31
|
+
const connect = async () => {
|
|
32
|
+
const client = fakeClient(options.evaluate);
|
|
33
|
+
clients.push(client);
|
|
34
|
+
return client;
|
|
35
|
+
};
|
|
36
|
+
const instance = new WorkerTargetRegistry('localhost', 9222, async () => options.targets ?? TARGETS, connect);
|
|
37
|
+
return { instance, clients };
|
|
38
|
+
}
|
|
39
|
+
describe('WorkerTargetRegistry.list', () => {
|
|
40
|
+
it('lists only targets that run script outside the page', async () => {
|
|
41
|
+
const { instance } = registry();
|
|
42
|
+
const types = (await instance.list()).map((t) => t.type);
|
|
43
|
+
expect(types).toEqual(['service_worker', 'worker', 'worker']);
|
|
44
|
+
});
|
|
45
|
+
it('marks a target attached once a client is open on it', async () => {
|
|
46
|
+
const { instance } = registry();
|
|
47
|
+
await instance.evaluate('sw-1', '1');
|
|
48
|
+
const sw = (await instance.list()).find((t) => t.targetId === 'sw-1');
|
|
49
|
+
expect(sw?.attached).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
describe('WorkerTargetRegistry.resolve', () => {
|
|
53
|
+
it('takes a target id', async () => {
|
|
54
|
+
const { instance } = registry();
|
|
55
|
+
expect((await instance.resolve('sw-1')).url).toBe('http://localhost:5173/sw.js');
|
|
56
|
+
});
|
|
57
|
+
it('takes a URL substring that matches one target', async () => {
|
|
58
|
+
const { instance } = registry();
|
|
59
|
+
expect((await instance.resolve('sw.js')).targetId).toBe('sw-1');
|
|
60
|
+
});
|
|
61
|
+
it('refuses a substring matching more than one, naming both', async () => {
|
|
62
|
+
const { instance } = registry();
|
|
63
|
+
await expect(instance.resolve('worker.js')).rejects.toBeInstanceOf(WorkerTargetAmbiguousError);
|
|
64
|
+
const error = await instance.resolve('worker.js').catch((e) => e);
|
|
65
|
+
expect(error.matches.map((t) => t.targetId)).toEqual(['w-1', 'w-2']);
|
|
66
|
+
});
|
|
67
|
+
it('refuses a reference matching nothing, listing what there is', async () => {
|
|
68
|
+
const { instance } = registry();
|
|
69
|
+
const error = await instance.resolve('absent.js').catch((e) => e);
|
|
70
|
+
expect(error).toBeInstanceOf(WorkerTargetNotFoundError);
|
|
71
|
+
expect(error.available.map((t) => t.targetId)).toEqual(['sw-1', 'w-1', 'w-2']);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('WorkerTargetRegistry.evaluate', () => {
|
|
75
|
+
it('returns the by-value result', async () => {
|
|
76
|
+
const { instance } = registry({ evaluate: () => ({ result: { value: 'ServiceWorkerGlobalScope' } }) });
|
|
77
|
+
expect(await instance.evaluate('sw-1', 'self.constructor.name')).toBe('ServiceWorkerGlobalScope');
|
|
78
|
+
});
|
|
79
|
+
it('raises what the worker threw', async () => {
|
|
80
|
+
const { instance } = registry({
|
|
81
|
+
evaluate: () => ({ exceptionDetails: { text: 'Uncaught', exception: { description: 'TypeError: nope' } } }),
|
|
82
|
+
});
|
|
83
|
+
await expect(instance.evaluate('sw-1', 'nope()')).rejects.toBeInstanceOf(WorkerEvaluateError);
|
|
84
|
+
});
|
|
85
|
+
it('opens one client per target, however many evaluations', async () => {
|
|
86
|
+
const { instance, clients } = registry();
|
|
87
|
+
await instance.evaluate('sw-1', '1');
|
|
88
|
+
await instance.evaluate('sw-1', '2');
|
|
89
|
+
expect(clients).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('WorkerTargetRegistry console', () => {
|
|
93
|
+
it('records console output from the target', async () => {
|
|
94
|
+
const { instance, clients } = registry();
|
|
95
|
+
await instance.evaluate('sw-1', '1');
|
|
96
|
+
clients[0].handlers.console({ type: 'log', args: [{ value: 'inside' }, { value: 42 }] });
|
|
97
|
+
const messages = await instance.messages('sw-1');
|
|
98
|
+
expect(messages.map((m) => m.text)).toEqual(['inside 42']);
|
|
99
|
+
});
|
|
100
|
+
it('records an exception thrown in the target as an error', async () => {
|
|
101
|
+
const { instance, clients } = registry();
|
|
102
|
+
await instance.evaluate('sw-1', '1');
|
|
103
|
+
clients[0].handlers.exception({ exceptionDetails: { exception: { description: 'TypeError: nope' } } });
|
|
104
|
+
const messages = await instance.messages('sw-1');
|
|
105
|
+
expect(messages[0]).toMatchObject({ type: 'error', text: 'TypeError: nope' });
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('WorkerTargetRegistry.dispose', () => {
|
|
109
|
+
it('closes every client it opened', async () => {
|
|
110
|
+
const { instance, clients } = registry();
|
|
111
|
+
await instance.evaluate('sw-1', '1');
|
|
112
|
+
await instance.evaluate('w-1', '1');
|
|
113
|
+
await instance.dispose();
|
|
114
|
+
expect(clients.map((c) => c.closed)).toEqual([true, true]);
|
|
115
|
+
expect((await instance.list()).every((t) => !t.attached)).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
//# sourceMappingURL=worker-targets.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker-targets.test.js","sourceRoot":"","sources":["../src/worker-targets.test.ts"],"names":[],"mappings":"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"}
|
package/docs/instructions.md
CHANGED
|
@@ -59,6 +59,9 @@ Chrome DevTools Protocol debugging for JavaScript/TypeScript in Chrome, Node.js,
|
|
|
59
59
|
- Note: DOM breakpoints use nodeIds which are invalidated on page reload
|
|
60
60
|
|
|
61
61
|
**Code search:**
|
|
62
|
+
- `inspect({ action: 'listTargets' })`: List worker targets (service, dedicated, shared)
|
|
63
|
+
- `inspect({ action: 'evaluateExpression', target })`: Evaluate inside a worker, addressed by target id or a URL substring
|
|
64
|
+
- `console({ action: 'list', target })`: Read a worker's console, which reaches no page listener
|
|
62
65
|
- `inspect({ action: 'searchCode' })`: Find patterns
|
|
63
66
|
- `inspect({ action: 'searchFunctions' })`: Locate definitions
|
|
64
67
|
- `getSourceCode`: View context
|
|
@@ -120,28 +123,31 @@ Chrome DevTools Protocol debugging for JavaScript/TypeScript in Chrome, Node.js,
|
|
|
120
123
|
- **Missing/invalid parameters**: the error includes a `continuationToken` and `missingParameters` (name/type/description/enum). Retry with just `{ continuationToken, <missing/bad field(s)> }` - don't resend everything. Expires after 5 min.
|
|
121
124
|
- **A validated call gets blocked by a guard** (port failure, dead server, breakpoint pause): the response footer shows `**Repeat:** replay({ action: 'repeat', indices: [N] })`. Acknowledge the guard (e.g. `server({ action: 'acknowledgePort' })`), then use that hint to resume the exact call. Don't reuse a `continuationToken` here - that's for fixing bad input, not for retrying an already-valid call.
|
|
122
125
|
|
|
123
|
-
##
|
|
126
|
+
## The event stream
|
|
124
127
|
|
|
125
|
-
|
|
128
|
+
Everything devharness pushes - a guard block, a message from another session - appends one JSON line to `~/.devharness/events/<sessionId>.jsonl`. One file per session, so one watch covers every kind, including kinds added later.
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
{"ts":"2026-08-08T03:21:45.588Z","guard":"pendingStartup","tool":"navigate","detail":"died before port detected: \"web\"","resolve":"server({ action: 'acknowledgeStartup', serverId: 'web' })"}
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
`guard` is one of `port`, `breakpoint`, `pendingStartup`, `bug`, `duplicateSession`. Lines are deduplicated: one per *new* block, not one per blocked call. The same block re-firing on later calls stays quiet until a call clears every guard and the block recurs.
|
|
132
|
-
|
|
133
|
-
In Claude Code, tail it with a persistent Monitor so blocks arrive as notifications while you work on something else:
|
|
130
|
+
Installed as a plugin, a `SessionStart` hook (`plugin/hooks/session-start.mjs`) creates that file and prints the `Monitor` call as session context before the first turn. In Claude Code, arm it:
|
|
134
131
|
|
|
135
132
|
```
|
|
136
133
|
Monitor({
|
|
137
|
-
command: "mkdir -p
|
|
138
|
-
description: "devharness
|
|
134
|
+
command: "mkdir -p ~/.devharness/events && touch <streamPath> && tail -f -n0 <streamPath>",
|
|
135
|
+
description: "devharness events",
|
|
139
136
|
persistent: true,
|
|
140
137
|
timeout_ms: 3600000
|
|
141
138
|
})
|
|
142
139
|
```
|
|
143
140
|
|
|
144
|
-
|
|
141
|
+
Nothing is lost without a watch - blocks and messages still surface on the next tool call. The watch is what makes them arrive while the session is doing something else.
|
|
142
|
+
|
|
143
|
+
Line kinds:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{"ts":"...","kind":"block","guard":"pendingStartup","tool":"navigate","detail":"died before port detected: \"web\"","resolve":"server({ action: 'acknowledgeStartup', serverId: 'web' })"}
|
|
147
|
+
{"ts":"...","kind":"message","from":"66ba2d65","id":"d94924a8-...","detail":"Message from 66ba2d65: ...","resolve":"message({ action: 'read' })"}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`guard` is one of `port`, `breakpoint`, `pendingStartup`, `bug`, `duplicateSession`. Blocks are deduplicated: one line per *new* block, not one per blocked call. Any client can tail the file.
|
|
145
151
|
|
|
146
152
|
## Restarting devharness
|
|
147
153
|
|
|
@@ -218,6 +224,11 @@ runs against (see Quick Start).
|
|
|
218
224
|
- **GitHub** (via the `gh` CLI; only `publish` and `sync` use the network): `publish` shows a draft and posts nothing until `confirm: true`; `sync` reconciles both ways and reports a conflict rather than overwriting when both sides changed; `import` materialises a GitHub-only issue locally; `link` stamps an existing number with no network call; `pullSequence` writes a sequence out of an issue body to disk (one authored by another GitHub account needs a person to read it and pass `confirm: true`)
|
|
219
225
|
- A sequence pulled from an issue is **never run automatically**, and one using `execution`, `saveToDisk`, `server`, `request` or `download` is refused unless you pass `allowPrivilegedSteps: true`. Read it first
|
|
220
226
|
|
|
227
|
+
**Messages**: `message` (actions: sessions, send, read, reply)
|
|
228
|
+
- Text between two devharness sessions on this machine - a session hitting a devharness bug talking to the session working on devharness itself. `sessions` lists reachable mailboxes and this session's own mailbox path
|
|
229
|
+
- `send({ to, text, waitForReplyMs })` holds the call open until something lands in this session's mailbox (max 300000ms) and returns it; without `waitForReplyMs` it returns as soon as the line is written. `reply({ replyTo, text })` routes back to the sender of that message. The wait returns on ANY arrival, not only a tagged reply, so two sessions blocking at the same moment both release
|
|
230
|
+
- Transport is one append-only JSONL file per session under `~/.devharness/messages/`, which is what makes it work across project roots. A busy session needs a Monitor armed on its mailbox file to be interrupted; otherwise messages surface on its next `message({ action: 'read' })`
|
|
231
|
+
|
|
221
232
|
**Server**: `server` (actions: start, stop, restart, list, logs, stopAll, setAutoRun, clearLogs, remove, monitorPort, unmonitorPort, listMonitored, acknowledgePort, acknowledgeStartup, extendStartup, cancelPendingRestart)
|
|
222
233
|
- Use `global: true` to access servers started from a different working directory
|
|
223
234
|
- `start({ watch: true, watchPaths?: [...] })`: cdp-tools watches the given paths (default: cwd) and auto-restarts the server on file changes, instead of relying on `--watch`/nodemon. Pause-aware: if a breakpoint debugger is paused on that server's inspector port, the restart queues instead of firing immediately - `cancelPendingRestart` discards a queued restart to keep debugging without it firing on resume
|
package/docs/messages.md
CHANGED
|
@@ -3881,6 +3881,97 @@ Cannot wait for {{condition}}: the debugger on "{{connectionReason}}" is paused
|
|
|
3881
3881
|
|
|
3882
3882
|
---
|
|
3883
3883
|
|
|
3884
|
+
## MESSAGE_SESSIONS
|
|
3885
|
+
|
|
3886
|
+
**Type:** success
|
|
3887
|
+
**Summary:** {{count}} session(s) reachable
|
|
3888
|
+
|
|
3889
|
+
This session is `{{self}}`. Its mailbox: `{{mailboxPath}}`
|
|
3890
|
+
|
|
3891
|
+
{{sessionList}}
|
|
3892
|
+
|
|
3893
|
+
**Note:** To receive a message while doing other work, arm a Monitor on this session's mailbox file - it appends one JSON line per message, so a `tail -f` style watch fires the moment one lands. Without a Monitor, messages surface only when `message({ action: 'read' })` runs.
|
|
3894
|
+
|
|
3895
|
+
---
|
|
3896
|
+
|
|
3897
|
+
## MESSAGE_SENT
|
|
3898
|
+
|
|
3899
|
+
**Type:** success
|
|
3900
|
+
**Summary:** Sent to {{to}}
|
|
3901
|
+
|
|
3902
|
+
Message `{{shortId}}` appended to `{{mailboxPath}}`. The recipient sees it on `message({ action: 'read' })`, or immediately if it has a Monitor armed on its event stream.{{deliveryNote}}
|
|
3903
|
+
|
|
3904
|
+
**Suggestions:**
|
|
3905
|
+
- To hold this call open until the other session answers: `message({ action: 'send', to: '{{to}}', text: '...', waitForReplyMs: 120000 })`
|
|
3906
|
+
|
|
3907
|
+
---
|
|
3908
|
+
|
|
3909
|
+
## MESSAGE_REPLY_RECEIVED
|
|
3910
|
+
|
|
3911
|
+
**Type:** success
|
|
3912
|
+
**Summary:** {{count}} message(s) after {{elapsedMs}}ms
|
|
3913
|
+
|
|
3914
|
+
{{messageList}}{{repeatNote}}
|
|
3915
|
+
|
|
3916
|
+
**Suggestions:**
|
|
3917
|
+
- Answer with `message({ action: 'reply', replyTo: '<id>', text: '...' })`
|
|
3918
|
+
|
|
3919
|
+
---
|
|
3920
|
+
|
|
3921
|
+
## MESSAGE_INBOX
|
|
3922
|
+
|
|
3923
|
+
**Type:** success
|
|
3924
|
+
**Summary:** {{count}} new of {{total}} in {{self}}
|
|
3925
|
+
|
|
3926
|
+
{{messageList}}
|
|
3927
|
+
|
|
3928
|
+
**Note:** Read advances a cursor, so the same message is returned once. The full history stays in `{{mailboxPath}}`.
|
|
3929
|
+
|
|
3930
|
+
---
|
|
3931
|
+
|
|
3932
|
+
## MESSAGE_REPLY_TIMEOUT
|
|
3933
|
+
|
|
3934
|
+
**Type:** error
|
|
3935
|
+
**Code:** MESSAGE_REPLY_TIMEOUT
|
|
3936
|
+
|
|
3937
|
+
Message `{{id}}` reached {{to}}, and nothing arrived in `{{self}}`'s mailbox within {{timeoutMs}}ms.
|
|
3938
|
+
|
|
3939
|
+
**Suggestions:**
|
|
3940
|
+
- The other session is alive but was not calling devharness - it sees the message on its next `message({ action: 'read' })`
|
|
3941
|
+
- Check it is running: `message({ action: 'sessions' })`
|
|
3942
|
+
- Wait again without resending: `message({ action: 'read' })`, or send a follow-up with a longer `waitForReplyMs` (max 300000)
|
|
3943
|
+
- Its mailbox is `{{mailboxPath}}` - a Monitor on that file removes the need to poll
|
|
3944
|
+
|
|
3945
|
+
---
|
|
3946
|
+
|
|
3947
|
+
## MESSAGE_TARGET_UNKNOWN
|
|
3948
|
+
|
|
3949
|
+
**Type:** error
|
|
3950
|
+
**Code:** MESSAGE_TARGET_UNKNOWN
|
|
3951
|
+
|
|
3952
|
+
Cannot address "{{target}}": {{reason}}. This session is `{{self}}`.
|
|
3953
|
+
|
|
3954
|
+
**Suggestions:**
|
|
3955
|
+
- List who is reachable: `message({ action: 'sessions' })`
|
|
3956
|
+
- A reply needs the id of a message in this session's own mailbox: `message({ action: 'read' })`
|
|
3957
|
+
|
|
3958
|
+
---
|
|
3959
|
+
|
|
3960
|
+
## MESSAGE_INVALID_ARGS
|
|
3961
|
+
|
|
3962
|
+
**Type:** error
|
|
3963
|
+
**Code:** MESSAGE_INVALID_ARGS
|
|
3964
|
+
|
|
3965
|
+
{{message}}
|
|
3966
|
+
|
|
3967
|
+
**Suggestions:**
|
|
3968
|
+
- `message({ action: 'sessions' })` - who is reachable, and this session's mailbox path
|
|
3969
|
+
- `message({ action: 'send', to, text })` - write to another session
|
|
3970
|
+
- `message({ action: 'read' })` - take new messages
|
|
3971
|
+
- `message({ action: 'reply', replyTo, text })` - answer a message by id
|
|
3972
|
+
|
|
3973
|
+
---
|
|
3974
|
+
|
|
3884
3975
|
## CONFIG_USE_LOCAL_FAILED
|
|
3885
3976
|
|
|
3886
3977
|
**Type:** error
|
|
@@ -3940,3 +4031,62 @@ Section "{{section}}" was not found in the page content.
|
|
|
3940
4031
|
|
|
3941
4032
|
---
|
|
3942
4033
|
|
|
4034
|
+
## WORKER_TARGETS_LISTED
|
|
4035
|
+
|
|
4036
|
+
**Type:** success
|
|
4037
|
+
**Code:** WORKER_TARGETS_LISTED
|
|
4038
|
+
|
|
4039
|
+
{{count}} worker target(s). Pass a target id, or a substring of its URL, as `target`.
|
|
4040
|
+
|
|
4041
|
+
{{targets}}
|
|
4042
|
+
|
|
4043
|
+
---
|
|
4044
|
+
|
|
4045
|
+
## WORKER_EVALUATED
|
|
4046
|
+
|
|
4047
|
+
**Type:** success
|
|
4048
|
+
**Code:** WORKER_EVALUATED
|
|
4049
|
+
|
|
4050
|
+
Evaluated inside worker target "{{target}}".
|
|
4051
|
+
|
|
4052
|
+
{{result}}
|
|
4053
|
+
|
|
4054
|
+
---
|
|
4055
|
+
|
|
4056
|
+
## WORKER_TARGET_NOT_FOUND
|
|
4057
|
+
|
|
4058
|
+
**Type:** error
|
|
4059
|
+
**Code:** WORKER_TARGET_NOT_FOUND
|
|
4060
|
+
|
|
4061
|
+
No worker target matches "{{target}}". Available: {{available}}
|
|
4062
|
+
|
|
4063
|
+
---
|
|
4064
|
+
|
|
4065
|
+
## WORKER_TARGET_AMBIGUOUS
|
|
4066
|
+
|
|
4067
|
+
**Type:** error
|
|
4068
|
+
**Code:** WORKER_TARGET_AMBIGUOUS
|
|
4069
|
+
|
|
4070
|
+
"{{target}}" matches more than one worker target: {{matches}}. Pass a target id instead.
|
|
4071
|
+
|
|
4072
|
+
---
|
|
4073
|
+
|
|
4074
|
+
## WORKER_EVALUATE_FAILED
|
|
4075
|
+
|
|
4076
|
+
**Type:** error
|
|
4077
|
+
**Code:** WORKER_EVALUATE_FAILED
|
|
4078
|
+
|
|
4079
|
+
Evaluating inside worker target "{{target}}" failed: {{error}}
|
|
4080
|
+
|
|
4081
|
+
---
|
|
4082
|
+
|
|
4083
|
+
## WORKER_CONSOLE_LISTED
|
|
4084
|
+
|
|
4085
|
+
**Type:** success
|
|
4086
|
+
**Code:** WORKER_CONSOLE_LISTED
|
|
4087
|
+
|
|
4088
|
+
{{count}} console message(s) from worker target "{{target}}".
|
|
4089
|
+
|
|
4090
|
+
{{messages}}
|
|
4091
|
+
|
|
4092
|
+
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devharness",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9",
|
|
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",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"test:run": "vitest run",
|
|
22
22
|
"test:coverage": "vitest run --coverage",
|
|
23
23
|
"stress:suspend": "node scripts/stress-suspend.mjs",
|
|
24
|
+
"check:targets": "node scripts/check-target-resume.mjs",
|
|
25
|
+
"check:workers": "node scripts/check-worker-eval.mjs",
|
|
24
26
|
"prepublishOnly": "npm run test:run && npm run build:verify",
|
|
25
27
|
"version": "node scripts/sync-skill-version.mjs",
|
|
26
28
|
"postversion": "git push && git push --tags"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
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
|
-
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.
|
|
4
|
+
compatibility: Requires the devharness MCP server to be connected (tools such as launchChrome, breakpoint, inspect, replay, server, issues). The shell commands need `devharness` on PATH (`npm i -g devharness`); without it use `npx -y devharness@<version> <command>`. Previously published as cdp-tools-mcp.
|
|
5
|
+
version: 0.9.9
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# devharness
|
|
@@ -27,6 +27,7 @@ Launched without a reference? `tab({ action: 'rename', reference: "unnamed-conne
|
|
|
27
27
|
|
|
28
28
|
Paused: `inspect({ action: 'getCallStack' })` → `getVariables` → `evaluateExpression`.
|
|
29
29
|
Watch: `console({ action: 'list' })`, `network({ action: 'list' })` (needs `network({ action: 'enable' })` first).
|
|
30
|
+
Inside a worker: `inspect({ action: 'listTargets' })` → `evaluateExpression({ target, expression })`, and `console({ action: 'list', target })` — a service worker's console reaches no page listener.
|
|
30
31
|
|
|
31
32
|
## `.devharness/` must be git-ignored
|
|
32
33
|
|
|
@@ -63,22 +64,49 @@ Server merges and re-validates. Same token, repeat until it passes. Expires in 5
|
|
|
63
64
|
|
|
64
65
|
**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.
|
|
65
66
|
|
|
66
|
-
##
|
|
67
|
+
## The event stream
|
|
67
68
|
|
|
68
|
-
|
|
69
|
+
Everything devharness pushes at you - a guard block, a message from another session - appends one JSON line to `~/.devharness/events/<sessionId>.jsonl`. One file, one watch, and any kind added later arrives on the same watch.
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
Installed as a plugin, a `SessionStart` hook prints that path and the `Monitor` call at the top of every session. Arm it when you see it:
|
|
71
72
|
|
|
72
73
|
```
|
|
73
74
|
Monitor({
|
|
74
|
-
command: "mkdir -p
|
|
75
|
-
description: "devharness
|
|
75
|
+
command: "mkdir -p ~/.devharness/events && touch <streamPath> && tail -f -n0 <streamPath>",
|
|
76
|
+
description: "devharness events",
|
|
76
77
|
persistent: true,
|
|
77
78
|
timeout_ms: 3600000
|
|
78
79
|
})
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
Nothing is lost without it: blocks and messages still surface on your next devharness call. The watch is what makes them arrive while you are doing something else, which for a dev server that died an hour ago is the difference that matters.
|
|
83
|
+
|
|
84
|
+
Each line carries `kind` and, where there is one, `resolve` - the call that clears it. `kind: "block"` also carries `guard`, one of `port`, `breakpoint`, `pendingStartup`, `bug`, `duplicateSession`; blocks are deduplicated, one line per *new* block rather than one per blocked call. `kind: "message"` carries `from` and the message id.
|
|
85
|
+
|
|
86
|
+
## Talking to another devharness session
|
|
87
|
+
|
|
88
|
+
Two devharness sessions on this machine reach each other by mailbox id - the session hitting a devharness bug and the session working on devharness itself, for example.
|
|
89
|
+
|
|
90
|
+
`message({ action: 'sessions' })` lists who is reachable and prints this session's own mailbox path.
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
message({ action: 'send', to: 'a1b2c3d4', text: 'Repro: ...', waitForReplyMs: 120000 })
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That holds the call open until something lands in this session's mailbox, then returns it; answer with `message({ action: 'reply', replyTo: '<id>', text: '...' })`. The wait returns on ANY arrival, not only a tagged reply - two sessions blocking at the same moment both release instead of both timing out.
|
|
97
|
+
|
|
98
|
+
An arriving message announces itself on the event stream, so the watch above covers it and there is no second watch to arm. `message({ action: 'read' })` is what takes them, advancing a cursor so each is returned once; the full history stays in the mailbox file.
|
|
99
|
+
|
|
100
|
+
## Running a tool from the shell
|
|
101
|
+
|
|
102
|
+
`devharness <command>` typed in a session's shell runs that tool **inside that session**, against the connections it already holds. The plugin does not put `devharness` on PATH - `npm i -g devharness` does, or run it as `npx -y devharness@<version> <command>`. The SessionStart hook reports which of those applies here. The session is found by walking up the process tree, so `! devharness screenshot` uses the browser this session opened, not a new one.
|
|
103
|
+
|
|
104
|
+
- `devharness which` - which session this shell resolves to
|
|
105
|
+
- `devharness call <tool> '<json>'` - any tool: `devharness call config '{"action":"status"}'`
|
|
106
|
+
- `devharness sessions` / `send <id> "text"` / `read` / `reply <id> "text"` - the message tool, with `--wait=<ms>` to hold for an answer
|
|
107
|
+
- `--session=<id>` names a session explicitly, `--json` prints the raw response, `--timeout=<ms>` bounds the call
|
|
108
|
+
|
|
109
|
+
Two differences from an MCP call. Guards do not apply - a dead dev server port, a paused breakpoint or a pending bug blocks a tool call through MCP and does not block this one. And a session the supervisor has suspended is not listening, so the CLI reports that rather than waiting.
|
|
82
110
|
|
|
83
111
|
## Restarting devharness
|
|
84
112
|
|