newmark-agent 0.5.11 → 0.5.13

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.
@@ -37,6 +37,7 @@ exports.isRuntimeProcessAlive = isRuntimeProcessAlive;
37
37
  exports.prepareRuntimeLifecycle = prepareRuntimeLifecycle;
38
38
  exports.beginRuntimeLifecycle = beginRuntimeLifecycle;
39
39
  exports.markRuntimeLifecycleClean = markRuntimeLifecycleClean;
40
+ exports.markRuntimeLifecycleExitedByPid = markRuntimeLifecycleExitedByPid;
40
41
  exports.runtimeLifecycleState = runtimeLifecycleState;
41
42
  const fs = __importStar(require("fs"));
42
43
  const path = __importStar(require("path"));
@@ -189,6 +190,34 @@ function markRuntimeLifecycleClean(root, role = 'main') {
189
190
  }
190
191
  }
191
192
  }
193
+ /** Parent-side fallback for a worker that died before it could clean its own marker. */
194
+ function markRuntimeLifecycleExitedByPid(root, role, pid, input = { unexpected: true }) {
195
+ const directory = path.join(root, '.newmark-runtime');
196
+ let files = [];
197
+ try {
198
+ files = fs.readdirSync(directory).filter(file => file.startsWith(`lifecycle-${role}-`) && file.endsWith('.json'));
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ for (const name of files) {
204
+ const file = path.join(directory, name);
205
+ const state = readState(file);
206
+ if (!state || Number(state.pid) !== Math.floor(Number(pid) || 0) || state.active !== true)
207
+ continue;
208
+ try {
209
+ writeState(root, role, {
210
+ ...state,
211
+ active: false,
212
+ unexpectedExit: input.unexpected,
213
+ exitedAt: new Date().toISOString(),
214
+ exitCode: input.exitCode,
215
+ error: String(input.error || '').slice(-800),
216
+ });
217
+ }
218
+ catch { }
219
+ }
220
+ }
192
221
  function runtimeLifecycleState(root, role = 'main') {
193
222
  return processStates.get(stateKey(root, role)) || null;
194
223
  }
@@ -0,0 +1,80 @@
1
+ export type SearchMcpTransport = 'stdio' | 'streamable_http' | 'sse' | 'template';
2
+ export interface SearchMcpEndpoint {
3
+ id: string;
4
+ name: string;
5
+ enabled: boolean;
6
+ priority: number;
7
+ transport: SearchMcpTransport;
8
+ command?: string;
9
+ args?: string[];
10
+ cwd?: string;
11
+ env?: Record<string, string>;
12
+ url?: string;
13
+ headers?: Record<string, string>;
14
+ tool?: string;
15
+ argument?: string;
16
+ timeoutMs?: number;
17
+ notes?: string;
18
+ }
19
+ export interface SearchMcpPoolConfig {
20
+ version: 1;
21
+ endpoints: SearchMcpEndpoint[];
22
+ }
23
+ export interface PublicSearchMcpEndpoint extends Omit<SearchMcpEndpoint, 'env' | 'headers' | 'command' | 'args' | 'cwd'> {
24
+ envKeys: string[];
25
+ headerKeys: string[];
26
+ url?: string;
27
+ }
28
+ export interface SearchMcpAttempt {
29
+ id: string;
30
+ name: string;
31
+ status: 'success' | 'empty' | 'error' | 'unconfigured';
32
+ durationMs: number;
33
+ error?: string;
34
+ }
35
+ export interface SearchMcpPoolResult {
36
+ invocationId: string;
37
+ checkedAt: string;
38
+ ok: boolean;
39
+ provider?: string;
40
+ text?: string;
41
+ attempts: SearchMcpAttempt[];
42
+ }
43
+ export interface SearchMcpPoolDependencies {
44
+ callEndpoint?: (endpoint: SearchMcpEndpoint, query: string, signal?: AbortSignal) => Promise<string>;
45
+ now?: () => number;
46
+ }
47
+ export declare const MAX_SEARCH_MCP_RESULT_CHARS = 60000;
48
+ /**
49
+ * Public protocol catalog. Only endpoints with evidence-backed launch metadata are runnable by default.
50
+ * The remaining requested implementations are exposed as disabled templates until a distributor/user
51
+ * supplies a concrete stdio command or Streamable HTTP/SSE URL in search-mcp.json.
52
+ */
53
+ export declare const DEFAULT_SEARCH_MCP_MANIFEST: SearchMcpEndpoint[];
54
+ export declare function loadSearchMcpManifest(root: string): SearchMcpEndpoint[];
55
+ export declare function publicSearchMcpManifest(root: string): PublicSearchMcpEndpoint[];
56
+ export declare function redactSearchMcpLocalPaths(value: string, additionalRoots?: string[]): string;
57
+ export declare function chooseSearchTool(tools: Array<{
58
+ name: string;
59
+ description?: string;
60
+ inputSchema?: {
61
+ type?: string;
62
+ properties?: Record<string, {
63
+ type?: string;
64
+ }>;
65
+ required?: string[];
66
+ additionalProperties?: boolean;
67
+ };
68
+ }>, preferred?: string): {
69
+ name: string;
70
+ argument: string;
71
+ } | undefined;
72
+ export declare class SearchMcpPool {
73
+ private readonly root;
74
+ private readonly callEndpoint;
75
+ private readonly now;
76
+ constructor(root: string, dependencies?: SearchMcpPoolDependencies);
77
+ manifest(): SearchMcpEndpoint[];
78
+ search(query: string, signal?: AbortSignal): Promise<SearchMcpPoolResult>;
79
+ }
80
+ //# sourceMappingURL=searchMcpPool.d.ts.map
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SearchMcpPool = exports.DEFAULT_SEARCH_MCP_MANIFEST = exports.MAX_SEARCH_MCP_RESULT_CHARS = void 0;
37
+ exports.loadSearchMcpManifest = loadSearchMcpManifest;
38
+ exports.publicSearchMcpManifest = publicSearchMcpManifest;
39
+ exports.redactSearchMcpLocalPaths = redactSearchMcpLocalPaths;
40
+ exports.chooseSearchTool = chooseSearchTool;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const crypto_1 = require("crypto");
44
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
45
+ const sse_js_1 = require("@modelcontextprotocol/sdk/client/sse.js");
46
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/client/stdio.js");
47
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
48
+ const DEFAULT_TIMEOUT_MS = 8_000;
49
+ const MAX_TIMEOUT_MS = 30_000;
50
+ exports.MAX_SEARCH_MCP_RESULT_CHARS = 60_000;
51
+ const SEARCH_MCP_HEALTH_FILE = 'search-mcp-health.json';
52
+ const SEARCH_TOOL_NAME = /^(?:web[_-]?search|search(?:_web)?|internet[_-]?search|duckduckgo(?:[_-](?:web[_-]?)?search)?)$/i;
53
+ const SEARCH_TOOL_DESCRIPTION = /\b(?:web|internet|duckduckgo|searxng)\b[\s\S]{0,80}\bsearch\b|\bsearch\b[\s\S]{0,80}\b(?:web|internet)\b/i;
54
+ const SEARCH_QUERY_ARGUMENTS = ['query', 'q', 'search_query', 'searchQuery', 'keywords', 'keyword', 'text'];
55
+ const SAFE_SEARCH_ARGUMENTS = new Set([
56
+ ...SEARCH_QUERY_ARGUMENTS,
57
+ 'count', 'limit', 'max_results', 'maxResults', 'page', 'offset', 'rankingMode',
58
+ 'category', 'categories', 'language', 'locale', 'region', 'country', 'freshness',
59
+ 'time_range', 'timeRange', 'safe_search', 'safesearch',
60
+ ]);
61
+ const DANGEROUS_SEARCH_ARGUMENT = /(?:^|_)(?:command|cmd|shell|path|file|write|delete|execute|script|code|cwd|env|headers|body|method|tool|action)(?:$|_)/i;
62
+ const EMPTY_SEARCH_TEXT = /^(?:\s*(?:\[(?:web[_-]?)?search\]\s*)?)(?:no\s+(?:search\s+)?results?(?:\s+(?:were\s+)?found)?(?=\s*(?:$|[.!。::,;]|(?:for|matching|returned|available)\b))|0\s+(?:search\s+)?results?(?=\s*(?:$|[.!。::,;]|(?:for|found|returned|matching)\b))|nothing\s+(?:was\s+)?found(?=\s*(?:$|[.!。::,;]|(?:for|matching)\b))|(?:未找到(?:任何)?(?:搜索)?结果|无(?:可用)?搜索结果)(?=\s*(?:$|[。!!,,::;;])))/i;
63
+ /**
64
+ * Public protocol catalog. Only endpoints with evidence-backed launch metadata are runnable by default.
65
+ * The remaining requested implementations are exposed as disabled templates until a distributor/user
66
+ * supplies a concrete stdio command or Streamable HTTP/SSE URL in search-mcp.json.
67
+ */
68
+ exports.DEFAULT_SEARCH_MCP_MANIFEST = [
69
+ {
70
+ id: 'wuxing-search-mcp', name: 'Wuxing Search MCP', enabled: false, priority: 100,
71
+ transport: 'stdio', command: process.execPath,
72
+ args: [path.join(__dirname, '../../node_modules/@iflow-mcp/maeshughes-wuxing-search-mcp/src/index.js')],
73
+ env: { ELECTRON_RUN_AS_NODE: '1', SEARXNG_URL: 'http://127.0.0.1:18080', MAX_RESULTS: '12', TIMEOUT: '7000' },
74
+ tool: 'web_search', argument: 'query', timeoutMs: 8_000,
75
+ notes: 'Bundled MCP adapter; requires a reachable SearXNG backend. Priority is configurable and is not privileged.',
76
+ },
77
+ { id: 'web-search-api', name: 'web-search-api', enabled: false, priority: 200, transport: 'template', notes: 'Set a verified stdio command or HTTP/SSE URL before enabling.' },
78
+ { id: 'miyami-websearch-mcp', name: 'miyami-websearch-mcp', enabled: false, priority: 300, transport: 'template', notes: 'Protocol template; no runtime is downloaded implicitly.' },
79
+ { id: 'searxng-mcp', name: 'searxng-mcp', enabled: false, priority: 400, transport: 'template', notes: 'Protocol template; configure a trusted SearXNG MCP deployment.' },
80
+ { id: 'mcp-server-freesearch', name: 'MCP Server FreeSearch', enabled: false, priority: 500, transport: 'template', notes: 'Protocol template; concrete implementation is distributor-selected.' },
81
+ {
82
+ id: 'ignidor-web-search-mcp', name: '@ignidor/web-search-mcp', enabled: true, priority: 600,
83
+ transport: 'stdio', command: process.execPath,
84
+ args: [path.join(__dirname, '../../node_modules/@ignidor/web-search-mcp/dist/index.js')],
85
+ env: { ELECTRON_RUN_AS_NODE: '1' },
86
+ tool: 'search', argument: 'query', timeoutMs: 12_000,
87
+ notes: 'Bundled after initialize/tools-list/tools-call verification; search-only invocation does not expose crawler or browser tools.',
88
+ },
89
+ { id: 'free-search-mcp', name: 'free-search-mcp', enabled: false, priority: 700, transport: 'template', notes: 'Protocol template; configure the intended package or remote service explicitly.' },
90
+ { id: 'duckduckgo-mcp', name: 'DuckDuckGo MCP', enabled: false, priority: 800, transport: 'template', notes: 'MCP implementation is configurable; HTTP DuckDuckGo remains the absolute final fallback.' },
91
+ { id: 'free-mcp-web-search-server', name: 'Free MCP Web Search Server', enabled: false, priority: 900, transport: 'template', notes: 'Protocol template; configure a verified endpoint before enabling.' },
92
+ ];
93
+ function stringRecord(value) {
94
+ if (!value || typeof value !== 'object' || Array.isArray(value))
95
+ return undefined;
96
+ const output = {};
97
+ for (const [key, item] of Object.entries(value)) {
98
+ const clean = String(key || '').trim();
99
+ if (!clean || /[\r\n\0]/.test(clean) || ['__proto__', 'prototype', 'constructor'].includes(clean))
100
+ continue;
101
+ const text = String(item ?? '');
102
+ if (/\0/.test(text) || /[\r\n]/.test(text) && /authorization|header/i.test(clean))
103
+ continue;
104
+ output[clean] = text;
105
+ }
106
+ return output;
107
+ }
108
+ function stringArray(value) {
109
+ if (!Array.isArray(value) || value.length > 100 || value.some(item => typeof item !== 'string'))
110
+ return undefined;
111
+ return value.map(item => String(item).slice(0, 4_000));
112
+ }
113
+ function safeEndpoint(value) {
114
+ if (!value || typeof value !== 'object' || Array.isArray(value))
115
+ return undefined;
116
+ const item = value;
117
+ const id = String(item.id || '').trim().slice(0, 160);
118
+ const name = String(item.name || '').trim().slice(0, 160);
119
+ const transport = ['stdio', 'streamable_http', 'sse', 'template'].includes(String(item.transport))
120
+ ? String(item.transport) : 'template';
121
+ if (!id || !name)
122
+ return undefined;
123
+ const priority = Number.isFinite(Number(item.priority)) ? Math.trunc(Number(item.priority)) : 1_000;
124
+ const timeoutMs = Math.max(1_000, Math.min(MAX_TIMEOUT_MS, Number(item.timeoutMs) || DEFAULT_TIMEOUT_MS));
125
+ const endpoint = {
126
+ id, name, transport, enabled: item.enabled === true, priority, timeoutMs,
127
+ command: String(item.command || '').trim().slice(0, 2_000) || undefined,
128
+ args: stringArray(item.args), cwd: String(item.cwd || '').trim().slice(0, 4_000) || undefined,
129
+ env: stringRecord(item.env), url: String(item.url || '').trim().slice(0, 4_000) || undefined,
130
+ headers: stringRecord(item.headers), tool: String(item.tool || '').trim().slice(0, 160) || undefined,
131
+ argument: String(item.argument || '').trim().slice(0, 160) || undefined,
132
+ notes: String(item.notes || '').trim().slice(0, 1_000) || undefined,
133
+ };
134
+ if (transport === 'stdio' && !endpoint.command)
135
+ endpoint.transport = 'template';
136
+ if (transport === 'streamable_http' || transport === 'sse') {
137
+ try {
138
+ const parsed = new URL(endpoint.url || '');
139
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password)
140
+ endpoint.transport = 'template';
141
+ }
142
+ catch {
143
+ endpoint.transport = 'template';
144
+ }
145
+ }
146
+ return endpoint;
147
+ }
148
+ function mergeManifest(configured, useDefaults = true) {
149
+ const overrides = Array.isArray(configured) ? configured.map(safeEndpoint).filter((item) => !!item) : [];
150
+ const byId = new Map((useDefaults ? exports.DEFAULT_SEARCH_MCP_MANIFEST : []).map(item => [item.id, { ...item }]));
151
+ for (const override of overrides)
152
+ byId.set(override.id, { ...(byId.get(override.id) || {}), ...override });
153
+ return [...byId.values()].sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
154
+ }
155
+ function loadSearchMcpManifest(root) {
156
+ try {
157
+ const parsed = JSON.parse(fs.readFileSync(path.join(root, 'search-mcp.json'), 'utf8'));
158
+ return mergeManifest(parsed.endpoints, false);
159
+ }
160
+ catch {
161
+ return mergeManifest([], true);
162
+ }
163
+ }
164
+ function publicSearchMcpManifest(root) {
165
+ return loadSearchMcpManifest(root).map(endpoint => {
166
+ const { env, headers, command: _command, args: _args, cwd: _cwd, ...rest } = endpoint;
167
+ let publicUrl = endpoint.url;
168
+ try {
169
+ const parsed = new URL(endpoint.url || '');
170
+ for (const key of Array.from(parsed.searchParams.keys())) {
171
+ if (/(?:api.?key|authorization|access.?token|secret|password|credential)/i.test(key))
172
+ parsed.searchParams.set(key, '<redacted>');
173
+ }
174
+ if (parsed.searchParams.size > 0 && parsed.searchParams.toString().length > 2_000)
175
+ parsed.search = '';
176
+ publicUrl = parsed.toString();
177
+ }
178
+ catch { /* template or stdio endpoint */ }
179
+ return { ...rest, url: publicUrl, envKeys: Object.keys(env || {}).sort(), headerKeys: Object.keys(headers || {}).sort() };
180
+ });
181
+ }
182
+ function replaceLiteral(value, target, replacement) {
183
+ if (!target)
184
+ return value;
185
+ return value.replace(new RegExp(target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'), replacement);
186
+ }
187
+ function redactSearchMcpLocalPaths(value, additionalRoots = []) {
188
+ let output = String(value || '');
189
+ const roots = [
190
+ ...additionalRoots,
191
+ process.cwd(),
192
+ process.env.USERPROFILE,
193
+ process.env.HOME,
194
+ process.env.LOCALAPPDATA,
195
+ process.env.APPDATA,
196
+ process.env.TEMP,
197
+ process.env.TMP,
198
+ process.env.npm_config_cache,
199
+ ].filter((item) => !!item).sort((a, b) => b.length - a.length);
200
+ for (const root of roots) {
201
+ const variants = new Set([
202
+ root,
203
+ root.replace(/\\/g, '/'),
204
+ root.replace(/\//g, '\\'),
205
+ encodeURI(root),
206
+ encodeURI(root.replace(/\\/g, '/')),
207
+ ]);
208
+ for (const variant of variants)
209
+ output = replaceLiteral(output, variant, '<local-path>');
210
+ }
211
+ return output
212
+ .replace(/file:\/\/\/[A-Za-z]:\/[^\s\"'<>|,;\r\n]+/gi, '<local-path>')
213
+ .replace(/(?<![A-Za-z0-9+.-])[A-Za-z]:[\\/][^\s\"'<>|,;\r\n]+/g, '<local-path>')
214
+ .replace(/(?:file:\/\/)?\/(?:home|Users|tmp|var\/tmp)\/[^\s\"'<>|,;\r\n]+/g, '<local-path>');
215
+ }
216
+ function cleanError(error, roots = []) {
217
+ return redactSearchMcpLocalPaths(error instanceof Error ? error.message : String(error || 'unknown error'), roots)
218
+ .replace(/(Authorization\s*:\s*Bearer\s+)[^\s,;]+/ig, '$1[redacted]')
219
+ .replace(/([?&](?:api[_-]?key|access_token|token|key)=)[^&\s]+/ig, '$1[redacted]')
220
+ .slice(0, 300);
221
+ }
222
+ function resultText(result) {
223
+ const content = Array.isArray(result.content) ? result.content : [];
224
+ const chunks = [];
225
+ let length = 0;
226
+ for (const item of content) {
227
+ if (!item || typeof item !== 'object' || item.type !== 'text')
228
+ continue;
229
+ const value = String(item.text || '').trim();
230
+ if (!value)
231
+ continue;
232
+ const separator = chunks.length ? 2 : 0;
233
+ const remaining = exports.MAX_SEARCH_MCP_RESULT_CHARS - length - separator;
234
+ if (remaining <= 0)
235
+ break;
236
+ chunks.push(value.slice(0, remaining));
237
+ length += separator + Math.min(value.length, remaining);
238
+ }
239
+ const text = chunks.join('\n\n').slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS);
240
+ if (text)
241
+ return text;
242
+ const structured = result.structuredContent;
243
+ return structured && typeof structured === 'object'
244
+ ? JSON.stringify(structured, null, 2).slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS)
245
+ : '';
246
+ }
247
+ function looksLikeEmptyOrError(text) {
248
+ const clean = String(text || '').trim();
249
+ if (!clean)
250
+ return true;
251
+ try {
252
+ const parsed = JSON.parse(clean);
253
+ if (parsed.success === false || parsed.ok === false || Boolean(parsed.error))
254
+ return true;
255
+ if (Array.isArray(parsed.results) && parsed.results.length === 0 && !parsed.answer)
256
+ return true;
257
+ }
258
+ catch { /* plain text results are valid */ }
259
+ return EMPTY_SEARCH_TEXT.test(clean);
260
+ }
261
+ function dangerousSearchArgument(key) {
262
+ const normalized = String(key || '')
263
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
264
+ .replace(/[^A-Za-z0-9]+/g, '_')
265
+ .toLowerCase();
266
+ return DANGEROUS_SEARCH_ARGUMENT.test(normalized);
267
+ }
268
+ function chooseSearchTool(tools, preferred) {
269
+ const safeTool = (tool) => {
270
+ const properties = tool.inputSchema?.properties;
271
+ if (!properties || tool.inputSchema?.type && tool.inputSchema.type !== 'object')
272
+ return undefined;
273
+ const propertyNames = Object.keys(properties);
274
+ if (propertyNames.some(key => dangerousSearchArgument(key) || !SAFE_SEARCH_ARGUMENTS.has(key)))
275
+ return undefined;
276
+ const argument = SEARCH_QUERY_ARGUMENTS.find(key => properties[key]?.type === 'string');
277
+ if (!argument)
278
+ return undefined;
279
+ const required = Array.isArray(tool.inputSchema?.required) ? tool.inputSchema.required : [];
280
+ if (required.some(key => key !== argument && !SAFE_SEARCH_ARGUMENTS.has(key)))
281
+ return undefined;
282
+ return { name: tool.name, argument };
283
+ };
284
+ if (preferred) {
285
+ const exact = tools.find(tool => tool.name === preferred);
286
+ if (exact && (SEARCH_TOOL_NAME.test(exact.name) || SEARCH_TOOL_DESCRIPTION.test(String(exact.description || '')))) {
287
+ return safeTool(exact);
288
+ }
289
+ }
290
+ for (const tool of tools) {
291
+ if (!SEARCH_TOOL_NAME.test(tool.name) && !SEARCH_TOOL_DESCRIPTION.test(String(tool.description || '')))
292
+ continue;
293
+ const selected = safeTool(tool);
294
+ if (selected)
295
+ return selected;
296
+ }
297
+ return undefined;
298
+ }
299
+ function writeSearchMcpHealthSnapshot(root, result, query) {
300
+ const target = path.join(root, SEARCH_MCP_HEALTH_FILE);
301
+ const temporary = `${target}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
302
+ const attempts = result.attempts.map(attempt => ({
303
+ ...attempt,
304
+ error: attempt.error ? cleanError(attempt.error, [root]) : undefined,
305
+ }));
306
+ const payload = JSON.stringify({
307
+ version: 1,
308
+ invocationId: result.invocationId,
309
+ checkedAt: result.checkedAt,
310
+ querySha256: (0, crypto_1.createHash)('sha256').update(query, 'utf8').digest('hex'),
311
+ ok: result.ok,
312
+ provider: result.provider || '',
313
+ attempts,
314
+ }, null, 2);
315
+ try {
316
+ fs.mkdirSync(root, { recursive: true });
317
+ fs.writeFileSync(temporary, payload, { encoding: 'utf8', mode: 0o600 });
318
+ fs.renameSync(temporary, target);
319
+ }
320
+ catch {
321
+ try {
322
+ fs.rmSync(temporary, { force: true });
323
+ }
324
+ catch { }
325
+ }
326
+ }
327
+ async function callRealEndpoint(endpoint, query, signal) {
328
+ if (endpoint.transport === 'template')
329
+ throw new Error('endpoint has no configured transport');
330
+ const timeout = Math.max(1_000, Math.min(MAX_TIMEOUT_MS, endpoint.timeoutMs || DEFAULT_TIMEOUT_MS));
331
+ const requestInit = endpoint.headers ? { headers: endpoint.headers } : undefined;
332
+ const transport = endpoint.transport === 'stdio'
333
+ ? new stdio_js_1.StdioClientTransport({
334
+ command: endpoint.command || '', args: endpoint.args || [], cwd: endpoint.cwd,
335
+ env: { ...process.env, ...(endpoint.env || {}) }, stderr: 'pipe',
336
+ })
337
+ : endpoint.transport === 'sse'
338
+ ? new sse_js_1.SSEClientTransport(new URL(endpoint.url || ''), { requestInit })
339
+ : new streamableHttp_js_1.StreamableHTTPClientTransport(new URL(endpoint.url || ''), {
340
+ requestInit,
341
+ reconnectionOptions: { initialReconnectionDelay: 250, maxReconnectionDelay: 500, reconnectionDelayGrowFactor: 1, maxRetries: 0 },
342
+ });
343
+ const client = new index_js_1.Client({ name: 'newmark-search-only', version: '0.5.13' }, { capabilities: {} });
344
+ try {
345
+ // Initialization is part of the endpoint probe and must obey the same
346
+ // bounded timeout as tools/list and tools/call. Without request options a
347
+ // launched-but-unresponsive MCP can hold the complete web_search fallback
348
+ // chain at Client.connect() until the SDK's much longer default timeout.
349
+ await client.connect(transport, { signal, timeout, maxTotalTimeout: timeout });
350
+ const listed = await client.listTools(undefined, { signal, timeout, maxTotalTimeout: timeout });
351
+ const selected = chooseSearchTool(listed.tools, endpoint.tool);
352
+ if (!selected)
353
+ throw new Error('server exposes no recognized search-only tool');
354
+ const argument = endpoint.argument || selected.argument;
355
+ if (!SEARCH_QUERY_ARGUMENTS.includes(argument))
356
+ throw new Error('configured MCP query argument is outside the search-only boundary');
357
+ const selectedTool = listed.tools.find(tool => tool.name === selected.name);
358
+ if (!selectedTool || chooseSearchTool([selectedTool], selected.name)?.argument !== argument) {
359
+ throw new Error('configured MCP query argument does not match a declared string search field');
360
+ }
361
+ const result = await client.callTool({ name: selected.name, arguments: { [argument]: query } }, undefined, {
362
+ signal, timeout, maxTotalTimeout: timeout,
363
+ });
364
+ if (result.isError === true)
365
+ throw new Error(resultText(result) || 'MCP search returned isError');
366
+ return resultText(result);
367
+ }
368
+ finally {
369
+ await client.close().catch(() => undefined);
370
+ }
371
+ }
372
+ class SearchMcpPool {
373
+ root;
374
+ callEndpoint;
375
+ now;
376
+ constructor(root, dependencies = {}) {
377
+ this.root = root;
378
+ this.callEndpoint = dependencies.callEndpoint || callRealEndpoint;
379
+ this.now = dependencies.now || Date.now;
380
+ }
381
+ manifest() {
382
+ return loadSearchMcpManifest(this.root).map(endpoint => ({ ...endpoint }));
383
+ }
384
+ async search(query, signal) {
385
+ const invocationId = `search-${process.pid}-${this.now()}-${Math.random().toString(16).slice(2, 10)}`;
386
+ const checkedAt = new Date(this.now()).toISOString();
387
+ const attempts = [];
388
+ const successes = [];
389
+ for (const endpoint of this.manifest().filter(item => item.enabled)) {
390
+ if (signal?.aborted)
391
+ throw signal.reason instanceof Error ? signal.reason : new Error('Agent run aborted');
392
+ const started = this.now();
393
+ if (endpoint.transport === 'template') {
394
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: 'unconfigured', durationMs: Math.max(0, this.now() - started) });
395
+ continue;
396
+ }
397
+ try {
398
+ const text = String(await this.callEndpoint(endpoint, query, signal) || '').trim().slice(0, exports.MAX_SEARCH_MCP_RESULT_CHARS);
399
+ const empty = looksLikeEmptyOrError(text);
400
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: empty ? 'empty' : 'success', durationMs: Math.max(0, this.now() - started) });
401
+ if (!empty)
402
+ successes.push({ endpoint, text });
403
+ }
404
+ catch (error) {
405
+ if (signal?.aborted)
406
+ throw signal.reason instanceof Error ? signal.reason : error;
407
+ attempts.push({ id: endpoint.id, name: endpoint.name, status: 'error', durationMs: Math.max(0, this.now() - started), error: cleanError(error, [this.root, endpoint.cwd || '']) });
408
+ }
409
+ }
410
+ const selected = successes.sort((a, b) => a.endpoint.priority - b.endpoint.priority || a.endpoint.id.localeCompare(b.endpoint.id))[0];
411
+ const result = selected
412
+ ? { invocationId, checkedAt, ok: true, provider: selected.endpoint.name, text: selected.text, attempts }
413
+ : { invocationId, checkedAt, ok: false, attempts };
414
+ writeSearchMcpHealthSnapshot(this.root, result, query);
415
+ return result;
416
+ }
417
+ }
418
+ exports.SearchMcpPool = SearchMcpPool;
419
+ //# sourceMappingURL=searchMcpPool.js.map
package/dist/main.js CHANGED
@@ -129,6 +129,30 @@ const mobileServerWorkEventSubscribers = new Set();
129
129
  const browserGuestContentsByHost = new Map();
130
130
  const browserGuestBindingsByRuntime = new Map();
131
131
  const browserGuestKeyboardBridgeIds = new Set();
132
+ // visible=false Browser-Use sessions are never attached to a BrowserWindow or
133
+ // renderer DOM. Each runtime gets one main-process-owned WebContentsView whose
134
+ // WebContents preserves observe/action continuity until the run is cleared.
135
+ const backgroundBrowserViewsByRuntime = new Map();
136
+ function backgroundBrowserPartition(runtimeKey) {
137
+ const digest = (0, crypto_1.createHash)('sha256').update(String(runtimeKey || 'default')).digest('hex').slice(0, 24);
138
+ return `newmark-browser-use-background-${digest}`;
139
+ }
140
+ function releaseBackgroundBrowserWebContents(runtimeKey, expectedContentsId) {
141
+ const trustedRuntimeKey = String(runtimeKey || '').trim();
142
+ if (!trustedRuntimeKey)
143
+ return;
144
+ const view = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey);
145
+ if (!view || (expectedContentsId && view.webContents.id !== expectedContentsId))
146
+ return;
147
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
148
+ const contents = view.webContents;
149
+ if (!contents.isDestroyed())
150
+ contents.close({ waitForBeforeUnload: false });
151
+ }
152
+ function releaseAllBackgroundBrowserWebContents() {
153
+ for (const runtimeKey of [...backgroundBrowserViewsByRuntime.keys()])
154
+ releaseBackgroundBrowserWebContents(runtimeKey);
155
+ }
132
156
  function browserGuestRuntimeKey(target) {
133
157
  return (0, conversationTarget_1.normalizeConversationTarget)(target).runtimeKey;
134
158
  }
@@ -296,6 +320,7 @@ function dispatchAgentWorkEvent(event, mirrorToMobile = true) {
296
320
  if ((workEvent.type === 'done' || workEvent.type === 'error') && workEvent.runtimeKey) {
297
321
  browserUseEngine?.clearRuntime(workEvent.runtimeKey);
298
322
  electronBrowserUseHost?.clear({ runtimeKey: workEvent.runtimeKey, owner: '' });
323
+ releaseBackgroundBrowserWebContents(workEvent.runtimeKey);
299
324
  }
300
325
  for (const win of electron_1.BrowserWindow.getAllWindows()) {
301
326
  if (win.isDestroyed())
@@ -952,10 +977,54 @@ async function ensureBrowserWebContents(boundContentsId, runtimeKey) {
952
977
  host.send('browser:ensureGuest', runtimeKey ? { runtimeKey } : undefined);
953
978
  return await waitForRegisteredBrowserGuest(host, runtimeKey, 12_000);
954
979
  }
980
+ async function ensureBackgroundBrowserWebContents(boundContentsId, runtimeKey) {
981
+ const trustedRuntimeKey = String(runtimeKey || '').trim();
982
+ if (!trustedRuntimeKey)
983
+ throw new Error('Background Browser-Use requires a runtimeKey');
984
+ if (boundContentsId) {
985
+ const bound = electron_1.webContents.fromId(boundContentsId);
986
+ const registered = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey)?.webContents;
987
+ if (bound && registered && !bound.isDestroyed() && registered.id === bound.id)
988
+ return bound;
989
+ }
990
+ const existing = backgroundBrowserViewsByRuntime.get(trustedRuntimeKey);
991
+ if (existing && !existing.webContents.isDestroyed())
992
+ return existing.webContents;
993
+ if (existing)
994
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
995
+ const view = new electron_1.WebContentsView({
996
+ webPreferences: {
997
+ partition: backgroundBrowserPartition(trustedRuntimeKey),
998
+ contextIsolation: true,
999
+ nodeIntegration: false,
1000
+ sandbox: true,
1001
+ javascript: true,
1002
+ allowRunningInsecureContent: false,
1003
+ backgroundThrottling: false,
1004
+ },
1005
+ });
1006
+ const contents = view.webContents;
1007
+ backgroundBrowserViewsByRuntime.set(trustedRuntimeKey, view);
1008
+ contents.on('will-prevent-unload', event => event.preventDefault());
1009
+ contents.once('destroyed', () => {
1010
+ if (backgroundBrowserViewsByRuntime.get(trustedRuntimeKey)?.webContents.id === contents.id) {
1011
+ backgroundBrowserViewsByRuntime.delete(trustedRuntimeKey);
1012
+ }
1013
+ });
1014
+ ensureElectronBrowserUseHost().attach(contents);
1015
+ await contents.loadURL('about:blank');
1016
+ return contents;
1017
+ }
955
1018
  function ensureElectronBrowserUseHost() {
956
1019
  if (!electronBrowserUseHost) {
957
1020
  electronBrowserUseHost = new electronBrowserUseHost_1.ElectronBrowserUseHost({
958
- resolveContents: async (scope, boundContentsId) => await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
1021
+ resolveContents: async (scope, boundContentsId) => scope.visible === false
1022
+ ? await ensureBackgroundBrowserWebContents(boundContentsId, scope.runtimeKey)
1023
+ : await ensureBrowserWebContents(boundContentsId, scope.runtimeKey),
1024
+ releaseContents: (scope, contents) => {
1025
+ if (scope.visible === false)
1026
+ releaseBackgroundBrowserWebContents(scope.runtimeKey, contents.id);
1027
+ },
959
1028
  openExternal: async (url) => { await electron_1.shell.openExternal(url); },
960
1029
  });
961
1030
  }
@@ -2660,6 +2729,7 @@ else {
2660
2729
  browserUseEngine = null;
2661
2730
  electronBrowserUseHost?.dispose();
2662
2731
  electronBrowserUseHost = null;
2732
+ releaseAllBackgroundBrowserWebContents();
2663
2733
  browserControl_1.BrowserControl.setBackend(null);
2664
2734
  browserGuestContentsByHost.clear();
2665
2735
  browserGuestBindingsByRuntime.clear();
@@ -2787,6 +2857,7 @@ else {
2787
2857
  cancelBrowserUseTarget: runtimeKey => {
2788
2858
  browserUseEngine?.clearRuntime(runtimeKey);
2789
2859
  electronBrowserUseHost?.clear({ runtimeKey, owner: '' });
2860
+ releaseBackgroundBrowserWebContents(runtimeKey);
2790
2861
  },
2791
2862
  runAutomation: async (tool, payload, signal) => {
2792
2863
  if (!agent)