droid-patch 0.10.0 → 0.11.0
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 +440 -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,379 @@ 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
|
+
});
|
|
324
213
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
+
});
|
|
225
|
+
|
|
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 (anthropic4droid) to handle format conversion
|
|
241
|
+
*
|
|
242
|
+
* Supported providers: Anthropic, OpenAI (extensible)
|
|
243
|
+
*/
|
|
244
|
+
function generateNativeSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
|
|
245
|
+
return `#!/usr/bin/env node
|
|
246
|
+
// Droid WebSearch Proxy Server (Native Provider Mode)
|
|
247
|
+
// Reads ~/.factory/settings.json for model configuration
|
|
248
|
+
// Requires proxy plugin (anthropic4droid) to handle format conversion
|
|
343
249
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
250
|
+
const http = require('http');
|
|
251
|
+
const https = require('https');
|
|
252
|
+
const { execSync } = require('child_process');
|
|
253
|
+
const fs = require('fs');
|
|
254
|
+
const path = require('path');
|
|
255
|
+
const os = require('os');
|
|
256
|
+
|
|
257
|
+
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
258
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
259
|
+
const FACTORY_API = '${factoryApiUrl}';
|
|
350
260
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
261
|
+
function log(...args) { if (DEBUG) console.error('[websearch]', ...args); }
|
|
262
|
+
|
|
263
|
+
// === Settings Configuration ===
|
|
264
|
+
|
|
265
|
+
let cachedSettings = null;
|
|
266
|
+
let settingsLastModified = 0;
|
|
267
|
+
|
|
268
|
+
function getFactorySettings() {
|
|
269
|
+
const settingsPath = path.join(os.homedir(), '.factory', 'settings.json');
|
|
270
|
+
try {
|
|
271
|
+
const stats = fs.statSync(settingsPath);
|
|
272
|
+
if (cachedSettings && stats.mtimeMs === settingsLastModified) return cachedSettings;
|
|
273
|
+
cachedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
274
|
+
settingsLastModified = stats.mtimeMs;
|
|
275
|
+
return cachedSettings;
|
|
276
|
+
} catch (e) {
|
|
277
|
+
log('Failed to load settings.json:', e.message);
|
|
278
|
+
return null;
|
|
356
279
|
}
|
|
280
|
+
}
|
|
357
281
|
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
361
|
-
|
|
362
|
-
|
|
282
|
+
function getCurrentModelConfig() {
|
|
283
|
+
const settings = getFactorySettings();
|
|
284
|
+
if (!settings) return null;
|
|
285
|
+
|
|
286
|
+
const currentModelId = settings.sessionDefaultSettings?.model;
|
|
287
|
+
if (!currentModelId) return null;
|
|
288
|
+
|
|
289
|
+
const customModels = settings.customModels || [];
|
|
290
|
+
const modelConfig = customModels.find(m => m.id === currentModelId);
|
|
291
|
+
|
|
292
|
+
if (modelConfig) {
|
|
293
|
+
log('Model:', modelConfig.displayName, '| Provider:', modelConfig.provider);
|
|
294
|
+
return modelConfig;
|
|
363
295
|
}
|
|
296
|
+
|
|
297
|
+
if (!currentModelId.startsWith('custom:')) return null;
|
|
298
|
+
log('Model not found:', currentModelId);
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
364
301
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
302
|
+
// === Native Provider WebSearch ===
|
|
303
|
+
|
|
304
|
+
async function searchAnthropicNative(query, numResults, modelConfig) {
|
|
305
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const requestBody = {
|
|
309
|
+
model: model,
|
|
310
|
+
max_tokens: 4096,
|
|
311
|
+
stream: false,
|
|
312
|
+
system: 'You are a web search assistant. Use the web_search tool to find relevant information and return the results.',
|
|
313
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 1 }],
|
|
314
|
+
tool_choice: { type: 'tool', name: 'web_search' },
|
|
315
|
+
messages: [{ role: 'user', content: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.' }]
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
let endpoint = baseUrl;
|
|
319
|
+
if (!endpoint.endsWith('/v1/messages')) endpoint = endpoint.replace(/\\/$/, '') + '/v1/messages';
|
|
320
|
+
|
|
321
|
+
log('Anthropic search:', query, '→', endpoint);
|
|
322
|
+
|
|
323
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
324
|
+
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 + "\\'";
|
|
325
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
326
|
+
|
|
327
|
+
let response;
|
|
328
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
329
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
330
|
+
|
|
331
|
+
const results = [];
|
|
332
|
+
for (const block of (response.content || [])) {
|
|
333
|
+
if (block.type === 'web_search_tool_result') {
|
|
334
|
+
for (const result of (block.content || [])) {
|
|
335
|
+
if (result.type === 'web_search_result') {
|
|
336
|
+
results.push({
|
|
337
|
+
title: result.title || '',
|
|
338
|
+
url: result.url || '',
|
|
339
|
+
content: result.snippet || result.page_content || ''
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
log('Results:', results.length);
|
|
347
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
348
|
+
} catch (e) {
|
|
349
|
+
log('Anthropic error:', e.message);
|
|
350
|
+
return null;
|
|
370
351
|
}
|
|
352
|
+
}
|
|
371
353
|
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
354
|
+
async function searchOpenAINative(query, numResults, modelConfig) {
|
|
355
|
+
const { baseUrl, apiKey, model } = modelConfig;
|
|
356
|
+
|
|
357
|
+
try {
|
|
358
|
+
const requestBody = {
|
|
359
|
+
model: model,
|
|
360
|
+
tools: [{ type: 'web_search' }],
|
|
361
|
+
tool_choice: 'required',
|
|
362
|
+
input: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.'
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
let endpoint = baseUrl;
|
|
366
|
+
if (!endpoint.endsWith('/responses')) endpoint = endpoint.replace(/\\/$/, '') + '/responses';
|
|
367
|
+
|
|
368
|
+
log('OpenAI search:', query, '→', endpoint);
|
|
369
|
+
|
|
370
|
+
const bodyStr = JSON.stringify(requestBody).replace(/'/g, "'\\\\''");
|
|
371
|
+
const curlCmd = 'curl -s -X POST "' + endpoint + '" -H "Content-Type: application/json" -H "Authorization: Bearer ' + apiKey + '" -d \\'' + bodyStr + "\\'";
|
|
372
|
+
const responseStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 60000 });
|
|
373
|
+
|
|
374
|
+
let response;
|
|
375
|
+
try { response = JSON.parse(responseStr); } catch { return null; }
|
|
376
|
+
if (response.error) { log('API error:', response.error.message); return null; }
|
|
377
|
+
|
|
378
|
+
const results = [];
|
|
379
|
+
for (const item of (response.output || [])) {
|
|
380
|
+
if (item.type === 'web_search_call' && item.status === 'completed') {
|
|
381
|
+
for (const result of (item.results || [])) {
|
|
382
|
+
results.push({
|
|
383
|
+
title: result.title || '',
|
|
384
|
+
url: result.url || '',
|
|
385
|
+
content: result.snippet || result.content || ''
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
log('Results:', results.length);
|
|
392
|
+
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
393
|
+
} catch (e) {
|
|
394
|
+
log('OpenAI error:', e.message);
|
|
395
|
+
return null;
|
|
377
396
|
}
|
|
397
|
+
}
|
|
378
398
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
399
|
+
async function search(query, numResults) {
|
|
400
|
+
numResults = numResults || 10;
|
|
401
|
+
log('Search:', query);
|
|
402
|
+
|
|
403
|
+
const modelConfig = getCurrentModelConfig();
|
|
404
|
+
if (!modelConfig) {
|
|
405
|
+
log('No custom model configured');
|
|
406
|
+
return { results: [], source: 'none' };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const provider = modelConfig.provider;
|
|
410
|
+
let results = null;
|
|
411
|
+
|
|
412
|
+
if (provider === 'anthropic') results = await searchAnthropicNative(query, numResults, modelConfig);
|
|
413
|
+
else if (provider === 'openai') results = await searchOpenAINative(query, numResults, modelConfig);
|
|
414
|
+
else log('Unsupported provider:', provider);
|
|
415
|
+
|
|
416
|
+
if (results && results.length > 0) return { results: results, source: 'native-' + provider };
|
|
417
|
+
return { results: [], source: 'none' };
|
|
383
418
|
}
|
|
384
419
|
|
|
385
420
|
// === HTTP Proxy Server ===
|
|
386
421
|
|
|
387
422
|
const server = http.createServer(async (req, res) => {
|
|
388
|
-
const url = new URL(req.url,
|
|
423
|
+
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
389
424
|
|
|
390
|
-
// Health check
|
|
391
425
|
if (url.pathname === '/health') {
|
|
392
426
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
393
|
-
res.end(JSON.stringify({ status: 'ok',
|
|
427
|
+
res.end(JSON.stringify({ status: 'ok', mode: 'native-provider' }));
|
|
394
428
|
return;
|
|
395
429
|
}
|
|
396
430
|
|
|
397
|
-
// Search endpoint - intercept
|
|
398
431
|
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
399
432
|
let body = '';
|
|
400
|
-
req.on('data', c
|
|
401
|
-
req.on('end', async ()
|
|
433
|
+
req.on('data', function(c) { body += c; });
|
|
434
|
+
req.on('end', async function() {
|
|
402
435
|
try {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
log('Results:', results.length, 'from', source);
|
|
436
|
+
const parsed = JSON.parse(body);
|
|
437
|
+
const result = await search(parsed.query, parsed.numResults || 10);
|
|
438
|
+
log('Results:', result.results.length, 'from', result.source);
|
|
407
439
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
408
|
-
res.end(JSON.stringify({ results }));
|
|
440
|
+
res.end(JSON.stringify({ results: result.results }));
|
|
409
441
|
} catch (e) {
|
|
410
442
|
log('Search error:', e.message);
|
|
411
443
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -415,243 +447,164 @@ const server = http.createServer(async (req, res) => {
|
|
|
415
447
|
return;
|
|
416
448
|
}
|
|
417
449
|
|
|
418
|
-
//
|
|
419
|
-
// Whitelist approach: only allow core LLM APIs, mock everything else
|
|
450
|
+
// Standalone mode: mock non-LLM APIs
|
|
420
451
|
if (process.env.STANDALONE_MODE === '1') {
|
|
421
452
|
const pathname = url.pathname;
|
|
422
|
-
|
|
423
|
-
// Whitelist: Core APIs that should be forwarded to upstream
|
|
424
453
|
const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');
|
|
425
|
-
// /api/tools/exa/search is already handled above
|
|
426
454
|
|
|
427
455
|
if (!isCoreLLMApi) {
|
|
428
|
-
// Special handling for specific routes
|
|
429
456
|
if (pathname === '/api/sessions/create') {
|
|
430
|
-
log('Mock (dynamic):', pathname);
|
|
431
|
-
const sessionId = \`local-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
|
|
432
457
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
433
|
-
res.end(JSON.stringify({ id:
|
|
458
|
+
res.end(JSON.stringify({ id: 'local-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10) }));
|
|
434
459
|
return;
|
|
435
460
|
}
|
|
436
|
-
|
|
437
461
|
if (pathname === '/api/cli/whoami') {
|
|
438
|
-
log('Mock (401):', pathname);
|
|
439
462
|
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' }));
|
|
463
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
448
464
|
return;
|
|
449
465
|
}
|
|
450
|
-
|
|
451
|
-
// All other non-core APIs: return empty success
|
|
452
|
-
log('Mock (default):', pathname);
|
|
453
466
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
467
|
res.end(JSON.stringify({}));
|
|
455
468
|
return;
|
|
456
469
|
}
|
|
457
470
|
}
|
|
458
471
|
|
|
459
|
-
//
|
|
472
|
+
// Simple proxy - no SSE transformation (handled by proxy plugin)
|
|
460
473
|
log('Proxy:', req.method, url.pathname);
|
|
461
|
-
|
|
462
474
|
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
463
|
-
// Choose http or https based on target protocol
|
|
464
475
|
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
465
476
|
const proxyReq = proxyModule.request(proxyUrl, {
|
|
466
477
|
method: req.method,
|
|
467
|
-
headers: {
|
|
468
|
-
}, proxyRes
|
|
478
|
+
headers: Object.assign({}, req.headers, { host: proxyUrl.host })
|
|
479
|
+
}, function(proxyRes) {
|
|
469
480
|
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
470
481
|
proxyRes.pipe(res);
|
|
471
482
|
});
|
|
472
483
|
|
|
473
|
-
proxyReq.on('error', e
|
|
474
|
-
log('Proxy error:', e.message);
|
|
484
|
+
proxyReq.on('error', function(e) {
|
|
475
485
|
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
476
486
|
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
477
487
|
});
|
|
478
488
|
|
|
479
|
-
if (req.method !== 'GET' && req.method !== 'HEAD')
|
|
480
|
-
|
|
481
|
-
} else {
|
|
482
|
-
proxyReq.end();
|
|
483
|
-
}
|
|
489
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') req.pipe(proxyReq);
|
|
490
|
+
else proxyReq.end();
|
|
484
491
|
});
|
|
485
492
|
|
|
486
|
-
|
|
487
|
-
server.listen(PORT, '127.0.0.1', () => {
|
|
493
|
+
server.listen(PORT, '127.0.0.1', function() {
|
|
488
494
|
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
495
|
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
|
|
496
|
+
if (portFile) fs.writeFileSync(portFile, String(actualPort));
|
|
498
497
|
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');
|
|
498
|
+
log('Native provider proxy on port', actualPort);
|
|
507
499
|
});
|
|
508
500
|
|
|
509
|
-
process.on('SIGTERM', ()
|
|
510
|
-
process.on('SIGINT', ()
|
|
501
|
+
process.on('SIGTERM', function() { server.close(); process.exit(0); });
|
|
502
|
+
process.on('SIGINT', function() { server.close(); process.exit(0); });
|
|
511
503
|
`;
|
|
512
504
|
}
|
|
505
|
+
|
|
506
|
+
//#endregion
|
|
507
|
+
//#region src/websearch-patch.ts
|
|
513
508
|
/**
|
|
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
|
|
509
|
+
* Generate unified wrapper script
|
|
520
510
|
*/
|
|
521
511
|
function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
|
|
522
512
|
const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
|
|
523
513
|
return `#!/bin/bash
|
|
524
514
|
# Droid with WebSearch
|
|
525
515
|
# Auto-generated by droid-patch --websearch
|
|
526
|
-
# Each instance runs its own proxy on a system-assigned port
|
|
527
516
|
|
|
528
517
|
PROXY_SCRIPT="${proxyScriptPath}"
|
|
529
518
|
DROID_BIN="${droidPath}"
|
|
530
519
|
PROXY_PID=""
|
|
531
|
-
PORT_FILE="/tmp/droid-websearch
|
|
520
|
+
PORT_FILE="/tmp/droid-websearch-$$.port"
|
|
532
521
|
STANDALONE="${standalone ? "1" : "0"}"
|
|
533
522
|
|
|
534
|
-
# Passthrough for non-interactive/meta commands (avoid starting a proxy for help/version/etc)
|
|
535
523
|
should_passthrough() {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
break
|
|
540
|
-
fi
|
|
541
|
-
case "\$arg" in
|
|
542
|
-
--help|-h|--version|-V)
|
|
543
|
-
return 0
|
|
544
|
-
;;
|
|
545
|
-
esac
|
|
524
|
+
for arg in "$@"; do
|
|
525
|
+
if [ "$arg" = "--" ]; then break; fi
|
|
526
|
+
case "$arg" in --help|-h|--version|-V) return 0 ;; esac
|
|
546
527
|
done
|
|
547
|
-
|
|
548
|
-
# Top-level command token
|
|
549
528
|
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
|
|
529
|
+
for arg in "$@"; do
|
|
530
|
+
if [ "$arg" = "--" ]; then end_opts=1; continue; fi
|
|
531
|
+
if [ "$end_opts" -eq 0 ] && [[ "$arg" == -* ]]; then continue; fi
|
|
532
|
+
case "$arg" in help|version|completion|completions|exec) return 0 ;; esac
|
|
563
533
|
break
|
|
564
534
|
done
|
|
565
|
-
|
|
566
535
|
return 1
|
|
567
536
|
}
|
|
568
537
|
|
|
569
|
-
if should_passthrough "
|
|
570
|
-
exec "\$DROID_BIN" "\$@"
|
|
571
|
-
fi
|
|
538
|
+
if should_passthrough "$@"; then exec "$DROID_BIN" "$@"; fi
|
|
572
539
|
|
|
573
|
-
# Cleanup function - kill proxy when droid exits
|
|
574
540
|
cleanup() {
|
|
575
|
-
if [ -n "
|
|
576
|
-
[ -n "
|
|
577
|
-
kill "
|
|
578
|
-
wait "
|
|
541
|
+
if [ -n "$PROXY_PID" ] && kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
542
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: $PROXY_PID)" >&2
|
|
543
|
+
kill "$PROXY_PID" 2>/dev/null
|
|
544
|
+
wait "$PROXY_PID" 2>/dev/null
|
|
579
545
|
fi
|
|
580
|
-
rm -f "
|
|
546
|
+
rm -f "$PORT_FILE"
|
|
581
547
|
}
|
|
582
|
-
|
|
583
|
-
# Set up trap to cleanup on exit
|
|
584
548
|
trap cleanup EXIT INT TERM
|
|
585
549
|
|
|
586
|
-
[ -n "
|
|
587
|
-
[ "
|
|
550
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
|
|
551
|
+
[ "$STANDALONE" = "1" ] && [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Standalone mode enabled" >&2
|
|
588
552
|
|
|
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 &
|
|
553
|
+
if [ -n "$DROID_SEARCH_DEBUG" ]; then
|
|
554
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" 2>&1 &
|
|
593
555
|
else
|
|
594
|
-
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="
|
|
556
|
+
${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="$PORT_FILE" node "$PROXY_SCRIPT" >/dev/null 2>&1 &
|
|
595
557
|
fi
|
|
596
|
-
PROXY_PID
|
|
558
|
+
PROXY_PID=$!
|
|
597
559
|
|
|
598
|
-
# Wait for proxy to start and get actual port (max 5 seconds)
|
|
599
560
|
for i in {1..50}; do
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
[ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
561
|
+
if ! kill -0 "$PROXY_PID" 2>/dev/null; then
|
|
562
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
|
|
603
563
|
break
|
|
604
564
|
fi
|
|
605
|
-
if [ -f "
|
|
606
|
-
ACTUAL_PORT
|
|
607
|
-
if [ -n "
|
|
608
|
-
[ -n "
|
|
565
|
+
if [ -f "$PORT_FILE" ]; then
|
|
566
|
+
ACTUAL_PORT=$(cat "$PORT_FILE" 2>/dev/null)
|
|
567
|
+
if [ -n "$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:$ACTUAL_PORT/health" > /dev/null 2>&1; then
|
|
568
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port $ACTUAL_PORT (PID: $PROXY_PID)" >&2
|
|
609
569
|
break
|
|
610
570
|
fi
|
|
611
571
|
fi
|
|
612
572
|
sleep 0.1
|
|
613
573
|
done
|
|
614
574
|
|
|
615
|
-
|
|
616
|
-
if [ ! -f "\$PORT_FILE" ] || [ -z "\$(cat "\$PORT_FILE" 2>/dev/null)" ]; then
|
|
575
|
+
if [ ! -f "$PORT_FILE" ] || [ -z "$(cat "$PORT_FILE" 2>/dev/null)" ]; then
|
|
617
576
|
echo "[websearch] Failed to start proxy, running without websearch" >&2
|
|
618
577
|
cleanup
|
|
619
|
-
exec "
|
|
578
|
+
exec "$DROID_BIN" "$@"
|
|
620
579
|
fi
|
|
621
580
|
|
|
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=\$?
|
|
581
|
+
ACTUAL_PORT=$(cat "$PORT_FILE")
|
|
582
|
+
rm -f "$PORT_FILE"
|
|
629
583
|
|
|
630
|
-
|
|
631
|
-
|
|
584
|
+
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$ACTUAL_PORT"
|
|
585
|
+
"$DROID_BIN" "$@"
|
|
586
|
+
DROID_EXIT_CODE=$?
|
|
587
|
+
exit $DROID_EXIT_CODE
|
|
632
588
|
`;
|
|
633
589
|
}
|
|
634
590
|
/**
|
|
635
591
|
* Create unified WebSearch files
|
|
636
592
|
*
|
|
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
593
|
* @param outputDir - Directory to write files to
|
|
644
594
|
* @param droidPath - Path to droid binary
|
|
645
595
|
* @param aliasName - Alias name for the wrapper
|
|
646
596
|
* @param apiBase - Custom API base URL for proxy to forward requests to
|
|
647
597
|
* @param standalone - Standalone mode: mock non-LLM Factory APIs
|
|
598
|
+
* @param useNativeProvider - Use native provider websearch (--websearch-proxy mode)
|
|
648
599
|
*/
|
|
649
|
-
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false) {
|
|
600
|
+
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false, useNativeProvider = false) {
|
|
650
601
|
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
651
602
|
const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
|
|
652
603
|
const wrapperScriptPath = join(outputDir, aliasName);
|
|
653
|
-
|
|
604
|
+
const factoryApiUrl = apiBase || "https://api.factory.ai";
|
|
605
|
+
await writeFile(proxyScriptPath, useNativeProvider ? generateNativeSearchProxyServer(factoryApiUrl) : generateExternalSearchProxyServer(factoryApiUrl));
|
|
654
606
|
console.log(`[*] Created proxy script: ${proxyScriptPath}`);
|
|
607
|
+
console.log(`[*] Mode: ${useNativeProvider ? "native provider (requires proxy plugin)" : "external providers"}`);
|
|
655
608
|
await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
|
|
656
609
|
await chmod(wrapperScriptPath, 493);
|
|
657
610
|
console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
|
|
@@ -714,12 +667,13 @@ function findDefaultDroidPath() {
|
|
|
714
667
|
for (const p of paths) if (existsSync(p)) return p;
|
|
715
668
|
return join(home, ".droid", "bin", "droid");
|
|
716
669
|
}
|
|
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 (
|
|
670
|
+
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
671
|
const alias = args?.[0];
|
|
719
672
|
const isCustom = options["is-custom"];
|
|
720
673
|
const skipLogin = options["skip-login"];
|
|
721
674
|
const apiBase = options["api-base"];
|
|
722
675
|
const websearch = options["websearch"];
|
|
676
|
+
const websearchProxy = options["websearch-proxy"];
|
|
723
677
|
const standalone = options["standalone"];
|
|
724
678
|
const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
|
|
725
679
|
const reasoningEffort = options["reasoning-effort"];
|
|
@@ -731,24 +685,37 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
731
685
|
const backup = options.backup !== false;
|
|
732
686
|
const verbose = options.verbose;
|
|
733
687
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
734
|
-
|
|
688
|
+
const needsBinaryPatch = !!isCustom || !!skipLogin || !!reasoningEffort || !!noTelemetry || !!autoHigh || !!apiBase && !websearch && !websearchProxy;
|
|
689
|
+
if (websearch && websearchProxy) {
|
|
690
|
+
console.log(styleText("red", "Error: Cannot use --websearch and --websearch-proxy together"));
|
|
691
|
+
console.log(styleText("gray", "Choose one:"));
|
|
692
|
+
console.log(styleText("gray", " --websearch External providers (Smithery, Google PSE, etc.)"));
|
|
693
|
+
console.log(styleText("gray", " --websearch-proxy Native provider (requires proxy plugin)"));
|
|
694
|
+
process.exit(1);
|
|
695
|
+
}
|
|
696
|
+
if (!needsBinaryPatch && (websearch || websearchProxy)) {
|
|
735
697
|
if (!alias) {
|
|
736
|
-
|
|
737
|
-
console.log(styleText("
|
|
698
|
+
const flag = websearchProxy ? "--websearch-proxy" : "--websearch";
|
|
699
|
+
console.log(styleText("red", `Error: Alias name required for ${flag}`));
|
|
700
|
+
console.log(styleText("gray", `Usage: npx droid-patch ${flag} <alias>`));
|
|
738
701
|
process.exit(1);
|
|
739
702
|
}
|
|
740
703
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
741
704
|
console.log(styleText(["cyan", "bold"], " Droid Wrapper Setup"));
|
|
742
705
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
743
706
|
console.log();
|
|
744
|
-
if (
|
|
745
|
-
console.log(styleText("white", `WebSearch:
|
|
707
|
+
if (websearchProxy) {
|
|
708
|
+
console.log(styleText("white", `WebSearch: native provider mode`));
|
|
709
|
+
console.log(styleText("gray", ` Requires proxy plugin (anthropic4droid)`));
|
|
710
|
+
console.log(styleText("gray", ` Reads model config from ~/.factory/settings.json`));
|
|
711
|
+
} else if (websearch) {
|
|
712
|
+
console.log(styleText("white", `WebSearch: external providers mode`));
|
|
746
713
|
console.log(styleText("white", `Forward target: ${websearchTarget}`));
|
|
747
|
-
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
748
714
|
}
|
|
715
|
+
if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
|
|
749
716
|
console.log();
|
|
750
717
|
let execTargetPath = path;
|
|
751
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
718
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
752
719
|
execTargetPath = wrapperScript;
|
|
753
720
|
const aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
754
721
|
const droidVersion = getDroidVersion(path);
|
|
@@ -757,6 +724,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
757
724
|
skipLogin: false,
|
|
758
725
|
apiBase: apiBase || null,
|
|
759
726
|
websearch: !!websearch,
|
|
727
|
+
websearchProxy: !!websearchProxy,
|
|
760
728
|
reasoningEffort: false,
|
|
761
729
|
noTelemetry: false,
|
|
762
730
|
standalone
|
|
@@ -773,10 +741,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
773
741
|
console.log("Run directly:");
|
|
774
742
|
console.log(styleText("yellow", ` ${alias}`));
|
|
775
743
|
console.log();
|
|
776
|
-
if (
|
|
777
|
-
console.log(styleText("cyan", "
|
|
778
|
-
console.log(styleText("gray", "
|
|
779
|
-
console.log(styleText("gray", "
|
|
744
|
+
if (websearchProxy) {
|
|
745
|
+
console.log(styleText("cyan", "Native Provider WebSearch (--websearch-proxy):"));
|
|
746
|
+
console.log(styleText("gray", " Uses model's built-in websearch via proxy plugin"));
|
|
747
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
748
|
+
console.log();
|
|
749
|
+
console.log(styleText("yellow", "IMPORTANT: Requires proxy plugin (anthropic4droid)"));
|
|
750
|
+
console.log();
|
|
751
|
+
console.log("Supported providers:");
|
|
752
|
+
console.log(styleText("yellow", " - anthropic: Claude web_search_20250305 server tool"));
|
|
753
|
+
console.log(styleText("yellow", " - openai: OpenAI web_search tool"));
|
|
754
|
+
console.log(styleText("gray", " - generic-chat-completion-api: Not supported"));
|
|
755
|
+
console.log();
|
|
756
|
+
console.log("Debug mode:");
|
|
757
|
+
console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1 # Basic logs"));
|
|
758
|
+
console.log(styleText("gray", " export DROID_SEARCH_VERBOSE=1 # Full request/response"));
|
|
759
|
+
} else if (websearch) {
|
|
760
|
+
console.log(styleText("cyan", "External Providers WebSearch (--websearch):"));
|
|
761
|
+
console.log(styleText("gray", " Uses external search providers (Smithery, Google PSE, etc.)"));
|
|
780
762
|
console.log();
|
|
781
763
|
console.log("Search providers (in priority order):");
|
|
782
764
|
console.log(styleText("yellow", " 1. Smithery Exa (best quality):"));
|
|
@@ -947,16 +929,22 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
947
929
|
if (result.success && result.outputPath && alias) {
|
|
948
930
|
console.log();
|
|
949
931
|
let execTargetPath = result.outputPath;
|
|
950
|
-
if (websearch) {
|
|
951
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchTarget, standalone);
|
|
932
|
+
if (websearch || websearchProxy) {
|
|
933
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, alias, websearchProxy ? void 0 : websearchTarget, standalone, websearchProxy);
|
|
952
934
|
execTargetPath = wrapperScript;
|
|
953
935
|
console.log();
|
|
954
|
-
|
|
955
|
-
|
|
936
|
+
if (websearchProxy) {
|
|
937
|
+
console.log(styleText("cyan", "WebSearch enabled (native provider mode)"));
|
|
938
|
+
console.log(styleText("gray", " Requires proxy plugin (anthropic4droid)"));
|
|
939
|
+
console.log(styleText("gray", " Reads model config from ~/.factory/settings.json"));
|
|
940
|
+
} else {
|
|
941
|
+
console.log(styleText("cyan", "WebSearch enabled (external providers mode)"));
|
|
942
|
+
console.log(styleText("white", ` Forward target: ${websearchTarget}`));
|
|
943
|
+
}
|
|
956
944
|
if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
|
|
957
945
|
}
|
|
958
946
|
let aliasResult;
|
|
959
|
-
if (websearch) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
947
|
+
if (websearch || websearchProxy) aliasResult = await createAliasForWrapper(execTargetPath, alias, verbose);
|
|
960
948
|
else aliasResult = await createAlias(result.outputPath, alias, verbose);
|
|
961
949
|
const droidVersion = getDroidVersion(path);
|
|
962
950
|
await saveAliasMetadata(createMetadata(alias, path, {
|
|
@@ -964,6 +952,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
964
952
|
skipLogin: !!skipLogin,
|
|
965
953
|
apiBase: apiBase || null,
|
|
966
954
|
websearch: !!websearch,
|
|
955
|
+
websearchProxy: !!websearchProxy,
|
|
967
956
|
reasoningEffort: !!reasoningEffort,
|
|
968
957
|
noTelemetry: !!noTelemetry,
|
|
969
958
|
standalone: !!standalone,
|