droid-patch 0.10.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -8
- package/README.zh-CN.md +80 -5
- package/dist/{alias-C2Iew8yJ.mjs → alias-CX4QSelz.mjs} +4 -3
- package/dist/alias-CX4QSelz.mjs.map +1 -0
- package/dist/cli.mjs +453 -451
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/alias-C2Iew8yJ.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-
|
|
2
|
+
import { a as removeAlias, d as listAllMetadata, f as loadAliasMetadata, i as listAliases, l as createMetadata, m as patchDroid, n as createAlias, o as removeAliasesByFilter, p as saveAliasMetadata, r as createAliasForWrapper, t as clearAllAliases, u as formatPatches } from "./alias-CX4QSelz.mjs";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -9,21 +9,16 @@ import { fileURLToPath } from "node:url";
|
|
|
9
9
|
import { execSync } from "node:child_process";
|
|
10
10
|
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
11
11
|
|
|
12
|
-
//#region src/websearch-
|
|
12
|
+
//#region src/websearch-external.ts
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
* Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,
|
|
16
|
-
* use a local proxy server to intercept search requests instead
|
|
14
|
+
* WebSearch External Providers Mode (--websearch)
|
|
17
15
|
*
|
|
18
|
-
*
|
|
19
|
-
* The proxy is killed automatically when droid exits.
|
|
20
|
-
* @param factoryApiUrl - Custom Factory API URL (default: https://api.factory.ai)
|
|
16
|
+
* Priority: Smithery Exa > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
21
17
|
*/
|
|
22
|
-
function
|
|
18
|
+
function generateSearchProxyServerCode() {
|
|
23
19
|
return `#!/usr/bin/env node
|
|
24
|
-
// Droid WebSearch Proxy Server
|
|
25
|
-
//
|
|
26
|
-
// This proxy runs as a child process of droid and is killed when droid exits
|
|
20
|
+
// Droid WebSearch Proxy Server (External Providers Mode)
|
|
21
|
+
// Priority: Smithery Exa > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
27
22
|
|
|
28
23
|
const http = require('http');
|
|
29
24
|
const https = require('https');
|
|
@@ -31,75 +26,38 @@ const { execSync } = require('child_process');
|
|
|
31
26
|
const fs = require('fs');
|
|
32
27
|
|
|
33
28
|
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
34
|
-
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
35
|
-
const FACTORY_API = '
|
|
29
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
30
|
+
const FACTORY_API = 'https://api.factory.ai';
|
|
36
31
|
|
|
37
|
-
function log(
|
|
38
|
-
if (DEBUG) console.error('[websearch]', ...args);
|
|
39
|
-
}
|
|
32
|
+
function log() { if (DEBUG) console.error.apply(console, ['[websearch]'].concat(Array.from(arguments))); }
|
|
40
33
|
|
|
41
|
-
// === Search
|
|
34
|
+
// === External Search Providers ===
|
|
42
35
|
|
|
43
|
-
// Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE
|
|
44
36
|
async function searchSmitheryExa(query, numResults) {
|
|
45
37
|
const apiKey = process.env.SMITHERY_API_KEY;
|
|
46
38
|
const profile = process.env.SMITHERY_PROFILE;
|
|
47
39
|
if (!apiKey || !profile) return null;
|
|
48
|
-
|
|
49
40
|
try {
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const requestBody = JSON.stringify({
|
|
56
|
-
jsonrpc: '2.0',
|
|
57
|
-
id: 1,
|
|
58
|
-
method: 'tools/call',
|
|
59
|
-
params: {
|
|
60
|
-
name: 'web_search_exa',
|
|
61
|
-
arguments: {
|
|
62
|
-
query: query,
|
|
63
|
-
numResults: numResults
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
const curlCmd = \`curl -s -X POST "\${serverUrl}" -H "Content-Type: application/json" -d '\${requestBody.replace(/'/g, "'\\\\\\\\''")}'\`;
|
|
69
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });
|
|
70
|
-
const response = JSON.parse(jsonStr);
|
|
71
|
-
|
|
72
|
-
// Parse MCP response
|
|
41
|
+
const serverUrl = 'https://server.smithery.ai/exa/mcp?api_key=' + encodeURIComponent(apiKey) + '&profile=' + encodeURIComponent(profile);
|
|
42
|
+
const requestBody = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'web_search_exa', arguments: { query: query, numResults: numResults } } });
|
|
43
|
+
const bodyStr = requestBody.replace(/'/g, "'\\\\''");
|
|
44
|
+
const curlCmd = 'curl -s -X POST "' + serverUrl + '" -H "Content-Type: application/json" -d \\'' + bodyStr + "\\'";
|
|
45
|
+
const response = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 }));
|
|
73
46
|
if (response.result && response.result.content) {
|
|
74
|
-
|
|
75
|
-
const textContent = response.result.content.find(c => c.type === 'text');
|
|
47
|
+
const textContent = response.result.content.find(function(c) { return c.type === 'text'; });
|
|
76
48
|
if (textContent && textContent.text) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return
|
|
81
|
-
title: item.title || '',
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
author: item.author || null,
|
|
86
|
-
score: item.score || null
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
} catch (parseErr) {
|
|
90
|
-
log('Smithery response parsing failed');
|
|
49
|
+
const results = JSON.parse(textContent.text);
|
|
50
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
51
|
+
return results.slice(0, numResults).map(function(item) {
|
|
52
|
+
return {
|
|
53
|
+
title: item.title || '', url: item.url || '',
|
|
54
|
+
content: item.text || item.snippet || (item.highlights ? item.highlights.join(' ') : '') || ''
|
|
55
|
+
};
|
|
56
|
+
});
|
|
91
57
|
}
|
|
92
58
|
}
|
|
93
59
|
}
|
|
94
|
-
|
|
95
|
-
if (response.error) {
|
|
96
|
-
log('Smithery Exa error:', response.error.message || response.error);
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
} catch (e) {
|
|
100
|
-
log('Smithery Exa failed:', e.message);
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
60
|
+
} catch (e) { log('Smithery failed:', e.message); }
|
|
103
61
|
return null;
|
|
104
62
|
}
|
|
105
63
|
|
|
@@ -107,305 +65,392 @@ async function searchGooglePSE(query, numResults) {
|
|
|
107
65
|
const apiKey = process.env.GOOGLE_PSE_API_KEY;
|
|
108
66
|
const cx = process.env.GOOGLE_PSE_CX;
|
|
109
67
|
if (!apiKey || !cx) return null;
|
|
110
|
-
|
|
111
68
|
try {
|
|
112
|
-
const url =
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const data = JSON.parse(jsonStr);
|
|
118
|
-
|
|
119
|
-
if (data.error) {
|
|
120
|
-
log('Google PSE error:', data.error.message);
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
return (data.items || []).map(item => ({
|
|
124
|
-
title: item.title,
|
|
125
|
-
url: item.link,
|
|
126
|
-
content: item.snippet || '',
|
|
127
|
-
publishedDate: null,
|
|
128
|
-
author: null,
|
|
129
|
-
score: null
|
|
130
|
-
}));
|
|
131
|
-
} catch (e) {
|
|
132
|
-
log('Google PSE failed:', e.message);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// SearXNG - self-hosted meta search engine
|
|
138
|
-
async function searchSearXNG(query, numResults) {
|
|
139
|
-
const searxngUrl = process.env.SEARXNG_URL;
|
|
140
|
-
if (!searxngUrl) return null;
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const url = \`\${searxngUrl}/search?q=\${encodeURIComponent(query)}&format=json&engines=google,bing,duckduckgo\`;
|
|
144
|
-
log('SearXNG request:', url);
|
|
145
|
-
|
|
146
|
-
const curlCmd = \`curl -s "\${url}" -H "Accept: application/json"\`;
|
|
147
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
148
|
-
const data = JSON.parse(jsonStr);
|
|
149
|
-
|
|
150
|
-
if (data.results && data.results.length > 0) {
|
|
151
|
-
return data.results.slice(0, numResults).map(item => ({
|
|
152
|
-
title: item.title,
|
|
153
|
-
url: item.url,
|
|
154
|
-
content: item.content || '',
|
|
155
|
-
publishedDate: null,
|
|
156
|
-
author: null,
|
|
157
|
-
score: null
|
|
158
|
-
}));
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
log('SearXNG failed:', e.message);
|
|
162
|
-
}
|
|
69
|
+
const url = 'https://www.googleapis.com/customsearch/v1?key=' + apiKey + '&cx=' + cx + '&q=' + encodeURIComponent(query) + '&num=' + Math.min(numResults, 10);
|
|
70
|
+
const data = JSON.parse(execSync('curl -s "' + url + '"', { encoding: 'utf-8', timeout: 15000 }));
|
|
71
|
+
if (data.error) return null;
|
|
72
|
+
return (data.items || []).map(function(item) { return { title: item.title, url: item.link, content: item.snippet || '' }; });
|
|
73
|
+
} catch (e) { log('Google PSE failed:', e.message); }
|
|
163
74
|
return null;
|
|
164
75
|
}
|
|
165
76
|
|
|
166
|
-
// Serper API - free tier available (2500 queries/month)
|
|
167
77
|
async function searchSerper(query, numResults) {
|
|
168
78
|
const apiKey = process.env.SERPER_API_KEY;
|
|
169
79
|
if (!apiKey) return null;
|
|
170
|
-
|
|
171
80
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
176
|
-
const data = JSON.parse(jsonStr);
|
|
177
|
-
|
|
81
|
+
const bodyStr = JSON.stringify({ q: query, num: numResults }).replace(/'/g, "'\\\\''");
|
|
82
|
+
const curlCmd = 'curl -s "https://google.serper.dev/search" -H "X-API-KEY: ' + apiKey + '" -H "Content-Type: application/json" -d \\'' + bodyStr + "\\'";
|
|
83
|
+
const data = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 }));
|
|
178
84
|
if (data.organic && data.organic.length > 0) {
|
|
179
|
-
return data.organic.slice(0, numResults).map(item
|
|
180
|
-
title: item.title,
|
|
181
|
-
url: item.link,
|
|
182
|
-
content: item.snippet || '',
|
|
183
|
-
publishedDate: null,
|
|
184
|
-
author: null,
|
|
185
|
-
score: null
|
|
186
|
-
}));
|
|
85
|
+
return data.organic.slice(0, numResults).map(function(item) { return { title: item.title, url: item.link, content: item.snippet || '' }; });
|
|
187
86
|
}
|
|
188
|
-
} catch (e) {
|
|
189
|
-
log('Serper failed:', e.message);
|
|
190
|
-
}
|
|
87
|
+
} catch (e) { log('Serper failed:', e.message); }
|
|
191
88
|
return null;
|
|
192
89
|
}
|
|
193
90
|
|
|
194
|
-
// Brave Search API - free tier available
|
|
195
91
|
async function searchBrave(query, numResults) {
|
|
196
92
|
const apiKey = process.env.BRAVE_API_KEY;
|
|
197
93
|
if (!apiKey) return null;
|
|
198
|
-
|
|
199
94
|
try {
|
|
200
|
-
const url =
|
|
201
|
-
const curlCmd =
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
205
|
-
const data = JSON.parse(jsonStr);
|
|
206
|
-
|
|
95
|
+
const url = 'https://api.search.brave.com/res/v1/web/search?q=' + encodeURIComponent(query) + '&count=' + numResults;
|
|
96
|
+
const curlCmd = 'curl -s "' + url + '" -H "Accept: application/json" -H "X-Subscription-Token: ' + apiKey + '"';
|
|
97
|
+
const data = JSON.parse(execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 }));
|
|
207
98
|
if (data.web && data.web.results && data.web.results.length > 0) {
|
|
208
|
-
return data.web.results.slice(0, numResults).map(item
|
|
209
|
-
title: item.title,
|
|
210
|
-
url: item.url,
|
|
211
|
-
content: item.description || '',
|
|
212
|
-
publishedDate: null,
|
|
213
|
-
author: null,
|
|
214
|
-
score: null
|
|
215
|
-
}));
|
|
99
|
+
return data.web.results.slice(0, numResults).map(function(item) { return { title: item.title, url: item.url, content: item.description || '' }; });
|
|
216
100
|
}
|
|
217
|
-
} catch (e) {
|
|
218
|
-
log('Brave failed:', e.message);
|
|
219
|
-
}
|
|
101
|
+
} catch (e) { log('Brave failed:', e.message); }
|
|
220
102
|
return null;
|
|
221
103
|
}
|
|
222
104
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
105
|
+
async function searchSearXNG(query, numResults) {
|
|
106
|
+
const searxngUrl = process.env.SEARXNG_URL;
|
|
107
|
+
if (!searxngUrl) return null;
|
|
226
108
|
try {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
109
|
+
const url = searxngUrl + '/search?q=' + encodeURIComponent(query) + '&format=json&engines=google,bing,duckduckgo';
|
|
110
|
+
const data = JSON.parse(execSync('curl -s "' + url + '" -H "Accept: application/json"', { encoding: 'utf-8', timeout: 15000 }));
|
|
111
|
+
if (data.results && data.results.length > 0) {
|
|
112
|
+
return data.results.slice(0, numResults).map(function(item) { return { title: item.title, url: item.url, content: item.content || '' }; });
|
|
113
|
+
}
|
|
114
|
+
} catch (e) { log('SearXNG failed:', e.message); }
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
231
117
|
|
|
118
|
+
async function searchDuckDuckGo(query, numResults) {
|
|
119
|
+
try {
|
|
120
|
+
const apiUrl = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1';
|
|
121
|
+
const data = JSON.parse(execSync('curl -s "' + apiUrl + '" -H "User-Agent: Mozilla/5.0"', { encoding: 'utf-8', timeout: 15000 }));
|
|
232
122
|
const results = [];
|
|
233
|
-
|
|
234
123
|
if (data.Abstract && data.AbstractURL) {
|
|
235
|
-
results.push({
|
|
236
|
-
title: data.Heading || query,
|
|
237
|
-
url: data.AbstractURL,
|
|
238
|
-
content: data.Abstract,
|
|
239
|
-
publishedDate: null,
|
|
240
|
-
author: null,
|
|
241
|
-
score: null
|
|
242
|
-
});
|
|
124
|
+
results.push({ title: data.Heading || query, url: data.AbstractURL, content: data.Abstract });
|
|
243
125
|
}
|
|
244
|
-
|
|
245
|
-
for (
|
|
246
|
-
|
|
247
|
-
if (topic.Text && topic.FirstURL) {
|
|
248
|
-
results.push({
|
|
249
|
-
title: topic.Text.substring(0, 100),
|
|
250
|
-
url: topic.FirstURL,
|
|
251
|
-
content: topic.Text,
|
|
252
|
-
publishedDate: null,
|
|
253
|
-
author: null,
|
|
254
|
-
score: null
|
|
255
|
-
});
|
|
256
|
-
}
|
|
126
|
+
var topics = data.RelatedTopics || [];
|
|
127
|
+
for (var i = 0; i < topics.length && results.length < numResults; i++) {
|
|
128
|
+
var topic = topics[i];
|
|
129
|
+
if (topic.Text && topic.FirstURL) results.push({ title: topic.Text.substring(0, 100), url: topic.FirstURL, content: topic.Text });
|
|
257
130
|
if (topic.Topics) {
|
|
258
|
-
for (
|
|
259
|
-
|
|
260
|
-
if (st.Text && st.FirstURL) {
|
|
261
|
-
results.push({
|
|
262
|
-
title: st.Text.substring(0, 100),
|
|
263
|
-
url: st.FirstURL,
|
|
264
|
-
content: st.Text,
|
|
265
|
-
publishedDate: null,
|
|
266
|
-
author: null,
|
|
267
|
-
score: null
|
|
268
|
-
});
|
|
269
|
-
}
|
|
131
|
+
for (var j = 0; j < topic.Topics.length && results.length < numResults; j++) {
|
|
132
|
+
var st = topic.Topics[j];
|
|
133
|
+
if (st.Text && st.FirstURL) results.push({ title: st.Text.substring(0, 100), url: st.FirstURL, content: st.Text });
|
|
270
134
|
}
|
|
271
135
|
}
|
|
272
136
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return results;
|
|
277
|
-
}
|
|
278
|
-
} catch (e) {
|
|
279
|
-
log('DDG API failed:', e.message);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return [];
|
|
137
|
+
return results.length > 0 ? results : null;
|
|
138
|
+
} catch (e) { log('DuckDuckGo failed:', e.message); }
|
|
139
|
+
return null;
|
|
283
140
|
}
|
|
284
141
|
|
|
285
|
-
function
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
142
|
+
async function search(query, numResults) {
|
|
143
|
+
numResults = numResults || 10;
|
|
144
|
+
log('Search:', query);
|
|
145
|
+
|
|
146
|
+
// Priority: Smithery > Google PSE > Serper > Brave > SearXNG > DuckDuckGo
|
|
147
|
+
var results = await searchSmitheryExa(query, numResults);
|
|
148
|
+
if (results && results.length > 0) return { results: results, source: 'smithery-exa' };
|
|
149
|
+
|
|
150
|
+
results = await searchGooglePSE(query, numResults);
|
|
151
|
+
if (results && results.length > 0) return { results: results, source: 'google-pse' };
|
|
152
|
+
|
|
153
|
+
results = await searchSerper(query, numResults);
|
|
154
|
+
if (results && results.length > 0) return { results: results, source: 'serper' };
|
|
155
|
+
|
|
156
|
+
results = await searchBrave(query, numResults);
|
|
157
|
+
if (results && results.length > 0) return { results: results, source: 'brave' };
|
|
158
|
+
|
|
159
|
+
results = await searchSearXNG(query, numResults);
|
|
160
|
+
if (results && results.length > 0) return { results: results, source: 'searxng' };
|
|
161
|
+
|
|
162
|
+
results = await searchDuckDuckGo(query, numResults);
|
|
163
|
+
if (results && results.length > 0) return { results: results, source: 'duckduckgo' };
|
|
164
|
+
|
|
165
|
+
return { results: [], source: 'none' };
|
|
166
|
+
}
|
|
289
167
|
|
|
290
|
-
|
|
291
|
-
let match;
|
|
168
|
+
// === HTTP Proxy Server ===
|
|
292
169
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;
|
|
296
|
-
if (url.includes('uddg=')) {
|
|
297
|
-
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
298
|
-
if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);
|
|
299
|
-
}
|
|
300
|
-
links.push({
|
|
301
|
-
url: url,
|
|
302
|
-
title: decodeHTMLEntities(match[2].trim())
|
|
303
|
-
});
|
|
304
|
-
}
|
|
170
|
+
const server = http.createServer(async (req, res) => {
|
|
171
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
305
172
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
173
|
+
if (url.pathname === '/health') {
|
|
174
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
175
|
+
res.end(JSON.stringify({ status: 'ok', mode: 'external-providers' }));
|
|
176
|
+
return;
|
|
309
177
|
}
|
|
310
178
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
179
|
+
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
180
|
+
let body = '';
|
|
181
|
+
req.on('data', function(c) { body += c; });
|
|
182
|
+
req.on('end', async function() {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(body);
|
|
185
|
+
const result = await search(parsed.query, parsed.numResults || 10);
|
|
186
|
+
log('Results:', result.results.length, 'from', result.source);
|
|
187
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ results: result.results }));
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log('Search error:', e.message);
|
|
191
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
192
|
+
res.end(JSON.stringify({ error: String(e), results: [] }));
|
|
193
|
+
}
|
|
319
194
|
});
|
|
195
|
+
return;
|
|
320
196
|
}
|
|
321
197
|
|
|
322
|
-
|
|
323
|
-
|
|
198
|
+
// Proxy other requests
|
|
199
|
+
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
200
|
+
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
201
|
+
const proxyReq = proxyModule.request(proxyUrl, {
|
|
202
|
+
method: req.method,
|
|
203
|
+
headers: Object.assign({}, req.headers, { host: proxyUrl.host })
|
|
204
|
+
}, function(proxyRes) {
|
|
205
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
206
|
+
proxyRes.pipe(res);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
proxyReq.on('error', function(e) {
|
|
210
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
211
|
+
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') req.pipe(proxyReq);
|
|
215
|
+
else proxyReq.end();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
server.listen(PORT, '127.0.0.1', function() {
|
|
219
|
+
const actualPort = server.address().port;
|
|
220
|
+
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
221
|
+
if (portFile) fs.writeFileSync(portFile, String(actualPort));
|
|
222
|
+
console.log('PORT=' + actualPort);
|
|
223
|
+
log('External providers proxy on port', actualPort);
|
|
224
|
+
});
|
|
324
225
|
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
.replace(/'/g, "'")
|
|
332
|
-
.replace(/ /g, ' ');
|
|
226
|
+
process.on('SIGTERM', function() { server.close(); process.exit(0); });
|
|
227
|
+
process.on('SIGINT', function() { server.close(); process.exit(0); });
|
|
228
|
+
`;
|
|
229
|
+
}
|
|
230
|
+
function generateExternalSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
|
|
231
|
+
return generateSearchProxyServerCode().replace("const FACTORY_API = 'https://api.factory.ai';", `const FACTORY_API = '${factoryApiUrl}';`);
|
|
333
232
|
}
|
|
334
233
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/websearch-native.ts
|
|
236
|
+
/**
|
|
237
|
+
* WebSearch Native Provider Mode (--websearch-proxy)
|
|
238
|
+
*
|
|
239
|
+
* Uses model's native websearch based on ~/.factory/settings.json configuration
|
|
240
|
+
* Requires proxy plugin to handle format conversion:
|
|
241
|
+
* - Anthropic provider: anthropic4droid plugin
|
|
242
|
+
* - OpenAI provider: openai4droid plugin (adds CODEX_INSTRUCTIONS)
|
|
243
|
+
*
|
|
244
|
+
* Supported providers:
|
|
245
|
+
* - Anthropic: web_search_20250305 server tool, results in web_search_tool_result
|
|
246
|
+
* - OpenAI: web_search tool, results in message.content[].annotations[] as url_citation
|
|
247
|
+
*/
|
|
248
|
+
function generateNativeSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
|
|
249
|
+
return `#!/usr/bin/env node
|
|
250
|
+
// Droid WebSearch Proxy Server (Native Provider Mode)
|
|
251
|
+
// Reads ~/.factory/settings.json for model configuration
|
|
252
|
+
// Requires proxy plugin (anthropic4droid) to handle format conversion
|
|
343
253
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
254
|
+
const http = require('http');
|
|
255
|
+
const https = require('https');
|
|
256
|
+
const { execSync } = require('child_process');
|
|
257
|
+
const fs = require('fs');
|
|
258
|
+
const path = require('path');
|
|
259
|
+
const os = require('os');
|
|
260
|
+
|
|
261
|
+
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
262
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
263
|
+
const FACTORY_API = '${factoryApiUrl}';
|
|
264
|
+
|
|
265
|
+
function log(...args) { if (DEBUG) console.error('[websearch]', ...args); }
|
|
350
266
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
267
|
+
// === Settings Configuration ===
|
|
268
|
+
|
|
269
|
+
let cachedSettings = null;
|
|
270
|
+
let settingsLastModified = 0;
|
|
271
|
+
|
|
272
|
+
function getFactorySettings() {
|
|
273
|
+
const settingsPath = path.join(os.homedir(), '.factory', 'settings.json');
|
|
274
|
+
try {
|
|
275
|
+
const stats = fs.statSync(settingsPath);
|
|
276
|
+
if (cachedSettings && stats.mtimeMs === settingsLastModified) return cachedSettings;
|
|
277
|
+
cachedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
278
|
+
settingsLastModified = stats.mtimeMs;
|
|
279
|
+
return cachedSettings;
|
|
280
|
+
} catch (e) {
|
|
281
|
+
log('Failed to load settings.json:', e.message);
|
|
282
|
+
return null;
|
|
356
283
|
}
|
|
284
|
+
}
|
|
357
285
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
286
|
+
function getCurrentModelConfig() {
|
|
287
|
+
const settings = getFactorySettings();
|
|
288
|
+
if (!settings) return null;
|
|
289
|
+
|
|
290
|
+
const currentModelId = settings.sessionDefaultSettings?.model;
|
|
291
|
+
if (!currentModelId) return null;
|
|
292
|
+
|
|
293
|
+
const customModels = settings.customModels || [];
|
|
294
|
+
const modelConfig = customModels.find(m => m.id === currentModelId);
|
|
295
|
+
|
|
296
|
+
if (modelConfig) {
|
|
297
|
+
log('Model:', modelConfig.displayName, '| Provider:', modelConfig.provider);
|
|
298
|
+
return modelConfig;
|
|
363
299
|
}
|
|
300
|
+
|
|
301
|
+
if (!currentModelId.startsWith('custom:')) return null;
|
|
302
|
+
log('Model not found:', currentModelId);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// === Native Provider WebSearch ===
|
|
364
307
|
|
|
365
|
-
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
308
|
+
async function searchAnthropicNative(query, numResults, modelConfig) {
|
|
309
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const requestBody = {
|
|
313
|
+
model: model,
|
|
314
|
+
max_tokens: 4096,
|
|
315
|
+
stream: false,
|
|
316
|
+
system: 'You are a web search assistant. Use the web_search tool to find relevant information and return the results.',
|
|
317
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 1 }],
|
|
318
|
+
tool_choice: { type: 'tool', name: 'web_search' },
|
|
319
|
+
messages: [{ role: 'user', content: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.' }]
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
let endpoint = baseUrl;
|
|
323
|
+
if (!endpoint.endsWith('/v1/messages')) endpoint = endpoint.replace(/\\/$/, '') + '/v1/messages';
|
|
324
|
+
|
|
325
|
+
log('Anthropic search:', query, '→', endpoint);
|
|
326
|
+
|
|
327
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
328
|
+
const curlCmd = 'curl -s -X POST "' + endpoint + '" -H "Content-Type: application/json" -H "anthropic-version: 2023-06-01" -H "x-api-key: ' + apiKey + '" -d \\'' + bodyStr + "\\'";
|
|
329
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
330
|
+
|
|
331
|
+
let response;
|
|
332
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
333
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
334
|
+
|
|
335
|
+
const results = [];
|
|
336
|
+
for (const block of (response.content || [])) {
|
|
337
|
+
if (block.type === 'web_search_tool_result') {
|
|
338
|
+
for (const result of (block.content || [])) {
|
|
339
|
+
if (result.type === 'web_search_result') {
|
|
340
|
+
results.push({
|
|
341
|
+
title: result.title || '',
|
|
342
|
+
url: result.url || '',
|
|
343
|
+
content: result.snippet || result.page_content || ''
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
log('Results:', results.length);
|
|
351
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
352
|
+
} catch (e) {
|
|
353
|
+
log('Anthropic error:', e.message);
|
|
354
|
+
return null;
|
|
370
355
|
}
|
|
356
|
+
}
|
|
371
357
|
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
358
|
+
async function searchOpenAINative(query, numResults, modelConfig) {
|
|
359
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
// Note: instructions will be added by openai4droid proxy plugin
|
|
363
|
+
const requestBody = {
|
|
364
|
+
model: model,
|
|
365
|
+
stream: false,
|
|
366
|
+
tools: [{ type: 'web_search' }],
|
|
367
|
+
tool_choice: 'required',
|
|
368
|
+
input: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.'
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
let endpoint = baseUrl;
|
|
372
|
+
if (!endpoint.endsWith('/responses')) endpoint = endpoint.replace(/\\/$/, '') + '/responses';
|
|
373
|
+
|
|
374
|
+
log('OpenAI search:', query, '→', endpoint);
|
|
375
|
+
|
|
376
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
377
|
+
const curlCmd = 'curl -s -X POST "' + endpoint + '" -H "Content-Type: application/json" -H "Authorization: Bearer ' + apiKey + '" -d \\'' + bodyStr + "\\'";
|
|
378
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
379
|
+
|
|
380
|
+
let response;
|
|
381
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
382
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
383
|
+
|
|
384
|
+
// Extract results from url_citation annotations in message output
|
|
385
|
+
const results = [];
|
|
386
|
+
for (const item of (response.output || [])) {
|
|
387
|
+
if (item.type === 'message' && Array.isArray(item.content)) {
|
|
388
|
+
for (const content of item.content) {
|
|
389
|
+
if (content.type === 'output_text' && Array.isArray(content.annotations)) {
|
|
390
|
+
for (const annotation of content.annotations) {
|
|
391
|
+
if (annotation.type === 'url_citation' && annotation.url) {
|
|
392
|
+
results.push({
|
|
393
|
+
title: annotation.title || '',
|
|
394
|
+
url: annotation.url || '',
|
|
395
|
+
content: annotation.title || ''
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
log('Results:', results.length);
|
|
405
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
log('OpenAI error:', e.message);
|
|
408
|
+
return null;
|
|
377
409
|
}
|
|
410
|
+
}
|
|
378
411
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
async function search(query, numResults) {
|
|
413
|
+
numResults = numResults || 10;
|
|
414
|
+
log('Search:', query);
|
|
415
|
+
|
|
416
|
+
const modelConfig = getCurrentModelConfig();
|
|
417
|
+
if (!modelConfig) {
|
|
418
|
+
log('No custom model configured');
|
|
419
|
+
return { results: [], source: 'none' };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const provider = modelConfig.provider;
|
|
423
|
+
let results = null;
|
|
424
|
+
|
|
425
|
+
if (provider === 'anthropic') results = await searchAnthropicNative(query, numResults, modelConfig);
|
|
426
|
+
else if (provider === 'openai') results = await searchOpenAINative(query, numResults, modelConfig);
|
|
427
|
+
else log('Unsupported provider:', provider);
|
|
428
|
+
|
|
429
|
+
if (results && results.length > 0) return { results: results, source: 'native-' + provider };
|
|
430
|
+
return { results: [], source: 'none' };
|
|
383
431
|
}
|
|
384
432
|
|
|
385
433
|
// === HTTP Proxy Server ===
|
|
386
434
|
|
|
387
435
|
const server = http.createServer(async (req, res) => {
|
|
388
|
-
const url = new URL(req.url,
|
|
436
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
389
437
|
|
|
390
|
-
// Health check
|
|
391
438
|
if (url.pathname === '/health') {
|
|
392
439
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
393
|
-
res.end(JSON.stringify({ status: 'ok',
|
|
440
|
+
res.end(JSON.stringify({ status: 'ok', mode: 'native-provider' }));
|
|
394
441
|
return;
|
|
395
442
|
}
|
|
396
443
|
|
|
397
|
-
// Search endpoint - intercept
|
|
398
444
|
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
399
445
|
let body = '';
|
|
400
|
-
req.on('data', c
|
|
401
|
-
req.on('end', async ()
|
|
446
|
+
req.on('data', function(c) { body += c; });
|
|
447
|
+
req.on('end', async function() {
|
|
402
448
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
log('Results:', results.length, 'from', source);
|
|
449
|
+
const parsed = JSON.parse(body);
|
|
450
|
+
const result = await search(parsed.query, parsed.numResults || 10);
|
|
451
|
+
log('Results:', result.results.length, 'from', result.source);
|
|
407
452
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
408
|
-
res.end(JSON.stringify({ results }));
|
|
453
|
+
res.end(JSON.stringify({ results: result.results }));
|
|
409
454
|
} catch (e) {
|
|
410
455
|
log('Search error:', e.message);
|
|
411
456
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -415,243 +460,164 @@ const server = http.createServer(async (req, res) => {
|
|
|
415
460
|
return;
|
|
416
461
|
}
|
|
417
462
|
|
|
418
|
-
//
|
|
419
|
-
// Whitelist approach: only allow core LLM APIs, mock everything else
|
|
463
|
+
// Standalone mode: mock non-LLM APIs
|
|
420
464
|
if (process.env.STANDALONE_MODE === '1') {
|
|
421
465
|
const pathname = url.pathname;
|
|
422
|
-
|
|
423
|
-
// Whitelist: Core APIs that should be forwarded to upstream
|
|
424
466
|
const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');
|
|
425
|
-
// /api/tools/exa/search is already handled above
|
|
426
467
|
|
|
427
468
|
if (!isCoreLLMApi) {
|
|
428
|
-
// Special handling for specific routes
|
|
429
469
|
if (pathname === '/api/sessions/create') {
|
|
430
|
-
log('Mock (dynamic):', pathname);
|
|
431
|
-
const sessionId = \`local-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
|
|
432
470
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
433
|
-
res.end(JSON.stringify({ id:
|
|
471
|
+
res.end(JSON.stringify({ id: 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10) }));
|
|
434
472
|
return;
|
|
435
473
|
}
|
|
436
|
-
|
|
437
474
|
if (pathname === '/api/cli/whoami') {
|
|
438
|
-
log('Mock (401):', pathname);
|
|
439
475
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
440
|
-
res.end(JSON.stringify({ error: 'Unauthorized'
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (pathname === '/api/tools/get-url-contents') {
|
|
445
|
-
log('Mock (404):', pathname);
|
|
446
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
447
|
-
res.end(JSON.stringify({ error: 'Not available', message: 'Use local URL fetch fallback' }));
|
|
476
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
448
477
|
return;
|
|
449
478
|
}
|
|
450
|
-
|
|
451
|
-
// All other non-core APIs: return empty success
|
|
452
|
-
log('Mock (default):', pathname);
|
|
453
479
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
480
|
res.end(JSON.stringify({}));
|
|
455
481
|
return;
|
|
456
482
|
}
|
|
457
483
|
}
|
|
458
484
|
|
|
459
|
-
//
|
|
485
|
+
// Simple proxy - no SSE transformation (handled by proxy plugin)
|
|
460
486
|
log('Proxy:', req.method, url.pathname);
|
|
461
|
-
|
|
462
487
|
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
463
|
-
// Choose http or https based on target protocol
|
|
464
488
|
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
465
489
|
const proxyReq = proxyModule.request(proxyUrl, {
|
|
466
490
|
method: req.method,
|
|
467
|
-
headers: {
|
|
468
|
-
}, proxyRes
|
|
491
|
+
headers: Object.assign({}, req.headers, { host: proxyUrl.host })
|
|
492
|
+
}, function(proxyRes) {
|
|
469
493
|
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
470
494
|
proxyRes.pipe(res);
|
|
471
495
|
});
|
|
472
496
|
|
|
473
|
-
proxyReq.on('error', e
|
|
474
|
-
log('Proxy error:', e.message);
|
|
497
|
+
proxyReq.on('error', function(e) {
|
|
475
498
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
476
499
|
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
477
500
|
});
|
|
478
501
|
|
|
479
|
-
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
480
|
-
|
|
481
|
-
} else {
|
|
482
|
-
proxyReq.end();
|
|
483
|
-
}
|
|
502
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') req.pipe(proxyReq);
|
|
503
|
+
else proxyReq.end();
|
|
484
504
|
});
|
|
485
505
|
|
|
486
|
-
|
|
487
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
506
|
+
server.listen(PORT, '127.0.0.1', function() {
|
|
488
507
|
const actualPort = server.address().port;
|
|
489
|
-
const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;
|
|
490
|
-
|
|
491
|
-
// Write port file for parent process to read
|
|
492
508
|
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
493
|
-
if (portFile)
|
|
494
|
-
fs.writeFileSync(portFile, String(actualPort));
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Output PORT= line for wrapper script to parse
|
|
509
|
+
if (portFile) fs.writeFileSync(portFile, String(actualPort));
|
|
498
510
|
console.log('PORT=' + actualPort);
|
|
499
|
-
|
|
500
|
-
const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;
|
|
501
|
-
log('Search proxy started on http://127.0.0.1:' + actualPort);
|
|
502
|
-
log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');
|
|
503
|
-
log('Google PSE:', hasGoogle ? 'configured' : 'not set');
|
|
504
|
-
log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');
|
|
505
|
-
log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');
|
|
506
|
-
log('SearXNG:', process.env.SEARXNG_URL || 'not set');
|
|
511
|
+
log('Native provider proxy on port', actualPort);
|
|
507
512
|
});
|
|
508
513
|
|
|
509
|
-
process.on('SIGTERM', ()
|
|
510
|
-
process.on('SIGINT', ()
|
|
514
|
+
process.on('SIGTERM', function() { server.close(); process.exit(0); });
|
|
515
|
+
process.on('SIGINT', function() { server.close(); process.exit(0); });
|
|
511
516
|
`;
|
|
512
517
|
}
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/websearch-patch.ts
|
|
513
521
|
/**
|
|
514
|
-
* Generate unified
|
|
515
|
-
* Each droid instance runs its own proxy:
|
|
516
|
-
* - Uses port 0 to let system auto-assign available port
|
|
517
|
-
* - Proxy runs as child process
|
|
518
|
-
* - Proxy is killed when droid exits
|
|
519
|
-
* - Supports multiple droid instances running simultaneously
|
|
522
|
+
* Generate unified wrapper script
|
|
520
523
|
*/
|
|
521
524
|
function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
|
|
522
525
|
const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
|
|
523
526
|
return `#!/bin/bash
|
|
524
527
|
# Droid with WebSearch
|
|
525
528
|
# Auto-generated by droid-patch --websearch
|
|
526
|
-
# Each instance runs its own proxy on a system-assigned port
|
|
527
529
|
|
|
528
530
|
PROXY_SCRIPT="${proxyScriptPath}"
|
|
529
531
|
DROID_BIN="${droidPath}"
|
|
530
532
|
PROXY_PID=""
|
|
531
|
-
PORT_FILE="/tmp/droid-websearch
|
|
533
|
+
PORT_FILE="/tmp/droid-websearch-$$.port"
|
|
532
534
|
STANDALONE="${standalone ? "1" : "0"}"
|
|
533
535
|
|
|
534
|
-
# Passthrough for non-interactive/meta commands (avoid starting a proxy for help/version/etc)
|
|
535
536
|
should_passthrough() {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
break
|
|
540
|
-
fi
|
|
541
|
-
case "\$arg" in
|
|
542
|
-
--help|-h|--version|-V)
|
|
543
|
-
return 0
|
|
544
|
-
;;
|
|
545
|
-
esac
|
|
537
|
+
for arg in "$@"; do
|
|
538
|
+
if [ "$arg" = "--" ]; then break; fi
|
|
539
|
+
case "$arg" in --help|-h|--version|-V) return 0 ;; esac
|
|
546
540
|
done
|
|
547
|
-
|
|
548
|
-
# Top-level command token
|
|
549
541
|
local end_opts=0
|
|
550
|
-
for arg in "
|
|
551
|
-
if [ "
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
fi
|
|
555
|
-
if [ "\$end_opts" -eq 0 ] && [[ "\$arg" == -* ]]; then
|
|
556
|
-
continue
|
|
557
|
-
fi
|
|
558
|
-
case "\$arg" in
|
|
559
|
-
help|version|completion|completions|exec)
|
|
560
|
-
return 0
|
|
561
|
-
;;
|
|
562
|
-
esac
|
|
542
|
+
for arg in "$@"; do
|
|
543
|
+
if [ "$arg" = "--" ]; then end_opts=1; continue; fi
|
|
544
|
+
if [ "$end_opts" -eq 0 ] && [[ "$arg" == -* ]]; then continue; fi
|
|
545
|
+
case "$arg" in help|version|completion|completions|exec) return 0 ;; esac
|
|
563
546
|
break
|
|
564
547
|
done
|
|
565
|
-
|
|
566
548
|
return 1
|
|
567
549
|
}
|
|
568
550
|
|
|
569
|
-
if should_passthrough "
|
|
570
|
-
exec "\$DROID_BIN" "\$@"
|
|
571
|
-
fi
|
|
551
|
+
if should_passthrough "$@"; then exec "$DROID_BIN" "$@"; fi
|
|
572
552
|
|
|
573
|
-
# Cleanup function - kill proxy when droid exits
|
|
574
553
|
cleanup() {
|
|
575
|
-
if [ -n "
|
|
576
|
-
[ -n "
|
|
577
|
-
kill "
|
|
578
|
-
wait "
|
|
554
|
+
if [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
555
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: $PROXY_PID)" >&2
|
|
556
|
+
kill "$PROXY_PID" 2>/dev/null
|
|
557
|
+
wait "$PROXY_PID" 2>/dev/null
|
|
579
558
|
fi
|
|
580
|
-
rm -f "
|
|
559
|
+
rm -f "$PORT_FILE"
|
|
581
560
|
}
|
|
582
|
-
|
|
583
|
-
# Set up trap to cleanup on exit
|
|
584
561
|
trap cleanup EXIT INT TERM
|
|
585
562
|
|
|
586
|
-
[ -n "
|
|
587
|
-
[ "
|
|
563
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
|
|
564
|
+
[ "$STANDALONE" = "1" ] && [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Standalone mode enabled" >&2
|
|
588
565
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if [ -n "\$DROID_SEARCH_DEBUG" ]; then
|
|
592
|
-
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" 2>&1 &
|
|
566
|
+
if [ -n "$DROID_SEARCH_DEBUG" ]; then
|
|
567
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" 2>&1 &
|
|
593
568
|
else
|
|
594
|
-
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="
|
|
569
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" >/dev/null 2>&1 &
|
|
595
570
|
fi
|
|
596
|
-
PROXY_PID
|
|
571
|
+
PROXY_PID=$!
|
|
597
572
|
|
|
598
|
-
# Wait for proxy to start and get actual port (max 5 seconds)
|
|
599
573
|
for i in {1..50}; do
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
574
|
+
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
575
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
603
576
|
break
|
|
604
577
|
fi
|
|
605
|
-
if [ -f "
|
|
606
|
-
ACTUAL_PORT
|
|
607
|
-
if [ -n "
|
|
608
|
-
[ -n "
|
|
578
|
+
if [ -f "$PORT_FILE" ]; then
|
|
579
|
+
ACTUAL_PORT=$(cat "$PORT_FILE" 2>/dev/null)
|
|
580
|
+
if [ -n "$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:$ACTUAL_PORT/health" > /dev/null 2>&1; then
|
|
581
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port $ACTUAL_PORT (PID: $PROXY_PID)" >&2
|
|
609
582
|
break
|
|
610
583
|
fi
|
|
611
584
|
fi
|
|
612
585
|
sleep 0.1
|
|
613
586
|
done
|
|
614
587
|
|
|
615
|
-
|
|
616
|
-
if [ ! -f "\$PORT_FILE" ] || [ -z "\$(cat "\$PORT_FILE" 2>/dev/null)" ]; then
|
|
588
|
+
if [ ! -f "$PORT_FILE" ] || [ -z "$(cat "$PORT_FILE" 2>/dev/null)" ]; then
|
|
617
589
|
echo "[websearch] Failed to start proxy, running without websearch" >&2
|
|
618
590
|
cleanup
|
|
619
|
-
exec "
|
|
591
|
+
exec "$DROID_BIN" "$@"
|
|
620
592
|
fi
|
|
621
593
|
|
|
622
|
-
ACTUAL_PORT
|
|
623
|
-
rm -f "
|
|
624
|
-
|
|
625
|
-
# Run droid with proxy
|
|
626
|
-
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:\$ACTUAL_PORT"
|
|
627
|
-
"\$DROID_BIN" "\$@"
|
|
628
|
-
DROID_EXIT_CODE=\$?
|
|
594
|
+
ACTUAL_PORT=$(cat "$PORT_FILE")
|
|
595
|
+
rm -f "$PORT_FILE"
|
|
629
596
|
|
|
630
|
-
|
|
631
|
-
|
|
597
|
+
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$ACTUAL_PORT"
|
|
598
|
+
"$DROID_BIN" "$@"
|
|
599
|
+
DROID_EXIT_CODE=$?
|
|
600
|
+
exit $DROID_EXIT_CODE
|
|
632
601
|
`;
|
|
633
602
|
}
|
|
634
603
|
/**
|
|
635
604
|
* Create unified WebSearch files
|
|
636
605
|
*
|
|
637
|
-
* Approach: Proxy server mode
|
|
638
|
-
* - wrapper script starts local proxy server
|
|
639
|
-
* - proxy server intercepts search requests, passes through other requests
|
|
640
|
-
* - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy
|
|
641
|
-
* - alias works directly, no extra steps needed
|
|
642
|
-
*
|
|
643
606
|
* @param outputDir - Directory to write files to
|
|
644
607
|
* @param droidPath - Path to droid binary
|
|
645
608
|
* @param aliasName - Alias name for the wrapper
|
|
646
609
|
* @param apiBase - Custom API base URL for proxy to forward requests to
|
|
647
610
|
* @param standalone - Standalone mode: mock non-LLM Factory APIs
|
|
611
|
+
* @param useNativeProvider - Use native provider websearch (--websearch-proxy mode)
|
|
648
612
|
*/
|
|
649
|
-
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false) {
|
|
613
|
+
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false, useNativeProvider = false) {
|
|
650
614
|
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
651
615
|
const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
|
|
652
616
|
const wrapperScriptPath = join(outputDir, aliasName);
|
|
653
|
-
|
|
617
|
+
const factoryApiUrl = apiBase || "https://api.factory.ai";
|
|
618
|
+
await writeFile(proxyScriptPath, useNativeProvider ? generateNativeSearchProxyServer(factoryApiUrl) : generateExternalSearchProxyServer(factoryApiUrl));
|
|
654
619
|
console.log(`[*] Created proxy script: ${proxyScriptPath}`);
|
|
620
|
+
console.log(`[*] Mode: ${useNativeProvider ? "native provider (requires proxy plugin)" : "external providers"}`);
|
|
655
621
|
await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
|
|
656
622
|
await chmod(wrapperScriptPath, 493);
|
|
657
623
|
console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
|
|
@@ -714,12 +680,13 @@ function findDefaultDroidPath() {
|
|
|
714
680
|
for (const p of paths) if (existsSync(p)) return p;
|
|
715
681
|
return join(home, ".droid", "bin", "droid");
|
|
716
682
|
}
|
|
717
|
-
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy (
|
|
683
|
+
bin("droid-patch", "CLI tool to patch droid binary with various modifications").package("droid-patch", version).option("--is-custom", "Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)").option("--skip-login", "Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)").option("--api-base <url>", "Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").option("--websearch", "Enable local WebSearch proxy with external providers (Smithery, Google PSE, etc.)").option("--websearch-proxy", "Enable native provider websearch (requires proxy plugin, reads ~/.factory/settings.json)").option("--standalone", "Standalone mode: mock non-LLM Factory APIs (use with --websearch)").option("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").option("--disable-telemetry", "Disable telemetry and Sentry error reporting (block data uploads)").option("--auto-high", "Set default autonomy mode to auto-high (bypass settings.json race condition)").option("--dry-run", "Verify patches without actually modifying the binary").option("-p, --path <path>", "Path to the droid binary").option("-o, --output <dir>", "Output directory for patched binary").option("--no-backup", "Do not create backup of original binary").option("-v, --verbose", "Enable verbose output").argument("[alias]", "Alias name for the patched binary").action(async (options, args) => {
|
|
718
684
|
const alias = args?.[0];
|
|
719
685
|
const isCustom = options["is-custom"];
|
|
720
686
|
const skipLogin = options["skip-login"];
|
|
721
687
|
const apiBase = options["api-base"];
|
|
722
688
|
const websearch = options["websearch"];
|
|
689
|
+
const websearchProxy = options["websearch-proxy"];
|
|
723
690
|
const standalone = options["standalone"];
|
|
724
691
|
const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
|
|
725
692
|
const reasoningEffort = options["reasoning-effort"];
|
|
@@ -731,24 +698,37 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
731
698
|
const backup = options.backup !== false;
|
|
732
699
|
const verbose = options.verbose;
|
|
733
700
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
734
|
-
|
|
701
|
+
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch && !websearchProxy;
|
|
702
|
+
if (websearch && websearchProxy) {
|
|
703
|
+
console.log(styleText("red", "Error: Cannot use --websearch and --websearch-proxy together"));
|
|
704
|
+
console.log(styleText("gray", "Choose one:"));
|
|
705
|
+
console.log(styleText("gray", " --websearch External providers (Smithery, Google PSE, etc.)"));
|
|
706
|
+
console.log(styleText("gray", " --websearch-proxy Native provider (requires proxy plugin)"));
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
if (!needsBinaryPatch && (websearch || websearchProxy)) {
|
|
735
710
|
if (!alias) {
|
|
736
|
-
|
|
737
|
-
console.log(styleText("
|
|
711
|
+
const flag = websearchProxy ? "--websearch-proxy" : "--websearch";
|
|
712
|
+
console.log(styleText("red", `Error: Alias name required for ${flag}`));
|
|
713
|
+
console.log(styleText("gray", `Usage: npx droid-patch ${flag} <alias>`));
|
|
738
714
|
process.exit(1);
|
|
739
715
|
}
|
|
740
716
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
741
717
|
console.log(styleText(["cyan", "bold"], " Droid Wrapper Setup"));
|
|
742
718
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
743
719
|
console.log();
|
|
744
|
-
if (
|
|
745
|
-
console.log(styleText("white", `WebSearch:
|
|
720
|
+
if (websearchProxy) {
|
|
721
|
+
console.log(styleText("white", `WebSearch: native provider mode`));
|
|
722
|
+
console.log(styleText("gray", ` Requires proxy plugin (anthropic4droid)`));
|
|
723
|
+
console.log(styleText("gray", ` Reads model config from ~/.factory/settings.json`));
|
|
724
|
+
} else if (websearch) {
|
|
725
|
+
console.log(styleText("white", `WebSearch: external providers mode`));
|
|
746
726
|
console.log(styleText("white", `Forward target: ${websearchTarget}`));
|
|
747
|
-
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
748
727
|
}
|
|
728
|
+
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
749
729
|
console.log();
|
|
750
730
|
let execTargetPath = path;
|
|
751
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
731
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
752
732
|
execTargetPath = wrapperScript;
|
|
753
733
|
const aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
754
734
|
const droidVersion = getDroidVersion(path);
|
|
@@ -757,6 +737,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
757
737
|
skipLogin: false,
|
|
758
738
|
apiBase: apiBase || null,
|
|
759
739
|
websearch: !!websearch,
|
|
740
|
+
websearchProxy: !!websearchProxy,
|
|
760
741
|
reasoningEffort: false,
|
|
761
742
|
noTelemetry: false,
|
|
762
743
|
standalone
|
|
@@ -773,10 +754,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
773
754
|
console.log("Run directly:");
|
|
774
755
|
console.log(styleText("yellow", ` ${alias}`));
|
|
775
756
|
console.log();
|
|
776
|
-
if (
|
|
777
|
-
console.log(styleText("cyan", "
|
|
778
|
-
console.log(styleText("gray", "
|
|
779
|
-
console.log(styleText("gray", "
|
|
757
|
+
if (websearchProxy) {
|
|
758
|
+
console.log(styleText("cyan", "Native Provider WebSearch (--websearch-proxy):"));
|
|
759
|
+
console.log(styleText("gray", " Uses model's built-in websearch via proxy plugin"));
|
|
760
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
761
|
+
console.log();
|
|
762
|
+
console.log(styleText("yellow", "IMPORTANT: Requires proxy plugin (anthropic4droid)"));
|
|
763
|
+
console.log();
|
|
764
|
+
console.log("Supported providers:");
|
|
765
|
+
console.log(styleText("yellow", " - anthropic: Claude web_search_20250305 server tool"));
|
|
766
|
+
console.log(styleText("yellow", " - openai: OpenAI web_search tool"));
|
|
767
|
+
console.log(styleText("gray", " - generic-chat-completion-api: Not supported"));
|
|
768
|
+
console.log();
|
|
769
|
+
console.log("Debug mode:");
|
|
770
|
+
console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1 # Basic logs"));
|
|
771
|
+
console.log(styleText("gray", " export DROID_SEARCH_VERBOSE=1 # Full request/response"));
|
|
772
|
+
} else if (websearch) {
|
|
773
|
+
console.log(styleText("cyan", "External Providers WebSearch (--websearch):"));
|
|
774
|
+
console.log(styleText("gray", " Uses external search providers (Smithery, Google PSE, etc.)"));
|
|
780
775
|
console.log();
|
|
781
776
|
console.log("Search providers (in priority order):");
|
|
782
777
|
console.log(styleText("yellow", " 1. Smithery Exa (best quality):"));
|
|
@@ -947,16 +942,22 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
947
942
|
if (result.success && result.outputPath && alias) {
|
|
948
943
|
console.log();
|
|
949
944
|
let execTargetPath = result.outputPath;
|
|
950
|
-
if (websearch) {
|
|
951
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
945
|
+
if (websearch || websearchProxy) {
|
|
946
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
952
947
|
execTargetPath = wrapperScript;
|
|
953
948
|
console.log();
|
|
954
|
-
|
|
955
|
-
|
|
949
|
+
if (websearchProxy) {
|
|
950
|
+
console.log(styleText("cyan", "WebSearch enabled (native provider mode)"));
|
|
951
|
+
console.log(styleText("gray", " Requires proxy plugin (anthropic4droid)"));
|
|
952
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
953
|
+
} else {
|
|
954
|
+
console.log(styleText("cyan", "WebSearch enabled (external providers mode)"));
|
|
955
|
+
console.log(styleText("white", ` Forward target: ${websearchTarget}`));
|
|
956
|
+
}
|
|
956
957
|
if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
|
|
957
958
|
}
|
|
958
959
|
let aliasResult;
|
|
959
|
-
if (websearch) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
960
|
+
if (websearch || websearchProxy) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
960
961
|
else aliasResult = await createAlias(result.outputPath, alias, verbose);
|
|
961
962
|
const droidVersion = getDroidVersion(path);
|
|
962
963
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -964,6 +965,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
964
965
|
skipLogin: !!skipLogin,
|
|
965
966
|
apiBase: apiBase || null,
|
|
966
967
|
websearch: !!websearch,
|
|
968
|
+
websearchProxy: !!websearchProxy,
|
|
967
969
|
reasoningEffort: !!reasoningEffort,
|
|
968
970
|
noTelemetry: !!noTelemetry,
|
|
969
971
|
standalone: !!standalone,
|