droid-patch 0.4.0 → 0.5.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-UwlvAO5o.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-DVggcM0G.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 = '';
@@ -492,7 +419,9 @@ const server = http.createServer(async (req, res) => {
492
419
  log('Proxy:', req.method, url.pathname);
493
420
 
494
421
  const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);
495
- const proxyReq = https.request(proxyUrl, {
422
+ // Choose http or https based on target protocol
423
+ const proxyModule = proxyUrl.protocol === 'https:' ? https : http;
424
+ const proxyReq = proxyModule.request(proxyUrl, {
496
425
  method: req.method,
497
426
  headers: { ...req.headers, host: proxyUrl.host }
498
427
  }, proxyRes => {
@@ -524,6 +453,9 @@ server.listen(PORT, '127.0.0.1', () => {
524
453
  fs.writeFileSync(portFile, String(actualPort));
525
454
  }
526
455
 
456
+ // Output PORT= line for wrapper script to parse
457
+ console.log('PORT=' + actualPort);
458
+
527
459
  const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;
528
460
  log('Search proxy started on http://127.0.0.1:' + actualPort);
529
461
  log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');
@@ -531,109 +463,89 @@ server.listen(PORT, '127.0.0.1', () => {
531
463
  log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');
532
464
  log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');
533
465
  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
466
  });
545
467
 
546
- process.on('SIGTERM', () => { cleanup(); server.close(); process.exit(0); });
547
- process.on('SIGINT', () => { cleanup(); server.close(); process.exit(0); });
468
+ process.on('SIGTERM', () => { server.close(); process.exit(0); });
469
+ process.on('SIGINT', () => { server.close(); process.exit(0); });
548
470
  `;
549
471
  }
550
472
  /**
551
473
  * 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
474
+ * Each droid instance runs its own proxy:
475
+ * - Uses port 0 to let system auto-assign available port
476
+ * - Proxy runs as child process
477
+ * - Proxy is killed when droid exits
478
+ * - Supports multiple droid instances running simultaneously
556
479
  */
557
480
  function generateUnifiedWrapper(droidPath, proxyScriptPath) {
558
481
  return `#!/bin/bash
559
482
  # Droid with WebSearch
560
483
  # Auto-generated by droid-patch --websearch
484
+ # Each instance runs its own proxy on a system-assigned port
561
485
 
562
486
  PROXY_SCRIPT="${proxyScriptPath}"
563
487
  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"
488
+ PROXY_PID=""
489
+ PORT_FILE="/tmp/droid-websearch-\$\$.port"
490
+
491
+ # Cleanup function - kill proxy when droid exits
492
+ cleanup() {
493
+ if [ -n "\$PROXY_PID" ] && kill -0 "\$PROXY_PID" 2>/dev/null; then
494
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Stopping proxy (PID: \$PROXY_PID)" >&2
495
+ kill "\$PROXY_PID" 2>/dev/null
496
+ wait "\$PROXY_PID" 2>/dev/null
581
497
  fi
582
- return 1
498
+ rm -f "\$PORT_FILE"
583
499
  }
584
500
 
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
501
+ # Set up trap to cleanup on exit
502
+ trap cleanup EXIT INT TERM
600
503
 
601
- # Start proxy as background daemon
602
- SEARCH_PROXY_PORT="$PORT" nohup node "$PROXY_SCRIPT" >> "$LOG_FILE" 2>&1 &
603
- echo $! > "$PID_FILE"
504
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Starting proxy..." >&2
604
505
 
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
506
+ # Start proxy with port 0 (system will assign available port)
507
+ # Proxy writes actual port to PORT_FILE
508
+ if [ -n "\$DROID_SEARCH_DEBUG" ]; then
509
+ SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" 2>&1 &
510
+ else
511
+ SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE="\$PORT_FILE" node "\$PROXY_SCRIPT" >/dev/null 2>&1 &
512
+ fi
513
+ PROXY_PID=\$!
514
+
515
+ # Wait for proxy to start and get actual port (max 5 seconds)
516
+ for i in {1..50}; do
517
+ # Check if proxy process is still running
518
+ if ! kill -0 "\$PROXY_PID" 2>/dev/null; then
519
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy process died" >&2
520
+ break
521
+ fi
522
+ if [ -f "\$PORT_FILE" ]; then
523
+ ACTUAL_PORT=\$(cat "\$PORT_FILE" 2>/dev/null)
524
+ if [ -n "\$ACTUAL_PORT" ] && curl -s "http://127.0.0.1:\$ACTUAL_PORT/health" > /dev/null 2>&1; then
525
+ [ -n "\$DROID_SEARCH_DEBUG" ] && echo "[websearch] Proxy ready on port \$ACTUAL_PORT (PID: \$PROXY_PID)" >&2
526
+ break
610
527
  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
528
  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" "$@"
529
+ sleep 0.1
530
+ done
531
+
532
+ # Check if proxy started successfully
533
+ if [ ! -f "\$PORT_FILE" ] || [ -z "\$(cat "\$PORT_FILE" 2>/dev/null)" ]; then
534
+ echo "[websearch] Failed to start proxy, running without websearch" >&2
535
+ cleanup
536
+ exec "\$DROID_BIN" "\$@"
632
537
  fi
633
538
 
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" "$@"
539
+ ACTUAL_PORT=\$(cat "\$PORT_FILE")
540
+ rm -f "\$PORT_FILE"
541
+
542
+ # Run droid with proxy
543
+ export FACTORY_API_BASE_URL_OVERRIDE="http://127.0.0.1:\$ACTUAL_PORT"
544
+ "\$DROID_BIN" "\$@"
545
+ DROID_EXIT_CODE=\$?
546
+
547
+ # Cleanup will be called by trap
548
+ exit \$DROID_EXIT_CODE
637
549
  `;
638
550
  }
639
551
  /**
@@ -644,12 +556,17 @@ exec "$DROID_BIN" "$@"
644
556
  * - proxy server intercepts search requests, passes through other requests
645
557
  * - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy
646
558
  * - alias works directly, no extra steps needed
559
+ *
560
+ * @param outputDir - Directory to write files to
561
+ * @param droidPath - Path to droid binary
562
+ * @param aliasName - Alias name for the wrapper
563
+ * @param apiBase - Custom API base URL for proxy to forward requests to
647
564
  */
648
- async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName) {
565
+ async function createWebSearchUnifiedFiles(outputDir, droidPath, aliasName, apiBase) {
649
566
  if (!existsSync(outputDir)) await mkdir(outputDir, { recursive: true });
650
567
  const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);
651
568
  const wrapperScriptPath = join(outputDir, aliasName);
652
- await writeFile(proxyScriptPath, generateSearchProxyServer());
569
+ await writeFile(proxyScriptPath, generateSearchProxyServer(apiBase || "https://api.factory.ai"));
653
570
  console.log(`[*] Created proxy script: ${proxyScriptPath}`);
654
571
  await writeFile(wrapperScriptPath, generateUnifiedWrapper(droidPath, proxyScriptPath));
655
572
  await chmod(wrapperScriptPath, 493);
@@ -666,13 +583,29 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
666
583
  function getVersion() {
667
584
  try {
668
585
  const pkgPath = join(__dirname, "..", "package.json");
669
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
670
- return pkg.version || "0.0.0";
586
+ return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "0.0.0";
671
587
  } catch {
672
588
  return "0.0.0";
673
589
  }
674
590
  }
675
591
  const version = getVersion();
592
+ function getDroidVersion(droidPath) {
593
+ try {
594
+ const result = execSync(`"${droidPath}" --version`, {
595
+ encoding: "utf-8",
596
+ stdio: [
597
+ "pipe",
598
+ "pipe",
599
+ "pipe"
600
+ ],
601
+ timeout: 5e3
602
+ }).trim();
603
+ const match = result.match(/(\d+\.\d+\.\d+)/);
604
+ return match ? match[1] : result || void 0;
605
+ } catch {
606
+ return;
607
+ }
608
+ }
676
609
  function findDefaultDroidPath() {
677
610
  const home = homedir();
678
611
  try {
@@ -696,12 +629,13 @@ function findDefaultDroidPath() {
696
629
  for (const p of paths) if (existsSync(p)) return p;
697
630
  return join(home, ".droid", "bin", "droid");
698
631
  }
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) => {
632
+ 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("--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) => {
700
633
  const alias = args?.[0];
701
634
  const isCustom = options["is-custom"];
702
635
  const skipLogin = options["skip-login"];
703
636
  const apiBase = options["api-base"];
704
- const webSearch = options["websearch"];
637
+ const websearch = options["websearch"];
638
+ const websearchTarget = websearch ? apiBase || "https://api.factory.ai" : void 0;
705
639
  const reasoningEffort = options["reasoning-effort"];
706
640
  const dryRun = options["dry-run"];
707
641
  const path = options.path || findDefaultDroidPath();
@@ -709,7 +643,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
709
643
  const backup = options.backup !== false;
710
644
  const verbose = options.verbose;
711
645
  const outputPath = outputDir && alias ? join(outputDir, alias) : void 0;
712
- if (webSearch && !isCustom && !skipLogin && !apiBase) {
646
+ if (websearch && !isCustom && !skipLogin && !reasoningEffort) {
713
647
  if (!alias) {
714
648
  console.log(styleText("red", "Error: Alias name required for --websearch"));
715
649
  console.log(styleText("gray", "Usage: npx droid-patch --websearch <alias>"));
@@ -719,17 +653,21 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
719
653
  console.log(styleText(["cyan", "bold"], " Droid WebSearch Setup"));
720
654
  console.log(styleText("cyan", "═".repeat(60)));
721
655
  console.log();
722
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
723
- const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, path, alias);
656
+ console.log(styleText("white", `Forward target: ${websearchTarget}`));
657
+ console.log();
658
+ const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), path, alias, websearchTarget);
724
659
  await createAliasForWrapper(wrapperScript, alias, verbose);
725
- const metadata = createMetadata(alias, path, {
660
+ const droidVersion = getDroidVersion(path);
661
+ await saveAliasMetadata(createMetadata(alias, path, {
726
662
  isCustom: false,
727
663
  skipLogin: false,
728
- apiBase: null,
664
+ apiBase: apiBase || null,
729
665
  websearch: true,
730
666
  reasoningEffort: false
731
- });
732
- await saveAliasMetadata(metadata);
667
+ }, {
668
+ droidPatchVersion: version,
669
+ droidVersion
670
+ }));
733
671
  console.log();
734
672
  console.log(styleText("green", "═".repeat(60)));
735
673
  console.log(styleText(["green", "bold"], " WebSearch Ready!"));
@@ -755,22 +693,20 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
755
693
  console.log(styleText("gray", " export DROID_SEARCH_DEBUG=1"));
756
694
  return;
757
695
  }
758
- if (!isCustom && !skipLogin && !apiBase && !webSearch && !reasoningEffort) {
696
+ if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort) {
759
697
  console.log(styleText("yellow", "No patch flags specified. Available patches:"));
760
698
  console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
761
699
  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)"));
700
+ console.log(styleText("gray", " --api-base Replace Factory API URL (binary patch)"));
701
+ console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
764
702
  console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
765
703
  console.log();
766
704
  console.log("Usage examples:");
767
705
  console.log(styleText("cyan", " npx droid-patch --is-custom droid-custom"));
768
706
  console.log(styleText("cyan", " npx droid-patch --skip-login droid-nologin"));
769
707
  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
708
  console.log(styleText("cyan", " npx droid-patch --websearch droid-search"));
773
- console.log(styleText("cyan", " npx droid-patch --reasoning-effort high droid-reasoning"));
709
+ console.log(styleText("cyan", " npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid"));
774
710
  process.exit(1);
775
711
  }
776
712
  if (!alias && !dryRun) {
@@ -795,9 +731,9 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
795
731
  pattern: Buffer.from("process.env.FACTORY_API_KEY"),
796
732
  replacement: Buffer.from("\"fk-droid-patch-skip-00000\"")
797
733
  });
798
- if (apiBase) {
734
+ if (apiBase && !websearch) {
799
735
  const originalUrl = "https://api.factory.ai";
800
- const originalLength = originalUrl.length;
736
+ const originalLength = 22;
801
737
  let normalizedUrl = apiBase.replace(/\/+$/, "");
802
738
  if (normalizedUrl.length > originalLength) {
803
739
  console.log(styleText("red", `Error: API base URL must be ${originalLength} characters or less`));
@@ -843,6 +779,12 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
843
779
  pattern: Buffer.from("supportedReasoningEfforts.length<=1"),
844
780
  replacement: Buffer.from("supportedReasoningEfforts.length<=0")
845
781
  });
782
+ patches.push({
783
+ name: "reasoningEffortValidationBypass",
784
+ description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
785
+ pattern: Buffer.from("if(R&&!B.supportedReasoningEfforts.includes(R))"),
786
+ replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
787
+ });
846
788
  }
847
789
  try {
848
790
  const result = await patchDroid({
@@ -874,30 +816,24 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
874
816
  }
875
817
  if (result.success && result.outputPath && alias) {
876
818
  console.log();
877
- if (webSearch) {
878
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
879
- const { wrapperScript } = await createWebSearchUnifiedFiles(websearchDir, result.outputPath, alias);
819
+ if (websearch) {
820
+ const { wrapperScript } = await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), result.outputPath, alias, websearchTarget);
880
821
  await createAliasForWrapper(wrapperScript, alias, verbose);
881
822
  console.log();
882
- console.log(styleText("cyan", "WebSearch providers (optional):"));
883
- console.log(styleText("gray", " Works out of the box with DuckDuckGo fallback"));
884
- console.log(styleText("gray", " For better results, configure a provider:"));
885
- console.log();
886
- console.log(styleText("yellow", " Smithery Exa"), styleText("gray", " - Best quality, free via smithery.ai"));
887
- console.log(styleText("gray", " export SMITHERY_API_KEY=... SMITHERY_PROFILE=..."));
888
- console.log(styleText("yellow", " Google PSE"), styleText("gray", " - 10,000/day free"));
889
- console.log(styleText("gray", " export GOOGLE_PSE_API_KEY=... GOOGLE_PSE_CX=..."));
890
- console.log();
891
- console.log(styleText("gray", " See README for all providers and setup guides"));
823
+ console.log(styleText("cyan", "WebSearch enabled"));
824
+ console.log(styleText("white", ` Forward target: ${websearchTarget}`));
892
825
  } else await createAlias(result.outputPath, alias, verbose);
893
- const metadata = createMetadata(alias, path, {
826
+ const droidVersion = getDroidVersion(path);
827
+ await saveAliasMetadata(createMetadata(alias, path, {
894
828
  isCustom: !!isCustom,
895
829
  skipLogin: !!skipLogin,
896
830
  apiBase: apiBase || null,
897
- websearch: !!webSearch,
831
+ websearch: !!websearch,
898
832
  reasoningEffort: !!reasoningEffort
899
- });
900
- await saveAliasMetadata(metadata);
833
+ }, {
834
+ droidPatchVersion: version,
835
+ droidVersion
836
+ }));
901
837
  }
902
838
  if (result.success) {
903
839
  console.log();
@@ -913,8 +849,23 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
913
849
  }
914
850
  }).command("list", "List all droid-patch aliases").action(async () => {
915
851
  await listAliases();
916
- }).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) => {
917
- const target = args[0];
852
+ }).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)").action(async (options, args) => {
853
+ const target = args?.[0];
854
+ const patchVersion = options["patch-version"];
855
+ const droidVersion = options["droid-version"];
856
+ const flag = options.flag;
857
+ if (patchVersion || droidVersion || flag) {
858
+ await removeAliasesByFilter({
859
+ patchVersion,
860
+ droidVersion,
861
+ flags: flag ? [flag] : void 0
862
+ });
863
+ return;
864
+ }
865
+ if (!target) {
866
+ console.error(styleText("red", "Error: Provide an alias name or use filter options (--patch-version, --droid-version, --flag)"));
867
+ process.exit(1);
868
+ }
918
869
  if (target.includes("/") || existsSync(target)) {
919
870
  const { unlink: unlink$1 } = await import("node:fs/promises");
920
871
  try {
@@ -927,86 +878,8 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
927
878
  } else await removeAlias(target);
928
879
  }).command("version", "Print droid-patch version").action(() => {
929
880
  console.log(`droid-patch v${version}`);
930
- }).command("proxy-status", "Check websearch proxy status").action(async () => {
931
- const pidFile = "/tmp/droid-search-proxy.pid";
932
- const logFile = "/tmp/droid-search-proxy.log";
933
- const port = 23119;
934
- console.log(styleText("cyan", "═".repeat(60)));
935
- console.log(styleText(["cyan", "bold"], " WebSearch Proxy Status"));
936
- console.log(styleText("cyan", "═".repeat(60)));
937
- console.log();
938
- try {
939
- const response = await fetch(`http://127.0.0.1:${port}/health`);
940
- if (response.ok) {
941
- const data = await response.json();
942
- console.log(styleText("green", ` Status: Running ✓`));
943
- console.log(styleText("white", ` Port: ${port}`));
944
- if (existsSync(pidFile)) {
945
- const { readFileSync: readFileSync$1 } = await import("node:fs");
946
- const pid = readFileSync$1(pidFile, "utf-8").trim();
947
- console.log(styleText("white", ` PID: ${pid}`));
948
- }
949
- if (data.droidRunning !== void 0) console.log(styleText("white", ` Droid running: ${data.droidRunning ? "yes (proxy will stay alive)" : "no"}`));
950
- if (data.idleTimeout !== void 0) if (data.idleTimeout > 0) {
951
- const idleMins = Math.floor((data.idleSeconds || 0) / 60);
952
- const idleSecs = (data.idleSeconds || 0) % 60;
953
- if (data.droidRunning) console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s (won't shutdown while droid runs)`));
954
- else if (data.willShutdownIn !== null) {
955
- const shutdownMins = Math.floor((data.willShutdownIn || 0) / 60);
956
- const shutdownSecs = (data.willShutdownIn || 0) % 60;
957
- console.log(styleText("white", ` Idle: ${idleMins}m ${idleSecs}s`));
958
- console.log(styleText("white", ` Auto-shutdown in: ${shutdownMins}m ${shutdownSecs}s`));
959
- }
960
- } else console.log(styleText("white", ` Auto-shutdown: disabled`));
961
- console.log(styleText("white", ` Log: ${logFile}`));
962
- console.log();
963
- console.log(styleText("gray", "To stop the proxy manually:"));
964
- console.log(styleText("cyan", " npx droid-patch proxy-stop"));
965
- console.log();
966
- console.log(styleText("gray", "To disable auto-shutdown:"));
967
- console.log(styleText("cyan", " export DROID_PROXY_IDLE_TIMEOUT=0"));
968
- }
969
- } catch {
970
- console.log(styleText("yellow", ` Status: Not running`));
971
- console.log();
972
- console.log(styleText("gray", "The proxy will start automatically when you run droid-full."));
973
- console.log(styleText("gray", "It will auto-shutdown after 5 minutes of idle (configurable)."));
974
- }
975
- console.log();
976
- }).command("proxy-stop", "Stop the websearch proxy").action(async () => {
977
- const pidFile = "/tmp/droid-search-proxy.pid";
978
- if (!existsSync(pidFile)) {
979
- console.log(styleText("yellow", "Proxy is not running (no PID file)"));
980
- return;
981
- }
982
- try {
983
- const { readFileSync: readFileSync$1, unlinkSync: unlinkSync$1 } = await import("node:fs");
984
- const pid = readFileSync$1(pidFile, "utf-8").trim();
985
- process.kill(parseInt(pid), "SIGTERM");
986
- unlinkSync$1(pidFile);
987
- console.log(styleText("green", `[*] Proxy stopped (PID: ${pid})`));
988
- } catch (error) {
989
- console.log(styleText("yellow", `[!] Could not stop proxy: ${error.message}`));
990
- try {
991
- const { unlinkSync: unlinkSync$1 } = await import("node:fs");
992
- unlinkSync$1(pidFile);
993
- console.log(styleText("gray", "Cleaned up stale PID file"));
994
- } catch {}
995
- }
996
- }).command("proxy-log", "Show websearch proxy logs").action(async () => {
997
- const logFile = "/tmp/droid-search-proxy.log";
998
- if (!existsSync(logFile)) {
999
- console.log(styleText("yellow", "No log file found"));
1000
- return;
1001
- }
1002
- const { readFileSync: readFileSync$1 } = await import("node:fs");
1003
- const log = readFileSync$1(logFile, "utf-8");
1004
- const lines = log.split("\n").slice(-50);
1005
- console.log(styleText("cyan", "═".repeat(60)));
1006
- console.log(styleText(["cyan", "bold"], " WebSearch Proxy Logs (last 50 lines)"));
1007
- console.log(styleText("cyan", "═".repeat(60)));
1008
- console.log();
1009
- console.log(lines.join("\n"));
881
+ }).command("clear", "Remove all droid-patch aliases and related files").action(async () => {
882
+ await clearAllAliases();
1010
883
  }).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) => {
1011
884
  const aliasName = args?.[0];
1012
885
  const dryRun = options["dry-run"];
@@ -1071,7 +944,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1071
944
  });
1072
945
  if (meta.patches.apiBase) {
1073
946
  const originalUrl = "https://api.factory.ai";
1074
- const paddedUrl = meta.patches.apiBase.padEnd(originalUrl.length, " ");
947
+ const paddedUrl = meta.patches.apiBase.padEnd(22, " ");
1075
948
  patches.push({
1076
949
  name: "apiBase",
1077
950
  description: `Replace Factory API URL with "${meta.patches.apiBase}"`,
@@ -1104,19 +977,23 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1104
977
  pattern: Buffer.from("supportedReasoningEfforts.length<=1"),
1105
978
  replacement: Buffer.from("supportedReasoningEfforts.length<=0")
1106
979
  });
980
+ patches.push({
981
+ name: "reasoningEffortValidationBypass",
982
+ description: "Bypass reasoning effort validation (allows xhigh in settings.json)",
983
+ pattern: Buffer.from("if(R&&!B.supportedReasoningEfforts.includes(R))"),
984
+ replacement: Buffer.from("if(0&&!B.supportedReasoningEfforts.includes(R))")
985
+ });
1107
986
  }
1108
- const binsDir = join(homedir(), ".droid-patch", "bins");
1109
- const outputPath = join(binsDir, `${meta.name}-patched`);
987
+ const outputPath = join(join(homedir(), ".droid-patch", "bins"), `${meta.name}-patched`);
1110
988
  if (patches.length > 0) {
1111
- const result = await patchDroid({
989
+ if (!(await patchDroid({
1112
990
  inputPath: newBinaryPath,
1113
991
  outputPath,
1114
992
  patches,
1115
993
  dryRun: false,
1116
994
  backup: false,
1117
995
  verbose
1118
- });
1119
- if (!result.success) {
996
+ })).success) {
1120
997
  console.log(styleText("red", ` ✗ Failed to apply patches`));
1121
998
  failCount++;
1122
999
  continue;
@@ -1129,13 +1006,17 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1129
1006
  console.log(styleText("yellow", ` [!] Could not re-sign binary`));
1130
1007
  }
1131
1008
  }
1132
- if (meta.patches.websearch) {
1133
- const websearchDir = join(homedir(), ".droid-patch", "websearch");
1134
- const targetBinaryPath = patches.length > 0 ? outputPath : newBinaryPath;
1135
- await createWebSearchUnifiedFiles(websearchDir, targetBinaryPath, meta.name);
1009
+ if (meta.patches.websearch || !!meta.patches.proxy) {
1010
+ const forwardTarget = meta.patches.apiBase || meta.patches.proxy || "https://api.factory.ai";
1011
+ await createWebSearchUnifiedFiles(join(homedir(), ".droid-patch", "proxy"), patches.length > 0 ? outputPath : newBinaryPath, meta.name, forwardTarget);
1136
1012
  if (verbose) console.log(styleText("gray", ` Regenerated websearch wrapper`));
1013
+ if (meta.patches.proxy && !meta.patches.websearch) {
1014
+ meta.patches.websearch = true;
1015
+ meta.patches.apiBase = meta.patches.proxy;
1016
+ delete meta.patches.proxy;
1017
+ }
1137
1018
  }
1138
- meta.updatedAt = new Date().toISOString();
1019
+ meta.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1139
1020
  meta.originalBinaryPath = newBinaryPath;
1140
1021
  await saveAliasMetadata(meta);
1141
1022
  console.log(styleText("green", ` ✓ Updated successfully`));
@@ -1165,4 +1046,5 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
1165
1046
  });
1166
1047
 
1167
1048
  //#endregion
1168
- //# sourceMappingURL=cli.js.map
1049
+ export { };
1050
+ //# sourceMappingURL=cli.mjs.map