monomind 1.10.37 → 1.10.39

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.
Files changed (25) hide show
  1. package/.claude/helpers/context-persistence-hook.mjs +1 -1
  2. package/.claude/helpers/utils/telemetry.cjs +1 -0
  3. package/package.json +1 -1
  4. package/packages/@monomind/cli/dist/src/browser/actions.d.ts +1 -0
  5. package/packages/@monomind/cli/dist/src/browser/actions.js +12 -5
  6. package/packages/@monomind/cli/dist/src/browser/batch.d.ts +0 -6
  7. package/packages/@monomind/cli/dist/src/browser/browser.js +37 -20
  8. package/packages/@monomind/cli/dist/src/browser/cdp.d.ts +1 -0
  9. package/packages/@monomind/cli/dist/src/browser/cdp.js +14 -4
  10. package/packages/@monomind/cli/dist/src/browser/console-log.d.ts +4 -4
  11. package/packages/@monomind/cli/dist/src/browser/console-log.js +49 -16
  12. package/packages/@monomind/cli/dist/src/browser/dialog.d.ts +2 -1
  13. package/packages/@monomind/cli/dist/src/browser/dialog.js +24 -10
  14. package/packages/@monomind/cli/dist/src/browser/find.d.ts +1 -1
  15. package/packages/@monomind/cli/dist/src/browser/find.js +29 -10
  16. package/packages/@monomind/cli/dist/src/browser/har.js +3 -3
  17. package/packages/@monomind/cli/dist/src/browser/network.d.ts +2 -0
  18. package/packages/@monomind/cli/dist/src/browser/network.js +64 -23
  19. package/packages/@monomind/cli/dist/src/browser/profiler.js +25 -15
  20. package/packages/@monomind/cli/dist/src/browser/record.js +7 -3
  21. package/packages/@monomind/cli/dist/src/browser/session.js +11 -7
  22. package/packages/@monomind/cli/dist/src/browser/trace.js +32 -17
  23. package/packages/@monomind/cli/dist/src/browser/wait.js +28 -14
  24. package/packages/@monomind/cli/dist/src/commands/browse.js +8 -14
  25. package/packages/@monomind/cli/package.json +1 -1
@@ -942,7 +942,7 @@ function chunkTranscript(messages) {
942
942
  if (msg.role === 'user') {
943
943
  const isSynthetic = Array.isArray(msg.content) &&
944
944
  msg.content.every(b => b.type === 'tool_result');
945
- if (isSynthetic && currentChunk) continue;
945
+ if (isSynthetic) continue;
946
946
  if (currentChunk) chunks.push(currentChunk);
947
947
  currentChunk = {
948
948
  userMessage: msg,
@@ -129,6 +129,7 @@ function _recordDecisionMarkers(promptText) {
129
129
  if (!matches || matches.length === 0) return;
130
130
  try {
131
131
  var f = path.join(CWD, '.monomind', 'decisions.jsonl');
132
+ fs.mkdirSync(path.dirname(f), { recursive: true });
132
133
  var entry = JSON.stringify({ ts: Date.now(), excerpts: matches.slice(0, 3), prompt: promptText.slice(0, 400) });
133
134
  fs.appendFileSync(f, entry + '\n');
134
135
  } catch (e) {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.10.37",
3
+ "version": "1.10.39",
4
4
  "description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -4,6 +4,7 @@ export declare function clickElement(client: CdpClient, sessionId: string, ref:
4
4
  export declare function clickPoint(client: CdpClient, sessionId: string, x: number, y: number, options?: ClickOptions): Promise<void>;
5
5
  export declare function fillElement(client: CdpClient, sessionId: string, ref: ElementRef, value: string): Promise<void>;
6
6
  export declare function typeText(client: CdpClient, sessionId: string, text: string): Promise<void>;
7
+ export declare function pressKeyCombo(client: CdpClient, sessionId: string, key: string, modifiers: number): Promise<void>;
7
8
  export declare function pressKey(client: CdpClient, sessionId: string, key: string): Promise<void>;
8
9
  export declare function scrollElement(client: CdpClient, sessionId: string, direction: 'up' | 'down' | 'left' | 'right', amount?: number, ref?: ElementRef): Promise<void>;
9
10
  export declare function hoverElement(client: CdpClient, sessionId: string, ref: ElementRef): Promise<void>;
@@ -20,10 +20,11 @@ export async function clickPoint(client, sessionId, x, y, options = {}) {
20
20
  const button = options.button ?? 'left';
21
21
  const clickCount = options.clickCount ?? 1;
22
22
  const modifiers = options.modifiers ?? 0;
23
- const shared = { x, y, button, modifiers, clickCount };
23
+ const shared = { x, y, button, modifiers };
24
24
  for (let i = 0; i < clickCount; i++) {
25
- await client.send('Input.dispatchMouseEvent', { ...shared, type: 'mousePressed', buttons: 1 }, sessionId);
26
- await client.send('Input.dispatchMouseEvent', { ...shared, type: 'mouseReleased', buttons: 0 }, sessionId);
25
+ const count = i + 1;
26
+ await client.send('Input.dispatchMouseEvent', { ...shared, type: 'mousePressed', buttons: 1, clickCount: count }, sessionId);
27
+ await client.send('Input.dispatchMouseEvent', { ...shared, type: 'mouseReleased', buttons: 0, clickCount: count }, sessionId);
27
28
  }
28
29
  }
29
30
  export async function fillElement(client, sessionId, ref, value) {
@@ -56,6 +57,12 @@ export async function fillElement(client, sessionId, ref, value) {
56
57
  export async function typeText(client, sessionId, text) {
57
58
  await client.send('Input.insertText', { text }, sessionId);
58
59
  }
60
+ export async function pressKeyCombo(client, sessionId, key, modifiers) {
61
+ const { text: _text, ...keyInfo } = resolveKey(key);
62
+ // rawKeyDown prevents Chrome from inserting the character text; only the shortcut fires
63
+ await client.send('Input.dispatchKeyEvent', { type: 'rawKeyDown', ...keyInfo, modifiers }, sessionId);
64
+ await client.send('Input.dispatchKeyEvent', { type: 'keyUp', ...keyInfo, modifiers }, sessionId);
65
+ }
59
66
  export async function pressKey(client, sessionId, key) {
60
67
  const keyInfo = resolveKey(key);
61
68
  await client.send('Input.dispatchKeyEvent', {
@@ -93,9 +100,9 @@ function resolveKey(key) {
93
100
  };
94
101
  if (keyMap[key])
95
102
  return keyMap[key];
96
- // Single printable character
103
+ // Single printable character — windowsVirtualKeyCode required for rawKeyDown shortcut dispatch
97
104
  if (key.length === 1) {
98
- return { key, code: `Key${key.toUpperCase()}`, text: key };
105
+ return { key, code: `Key${key.toUpperCase()}`, text: key, windowsVirtualKeyCode: key.toUpperCase().charCodeAt(0) };
99
106
  }
100
107
  return { key, code: key };
101
108
  }
@@ -2,12 +2,6 @@ export interface BatchCommand {
2
2
  command: string;
3
3
  args: string[];
4
4
  }
5
- export type BatchResult = {
6
- command: string;
7
- success: boolean;
8
- output?: string;
9
- error?: string;
10
- };
11
5
  export declare function parseBatchCommands(input: string[]): Promise<BatchCommand[]>;
12
6
  export declare function parseBatchJson(json: string): Promise<BatchCommand[]>;
13
7
  //# sourceMappingURL=batch.d.ts.map
@@ -4,7 +4,7 @@ import { tmpdir } from 'os';
4
4
  import { join } from 'path';
5
5
  import { CdpClient, fetchTargets, fetchNewTarget } from './cdp.js';
6
6
  import { CHROME_EXECUTABLES } from './types.js';
7
- import { enableConsoleCapture } from './console-log.js';
7
+ import { enableConsoleCapture, setupConsoleCapture } from './console-log.js';
8
8
  import { setupDialogAutoHandling } from './dialog.js';
9
9
  const DEFAULT_PORT = 9222;
10
10
  const LAUNCH_TIMEOUT = 10_000;
@@ -115,7 +115,8 @@ export async function connectToTarget(port, targetId) {
115
115
  client.send('Accessibility.enable', {}, sessionId),
116
116
  ]);
117
117
  // Wire up auto-capture listeners so console/errors/dialogs work immediately
118
- enableConsoleCapture(client, sessionId);
118
+ setupConsoleCapture(client, sessionId);
119
+ await enableConsoleCapture(client, sessionId);
119
120
  setupDialogAutoHandling(client, sessionId);
120
121
  return { client, target, sessionId };
121
122
  }
@@ -126,38 +127,54 @@ export async function openUrl(client, sessionId, url) {
126
127
  export async function waitForLoad(client, sessionId, condition = 'load', timeout = 30_000) {
127
128
  if (condition === 'load' || condition === 'domcontentloaded') {
128
129
  const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
129
- await Promise.race([
130
- client.once(event, sessionId),
131
- sleep(timeout).then(() => { throw new Error(`Timeout waiting for ${condition}`); }),
132
- ]);
130
+ const [eventPromise, cancelOnce] = client.onceWithOff(event, sessionId);
131
+ let timedOut = false;
132
+ const timeoutPromise = sleep(timeout).then(() => { timedOut = true; });
133
+ try {
134
+ await Promise.race([eventPromise, timeoutPromise]);
135
+ if (timedOut)
136
+ throw new Error(`Timeout waiting for ${condition}`);
137
+ }
138
+ finally {
139
+ cancelOnce();
140
+ }
133
141
  return;
134
142
  }
135
143
  // networkidle: no network requests for 500ms
136
- await Promise.race([
137
- waitForNetworkIdle(client, sessionId, 500),
138
- sleep(timeout).then(() => { throw new Error('Timeout waiting for networkidle'); }),
139
- ]);
144
+ await waitForNetworkIdle(client, sessionId, 500, timeout);
140
145
  }
141
- async function waitForNetworkIdle(client, sessionId, idleMs) {
142
- return new Promise((resolve) => {
146
+ async function waitForNetworkIdle(client, sessionId, idleMs, timeout) {
147
+ return new Promise((resolve, reject) => {
143
148
  let pending = 0;
144
- let timer = null;
145
- const settle = () => {
149
+ let idleTimer = null;
150
+ const killTimer = setTimeout(() => {
151
+ cleanup();
152
+ reject(new Error('Timeout waiting for networkidle'));
153
+ }, timeout);
154
+ const cleanup = () => {
155
+ if (idleTimer) {
156
+ clearTimeout(idleTimer);
157
+ idleTimer = null;
158
+ }
159
+ clearTimeout(killTimer);
146
160
  offReq();
147
161
  offResp();
148
162
  offFail();
163
+ };
164
+ const settle = () => {
165
+ cleanup();
149
166
  resolve();
150
167
  };
151
168
  const check = () => {
152
169
  if (pending === 0) {
153
- if (timer)
154
- clearTimeout(timer);
155
- timer = setTimeout(settle, idleMs);
170
+ if (idleTimer)
171
+ clearTimeout(idleTimer);
172
+ idleTimer = setTimeout(settle, idleMs);
156
173
  }
157
174
  else {
158
- if (timer) {
159
- clearTimeout(timer);
160
- timer = null;
175
+ if (idleTimer) {
176
+ clearTimeout(idleTimer);
177
+ idleTimer = null;
161
178
  }
162
179
  }
163
180
  };
@@ -9,6 +9,7 @@ export declare class CdpClient {
9
9
  send<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T>;
10
10
  on(event: string, fn: (params: Record<string, unknown>, sessionId?: string) => void): () => void;
11
11
  once(event: string, sessionId?: string): Promise<Record<string, unknown>>;
12
+ onceWithOff(event: string, sessionId?: string): [Promise<Record<string, unknown>>, () => void];
12
13
  close(): void;
13
14
  isConnected(): boolean;
14
15
  }
@@ -39,8 +39,12 @@ export class CdpClient {
39
39
  else if (msg.method) {
40
40
  const listeners = this.eventListeners.get(msg.method);
41
41
  if (listeners) {
42
- for (const fn of listeners)
43
- fn(msg.params ?? {}, msg.sessionId);
42
+ for (const fn of listeners) {
43
+ try {
44
+ fn(msg.params ?? {}, msg.sessionId);
45
+ }
46
+ catch { /* isolate per-listener errors */ }
47
+ }
44
48
  }
45
49
  }
46
50
  }
@@ -75,14 +79,20 @@ export class CdpClient {
75
79
  return () => this.eventListeners.get(event)?.delete(fn);
76
80
  }
77
81
  once(event, sessionId) {
78
- return new Promise((resolve) => {
79
- const off = this.on(event, (params, sid) => {
82
+ const [promise] = this.onceWithOff(event, sessionId);
83
+ return promise;
84
+ }
85
+ onceWithOff(event, sessionId) {
86
+ let off = () => { };
87
+ const promise = new Promise((resolve) => {
88
+ off = this.on(event, (params, sid) => {
80
89
  if (sessionId !== undefined && sid !== sessionId)
81
90
  return;
82
91
  off();
83
92
  resolve(params);
84
93
  });
85
94
  });
95
+ return [promise, () => off()];
86
96
  }
87
97
  close() {
88
98
  this.ws?.close();
@@ -15,8 +15,8 @@ export interface PageError {
15
15
  }
16
16
  export declare function setupConsoleCapture(client: CdpClient, sessionId: string): void;
17
17
  export declare function enableConsoleCapture(client: CdpClient, sessionId: string): Promise<void>;
18
- export declare function getConsoleMessages(): ConsoleMessage[];
19
- export declare function clearConsoleMessages(): void;
20
- export declare function getPageErrors(): PageError[];
21
- export declare function clearPageErrors(): void;
18
+ export declare function getConsoleMessages(sessionId?: string): ConsoleMessage[];
19
+ export declare function clearConsoleMessages(sessionId?: string): void;
20
+ export declare function getPageErrors(sessionId?: string): PageError[];
21
+ export declare function clearPageErrors(sessionId?: string): void;
22
22
  //# sourceMappingURL=console-log.d.ts.map
@@ -1,22 +1,41 @@
1
- let consoleMessages = [];
2
- let pageErrors = [];
1
+ const _consoleMessages = new Map();
2
+ const _pageErrors = new Map();
3
+ const _consoleListeners = new Map();
4
+ function messagesFor(sessionId) {
5
+ if (!_consoleMessages.has(sessionId))
6
+ _consoleMessages.set(sessionId, []);
7
+ return _consoleMessages.get(sessionId);
8
+ }
9
+ function errorsFor(sessionId) {
10
+ if (!_pageErrors.has(sessionId))
11
+ _pageErrors.set(sessionId, []);
12
+ return _pageErrors.get(sessionId);
13
+ }
3
14
  export function setupConsoleCapture(client, sessionId) {
4
- client.on('Runtime.consoleAPICalled', (params, sid) => {
15
+ // Remove stale listeners from any prior connection on this sessionId
16
+ const prevOffs = _consoleListeners.get(sessionId);
17
+ if (prevOffs) {
18
+ for (const off of prevOffs)
19
+ off();
20
+ }
21
+ _consoleMessages.set(sessionId, []);
22
+ _pageErrors.set(sessionId, []);
23
+ const off1 = client.on('Runtime.consoleAPICalled', (params, sid) => {
5
24
  if (sid !== sessionId)
6
25
  return;
7
26
  const args = params.args ?? [];
8
27
  const text = args.map((a) => a.description ?? String(a.value ?? '')).join(' ');
9
- consoleMessages.push({
28
+ messagesFor(sessionId).push({
10
29
  type: params.type ?? 'log',
11
30
  text,
12
31
  timestamp: Date.now(),
13
32
  });
14
33
  });
15
- client.on('Log.entryAdded', (params, sid) => {
34
+ const off2 = client.on('Log.entryAdded', (params, sid) => {
16
35
  if (sid !== sessionId)
17
36
  return;
18
37
  const entry = params.entry;
19
- consoleMessages.push({
38
+ messagesFor(sessionId).push({
20
39
  type: entry.level ?? 'log',
21
40
  text: entry.text ?? '',
22
41
  timestamp: Date.now(),
@@ -24,11 +43,11 @@ export function setupConsoleCapture(client, sessionId) {
24
43
  lineNumber: entry.lineNumber,
25
44
  });
26
45
  });
27
- client.on('Runtime.exceptionThrown', (params, sid) => {
46
+ const off3 = client.on('Runtime.exceptionThrown', (params, sid) => {
28
47
  if (sid !== sessionId)
29
48
  return;
30
49
  const detail = params.exceptionDetails;
31
- pageErrors.push({
50
+ errorsFor(sessionId).push({
32
51
  text: detail.text ?? 'Unknown error',
33
52
  url: detail.url,
34
53
  lineNumber: detail.lineNumber,
@@ -36,20 +55,34 @@ export function setupConsoleCapture(client, sessionId) {
36
55
  timestamp: Date.now(),
37
56
  });
38
57
  });
58
+ _consoleListeners.set(sessionId, [off1, off2, off3]);
39
59
  }
40
60
  export async function enableConsoleCapture(client, sessionId) {
41
61
  await client.send('Log.enable', {}, sessionId);
42
62
  }
43
- export function getConsoleMessages() {
44
- return [...consoleMessages];
63
+ export function getConsoleMessages(sessionId) {
64
+ if (sessionId)
65
+ return [...(messagesFor(sessionId))];
66
+ // Fallback: return all messages across all sessions (legacy callers)
67
+ return [..._consoleMessages.values()].flat();
45
68
  }
46
- export function clearConsoleMessages() {
47
- consoleMessages = [];
69
+ export function clearConsoleMessages(sessionId) {
70
+ if (sessionId) {
71
+ _consoleMessages.set(sessionId, []);
72
+ return;
73
+ }
74
+ _consoleMessages.clear();
48
75
  }
49
- export function getPageErrors() {
50
- return [...pageErrors];
76
+ export function getPageErrors(sessionId) {
77
+ if (sessionId)
78
+ return [...(errorsFor(sessionId))];
79
+ return [..._pageErrors.values()].flat();
51
80
  }
52
- export function clearPageErrors() {
53
- pageErrors = [];
81
+ export function clearPageErrors(sessionId) {
82
+ if (sessionId) {
83
+ _pageErrors.set(sessionId, []);
84
+ return;
85
+ }
86
+ _pageErrors.clear();
54
87
  }
55
88
  //# sourceMappingURL=console-log.js.map
@@ -5,7 +5,8 @@ export interface DialogInfo {
5
5
  defaultPrompt?: string;
6
6
  }
7
7
  export declare function setupDialogAutoHandling(client: CdpClient, sessionId: string, autoAccept?: boolean): void;
8
+ export declare function teardownDialogHandling(sessionId: string): void;
8
9
  export declare function acceptDialog(client: CdpClient, sessionId: string, text?: string): Promise<void>;
9
10
  export declare function dismissDialog(client: CdpClient, sessionId: string): Promise<void>;
10
- export declare function getDialogStatus(): DialogInfo | null;
11
+ export declare function getDialogStatus(sessionId: string): DialogInfo | null;
11
12
  //# sourceMappingURL=dialog.d.ts.map
@@ -1,6 +1,10 @@
1
- let pendingDialog = null;
1
+ const _pendingDialogs = new Map();
2
+ const _dialogListeners = new Map();
2
3
  export function setupDialogAutoHandling(client, sessionId, autoAccept = true) {
3
- client.on('Page.javascriptDialogOpening', async (params, sid) => {
4
+ if (_pendingDialogs.has(sessionId))
5
+ return;
6
+ _pendingDialogs.set(sessionId, null);
7
+ const off1 = client.on('Page.javascriptDialogOpening', async (params, sid) => {
4
8
  if (sid !== sessionId)
5
9
  return;
6
10
  const info = {
@@ -8,29 +12,39 @@ export function setupDialogAutoHandling(client, sessionId, autoAccept = true) {
8
12
  message: params.message,
9
13
  defaultPrompt: params.defaultPrompt,
10
14
  };
11
- pendingDialog = info;
15
+ _pendingDialogs.set(sessionId, info);
12
16
  if (autoAccept && (info.type === 'alert' || info.type === 'beforeunload')) {
13
17
  await client.send('Page.handleJavaScriptDialog', { accept: true }, sessionId);
14
- pendingDialog = null;
18
+ _pendingDialogs.set(sessionId, null);
15
19
  }
16
20
  });
17
- client.on('Page.javascriptDialogClosed', (_, sid) => {
21
+ const off2 = client.on('Page.javascriptDialogClosed', (_, sid) => {
18
22
  if (sid === sessionId)
19
- pendingDialog = null;
23
+ _pendingDialogs.set(sessionId, null);
20
24
  });
25
+ _dialogListeners.set(sessionId, [off1, off2]);
26
+ }
27
+ export function teardownDialogHandling(sessionId) {
28
+ const offs = _dialogListeners.get(sessionId);
29
+ if (offs) {
30
+ for (const off of offs)
31
+ off();
32
+ _dialogListeners.delete(sessionId);
33
+ }
34
+ _pendingDialogs.delete(sessionId);
21
35
  }
22
36
  export async function acceptDialog(client, sessionId, text) {
23
37
  await client.send('Page.handleJavaScriptDialog', {
24
38
  accept: true,
25
39
  promptText: text,
26
40
  }, sessionId);
27
- pendingDialog = null;
41
+ _pendingDialogs.set(sessionId, null);
28
42
  }
29
43
  export async function dismissDialog(client, sessionId) {
30
44
  await client.send('Page.handleJavaScriptDialog', { accept: false }, sessionId);
31
- pendingDialog = null;
45
+ _pendingDialogs.set(sessionId, null);
32
46
  }
33
- export function getDialogStatus() {
34
- return pendingDialog;
47
+ export function getDialogStatus(sessionId) {
48
+ return _pendingDialogs.get(sessionId) ?? null;
35
49
  }
36
50
  //# sourceMappingURL=dialog.js.map
@@ -8,7 +8,7 @@ export interface FindOptions {
8
8
  first?: boolean;
9
9
  last?: boolean;
10
10
  }
11
- export declare function findBySelector(client: CdpClient, sessionId: string, selector: string, options?: FindOptions): Promise<string | null>;
11
+ export declare function findBySelector(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, selector: string, options?: FindOptions): Promise<ElementRef | null>;
12
12
  export declare function findByRole(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, role: string, options?: FindOptions): Promise<ElementRef | null>;
13
13
  export declare function findByText(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, text: string, options?: FindOptions): Promise<ElementRef | null>;
14
14
  export declare function findByLabel(client: CdpClient, sessionId: string, refs: Map<string, ElementRef>, label: string, options?: FindOptions): Promise<ElementRef | null>;
@@ -1,17 +1,36 @@
1
1
  import { evaluateJs } from './actions.js';
2
2
  import { getObjectIdForRef } from './snapshot.js';
3
- export async function findBySelector(client, sessionId, selector, options = {}) {
4
- let expr;
5
- if (options.nth !== undefined) {
6
- expr = `document.querySelectorAll(${JSON.stringify(selector)})[${options.nth - 1}]?.outerHTML ?? null`;
3
+ export async function findBySelector(client, sessionId, refs, selector, options = {}) {
4
+ try {
5
+ const doc = await client.send('DOM.getDocument', {}, sessionId);
6
+ let targetNodeId;
7
+ if (options.nth !== undefined || options.last) {
8
+ const result = await client.send('DOM.querySelectorAll', { nodeId: doc.root.nodeId, selector }, sessionId);
9
+ const nodeIds = result.nodeIds ?? [];
10
+ if (nodeIds.length === 0)
11
+ return null;
12
+ targetNodeId = options.last ? nodeIds[nodeIds.length - 1] : (nodeIds[(options.nth ?? 1) - 1] ?? 0);
13
+ }
14
+ else {
15
+ const result = await client.send('DOM.querySelector', { nodeId: doc.root.nodeId, selector }, sessionId);
16
+ targetNodeId = result.nodeId ?? 0;
17
+ }
18
+ if (!targetNodeId)
19
+ return null;
20
+ const desc = await client.send('DOM.describeNode', { nodeId: targetNodeId }, sessionId);
21
+ const backendDOMNodeId = desc.node?.backendNodeId;
22
+ if (!backendDOMNodeId)
23
+ return null;
24
+ // Return existing ref from snapshot if this node is already indexed
25
+ const existing = [...refs.values()].find((r) => r.backendDOMNodeId === backendDOMNodeId);
26
+ if (existing)
27
+ return existing;
28
+ // Synthetic ref for elements not represented in the AX tree
29
+ return { ref: `sel-${backendDOMNodeId}`, role: 'generic', name: selector, nodeId: targetNodeId, backendDOMNodeId };
7
30
  }
8
- else if (options.last) {
9
- expr = `Array.from(document.querySelectorAll(${JSON.stringify(selector)})).at(-1)?.outerHTML ?? null`;
10
- }
11
- else {
12
- expr = `document.querySelector(${JSON.stringify(selector)})?.outerHTML ?? null`;
31
+ catch {
32
+ return null;
13
33
  }
14
- return evaluateJs(client, sessionId, expr);
15
34
  }
16
35
  export async function findByRole(client, sessionId, refs, role, options = {}) {
17
36
  const lowerRole = role.toLowerCase();
@@ -51,7 +51,7 @@ export async function startHarRecording(client, sessionId) {
51
51
  entry.endTime = cdpToWall(p.timestamp);
52
52
  }
53
53
  });
54
- _sessions.set(sessionId, { requests, offReq, offResp, offFinished, startTime, startWallMs, refCdpTs });
54
+ _sessions.set(sessionId, { requests, offReq, offResp, offFinished, startTime, startWallMs });
55
55
  }
56
56
  export async function stopHarRecording(client, sessionId, outputPath, captureResponseBodies = false) {
57
57
  const state = _sessions.get(sessionId);
@@ -68,14 +68,14 @@ export async function stopHarRecording(client, sessionId, outputPath, captureRes
68
68
  entry.responseBody = body.base64Encoded
69
69
  ? Buffer.from(body.body, 'base64').toString('utf8')
70
70
  : body.body;
71
- entry.size = entry.responseBody.length;
71
+ entry.size = entry.responseBody?.length ?? 0;
72
72
  }
73
73
  catch {
74
74
  // body may not be available for cached/redirected responses
75
75
  }
76
76
  }
77
77
  }
78
- _sessions.delete(sessionId);
78
+ _sessions.delete(sessionId); // always runs — response body loop errors are caught per-entry
79
79
  const har = buildHar(Array.from(state.requests.values()), state.startTime);
80
80
  const path = outputPath ?? join(tmpdir(), `monomind-har-${Date.now()}.har`);
81
81
  await writeFile(path, JSON.stringify(har, null, 2));
@@ -24,4 +24,6 @@ export declare function clearCapturedRequests(sessionId: string): void;
24
24
  export declare function disableInterception(client: CdpClient, sessionId: string): Promise<void>;
25
25
  export declare function getLocalStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
26
26
  export declare function setLocalStorage(client: CdpClient, sessionId: string, data: Record<string, string>): Promise<void>;
27
+ export declare function getSessionStorage(client: CdpClient, sessionId: string): Promise<Record<string, string>>;
28
+ export declare function setSessionStorage(client: CdpClient, sessionId: string, data: Record<string, string>): Promise<void>;
27
29
  //# sourceMappingURL=network.d.ts.map
@@ -16,39 +16,55 @@ export async function enableInterception(client, sessionId) {
16
16
  patterns: [{ requestStage: 'Request' }],
17
17
  }, sessionId);
18
18
  }
19
+ const _routeListeners = new Map();
19
20
  export async function setupRoutes(client, sessionId, routes) {
20
- if (routes.length === 0)
21
+ // Remove previous route listener for this session before registering a new one
22
+ const prevOff = _routeListeners.get(sessionId);
23
+ if (prevOff) {
24
+ prevOff();
25
+ _routeListeners.delete(sessionId);
26
+ }
27
+ if (routes.length === 0) {
28
+ await client.send('Fetch.disable', {}, sessionId);
21
29
  return;
30
+ }
22
31
  await client.send('Fetch.enable', {
23
32
  patterns: routes.map((r) => ({ urlPattern: globToFetchPattern(r.pattern), requestStage: 'Request' })),
24
33
  }, sessionId);
25
- client.on('Fetch.requestPaused', async (params, sid) => {
34
+ const off = client.on('Fetch.requestPaused', async (params, sid) => {
26
35
  if (sid !== sessionId)
27
36
  return;
28
37
  const { requestId, request } = params;
29
- const matchedRoute = routes.find((r) => globToRegex(r.pattern).test(request.url));
30
- if (!matchedRoute) {
31
- await client.send('Fetch.continueRequest', { requestId }, sessionId);
32
- return;
33
- }
34
- switch (matchedRoute.action) {
35
- case 'abort':
36
- await client.send('Fetch.failRequest', { requestId, errorReason: 'Failed' }, sessionId);
37
- break;
38
- case 'fulfill':
39
- await client.send('Fetch.fulfillRequest', {
40
- requestId,
41
- responseCode: matchedRoute.response?.status ?? 200,
42
- responseHeaders: Object.entries(matchedRoute.response?.headers ?? {}).map(([name, value]) => ({ name, value })),
43
- body: matchedRoute.response?.body ? Buffer.from(matchedRoute.response.body).toString('base64') : '',
44
- }, sessionId);
45
- break;
46
- case 'continue':
47
- default:
38
+ try {
39
+ const matchedRoute = routes.find((r) => globToRegex(r.pattern).test(request.url));
40
+ if (!matchedRoute) {
48
41
  await client.send('Fetch.continueRequest', { requestId }, sessionId);
49
- break;
42
+ return;
43
+ }
44
+ switch (matchedRoute.action) {
45
+ case 'abort':
46
+ await client.send('Fetch.failRequest', { requestId, errorReason: 'Failed' }, sessionId);
47
+ break;
48
+ case 'fulfill':
49
+ await client.send('Fetch.fulfillRequest', {
50
+ requestId,
51
+ responseCode: matchedRoute.response?.status ?? 200,
52
+ responseHeaders: Object.entries(matchedRoute.response?.headers ?? {}).map(([name, value]) => ({ name, value })),
53
+ body: matchedRoute.response?.body ? Buffer.from(matchedRoute.response.body).toString('base64') : '',
54
+ }, sessionId);
55
+ break;
56
+ case 'continue':
57
+ default:
58
+ await client.send('Fetch.continueRequest', { requestId }, sessionId);
59
+ break;
60
+ }
61
+ }
62
+ catch {
63
+ // Best-effort: resume the request so it doesn't hang in Chrome
64
+ await client.send('Fetch.continueRequest', { requestId }, sessionId).catch(() => { });
50
65
  }
51
66
  });
67
+ _routeListeners.set(sessionId, off);
52
68
  }
53
69
  const _capturedRequests = new Map();
54
70
  const _captureListeners = new Map();
@@ -103,6 +119,11 @@ export function clearCapturedRequests(sessionId) {
103
119
  _capturedRequests.set(sessionId, []);
104
120
  }
105
121
  export async function disableInterception(client, sessionId) {
122
+ const prevOff = _routeListeners.get(sessionId);
123
+ if (prevOff) {
124
+ prevOff();
125
+ _routeListeners.delete(sessionId);
126
+ }
106
127
  await client.send('Fetch.disable', {}, sessionId);
107
128
  }
108
129
  export async function getLocalStorage(client, sessionId) {
@@ -121,7 +142,27 @@ export async function setLocalStorage(client, sessionId, data) {
121
142
  const script = Object.entries(data)
122
143
  .map(([k, v]) => `localStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
123
144
  .join('; ');
124
- await client.send('Runtime.evaluate', { expression: script }, sessionId);
145
+ if (script)
146
+ await client.send('Runtime.evaluate', { expression: script }, sessionId);
147
+ }
148
+ export async function getSessionStorage(client, sessionId) {
149
+ const result = await client.send('Runtime.evaluate', {
150
+ expression: 'JSON.stringify(Object.fromEntries(Object.entries(sessionStorage)))',
151
+ returnByValue: true,
152
+ }, sessionId);
153
+ try {
154
+ return JSON.parse(result.result?.value ?? '{}');
155
+ }
156
+ catch {
157
+ return {};
158
+ }
159
+ }
160
+ export async function setSessionStorage(client, sessionId, data) {
161
+ const script = Object.entries(data)
162
+ .map(([k, v]) => `sessionStorage.setItem(${JSON.stringify(k)}, ${JSON.stringify(v)})`)
163
+ .join('; ');
164
+ if (script)
165
+ await client.send('Runtime.evaluate', { expression: script }, sessionId);
125
166
  }
126
167
  function globToRegex(pattern) {
127
168
  const escaped = pattern
@@ -17,9 +17,15 @@ export async function stopCpuProfile(client, sessionId, outputPath) {
17
17
  if (!_sessions.has(sessionId)) {
18
18
  throw new Error('No active CPU profiler for this session');
19
19
  }
20
- const result = await client.send('Profiler.stop', {}, sessionId);
21
- await client.send('Profiler.disable', {}, sessionId);
22
- _sessions.delete(sessionId);
20
+ let result;
21
+ try {
22
+ result = await client.send('Profiler.stop', {}, sessionId);
23
+ }
24
+ finally {
25
+ _sessions.delete(sessionId);
26
+ }
27
+ // Disable after capturing the profile so a disable failure doesn't discard valid data
28
+ await client.send('Profiler.disable', {}, sessionId).catch(() => { });
23
29
  const path = outputPath ?? join(tmpdir(), `monomind-profile-${Date.now()}.cpuprofile`);
24
30
  await writeFile(path, JSON.stringify(result.profile));
25
31
  return path;
@@ -35,19 +41,23 @@ export async function startHeapSnapshot(client, sessionId, outputPath) {
35
41
  return;
36
42
  chunks.push(params.chunk);
37
43
  });
38
- await new Promise((resolve, reject) => {
39
- const off2 = client.on('HeapProfiler.reportHeapSnapshotProgress', (params, sid) => {
40
- if (sid !== sessionId)
41
- return;
42
- if (params.finished) {
43
- off2();
44
- resolve();
45
- }
44
+ try {
45
+ await new Promise((resolve, reject) => {
46
+ const off2 = client.on('HeapProfiler.reportHeapSnapshotProgress', (params, sid) => {
47
+ if (sid !== sessionId)
48
+ return;
49
+ if (params.finished) {
50
+ off2();
51
+ resolve();
52
+ }
53
+ });
54
+ client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true }, sessionId)
55
+ .catch((err) => { off2(); reject(err); });
46
56
  });
47
- client.send('HeapProfiler.takeHeapSnapshot', { reportProgress: true }, sessionId)
48
- .catch((err) => { off2(); reject(err); });
49
- });
50
- off();
57
+ }
58
+ finally {
59
+ off();
60
+ }
51
61
  await client.send('HeapProfiler.disable', {}, sessionId);
52
62
  const path = outputPath ?? join(tmpdir(), `monomind-heap-${Date.now()}.heapsnapshot`);
53
63
  await writeFile(path, chunks.join(''));
@@ -34,9 +34,13 @@ export async function stopRecording(client, sessionId, outputPath) {
34
34
  const state = _sessions.get(sessionId);
35
35
  if (!state)
36
36
  throw new Error('No active recording for this session');
37
- await client.send('Page.stopScreencast', {}, sessionId);
38
- state.offScreencast?.();
39
- _sessions.delete(sessionId);
37
+ try {
38
+ await client.send('Page.stopScreencast', {}, sessionId);
39
+ }
40
+ finally {
41
+ state.offScreencast?.();
42
+ _sessions.delete(sessionId);
43
+ }
40
44
  const path = outputPath ?? join(tmpdir(), `monomind-screencast-${Date.now()}.frames.json`);
41
45
  await writeFile(path, JSON.stringify({ frameCount: state.frames.length, frames: state.frames }));
42
46
  return path;
@@ -2,13 +2,14 @@ import { readFile, writeFile, mkdir } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { homedir } from 'os';
5
- import { getCookies, setCookies, getLocalStorage, setLocalStorage } from './network.js';
5
+ import { getCookies, setCookies, getLocalStorage, setLocalStorage, getSessionStorage, setSessionStorage } from './network.js';
6
6
  const SESSION_DIR = join(homedir(), '.monomind', 'browser-sessions');
7
7
  export async function saveSession(client, sessionId, targetId, name, url, title) {
8
8
  await mkdir(SESSION_DIR, { recursive: true });
9
9
  const cookies = await getCookies(client, sessionId);
10
10
  const localStorage = await getLocalStorage(client, sessionId);
11
- const state = { targetId, sessionId, url, title, cookies, localStorage };
11
+ const sessionStorage = await getSessionStorage(client, sessionId);
12
+ const state = { targetId, sessionId, url, title, cookies, localStorage, sessionStorage };
12
13
  const filePath = join(SESSION_DIR, `${name}.json`);
13
14
  await writeFile(filePath, JSON.stringify(state, null, 2));
14
15
  return filePath;
@@ -20,24 +21,27 @@ export async function loadSession(client, sessionId, name) {
20
21
  const raw = await readFile(filePath, 'utf8');
21
22
  const state = JSON.parse(raw);
22
23
  await setCookies(client, sessionId, state.cookies);
23
- if (state.localStorage) {
24
+ if (state.localStorage)
24
25
  await setLocalStorage(client, sessionId, state.localStorage);
25
- }
26
+ if (state.sessionStorage)
27
+ await setSessionStorage(client, sessionId, state.sessionStorage);
26
28
  return state;
27
29
  }
28
30
  export async function saveStateFile(client, sessionId, targetId, filePath, url, title) {
29
31
  const cookies = await getCookies(client, sessionId);
30
32
  const localStorage = await getLocalStorage(client, sessionId);
31
- const state = { targetId, sessionId, url, title, cookies, localStorage };
33
+ const sessionStorage = await getSessionStorage(client, sessionId);
34
+ const state = { targetId, sessionId, url, title, cookies, localStorage, sessionStorage };
32
35
  await writeFile(filePath, JSON.stringify(state, null, 2));
33
36
  }
34
37
  export async function loadStateFile(client, sessionId, filePath) {
35
38
  const raw = await readFile(filePath, 'utf8');
36
39
  const state = JSON.parse(raw);
37
40
  await setCookies(client, sessionId, state.cookies);
38
- if (state.localStorage) {
41
+ if (state.localStorage)
39
42
  await setLocalStorage(client, sessionId, state.localStorage);
40
- }
43
+ if (state.sessionStorage)
44
+ await setSessionStorage(client, sessionId, state.sessionStorage);
41
45
  return state;
42
46
  }
43
47
  export async function listSessions() {
@@ -30,28 +30,43 @@ export async function startTrace(client, sessionId, options = {}) {
30
30
  const cats = [...(options.categories ?? DEFAULT_CATEGORIES)];
31
31
  if (options.screenshots)
32
32
  cats.push('disabled-by-default-devtools.screenshot');
33
- await client.send('Tracing.start', {
34
- traceConfig: {
35
- includedCategories: cats.filter((c) => !c.startsWith('-')),
36
- excludedCategories: cats.filter((c) => c.startsWith('-')).map((c) => c.slice(1)),
37
- },
38
- }, sessionId);
33
+ try {
34
+ await client.send('Tracing.start', {
35
+ traceConfig: {
36
+ includedCategories: cats.filter((c) => !c.startsWith('-')),
37
+ excludedCategories: cats.filter((c) => c.startsWith('-')).map((c) => c.slice(1)),
38
+ },
39
+ }, sessionId);
40
+ }
41
+ catch (err) {
42
+ offData();
43
+ _sessions.delete(sessionId);
44
+ throw err;
45
+ }
39
46
  }
40
47
  export async function stopTrace(client, sessionId, outputPath) {
41
48
  const state = _sessions.get(sessionId);
42
49
  if (!state)
43
50
  throw new Error('No active trace for this session');
44
- await new Promise((resolve) => {
45
- const off = client.on('Tracing.tracingComplete', (_params, sid) => {
46
- if (sid !== sessionId)
47
- return;
48
- off();
49
- resolve();
50
- });
51
- client.send('Tracing.end', {}, sessionId).catch(() => { });
52
- });
53
- state.offData();
54
- _sessions.delete(sessionId);
51
+ try {
52
+ const [completePromise, cancelOnce] = client.onceWithOff('Tracing.tracingComplete', sessionId);
53
+ let timedOut = false;
54
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => { timedOut = true; cancelOnce(); reject(new Error('Timeout waiting for Tracing.tracingComplete')); }, 30_000));
55
+ try {
56
+ await client.send('Tracing.end', {}, sessionId);
57
+ await Promise.race([completePromise, timeoutPromise]);
58
+ }
59
+ catch (err) {
60
+ cancelOnce();
61
+ throw err;
62
+ }
63
+ if (timedOut)
64
+ throw new Error('Timeout waiting for Tracing.tracingComplete');
65
+ }
66
+ finally {
67
+ state.offData();
68
+ _sessions.delete(sessionId);
69
+ }
55
70
  const trace = {
56
71
  traceEvents: state.events,
57
72
  metadata: { 'clock-offset-since-epoch': Date.now() },
@@ -30,33 +30,47 @@ async function waitForLoad(client, sessionId, condition, timeout) {
30
30
  return;
31
31
  }
32
32
  const event = condition === 'load' ? 'Page.loadEventFired' : 'Page.domContentEventFired';
33
- await Promise.race([
34
- client.once(event, sessionId),
35
- sleep(timeout).then(() => { throw new Error(`Timeout waiting for ${condition}`); }),
36
- ]);
33
+ const [eventPromise, cancelOnce] = client.onceWithOff(event, sessionId);
34
+ let timedOut = false;
35
+ const timeoutPromise = sleep(timeout).then(() => { timedOut = true; });
36
+ try {
37
+ await Promise.race([eventPromise, timeoutPromise]);
38
+ if (timedOut)
39
+ throw new Error(`Timeout waiting for ${condition}`);
40
+ }
41
+ finally {
42
+ cancelOnce();
43
+ }
37
44
  }
38
45
  async function waitForNetworkIdle(client, sessionId, idleMs, timeout) {
39
46
  return new Promise((resolve, reject) => {
40
47
  let pending = 0;
41
- let timer = null;
42
- const killTimer = setTimeout(() => reject(new Error('Timeout waiting for networkidle')), timeout);
43
- const settle = () => {
48
+ let idleTimer = null;
49
+ const cleanup = () => {
50
+ if (idleTimer) {
51
+ clearTimeout(idleTimer);
52
+ idleTimer = null;
53
+ }
44
54
  clearTimeout(killTimer);
45
55
  offReq();
46
56
  offResp();
47
57
  offFail();
48
- resolve();
49
58
  };
59
+ const killTimer = setTimeout(() => {
60
+ cleanup();
61
+ reject(new Error('Timeout waiting for networkidle'));
62
+ }, timeout);
63
+ const settle = () => { cleanup(); resolve(); };
50
64
  const check = () => {
51
65
  if (pending === 0) {
52
- if (timer)
53
- clearTimeout(timer);
54
- timer = setTimeout(settle, idleMs);
66
+ if (idleTimer)
67
+ clearTimeout(idleTimer);
68
+ idleTimer = setTimeout(settle, idleMs);
55
69
  }
56
70
  else {
57
- if (timer) {
58
- clearTimeout(timer);
59
- timer = null;
71
+ if (idleTimer) {
72
+ clearTimeout(idleTimer);
73
+ idleTimer = null;
60
74
  }
61
75
  }
62
76
  };
@@ -994,12 +994,12 @@ const clipboardCommand = {
994
994
  break;
995
995
  }
996
996
  case 'copy':
997
- await browser.pressKey(client, sessionId, 'c');
998
- output.printSuccess('Copy sent (Ctrl+C equivalent)');
997
+ await browser.pressKeyCombo(client, sessionId, 'c', 2); // Ctrl modifier
998
+ output.printSuccess('Copy sent');
999
999
  break;
1000
1000
  case 'paste':
1001
- await browser.pressKey(client, sessionId, 'v');
1002
- output.printSuccess('Paste sent (Ctrl+V equivalent)');
1001
+ await browser.pressKeyCombo(client, sessionId, 'v', 2); // Ctrl modifier
1002
+ output.printSuccess('Paste sent');
1003
1003
  break;
1004
1004
  default:
1005
1005
  throw new Error('Usage: monomind browse clipboard read|write|copy|paste');
@@ -1026,7 +1026,7 @@ const dialogCommand = {
1026
1026
  output.printSuccess('Dialog dismissed');
1027
1027
  break;
1028
1028
  case 'status': {
1029
- const info = browser.getDialogStatus();
1029
+ const info = browser.getDialogStatus(sessionId);
1030
1030
  if (info) {
1031
1031
  print(`Dialog open: type=${info.type} message="${info.message}"`);
1032
1032
  }
@@ -1354,15 +1354,9 @@ const findCommand = {
1354
1354
  case 'placeholder':
1355
1355
  ref = await browser.findByPlaceholder(client, sessionId, _refs, value, opts);
1356
1356
  break;
1357
- case 'selector': {
1358
- const html = await browser.findBySelector(client, sessionId, value, opts);
1359
- if (!html) {
1360
- output.printWarning(`selector not found: ${value}`);
1361
- return { success: false };
1362
- }
1363
- output.printSuccess(`Found: ${value}`);
1364
- return { success: true, data: { html } };
1365
- }
1357
+ case 'selector':
1358
+ ref = await browser.findBySelector(client, sessionId, _refs, value, opts);
1359
+ break;
1366
1360
  case 'testid': {
1367
1361
  const sel = await browser.findByTestId(client, sessionId, value);
1368
1362
  if (!sel) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.37",
3
+ "version": "1.10.39",
4
4
  "type": "module",
5
5
  "description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",