droid-patch 0.15.2 → 0.16.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/dist/cli.mjs +456 -135
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -29,9 +29,14 @@ const fs = require('fs');
|
|
|
29
29
|
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
30
30
|
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
31
31
|
const FACTORY_API = 'https://api.factory.ai';
|
|
32
|
+
const SEARCH_ROUTE_ALIASES = new Set(['/api/tools/web-search', '/api/tools/exa/search']);
|
|
32
33
|
|
|
33
34
|
function log() { if (DEBUG) console.error.apply(console, ['[websearch]'].concat(Array.from(arguments))); }
|
|
34
35
|
|
|
36
|
+
function isSearchRequest(url, method) {
|
|
37
|
+
return method === 'POST' && SEARCH_ROUTE_ALIASES.has(url.pathname);
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
// === External Search Providers ===
|
|
36
41
|
|
|
37
42
|
async function searchSmitheryExa(query, numResults) {
|
|
@@ -204,7 +209,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
204
209
|
return;
|
|
205
210
|
}
|
|
206
211
|
|
|
207
|
-
if (url
|
|
212
|
+
if (isSearchRequest(url, req.method)) {
|
|
208
213
|
let body = '';
|
|
209
214
|
req.on('data', function(c) { body += c; });
|
|
210
215
|
req.on('end', async function() {
|
|
@@ -281,7 +286,6 @@ function generateNativeSearchProxyServer(factoryApiUrl = "https://api.factory.ai
|
|
|
281
286
|
|
|
282
287
|
const http = require('http');
|
|
283
288
|
const https = require('https');
|
|
284
|
-
const { execSync } = require('child_process');
|
|
285
289
|
const fs = require('fs');
|
|
286
290
|
const path = require('path');
|
|
287
291
|
const os = require('os');
|
|
@@ -289,13 +293,18 @@ const os = require('os');
|
|
|
289
293
|
const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
|
|
290
294
|
const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0');
|
|
291
295
|
const FACTORY_API = '${factoryApiUrl}';
|
|
296
|
+
const SEARCH_ROUTE_ALIASES = new Set(['/api/tools/web-search', '/api/tools/exa/search']);
|
|
297
|
+
const SUPPORTED_PROVIDERS = new Set(['anthropic', 'openai']);
|
|
292
298
|
|
|
293
299
|
function log(...args) { if (DEBUG) console.error('[websearch]', ...args); }
|
|
294
300
|
|
|
295
|
-
|
|
301
|
+
function isSearchRequest(url, method) {
|
|
302
|
+
return method === 'POST' && SEARCH_ROUTE_ALIASES.has(url.pathname);
|
|
303
|
+
}
|
|
296
304
|
|
|
297
305
|
let cachedSettings = null;
|
|
298
306
|
let settingsLastModified = 0;
|
|
307
|
+
let lastObservedProvider = null;
|
|
299
308
|
|
|
300
309
|
function getFactorySettings() {
|
|
301
310
|
const settingsPath = path.join(os.homedir(), '.factory', 'settings.json');
|
|
@@ -311,151 +320,409 @@ function getFactorySettings() {
|
|
|
311
320
|
}
|
|
312
321
|
}
|
|
313
322
|
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
323
|
+
function listCustomModels(settings) {
|
|
324
|
+
return Array.isArray(settings && settings.customModels) ? settings.customModels : [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function isSupportedModel(modelConfig, preferredProvider) {
|
|
328
|
+
return !!(
|
|
329
|
+
modelConfig &&
|
|
330
|
+
SUPPORTED_PROVIDERS.has(modelConfig.provider) &&
|
|
331
|
+
(!preferredProvider || modelConfig.provider === preferredProvider) &&
|
|
332
|
+
modelConfig.id &&
|
|
333
|
+
modelConfig.baseUrl &&
|
|
334
|
+
modelConfig.apiKey &&
|
|
335
|
+
modelConfig.model
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function buildCandidateModelIds(settings) {
|
|
340
|
+
const candidates = [
|
|
341
|
+
process.env.DROID_SEARCH_MODEL_ID,
|
|
342
|
+
settings && settings.sessionDefaultSettings && settings.sessionDefaultSettings.model,
|
|
343
|
+
settings && settings.missionModelSettings && settings.missionModelSettings.workerModel,
|
|
344
|
+
settings && settings.missionModelSettings && settings.missionModelSettings.validationWorkerModel,
|
|
345
|
+
settings && settings.missionModelSettings && settings.missionModelSettings.orchestratorModel,
|
|
346
|
+
];
|
|
347
|
+
const unique = [];
|
|
348
|
+
for (const candidate of candidates) {
|
|
349
|
+
if (candidate && !unique.includes(candidate)) unique.push(candidate);
|
|
327
350
|
}
|
|
328
|
-
|
|
329
|
-
if (!currentModelId.startsWith('custom:')) return null;
|
|
330
|
-
log('Model not found:', currentModelId);
|
|
331
|
-
return null;
|
|
351
|
+
return unique;
|
|
332
352
|
}
|
|
333
353
|
|
|
334
|
-
|
|
354
|
+
function summarizeSupportedModels(settings, preferredProvider) {
|
|
355
|
+
return listCustomModels(settings)
|
|
356
|
+
.filter(function(modelConfig) { return isSupportedModel(modelConfig, preferredProvider); })
|
|
357
|
+
.map(function(modelConfig) { return modelConfig.id + ' [' + modelConfig.provider + ']'; })
|
|
358
|
+
.join(', ');
|
|
359
|
+
}
|
|
335
360
|
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
max_tokens: 4096,
|
|
343
|
-
stream: false,
|
|
344
|
-
system: 'You are a web search assistant. Use the web_search tool to find relevant information and return the results.',
|
|
345
|
-
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 1 }],
|
|
346
|
-
tool_choice: { type: 'tool', name: 'web_search' },
|
|
347
|
-
messages: [{ role: 'user', content: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.' }]
|
|
361
|
+
function getCurrentModelConfig(preferredProvider) {
|
|
362
|
+
const settings = getFactorySettings();
|
|
363
|
+
if (!settings) {
|
|
364
|
+
return {
|
|
365
|
+
error: 'Failed to load ~/.factory/settings.json for native websearch',
|
|
366
|
+
statusCode: 500,
|
|
348
367
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const customModels = listCustomModels(settings);
|
|
371
|
+
const candidateIds = buildCandidateModelIds(settings);
|
|
372
|
+
for (const candidateId of candidateIds) {
|
|
373
|
+
const modelConfig = customModels.find(function(model) {
|
|
374
|
+
return model.id === candidateId && isSupportedModel(model, preferredProvider);
|
|
375
|
+
});
|
|
376
|
+
if (modelConfig) {
|
|
377
|
+
lastObservedProvider = modelConfig.provider;
|
|
378
|
+
log('Resolved model:', modelConfig.id, '| Provider:', modelConfig.provider);
|
|
379
|
+
return { modelConfig: modelConfig, source: candidateId };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const fallbackModels = customModels.filter(function(modelConfig) {
|
|
384
|
+
return isSupportedModel(modelConfig, preferredProvider);
|
|
385
|
+
});
|
|
386
|
+
if (fallbackModels.length === 1) {
|
|
387
|
+
lastObservedProvider = fallbackModels[0].provider;
|
|
388
|
+
log('Falling back to only supported model:', fallbackModels[0].id);
|
|
389
|
+
return { modelConfig: fallbackModels[0], source: 'single-supported-model' };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const providerLabel = preferredProvider || 'anthropic/openai';
|
|
393
|
+
const supported = summarizeSupportedModels(settings, preferredProvider);
|
|
394
|
+
const message = supported
|
|
395
|
+
? 'Could not resolve an active ' + providerLabel + ' custom model for native websearch. Available models: ' + supported
|
|
396
|
+
: 'No supported ' + providerLabel + ' custom models found in ~/.factory/settings.json';
|
|
397
|
+
|
|
398
|
+
return { error: message, statusCode: 400 };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function normalizeEndpoint(baseUrl, suffix) {
|
|
402
|
+
return baseUrl.endsWith(suffix) ? baseUrl : baseUrl.replace(/\\/$/, '') + suffix;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function extractErrorMessage(value) {
|
|
406
|
+
if (!value) return 'Unknown upstream error';
|
|
407
|
+
if (typeof value === 'string') return value;
|
|
408
|
+
if (typeof value.message === 'string') return value.message;
|
|
409
|
+
return JSON.stringify(value);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function sleep(ms) {
|
|
413
|
+
return new Promise(function(resolve) { setTimeout(resolve, ms); });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function getRetryDelayMs(attempt) {
|
|
417
|
+
return Math.min(250 * Math.pow(2, attempt - 1), 2000);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isRetryableSearchFailure(statusCode, message) {
|
|
421
|
+
if (statusCode === 408 || statusCode === 425 || statusCode === 429) return true;
|
|
422
|
+
if (statusCode >= 500 && statusCode <= 599) return true;
|
|
423
|
+
|
|
424
|
+
const normalized = String(message || '').toLowerCase();
|
|
425
|
+
return normalized.includes('proxy failed:') ||
|
|
426
|
+
normalized.includes('client network socket disconnected before secure tls connection was established') ||
|
|
427
|
+
normalized.includes('fetch failed') ||
|
|
428
|
+
normalized.includes('socket hang up') ||
|
|
429
|
+
normalized.includes('request timed out') ||
|
|
430
|
+
normalized.includes('timed out') ||
|
|
431
|
+
normalized.includes('econnreset') ||
|
|
432
|
+
normalized.includes('ecconnreset') ||
|
|
433
|
+
normalized.includes('econnrefused') ||
|
|
434
|
+
normalized.includes('ehostunreach') ||
|
|
435
|
+
normalized.includes('enotfound') ||
|
|
436
|
+
normalized.includes('eai_again');
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function pushUniqueResult(results, result) {
|
|
440
|
+
if (!result || !result.url) return;
|
|
441
|
+
if (results.some(function(existing) { return existing.url === result.url; })) return;
|
|
442
|
+
results.push({
|
|
443
|
+
title: result.title || result.url,
|
|
444
|
+
url: result.url,
|
|
445
|
+
content: result.content || '',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parseOpenAITextResults(text, numResults) {
|
|
450
|
+
const results = [];
|
|
451
|
+
const lines = String(text || '').split(/\\r?\\n/);
|
|
452
|
+
let current = null;
|
|
453
|
+
|
|
454
|
+
function flushCurrent() {
|
|
455
|
+
if (!current || !current.url) {
|
|
456
|
+
current = null;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
pushUniqueResult(results, {
|
|
460
|
+
title: current.title,
|
|
461
|
+
url: current.url,
|
|
462
|
+
content: current.content.join(' ').trim(),
|
|
463
|
+
});
|
|
464
|
+
current = null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
for (const rawLine of lines) {
|
|
468
|
+
const line = rawLine.trim();
|
|
469
|
+
if (!line) continue;
|
|
470
|
+
|
|
471
|
+
const titleMatch = line.match(/^\\d+\\.\\s+(?:\\*\\*(.+?)\\*\\*|(.+))$/);
|
|
472
|
+
if (titleMatch) {
|
|
473
|
+
flushCurrent();
|
|
474
|
+
current = {
|
|
475
|
+
title: (titleMatch[1] || titleMatch[2] || '').trim(),
|
|
476
|
+
url: '',
|
|
477
|
+
content: [],
|
|
478
|
+
};
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const urlMatch = line.match(/^(?:[-*]\\s+)?(https?:\\/\\/\\S+)/);
|
|
483
|
+
if (urlMatch) {
|
|
484
|
+
if (!current) {
|
|
485
|
+
current = { title: urlMatch[1], url: '', content: [] };
|
|
486
|
+
}
|
|
487
|
+
current.url = urlMatch[1].replace(/[),.;]+$/, '');
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const bulletTextMatch = line.match(/^[-*]\\s+(.+)/);
|
|
492
|
+
const contentText = (bulletTextMatch ? bulletTextMatch[1] : line).trim();
|
|
493
|
+
if (!current) {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (contentText) {
|
|
497
|
+
current.content.push(contentText);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
flushCurrent();
|
|
502
|
+
return results.slice(0, numResults);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function postJson(endpoint, headers, requestBody) {
|
|
506
|
+
const maxAttempts = 5;
|
|
507
|
+
let lastError = null;
|
|
508
|
+
|
|
509
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
510
|
+
const controller = new AbortController();
|
|
511
|
+
const timeoutId = setTimeout(function() { controller.abort(); }, 60000);
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
const response = await fetch(endpoint, {
|
|
515
|
+
method: 'POST',
|
|
516
|
+
headers: headers,
|
|
517
|
+
body: JSON.stringify(requestBody),
|
|
518
|
+
signal: controller.signal,
|
|
519
|
+
});
|
|
520
|
+
const responseText = await response.text();
|
|
521
|
+
let payload = {};
|
|
522
|
+
if (responseText) {
|
|
523
|
+
try {
|
|
524
|
+
payload = JSON.parse(responseText);
|
|
525
|
+
} catch {
|
|
526
|
+
throw new Error('Invalid JSON response from ' + endpoint);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!response.ok) {
|
|
531
|
+
const message = extractErrorMessage(payload && payload.error) || ('HTTP ' + response.status);
|
|
532
|
+
if (attempt < maxAttempts && isRetryableSearchFailure(response.status, message)) {
|
|
533
|
+
lastError = new Error(message);
|
|
534
|
+
const delayMs = getRetryDelayMs(attempt);
|
|
535
|
+
log('Retrying request after upstream error:', response.status, message, '| next attempt', attempt + 1, 'of', maxAttempts);
|
|
536
|
+
await sleep(delayMs);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
throw new Error(message);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (payload && payload.error) {
|
|
543
|
+
const message = extractErrorMessage(payload.error);
|
|
544
|
+
if (attempt < maxAttempts && isRetryableSearchFailure(undefined, message)) {
|
|
545
|
+
lastError = new Error(message);
|
|
546
|
+
const delayMs = getRetryDelayMs(attempt);
|
|
547
|
+
log('Retrying request after payload error:', message, '| next attempt', attempt + 1, 'of', maxAttempts);
|
|
548
|
+
await sleep(delayMs);
|
|
549
|
+
continue;
|
|
374
550
|
}
|
|
551
|
+
throw new Error(message);
|
|
375
552
|
}
|
|
553
|
+
|
|
554
|
+
return payload;
|
|
555
|
+
} catch (e) {
|
|
556
|
+
const message = e && e.name === 'AbortError' ? 'Request timed out' : (e && e.message ? e.message : String(e));
|
|
557
|
+
if (attempt < maxAttempts && isRetryableSearchFailure(undefined, message)) {
|
|
558
|
+
lastError = new Error(message);
|
|
559
|
+
const delayMs = getRetryDelayMs(attempt);
|
|
560
|
+
log('Retrying request after transport error:', message, '| next attempt', attempt + 1, 'of', maxAttempts);
|
|
561
|
+
await sleep(delayMs);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
throw new Error(message);
|
|
565
|
+
} finally {
|
|
566
|
+
clearTimeout(timeoutId);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
throw lastError || new Error('Request failed');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
async function searchAnthropicNative(query, numResults, modelConfig) {
|
|
574
|
+
const endpoint = normalizeEndpoint(modelConfig.baseUrl, '/v1/messages');
|
|
575
|
+
const requestBody = {
|
|
576
|
+
model: modelConfig.model,
|
|
577
|
+
max_tokens: 4096,
|
|
578
|
+
stream: false,
|
|
579
|
+
system: 'You are a web search assistant. Use the web_search tool to find relevant information and return the results.',
|
|
580
|
+
tools: [{ type: 'web_search_20250305', name: 'web_search', max_uses: 1 }],
|
|
581
|
+
tool_choice: { type: 'tool', name: 'web_search' },
|
|
582
|
+
messages: [{ role: 'user', content: 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.' }],
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
log('Anthropic search:', query, '→', endpoint);
|
|
586
|
+
const response = await postJson(endpoint, {
|
|
587
|
+
'Content-Type': 'application/json',
|
|
588
|
+
'anthropic-version': '2023-06-01',
|
|
589
|
+
'x-api-key': modelConfig.apiKey,
|
|
590
|
+
}, requestBody);
|
|
591
|
+
|
|
592
|
+
const results = [];
|
|
593
|
+
for (const block of (response.content || [])) {
|
|
594
|
+
if (block.type !== 'web_search_tool_result') continue;
|
|
595
|
+
for (const result of (block.content || [])) {
|
|
596
|
+
if (result.type !== 'web_search_result') continue;
|
|
597
|
+
results.push({
|
|
598
|
+
title: result.title || '',
|
|
599
|
+
url: result.url || '',
|
|
600
|
+
content: result.snippet || result.page_content || '',
|
|
601
|
+
});
|
|
376
602
|
}
|
|
377
|
-
|
|
378
|
-
log('Results:', results.length);
|
|
379
|
-
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
380
|
-
} catch (e) {
|
|
381
|
-
log('Anthropic error:', e.message);
|
|
382
|
-
return null;
|
|
383
603
|
}
|
|
604
|
+
|
|
605
|
+
log('Anthropic results:', results.length);
|
|
606
|
+
return results.slice(0, numResults);
|
|
384
607
|
}
|
|
385
608
|
|
|
386
609
|
async function searchOpenAINative(query, numResults, modelConfig) {
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
610
|
+
const endpoint = normalizeEndpoint(modelConfig.baseUrl, '/responses');
|
|
611
|
+
const input = 'Search the web for: ' + query + '\\n\\nReturn up to ' + numResults + ' relevant results.';
|
|
612
|
+
const requestVariants = [
|
|
613
|
+
{
|
|
614
|
+
label: 'web_search',
|
|
615
|
+
body: {
|
|
616
|
+
model: modelConfig.model,
|
|
617
|
+
stream: false,
|
|
618
|
+
tools: [{ type: 'web_search' }],
|
|
619
|
+
tool_choice: 'required',
|
|
620
|
+
input: input,
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
label: 'web_search_preview',
|
|
625
|
+
body: {
|
|
626
|
+
model: modelConfig.model,
|
|
627
|
+
stream: false,
|
|
628
|
+
tools: [{ type: 'web_search_preview' }],
|
|
629
|
+
input: input,
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
let lastError = null;
|
|
635
|
+
for (const variant of requestVariants) {
|
|
636
|
+
try {
|
|
637
|
+
log('OpenAI search:', query, '→', endpoint, '(' + variant.label + ')');
|
|
638
|
+
const response = await postJson(endpoint, {
|
|
639
|
+
'Content-Type': 'application/json',
|
|
640
|
+
Authorization: 'Bearer ' + modelConfig.apiKey,
|
|
641
|
+
}, variant.body);
|
|
642
|
+
|
|
643
|
+
const results = [];
|
|
644
|
+
const textBlocks = [];
|
|
645
|
+
for (const item of (response.output || [])) {
|
|
646
|
+
if (item.type !== 'message' || !Array.isArray(item.content)) continue;
|
|
416
647
|
for (const content of item.content) {
|
|
417
|
-
if (content.type
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
results.push({
|
|
421
|
-
title: annotation.title || '',
|
|
422
|
-
url: annotation.url || '',
|
|
423
|
-
content: annotation.title || ''
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
648
|
+
if (content.type !== 'output_text') continue;
|
|
649
|
+
if (content.text) {
|
|
650
|
+
textBlocks.push(content.text);
|
|
427
651
|
}
|
|
652
|
+
if (!Array.isArray(content.annotations)) continue;
|
|
653
|
+
for (const annotation of content.annotations) {
|
|
654
|
+
if (annotation.type !== 'url_citation' || !annotation.url) continue;
|
|
655
|
+
pushUniqueResult(results, {
|
|
656
|
+
title: annotation.title || '',
|
|
657
|
+
url: annotation.url || '',
|
|
658
|
+
content: annotation.title || '',
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (results.length === 0) {
|
|
665
|
+
for (const textBlock of textBlocks) {
|
|
666
|
+
for (const parsedResult of parseOpenAITextResults(textBlock, numResults)) {
|
|
667
|
+
pushUniqueResult(results, parsedResult);
|
|
668
|
+
}
|
|
669
|
+
if (results.length >= numResults) break;
|
|
428
670
|
}
|
|
429
671
|
}
|
|
672
|
+
|
|
673
|
+
log('OpenAI results:', results.length, 'via', variant.label);
|
|
674
|
+
return results.slice(0, numResults);
|
|
675
|
+
} catch (e) {
|
|
676
|
+
lastError = e;
|
|
677
|
+
log('OpenAI variant failed:', variant.label, '-', e.message);
|
|
430
678
|
}
|
|
431
|
-
|
|
432
|
-
log('Results:', results.length);
|
|
433
|
-
return results.length > 0 ? results.slice(0, numResults) : null;
|
|
434
|
-
} catch (e) {
|
|
435
|
-
log('OpenAI error:', e.message);
|
|
436
|
-
return null;
|
|
437
679
|
}
|
|
680
|
+
|
|
681
|
+
throw lastError || new Error('OpenAI web search failed');
|
|
438
682
|
}
|
|
439
683
|
|
|
440
684
|
async function search(query, numResults) {
|
|
441
685
|
numResults = numResults || 10;
|
|
442
686
|
log('Search:', query);
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
if (!modelConfig) {
|
|
446
|
-
|
|
447
|
-
|
|
687
|
+
|
|
688
|
+
const resolved = getCurrentModelConfig(lastObservedProvider);
|
|
689
|
+
if (!resolved.modelConfig) {
|
|
690
|
+
return {
|
|
691
|
+
results: [],
|
|
692
|
+
source: 'none',
|
|
693
|
+
error: resolved.error,
|
|
694
|
+
statusCode: resolved.statusCode || 400,
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
let results = [];
|
|
700
|
+
if (resolved.modelConfig.provider === 'anthropic') {
|
|
701
|
+
results = await searchAnthropicNative(query, numResults, resolved.modelConfig);
|
|
702
|
+
} else if (resolved.modelConfig.provider === 'openai') {
|
|
703
|
+
results = await searchOpenAINative(query, numResults, resolved.modelConfig);
|
|
704
|
+
} else {
|
|
705
|
+
return {
|
|
706
|
+
results: [],
|
|
707
|
+
source: 'none',
|
|
708
|
+
error: 'Unsupported provider: ' + resolved.modelConfig.provider,
|
|
709
|
+
statusCode: 400,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return {
|
|
714
|
+
results: results,
|
|
715
|
+
source: 'native-' + resolved.modelConfig.provider,
|
|
716
|
+
modelId: resolved.modelConfig.id,
|
|
717
|
+
};
|
|
718
|
+
} catch (e) {
|
|
719
|
+
return {
|
|
720
|
+
results: [],
|
|
721
|
+
source: 'none',
|
|
722
|
+
error: e && e.message ? e.message : String(e),
|
|
723
|
+
statusCode: 502,
|
|
724
|
+
};
|
|
448
725
|
}
|
|
449
|
-
|
|
450
|
-
const provider = modelConfig.provider;
|
|
451
|
-
let results = null;
|
|
452
|
-
|
|
453
|
-
if (provider === 'anthropic') results = await searchAnthropicNative(query, numResults, modelConfig);
|
|
454
|
-
else if (provider === 'openai') results = await searchOpenAINative(query, numResults, modelConfig);
|
|
455
|
-
else log('Unsupported provider:', provider);
|
|
456
|
-
|
|
457
|
-
if (results && results.length > 0) return { results: results, source: 'native-' + provider };
|
|
458
|
-
return { results: [], source: 'none' };
|
|
459
726
|
}
|
|
460
727
|
|
|
461
728
|
// === HTTP Proxy Server ===
|
|
@@ -469,14 +736,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
469
736
|
return;
|
|
470
737
|
}
|
|
471
738
|
|
|
472
|
-
if (url
|
|
739
|
+
if (isSearchRequest(url, req.method)) {
|
|
473
740
|
let body = '';
|
|
474
741
|
req.on('data', function(c) { body += c; });
|
|
475
742
|
req.on('end', async function() {
|
|
476
743
|
try {
|
|
477
744
|
const parsed = JSON.parse(body);
|
|
478
745
|
const result = await search(parsed.query, parsed.numResults || 10);
|
|
479
|
-
|
|
746
|
+
if (result.error) {
|
|
747
|
+
log('Search failed:', result.error);
|
|
748
|
+
res.writeHead(result.statusCode || 500, { 'Content-Type': 'application/json' });
|
|
749
|
+
res.end(JSON.stringify({ error: result.error, results: [] }));
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
log('Results:', result.results.length, 'from', result.source, 'model', result.modelId || 'unknown');
|
|
480
753
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
481
754
|
res.end(JSON.stringify({ results: result.results }));
|
|
482
755
|
} catch (e) {
|
|
@@ -511,6 +784,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
511
784
|
}
|
|
512
785
|
|
|
513
786
|
// Simple proxy - no SSE transformation (handled by proxy plugin)
|
|
787
|
+
if (url.pathname.startsWith('/api/llm/a/')) lastObservedProvider = 'anthropic';
|
|
788
|
+
if (url.pathname.startsWith('/api/llm/o/')) lastObservedProvider = 'openai';
|
|
514
789
|
log('Proxy:', req.method, url.pathname);
|
|
515
790
|
const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
|
|
516
791
|
const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
|
|
@@ -571,7 +846,7 @@ should_passthrough() {
|
|
|
571
846
|
for arg in "$@"; do
|
|
572
847
|
if [ "$arg" = "--" ]; then end_opts=1; continue; fi
|
|
573
848
|
if [ "$end_opts" -eq 0 ] && [[ "$arg" == -* ]]; then continue; fi
|
|
574
|
-
case "$arg" in help|version|completion|completions|
|
|
849
|
+
case "$arg" in help|version|completion|completions|plugin) return 0 ;; esac
|
|
575
850
|
break
|
|
576
851
|
done
|
|
577
852
|
return 1
|
|
@@ -625,6 +900,9 @@ rm -f "$PORT_FILE"
|
|
|
625
900
|
|
|
626
901
|
export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$ACTUAL_PORT"
|
|
627
902
|
export FACTORY_API_BASE_URL="http://127.0.0.1:$ACTUAL_PORT"
|
|
903
|
+
export FACTORY_APP_BASE_URL_OVERRIDE="http://127.0.0.1:$ACTUAL_PORT"
|
|
904
|
+
export FACTORY_APP_BASE_URL="http://127.0.0.1:$ACTUAL_PORT"
|
|
905
|
+
export TOOLS_WEBSEARCH_BASE_URL="http://127.0.0.1:$ACTUAL_PORT"
|
|
628
906
|
"$DROID_BIN" "$@"
|
|
629
907
|
DROID_EXIT_CODE=$?
|
|
630
908
|
exit $DROID_EXIT_CODE
|
|
@@ -654,7 +932,6 @@ for %%a in (%*) do (
|
|
|
654
932
|
if "%%a"=="version" set "PASSTHROUGH=1"
|
|
655
933
|
if "%%a"=="completion" set "PASSTHROUGH=1"
|
|
656
934
|
if "%%a"=="completions" set "PASSTHROUGH=1"
|
|
657
|
-
if "%%a"=="exec" set "PASSTHROUGH=1"
|
|
658
935
|
if "%%a"=="plugin" set "PASSTHROUGH=1"
|
|
659
936
|
)
|
|
660
937
|
if "%PASSTHROUGH%"=="1" (
|
|
@@ -708,6 +985,9 @@ del "%PORT_FILE%" 2>nul
|
|
|
708
985
|
|
|
709
986
|
set "FACTORY_API_BASE_URL_OVERRIDE=http://127.0.0.1:%ACTUAL_PORT%"
|
|
710
987
|
set "FACTORY_API_BASE_URL=http://127.0.0.1:%ACTUAL_PORT%"
|
|
988
|
+
set "FACTORY_APP_BASE_URL_OVERRIDE=http://127.0.0.1:%ACTUAL_PORT%"
|
|
989
|
+
set "FACTORY_APP_BASE_URL=http://127.0.0.1:%ACTUAL_PORT%"
|
|
990
|
+
set "TOOLS_WEBSEARCH_BASE_URL=http://127.0.0.1:%ACTUAL_PORT%"
|
|
711
991
|
"%DROID_BIN%" %*
|
|
712
992
|
set "DROID_EXIT_CODE=%ERRORLEVEL%"
|
|
713
993
|
|
|
@@ -1129,6 +1409,40 @@ const SKIP_LOGIN_PATCH_RULES = [{
|
|
|
1129
1409
|
alreadyPatchedRegexPattern: SKIP_LOGIN_V068_PLUS_PATCHED_REGEX
|
|
1130
1410
|
}]
|
|
1131
1411
|
}];
|
|
1412
|
+
const FACTORYD_SELF_PATH_REGEX = /(function ([A-Za-z$_][A-Za-z0-9$_]*)\(([A-Za-z$_][A-Za-z0-9$_]*)\)\{if\()([A-Za-z$_][A-Za-z0-9$_]*)\.basename\(process\.execPath\)\.includes\("droid"\)(\)return process\.execPath;return \3\?"droid-dev":"droid"\})/g;
|
|
1413
|
+
const FACTORYD_SELF_PATH_PATCHED_REGEX = /function ([A-Za-z$_][A-Za-z0-9$_]*)\(([A-Za-z$_][A-Za-z0-9$_]*)\)\{if\(\(1\|\|([A-Za-z$_][A-Za-z0-9$_]*)\.basename\(process\.execPath\)\.includes\(""\)\)\)return process\.execPath;return \2\?"droid-dev":"droid"\}/g;
|
|
1414
|
+
const FACTORYD_SKIP_LOGIN_AUTH_REGEX = /async function ([A-Za-z$_][A-Za-z0-9$_]*)\(([A-Za-z$_][A-Za-z0-9$_]*)\)\{let ([A-Za-z$_][A-Za-z0-9$_]*)=([A-Za-z$_][A-Za-z0-9$_]*)\(\)\.apiBaseUrl,([A-Za-z$_][A-Za-z0-9$_]*)=await fetch\(`\$\{\3\}\/api\/cli\/whoami`,\{method:"GET",headers:\{Authorization:`Bearer \$\{\2\}`\}\}\),([A-Za-z$_][A-Za-z0-9$_]*)=await \5\.text\(\);if\(!\5\.ok\)throw new ([A-Za-z$_][A-Za-z0-9$_]*)\("API key verification failed",\{statusCode:\5\.status,body:\6\}\);let ([A-Za-z$_][A-Za-z0-9$_]*)=([A-Za-z$_][A-Za-z0-9$_]*)\(\6,([A-Za-z$_][A-Za-z0-9$_]*),"whoami response"\);return\{userId:\8\.userId,email:"",orgId:\8\.orgId\}\}/g;
|
|
1415
|
+
const FACTORYD_SKIP_LOGIN_AUTH_PATCHED_REGEX = /async function [A-Za-z$_][A-Za-z0-9$_]*\(([A-Za-z$_][A-Za-z0-9$_]*)\)\{if\(\/\^fk\/\.test\(\1\)\)return\{userId:"f",orgId:"f"\};let ([A-Za-z$_][A-Za-z0-9$_]*)=await fetch\(`\$\{([A-Za-z$_][A-Za-z0-9$_]*)\(\)\.apiBaseUrl\}\/api\/cli\/whoami`,\{headers:\{Authorization:`Bearer \$\{\1\}`\}\}\);if\(!\2\.ok\)throw new [A-Za-z$_][A-Za-z0-9$_]*\("API key verification failed"\);\2=[A-Za-z$_][A-Za-z0-9$_]*\(await \2\.text\(\),([A-Za-z$_][A-Za-z0-9$_]*),"whoami response"\);return\{userId:\2\.userId,email:"",orgId:\2\.orgId\}\s+\}/g;
|
|
1416
|
+
function createFactorydSelfPathPatch() {
|
|
1417
|
+
return {
|
|
1418
|
+
name: "factorydSelfPath",
|
|
1419
|
+
description: "Force factoryd auto-start to reuse the current executable instead of falling back to plain droid",
|
|
1420
|
+
pattern: Buffer.from(""),
|
|
1421
|
+
replacement: Buffer.from(""),
|
|
1422
|
+
regexPattern: FACTORYD_SELF_PATH_REGEX,
|
|
1423
|
+
regexReplacement: "$1(1||$4.basename(process.execPath).includes(\"\"))$5",
|
|
1424
|
+
alreadyPatchedRegexPattern: FACTORYD_SELF_PATH_PATCHED_REGEX
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function createFactorydSkipLoginAuthPatch() {
|
|
1428
|
+
return {
|
|
1429
|
+
name: "factorydSkipLoginAuth",
|
|
1430
|
+
description: "Allow mission/factoryd auth to reuse fk- API key sessions via the shared /api/cli/whoami helper",
|
|
1431
|
+
pattern: Buffer.from(""),
|
|
1432
|
+
replacement: Buffer.from(""),
|
|
1433
|
+
regexPattern: FACTORYD_SKIP_LOGIN_AUTH_REGEX,
|
|
1434
|
+
regexReplacement: "async function $1($2){if(/^fk/.test($2))return{userId:\"f\",orgId:\"f\"};let $3=await fetch(`${$4().apiBaseUrl}/api/cli/whoami`,{headers:{Authorization:`Bearer ${$2}`}});if(!$3.ok)throw new $7(\"API key verification failed\");$3=$9(await $3.text(),$10,\"whoami response\");return{userId:$3.userId,email:\"\",orgId:$3.orgId} }",
|
|
1435
|
+
alreadyPatchedRegexPattern: FACTORYD_SKIP_LOGIN_AUTH_PATCHED_REGEX
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function needsBinaryPatches(config) {
|
|
1439
|
+
return config.isCustom || config.skipLogin || config.reasoningEffort || !!config.noTelemetry || !!config.apiBase && !config.websearch && !config.websearchProxy;
|
|
1440
|
+
}
|
|
1441
|
+
function createMissionFactorydPatches(config) {
|
|
1442
|
+
const patches = [createFactorydSelfPathPatch()];
|
|
1443
|
+
if (config.skipLogin) patches.push(createFactorydSkipLoginAuthPatch());
|
|
1444
|
+
return patches;
|
|
1445
|
+
}
|
|
1132
1446
|
function findDefaultDroidPath() {
|
|
1133
1447
|
const home = homedir();
|
|
1134
1448
|
if (IS_WINDOWS) {
|
|
@@ -1192,7 +1506,15 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
1192
1506
|
const verbose = options.verbose;
|
|
1193
1507
|
const droidVersion = getDroidVersion(path);
|
|
1194
1508
|
const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
|
|
1195
|
-
const needsBinaryPatch =
|
|
1509
|
+
const needsBinaryPatch = needsBinaryPatches({
|
|
1510
|
+
isCustom: !!isCustom,
|
|
1511
|
+
skipLogin: !!skipLogin,
|
|
1512
|
+
apiBase,
|
|
1513
|
+
websearch: !!websearch,
|
|
1514
|
+
websearchProxy: !!websearchProxy,
|
|
1515
|
+
reasoningEffort: !!reasoningEffort,
|
|
1516
|
+
noTelemetry: !!noTelemetry
|
|
1517
|
+
});
|
|
1196
1518
|
if (websearch && websearchProxy) {
|
|
1197
1519
|
console.log(styleText("red", "Error: Cannot use --websearch and --websearch-proxy together"));
|
|
1198
1520
|
console.log(styleText("gray", "Choose one:"));
|
|
@@ -1311,7 +1633,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
1311
1633
|
console.log(styleText(["cyan", "bold"], " Droid Binary Patcher"));
|
|
1312
1634
|
console.log(styleText("cyan", "═".repeat(60)));
|
|
1313
1635
|
console.log();
|
|
1314
|
-
const patches =
|
|
1636
|
+
const patches = createMissionFactorydPatches({ skipLogin });
|
|
1315
1637
|
if (isCustom) patches.push({
|
|
1316
1638
|
name: "isCustom",
|
|
1317
1639
|
description: "Change isCustom:!0 to isCustom:!1",
|
|
@@ -1458,8 +1780,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
1458
1780
|
console.log(styleText(["blue", "bold"], " DRY RUN COMPLETE"));
|
|
1459
1781
|
console.log(styleText("blue", "═".repeat(60)));
|
|
1460
1782
|
console.log();
|
|
1461
|
-
console.log(styleText("gray", "To apply the patches,
|
|
1462
|
-
console.log(styleText("cyan", ` npx droid-patch --is-custom ${alias || "<alias-name>"}`));
|
|
1783
|
+
console.log(styleText("gray", "To apply the patches, rerun the same command without --dry-run."));
|
|
1463
1784
|
process.exit(0);
|
|
1464
1785
|
}
|
|
1465
1786
|
if (outputDir && result.success && result.outputPath) {
|
|
@@ -1623,7 +1944,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
1623
1944
|
continue;
|
|
1624
1945
|
}
|
|
1625
1946
|
try {
|
|
1626
|
-
const patches = [];
|
|
1947
|
+
const patches = needsBinaryPatches(meta.patches) ? createMissionFactorydPatches({ skipLogin: meta.patches.skipLogin }) : [];
|
|
1627
1948
|
if (meta.patches.isCustom) patches.push({
|
|
1628
1949
|
name: "isCustom",
|
|
1629
1950
|
description: "Change isCustom:!0 to isCustom:!1",
|
|
@@ -1759,9 +2080,9 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
1759
2080
|
}
|
|
1760
2081
|
}
|
|
1761
2082
|
let execTargetPath = patches.length > 0 ? outputPath : newBinaryPath;
|
|
1762
|
-
if (meta.patches.websearch || !!meta.patches.proxy) {
|
|
2083
|
+
if (meta.patches.websearch || !!meta.patches.websearchProxy || !!meta.patches.proxy) {
|
|
1763
2084
|
const forwardTarget = meta.patches.apiBase || meta.patches.proxy || "https://api.factory.ai";
|
|
1764
|
-
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, meta.name, forwardTarget, meta.patches.standalone || false);
|
|
2085
|
+
const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), execTargetPath, meta.name, forwardTarget, meta.patches.standalone || false, meta.patches.websearchProxy || false);
|
|
1765
2086
|
execTargetPath = wrapperScript;
|
|
1766
2087
|
if (verbose) {
|
|
1767
2088
|
console.log(styleText("gray", ` Regenerated websearch wrapper`));
|