droid-patch 0.1.2 → 0.2.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 +360 -6
- package/README.zh-CN.md +491 -0
- package/dist/{alias-DcCF7R2B.js → alias-C9LRaTwF.js} +106 -4
- package/dist/alias-C9LRaTwF.js.map +1 -0
- package/dist/cli.js +814 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/alias-DcCF7R2B.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,665 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createAlias, listAliases, patchDroid, removeAlias } from "./alias-
|
|
2
|
+
import { createAlias, createAliasForWrapper, listAliases, patchDroid, removeAlias } from "./alias-C9LRaTwF.js";
|
|
3
3
|
import bin from "tiny-bin";
|
|
4
4
|
import { styleText } from "node:util";
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
9
10
|
|
|
11
|
+
//#region src/websearch-patch.ts
|
|
12
|
+
/**
|
|
13
|
+
* Generate search proxy server code (runs in background)
|
|
14
|
+
* Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,
|
|
15
|
+
* use a local proxy server to intercept search requests instead
|
|
16
|
+
*
|
|
17
|
+
* Features:
|
|
18
|
+
* - Auto-shutdown on idle (default 5 minutes without requests)
|
|
19
|
+
* - Timeout configurable via DROID_PROXY_IDLE_TIMEOUT env var (seconds)
|
|
20
|
+
* - Set to 0 to disable timeout
|
|
21
|
+
*/
|
|
22
|
+
function generateSearchProxyServer() {
|
|
23
|
+
return `#!/usr/bin/env node
|
|
24
|
+
// Droid WebSearch Proxy Server
|
|
25
|
+
// Auto-generated by droid-patch --websearch
|
|
26
|
+
|
|
27
|
+
const http = require('http');
|
|
28
|
+
const https = require('https');
|
|
29
|
+
const { execSync } = require('child_process');
|
|
30
|
+
const fs = require('fs');
|
|
31
|
+
|
|
32
|
+
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
33
|
+
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '23119');
|
|
34
|
+
|
|
35
|
+
// Idle timeout in seconds, default 5 minutes, set to 0 to disable
|
|
36
|
+
const IDLE_TIMEOUT = parseInt(process.env.DROID_PROXY_IDLE_TIMEOUT || '300');
|
|
37
|
+
let lastActivityTime = Date.now();
|
|
38
|
+
let idleCheckTimer = null;
|
|
39
|
+
|
|
40
|
+
function log(...args) {
|
|
41
|
+
if (DEBUG) console.error('[websearch]', ...args);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Update activity time
|
|
45
|
+
function updateActivity() {
|
|
46
|
+
lastActivityTime = Date.now();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if any droid process is running (droid instances using the proxy)
|
|
50
|
+
function isDroidRunning() {
|
|
51
|
+
try {
|
|
52
|
+
const { execSync } = require('child_process');
|
|
53
|
+
// Use ps to check if droid.patched binary is running
|
|
54
|
+
// Exclude scripts and grep itself, only match actual droid binary processes
|
|
55
|
+
const result = execSync(
|
|
56
|
+
'ps aux | grep -E "[d]roid\\\\.patched" | grep -v grep | wc -l',
|
|
57
|
+
{ encoding: 'utf-8', timeout: 1000 }
|
|
58
|
+
).trim();
|
|
59
|
+
return parseInt(result) > 0;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check idle time and possibly exit
|
|
66
|
+
function checkIdleAndExit() {
|
|
67
|
+
if (IDLE_TIMEOUT <= 0) return; // Timeout disabled
|
|
68
|
+
|
|
69
|
+
// If droid process is running, refresh activity time (like heartbeat)
|
|
70
|
+
if (isDroidRunning()) {
|
|
71
|
+
log('Droid process detected, keeping proxy alive');
|
|
72
|
+
updateActivity();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const idleMs = Date.now() - lastActivityTime;
|
|
77
|
+
const timeoutMs = IDLE_TIMEOUT * 1000;
|
|
78
|
+
|
|
79
|
+
if (idleMs >= timeoutMs) {
|
|
80
|
+
log(\`Idle for \${Math.round(idleMs / 1000)}s and no droid running, shutting down...\`);
|
|
81
|
+
cleanup();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cleanup resources
|
|
87
|
+
function cleanup() {
|
|
88
|
+
if (idleCheckTimer) {
|
|
89
|
+
clearInterval(idleCheckTimer);
|
|
90
|
+
idleCheckTimer = null;
|
|
91
|
+
}
|
|
92
|
+
// Delete PID file
|
|
93
|
+
try {
|
|
94
|
+
fs.unlinkSync('/tmp/droid-search-proxy.pid');
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// === Search Implementation ===
|
|
99
|
+
|
|
100
|
+
// Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE
|
|
101
|
+
async function searchSmitheryExa(query, numResults) {
|
|
102
|
+
const apiKey = process.env.SMITHERY_API_KEY;
|
|
103
|
+
const profile = process.env.SMITHERY_PROFILE;
|
|
104
|
+
if (!apiKey || !profile) return null;
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
// Construct URL with authentication
|
|
108
|
+
const serverUrl = \`https://server.smithery.ai/exa/mcp?api_key=\${encodeURIComponent(apiKey)}&profile=\${encodeURIComponent(profile)}\`;
|
|
109
|
+
log('Smithery Exa request');
|
|
110
|
+
|
|
111
|
+
// Use MCP protocol to call the search tool via HTTP POST
|
|
112
|
+
const requestBody = JSON.stringify({
|
|
113
|
+
jsonrpc: '2.0',
|
|
114
|
+
id: 1,
|
|
115
|
+
method: 'tools/call',
|
|
116
|
+
params: {
|
|
117
|
+
name: 'web_search_exa',
|
|
118
|
+
arguments: {
|
|
119
|
+
query: query,
|
|
120
|
+
numResults: numResults
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const curlCmd = \`curl -s -X POST "\${serverUrl}" -H "Content-Type: application/json" -d '\${requestBody.replace(/'/g, "'\\\\\\\\''")}'\`;
|
|
126
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });
|
|
127
|
+
const response = JSON.parse(jsonStr);
|
|
128
|
+
|
|
129
|
+
// Parse MCP response
|
|
130
|
+
if (response.result && response.result.content) {
|
|
131
|
+
// MCP returns content as array of text blocks
|
|
132
|
+
const textContent = response.result.content.find(c => c.type === 'text');
|
|
133
|
+
if (textContent && textContent.text) {
|
|
134
|
+
try {
|
|
135
|
+
const searchResults = JSON.parse(textContent.text);
|
|
136
|
+
if (Array.isArray(searchResults) && searchResults.length > 0) {
|
|
137
|
+
return searchResults.slice(0, numResults).map(item => ({
|
|
138
|
+
title: item.title || '',
|
|
139
|
+
url: item.url || '',
|
|
140
|
+
content: item.text || item.snippet || item.highlights?.join(' ') || '',
|
|
141
|
+
publishedDate: item.publishedDate || null,
|
|
142
|
+
author: item.author || null,
|
|
143
|
+
score: item.score || null
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
} catch (parseErr) {
|
|
147
|
+
log('Smithery response parsing failed');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (response.error) {
|
|
153
|
+
log('Smithery Exa error:', response.error.message || response.error);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
} catch (e) {
|
|
157
|
+
log('Smithery Exa failed:', e.message);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function searchGooglePSE(query, numResults) {
|
|
164
|
+
const apiKey = process.env.GOOGLE_PSE_API_KEY;
|
|
165
|
+
const cx = process.env.GOOGLE_PSE_CX;
|
|
166
|
+
if (!apiKey || !cx) return null;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const url = \`https://www.googleapis.com/customsearch/v1?key=\${apiKey}&cx=\${cx}&q=\${encodeURIComponent(query)}&num=\${Math.min(numResults, 10)}\`;
|
|
170
|
+
log('Google PSE request:', url.replace(apiKey, '***'));
|
|
171
|
+
|
|
172
|
+
const curlCmd = \`curl -s "\${url}"\`;
|
|
173
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
174
|
+
const data = JSON.parse(jsonStr);
|
|
175
|
+
|
|
176
|
+
if (data.error) {
|
|
177
|
+
log('Google PSE error:', data.error.message);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return (data.items || []).map(item => ({
|
|
181
|
+
title: item.title,
|
|
182
|
+
url: item.link,
|
|
183
|
+
content: item.snippet || '',
|
|
184
|
+
publishedDate: null,
|
|
185
|
+
author: null,
|
|
186
|
+
score: null
|
|
187
|
+
}));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
log('Google PSE failed:', e.message);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// SearXNG - self-hosted meta search engine
|
|
195
|
+
async function searchSearXNG(query, numResults) {
|
|
196
|
+
const searxngUrl = process.env.SEARXNG_URL;
|
|
197
|
+
if (!searxngUrl) return null;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const url = \`\${searxngUrl}/search?q=\${encodeURIComponent(query)}&format=json&engines=google,bing,duckduckgo\`;
|
|
201
|
+
log('SearXNG request:', url);
|
|
202
|
+
|
|
203
|
+
const curlCmd = \`curl -s "\${url}" -H "Accept: application/json"\`;
|
|
204
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
205
|
+
const data = JSON.parse(jsonStr);
|
|
206
|
+
|
|
207
|
+
if (data.results && data.results.length > 0) {
|
|
208
|
+
return data.results.slice(0, numResults).map(item => ({
|
|
209
|
+
title: item.title,
|
|
210
|
+
url: item.url,
|
|
211
|
+
content: item.content || '',
|
|
212
|
+
publishedDate: null,
|
|
213
|
+
author: null,
|
|
214
|
+
score: null
|
|
215
|
+
}));
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
log('SearXNG failed:', e.message);
|
|
219
|
+
}
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Serper API - free tier available (2500 queries/month)
|
|
224
|
+
async function searchSerper(query, numResults) {
|
|
225
|
+
const apiKey = process.env.SERPER_API_KEY;
|
|
226
|
+
if (!apiKey) return null;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const curlCmd = \`curl -s "https://google.serper.dev/search" -H "X-API-KEY: \${apiKey}" -H "Content-Type: application/json" -d '{"q":"\${query.replace(/"/g, '\\\\"')}","num":\${numResults}}'\`;
|
|
230
|
+
log('Serper request');
|
|
231
|
+
|
|
232
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
233
|
+
const data = JSON.parse(jsonStr);
|
|
234
|
+
|
|
235
|
+
if (data.organic && data.organic.length > 0) {
|
|
236
|
+
return data.organic.slice(0, numResults).map(item => ({
|
|
237
|
+
title: item.title,
|
|
238
|
+
url: item.link,
|
|
239
|
+
content: item.snippet || '',
|
|
240
|
+
publishedDate: null,
|
|
241
|
+
author: null,
|
|
242
|
+
score: null
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
} catch (e) {
|
|
246
|
+
log('Serper failed:', e.message);
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Brave Search API - free tier available
|
|
252
|
+
async function searchBrave(query, numResults) {
|
|
253
|
+
const apiKey = process.env.BRAVE_API_KEY;
|
|
254
|
+
if (!apiKey) return null;
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const url = \`https://api.search.brave.com/res/v1/web/search?q=\${encodeURIComponent(query)}&count=\${numResults}\`;
|
|
258
|
+
const curlCmd = \`curl -s "\${url}" -H "Accept: application/json" -H "X-Subscription-Token: \${apiKey}"\`;
|
|
259
|
+
log('Brave request');
|
|
260
|
+
|
|
261
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
262
|
+
const data = JSON.parse(jsonStr);
|
|
263
|
+
|
|
264
|
+
if (data.web && data.web.results && data.web.results.length > 0) {
|
|
265
|
+
return data.web.results.slice(0, numResults).map(item => ({
|
|
266
|
+
title: item.title,
|
|
267
|
+
url: item.url,
|
|
268
|
+
content: item.description || '',
|
|
269
|
+
publishedDate: null,
|
|
270
|
+
author: null,
|
|
271
|
+
score: null
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
} catch (e) {
|
|
275
|
+
log('Brave failed:', e.message);
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// DuckDuckGo - limited reliability due to bot detection
|
|
281
|
+
async function searchDuckDuckGo(query, numResults) {
|
|
282
|
+
// DuckDuckGo Instant Answer API (limited results but more reliable)
|
|
283
|
+
try {
|
|
284
|
+
const apiUrl = \`https://api.duckduckgo.com/?q=\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\`;
|
|
285
|
+
const curlCmd = \`curl -s "\${apiUrl}" -H "User-Agent: Mozilla/5.0"\`;
|
|
286
|
+
const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });
|
|
287
|
+
const data = JSON.parse(jsonStr);
|
|
288
|
+
|
|
289
|
+
const results = [];
|
|
290
|
+
|
|
291
|
+
if (data.Abstract && data.AbstractURL) {
|
|
292
|
+
results.push({
|
|
293
|
+
title: data.Heading || query,
|
|
294
|
+
url: data.AbstractURL,
|
|
295
|
+
content: data.Abstract,
|
|
296
|
+
publishedDate: null,
|
|
297
|
+
author: null,
|
|
298
|
+
score: null
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const topic of (data.RelatedTopics || [])) {
|
|
303
|
+
if (results.length >= numResults) break;
|
|
304
|
+
if (topic.Text && topic.FirstURL) {
|
|
305
|
+
results.push({
|
|
306
|
+
title: topic.Text.substring(0, 100),
|
|
307
|
+
url: topic.FirstURL,
|
|
308
|
+
content: topic.Text,
|
|
309
|
+
publishedDate: null,
|
|
310
|
+
author: null,
|
|
311
|
+
score: null
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
if (topic.Topics) {
|
|
315
|
+
for (const st of topic.Topics) {
|
|
316
|
+
if (results.length >= numResults) break;
|
|
317
|
+
if (st.Text && st.FirstURL) {
|
|
318
|
+
results.push({
|
|
319
|
+
title: st.Text.substring(0, 100),
|
|
320
|
+
url: st.FirstURL,
|
|
321
|
+
content: st.Text,
|
|
322
|
+
publishedDate: null,
|
|
323
|
+
author: null,
|
|
324
|
+
score: null
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (results.length > 0) {
|
|
332
|
+
log('DDG API:', results.length, 'results');
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
} catch (e) {
|
|
336
|
+
log('DDG API failed:', e.message);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function parseDDGLiteHTML(html, maxResults) {
|
|
343
|
+
const results = [];
|
|
344
|
+
const linkRegex = /<a[^>]+rel="nofollow"[^>]+href="([^"]+)"[^>]*>([^<]+)<\\/a>/gi;
|
|
345
|
+
const snippetRegex = /<td[^>]*class="result-snippet"[^>]*>([^<]*)<\\/td>/gi;
|
|
346
|
+
|
|
347
|
+
const links = [];
|
|
348
|
+
let match;
|
|
349
|
+
|
|
350
|
+
while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {
|
|
351
|
+
let url = match[1];
|
|
352
|
+
if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;
|
|
353
|
+
if (url.includes('uddg=')) {
|
|
354
|
+
const uddgMatch = url.match(/uddg=([^&]+)/);
|
|
355
|
+
if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);
|
|
356
|
+
}
|
|
357
|
+
links.push({
|
|
358
|
+
url: url,
|
|
359
|
+
title: decodeHTMLEntities(match[2].trim())
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const snippets = [];
|
|
364
|
+
while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {
|
|
365
|
+
snippets.push(decodeHTMLEntities(match[1].trim()));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
for (let i = 0; i < links.length && results.length < maxResults; i++) {
|
|
369
|
+
results.push({
|
|
370
|
+
title: links[i].title,
|
|
371
|
+
url: links[i].url,
|
|
372
|
+
content: snippets[i] || '',
|
|
373
|
+
publishedDate: null,
|
|
374
|
+
author: null,
|
|
375
|
+
score: null
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return results;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function decodeHTMLEntities(str) {
|
|
383
|
+
return str
|
|
384
|
+
.replace(/&/g, '&')
|
|
385
|
+
.replace(/</g, '<')
|
|
386
|
+
.replace(/>/g, '>')
|
|
387
|
+
.replace(/"/g, '"')
|
|
388
|
+
.replace(/'/g, "'")
|
|
389
|
+
.replace(/ /g, ' ');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function search(query, numResults = 10) {
|
|
393
|
+
// Priority order:
|
|
394
|
+
// 1. Smithery Exa MCP (best quality if configured)
|
|
395
|
+
// 2. Google PSE (most reliable if configured)
|
|
396
|
+
// 3. Serper (free tier: 2500/month)
|
|
397
|
+
// 4. Brave Search (free tier available)
|
|
398
|
+
// 5. SearXNG (self-hosted)
|
|
399
|
+
// 6. DuckDuckGo (limited due to bot detection)
|
|
400
|
+
|
|
401
|
+
// 1. Smithery Exa MCP (highest priority)
|
|
402
|
+
const smitheryResults = await searchSmitheryExa(query, numResults);
|
|
403
|
+
if (smitheryResults && smitheryResults.length > 0) {
|
|
404
|
+
log('Using Smithery Exa');
|
|
405
|
+
return { results: smitheryResults, source: 'smithery-exa' };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 2. Google PSE
|
|
409
|
+
const googleResults = await searchGooglePSE(query, numResults);
|
|
410
|
+
if (googleResults && googleResults.length > 0) {
|
|
411
|
+
log('Using Google PSE');
|
|
412
|
+
return { results: googleResults, source: 'google-pse' };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 3. Serper
|
|
416
|
+
const serperResults = await searchSerper(query, numResults);
|
|
417
|
+
if (serperResults && serperResults.length > 0) {
|
|
418
|
+
log('Using Serper');
|
|
419
|
+
return { results: serperResults, source: 'serper' };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 4. Brave Search
|
|
423
|
+
const braveResults = await searchBrave(query, numResults);
|
|
424
|
+
if (braveResults && braveResults.length > 0) {
|
|
425
|
+
log('Using Brave Search');
|
|
426
|
+
return { results: braveResults, source: 'brave' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// 5. SearXNG
|
|
430
|
+
const searxngResults = await searchSearXNG(query, numResults);
|
|
431
|
+
if (searxngResults && searxngResults.length > 0) {
|
|
432
|
+
log('Using SearXNG');
|
|
433
|
+
return { results: searxngResults, source: 'searxng' };
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// 6. DuckDuckGo (last resort, limited results)
|
|
437
|
+
log('Using DuckDuckGo (fallback)');
|
|
438
|
+
const ddgResults = await searchDuckDuckGo(query, numResults);
|
|
439
|
+
return { results: ddgResults, source: 'duckduckgo' };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// === HTTP Proxy Server ===
|
|
443
|
+
|
|
444
|
+
const FACTORY_API = 'https://api.factory.ai';
|
|
445
|
+
|
|
446
|
+
const server = http.createServer(async (req, res) => {
|
|
447
|
+
const url = new URL(req.url, \`http://\${req.headers.host}\`);
|
|
448
|
+
|
|
449
|
+
// Health check - don't update activity time to avoid self-ping preventing timeout
|
|
450
|
+
if (url.pathname === '/health') {
|
|
451
|
+
const idleSeconds = Math.round((Date.now() - lastActivityTime) / 1000);
|
|
452
|
+
const droidRunning = isDroidRunning();
|
|
453
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
454
|
+
res.end(JSON.stringify({
|
|
455
|
+
status: 'ok',
|
|
456
|
+
port: PORT,
|
|
457
|
+
idleTimeout: IDLE_TIMEOUT,
|
|
458
|
+
idleSeconds: idleSeconds,
|
|
459
|
+
droidRunning: droidRunning,
|
|
460
|
+
// If droid is running, won't shutdown; otherwise calculate based on idle time
|
|
461
|
+
willShutdownIn: IDLE_TIMEOUT > 0 && !droidRunning ? Math.max(0, IDLE_TIMEOUT - idleSeconds) : null
|
|
462
|
+
}));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Update activity time (only non-health-check requests refresh it)
|
|
467
|
+
updateActivity();
|
|
468
|
+
|
|
469
|
+
// Search endpoint - intercept
|
|
470
|
+
if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
|
|
471
|
+
let body = '';
|
|
472
|
+
req.on('data', c => body += c);
|
|
473
|
+
req.on('end', async () => {
|
|
474
|
+
try {
|
|
475
|
+
const { query, numResults } = JSON.parse(body);
|
|
476
|
+
log('Search query:', query);
|
|
477
|
+
const { results, source } = await search(query, numResults || 10);
|
|
478
|
+
log('Results:', results.length, 'from', source);
|
|
479
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
480
|
+
res.end(JSON.stringify({ results }));
|
|
481
|
+
} catch (e) {
|
|
482
|
+
log('Search error:', e.message);
|
|
483
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
484
|
+
res.end(JSON.stringify({ error: String(e), results: [] }));
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Proxy all other requests to Factory API
|
|
491
|
+
log('Proxy:', req.method, url.pathname);
|
|
492
|
+
|
|
493
|
+
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
494
|
+
const proxyReq = https.request(proxyUrl, {
|
|
495
|
+
method: req.method,
|
|
496
|
+
headers: { ...req.headers, host: proxyUrl.host }
|
|
497
|
+
}, proxyRes => {
|
|
498
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
499
|
+
proxyRes.pipe(res);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
proxyReq.on('error', e => {
|
|
503
|
+
log('Proxy error:', e.message);
|
|
504
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
505
|
+
res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
509
|
+
req.pipe(proxyReq);
|
|
510
|
+
} else {
|
|
511
|
+
proxyReq.end();
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// If port is 0, system will automatically assign an available port
|
|
516
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
517
|
+
const actualPort = server.address().port;
|
|
518
|
+
const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;
|
|
519
|
+
|
|
520
|
+
// Write port file for parent process to read
|
|
521
|
+
const portFile = process.env.SEARCH_PROXY_PORT_FILE;
|
|
522
|
+
if (portFile) {
|
|
523
|
+
fs.writeFileSync(portFile, String(actualPort));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;
|
|
527
|
+
log('Search proxy started on http://127.0.0.1:' + actualPort);
|
|
528
|
+
log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');
|
|
529
|
+
log('Google PSE:', hasGoogle ? 'configured' : 'not set');
|
|
530
|
+
log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');
|
|
531
|
+
log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');
|
|
532
|
+
log('SearXNG:', process.env.SEARXNG_URL || 'not set');
|
|
533
|
+
|
|
534
|
+
// Start idle check timer
|
|
535
|
+
// Check interval = min(timeout/2, 30s) to ensure timely timeout detection
|
|
536
|
+
if (IDLE_TIMEOUT > 0) {
|
|
537
|
+
const checkInterval = Math.min(IDLE_TIMEOUT * 500, 30000); // milliseconds
|
|
538
|
+
log(\`Idle timeout: \${IDLE_TIMEOUT}s (will auto-shutdown when idle)\`);
|
|
539
|
+
idleCheckTimer = setInterval(checkIdleAndExit, checkInterval);
|
|
540
|
+
} else {
|
|
541
|
+
log('Idle timeout: disabled (will run forever)');
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
process.on('SIGTERM', () => { cleanup(); server.close(); process.exit(0); });
|
|
546
|
+
process.on('SIGINT', () => { cleanup(); server.close(); process.exit(0); });
|
|
547
|
+
`;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Generate unified Wrapper script
|
|
551
|
+
* Uses shared proxy server mode:
|
|
552
|
+
* - All droid instances share the same proxy process
|
|
553
|
+
* - Proxy starts automatically if not running
|
|
554
|
+
* - Proxy runs as background daemon, doesn't exit with droid
|
|
555
|
+
*/
|
|
556
|
+
function generateUnifiedWrapper(droidPath, proxyScriptPath) {
|
|
557
|
+
return `#!/bin/bash
|
|
558
|
+
# Droid with WebSearch
|
|
559
|
+
# Auto-generated by droid-patch --websearch
|
|
560
|
+
|
|
561
|
+
PROXY_SCRIPT="${proxyScriptPath}"
|
|
562
|
+
DROID_BIN="${droidPath}"
|
|
563
|
+
PORT=23119
|
|
564
|
+
PID_FILE="/tmp/droid-search-proxy.pid"
|
|
565
|
+
LOG_FILE="/tmp/droid-search-proxy.log"
|
|
566
|
+
|
|
567
|
+
# Check if proxy is running
|
|
568
|
+
is_proxy_running() {
|
|
569
|
+
if [ -f "$PID_FILE" ]; then
|
|
570
|
+
local pid
|
|
571
|
+
pid=$(cat "$PID_FILE")
|
|
572
|
+
if kill -0 "$pid" 2>/dev/null; then
|
|
573
|
+
# Process exists, check if port responds
|
|
574
|
+
if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
|
|
575
|
+
return 0
|
|
576
|
+
fi
|
|
577
|
+
fi
|
|
578
|
+
# PID file exists but process doesn't exist or port not responding, cleanup
|
|
579
|
+
rm -f "$PID_FILE"
|
|
580
|
+
fi
|
|
581
|
+
return 1
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
# Start shared proxy
|
|
585
|
+
start_shared_proxy() {
|
|
586
|
+
# First check if port is occupied by another program
|
|
587
|
+
if lsof -i:"$PORT" > /dev/null 2>&1; then
|
|
588
|
+
# Port is occupied, check if it's our proxy
|
|
589
|
+
if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
|
|
590
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy already running on port $PORT" >&2
|
|
591
|
+
return 0
|
|
592
|
+
else
|
|
593
|
+
echo "[websearch] Port $PORT is occupied by another process" >&2
|
|
594
|
+
return 1
|
|
595
|
+
fi
|
|
596
|
+
fi
|
|
597
|
+
|
|
598
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting shared proxy on port $PORT..." >&2
|
|
599
|
+
|
|
600
|
+
# Start proxy as background daemon
|
|
601
|
+
SEARCH_PROXY_PORT="$PORT" nohup node "$PROXY_SCRIPT" >> "$LOG_FILE" 2>&1 &
|
|
602
|
+
echo $! > "$PID_FILE"
|
|
603
|
+
|
|
604
|
+
# Wait for proxy to start
|
|
605
|
+
for i in {1..30}; do
|
|
606
|
+
if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
|
|
607
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port $PORT (PID: $(cat $PID_FILE))" >&2
|
|
608
|
+
return 0
|
|
609
|
+
fi
|
|
610
|
+
sleep 0.1
|
|
611
|
+
done
|
|
612
|
+
|
|
613
|
+
echo "[websearch] Failed to start proxy" >&2
|
|
614
|
+
rm -f "$PID_FILE"
|
|
615
|
+
return 1
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
# Ensure proxy is running
|
|
619
|
+
ensure_proxy() {
|
|
620
|
+
if is_proxy_running; then
|
|
621
|
+
[ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Using existing proxy on port $PORT" >&2
|
|
622
|
+
return 0
|
|
623
|
+
fi
|
|
624
|
+
start_shared_proxy
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
# Start/reuse proxy
|
|
628
|
+
if ! ensure_proxy; then
|
|
629
|
+
echo "[websearch] Running without search proxy" >&2
|
|
630
|
+
exec "$DROID_BIN" "$@"
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
# Run droid, set API to point to local proxy
|
|
634
|
+
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$PORT"
|
|
635
|
+
exec "$DROID_BIN" "$@"
|
|
636
|
+
`;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Create unified WebSearch files
|
|
640
|
+
*
|
|
641
|
+
* Approach: Proxy server mode
|
|
642
|
+
* - wrapper script starts local proxy server
|
|
643
|
+
* - proxy server intercepts search requests, passes through other requests
|
|
644
|
+
* - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy
|
|
645
|
+
* - alias works directly, no extra steps needed
|
|
646
|
+
*/
|
|
647
|
+
async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName) {
|
|
648
|
+
if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
|
|
649
|
+
const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
|
|
650
|
+
const wrapperScriptPath = join(outputDir, aliasName);
|
|
651
|
+
await writeFile(proxyScriptPath, generateSearchProxyServer());
|
|
652
|
+
console.log(`[*] Created proxy script: ${proxyScriptPath}`);
|
|
653
|
+
await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath));
|
|
654
|
+
await chmod(wrapperScriptPath, 493);
|
|
655
|
+
console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
|
|
656
|
+
return {
|
|
657
|
+
wrapperScript: wrapperScriptPath,
|
|
658
|
+
preloadScript: proxyScriptPath
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
//#endregion
|
|
10
663
|
//#region src/cli.ts
|
|
11
664
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
665
|
function getVersion() {
|
|
@@ -29,26 +682,70 @@ function findDefaultDroidPath() {
|
|
|
29
682
|
for (const p of paths) if (existsSync(p)) return p;
|
|
30
683
|
return join(home, ".droid/bin/droid");
|
|
31
684
|
}
|
|
32
|
-
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("--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) => {
|
|
685
|
+
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 Factory API base URL (https://api.factory.ai) with custom URL").option("--websearch", "Enable local WebSearch via fetch hook (Google PSE + DuckDuckGo fallback)").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) => {
|
|
33
686
|
const alias = args?.[0];
|
|
34
687
|
const isCustom = options["is-custom"];
|
|
35
688
|
const skipLogin = options["skip-login"];
|
|
689
|
+
const apiBase = options["api-base"];
|
|
690
|
+
const webSearch = options["websearch"];
|
|
36
691
|
const dryRun = options["dry-run"];
|
|
37
692
|
const path = options.path || findDefaultDroidPath();
|
|
38
693
|
const outputDir = options.output;
|
|
39
694
|
const backup = options.backup !== false;
|
|
40
695
|
const verbose = options.verbose;
|
|
41
696
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
42
|
-
if (!isCustom && !skipLogin) {
|
|
697
|
+
if (webSearch && !isCustom && !skipLogin && !apiBase) {
|
|
698
|
+
if (!alias) {
|
|
699
|
+
console.log(styleText("red", "Error: Alias name required for --websearch"));
|
|
700
|
+
console.log(styleText("gray", "Usage: npx droid-patch --websearch <alias>"));
|
|
701
|
+
process.exit(1);
|
|
702
|
+
}
|
|
703
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
704
|
+
console.log(styleText(["cyan", "bold"], " Droid WebSearch Setup"));
|
|
705
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
706
|
+
console.log();
|
|
707
|
+
const websearchDir = join(homedir(), ".droid-patch", "websearch");
|
|
708
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, path, alias);
|
|
709
|
+
await createAliasForWrapper(wrapperScript, alias, verbose);
|
|
710
|
+
console.log();
|
|
711
|
+
console.log(styleText("green", "═".repeat(60)));
|
|
712
|
+
console.log(styleText(["green", "bold"], " WebSearch Ready!"));
|
|
713
|
+
console.log(styleText("green", "═".repeat(60)));
|
|
714
|
+
console.log();
|
|
715
|
+
console.log("Run directly:");
|
|
716
|
+
console.log(styleText("yellow", ` ${alias}`));
|
|
717
|
+
console.log();
|
|
718
|
+
console.log(styleText("cyan", "Auto-shutdown:"));
|
|
719
|
+
console.log(styleText("gray", " Proxy auto-shuts down after 5 min idle (no manual cleanup needed)"));
|
|
720
|
+
console.log(styleText("gray", " To disable: export DROID_PROXY_IDLE_TIMEOUT=0"));
|
|
721
|
+
console.log();
|
|
722
|
+
console.log("Search providers (in priority order):");
|
|
723
|
+
console.log(styleText("yellow", " 1. Smithery Exa (best quality):"));
|
|
724
|
+
console.log(styleText("gray", " export SMITHERY_API_KEY=your_api_key"));
|
|
725
|
+
console.log(styleText("gray", " export SMITHERY_PROFILE=your_profile"));
|
|
726
|
+
console.log(styleText("gray", " 2. Google PSE:"));
|
|
727
|
+
console.log(styleText("gray", " export GOOGLE_PSE_API_KEY=your_api_key"));
|
|
728
|
+
console.log(styleText("gray", " export GOOGLE_PSE_CX=your_search_engine_id"));
|
|
729
|
+
console.log(styleText("gray", " 3-6. Serper, Brave, SearXNG, DuckDuckGo (fallbacks)"));
|
|
730
|
+
console.log();
|
|
731
|
+
console.log("Debug mode:");
|
|
732
|
+
console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1"));
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (!isCustom && !skipLogin && !apiBase && !webSearch) {
|
|
43
736
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
44
737
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
45
738
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
739
|
+
console.log(styleText("gray", " --api-base Replace Factory API URL with custom server"));
|
|
740
|
+
console.log(styleText("gray", " --websearch Enable local WebSearch (Google PSE + DuckDuckGo)"));
|
|
46
741
|
console.log();
|
|
47
742
|
console.log("Usage examples:");
|
|
48
743
|
console.log(styleText("cyan", " npx droid-patch --is-custom droid-custom"));
|
|
49
744
|
console.log(styleText("cyan", " npx droid-patch --skip-login droid-nologin"));
|
|
50
745
|
console.log(styleText("cyan", " npx droid-patch --is-custom --skip-login droid-patched"));
|
|
51
746
|
console.log(styleText("cyan", " npx droid-patch --skip-login -o . my-droid"));
|
|
747
|
+
console.log(styleText("cyan", " npx droid-patch --api-base http://localhost:3000 droid-local"));
|
|
748
|
+
console.log(styleText("cyan", " npx droid-patch --websearch droid-search"));
|
|
52
749
|
process.exit(1);
|
|
53
750
|
}
|
|
54
751
|
if (!alias && !dryRun) {
|
|
@@ -73,6 +770,29 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
73
770
|
pattern: Buffer.from("process.env.FACTORY_API_KEY"),
|
|
74
771
|
replacement: Buffer.from("\"fk-droid-patch-skip-00000\"")
|
|
75
772
|
});
|
|
773
|
+
if (apiBase) {
|
|
774
|
+
const originalUrl = "https://api.factory.ai";
|
|
775
|
+
const originalLength = originalUrl.length;
|
|
776
|
+
let normalizedUrl = apiBase.replace(/\/+$/, "");
|
|
777
|
+
if (normalizedUrl.length > originalLength) {
|
|
778
|
+
console.log(styleText("red", `Error: API base URL must be ${originalLength} characters or less`));
|
|
779
|
+
console.log(styleText("gray", ` Your URL: "${normalizedUrl}" (${normalizedUrl.length} chars)`));
|
|
780
|
+
console.log(styleText("gray", ` Maximum: ${originalLength} characters`));
|
|
781
|
+
console.log();
|
|
782
|
+
console.log(styleText("yellow", "Tip: Use a shorter URL or set up a local redirect."));
|
|
783
|
+
console.log(styleText("gray", " Examples:"));
|
|
784
|
+
console.log(styleText("gray", " http://127.0.0.1:3000 (19 chars)"));
|
|
785
|
+
console.log(styleText("gray", " http://localhost:80 (19 chars)"));
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
const paddedUrl = normalizedUrl.padEnd(originalLength, " ");
|
|
789
|
+
patches.push({
|
|
790
|
+
name: "apiBase",
|
|
791
|
+
description: `Replace Factory API URL with "${normalizedUrl}"`,
|
|
792
|
+
pattern: Buffer.from(originalUrl),
|
|
793
|
+
replacement: Buffer.from(paddedUrl)
|
|
794
|
+
});
|
|
795
|
+
}
|
|
76
796
|
try {
|
|
77
797
|
const result = await patchDroid({
|
|
78
798
|
inputPath: path,
|
|
@@ -103,7 +823,15 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
103
823
|
}
|
|
104
824
|
if (result.success && result.outputPath && alias) {
|
|
105
825
|
console.log();
|
|
106
|
-
|
|
826
|
+
if (webSearch) {
|
|
827
|
+
const websearchDir = join(homedir(), ".droid-patch", "websearch");
|
|
828
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, result.outputPath, alias);
|
|
829
|
+
await createAliasForWrapper(wrapperScript, alias, verbose);
|
|
830
|
+
console.log();
|
|
831
|
+
console.log("Optional: Set Google PSE for better results:");
|
|
832
|
+
console.log(styleText("gray", " export GOOGLE_PSE_API_KEY=your_api_key"));
|
|
833
|
+
console.log(styleText("gray", " export GOOGLE_PSE_CX=your_search_engine_id"));
|
|
834
|
+
} else await createAlias(result.outputPath, alias, verbose);
|
|
107
835
|
}
|
|
108
836
|
if (result.success) {
|
|
109
837
|
console.log();
|
|
@@ -122,9 +850,9 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
122
850
|
}).command("remove", "Remove a droid-patch alias or patched binary file").argument("<alias-or-path>", "Alias name or file path to remove").action(async (_options, args) => {
|
|
123
851
|
const target = args[0];
|
|
124
852
|
if (target.includes("/") || existsSync(target)) {
|
|
125
|
-
const { unlink } = await import("node:fs/promises");
|
|
853
|
+
const { unlink: unlink$1 } = await import("node:fs/promises");
|
|
126
854
|
try {
|
|
127
|
-
await unlink(target);
|
|
855
|
+
await unlink$1(target);
|
|
128
856
|
console.log(styleText("green", `[*] Removed: ${target}`));
|
|
129
857
|
} catch (error) {
|
|
130
858
|
console.error(styleText("red", `Error: ${error.message}`));
|
|
@@ -133,6 +861,86 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
133
861
|
} else await removeAlias(target);
|
|
134
862
|
}).command("version", "Print droid-patch version").action(() => {
|
|
135
863
|
console.log(`droid-patch v${version}`);
|
|
864
|
+
}).command("proxy-status", "Check websearch proxy status").action(async () => {
|
|
865
|
+
const pidFile = "/tmp/droid-search-proxy.pid";
|
|
866
|
+
const logFile = "/tmp/droid-search-proxy.log";
|
|
867
|
+
const port = 23119;
|
|
868
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
869
|
+
console.log(styleText(["cyan", "bold"], " WebSearch Proxy Status"));
|
|
870
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
871
|
+
console.log();
|
|
872
|
+
try {
|
|
873
|
+
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
|
874
|
+
if (response.ok) {
|
|
875
|
+
const data = await response.json();
|
|
876
|
+
console.log(styleText("green", ` Status: Running ✓`));
|
|
877
|
+
console.log(styleText("white", ` Port: ${port}`));
|
|
878
|
+
if (existsSync(pidFile)) {
|
|
879
|
+
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
880
|
+
const pid = readFileSync$1(pidFile, "utf-8").trim();
|
|
881
|
+
console.log(styleText("white", ` PID: ${pid}`));
|
|
882
|
+
}
|
|
883
|
+
if (data.droidRunning !== void 0) console.log(styleText("white", ` Droid running: ${data.droidRunning ? "yes (proxy will stay alive)" : "no"}`));
|
|
884
|
+
if (data.idleTimeout !== void 0) if (data.idleTimeout > 0) {
|
|
885
|
+
const idleMins = Math.floor((data.idleSeconds || 0) / 60);
|
|
886
|
+
const idleSecs = (data.idleSeconds || 0) % 60;
|
|
887
|
+
if (data.droidRunning) console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s (won't shutdown while droid runs)`));
|
|
888
|
+
else if (data.willShutdownIn !== null) {
|
|
889
|
+
const shutdownMins = Math.floor((data.willShutdownIn || 0) / 60);
|
|
890
|
+
const shutdownSecs = (data.willShutdownIn || 0) % 60;
|
|
891
|
+
console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s`));
|
|
892
|
+
console.log(styleText("white", ` Auto-shutdown in: ${shutdownMins}m ${shutdownSecs}s`));
|
|
893
|
+
}
|
|
894
|
+
} else console.log(styleText("white", ` Auto-shutdown: disabled`));
|
|
895
|
+
console.log(styleText("white", ` Log: ${logFile}`));
|
|
896
|
+
console.log();
|
|
897
|
+
console.log(styleText("gray", "To stop the proxy manually:"));
|
|
898
|
+
console.log(styleText("cyan", " npx droid-patch proxy-stop"));
|
|
899
|
+
console.log();
|
|
900
|
+
console.log(styleText("gray", "To disable auto-shutdown:"));
|
|
901
|
+
console.log(styleText("cyan", " export DROID_PROXY_IDLE_TIMEOUT=0"));
|
|
902
|
+
}
|
|
903
|
+
} catch {
|
|
904
|
+
console.log(styleText("yellow", ` Status: Not running`));
|
|
905
|
+
console.log();
|
|
906
|
+
console.log(styleText("gray", "The proxy will start automatically when you run droid-full."));
|
|
907
|
+
console.log(styleText("gray", "It will auto-shutdown after 5 minutes of idle (configurable)."));
|
|
908
|
+
}
|
|
909
|
+
console.log();
|
|
910
|
+
}).command("proxy-stop", "Stop the websearch proxy").action(async () => {
|
|
911
|
+
const pidFile = "/tmp/droid-search-proxy.pid";
|
|
912
|
+
if (!existsSync(pidFile)) {
|
|
913
|
+
console.log(styleText("yellow", "Proxy is not running (no PID file)"));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const { readFileSync: readFileSync$1, unlinkSync: unlinkSync$1 } = await import("node:fs");
|
|
918
|
+
const pid = readFileSync$1(pidFile, "utf-8").trim();
|
|
919
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
920
|
+
unlinkSync$1(pidFile);
|
|
921
|
+
console.log(styleText("green", `[*] Proxy stopped (PID: ${pid})`));
|
|
922
|
+
} catch (error) {
|
|
923
|
+
console.log(styleText("yellow", `[!] Could not stop proxy: ${error.message}`));
|
|
924
|
+
try {
|
|
925
|
+
const { unlinkSync: unlinkSync$1 } = await import("node:fs");
|
|
926
|
+
unlinkSync$1(pidFile);
|
|
927
|
+
console.log(styleText("gray", "Cleaned up stale PID file"));
|
|
928
|
+
} catch {}
|
|
929
|
+
}
|
|
930
|
+
}).command("proxy-log", "Show websearch proxy logs").action(async () => {
|
|
931
|
+
const logFile = "/tmp/droid-search-proxy.log";
|
|
932
|
+
if (!existsSync(logFile)) {
|
|
933
|
+
console.log(styleText("yellow", "No log file found"));
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const { readFileSync: readFileSync$1 } = await import("node:fs");
|
|
937
|
+
const log = readFileSync$1(logFile, "utf-8");
|
|
938
|
+
const lines = log.split("\n").slice(-50);
|
|
939
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
940
|
+
console.log(styleText(["cyan", "bold"], " WebSearch Proxy Logs (last 50 lines)"));
|
|
941
|
+
console.log(styleText("cyan", "═".repeat(60)));
|
|
942
|
+
console.log();
|
|
943
|
+
console.log(lines.join("\n"));
|
|
136
944
|
}).run().catch((err) => {
|
|
137
945
|
console.error(err);
|
|
138
946
|
process.exit(1);
|