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.
- package/dist/conversation-utility-host.bundle.cjs +75356 -26589
- package/dist/core/agent.d.ts +43 -18
- package/dist/core/agent.js +321 -69
- package/dist/core/browserUse.d.ts +7 -0
- package/dist/core/browserUse.js +24 -7
- package/dist/core/conversationKernel.d.ts +4 -0
- package/dist/core/conversationKernel.js +65 -3
- package/dist/core/displayImages.d.ts +10 -0
- package/dist/core/displayImages.js +31 -8
- package/dist/core/electronBrowserUseHost.d.ts +4 -0
- package/dist/core/electronBrowserUseHost.js +32 -6
- package/dist/core/electronUtilityAgentClient.d.ts +1 -0
- package/dist/core/electronUtilityAgentClient.js +70 -2
- package/dist/core/runtimeDiagnostics.d.ts +18 -0
- package/dist/core/runtimeDiagnostics.js +89 -0
- package/dist/core/runtimeLifecycle.d.ts +7 -1
- package/dist/core/runtimeLifecycle.js +29 -0
- package/dist/core/searchMcpPool.d.ts +80 -0
- package/dist/core/searchMcpPool.js +419 -0
- package/dist/main.js +72 -1
- package/dist/server.js +35 -0
- package/dist/tools/index.d.ts +17 -1
- package/dist/tools/index.js +203 -64
- package/dist/tui/src/data.js +2 -2
- package/dist/ui/index.html +208 -26
- package/dist/wsl-agent-host.bundle.cjs +75333 -26566
- package/package.json +11 -2
|
@@ -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) =>
|
|
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)
|