droid-patch 0.4.1 → 0.6.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createAlias, createAliasForWrapper, createMetadata, formatPatches, listAliases, listAllMetadata, loadAliasMetadata, patchDroid, removeAlias, saveAliasMetadata } from "./alias-BGsm9wXL.js";
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-DKVU8DM_.mjs";
3
3
  import bin from "tiny-bin";
4
4
  import { styleText } from "node:util";
5
5
  import { existsSync, readFileSync } from "node:fs";
@@ -15,15 +15,15 @@ import { chmod, mkdir, writeFile } from "node:fs/promises";
15
15
  * Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,
16
16
  * use a local proxy server to intercept search requests instead
17
17
  *
18
- * Features:
19
- * - Auto-shutdown on idle (default 5 minutes without requests)
20
- * - Timeout configurable via DROID_PROXY_IDLE_TIMEOUT env var (seconds)
21
- * - Set to 0 to disable timeout
18
+ * Each droid instance runs its own proxy server.
19
+ * The proxy is killed automatically when droid exits.
20
+ * @param factoryApiUrl - Custom Factory API URL (default: https://api.factory.ai)
22
21
  */
23
- function generateSearchProxyServer() {
22
+ function generateSearchProxyServer(factoryApiUrl = "https://api.factory.ai") {
24
23
  return `#!/usr/bin/env node
25
24
  // Droid WebSearch Proxy Server
26
25
  // Auto-generated by droid-patch --websearch
26
+ // This proxy runs as a child process of droid and is killed when droid exits
27
27
 
28
28
  const http = require('http');
29
29
  const https = require('https');
@@ -31,71 +31,13 @@ const { execSync } = require('child_process');
31
31
  const fs = require('fs');
32
32
 
33
33
  const DEBUG = process.env.DROID_SEARCH_DEBUG === '1';
34
- const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '23119');
35
-
36
- // Idle timeout in seconds, default 5 minutes, set to 0 to disable
37
- const IDLE_TIMEOUT = parseInt(process.env.DROID_PROXY_IDLE_TIMEOUT || '300');
38
- let lastActivityTime = Date.now();
39
- let idleCheckTimer = null;
34
+ const PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0'); // 0 = auto-assign
35
+ const FACTORY_API = '${factoryApiUrl}';
40
36
 
41
37
  function log(...args) {
42
38
  if (DEBUG) console.error('[websearch]', ...args);
43
39
  }
44
40
 
45
- // Update activity time
46
- function updateActivity() {
47
- lastActivityTime = Date.now();
48
- }
49
-
50
- // Check if any droid process is running (droid instances using the proxy)
51
- function isDroidRunning() {
52
- try {
53
- const { execSync } = require('child_process');
54
- // Use ps to check if droid.patched binary is running
55
- // Exclude scripts and grep itself, only match actual droid binary processes
56
- const result = execSync(
57
- 'ps aux | grep -E "[d]roid\\\\.patched" | grep -v grep | wc -l',
58
- { encoding: 'utf-8', timeout: 1000 }
59
- ).trim();
60
- return parseInt(result) > 0;
61
- } catch {
62
- return false;
63
- }
64
- }
65
-
66
- // Check idle time and possibly exit
67
- function checkIdleAndExit() {
68
- if (IDLE_TIMEOUT <= 0) return; // Timeout disabled
69
-
70
- // If droid process is running, refresh activity time (like heartbeat)
71
- if (isDroidRunning()) {
72
- log('Droid process detected, keeping proxy alive');
73
- updateActivity();
74
- return;
75
- }
76
-
77
- const idleMs = Date.now() - lastActivityTime;
78
- const timeoutMs = IDLE_TIMEOUT * 1000;
79
-
80
- if (idleMs >= timeoutMs) {
81
- log(\`Idle for \${Math.round(idleMs / 1000)}s and no droid running, shutting down...\`);
82
- cleanup();
83
- process.exit(0);
84
- }
85
- }
86
-
87
- // Cleanup resources
88
- function cleanup() {
89
- if (idleCheckTimer) {
90
- clearInterval(idleCheckTimer);
91
- idleCheckTimer = null;
92
- }
93
- // Delete PID file
94
- try {
95
- fs.unlinkSync('/tmp/droid-search-proxy.pid');
96
- } catch {}
97
- }
98
-
99
41
  // === Search Implementation ===
100
42
 
101
43
  // Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE
@@ -442,31 +384,16 @@ async function search(query, numResults = 10) {
442
384
 
443
385
  // === HTTP Proxy Server ===
444
386
 
445
- const FACTORY_API = 'https://api.factory.ai';
446
-
447
387
  const server = http.createServer(async (req, res) => {
448
388
  const url = new URL(req.url, \`http://\${req.headers.host}\`);
449
389
 
450
- // Health check - don't update activity time to avoid self-ping preventing timeout
390
+ // Health check
451
391
  if (url.pathname === '/health') {
452
- const idleSeconds = Math.round((Date.now() - lastActivityTime) / 1000);
453
- const droidRunning = isDroidRunning();
454
392
  res.writeHead(200, { 'Content-Type': 'application/json' });
455
- res.end(JSON.stringify({
456
- status: 'ok',
457
- port: PORT,
458
- idleTimeout: IDLE_TIMEOUT,
459
- idleSeconds: idleSeconds,
460
- droidRunning: droidRunning,
461
- // If droid is running, won't shutdown; otherwise calculate based on idle time
462
- willShutdownIn: IDLE_TIMEOUT > 0 && !droidRunning ? Math.max(0, IDLE_TIMEOUT - idleSeconds) : null
463
- }));
393
+ res.end(JSON.stringify({ status: 'ok', port: server.address()?.port || PORT }));
464
394
  return;
465
395
  }
466
396
 
467
- // Update activity time (only non-health-check requests refresh it)
468
- updateActivity();
469
-
470
397
  // Search endpoint - intercept
471
398
  if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {
472
399
  let body = '';
@@ -488,11 +415,54 @@ const server = http.createServer(async (req, res) => {
488
415
  return;
489
416
  }
490
417
 
491
- // Proxy all other requests to Factory API
418
+ // === Standalone mode (controlled by STANDALONE_MODE env) ===
419
+ // Whitelist approach: only allow core LLM APIs, mock everything else
420
+ if (process.env.STANDALONE_MODE === '1') {
421
+ const pathname = url.pathname;
422
+
423
+ // Whitelist: Core APIs that should be forwarded to upstream
424
+ const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');
425
+ // /api/tools/exa/search is already handled above
426
+
427
+ if (!isCoreLLMApi) {
428
+ // Special handling for specific routes
429
+ if (pathname === '/api/sessions/create') {
430
+ log('Mock (dynamic):', pathname);
431
+ const sessionId = \`local-\${Date.now()}-\${Math.random().toString(36).slice(2, 10)}\`;
432
+ res.writeHead(200, { 'Content-Type': 'application/json' });
433
+ res.end(JSON.stringify({ id: sessionId }));
434
+ return;
435
+ }
436
+
437
+ if (pathname === '/api/cli/whoami') {
438
+ log('Mock (401):', pathname);
439
+ res.writeHead(401, { 'Content-Type': 'application/json' });
440
+ res.end(JSON.stringify({ error: 'Unauthorized', message: 'Local mode - use token fallback' }));
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' }));
448
+ return;
449
+ }
450
+
451
+ // All other non-core APIs: return empty success
452
+ log('Mock (default):', pathname);
453
+ res.writeHead(200, { 'Content-Type': 'application/json' });
454
+ res.end(JSON.stringify({}));
455
+ return;
456
+ }
457
+ }
458
+
459
+ // Proxy core LLM requests to upstream API
492
460
  log('Proxy:', req.method, url.pathname);
493
461
 
494
462
  const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
495
- const proxyReq = https.request(proxyUrl, {
463
+ // Choose http or https based on target protocol
464
+ const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
465
+ const proxyReq = proxyModule.request(proxyUrl, {
496
466
  method: req.method,
497
467
  headers: { ...req.headers, host: proxyUrl.host }
498
468
  }, proxyRes => {
@@ -524,6 +494,9 @@ server.listen(PORT, '127.0.0.1', () => {
524
494
  fs.writeFileSync(portFile, String(actualPort));
525
495
  }
526
496
 
497
+ // Output PORT= line for wrapper script to parse
498
+ console.log('PORT=' + actualPort);
499
+
527
500
  const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;
528
501
  log('Search proxy started on http://127.0.0.1:' + actualPort);
529
502
  log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');
@@ -531,109 +504,92 @@ server.listen(PORT, '127.0.0.1', () => {
531
504
  log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');
532
505
  log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');
533
506
  log('SearXNG:', process.env.SEARXNG_URL || 'not set');
534
-
535
- // Start idle check timer
536
- // Check interval = min(timeout/2, 30s) to ensure timely timeout detection
537
- if (IDLE_TIMEOUT > 0) {
538
- const checkInterval = Math.min(IDLE_TIMEOUT * 500, 30000); // milliseconds
539
- log(\`Idle timeout: \${IDLE_TIMEOUT}s (will auto-shutdown when idle)\`);
540
- idleCheckTimer = setInterval(checkIdleAndExit, checkInterval);
541
- } else {
542
- log('Idle timeout: disabled (will run forever)');
543
- }
544
507
  });
545
508
 
546
- process.on('SIGTERM', () => { cleanup(); server.close(); process.exit(0); });
547
- process.on('SIGINT', () => { cleanup(); server.close(); process.exit(0); });
509
+ process.on('SIGTERM', () => { server.close(); process.exit(0); });
510
+ process.on('SIGINT', () => { server.close(); process.exit(0); });
548
511
  `;
549
512
  }
550
513
  /**
551
514
  * Generate unified Wrapper script
552
- * Uses shared proxy server mode:
553
- * - All droid instances share the same proxy process
554
- * - Proxy starts automatically if not running
555
- * - Proxy runs as background daemon, doesn't exit with droid
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
556
520
  */
557
- function generateUnifiedWrapper(droidPath, proxyScriptPath) {
521
+ function generateUnifiedWrapper(droidPath, proxyScriptPath, standalone = false) {
522
+ const standaloneEnv = standalone ? "STANDALONE_MODE=1 " : "";
558
523
  return `#!/bin/bash
559
524
  # Droid with WebSearch
560
525
  # Auto-generated by droid-patch --websearch
526
+ # Each instance runs its own proxy on a system-assigned port
561
527
 
562
528
  PROXY_SCRIPT="${proxyScriptPath}"
563
529
  DROID_BIN="${droidPath}"
564
- PORT=23119
565
- PID_FILE="/tmp/droid-search-proxy.pid"
566
- LOG_FILE="/tmp/droid-search-proxy.log"
567
-
568
- # Check if proxy is running
569
- is_proxy_running() {
570
- if [ -f "$PID_FILE" ]; then
571
- local pid
572
- pid=$(cat "$PID_FILE")
573
- if kill -0 "$pid" 2>/dev/null; then
574
- # Process exists, check if port responds
575
- if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
576
- return 0
577
- fi
578
- fi
579
- # PID file exists but process doesn't exist or port not responding, cleanup
580
- rm -f "$PID_FILE"
530
+ PROXY_PID=""
531
+ PORT_FILE="/tmp/droid-websearch-\$\$.port"
532
+ STANDALONE="${standalone ? "1" : "0"}"
533
+
534
+ # Cleanup function - kill proxy when droid exits
535
+ cleanup() {
536
+ if [ -n "\$PROXY_PID" ] && kill -0 "\$PROXY_PID" 2>/dev/null; then
537
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: \$PROXY_PID)" >&2
538
+ kill "\$PROXY_PID" 2>/dev/null
539
+ wait "\$PROXY_PID" 2>/dev/null
581
540
  fi
582
- return 1
541
+ rm -f "\$PORT_FILE"
583
542
  }
584
543
 
585
- # Start shared proxy
586
- start_shared_proxy() {
587
- # First check if port is occupied by another program
588
- if lsof -i:"$PORT" > /dev/null 2>&1; then
589
- # Port is occupied, check if it's our proxy
590
- if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
591
- [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy already running on port $PORT" >&2
592
- return 0
593
- else
594
- echo "[websearch] Port $PORT is occupied by another process" >&2
595
- return 1
596
- fi
597
- fi
598
-
599
- [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting shared proxy on port $PORT..." >&2
544
+ # Set up trap to cleanup on exit
545
+ trap cleanup EXIT INT TERM
600
546
 
601
- # Start proxy as background daemon
602
- SEARCH_PROXY_PORT="$PORT" nohup node "$PROXY_SCRIPT" >> "$LOG_FILE" 2>&1 &
603
- echo $! > "$PID_FILE"
547
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
548
+ [ "\$STANDALONE" = "1" ] && [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Standalone mode enabled" >&2
604
549
 
605
- # Wait for proxy to start
606
- for i in {1..30}; do
607
- if curl -s "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then
608
- [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port $PORT (PID: $(cat $PID_FILE))" >&2
609
- return 0
550
+ # Start proxy with port 0 (system will assign available port)
551
+ # Proxy writes actual port to PORT_FILE
552
+ if [ -n "\$DROID_SEARCH_DEBUG" ]; then
553
+ ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" 2>&1 &
554
+ else
555
+ ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" >/dev/null 2>&1 &
556
+ fi
557
+ PROXY_PID=\$!
558
+
559
+ # Wait for proxy to start and get actual port (max 5 seconds)
560
+ for i in {1..50}; do
561
+ # Check if proxy process is still running
562
+ if ! kill -0 "\$PROXY_PID" 2>/dev/null; then
563
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
564
+ break
565
+ fi
566
+ if [ -f "\$PORT_FILE" ]; then
567
+ ACTUAL_PORT=\$(cat "\$PORT_FILE" 2>/dev/null)
568
+ if [ -n "\$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:\$ACTUAL_PORT/health" > /dev/null 2>&1; then
569
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port \$ACTUAL_PORT (PID: \$PROXY_PID)" >&2
570
+ break
610
571
  fi
611
- sleep 0.1
612
- done
613
-
614
- echo "[websearch] Failed to start proxy" >&2
615
- rm -f "$PID_FILE"
616
- return 1
617
- }
618
-
619
- # Ensure proxy is running
620
- ensure_proxy() {
621
- if is_proxy_running; then
622
- [ -n "$DROID_SEARCH_DEBUG" ] && echo "[websearch] Using existing proxy on port $PORT" >&2
623
- return 0
624
572
  fi
625
- start_shared_proxy
626
- }
627
-
628
- # Start/reuse proxy
629
- if ! ensure_proxy; then
630
- echo "[websearch] Running without search proxy" >&2
631
- exec "$DROID_BIN" "$@"
573
+ sleep 0.1
574
+ done
575
+
576
+ # Check if proxy started successfully
577
+ if [ ! -f "\$PORT_FILE" ] || [ -z "\$(cat "\$PORT_FILE" 2>/dev/null)" ]; then
578
+ echo "[websearch] Failed to start proxy, running without websearch" >&2
579
+ cleanup
580
+ exec "\$DROID_BIN" "\$@"
632
581
  fi
633
582
 
634
- # Run droid, set API to point to local proxy
635
- export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:$PORT"
636
- exec "$DROID_BIN" "$@"
583
+ ACTUAL_PORT=\$(cat "\$PORT_FILE")
584
+ rm -f "\$PORT_FILE"
585
+
586
+ # Run droid with proxy
587
+ export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:\$ACTUAL_PORT"
588
+ "\$DROID_BIN" "\$@"
589
+ DROID_EXIT_CODE=\$?
590
+
591
+ # Cleanup will be called by trap
592
+ exit \$DROID_EXIT_CODE
637
593
  `;
638
594
  }
639
595
  /**
@@ -644,16 +600,23 @@ exec "$DROID_BIN" "$@"
644
600
  * - proxy server intercepts search requests, passes through other requests
645
601
  * - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy
646
602
  * - alias works directly, no extra steps needed
603
+ *
604
+ * @param outputDir - Directory to write files to
605
+ * @param droidPath - Path to droid binary
606
+ * @param aliasName - Alias name for the wrapper
607
+ * @param apiBase - Custom API base URL for proxy to forward requests to
608
+ * @param standalone - Standalone mode: mock non-LLM Factory APIs
647
609
  */
648
- async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName) {
610
+ async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase, standalone = false) {
649
611
  if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
650
612
  const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
651
613
  const wrapperScriptPath = join(outputDir, aliasName);
652
- await writeFile(proxyScriptPath, generateSearchProxyServer());
614
+ await writeFile(proxyScriptPath, generateSearchProxyServer(apiBase || "https://api.factory.ai"));
653
615
  console.log(`[*] Created proxy script: ${proxyScriptPath}`);
654
- await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath));
616
+ await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath, standalone));
655
617
  await chmod(wrapperScriptPath, 493);
656
618
  console.log(`[*] Created wrapper: ${wrapperScriptPath}`);
619
+ if (standalone) console.log(`[*] Standalone mode enabled`);
657
620
  return {
658
621
  wrapperScript: wrapperScriptPath,
659
622
  preloadScript: proxyScriptPath
@@ -666,13 +629,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
666
629
  function getVersion() {
667
630
  try {
668
631
  const pkgPath = join(__dirname, "..", "package.json");
669
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
670
- return pkg.version || "0.0.0";
632
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "0.0.0";
671
633
  } catch {
672
634
  return "0.0.0";
673
635
  }
674
636
  }
675
637
  const version = getVersion();
638
+ function getDroidVersion(droidPath) {
639
+ try {
640
+ const result = execSync(`"${droidPath}" --version`, {
641
+ encoding: "utf-8",
642
+ stdio: [
643
+ "pipe",
644
+ "pipe",
645
+ "pipe"
646
+ ],
647
+ timeout: 5e3
648
+ }).trim();
649
+ const match = result.match(/(\d+\.\d+\.\d+)/);
650
+ return match ? match[1] : result || void 0;
651
+ } catch {
652
+ return;
653
+ }
654
+ }
676
655
  function findDefaultDroidPath() {
677
656
  const home = homedir();
678
657
  try {
@@ -696,20 +675,23 @@ function findDefaultDroidPath() {
696
675
  for (const p of paths) if (existsSync(p)) return p;
697
676
  return join(home, ".droid", "bin", "droid");
698
677
  }
699
- 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("--reasoning-effort", "Enable reasoning effort for custom models (set to high, enable UI selector)").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) => {
678
+ 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 URL with custom URL (binary patch, or forward target with --websearch)").option("--websearch", "Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)").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("--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) => {
700
679
  const alias = args?.[0];
701
680
  const isCustom = options["is-custom"];
702
681
  const skipLogin = options["skip-login"];
703
682
  const apiBase = options["api-base"];
704
- const webSearch = options["websearch"];
683
+ const websearch = options["websearch"];
684
+ const standalone = options["standalone"];
685
+ const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
705
686
  const reasoningEffort = options["reasoning-effort"];
687
+ const noTelemetry = options["disable-telemetry"];
706
688
  const dryRun = options["dry-run"];
707
689
  const path = options.path || findDefaultDroidPath();
708
690
  const outputDir = options.output;
709
691
  const backup = options.backup !== false;
710
692
  const verbose = options.verbose;
711
693
  const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
712
- if (webSearch && !isCustom && !skipLogin && !apiBase) {
694
+ if (websearch && !isCustom && !skipLogin && !reasoningEffort && !noTelemetry) {
713
695
  if (!alias) {
714
696
  console.log(styleText("red", "Error: Alias name required for --websearch"));
715
697
  console.log(styleText("gray", "Usage: npx droid-patch --websearch <alias>"));
@@ -719,17 +701,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
719
701
  console.log(styleText(["cyan", "bold"], " Droid WebSearch Setup"));
720
702
  console.log(styleText("cyan", "═".repeat(60)));
721
703
  console.log();
722
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
723
- const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, path, alias);
704
+ console.log(styleText("white", `Forward target: ${websearchTarget}`));
705
+ if (standalone) console.log(styleText("white", `Standalone mode: enabled`));
706
+ console.log();
707
+ const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), path, alias, websearchTarget, standalone);
724
708
  await createAliasForWrapper(wrapperScript, alias, verbose);
725
- const metadata = createMetadata(alias, path, {
709
+ const droidVersion = getDroidVersion(path);
710
+ await saveAliasMetadata(createMetadata(alias, path, {
726
711
  isCustom: false,
727
712
  skipLogin: false,
728
- apiBase: null,
713
+ apiBase: apiBase || null,
729
714
  websearch: true,
730
- reasoningEffort: false
731
- });
732
- await saveAliasMetadata(metadata);
715
+ reasoningEffort: false,
716
+ noTelemetry: false,
717
+ standalone
718
+ }, {
719
+ droidPatchVersion: version,
720
+ droidVersion
721
+ }));
733
722
  console.log();
734
723
  console.log(styleText("green", "═".repeat(60)));
735
724
  console.log(styleText(["green", "bold"], " WebSearch Ready!"));
@@ -755,22 +744,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
755
744
  console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1"));
756
745
  return;
757
746
  }
758
- if (!isCustom && !skipLogin && !apiBase && !webSearch && !reasoningEffort) {
747
+ if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry) {
759
748
  console.log(styleText("yellow", "No patch flags specified. Available patches:"));
760
749
  console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
761
750
  console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
762
- console.log(styleText("gray", " --api-base Replace Factory API URL with custom server"));
763
- console.log(styleText("gray", " --websearch Enable local WebSearch (Google PSE + DuckDuckGo)"));
751
+ console.log(styleText("gray", " --api-base Replace Factory API URL (binary patch)"));
752
+ console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
764
753
  console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
754
+ console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
755
+ console.log(styleText("gray", " --standalone Standalone mode: mock non-LLM Factory APIs"));
765
756
  console.log();
766
757
  console.log("Usage examples:");
767
758
  console.log(styleText("cyan", " npx droid-patch --is-custom droid-custom"));
768
759
  console.log(styleText("cyan", " npx droid-patch --skip-login droid-nologin"));
769
760
  console.log(styleText("cyan", " npx droid-patch --is-custom --skip-login droid-patched"));
770
- console.log(styleText("cyan", " npx droid-patch --skip-login -o . my-droid"));
771
- console.log(styleText("cyan", " npx droid-patch --api-base http://localhost:3000 droid-local"));
772
761
  console.log(styleText("cyan", " npx droid-patch --websearch droid-search"));
773
- console.log(styleText("cyan", " npx droid-patch --reasoning-effort high droid-reasoning"));
762
+ console.log(styleText("cyan", " npx droid-patch --websearch --standalone droid-local"));
763
+ console.log(styleText("cyan", " npx droid-patch --disable-telemetry droid-private"));
764
+ console.log(styleText("cyan", " npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid"));
774
765
  process.exit(1);
775
766
  }
776
767
  if (!alias && !dryRun) {
@@ -795,9 +786,9 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
795
786
  pattern: Buffer.from("process.env.FACTORY_API_KEY"),
796
787
  replacement: Buffer.from("\"fk-droid-patch-skip-00000\"")
797
788
  });
798
- if (apiBase) {
789
+ if (apiBase && !websearch) {
799
790
  const originalUrl = "https://api.factory.ai";
800
- const originalLength = originalUrl.length;
791
+ const originalLength = 22;
801
792
  let normalizedUrl = apiBase.replace(/\/+$/, "");
802
793
  if (normalizedUrl.length > originalLength) {
803
794
  console.log(styleText("red", `Error: API base URL must be ${originalLength} characters or less`));
@@ -850,6 +841,26 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
850
841
  replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
851
842
  });
852
843
  }
844
+ if (noTelemetry) {
845
+ patches.push({
846
+ name: "noTelemetrySentryEnv1",
847
+ description: "Break ENABLE_SENTRY env var check (E->X)",
848
+ pattern: Buffer.from("ENABLE_SENTRY"),
849
+ replacement: Buffer.from("XNABLE_SENTRY")
850
+ });
851
+ patches.push({
852
+ name: "noTelemetrySentryEnv2",
853
+ description: "Break VITE_VERCEL_ENV env var check (V->X)",
854
+ pattern: Buffer.from("VITE_VERCEL_ENV"),
855
+ replacement: Buffer.from("XITE_VERCEL_ENV")
856
+ });
857
+ patches.push({
858
+ name: "noTelemetryFlushBlock",
859
+ description: "Make flushToWeb always return (!0|| = always true)",
860
+ pattern: Buffer.from("this.webEvents.length===0"),
861
+ replacement: Buffer.from("!0||this.webEvents.length")
862
+ });
863
+ }
853
864
  try {
854
865
  const result = await patchDroid({
855
866
  inputPath: path,
@@ -880,30 +891,27 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
880
891
  }
881
892
  if (result.success && result.outputPath && alias) {
882
893
  console.log();
883
- if (webSearch) {
884
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
885
- const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, result.outputPath, alias);
894
+ if (websearch) {
895
+ const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), result.outputPath, alias, websearchTarget, standalone);
886
896
  await createAliasForWrapper(wrapperScript, alias, verbose);
887
897
  console.log();
888
- console.log(styleText("cyan", "WebSearch providers (optional):"));
889
- console.log(styleText("gray", " Works out of the box with DuckDuckGo fallback"));
890
- console.log(styleText("gray", " For better results, configure a provider:"));
891
- console.log();
892
- console.log(styleText("yellow", " Smithery Exa"), styleText("gray", " - Best quality, free via smithery.ai"));
893
- console.log(styleText("gray", " export SMITHERY_API_KEY=... SMITHERY_PROFILE=..."));
894
- console.log(styleText("yellow", " Google PSE"), styleText("gray", " - 10,000/day free"));
895
- console.log(styleText("gray", " export GOOGLE_PSE_API_KEY=... GOOGLE_PSE_CX=..."));
896
- console.log();
897
- console.log(styleText("gray", " See README for all providers and setup guides"));
898
+ console.log(styleText("cyan", "WebSearch enabled"));
899
+ console.log(styleText("white", ` Forward target: ${websearchTarget}`));
900
+ if (standalone) console.log(styleText("white", ` Standalone mode: enabled`));
898
901
  } else await createAlias(result.outputPath, alias, verbose);
899
- const metadata = createMetadata(alias, path, {
902
+ const droidVersion = getDroidVersion(path);
903
+ await saveAliasMetadata(createMetadata(alias, path, {
900
904
  isCustom: !!isCustom,
901
905
  skipLogin: !!skipLogin,
902
906
  apiBase: apiBase || null,
903
- websearch: !!webSearch,
904
- reasoningEffort: !!reasoningEffort
905
- });
906
- await saveAliasMetadata(metadata);
907
+ websearch: !!websearch,
908
+ reasoningEffort: !!reasoningEffort,
909
+ noTelemetry: !!noTelemetry,
910
+ standalone: !!standalone
911
+ }, {
912
+ droidPatchVersion: version,
913
+ droidVersion
914
+ }));
907
915
  }
908
916
  if (result.success) {
909
917
  console.log();
@@ -919,8 +927,23 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
919
927
  }
920
928
  }).command("list", "List all droid-patch aliases").action(async () => {
921
929
  await listAliases();
922
- }).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) => {
923
- const target = args[0];
930
+ }).command("remove", "Remove alias(es) by name or filter").argument("[alias-or-path]", "Alias name or file path to remove").option("--patch-version <version>", "Remove aliases created by this droid-patch version").option("--droid-version <version>", "Remove aliases for this droid version").option("--flag <flag>", "Remove aliases with this flag (is-custom, skip-login, websearch, api-base, reasoning-effort, disable-telemetry, standalone)").action(async (options, args) => {
931
+ const target = args?.[0];
932
+ const patchVersion = options["patch-version"];
933
+ const droidVersion = options["droid-version"];
934
+ const flag = options.flag;
935
+ if (patchVersion || droidVersion || flag) {
936
+ await removeAliasesByFilter({
937
+ patchVersion,
938
+ droidVersion,
939
+ flags: flag ? [flag] : void 0
940
+ });
941
+ return;
942
+ }
943
+ if (!target) {
944
+ console.error(styleText("red", "Error: Provide an alias name or use filter options (--patch-version, --droid-version, --flag)"));
945
+ process.exit(1);
946
+ }
924
947
  if (target.includes("/") || existsSync(target)) {
925
948
  const { unlink: unlink$1 } = await import("node:fs/promises");
926
949
  try {
@@ -933,86 +956,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
933
956
  } else await removeAlias(target);
934
957
  }).command("version", "Print droid-patch version").action(() => {
935
958
  console.log(`droid-patch v${version}`);
936
- }).command("proxy-status", "Check websearch proxy status").action(async () => {
937
- const pidFile = "/tmp/droid-search-proxy.pid";
938
- const logFile = "/tmp/droid-search-proxy.log";
939
- const port = 23119;
940
- console.log(styleText("cyan", "═".repeat(60)));
941
- console.log(styleText(["cyan", "bold"], " WebSearch Proxy Status"));
942
- console.log(styleText("cyan", "═".repeat(60)));
943
- console.log();
944
- try {
945
- const response = await fetch(`http://127.0.0.1:${port}/health`);
946
- if (response.ok) {
947
- const data = await response.json();
948
- console.log(styleText("green", ` Status: Running ✓`));
949
- console.log(styleText("white", ` Port: ${port}`));
950
- if (existsSync(pidFile)) {
951
- const { readFileSync: readFileSync$1 } = await import("node:fs");
952
- const pid = readFileSync$1(pidFile, "utf-8").trim();
953
- console.log(styleText("white", ` PID: ${pid}`));
954
- }
955
- if (data.droidRunning !== void 0) console.log(styleText("white", ` Droid running: ${data.droidRunning ? "yes (proxy will stay alive)" : "no"}`));
956
- if (data.idleTimeout !== void 0) if (data.idleTimeout > 0) {
957
- const idleMins = Math.floor((data.idleSeconds || 0) / 60);
958
- const idleSecs = (data.idleSeconds || 0) % 60;
959
- if (data.droidRunning) console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s (won't shutdown while droid runs)`));
960
- else if (data.willShutdownIn !== null) {
961
- const shutdownMins = Math.floor((data.willShutdownIn || 0) / 60);
962
- const shutdownSecs = (data.willShutdownIn || 0) % 60;
963
- console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s`));
964
- console.log(styleText("white", ` Auto-shutdown in: ${shutdownMins}m ${shutdownSecs}s`));
965
- }
966
- } else console.log(styleText("white", ` Auto-shutdown: disabled`));
967
- console.log(styleText("white", ` Log: ${logFile}`));
968
- console.log();
969
- console.log(styleText("gray", "To stop the proxy manually:"));
970
- console.log(styleText("cyan", " npx droid-patch proxy-stop"));
971
- console.log();
972
- console.log(styleText("gray", "To disable auto-shutdown:"));
973
- console.log(styleText("cyan", " export DROID_PROXY_IDLE_TIMEOUT=0"));
974
- }
975
- } catch {
976
- console.log(styleText("yellow", ` Status: Not running`));
977
- console.log();
978
- console.log(styleText("gray", "The proxy will start automatically when you run droid-full."));
979
- console.log(styleText("gray", "It will auto-shutdown after 5 minutes of idle (configurable)."));
980
- }
981
- console.log();
982
- }).command("proxy-stop", "Stop the websearch proxy").action(async () => {
983
- const pidFile = "/tmp/droid-search-proxy.pid";
984
- if (!existsSync(pidFile)) {
985
- console.log(styleText("yellow", "Proxy is not running (no PID file)"));
986
- return;
987
- }
988
- try {
989
- const { readFileSync: readFileSync$1, unlinkSync: unlinkSync$1 } = await import("node:fs");
990
- const pid = readFileSync$1(pidFile, "utf-8").trim();
991
- process.kill(parseInt(pid), "SIGTERM");
992
- unlinkSync$1(pidFile);
993
- console.log(styleText("green", `[*] Proxy stopped (PID: ${pid})`));
994
- } catch (error) {
995
- console.log(styleText("yellow", `[!] Could not stop proxy: ${error.message}`));
996
- try {
997
- const { unlinkSync: unlinkSync$1 } = await import("node:fs");
998
- unlinkSync$1(pidFile);
999
- console.log(styleText("gray", "Cleaned up stale PID file"));
1000
- } catch {}
1001
- }
1002
- }).command("proxy-log", "Show websearch proxy logs").action(async () => {
1003
- const logFile = "/tmp/droid-search-proxy.log";
1004
- if (!existsSync(logFile)) {
1005
- console.log(styleText("yellow", "No log file found"));
1006
- return;
1007
- }
1008
- const { readFileSync: readFileSync$1 } = await import("node:fs");
1009
- const log = readFileSync$1(logFile, "utf-8");
1010
- const lines = log.split("\n").slice(-50);
1011
- console.log(styleText("cyan", "═".repeat(60)));
1012
- console.log(styleText(["cyan", "bold"], " WebSearch Proxy Logs (last 50 lines)"));
1013
- console.log(styleText("cyan", "═".repeat(60)));
1014
- console.log();
1015
- console.log(lines.join("\n"));
959
+ }).command("clear", "Remove all droid-patch aliases and related files").action(async () => {
960
+ await clearAllAliases();
1016
961
  }).command("update", "Update aliases with latest droid binary").argument("[alias]", "Specific alias to update (optional, updates all if not specified)").option("--dry-run", "Preview without making changes").option("-p, --path <path>", "Path to new droid binary").option("-v, --verbose", "Enable verbose output").action(async (options, args) => {
1017
962
  const aliasName = args?.[0];
1018
963
  const dryRun = options["dry-run"];
@@ -1077,7 +1022,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1077
1022
  });
1078
1023
  if (meta.patches.apiBase) {
1079
1024
  const originalUrl = "https://api.factory.ai";
1080
- const paddedUrl = meta.patches.apiBase.padEnd(originalUrl.length, " ");
1025
+ const paddedUrl = meta.patches.apiBase.padEnd(22, " ");
1081
1026
  patches.push({
1082
1027
  name: "apiBase",
1083
1028
  description: `Replace Factory API URL with "${meta.patches.apiBase}"`,
@@ -1117,18 +1062,36 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1117
1062
  replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
1118
1063
  });
1119
1064
  }
1120
- const binsDir = join(homedir(), ".droid-patch", "bins");
1121
- const outputPath = join(binsDir, `${meta.name}-patched`);
1065
+ if (meta.patches.noTelemetry) {
1066
+ patches.push({
1067
+ name: "noTelemetrySentryEnv1",
1068
+ description: "Break ENABLE_SENTRY env var check (E->X)",
1069
+ pattern: Buffer.from("ENABLE_SENTRY"),
1070
+ replacement: Buffer.from("XNABLE_SENTRY")
1071
+ });
1072
+ patches.push({
1073
+ name: "noTelemetrySentryEnv2",
1074
+ description: "Break VITE_VERCEL_ENV env var check (V->X)",
1075
+ pattern: Buffer.from("VITE_VERCEL_ENV"),
1076
+ replacement: Buffer.from("XITE_VERCEL_ENV")
1077
+ });
1078
+ patches.push({
1079
+ name: "noTelemetryFlushBlock",
1080
+ description: "Make flushToWeb always return (!0|| = always true)",
1081
+ pattern: Buffer.from("this.webEvents.length===0"),
1082
+ replacement: Buffer.from("!0||this.webEvents.length")
1083
+ });
1084
+ }
1085
+ const outputPath = join(join(homedir(), ".droid-patch", "bins"), `${meta.name}-patched`);
1122
1086
  if (patches.length > 0) {
1123
- const result = await patchDroid({
1087
+ if (!(await patchDroid({
1124
1088
  inputPath: newBinaryPath,
1125
1089
  outputPath,
1126
1090
  patches,
1127
1091
  dryRun: false,
1128
1092
  backup: false,
1129
1093
  verbose
1130
- });
1131
- if (!result.success) {
1094
+ })).success) {
1132
1095
  console.log(styleText("red", ` ✗ Failed to apply patches`));
1133
1096
  failCount++;
1134
1097
  continue;
@@ -1141,13 +1104,20 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1141
1104
  console.log(styleText("yellow", ` [!] Could not re-sign binary`));
1142
1105
  }
1143
1106
  }
1144
- if (meta.patches.websearch) {
1145
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
1146
- const targetBinaryPath = patches.length > 0 ? outputPath : newBinaryPath;
1147
- await createWebSearchUnifiedFiles(websearchDir, targetBinaryPath, meta.name);
1148
- if (verbose) console.log(styleText("gray", ` Regenerated websearch wrapper`));
1107
+ if (meta.patches.websearch || !!meta.patches.proxy) {
1108
+ const forwardTarget = meta.patches.apiBase || meta.patches.proxy || "https://api.factory.ai";
1109
+ await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), patches.length > 0 ? outputPath : newBinaryPath, meta.name, forwardTarget, meta.patches.standalone || false);
1110
+ if (verbose) {
1111
+ console.log(styleText("gray", ` Regenerated websearch wrapper`));
1112
+ if (meta.patches.standalone) console.log(styleText("gray", ` Standalone mode: enabled`));
1113
+ }
1114
+ if (meta.patches.proxy && !meta.patches.websearch) {
1115
+ meta.patches.websearch = true;
1116
+ meta.patches.apiBase = meta.patches.proxy;
1117
+ delete meta.patches.proxy;
1118
+ }
1149
1119
  }
1150
- meta.updatedAt = new Date().toISOString();
1120
+ meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1151
1121
  meta.originalBinaryPath = newBinaryPath;
1152
1122
  await saveAliasMetadata(meta);
1153
1123
  console.log(styleText("green", ` ✓ Updated successfully`));
@@ -1177,4 +1147,5 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1177
1147
  });
1178
1148
 
1179
1149
  //#endregion
1180
- //# sourceMappingURL=cli.js.map
1150
+ export { };
1151
+ //# sourceMappingURL=cli.mjs.map