droid-patch 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -18
- package/README.zh-CN.md +32 -18
- package/dist/cli.mjs +2 -2
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -57,20 +57,20 @@ npx droid-patch --skip-login -o /path/to/dir my-droid
|
|
|
57
57
|
|
|
58
58
|
### Available Options
|
|
59
59
|
|
|
60
|
-
| Option | Description
|
|
61
|
-
| --------------------- |
|
|
62
|
-
| `--is-custom` | Patch `isCustom:!0` to `isCustom:!1` (enables context compression for custom models)
|
|
63
|
-
| `--skip-login` | Bypass login by injecting a fake `FACTORY_API_KEY` into the binary
|
|
64
|
-
| `--api-base <url>` | Replace
|
|
65
|
-
| `--websearch` | Inject local WebSearch proxy with multiple search providers
|
|
66
|
-
| `--standalone` | Standalone mode: mock non-LLM Factory APIs (use with `--websearch`)
|
|
67
|
-
| `--reasoning-effort` | Enable reasoning effort UI selector for custom models (set to high)
|
|
68
|
-
| `--disable-telemetry` | Disable telemetry and Sentry error reporting
|
|
69
|
-
| `--dry-run` | Verify patches without actually modifying the binary
|
|
70
|
-
| `-p, --path <path>` | Path to the droid binary (default: `~/.droid/bin/droid`)
|
|
71
|
-
| `-o, --output <dir>` | Output directory for patched binary (creates file without alias)
|
|
72
|
-
| `--no-backup` | Skip creating backup of original binary
|
|
73
|
-
| `-v, --verbose` | Enable verbose output
|
|
60
|
+
| Option | Description |
|
|
61
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------------ |
|
|
62
|
+
| `--is-custom` | Patch `isCustom:!0` to `isCustom:!1` (enables context compression for custom models) |
|
|
63
|
+
| `--skip-login` | Bypass login by injecting a fake `FACTORY_API_KEY` into the binary |
|
|
64
|
+
| `--api-base <url>` | Replace API URL (standalone: binary patch, max 22 chars; with `--websearch`: proxy forward target, no limit) |
|
|
65
|
+
| `--websearch` | Inject local WebSearch proxy with multiple search providers |
|
|
66
|
+
| `--standalone` | Standalone mode: mock non-LLM Factory APIs (use with `--websearch`) |
|
|
67
|
+
| `--reasoning-effort` | Enable reasoning effort UI selector for custom models (set to high) |
|
|
68
|
+
| `--disable-telemetry` | Disable telemetry and Sentry error reporting |
|
|
69
|
+
| `--dry-run` | Verify patches without actually modifying the binary |
|
|
70
|
+
| `-p, --path <path>` | Path to the droid binary (default: `~/.droid/bin/droid`) |
|
|
71
|
+
| `-o, --output <dir>` | Output directory for patched binary (creates file without alias) |
|
|
72
|
+
| `--no-backup` | Skip creating backup of original binary |
|
|
73
|
+
| `-v, --verbose` | Enable verbose output |
|
|
74
74
|
|
|
75
75
|
### Manage Aliases and Files
|
|
76
76
|
|
|
@@ -164,13 +164,14 @@ Replaces all `process.env.FACTORY_API_KEY` references in the binary with a hardc
|
|
|
164
164
|
|
|
165
165
|
### `--api-base <url>`
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Replace the Factory API base URL. Has different behavior depending on usage:
|
|
168
168
|
|
|
169
|
-
**
|
|
169
|
+
**1. Standalone (without `--websearch`)**
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
Binary patch to replace `https://api.factory.ai` with your custom URL.
|
|
172
172
|
|
|
173
|
-
**
|
|
173
|
+
- **Limitation**: URL must be 22 characters or less (same length as original URL)
|
|
174
|
+
- **Use case**: Direct API URL replacement without proxy
|
|
174
175
|
|
|
175
176
|
```bash
|
|
176
177
|
# Valid URLs (<=22 chars)
|
|
@@ -181,6 +182,19 @@ npx droid-patch --api-base "http://localhost:80" droid-local
|
|
|
181
182
|
npx droid-patch --api-base "http://my-long-domain.com:3000" droid # Error!
|
|
182
183
|
```
|
|
183
184
|
|
|
185
|
+
**2. With `--websearch`**
|
|
186
|
+
|
|
187
|
+
Sets the forward target URL for the WebSearch proxy by configuring the `FACTORY_API` variable in the proxy script.
|
|
188
|
+
|
|
189
|
+
- **No length limitation**: Any valid URL can be used
|
|
190
|
+
- **Use case**: Forward non-search requests to your custom LLM backend
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Forward to custom backend (no length limit)
|
|
194
|
+
npx droid-patch --websearch --api-base "http://127.0.0.1:20002" droid-custom
|
|
195
|
+
npx droid-patch --websearch --api-base "http://my-proxy.example.com:3000" droid-custom
|
|
196
|
+
```
|
|
197
|
+
|
|
184
198
|
### `--websearch`
|
|
185
199
|
|
|
186
200
|
Enables WebSearch functionality through a local proxy server that intercepts `/api/tools/exa/search` requests.
|
package/README.zh-CN.md
CHANGED
|
@@ -57,20 +57,20 @@ npx droid-patch --skip-login -o /path/to/dir my-droid
|
|
|
57
57
|
|
|
58
58
|
### 可用选项
|
|
59
59
|
|
|
60
|
-
| 选项 | 说明
|
|
61
|
-
| --------------------- |
|
|
62
|
-
| `--is-custom` | 将 `isCustom:!0` 修改为 `isCustom:!1`(为自定义模型启用上下文压缩)
|
|
63
|
-
| `--skip-login` | 通过注入假的 `FACTORY_API_KEY` 跳过登录验证
|
|
64
|
-
| `--api-base <url>` |
|
|
65
|
-
| `--websearch` | 注入本地 WebSearch 代理,支持多个搜索提供商
|
|
66
|
-
| `--standalone` | 独立模式:mock 非 LLM 的 Factory API(与 `--websearch` 配合使用)
|
|
67
|
-
| `--reasoning-effort` | 为自定义模型启用推理强度 UI 选择器(设置为 high)
|
|
68
|
-
| `--disable-telemetry` | 禁用遥测数据上传和 Sentry 错误报告
|
|
69
|
-
| `--dry-run` | 验证修补但不实际修改二进制文件
|
|
70
|
-
| `-p, --path <path>` | droid 二进制文件路径(默认:`~/.droid/bin/droid`)
|
|
71
|
-
| `-o, --output <dir>` | 修补后二进制文件的输出目录(直接创建文件,不创建别名)
|
|
72
|
-
| `--no-backup` | 跳过创建原始二进制文件的备份
|
|
73
|
-
| `-v, --verbose` | 启用详细输出
|
|
60
|
+
| 选项 | 说明 |
|
|
61
|
+
| --------------------- | ----------------------------------------------------------------------------------------------- |
|
|
62
|
+
| `--is-custom` | 将 `isCustom:!0` 修改为 `isCustom:!1`(为自定义模型启用上下文压缩) |
|
|
63
|
+
| `--skip-login` | 通过注入假的 `FACTORY_API_KEY` 跳过登录验证 |
|
|
64
|
+
| `--api-base <url>` | 替换 API URL(单独使用:二进制补丁,最多 22 字符;与 `--websearch` 配合:代理转发目标,无限制) |
|
|
65
|
+
| `--websearch` | 注入本地 WebSearch 代理,支持多个搜索提供商 |
|
|
66
|
+
| `--standalone` | 独立模式:mock 非 LLM 的 Factory API(与 `--websearch` 配合使用) |
|
|
67
|
+
| `--reasoning-effort` | 为自定义模型启用推理强度 UI 选择器(设置为 high) |
|
|
68
|
+
| `--disable-telemetry` | 禁用遥测数据上传和 Sentry 错误报告 |
|
|
69
|
+
| `--dry-run` | 验证修补但不实际修改二进制文件 |
|
|
70
|
+
| `-p, --path <path>` | droid 二进制文件路径(默认:`~/.droid/bin/droid`) |
|
|
71
|
+
| `-o, --output <dir>` | 修补后二进制文件的输出目录(直接创建文件,不创建别名) |
|
|
72
|
+
| `--no-backup` | 跳过创建原始二进制文件的备份 |
|
|
73
|
+
| `-v, --verbose` | 启用详细输出 |
|
|
74
74
|
|
|
75
75
|
### 管理别名和文件
|
|
76
76
|
|
|
@@ -164,13 +164,14 @@ export PATH="$HOME/.droid-patch/aliases:$PATH"
|
|
|
164
164
|
|
|
165
165
|
### `--api-base <url>`
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
替换 Factory API 基础 URL。根据使用方式有不同的行为:
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
**1. 单独使用(不与 `--websearch` 配合)**
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
通过二进制补丁将 `https://api.factory.ai` 替换为你的自定义 URL。
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
- **限制**:URL 必须不超过 22 个字符(与原始 URL 长度相同)
|
|
174
|
+
- **用途**:直接替换 API URL,不使用代理
|
|
174
175
|
|
|
175
176
|
```bash
|
|
176
177
|
# 有效的 URL(<=22 个字符)
|
|
@@ -181,6 +182,19 @@ npx droid-patch --api-base "http://localhost:80" droid-local
|
|
|
181
182
|
npx droid-patch --api-base "http://my-long-domain.com:3000" droid # 错误!
|
|
182
183
|
```
|
|
183
184
|
|
|
185
|
+
**2. 与 `--websearch` 配合使用**
|
|
186
|
+
|
|
187
|
+
通过配置代理脚本中的 `FACTORY_API` 变量来设置 WebSearch 代理的转发目标 URL。
|
|
188
|
+
|
|
189
|
+
- **无长度限制**:可以使用任何有效的 URL
|
|
190
|
+
- **用途**:将非搜索请求转发到你的自定义 LLM 后端
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# 转发到自定义后端(无长度限制)
|
|
194
|
+
npx droid-patch --websearch --api-base "http://127.0.0.1:20002" droid-custom
|
|
195
|
+
npx droid-patch --websearch --api-base "http://my-proxy.example.com:3000" droid-custom
|
|
196
|
+
```
|
|
197
|
+
|
|
184
198
|
### `--websearch`
|
|
185
199
|
|
|
186
200
|
通过本地代理服务器启用 WebSearch 功能,拦截 `/api/tools/exa/search` 请求。
|
package/dist/cli.mjs
CHANGED
|
@@ -675,7 +675,7 @@ function findDefaultDroidPath() {
|
|
|
675
675
|
for (const p of paths) if (existsSync(p)) return p;
|
|
676
676
|
return join(home, ".droid", "bin", "droid");
|
|
677
677
|
}
|
|
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
|
|
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 API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)").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) => {
|
|
679
679
|
const alias = args?.[0];
|
|
680
680
|
const isCustom = options["is-custom"];
|
|
681
681
|
const skipLogin = options["skip-login"];
|
|
@@ -748,7 +748,7 @@ bin("droid-patch", "CLI tool to patch droid binary with various modifications").
|
|
|
748
748
|
console.log(styleText("yellow", "No patch flags specified. Available patches:"));
|
|
749
749
|
console.log(styleText("gray", " --is-custom Patch isCustom for custom models"));
|
|
750
750
|
console.log(styleText("gray", " --skip-login Bypass login by injecting a fake API key"));
|
|
751
|
-
console.log(styleText("gray", " --api-base Replace
|
|
751
|
+
console.log(styleText("gray", " --api-base Replace API URL (standalone: max 22 chars; with --websearch: no limit)"));
|
|
752
752
|
console.log(styleText("gray", " --websearch Enable local WebSearch proxy"));
|
|
753
753
|
console.log(styleText("gray", " --reasoning-effort Set reasoning effort level for custom models"));
|
|
754
754
|
console.log(styleText("gray", " --disable-telemetry Disable telemetry and Sentry error reporting"));
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["patches: Patch[]","unlink","metaList: Awaited<ReturnType<typeof loadAliasMetadata>>[]"],"sources":["../src/websearch-patch.ts","../src/cli.ts"],"sourcesContent":["import type { Patch } from \"./patcher.ts\";\nimport { writeFile, chmod, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\n/**\n * WebSearch Patch Generator\n *\n * Since injecting code directly into binary is complex (requires exact byte length matching),\n * we use a more practical approach:\n *\n * 1. --websearch option will:\n * a) Generate a standalone search proxy server script\n * b) Modify droid's API URL to point to local proxy (using --api-base)\n * c) Create a wrapper script to start both proxy and droid\n *\n * Environment variables:\n * - GOOGLE_PSE_API_KEY: Google Programmable Search Engine API Key\n * - GOOGLE_PSE_CX: Google Custom Search Engine ID\n * - If not set, will fallback to DuckDuckGo\n */\n\n/**\n * Generate search proxy server code\n */\nfunction generateSearchProxyServerCode(): string {\n return `#!/usr/bin/env node\n/**\n * Droid WebSearch Proxy Server\n * Auto-generated by droid-patch --websearch\n * \n * Supports:\n * - Google PSE (requires GOOGLE_PSE_API_KEY and GOOGLE_PSE_CX)\n * - DuckDuckGo (free fallback)\n */\n\nconst http = require('http');\nconst https = require('https');\n\nconst FACTORY_API = 'https://api.factory.ai';\n\n// Auto-find available port\nfunction findAvailablePort(startPort = 23119) {\n return new Promise((resolve, reject) => {\n const net = require('net');\n const server = net.createServer();\n \n server.listen(startPort, '127.0.0.1', () => {\n const port = server.address().port;\n server.close(() => resolve(port));\n });\n \n server.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n // Port is in use, try next one\n resolve(findAvailablePort(startPort + 1));\n } else {\n reject(err);\n }\n });\n });\n}\n\nlet PORT = process.env.SEARCH_PROXY_PORT || 23119;\n\n// === Search Implementation ===\n\nasync function searchGooglePSE(query, numResults, apiKey, cx) {\n // Use curl command\n const { execSync } = require('child_process');\n \n const url = 'https://www.googleapis.com/customsearch/v1?key=' + apiKey + '&cx=' + cx + '&q=' + encodeURIComponent(query) + '&num=' + Math.min(numResults, 10);\n const curlCmd = \\`curl -s \"\\${url}\"\\`;\n \n try {\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n \n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n snippet: item.snippet,\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n throw new Error('Google PSE error: ' + e.message);\n }\n}\n\nasync function searchDuckDuckGo(query, numResults) {\n // Use curl command, because Node.js fetch may have issues in some environments\n const { execSync } = require('child_process');\n\n // Method 1: Try using DuckDuckGo HTML lite version (via curl)\n try {\n const curlCmd = \\`curl -s -X POST \"https://lite.duckduckgo.com/lite/\" -H \"Content-Type: application/x-www-form-urlencoded\" -H \"User-Agent: Mozilla/5.0\" -d \"q=\\${encodeURIComponent(query)}\"\\`;\n const html = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n\n if (html && html.length > 1000) {\n const results = parseDDGLiteHTML(html, numResults);\n if (results.length > 0) {\n console.error('[search] DDG lite returned ' + results.length + ' results');\n return results;\n }\n }\n } catch (e) {\n console.error('[search] DDG lite (curl) failed:', e.message);\n }\n\n // Method 2: Fallback to Instant Answer API (via curl)\n try {\n const apiUrl = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1';\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n \n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n snippet: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n snippet: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n snippet: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n console.error('[search] DDG API returned ' + results.length + ' results');\n return results;\n }\n } catch (e) {\n console.error('[search] DDG API (curl) failed:', e.message);\n }\n\n return [];\n}\n\n// Parse DuckDuckGo Lite HTML\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n\n // Match result links - DuckDuckGo Lite format\n // <a rel=\"nofollow\" href=\"URL\">TITLE</a>\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n // Extract all links\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n // Skip DuckDuckGo internal links\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n // Decode redirect URL\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n // Extract snippets\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n // Combine results\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n snippet: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n \n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n const googleApiKey = process.env.GOOGLE_PSE_API_KEY;\n const googleCx = process.env.GOOGLE_PSE_CX;\n\n // Try Google PSE first\n if (googleApiKey && googleCx) {\n try {\n console.error('[search] Trying Google PSE...');\n const results = await searchGooglePSE(query, numResults, googleApiKey, googleCx);\n if (results.length > 0) {\n console.error('[search] Google PSE returned ' + results.length + ' results');\n return { results, source: 'google-pse' };\n }\n } catch (e) {\n console.error('[search] Google PSE failed:', e.message);\n }\n }\n\n // Fallback to DuckDuckGo\n try {\n console.error('[search] Using DuckDuckGo...');\n const results = await searchDuckDuckGo(query, numResults);\n console.error('[search] DuckDuckGo returned ' + results.length + ' results');\n return { results, source: 'duckduckgo' };\n } catch (e) {\n console.error('[search] DuckDuckGo failed:', e.message);\n }\n\n return { results: [], source: 'none' };\n}\n\n// === HTTP Server ===\n\nconst server = http.createServer(async (req, res) => {\n const url = new URL(req.url, 'http://' + req.headers.host);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n status: 'ok',\n google: !!(process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX),\n duckduckgo: true\n }));\n return;\n }\n\n // Search endpoint\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', chunk => body += chunk);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n console.error('[search] Query: \"' + query + '\"');\n \n const { results, source } = await search(query, numResults || 10);\n console.error('[search] ' + results.length + ' results from ' + source);\n \n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n console.error('[search] Error:', e);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // Proxy other requests to Factory API\n console.error('[proxy] ' + req.method + ' ' + url.pathname);\n \n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n \n const proxyReq = https.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n\n proxyReq.on('error', e => {\n console.error('[proxy] Error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed' }));\n });\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n});\n\n// Start server (async, auto-find available port)\n(async () => {\n const fs = require('fs');\n const path = require('path');\n\n // If port not specified, auto-find available port\n if (!process.env.SEARCH_PROXY_PORT) {\n PORT = await findAvailablePort(23119);\n }\n\n server.listen(PORT, '127.0.0.1', () => {\n const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;\n\n // Write port number to temp file for wrapper script to read\n const portFile = process.env.SEARCH_PROXY_PORT_FILE || path.join(require('os').tmpdir(), 'droid-search-proxy-' + process.pid + '.port');\n fs.writeFileSync(portFile, PORT.toString());\n\n // Output port number to stdout (for parent process to capture)\n console.log('PORT=' + PORT);\n \n console.error('');\n console.error('╔═══════════════════════════════════════════════════════════════╗');\n console.error('║ Droid WebSearch Proxy ║');\n console.error('╠═══════════════════════════════════════════════════════════════╣');\n console.error('║ 🔍 Google PSE: ' + (hasGoogle ? 'Configured ✓' : 'Not set (set GOOGLE_PSE_API_KEY & CX)').padEnd(45) + '║');\n console.error('║ 🦆 DuckDuckGo: Always available ║');\n console.error('║ 🚀 Server: http://127.0.0.1:' + PORT + ' ║'.slice(0, 65) + '║');\n console.error('╚═══════════════════════════════════════════════════════════════╝');\n console.error('');\n });\n})();\n\n// Handle graceful shutdown\nprocess.on('SIGTERM', () => server.close());\nprocess.on('SIGINT', () => server.close());\n`;\n}\n\n/**\n * Generate wrapper script, auto-start proxy and droid\n */\nfunction generateWrapperScript(droidPath: string, proxyScriptPath: string): string {\n return `#!/bin/bash\n# Droid with WebSearch Proxy\n# Auto-generated by droid-patch --websearch\n\nPROXY_SCRIPT=\"${proxyScriptPath}\"\nDROID_BIN=\"${droidPath}\"\nPORT_FILE=\"/tmp/droid-search-proxy-$$.port\"\n\n# Start proxy and get dynamic port\nstart_proxy() {\n # Start proxy, capture output to get port\n SEARCH_PROXY_PORT_FILE=\"$PORT_FILE\" node \"$PROXY_SCRIPT\" &\n PROXY_PID=$!\n\n # Wait for proxy to start and get port\n for i in {1..20}; do\n if [ -f \"$PORT_FILE\" ]; then\n PORT=$(cat \"$PORT_FILE\")\n if curl -s \"http://127.0.0.1:$PORT/health\" > /dev/null 2>&1; then\n echo \"[websearch] Proxy started on port $PORT\"\n return 0\n fi\n fi\n sleep 0.2\n done\n\n echo \"[websearch] Failed to start proxy\"\n kill $PROXY_PID 2>/dev/null\n return 1\n}\n\n# Cleanup function\ncleanup() {\n [ -n \"$PROXY_PID\" ] && kill $PROXY_PID 2>/dev/null\n [ -f \"$PORT_FILE\" ] && rm -f \"$PORT_FILE\"\n}\ntrap cleanup EXIT\n\n# Start proxy\nif ! start_proxy; then\n exit 1\nfi\n\n# Run droid\nexport FACTORY_API_BASE_URL_OVERRIDE=\"http://127.0.0.1:$PORT\"\nexec \"$DROID_BIN\" \"$@\"\n`;\n}\n\n/**\n * Generate WebSearch Patch\n *\n * Since injecting code directly into binary is complex, we use the following strategy:\n * 1. Create proxy server script\n * 2. Modify API URL to point to local\n * 3. Return a combined patch\n */\nexport function generateWebSearchPatch(): Patch | null {\n // Return a URL replacement patch\n // Use local proxy port 23119 (idle port)\n const originalUrl = \"https://api.factory.ai\";\n const localUrl = \"http://127.0.0.1:23119\";\n\n // Need to pad to same length\n if (localUrl.length > originalUrl.length) {\n console.error(`[websearch] Local URL too long: ${localUrl.length} > ${originalUrl.length}`);\n return null;\n }\n\n const paddedUrl = localUrl.padEnd(originalUrl.length, \" \");\n\n return {\n name: \"webSearch\",\n description: `Replace API URL with local proxy (${localUrl})`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n };\n}\n\n/**\n * Create WebSearch proxy files\n */\nexport async function createWebSearchProxyFiles(\n outputDir: string,\n droidPath: string,\n aliasName: string,\n): Promise<{ proxyScript: string; wrapperScript: string }> {\n // Ensure directory exists\n if (!existsSync(outputDir)) {\n await mkdir(outputDir, { recursive: true });\n }\n\n const proxyScriptPath = join(outputDir, `${aliasName}-search-proxy.js`);\n const wrapperScriptPath = join(outputDir, `${aliasName}-with-search`);\n\n // Write proxy server script\n await writeFile(proxyScriptPath, generateSearchProxyServerCode());\n console.log(`[*] Created search proxy: ${proxyScriptPath}`);\n\n // Write wrapper script\n await writeFile(wrapperScriptPath, generateWrapperScript(droidPath, proxyScriptPath));\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper script: ${wrapperScriptPath}`);\n\n return {\n proxyScript: proxyScriptPath,\n wrapperScript: wrapperScriptPath,\n };\n}\n\n/**\n * Get proxy server code (for export)\n */\nexport function getSearchProxyCode(): string {\n return generateSearchProxyServerCode();\n}\n\n/**\n * Generate Bun preload script\n * This script executes before droid main program, starts search proxy\n */\nfunction generatePreloadScript(): string {\n return `// Droid WebSearch Preload Script\n// Auto-generated by droid-patch --websearch-preload\n// Start search proxy before droid main program\n\nconst http = require('http');\nconst https = require('https');\nconst { execSync } = require('child_process');\n\nconst PORT = process.env.DROID_SEARCH_PORT || 23119;\nconst FACTORY_API = 'https://api.factory.ai';\n\n// Google PSE search\nasync function searchGooglePSE(query, num) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n \n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(num, 10)}\\`;\n const res = await fetch(url);\n const data = await res.json();\n if (data.error) return null;\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || ''\n }));\n } catch (e) {\n return null;\n }\n}\n\n// DuckDuckGo search (use curl for reliability)\nfunction searchDuckDuckGo(query, num) {\n try {\n const url = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const output = execSync(\\`curl -s \"\\${url}\"\\`, { encoding: 'utf8', timeout: 10000 });\n const data = JSON.parse(output);\n const results = [];\n\n if (data.AbstractText && data.AbstractURL) {\n results.push({ title: data.Heading || query, url: data.AbstractURL, content: data.AbstractText });\n }\n\n for (const t of (data.RelatedTopics || [])) {\n if (results.length >= num) break;\n if (t.Text && t.FirstURL) {\n results.push({ title: t.Text.split(' - ')[0], url: t.FirstURL, content: t.Text });\n }\n // Handle subcategories\n if (t.Topics) {\n for (const sub of t.Topics) {\n if (results.length >= num) break;\n if (sub.Text && sub.FirstURL) {\n results.push({ title: sub.Text.split(' - ')[0], url: sub.FirstURL, content: sub.Text });\n }\n }\n }\n }\n return results;\n } catch (e) {\n return [];\n }\n}\n\n// Search function\nasync function search(query, num) {\n // Try Google PSE first\n const googleResults = await searchGooglePSE(query, num);\n if (googleResults && googleResults.length > 0) {\n console.error('[preload-search] Using Google PSE');\n return googleResults;\n }\n\n // Fallback to DuckDuckGo\n console.error('[preload-search] Using DuckDuckGo');\n return searchDuckDuckGo(query, num);\n}\n\n// Check if port is already in use\nfunction isPortInUse(port) {\n try {\n execSync(\\`curl -s http://127.0.0.1:\\${port}/health\\`, { timeout: 1000 });\n return true;\n } catch {\n return false;\n }\n}\n\n// Skip if proxy already running\nif (isPortInUse(PORT)) {\n console.error(\\`[preload] Search proxy already running on port \\${PORT}\\`);\n} else {\n // Start proxy server\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url, \\`http://\\${req.headers.host}\\`);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // Search endpoint\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', c => body += c);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n console.error(\\`[preload-search] Query: \"\\${query}\"\\`);\n const results = await search(query, numResults || 10);\n console.error(\\`[preload-search] Found \\${results.length} results\\`);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n console.error('[preload-search] Error:', e.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // Proxy other requests to Factory API\n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n const proxyReq = https.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n proxyReq.on('error', (e) => {\n console.error('[preload-proxy] Error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed' }));\n });\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n });\n\n server.listen(PORT, '127.0.0.1', () => {\n console.error(\\`[preload] Search proxy started on http://127.0.0.1:\\${PORT}\\`);\n });\n}\n`;\n}\n\n/**\n * Generate bunfig.toml content\n */\nfunction generateBunfigToml(preloadScriptPath: string): string {\n return `# Droid WebSearch Configuration\n# Auto-generated by droid-patch --websearch-preload\n\npreload = [\"${preloadScriptPath}\"]\n`;\n}\n\n/**\n * Generate preload wrapper script\n * This script cd's to the bunfig.toml directory, then executes droid\n */\nfunction generatePreloadWrapperScript(droidPath: string, bunfigDir: string): string {\n return `#!/bin/bash\n# Droid with WebSearch (Preload)\n# Auto-generated by droid-patch --preload\n\nBUNFIG_DIR=\"${bunfigDir}\"\nDROID_BIN=\"${droidPath}\"\nORIGINAL_DIR=\"$(pwd)\"\n\n# cd to bunfig.toml directory (Bun reads bunfig.toml from cwd)\ncd \"$BUNFIG_DIR\"\n\n# Execute droid, pass all arguments, set working directory to original\nexec \"$DROID_BIN\" --cwd \"$ORIGINAL_DIR\" \"$@\"\n`;\n}\n\n/**\n * Create WebSearch files using Preload method\n *\n * Advantages:\n * - No need to modify binary\n * - Uses Bun's native preload mechanism\n *\n * Files created:\n * - preload script (search proxy)\n * - bunfig.toml (Bun configuration)\n * - wrapper script (directly executable command)\n */\nexport async function createWebSearchPreloadFiles(\n droidDir: string,\n droidPath: string,\n aliasName: string,\n): Promise<{\n preloadScript: string;\n bunfigPath: string;\n wrapperScript: string;\n}> {\n // Ensure directory exists\n if (!existsSync(droidDir)) {\n await mkdir(droidDir, { recursive: true });\n }\n\n const preloadScriptPath = join(droidDir, `${aliasName}-search-preload.js`);\n const bunfigPath = join(droidDir, \"bunfig.toml\");\n const wrapperScriptPath = join(droidDir, aliasName);\n\n // Write preload script\n await writeFile(preloadScriptPath, generatePreloadScript());\n console.log(`[*] Created preload script: ${preloadScriptPath}`);\n\n // Write bunfig.toml\n await writeFile(bunfigPath, generateBunfigToml(preloadScriptPath));\n console.log(`[*] Created bunfig.toml: ${bunfigPath}`);\n\n // Write wrapper script\n await writeFile(wrapperScriptPath, generatePreloadWrapperScript(droidPath, droidDir));\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper: ${wrapperScriptPath}`);\n\n return {\n preloadScript: preloadScriptPath,\n bunfigPath: bunfigPath,\n wrapperScript: wrapperScriptPath,\n };\n}\n\n/**\n * Get preload script code (for export)\n */\nexport function getPreloadScriptCode(): string {\n return generatePreloadScript();\n}\n\n/**\n * Generate unified Fetch Hook Preload script\n * Directly hooks globalThis.fetch, no proxy server needed\n * @internal Reserved for future use - alternative to proxy server approach\n */\nfunction _generateFetchHookPreload(): string {\n return `// Droid WebSearch Fetch Hook\n// Auto-generated by droid-patch --websearch\n// Hook globalThis.fetch to intercept search requests\n\nconst DEBUG = process.env.DROID_SEARCH_DEBUG === '1';\n\nfunction log(...args) {\n if (DEBUG) console.error('[websearch]', ...args);\n}\n\n// === Search Implementation ===\n\nasync function searchGooglePSE(query, numResults) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n\n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(numResults, 10)}\\`;\n const res = await fetch(url);\n const data = await res.json();\n if (data.error) {\n log('Google PSE error:', data.error.message);\n return null;\n }\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n log('Google PSE failed:', e.message);\n return null;\n }\n}\n\nasync function searchDuckDuckGo(query, numResults) {\n const { execSync } = require('child_process');\n\n // Method 1: Try DuckDuckGo HTML lite\n try {\n const curlCmd = \\`curl -s -X POST \"https://lite.duckduckgo.com/lite/\" -H \"Content-Type: application/x-www-form-urlencoded\" -H \"User-Agent: Mozilla/5.0\" -d \"q=\\${encodeURIComponent(query)}\"\\`;\n const html = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n\n if (html && html.length > 1000) {\n const results = parseDDGLiteHTML(html, numResults);\n if (results.length > 0) {\n log('DDG lite:', results.length, 'results');\n return results;\n }\n }\n } catch (e) {\n log('DDG lite failed:', e.message);\n }\n\n // Method 2: Fallback to Instant Answer API\n try {\n const apiUrl = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n content: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n content: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n content: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n log('DDG API:', results.length, 'results');\n return results;\n }\n } catch (e) {\n log('DDG API failed:', e.message);\n }\n\n return [];\n}\n\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n content: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n // Try Google PSE first\n const googleResults = await searchGooglePSE(query, numResults);\n if (googleResults && googleResults.length > 0) {\n log('Using Google PSE');\n return { results: googleResults, source: 'google-pse' };\n }\n\n // Fallback to DuckDuckGo\n log('Using DuckDuckGo');\n const ddgResults = await searchDuckDuckGo(query, numResults);\n return { results: ddgResults, source: 'duckduckgo' };\n}\n\n// === Fetch Hook ===\n\nconst originalFetch = globalThis.fetch;\n\nglobalThis.fetch = async function(input, init) {\n const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url);\n\n // Intercept search requests\n if (url && url.includes('/api/tools/exa/search')) {\n log('Intercepted search request');\n\n try {\n let body = init?.body;\n if (body && typeof body !== 'string') {\n body = await new Response(body).text();\n }\n\n const { query, numResults } = JSON.parse(body || '{}');\n log('Query:', query);\n\n const { results, source } = await search(query, numResults || 10);\n log('Results:', results.length, 'from', source);\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n });\n } catch (e) {\n log('Search error:', e.message);\n return new Response(JSON.stringify({ error: String(e), results: [] }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' }\n });\n }\n }\n\n // Pass through all other requests\n return originalFetch.apply(this, arguments);\n};\n\n// Also hook Bun.fetch if available\nif (typeof Bun !== 'undefined' && Bun.fetch) {\n const originalBunFetch = Bun.fetch;\n Bun.fetch = globalThis.fetch;\n}\n\nlog('Fetch hook installed');\n`;\n}\n\n/**\n * Generate search proxy server code (runs in background)\n * Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,\n * use a local proxy server to intercept search requests instead\n *\n * Each droid instance runs its own proxy server.\n * The proxy is killed automatically when droid exits.\n * @param factoryApiUrl - Custom Factory API URL (default: https://api.factory.ai)\n */\nfunction generateSearchProxyServer(factoryApiUrl: string = \"https://api.factory.ai\"): string {\n return `#!/usr/bin/env node\n// Droid WebSearch Proxy Server\n// Auto-generated by droid-patch --websearch\n// This proxy runs as a child process of droid and is killed when droid exits\n\nconst http = require('http');\nconst https = require('https');\nconst { execSync } = require('child_process');\nconst fs = require('fs');\n\nconst DEBUG = process.env.DROID_SEARCH_DEBUG === '1';\nconst PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0'); // 0 = auto-assign\nconst FACTORY_API = '${factoryApiUrl}';\n\nfunction log(...args) {\n if (DEBUG) console.error('[websearch]', ...args);\n}\n\n// === Search Implementation ===\n\n// Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE\nasync function searchSmitheryExa(query, numResults) {\n const apiKey = process.env.SMITHERY_API_KEY;\n const profile = process.env.SMITHERY_PROFILE;\n if (!apiKey || !profile) return null;\n\n try {\n // Construct URL with authentication\n const serverUrl = \\`https://server.smithery.ai/exa/mcp?api_key=\\${encodeURIComponent(apiKey)}&profile=\\${encodeURIComponent(profile)}\\`;\n log('Smithery Exa request');\n\n // Use MCP protocol to call the search tool via HTTP POST\n const requestBody = JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/call',\n params: {\n name: 'web_search_exa',\n arguments: {\n query: query,\n numResults: numResults\n }\n }\n });\n\n const curlCmd = \\`curl -s -X POST \"\\${serverUrl}\" -H \"Content-Type: application/json\" -d '\\${requestBody.replace(/'/g, \"'\\\\\\\\\\\\\\\\''\")}'\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });\n const response = JSON.parse(jsonStr);\n\n // Parse MCP response\n if (response.result && response.result.content) {\n // MCP returns content as array of text blocks\n const textContent = response.result.content.find(c => c.type === 'text');\n if (textContent && textContent.text) {\n try {\n const searchResults = JSON.parse(textContent.text);\n if (Array.isArray(searchResults) && searchResults.length > 0) {\n return searchResults.slice(0, numResults).map(item => ({\n title: item.title || '',\n url: item.url || '',\n content: item.text || item.snippet || item.highlights?.join(' ') || '',\n publishedDate: item.publishedDate || null,\n author: item.author || null,\n score: item.score || null\n }));\n }\n } catch (parseErr) {\n log('Smithery response parsing failed');\n }\n }\n }\n\n if (response.error) {\n log('Smithery Exa error:', response.error.message || response.error);\n return null;\n }\n } catch (e) {\n log('Smithery Exa failed:', e.message);\n return null;\n }\n return null;\n}\n\nasync function searchGooglePSE(query, numResults) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n\n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(numResults, 10)}\\`;\n log('Google PSE request:', url.replace(apiKey, '***'));\n\n const curlCmd = \\`curl -s \"\\${url}\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.error) {\n log('Google PSE error:', data.error.message);\n return null;\n }\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n log('Google PSE failed:', e.message);\n return null;\n }\n}\n\n// SearXNG - self-hosted meta search engine\nasync function searchSearXNG(query, numResults) {\n const searxngUrl = process.env.SEARXNG_URL;\n if (!searxngUrl) return null;\n\n try {\n const url = \\`\\${searxngUrl}/search?q=\\${encodeURIComponent(query)}&format=json&engines=google,bing,duckduckgo\\`;\n log('SearXNG request:', url);\n\n const curlCmd = \\`curl -s \"\\${url}\" -H \"Accept: application/json\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.results && data.results.length > 0) {\n return data.results.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.url,\n content: item.content || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('SearXNG failed:', e.message);\n }\n return null;\n}\n\n// Serper API - free tier available (2500 queries/month)\nasync function searchSerper(query, numResults) {\n const apiKey = process.env.SERPER_API_KEY;\n if (!apiKey) return null;\n\n try {\n const curlCmd = \\`curl -s \"https://google.serper.dev/search\" -H \"X-API-KEY: \\${apiKey}\" -H \"Content-Type: application/json\" -d '{\"q\":\"\\${query.replace(/\"/g, '\\\\\\\\\"')}\",\"num\":\\${numResults}}'\\`;\n log('Serper request');\n\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.organic && data.organic.length > 0) {\n return data.organic.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('Serper failed:', e.message);\n }\n return null;\n}\n\n// Brave Search API - free tier available\nasync function searchBrave(query, numResults) {\n const apiKey = process.env.BRAVE_API_KEY;\n if (!apiKey) return null;\n\n try {\n const url = \\`https://api.search.brave.com/res/v1/web/search?q=\\${encodeURIComponent(query)}&count=\\${numResults}\\`;\n const curlCmd = \\`curl -s \"\\${url}\" -H \"Accept: application/json\" -H \"X-Subscription-Token: \\${apiKey}\"\\`;\n log('Brave request');\n\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.web && data.web.results && data.web.results.length > 0) {\n return data.web.results.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.url,\n content: item.description || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('Brave failed:', e.message);\n }\n return null;\n}\n\n// DuckDuckGo - limited reliability due to bot detection\nasync function searchDuckDuckGo(query, numResults) {\n // DuckDuckGo Instant Answer API (limited results but more reliable)\n try {\n const apiUrl = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n content: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n content: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n content: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n log('DDG API:', results.length, 'results');\n return results;\n }\n } catch (e) {\n log('DDG API failed:', e.message);\n }\n\n return [];\n}\n\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n content: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n // Priority order:\n // 1. Smithery Exa MCP (best quality if configured)\n // 2. Google PSE (most reliable if configured)\n // 3. Serper (free tier: 2500/month)\n // 4. Brave Search (free tier available)\n // 5. SearXNG (self-hosted)\n // 6. DuckDuckGo (limited due to bot detection)\n\n // 1. Smithery Exa MCP (highest priority)\n const smitheryResults = await searchSmitheryExa(query, numResults);\n if (smitheryResults && smitheryResults.length > 0) {\n log('Using Smithery Exa');\n return { results: smitheryResults, source: 'smithery-exa' };\n }\n\n // 2. Google PSE\n const googleResults = await searchGooglePSE(query, numResults);\n if (googleResults && googleResults.length > 0) {\n log('Using Google PSE');\n return { results: googleResults, source: 'google-pse' };\n }\n\n // 3. Serper\n const serperResults = await searchSerper(query, numResults);\n if (serperResults && serperResults.length > 0) {\n log('Using Serper');\n return { results: serperResults, source: 'serper' };\n }\n\n // 4. Brave Search\n const braveResults = await searchBrave(query, numResults);\n if (braveResults && braveResults.length > 0) {\n log('Using Brave Search');\n return { results: braveResults, source: 'brave' };\n }\n\n // 5. SearXNG\n const searxngResults = await searchSearXNG(query, numResults);\n if (searxngResults && searxngResults.length > 0) {\n log('Using SearXNG');\n return { results: searxngResults, source: 'searxng' };\n }\n\n // 6. DuckDuckGo (last resort, limited results)\n log('Using DuckDuckGo (fallback)');\n const ddgResults = await searchDuckDuckGo(query, numResults);\n return { results: ddgResults, source: 'duckduckgo' };\n}\n\n// === HTTP Proxy Server ===\n\nconst server = http.createServer(async (req, res) => {\n const url = new URL(req.url, \\`http://\\${req.headers.host}\\`);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok', port: server.address()?.port || PORT }));\n return;\n }\n\n // Search endpoint - intercept\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', c => body += c);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n log('Search query:', query);\n const { results, source } = await search(query, numResults || 10);\n log('Results:', results.length, 'from', source);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n log('Search error:', e.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // === Standalone mode (controlled by STANDALONE_MODE env) ===\n // Whitelist approach: only allow core LLM APIs, mock everything else\n if (process.env.STANDALONE_MODE === '1') {\n const pathname = url.pathname;\n\n // Whitelist: Core APIs that should be forwarded to upstream\n const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');\n // /api/tools/exa/search is already handled above\n\n if (!isCoreLLMApi) {\n // Special handling for specific routes\n if (pathname === '/api/sessions/create') {\n log('Mock (dynamic):', pathname);\n const sessionId = \\`local-\\${Date.now()}-\\${Math.random().toString(36).slice(2, 10)}\\`;\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ id: sessionId }));\n return;\n }\n\n if (pathname === '/api/cli/whoami') {\n log('Mock (401):', pathname);\n res.writeHead(401, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Unauthorized', message: 'Local mode - use token fallback' }));\n return;\n }\n\n if (pathname === '/api/tools/get-url-contents') {\n log('Mock (404):', pathname);\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not available', message: 'Use local URL fetch fallback' }));\n return;\n }\n\n // All other non-core APIs: return empty success\n log('Mock (default):', pathname);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({}));\n return;\n }\n }\n\n // Proxy core LLM requests to upstream API\n log('Proxy:', req.method, url.pathname);\n\n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n // Choose http or https based on target protocol\n const proxyModule = proxyUrl.protocol === 'https:' ? https : http;\n const proxyReq = proxyModule.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n\n proxyReq.on('error', e => {\n log('Proxy error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));\n });\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n});\n\n// If port is 0, system will automatically assign an available port\nserver.listen(PORT, '127.0.0.1', () => {\n const actualPort = server.address().port;\n const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;\n\n // Write port file for parent process to read\n const portFile = process.env.SEARCH_PROXY_PORT_FILE;\n if (portFile) {\n fs.writeFileSync(portFile, String(actualPort));\n }\n\n // Output PORT= line for wrapper script to parse\n console.log('PORT=' + actualPort);\n\n const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;\n log('Search proxy started on http://127.0.0.1:' + actualPort);\n log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');\n log('Google PSE:', hasGoogle ? 'configured' : 'not set');\n log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');\n log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');\n log('SearXNG:', process.env.SEARXNG_URL || 'not set');\n});\n\nprocess.on('SIGTERM', () => { server.close(); process.exit(0); });\nprocess.on('SIGINT', () => { server.close(); process.exit(0); });\n`;\n}\n\n/**\n * Generate unified Wrapper script\n * Each droid instance runs its own proxy:\n * - Uses port 0 to let system auto-assign available port\n * - Proxy runs as child process\n * - Proxy is killed when droid exits\n * - Supports multiple droid instances running simultaneously\n */\n/* eslint-disable no-useless-escape */\nfunction generateUnifiedWrapper(\n droidPath: string,\n proxyScriptPath: string,\n standalone: boolean = false,\n): string {\n const standaloneEnv = standalone ? \"STANDALONE_MODE=1 \" : \"\";\n return `#!/bin/bash\n# Droid with WebSearch\n# Auto-generated by droid-patch --websearch\n# Each instance runs its own proxy on a system-assigned port\n\nPROXY_SCRIPT=\"${proxyScriptPath}\"\nDROID_BIN=\"${droidPath}\"\nPROXY_PID=\"\"\nPORT_FILE=\"/tmp/droid-websearch-\\$\\$.port\"\nSTANDALONE=\"${standalone ? \"1\" : \"0\"}\"\n\n# Cleanup function - kill proxy when droid exits\ncleanup() {\n if [ -n \"\\$PROXY_PID\" ] && kill -0 \"\\$PROXY_PID\" 2>/dev/null; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Stopping proxy (PID: \\$PROXY_PID)\" >&2\n kill \"\\$PROXY_PID\" 2>/dev/null\n wait \"\\$PROXY_PID\" 2>/dev/null\n fi\n rm -f \"\\$PORT_FILE\"\n}\n\n# Set up trap to cleanup on exit\ntrap cleanup EXIT INT TERM\n\n[ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Starting proxy...\" >&2\n[ \"\\$STANDALONE\" = \"1\" ] && [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Standalone mode enabled\" >&2\n\n# Start proxy with port 0 (system will assign available port)\n# Proxy writes actual port to PORT_FILE\nif [ -n \"\\$DROID_SEARCH_DEBUG\" ]; then\n ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE=\"\\$PORT_FILE\" node \"\\$PROXY_SCRIPT\" 2>&1 &\nelse\n ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE=\"\\$PORT_FILE\" node \"\\$PROXY_SCRIPT\" >/dev/null 2>&1 &\nfi\nPROXY_PID=\\$!\n\n# Wait for proxy to start and get actual port (max 5 seconds)\nfor i in {1..50}; do\n # Check if proxy process is still running\n if ! kill -0 \"\\$PROXY_PID\" 2>/dev/null; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Proxy process died\" >&2\n break\n fi\n if [ -f \"\\$PORT_FILE\" ]; then\n ACTUAL_PORT=\\$(cat \"\\$PORT_FILE\" 2>/dev/null)\n if [ -n \"\\$ACTUAL_PORT\" ] && curl -s \"http://127.0.0.1:\\$ACTUAL_PORT/health\" > /dev/null 2>&1; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Proxy ready on port \\$ACTUAL_PORT (PID: \\$PROXY_PID)\" >&2\n break\n fi\n fi\n sleep 0.1\ndone\n\n# Check if proxy started successfully\nif [ ! -f \"\\$PORT_FILE\" ] || [ -z \"\\$(cat \"\\$PORT_FILE\" 2>/dev/null)\" ]; then\n echo \"[websearch] Failed to start proxy, running without websearch\" >&2\n cleanup\n exec \"\\$DROID_BIN\" \"\\$@\"\nfi\n\nACTUAL_PORT=\\$(cat \"\\$PORT_FILE\")\nrm -f \"\\$PORT_FILE\"\n\n# Run droid with proxy\nexport FACTORY_API_BASE_URL_OVERRIDE=\"http://127.0.0.1:\\$ACTUAL_PORT\"\n\"\\$DROID_BIN\" \"\\$@\"\nDROID_EXIT_CODE=\\$?\n\n# Cleanup will be called by trap\nexit \\$DROID_EXIT_CODE\n`;\n}\n/* eslint-enable no-useless-escape */\n\n/**\n * Create unified WebSearch files\n *\n * Approach: Proxy server mode\n * - wrapper script starts local proxy server\n * - proxy server intercepts search requests, passes through other requests\n * - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy\n * - alias works directly, no extra steps needed\n *\n * @param outputDir - Directory to write files to\n * @param droidPath - Path to droid binary\n * @param aliasName - Alias name for the wrapper\n * @param apiBase - Custom API base URL for proxy to forward requests to\n * @param standalone - Standalone mode: mock non-LLM Factory APIs\n */\nexport async function createWebSearchUnifiedFiles(\n outputDir: string,\n droidPath: string,\n aliasName: string,\n apiBase?: string,\n standalone: boolean = false,\n): Promise<{ wrapperScript: string; preloadScript: string }> {\n if (!existsSync(outputDir)) {\n await mkdir(outputDir, { recursive: true });\n }\n\n const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);\n const wrapperScriptPath = join(outputDir, aliasName);\n\n // Write proxy server script with custom API base if provided\n const factoryApiUrl = apiBase || \"https://api.factory.ai\";\n await writeFile(proxyScriptPath, generateSearchProxyServer(factoryApiUrl));\n console.log(`[*] Created proxy script: ${proxyScriptPath}`);\n\n // Write unified wrapper\n await writeFile(\n wrapperScriptPath,\n generateUnifiedWrapper(droidPath, proxyScriptPath, standalone),\n );\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper: ${wrapperScriptPath}`);\n\n if (standalone) {\n console.log(`[*] Standalone mode enabled`);\n }\n\n return {\n wrapperScript: wrapperScriptPath,\n preloadScript: proxyScriptPath, // Keep interface compatible\n };\n}\n","import bin from \"tiny-bin\";\nimport { styleText } from \"node:util\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { execSync } from \"node:child_process\";\nimport { patchDroid, type Patch } from \"./patcher.ts\";\nimport {\n createAlias,\n removeAlias,\n listAliases,\n createAliasForWrapper,\n clearAllAliases,\n removeAliasesByFilter,\n type FilterFlag,\n} from \"./alias.ts\";\nimport { createWebSearchUnifiedFiles } from \"./websearch-patch.ts\";\nimport {\n saveAliasMetadata,\n createMetadata,\n loadAliasMetadata,\n listAllMetadata,\n formatPatches,\n} from \"./metadata.ts\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nfunction getVersion(): string {\n try {\n const pkgPath = join(__dirname, \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n return pkg.version || \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst version = getVersion();\n\nfunction getDroidVersion(droidPath: string): string | undefined {\n try {\n const result = execSync(`\"${droidPath}\" --version`, {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 5000,\n }).trim();\n // Parse version from output like \"droid 1.2.3\" or just \"1.2.3\"\n const match = result.match(/(\\d+\\.\\d+\\.\\d+)/);\n return match ? match[1] : result || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction findDefaultDroidPath(): string {\n const home = homedir();\n\n // Try `which droid` first to find droid in PATH\n try {\n const result = execSync(\"which droid\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (result && existsSync(result)) {\n return result;\n }\n } catch {\n // which command failed, continue with fallback paths\n }\n\n // Common installation paths\n const paths = [\n // Default sh install location\n join(home, \".droid\", \"bin\", \"droid\"),\n // Homebrew on Apple Silicon\n \"/opt/homebrew/bin/droid\",\n // Homebrew on Intel Mac / Linux\n \"/usr/local/bin/droid\",\n // Linux system-wide\n \"/usr/bin/droid\",\n // Current directory\n \"./droid\",\n ];\n\n for (const p of paths) {\n if (existsSync(p)) return p;\n }\n\n // Return default path even if not found (will error later with helpful message)\n return join(home, \".droid\", \"bin\", \"droid\");\n}\n\nbin(\"droid-patch\", \"CLI tool to patch droid binary with various modifications\")\n .package(\"droid-patch\", version)\n .option(\n \"--is-custom\",\n \"Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)\",\n )\n .option(\n \"--skip-login\",\n \"Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)\",\n )\n .option(\n \"--api-base <url>\",\n \"Replace Factory API URL with custom URL (binary patch, or forward target with --websearch)\",\n )\n .option(\n \"--websearch\",\n \"Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)\",\n )\n .option(\"--standalone\", \"Standalone mode: mock non-LLM Factory APIs (use with --websearch)\")\n .option(\n \"--reasoning-effort\",\n \"Enable reasoning effort for custom models (set to high, enable UI selector)\",\n )\n .option(\n \"--disable-telemetry\",\n \"Disable telemetry and Sentry error reporting (block data uploads)\",\n )\n .option(\"--dry-run\", \"Verify patches without actually modifying the binary\")\n .option(\"-p, --path <path>\", \"Path to the droid binary\")\n .option(\"-o, --output <dir>\", \"Output directory for patched binary\")\n .option(\"--no-backup\", \"Do not create backup of original binary\")\n .option(\"-v, --verbose\", \"Enable verbose output\")\n .argument(\"[alias]\", \"Alias name for the patched binary\")\n .action(async (options, args) => {\n const alias = args?.[0] as string | undefined;\n const isCustom = options[\"is-custom\"] as boolean;\n const skipLogin = options[\"skip-login\"] as boolean;\n const apiBase = options[\"api-base\"] as string | undefined;\n const websearch = options[\"websearch\"] as boolean;\n const standalone = options[\"standalone\"] as boolean;\n // When --websearch is used with --api-base, forward to custom URL\n // Otherwise forward to official Factory API\n const websearchTarget = websearch ? apiBase || \"https://api.factory.ai\" : undefined;\n const reasoningEffort = options[\"reasoning-effort\"] as boolean;\n const noTelemetry = options[\"disable-telemetry\"] as boolean;\n const dryRun = options[\"dry-run\"] as boolean;\n const path = (options.path as string) || findDefaultDroidPath();\n const outputDir = options.output as string | undefined;\n const backup = options.backup !== false;\n const verbose = options.verbose as boolean;\n\n // If -o is specified with alias, output to that directory with alias name\n const outputPath = outputDir && alias ? join(outputDir, alias) : undefined;\n\n // Handle --websearch only (no binary patching needed)\n // When --websearch is used alone (with optional --standalone), create proxy wrapper without modifying binary\n if (websearch && !isCustom && !skipLogin && !reasoningEffort && !noTelemetry) {\n if (!alias) {\n console.log(styleText(\"red\", \"Error: Alias name required for --websearch\"));\n console.log(styleText(\"gray\", \"Usage: npx droid-patch --websearch <alias>\"));\n process.exit(1);\n }\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid WebSearch Setup\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"white\", `Forward target: ${websearchTarget}`));\n if (standalone) {\n console.log(styleText(\"white\", `Standalone mode: enabled`));\n }\n console.log();\n\n // Create websearch proxy files (proxy script + wrapper)\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const { wrapperScript } = await createWebSearchUnifiedFiles(\n proxyDir,\n path,\n alias,\n websearchTarget,\n standalone,\n );\n\n // Create alias pointing to wrapper\n await createAliasForWrapper(wrapperScript, alias, verbose);\n\n // Save metadata for update command\n const droidVersion = getDroidVersion(path);\n const metadata = createMetadata(\n alias,\n path,\n {\n isCustom: false,\n skipLogin: false,\n apiBase: apiBase || null,\n websearch: true,\n reasoningEffort: false,\n noTelemetry: false,\n standalone: standalone,\n },\n {\n droidPatchVersion: version,\n droidVersion,\n },\n );\n await saveAliasMetadata(metadata);\n\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" WebSearch Ready!\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log();\n console.log(\"Run directly:\");\n console.log(styleText(\"yellow\", ` ${alias}`));\n console.log();\n console.log(styleText(\"cyan\", \"Auto-shutdown:\"));\n console.log(\n styleText(\"gray\", \" Proxy auto-shuts down after 5 min idle (no manual cleanup needed)\"),\n );\n console.log(styleText(\"gray\", \" To disable: export DROID_PROXY_IDLE_TIMEOUT=0\"));\n console.log();\n console.log(\"Search providers (in priority order):\");\n console.log(styleText(\"yellow\", \" 1. Smithery Exa (best quality):\"));\n console.log(styleText(\"gray\", \" export SMITHERY_API_KEY=your_api_key\"));\n console.log(styleText(\"gray\", \" export SMITHERY_PROFILE=your_profile\"));\n console.log(styleText(\"gray\", \" 2. Google PSE:\"));\n console.log(styleText(\"gray\", \" export GOOGLE_PSE_API_KEY=your_api_key\"));\n console.log(styleText(\"gray\", \" export GOOGLE_PSE_CX=your_search_engine_id\"));\n console.log(styleText(\"gray\", \" 3-6. Serper, Brave, SearXNG, DuckDuckGo (fallbacks)\"));\n console.log();\n console.log(\"Debug mode:\");\n console.log(styleText(\"gray\", \" export DROID_SEARCH_DEBUG=1\"));\n return;\n }\n\n if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry) {\n console.log(styleText(\"yellow\", \"No patch flags specified. Available patches:\"));\n console.log(styleText(\"gray\", \" --is-custom Patch isCustom for custom models\"));\n console.log(\n styleText(\"gray\", \" --skip-login Bypass login by injecting a fake API key\"),\n );\n console.log(\n styleText(\"gray\", \" --api-base Replace Factory API URL (binary patch)\"),\n );\n console.log(styleText(\"gray\", \" --websearch Enable local WebSearch proxy\"));\n console.log(\n styleText(\"gray\", \" --reasoning-effort Set reasoning effort level for custom models\"),\n );\n console.log(\n styleText(\"gray\", \" --disable-telemetry Disable telemetry and Sentry error reporting\"),\n );\n console.log(\n styleText(\"gray\", \" --standalone Standalone mode: mock non-LLM Factory APIs\"),\n );\n console.log();\n console.log(\"Usage examples:\");\n console.log(styleText(\"cyan\", \" npx droid-patch --is-custom droid-custom\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --skip-login droid-nologin\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --is-custom --skip-login droid-patched\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --websearch droid-search\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --websearch --standalone droid-local\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --disable-telemetry droid-private\"));\n console.log(\n styleText(\n \"cyan\",\n \" npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid\",\n ),\n );\n process.exit(1);\n }\n\n if (!alias && !dryRun) {\n console.log(styleText(\"red\", \"Error: alias name is required\"));\n console.log(\n styleText(\n \"gray\",\n \"Usage: droid-patch [--is-custom] [--skip-login] [-o <dir>] <alias-name>\",\n ),\n );\n process.exit(1);\n }\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid Binary Patcher\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n\n const patches: Patch[] = [];\n if (isCustom) {\n patches.push({\n name: \"isCustom\",\n description: \"Change isCustom:!0 to isCustom:!1\",\n pattern: Buffer.from(\"isCustom:!0\"),\n replacement: Buffer.from(\"isCustom:!1\"),\n });\n }\n\n // Add skip-login patch: replace process.env.FACTORY_API_KEY with a fixed fake key\n // \"process.env.FACTORY_API_KEY\" is 27 chars, we replace with \"fk-droid-patch-skip-00000\" (25 chars + quotes = 27)\n if (skipLogin) {\n patches.push({\n name: \"skipLogin\",\n description: 'Replace process.env.FACTORY_API_KEY with \"fk-droid-patch-skip-00000\"',\n pattern: Buffer.from(\"process.env.FACTORY_API_KEY\"),\n replacement: Buffer.from('\"fk-droid-patch-skip-00000\"'),\n });\n }\n\n // Add api-base patch: replace the Factory API base URL\n // Original: \"https://api.factory.ai\" (22 chars)\n // We need to pad the replacement URL to be exactly 22 chars\n // Note: When --websearch is used, --api-base sets the forward target instead of binary patching\n if (apiBase && !websearch) {\n const originalUrl = \"https://api.factory.ai\";\n const originalLength = originalUrl.length; // 22 chars\n\n // Validate and normalize the URL\n let normalizedUrl = apiBase.replace(/\\/+$/, \"\"); // Remove trailing slashes\n\n if (normalizedUrl.length > originalLength) {\n console.log(\n styleText(\"red\", `Error: API base URL must be ${originalLength} characters or less`),\n );\n console.log(\n styleText(\"gray\", ` Your URL: \"${normalizedUrl}\" (${normalizedUrl.length} chars)`),\n );\n console.log(styleText(\"gray\", ` Maximum: ${originalLength} characters`));\n console.log();\n console.log(styleText(\"yellow\", \"Tip: Use a shorter URL or set up a local redirect.\"));\n console.log(styleText(\"gray\", \" Examples:\"));\n console.log(styleText(\"gray\", \" http://127.0.0.1:3000 (19 chars)\"));\n console.log(styleText(\"gray\", \" http://localhost:80 (19 chars)\"));\n process.exit(1);\n }\n\n // Pad the URL with spaces at the end to match original length\n // Note: trailing spaces in URL are generally ignored\n const paddedUrl = normalizedUrl.padEnd(originalLength, \" \");\n\n patches.push({\n name: \"apiBase\",\n description: `Replace Factory API URL with \"${normalizedUrl}\"`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n });\n }\n\n // Add reasoning-effort patch: set custom models to use \"high\" reasoning\n // Also modify UI conditions to show reasoning selector for custom models\n if (reasoningEffort) {\n // [\"none\"] is 8 chars, [\"high\"] is 8 chars - perfect match!\n patches.push({\n name: \"reasoningEffortSupported\",\n description: 'Change supportedReasoningEfforts:[\"none\"] to [\"high\"]',\n pattern: Buffer.from('supportedReasoningEfforts:[\"none\"]'),\n replacement: Buffer.from('supportedReasoningEfforts:[\"high\"]'),\n });\n\n // \"none\" is 4 chars, \"high\" is 4 chars - perfect match!\n patches.push({\n name: \"reasoningEffortDefault\",\n description: 'Change defaultReasoningEffort:\"none\" to \"high\"',\n pattern: Buffer.from('defaultReasoningEffort:\"none\"'),\n replacement: Buffer.from('defaultReasoningEffort:\"high\"'),\n });\n\n // Change UI condition from length>1 to length>0\n // This allows custom models with single reasoning option to show the selector\n patches.push({\n name: \"reasoningEffortUIShow\",\n description: \"Change supportedReasoningEfforts.length>1 to length>0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length>1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length>0\"),\n });\n\n // Change UI condition from length<=1 to length<=0\n // This enables the reasoning setting in /settings menu for custom models\n patches.push({\n name: \"reasoningEffortUIEnable\",\n description: \"Change supportedReasoningEfforts.length<=1 to length<=0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length<=1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length<=0\"),\n });\n\n // Bypass reasoning effort validation to allow settings.json override\n // This allows \"xhigh\" in settings.json to work even though default is \"high\"\n // Original: if(R&&!B.supportedReasoningEfforts.includes(R)) throw error\n // Changed: if(0&&...) - never throws, any value is accepted\n patches.push({\n name: \"reasoningEffortValidationBypass\",\n description: \"Bypass reasoning effort validation (allows xhigh in settings.json)\",\n pattern: Buffer.from(\"if(R&&!B.supportedReasoningEfforts.includes(R))\"),\n replacement: Buffer.from(\"if(0&&!B.supportedReasoningEfforts.includes(R))\"),\n });\n }\n\n // Add no-telemetry patches: disable telemetry uploads and Sentry error reporting\n // Strategy:\n // 1. Break environment variable names so Sentry is never initialized (Q1() returns false)\n // 2. Invert flushToWeb condition so it returns early without making any fetch request\n if (noTelemetry) {\n // Patch 1: Break Sentry environment variable checks\n // Q1() function checks: VITE_VERCEL_ENV, ENABLE_SENTRY, NEXT_PUBLIC_ENABLE_SENTRY, FACTORY_ENABLE_SENTRY\n // By changing first letter to X, the env vars will never match, so Q1() returns false\n // and Sentry is never initialized\n patches.push({\n name: \"noTelemetrySentryEnv1\",\n description: \"Break ENABLE_SENTRY env var check (E->X)\",\n pattern: Buffer.from(\"ENABLE_SENTRY\"),\n replacement: Buffer.from(\"XNABLE_SENTRY\"),\n });\n\n patches.push({\n name: \"noTelemetrySentryEnv2\",\n description: \"Break VITE_VERCEL_ENV env var check (V->X)\",\n pattern: Buffer.from(\"VITE_VERCEL_ENV\"),\n replacement: Buffer.from(\"XITE_VERCEL_ENV\"),\n });\n\n // Patch 2: Make flushToWeb always return early to prevent ANY fetch request\n // Original: if(this.webEvents.length===0)return; // returns only when empty\n // Changed: if(!0||this.webEvents.length)return; // !0=true, ALWAYS returns\n // Result: Function always exits immediately, no telemetry is ever sent\n patches.push({\n name: \"noTelemetryFlushBlock\",\n description: \"Make flushToWeb always return (!0|| = always true)\",\n pattern: Buffer.from(\"this.webEvents.length===0\"),\n replacement: Buffer.from(\"!0||this.webEvents.length\"),\n });\n }\n\n try {\n const result = await patchDroid({\n inputPath: path,\n outputPath: outputPath,\n patches,\n dryRun,\n backup,\n verbose,\n });\n\n if (dryRun) {\n console.log();\n console.log(styleText(\"blue\", \"═\".repeat(60)));\n console.log(styleText([\"blue\", \"bold\"], \" DRY RUN COMPLETE\"));\n console.log(styleText(\"blue\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"gray\", \"To apply the patches, run without --dry-run:\"));\n console.log(styleText(\"cyan\", ` npx droid-patch --is-custom ${alias || \"<alias-name>\"}`));\n process.exit(0);\n }\n\n // If -o is specified, just output the file without creating alias\n if (outputDir && result.success && result.outputPath) {\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" PATCH SUCCESSFUL\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"white\", `Patched binary saved to: ${result.outputPath}`));\n process.exit(0);\n }\n\n if (result.success && result.outputPath && alias) {\n console.log();\n\n // If --websearch is also used, create wrapper and point to it\n if (websearch) {\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const { wrapperScript } = await createWebSearchUnifiedFiles(\n proxyDir,\n result.outputPath,\n alias,\n websearchTarget,\n standalone,\n );\n await createAliasForWrapper(wrapperScript, alias, verbose);\n\n console.log();\n console.log(styleText(\"cyan\", \"WebSearch enabled\"));\n console.log(styleText(\"white\", ` Forward target: ${websearchTarget}`));\n if (standalone) {\n console.log(styleText(\"white\", ` Standalone mode: enabled`));\n }\n } else {\n await createAlias(result.outputPath, alias, verbose);\n }\n\n // Save metadata for update command\n const droidVersion = getDroidVersion(path);\n const metadata = createMetadata(\n alias,\n path,\n {\n isCustom: !!isCustom,\n skipLogin: !!skipLogin,\n apiBase: apiBase || null,\n websearch: !!websearch,\n reasoningEffort: !!reasoningEffort,\n noTelemetry: !!noTelemetry,\n standalone: !!standalone,\n },\n {\n droidPatchVersion: version,\n droidVersion,\n },\n );\n await saveAliasMetadata(metadata);\n }\n\n if (result.success) {\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" PATCH SUCCESSFUL\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n }\n\n process.exit(result.success ? 0 : 1);\n } catch (error) {\n console.error(styleText(\"red\", `Error: ${(error as Error).message}`));\n if (verbose) console.error((error as Error).stack);\n process.exit(1);\n }\n })\n .command(\"list\", \"List all droid-patch aliases\")\n .action(async () => {\n await listAliases();\n })\n .command(\"remove\", \"Remove alias(es) by name or filter\")\n .argument(\"[alias-or-path]\", \"Alias name or file path to remove\")\n .option(\"--patch-version <version>\", \"Remove aliases created by this droid-patch version\")\n .option(\"--droid-version <version>\", \"Remove aliases for this droid version\")\n .option(\n \"--flag <flag>\",\n \"Remove aliases with this flag (is-custom, skip-login, websearch, api-base, reasoning-effort, disable-telemetry, standalone)\",\n )\n .action(async (options, args) => {\n const target = args?.[0] as string | undefined;\n const patchVersion = options[\"patch-version\"] as string | undefined;\n const droidVersion = options[\"droid-version\"] as string | undefined;\n const flag = options.flag as FilterFlag | undefined;\n\n // If filter options are provided, use filter mode\n if (patchVersion || droidVersion || flag) {\n await removeAliasesByFilter({\n patchVersion,\n droidVersion,\n flags: flag ? [flag] : undefined,\n });\n return;\n }\n\n // If no target and no filter, show error\n if (!target) {\n console.error(\n styleText(\n \"red\",\n \"Error: Provide an alias name or use filter options (--patch-version, --droid-version, --flag)\",\n ),\n );\n process.exit(1);\n }\n\n // Check if it's a file path (contains / or .)\n if (target.includes(\"/\") || existsSync(target)) {\n // It's a file path, delete directly\n const { unlink } = await import(\"node:fs/promises\");\n try {\n await unlink(target);\n console.log(styleText(\"green\", `[*] Removed: ${target}`));\n } catch (error) {\n console.error(styleText(\"red\", `Error: ${(error as Error).message}`));\n process.exit(1);\n }\n } else {\n // It's an alias name\n await removeAlias(target);\n }\n })\n .command(\"version\", \"Print droid-patch version\")\n .action(() => {\n console.log(`droid-patch v${version}`);\n })\n .command(\"clear\", \"Remove all droid-patch aliases and related files\")\n .action(async () => {\n await clearAllAliases();\n })\n .command(\"update\", \"Update aliases with latest droid binary\")\n .argument(\"[alias]\", \"Specific alias to update (optional, updates all if not specified)\")\n .option(\"--dry-run\", \"Preview without making changes\")\n .option(\"-p, --path <path>\", \"Path to new droid binary\")\n .option(\"-v, --verbose\", \"Enable verbose output\")\n .action(async (options, args) => {\n const aliasName = args?.[0] as string | undefined;\n const dryRun = options[\"dry-run\"] as boolean;\n const newBinaryPath = (options.path as string) || findDefaultDroidPath();\n const verbose = options.verbose as boolean;\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid-Patch Update\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n\n // Verify the new binary exists\n if (!existsSync(newBinaryPath)) {\n console.log(styleText(\"red\", `Error: Droid binary not found at ${newBinaryPath}`));\n console.log(styleText(\"gray\", \"Use -p to specify a different path\"));\n process.exit(1);\n }\n\n // Get aliases to update\n let metaList: Awaited<ReturnType<typeof loadAliasMetadata>>[];\n if (aliasName) {\n const meta = await loadAliasMetadata(aliasName);\n if (!meta) {\n console.log(styleText(\"red\", `Error: No metadata found for alias \"${aliasName}\"`));\n console.log(\n styleText(\"gray\", \"This alias may have been created before update tracking was added.\"),\n );\n console.log(styleText(\"gray\", \"Remove and recreate the alias to enable update support.\"));\n process.exit(1);\n }\n metaList = [meta];\n } else {\n metaList = await listAllMetadata();\n if (metaList.length === 0) {\n console.log(styleText(\"yellow\", \"No aliases with metadata found.\"));\n console.log(styleText(\"gray\", \"Create aliases with droid-patch to enable update support.\"));\n process.exit(0);\n }\n }\n\n console.log(styleText(\"white\", `Using droid binary: ${newBinaryPath}`));\n console.log(styleText(\"white\", `Found ${metaList.length} alias(es) to update`));\n if (dryRun) {\n console.log(styleText(\"blue\", \"(DRY RUN - no changes will be made)\"));\n }\n console.log();\n\n let successCount = 0;\n let failCount = 0;\n\n for (const meta of metaList) {\n if (!meta) continue;\n\n console.log(styleText(\"cyan\", `─`.repeat(40)));\n console.log(styleText(\"white\", `Updating: ${styleText([\"cyan\", \"bold\"], meta.name)}`));\n console.log(styleText(\"gray\", ` Patches: ${formatPatches(meta.patches)}`));\n\n if (dryRun) {\n console.log(styleText(\"blue\", ` [DRY RUN] Would re-apply patches`));\n successCount++;\n continue;\n }\n\n try {\n // Build patch list based on metadata\n const patches: Patch[] = [];\n\n if (meta.patches.isCustom) {\n patches.push({\n name: \"isCustom\",\n description: \"Change isCustom:!0 to isCustom:!1\",\n pattern: Buffer.from(\"isCustom:!0\"),\n replacement: Buffer.from(\"isCustom:!1\"),\n });\n }\n\n if (meta.patches.skipLogin) {\n patches.push({\n name: \"skipLogin\",\n description: \"Replace process.env.FACTORY_API_KEY with fake key\",\n pattern: Buffer.from(\"process.env.FACTORY_API_KEY\"),\n replacement: Buffer.from('\"fk-droid-patch-skip-00000\"'),\n });\n }\n\n if (meta.patches.apiBase) {\n const originalUrl = \"https://api.factory.ai\";\n const paddedUrl = meta.patches.apiBase.padEnd(originalUrl.length, \" \");\n patches.push({\n name: \"apiBase\",\n description: `Replace Factory API URL with \"${meta.patches.apiBase}\"`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n });\n }\n\n if (meta.patches.reasoningEffort) {\n patches.push({\n name: \"reasoningEffortSupported\",\n description: 'Change supportedReasoningEfforts:[\"none\"] to [\"high\"]',\n pattern: Buffer.from('supportedReasoningEfforts:[\"none\"]'),\n replacement: Buffer.from('supportedReasoningEfforts:[\"high\"]'),\n });\n patches.push({\n name: \"reasoningEffortDefault\",\n description: 'Change defaultReasoningEffort:\"none\" to \"high\"',\n pattern: Buffer.from('defaultReasoningEffort:\"none\"'),\n replacement: Buffer.from('defaultReasoningEffort:\"high\"'),\n });\n patches.push({\n name: \"reasoningEffortUIShow\",\n description: \"Change supportedReasoningEfforts.length>1 to length>0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length>1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length>0\"),\n });\n patches.push({\n name: \"reasoningEffortUIEnable\",\n description: \"Change supportedReasoningEfforts.length<=1 to length<=0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length<=1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length<=0\"),\n });\n patches.push({\n name: \"reasoningEffortValidationBypass\",\n description: \"Bypass reasoning effort validation (allows xhigh in settings.json)\",\n pattern: Buffer.from(\"if(R&&!B.supportedReasoningEfforts.includes(R))\"),\n replacement: Buffer.from(\"if(0&&!B.supportedReasoningEfforts.includes(R))\"),\n });\n }\n\n if (meta.patches.noTelemetry) {\n patches.push({\n name: \"noTelemetrySentryEnv1\",\n description: \"Break ENABLE_SENTRY env var check (E->X)\",\n pattern: Buffer.from(\"ENABLE_SENTRY\"),\n replacement: Buffer.from(\"XNABLE_SENTRY\"),\n });\n patches.push({\n name: \"noTelemetrySentryEnv2\",\n description: \"Break VITE_VERCEL_ENV env var check (V->X)\",\n pattern: Buffer.from(\"VITE_VERCEL_ENV\"),\n replacement: Buffer.from(\"XITE_VERCEL_ENV\"),\n });\n patches.push({\n name: \"noTelemetryFlushBlock\",\n description: \"Make flushToWeb always return (!0|| = always true)\",\n pattern: Buffer.from(\"this.webEvents.length===0\"),\n replacement: Buffer.from(\"!0||this.webEvents.length\"),\n });\n }\n\n // Determine output path based on whether this is a websearch alias\n const binsDir = join(homedir(), \".droid-patch\", \"bins\");\n const outputPath = join(binsDir, `${meta.name}-patched`);\n\n // Apply patches (only if there are binary patches to apply)\n if (patches.length > 0) {\n const result = await patchDroid({\n inputPath: newBinaryPath,\n outputPath,\n patches,\n dryRun: false,\n backup: false,\n verbose,\n });\n\n if (!result.success) {\n console.log(styleText(\"red\", ` ✗ Failed to apply patches`));\n failCount++;\n continue;\n }\n\n // Re-sign on macOS\n if (process.platform === \"darwin\") {\n try {\n const { execSync } = await import(\"node:child_process\");\n execSync(`codesign --force --deep --sign - \"${outputPath}\"`, {\n stdio: \"pipe\",\n });\n if (verbose) {\n console.log(styleText(\"gray\", ` Re-signed binary`));\n }\n } catch {\n console.log(styleText(\"yellow\", ` [!] Could not re-sign binary`));\n }\n }\n }\n\n // If websearch is enabled, regenerate wrapper files\n // Support both new 'websearch' field and old 'proxy' field for backward compatibility\n const hasWebsearch = meta.patches.websearch || !!meta.patches.proxy;\n if (hasWebsearch) {\n // Determine forward target: apiBase > proxy (legacy) > default\n const forwardTarget =\n meta.patches.apiBase || meta.patches.proxy || \"https://api.factory.ai\";\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const targetBinaryPath = patches.length > 0 ? outputPath : newBinaryPath;\n await createWebSearchUnifiedFiles(\n proxyDir,\n targetBinaryPath,\n meta.name,\n forwardTarget,\n meta.patches.standalone || false,\n );\n if (verbose) {\n console.log(styleText(\"gray\", ` Regenerated websearch wrapper`));\n if (meta.patches.standalone) {\n console.log(styleText(\"gray\", ` Standalone mode: enabled`));\n }\n }\n // Migrate old proxy field to new websearch field\n if (meta.patches.proxy && !meta.patches.websearch) {\n meta.patches.websearch = true;\n meta.patches.apiBase = meta.patches.proxy;\n delete meta.patches.proxy;\n }\n }\n\n // Update metadata\n meta.updatedAt = new Date().toISOString();\n meta.originalBinaryPath = newBinaryPath;\n await saveAliasMetadata(meta);\n\n console.log(styleText(\"green\", ` ✓ Updated successfully`));\n successCount++;\n } catch (error) {\n console.log(styleText(\"red\", ` ✗ Error: ${(error as Error).message}`));\n if (verbose) {\n console.error((error as Error).stack);\n }\n failCount++;\n }\n }\n\n console.log();\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n if (dryRun) {\n console.log(styleText([\"blue\", \"bold\"], \" DRY RUN COMPLETE\"));\n console.log(styleText(\"gray\", ` Would update ${successCount} alias(es)`));\n } else if (failCount === 0) {\n console.log(styleText([\"green\", \"bold\"], \" UPDATE COMPLETE\"));\n console.log(styleText(\"gray\", ` Updated ${successCount} alias(es)`));\n } else {\n console.log(styleText([\"yellow\", \"bold\"], \" UPDATE FINISHED WITH ERRORS\"));\n console.log(styleText(\"gray\", ` Success: ${successCount}, Failed: ${failCount}`));\n }\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n })\n .run()\n .catch((err: Error) => {\n console.error(err);\n process.exit(1);\n });\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAm9BA,SAAS,0BAA0B,gBAAwB,0BAAkC;AAC3F,QAAO;;;;;;;;;;;;uBAYc,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwerC,SAAS,uBACP,WACA,iBACA,aAAsB,OACd;CACR,MAAM,gBAAgB,aAAa,uBAAuB;AAC1D,QAAO;;;;;gBAKO,gBAAgB;aACnB,UAAU;;;cAGT,aAAa,MAAM,IAAI;;;;;;;;;;;;;;;;;;;;;IAqBjC,cAAc;;IAEd,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDlB,eAAsB,4BACpB,WACA,WACA,WACA,SACA,aAAsB,OACqC;AAC3D,KAAI,CAAC,WAAW,UAAU,CACxB,OAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;CAG7C,MAAM,kBAAkB,KAAK,WAAW,GAAG,UAAU,WAAW;CAChE,MAAM,oBAAoB,KAAK,WAAW,UAAU;AAIpD,OAAM,UAAU,iBAAiB,0BADX,WAAW,yBACwC,CAAC;AAC1E,SAAQ,IAAI,6BAA6B,kBAAkB;AAG3D,OAAM,UACJ,mBACA,uBAAuB,WAAW,iBAAiB,WAAW,CAC/D;AACD,OAAM,MAAM,mBAAmB,IAAM;AACrC,SAAQ,IAAI,wBAAwB,oBAAoB;AAExD,KAAI,WACF,SAAQ,IAAI,8BAA8B;AAG5C,QAAO;EACL,eAAe;EACf,eAAe;EAChB;;;;;AC/iDH,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAEzD,SAAS,aAAqB;AAC5B,KAAI;EACF,MAAM,UAAU,KAAK,WAAW,MAAM,eAAe;AAErD,SADY,KAAK,MAAM,aAAa,SAAS,QAAQ,CAAC,CAC3C,WAAW;SAChB;AACN,SAAO;;;AAIX,MAAM,UAAU,YAAY;AAE5B,SAAS,gBAAgB,WAAuC;AAC9D,KAAI;EACF,MAAM,SAAS,SAAS,IAAI,UAAU,cAAc;GAClD,UAAU;GACV,OAAO;IAAC;IAAQ;IAAQ;IAAO;GAC/B,SAAS;GACV,CAAC,CAAC,MAAM;EAET,MAAM,QAAQ,OAAO,MAAM,kBAAkB;AAC7C,SAAO,QAAQ,MAAM,KAAK,UAAU;SAC9B;AACN;;;AAIJ,SAAS,uBAA+B;CACtC,MAAM,OAAO,SAAS;AAGtB,KAAI;EACF,MAAM,SAAS,SAAS,eAAe;GACrC,UAAU;GACV,OAAO;IAAC;IAAQ;IAAQ;IAAO;GAChC,CAAC,CAAC,MAAM;AACT,MAAI,UAAU,WAAW,OAAO,CAC9B,QAAO;SAEH;CAKR,MAAM,QAAQ;EAEZ,KAAK,MAAM,UAAU,OAAO,QAAQ;EAEpC;EAEA;EAEA;EAEA;EACD;AAED,MAAK,MAAM,KAAK,MACd,KAAI,WAAW,EAAE,CAAE,QAAO;AAI5B,QAAO,KAAK,MAAM,UAAU,OAAO,QAAQ;;AAG7C,IAAI,eAAe,4DAA4D,CAC5E,QAAQ,eAAe,QAAQ,CAC/B,OACC,eACA,kFACD,CACA,OACC,gBACA,iFACD,CACA,OACC,oBACA,6FACD,CACA,OACC,eACA,oFACD,CACA,OAAO,gBAAgB,oEAAoE,CAC3F,OACC,sBACA,8EACD,CACA,OACC,uBACA,oEACD,CACA,OAAO,aAAa,uDAAuD,CAC3E,OAAO,qBAAqB,2BAA2B,CACvD,OAAO,sBAAsB,sCAAsC,CACnE,OAAO,eAAe,0CAA0C,CAChE,OAAO,iBAAiB,wBAAwB,CAChD,SAAS,WAAW,oCAAoC,CACxD,OAAO,OAAO,SAAS,SAAS;CAC/B,MAAM,QAAQ,OAAO;CACrB,MAAM,WAAW,QAAQ;CACzB,MAAM,YAAY,QAAQ;CAC1B,MAAM,UAAU,QAAQ;CACxB,MAAM,YAAY,QAAQ;CAC1B,MAAM,aAAa,QAAQ;CAG3B,MAAM,kBAAkB,YAAY,WAAW,2BAA2B;CAC1E,MAAM,kBAAkB,QAAQ;CAChC,MAAM,cAAc,QAAQ;CAC5B,MAAM,SAAS,QAAQ;CACvB,MAAM,OAAQ,QAAQ,QAAmB,sBAAsB;CAC/D,MAAM,YAAY,QAAQ;CAC1B,MAAM,SAAS,QAAQ,WAAW;CAClC,MAAM,UAAU,QAAQ;CAGxB,MAAM,aAAa,aAAa,QAAQ,KAAK,WAAW,MAAM,GAAG;AAIjE,KAAI,aAAa,CAAC,YAAY,CAAC,aAAa,CAAC,mBAAmB,CAAC,aAAa;AAC5E,MAAI,CAAC,OAAO;AACV,WAAQ,IAAI,UAAU,OAAO,6CAA6C,CAAC;AAC3E,WAAQ,IAAI,UAAU,QAAQ,6CAA6C,CAAC;AAC5E,WAAQ,KAAK,EAAE;;AAGjB,UAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,UAAQ,IAAI,UAAU,CAAC,QAAQ,OAAO,EAAE,0BAA0B,CAAC;AACnE,UAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,UAAQ,KAAK;AACb,UAAQ,IAAI,UAAU,SAAS,mBAAmB,kBAAkB,CAAC;AACrE,MAAI,WACF,SAAQ,IAAI,UAAU,SAAS,2BAA2B,CAAC;AAE7D,UAAQ,KAAK;EAIb,MAAM,EAAE,kBAAkB,MAAM,4BADf,KAAK,SAAS,EAAE,gBAAgB,QAAQ,EAGvD,MACA,OACA,iBACA,WACD;AAGD,QAAM,sBAAsB,eAAe,OAAO,QAAQ;EAG1D,MAAM,eAAe,gBAAgB,KAAK;AAkB1C,QAAM,kBAjBW,eACf,OACA,MACA;GACE,UAAU;GACV,WAAW;GACX,SAAS,WAAW;GACpB,WAAW;GACX,iBAAiB;GACjB,aAAa;GACD;GACb,EACD;GACE,mBAAmB;GACnB;GACD,CACF,CACgC;AAEjC,UAAQ,KAAK;AACb,UAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;AAC/C,UAAQ,IAAI,UAAU,CAAC,SAAS,OAAO,EAAE,qBAAqB,CAAC;AAC/D,UAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;AAC/C,UAAQ,KAAK;AACb,UAAQ,IAAI,gBAAgB;AAC5B,UAAQ,IAAI,UAAU,UAAU,KAAK,QAAQ,CAAC;AAC9C,UAAQ,KAAK;AACb,UAAQ,IAAI,UAAU,QAAQ,iBAAiB,CAAC;AAChD,UAAQ,IACN,UAAU,QAAQ,sEAAsE,CACzF;AACD,UAAQ,IAAI,UAAU,QAAQ,kDAAkD,CAAC;AACjF,UAAQ,KAAK;AACb,UAAQ,IAAI,wCAAwC;AACpD,UAAQ,IAAI,UAAU,UAAU,oCAAoC,CAAC;AACrE,UAAQ,IAAI,UAAU,QAAQ,4CAA4C,CAAC;AAC3E,UAAQ,IAAI,UAAU,QAAQ,4CAA4C,CAAC;AAC3E,UAAQ,IAAI,UAAU,QAAQ,mBAAmB,CAAC;AAClD,UAAQ,IAAI,UAAU,QAAQ,8CAA8C,CAAC;AAC7E,UAAQ,IAAI,UAAU,QAAQ,kDAAkD,CAAC;AACjF,UAAQ,IAAI,UAAU,QAAQ,wDAAwD,CAAC;AACvF,UAAQ,KAAK;AACb,UAAQ,IAAI,cAAc;AAC1B,UAAQ,IAAI,UAAU,QAAQ,gCAAgC,CAAC;AAC/D;;AAGF,KAAI,CAAC,YAAY,CAAC,aAAa,CAAC,WAAW,CAAC,aAAa,CAAC,mBAAmB,CAAC,aAAa;AACzF,UAAQ,IAAI,UAAU,UAAU,+CAA+C,CAAC;AAChF,UAAQ,IAAI,UAAU,QAAQ,yDAAyD,CAAC;AACxF,UAAQ,IACN,UAAU,QAAQ,iEAAiE,CACpF;AACD,UAAQ,IACN,UAAU,QAAQ,+DAA+D,CAClF;AACD,UAAQ,IAAI,UAAU,QAAQ,qDAAqD,CAAC;AACpF,UAAQ,IACN,UAAU,QAAQ,qEAAqE,CACxF;AACD,UAAQ,IACN,UAAU,QAAQ,qEAAqE,CACxF;AACD,UAAQ,IACN,UAAU,QAAQ,mEAAmE,CACtF;AACD,UAAQ,KAAK;AACb,UAAQ,IAAI,kBAAkB;AAC9B,UAAQ,IAAI,UAAU,QAAQ,6CAA6C,CAAC;AAC5E,UAAQ,IAAI,UAAU,QAAQ,+CAA+C,CAAC;AAC9E,UAAQ,IAAI,UAAU,QAAQ,2DAA2D,CAAC;AAC1F,UAAQ,IAAI,UAAU,QAAQ,6CAA6C,CAAC;AAC5E,UAAQ,IAAI,UAAU,QAAQ,yDAAyD,CAAC;AACxF,UAAQ,IAAI,UAAU,QAAQ,sDAAsD,CAAC;AACrF,UAAQ,IACN,UACE,QACA,2EACD,CACF;AACD,UAAQ,KAAK,EAAE;;AAGjB,KAAI,CAAC,SAAS,CAAC,QAAQ;AACrB,UAAQ,IAAI,UAAU,OAAO,gCAAgC,CAAC;AAC9D,UAAQ,IACN,UACE,QACA,0EACD,CACF;AACD,UAAQ,KAAK,EAAE;;AAGjB,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,SAAQ,IAAI,UAAU,CAAC,QAAQ,OAAO,EAAE,yBAAyB,CAAC;AAClE,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,SAAQ,KAAK;CAEb,MAAMA,UAAmB,EAAE;AAC3B,KAAI,SACF,SAAQ,KAAK;EACX,MAAM;EACN,aAAa;EACb,SAAS,OAAO,KAAK,cAAc;EACnC,aAAa,OAAO,KAAK,cAAc;EACxC,CAAC;AAKJ,KAAI,UACF,SAAQ,KAAK;EACX,MAAM;EACN,aAAa;EACb,SAAS,OAAO,KAAK,8BAA8B;EACnD,aAAa,OAAO,KAAK,gCAA8B;EACxD,CAAC;AAOJ,KAAI,WAAW,CAAC,WAAW;EACzB,MAAM,cAAc;EACpB,MAAM,iBAAiB;EAGvB,IAAI,gBAAgB,QAAQ,QAAQ,QAAQ,GAAG;AAE/C,MAAI,cAAc,SAAS,gBAAgB;AACzC,WAAQ,IACN,UAAU,OAAO,+BAA+B,eAAe,qBAAqB,CACrF;AACD,WAAQ,IACN,UAAU,QAAQ,gBAAgB,cAAc,KAAK,cAAc,OAAO,SAAS,CACpF;AACD,WAAQ,IAAI,UAAU,QAAQ,eAAe,eAAe,aAAa,CAAC;AAC1E,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,UAAU,qDAAqD,CAAC;AACtF,WAAQ,IAAI,UAAU,QAAQ,cAAc,CAAC;AAC7C,WAAQ,IAAI,UAAU,QAAQ,uCAAuC,CAAC;AACtE,WAAQ,IAAI,UAAU,QAAQ,sCAAsC,CAAC;AACrE,WAAQ,KAAK,EAAE;;EAKjB,MAAM,YAAY,cAAc,OAAO,gBAAgB,IAAI;AAE3D,UAAQ,KAAK;GACX,MAAM;GACN,aAAa,iCAAiC,cAAc;GAC5D,SAAS,OAAO,KAAK,YAAY;GACjC,aAAa,OAAO,KAAK,UAAU;GACpC,CAAC;;AAKJ,KAAI,iBAAiB;AAEnB,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,uCAAqC;GAC1D,aAAa,OAAO,KAAK,uCAAqC;GAC/D,CAAC;AAGF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,kCAAgC;GACrD,aAAa,OAAO,KAAK,kCAAgC;GAC1D,CAAC;AAIF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,qCAAqC;GAC1D,aAAa,OAAO,KAAK,qCAAqC;GAC/D,CAAC;AAIF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,sCAAsC;GAC3D,aAAa,OAAO,KAAK,sCAAsC;GAChE,CAAC;AAMF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,kDAAkD;GACvE,aAAa,OAAO,KAAK,kDAAkD;GAC5E,CAAC;;AAOJ,KAAI,aAAa;AAKf,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,gBAAgB;GACrC,aAAa,OAAO,KAAK,gBAAgB;GAC1C,CAAC;AAEF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,kBAAkB;GACvC,aAAa,OAAO,KAAK,kBAAkB;GAC5C,CAAC;AAMF,UAAQ,KAAK;GACX,MAAM;GACN,aAAa;GACb,SAAS,OAAO,KAAK,4BAA4B;GACjD,aAAa,OAAO,KAAK,4BAA4B;GACtD,CAAC;;AAGJ,KAAI;EACF,MAAM,SAAS,MAAM,WAAW;GAC9B,WAAW;GACC;GACZ;GACA;GACA;GACA;GACD,CAAC;AAEF,MAAI,QAAQ;AACV,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,WAAQ,IAAI,UAAU,CAAC,QAAQ,OAAO,EAAE,qBAAqB,CAAC;AAC9D,WAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,QAAQ,+CAA+C,CAAC;AAC9E,WAAQ,IAAI,UAAU,QAAQ,iCAAiC,SAAS,iBAAiB,CAAC;AAC1F,WAAQ,KAAK,EAAE;;AAIjB,MAAI,aAAa,OAAO,WAAW,OAAO,YAAY;AACpD,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;AAC/C,WAAQ,IAAI,UAAU,CAAC,SAAS,OAAO,EAAE,qBAAqB,CAAC;AAC/D,WAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;AAC/C,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,SAAS,4BAA4B,OAAO,aAAa,CAAC;AAChF,WAAQ,KAAK,EAAE;;AAGjB,MAAI,OAAO,WAAW,OAAO,cAAc,OAAO;AAChD,WAAQ,KAAK;AAGb,OAAI,WAAW;IAEb,MAAM,EAAE,kBAAkB,MAAM,4BADf,KAAK,SAAS,EAAE,gBAAgB,QAAQ,EAGvD,OAAO,YACP,OACA,iBACA,WACD;AACD,UAAM,sBAAsB,eAAe,OAAO,QAAQ;AAE1D,YAAQ,KAAK;AACb,YAAQ,IAAI,UAAU,QAAQ,oBAAoB,CAAC;AACnD,YAAQ,IAAI,UAAU,SAAS,qBAAqB,kBAAkB,CAAC;AACvE,QAAI,WACF,SAAQ,IAAI,UAAU,SAAS,6BAA6B,CAAC;SAG/D,OAAM,YAAY,OAAO,YAAY,OAAO,QAAQ;GAItD,MAAM,eAAe,gBAAgB,KAAK;AAkB1C,SAAM,kBAjBW,eACf,OACA,MACA;IACE,UAAU,CAAC,CAAC;IACZ,WAAW,CAAC,CAAC;IACb,SAAS,WAAW;IACpB,WAAW,CAAC,CAAC;IACb,iBAAiB,CAAC,CAAC;IACnB,aAAa,CAAC,CAAC;IACf,YAAY,CAAC,CAAC;IACf,EACD;IACE,mBAAmB;IACnB;IACD,CACF,CACgC;;AAGnC,MAAI,OAAO,SAAS;AAClB,WAAQ,KAAK;AACb,WAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;AAC/C,WAAQ,IAAI,UAAU,CAAC,SAAS,OAAO,EAAE,qBAAqB,CAAC;AAC/D,WAAQ,IAAI,UAAU,SAAS,IAAI,OAAO,GAAG,CAAC,CAAC;;AAGjD,UAAQ,KAAK,OAAO,UAAU,IAAI,EAAE;UAC7B,OAAO;AACd,UAAQ,MAAM,UAAU,OAAO,UAAW,MAAgB,UAAU,CAAC;AACrE,MAAI,QAAS,SAAQ,MAAO,MAAgB,MAAM;AAClD,UAAQ,KAAK,EAAE;;EAEjB,CACD,QAAQ,QAAQ,+BAA+B,CAC/C,OAAO,YAAY;AAClB,OAAM,aAAa;EACnB,CACD,QAAQ,UAAU,qCAAqC,CACvD,SAAS,mBAAmB,oCAAoC,CAChE,OAAO,6BAA6B,qDAAqD,CACzF,OAAO,6BAA6B,wCAAwC,CAC5E,OACC,iBACA,8HACD,CACA,OAAO,OAAO,SAAS,SAAS;CAC/B,MAAM,SAAS,OAAO;CACtB,MAAM,eAAe,QAAQ;CAC7B,MAAM,eAAe,QAAQ;CAC7B,MAAM,OAAO,QAAQ;AAGrB,KAAI,gBAAgB,gBAAgB,MAAM;AACxC,QAAM,sBAAsB;GAC1B;GACA;GACA,OAAO,OAAO,CAAC,KAAK,GAAG;GACxB,CAAC;AACF;;AAIF,KAAI,CAAC,QAAQ;AACX,UAAQ,MACN,UACE,OACA,gGACD,CACF;AACD,UAAQ,KAAK,EAAE;;AAIjB,KAAI,OAAO,SAAS,IAAI,IAAI,WAAW,OAAO,EAAE;EAE9C,MAAM,EAAE,qBAAW,MAAM,OAAO;AAChC,MAAI;AACF,SAAMC,SAAO,OAAO;AACpB,WAAQ,IAAI,UAAU,SAAS,gBAAgB,SAAS,CAAC;WAClD,OAAO;AACd,WAAQ,MAAM,UAAU,OAAO,UAAW,MAAgB,UAAU,CAAC;AACrE,WAAQ,KAAK,EAAE;;OAIjB,OAAM,YAAY,OAAO;EAE3B,CACD,QAAQ,WAAW,4BAA4B,CAC/C,aAAa;AACZ,SAAQ,IAAI,gBAAgB,UAAU;EACtC,CACD,QAAQ,SAAS,mDAAmD,CACpE,OAAO,YAAY;AAClB,OAAM,iBAAiB;EACvB,CACD,QAAQ,UAAU,0CAA0C,CAC5D,SAAS,WAAW,oEAAoE,CACxF,OAAO,aAAa,iCAAiC,CACrD,OAAO,qBAAqB,2BAA2B,CACvD,OAAO,iBAAiB,wBAAwB,CAChD,OAAO,OAAO,SAAS,SAAS;CAC/B,MAAM,YAAY,OAAO;CACzB,MAAM,SAAS,QAAQ;CACvB,MAAM,gBAAiB,QAAQ,QAAmB,sBAAsB;CACxE,MAAM,UAAU,QAAQ;AAExB,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,SAAQ,IAAI,UAAU,CAAC,QAAQ,OAAO,EAAE,uBAAuB,CAAC;AAChE,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,SAAQ,KAAK;AAGb,KAAI,CAAC,WAAW,cAAc,EAAE;AAC9B,UAAQ,IAAI,UAAU,OAAO,oCAAoC,gBAAgB,CAAC;AAClF,UAAQ,IAAI,UAAU,QAAQ,qCAAqC,CAAC;AACpE,UAAQ,KAAK,EAAE;;CAIjB,IAAIC;AACJ,KAAI,WAAW;EACb,MAAM,OAAO,MAAM,kBAAkB,UAAU;AAC/C,MAAI,CAAC,MAAM;AACT,WAAQ,IAAI,UAAU,OAAO,uCAAuC,UAAU,GAAG,CAAC;AAClF,WAAQ,IACN,UAAU,QAAQ,qEAAqE,CACxF;AACD,WAAQ,IAAI,UAAU,QAAQ,0DAA0D,CAAC;AACzF,WAAQ,KAAK,EAAE;;AAEjB,aAAW,CAAC,KAAK;QACZ;AACL,aAAW,MAAM,iBAAiB;AAClC,MAAI,SAAS,WAAW,GAAG;AACzB,WAAQ,IAAI,UAAU,UAAU,kCAAkC,CAAC;AACnE,WAAQ,IAAI,UAAU,QAAQ,4DAA4D,CAAC;AAC3F,WAAQ,KAAK,EAAE;;;AAInB,SAAQ,IAAI,UAAU,SAAS,uBAAuB,gBAAgB,CAAC;AACvE,SAAQ,IAAI,UAAU,SAAS,SAAS,SAAS,OAAO,sBAAsB,CAAC;AAC/E,KAAI,OACF,SAAQ,IAAI,UAAU,QAAQ,sCAAsC,CAAC;AAEvE,SAAQ,KAAK;CAEb,IAAI,eAAe;CACnB,IAAI,YAAY;AAEhB,MAAK,MAAM,QAAQ,UAAU;AAC3B,MAAI,CAAC,KAAM;AAEX,UAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,UAAQ,IAAI,UAAU,SAAS,aAAa,UAAU,CAAC,QAAQ,OAAO,EAAE,KAAK,KAAK,GAAG,CAAC;AACtF,UAAQ,IAAI,UAAU,QAAQ,cAAc,cAAc,KAAK,QAAQ,GAAG,CAAC;AAE3E,MAAI,QAAQ;AACV,WAAQ,IAAI,UAAU,QAAQ,qCAAqC,CAAC;AACpE;AACA;;AAGF,MAAI;GAEF,MAAMF,UAAmB,EAAE;AAE3B,OAAI,KAAK,QAAQ,SACf,SAAQ,KAAK;IACX,MAAM;IACN,aAAa;IACb,SAAS,OAAO,KAAK,cAAc;IACnC,aAAa,OAAO,KAAK,cAAc;IACxC,CAAC;AAGJ,OAAI,KAAK,QAAQ,UACf,SAAQ,KAAK;IACX,MAAM;IACN,aAAa;IACb,SAAS,OAAO,KAAK,8BAA8B;IACnD,aAAa,OAAO,KAAK,gCAA8B;IACxD,CAAC;AAGJ,OAAI,KAAK,QAAQ,SAAS;IACxB,MAAM,cAAc;IACpB,MAAM,YAAY,KAAK,QAAQ,QAAQ,OAAO,IAAoB,IAAI;AACtE,YAAQ,KAAK;KACX,MAAM;KACN,aAAa,iCAAiC,KAAK,QAAQ,QAAQ;KACnE,SAAS,OAAO,KAAK,YAAY;KACjC,aAAa,OAAO,KAAK,UAAU;KACpC,CAAC;;AAGJ,OAAI,KAAK,QAAQ,iBAAiB;AAChC,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,uCAAqC;KAC1D,aAAa,OAAO,KAAK,uCAAqC;KAC/D,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,kCAAgC;KACrD,aAAa,OAAO,KAAK,kCAAgC;KAC1D,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,qCAAqC;KAC1D,aAAa,OAAO,KAAK,qCAAqC;KAC/D,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,sCAAsC;KAC3D,aAAa,OAAO,KAAK,sCAAsC;KAChE,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,kDAAkD;KACvE,aAAa,OAAO,KAAK,kDAAkD;KAC5E,CAAC;;AAGJ,OAAI,KAAK,QAAQ,aAAa;AAC5B,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,gBAAgB;KACrC,aAAa,OAAO,KAAK,gBAAgB;KAC1C,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,kBAAkB;KACvC,aAAa,OAAO,KAAK,kBAAkB;KAC5C,CAAC;AACF,YAAQ,KAAK;KACX,MAAM;KACN,aAAa;KACb,SAAS,OAAO,KAAK,4BAA4B;KACjD,aAAa,OAAO,KAAK,4BAA4B;KACtD,CAAC;;GAKJ,MAAM,aAAa,KADH,KAAK,SAAS,EAAE,gBAAgB,OAAO,EACtB,GAAG,KAAK,KAAK,UAAU;AAGxD,OAAI,QAAQ,SAAS,GAAG;AAUtB,QAAI,EATW,MAAM,WAAW;KAC9B,WAAW;KACX;KACA;KACA,QAAQ;KACR,QAAQ;KACR;KACD,CAAC,EAEU,SAAS;AACnB,aAAQ,IAAI,UAAU,OAAO,8BAA8B,CAAC;AAC5D;AACA;;AAIF,QAAI,QAAQ,aAAa,SACvB,KAAI;KACF,MAAM,EAAE,yBAAa,MAAM,OAAO;AAClC,gBAAS,qCAAqC,WAAW,IAAI,EAC3D,OAAO,QACR,CAAC;AACF,SAAI,QACF,SAAQ,IAAI,UAAU,QAAQ,qBAAqB,CAAC;YAEhD;AACN,aAAQ,IAAI,UAAU,UAAU,iCAAiC,CAAC;;;AAQxE,OADqB,KAAK,QAAQ,aAAa,CAAC,CAAC,KAAK,QAAQ,OAC5C;IAEhB,MAAM,gBACJ,KAAK,QAAQ,WAAW,KAAK,QAAQ,SAAS;AAGhD,UAAM,4BAFW,KAAK,SAAS,EAAE,gBAAgB,QAAQ,EAChC,QAAQ,SAAS,IAAI,aAAa,eAIzD,KAAK,MACL,eACA,KAAK,QAAQ,cAAc,MAC5B;AACD,QAAI,SAAS;AACX,aAAQ,IAAI,UAAU,QAAQ,kCAAkC,CAAC;AACjE,SAAI,KAAK,QAAQ,WACf,SAAQ,IAAI,UAAU,QAAQ,6BAA6B,CAAC;;AAIhE,QAAI,KAAK,QAAQ,SAAS,CAAC,KAAK,QAAQ,WAAW;AACjD,UAAK,QAAQ,YAAY;AACzB,UAAK,QAAQ,UAAU,KAAK,QAAQ;AACpC,YAAO,KAAK,QAAQ;;;AAKxB,QAAK,6BAAY,IAAI,MAAM,EAAC,aAAa;AACzC,QAAK,qBAAqB;AAC1B,SAAM,kBAAkB,KAAK;AAE7B,WAAQ,IAAI,UAAU,SAAS,2BAA2B,CAAC;AAC3D;WACO,OAAO;AACd,WAAQ,IAAI,UAAU,OAAO,cAAe,MAAgB,UAAU,CAAC;AACvE,OAAI,QACF,SAAQ,MAAO,MAAgB,MAAM;AAEvC;;;AAIJ,SAAQ,KAAK;AACb,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;AAC9C,KAAI,QAAQ;AACV,UAAQ,IAAI,UAAU,CAAC,QAAQ,OAAO,EAAE,qBAAqB,CAAC;AAC9D,UAAQ,IAAI,UAAU,QAAQ,kBAAkB,aAAa,YAAY,CAAC;YACjE,cAAc,GAAG;AAC1B,UAAQ,IAAI,UAAU,CAAC,SAAS,OAAO,EAAE,oBAAoB,CAAC;AAC9D,UAAQ,IAAI,UAAU,QAAQ,aAAa,aAAa,YAAY,CAAC;QAChE;AACL,UAAQ,IAAI,UAAU,CAAC,UAAU,OAAO,EAAE,gCAAgC,CAAC;AAC3E,UAAQ,IAAI,UAAU,QAAQ,cAAc,aAAa,YAAY,YAAY,CAAC;;AAEpF,SAAQ,IAAI,UAAU,QAAQ,IAAI,OAAO,GAAG,CAAC,CAAC;EAC9C,CACD,KAAK,CACL,OAAO,QAAe;AACrB,SAAQ,MAAM,IAAI;AAClB,SAAQ,KAAK,EAAE;EACf"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["patches: Patch[]","unlink","metaList: Awaited<ReturnType<typeof loadAliasMetadata>>[]"],"sources":["../src/websearch-patch.ts","../src/cli.ts"],"sourcesContent":["import type { Patch } from \"./patcher.ts\";\nimport { writeFile, chmod, mkdir } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { existsSync } from \"node:fs\";\n\n/**\n * WebSearch Patch Generator\n *\n * Since injecting code directly into binary is complex (requires exact byte length matching),\n * we use a more practical approach:\n *\n * 1. --websearch option will:\n * a) Generate a standalone search proxy server script\n * b) Modify droid's API URL to point to local proxy (using --api-base)\n * c) Create a wrapper script to start both proxy and droid\n *\n * Environment variables:\n * - GOOGLE_PSE_API_KEY: Google Programmable Search Engine API Key\n * - GOOGLE_PSE_CX: Google Custom Search Engine ID\n * - If not set, will fallback to DuckDuckGo\n */\n\n/**\n * Generate search proxy server code\n */\nfunction generateSearchProxyServerCode(): string {\n return `#!/usr/bin/env node\n/**\n * Droid WebSearch Proxy Server\n * Auto-generated by droid-patch --websearch\n * \n * Supports:\n * - Google PSE (requires GOOGLE_PSE_API_KEY and GOOGLE_PSE_CX)\n * - DuckDuckGo (free fallback)\n */\n\nconst http = require('http');\nconst https = require('https');\n\nconst FACTORY_API = 'https://api.factory.ai';\n\n// Auto-find available port\nfunction findAvailablePort(startPort = 23119) {\n return new Promise((resolve, reject) => {\n const net = require('net');\n const server = net.createServer();\n \n server.listen(startPort, '127.0.0.1', () => {\n const port = server.address().port;\n server.close(() => resolve(port));\n });\n \n server.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n // Port is in use, try next one\n resolve(findAvailablePort(startPort + 1));\n } else {\n reject(err);\n }\n });\n });\n}\n\nlet PORT = process.env.SEARCH_PROXY_PORT || 23119;\n\n// === Search Implementation ===\n\nasync function searchGooglePSE(query, numResults, apiKey, cx) {\n // Use curl command\n const { execSync } = require('child_process');\n \n const url = 'https://www.googleapis.com/customsearch/v1?key=' + apiKey + '&cx=' + cx + '&q=' + encodeURIComponent(query) + '&num=' + Math.min(numResults, 10);\n const curlCmd = \\`curl -s \"\\${url}\"\\`;\n \n try {\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n \n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n snippet: item.snippet,\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n throw new Error('Google PSE error: ' + e.message);\n }\n}\n\nasync function searchDuckDuckGo(query, numResults) {\n // Use curl command, because Node.js fetch may have issues in some environments\n const { execSync } = require('child_process');\n\n // Method 1: Try using DuckDuckGo HTML lite version (via curl)\n try {\n const curlCmd = \\`curl -s -X POST \"https://lite.duckduckgo.com/lite/\" -H \"Content-Type: application/x-www-form-urlencoded\" -H \"User-Agent: Mozilla/5.0\" -d \"q=\\${encodeURIComponent(query)}\"\\`;\n const html = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n\n if (html && html.length > 1000) {\n const results = parseDDGLiteHTML(html, numResults);\n if (results.length > 0) {\n console.error('[search] DDG lite returned ' + results.length + ' results');\n return results;\n }\n }\n } catch (e) {\n console.error('[search] DDG lite (curl) failed:', e.message);\n }\n\n // Method 2: Fallback to Instant Answer API (via curl)\n try {\n const apiUrl = 'https://api.duckduckgo.com/?q=' + encodeURIComponent(query) + '&format=json&no_html=1&skip_disambig=1';\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n \n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n snippet: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n snippet: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n snippet: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n console.error('[search] DDG API returned ' + results.length + ' results');\n return results;\n }\n } catch (e) {\n console.error('[search] DDG API (curl) failed:', e.message);\n }\n\n return [];\n}\n\n// Parse DuckDuckGo Lite HTML\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n\n // Match result links - DuckDuckGo Lite format\n // <a rel=\"nofollow\" href=\"URL\">TITLE</a>\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n // Extract all links\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n // Skip DuckDuckGo internal links\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n // Decode redirect URL\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n // Extract snippets\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n // Combine results\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n snippet: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n \n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n const googleApiKey = process.env.GOOGLE_PSE_API_KEY;\n const googleCx = process.env.GOOGLE_PSE_CX;\n\n // Try Google PSE first\n if (googleApiKey && googleCx) {\n try {\n console.error('[search] Trying Google PSE...');\n const results = await searchGooglePSE(query, numResults, googleApiKey, googleCx);\n if (results.length > 0) {\n console.error('[search] Google PSE returned ' + results.length + ' results');\n return { results, source: 'google-pse' };\n }\n } catch (e) {\n console.error('[search] Google PSE failed:', e.message);\n }\n }\n\n // Fallback to DuckDuckGo\n try {\n console.error('[search] Using DuckDuckGo...');\n const results = await searchDuckDuckGo(query, numResults);\n console.error('[search] DuckDuckGo returned ' + results.length + ' results');\n return { results, source: 'duckduckgo' };\n } catch (e) {\n console.error('[search] DuckDuckGo failed:', e.message);\n }\n\n return { results: [], source: 'none' };\n}\n\n// === HTTP Server ===\n\nconst server = http.createServer(async (req, res) => {\n const url = new URL(req.url, 'http://' + req.headers.host);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ \n status: 'ok',\n google: !!(process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX),\n duckduckgo: true\n }));\n return;\n }\n\n // Search endpoint\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', chunk => body += chunk);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n console.error('[search] Query: \"' + query + '\"');\n \n const { results, source } = await search(query, numResults || 10);\n console.error('[search] ' + results.length + ' results from ' + source);\n \n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n console.error('[search] Error:', e);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // Proxy other requests to Factory API\n console.error('[proxy] ' + req.method + ' ' + url.pathname);\n \n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n \n const proxyReq = https.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n\n proxyReq.on('error', e => {\n console.error('[proxy] Error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed' }));\n });\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n});\n\n// Start server (async, auto-find available port)\n(async () => {\n const fs = require('fs');\n const path = require('path');\n\n // If port not specified, auto-find available port\n if (!process.env.SEARCH_PROXY_PORT) {\n PORT = await findAvailablePort(23119);\n }\n\n server.listen(PORT, '127.0.0.1', () => {\n const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;\n\n // Write port number to temp file for wrapper script to read\n const portFile = process.env.SEARCH_PROXY_PORT_FILE || path.join(require('os').tmpdir(), 'droid-search-proxy-' + process.pid + '.port');\n fs.writeFileSync(portFile, PORT.toString());\n\n // Output port number to stdout (for parent process to capture)\n console.log('PORT=' + PORT);\n \n console.error('');\n console.error('╔═══════════════════════════════════════════════════════════════╗');\n console.error('║ Droid WebSearch Proxy ║');\n console.error('╠═══════════════════════════════════════════════════════════════╣');\n console.error('║ 🔍 Google PSE: ' + (hasGoogle ? 'Configured ✓' : 'Not set (set GOOGLE_PSE_API_KEY & CX)').padEnd(45) + '║');\n console.error('║ 🦆 DuckDuckGo: Always available ║');\n console.error('║ 🚀 Server: http://127.0.0.1:' + PORT + ' ║'.slice(0, 65) + '║');\n console.error('╚═══════════════════════════════════════════════════════════════╝');\n console.error('');\n });\n})();\n\n// Handle graceful shutdown\nprocess.on('SIGTERM', () => server.close());\nprocess.on('SIGINT', () => server.close());\n`;\n}\n\n/**\n * Generate wrapper script, auto-start proxy and droid\n */\nfunction generateWrapperScript(droidPath: string, proxyScriptPath: string): string {\n return `#!/bin/bash\n# Droid with WebSearch Proxy\n# Auto-generated by droid-patch --websearch\n\nPROXY_SCRIPT=\"${proxyScriptPath}\"\nDROID_BIN=\"${droidPath}\"\nPORT_FILE=\"/tmp/droid-search-proxy-$$.port\"\n\n# Start proxy and get dynamic port\nstart_proxy() {\n # Start proxy, capture output to get port\n SEARCH_PROXY_PORT_FILE=\"$PORT_FILE\" node \"$PROXY_SCRIPT\" &\n PROXY_PID=$!\n\n # Wait for proxy to start and get port\n for i in {1..20}; do\n if [ -f \"$PORT_FILE\" ]; then\n PORT=$(cat \"$PORT_FILE\")\n if curl -s \"http://127.0.0.1:$PORT/health\" > /dev/null 2>&1; then\n echo \"[websearch] Proxy started on port $PORT\"\n return 0\n fi\n fi\n sleep 0.2\n done\n\n echo \"[websearch] Failed to start proxy\"\n kill $PROXY_PID 2>/dev/null\n return 1\n}\n\n# Cleanup function\ncleanup() {\n [ -n \"$PROXY_PID\" ] && kill $PROXY_PID 2>/dev/null\n [ -f \"$PORT_FILE\" ] && rm -f \"$PORT_FILE\"\n}\ntrap cleanup EXIT\n\n# Start proxy\nif ! start_proxy; then\n exit 1\nfi\n\n# Run droid\nexport FACTORY_API_BASE_URL_OVERRIDE=\"http://127.0.0.1:$PORT\"\nexec \"$DROID_BIN\" \"$@\"\n`;\n}\n\n/**\n * Generate WebSearch Patch\n *\n * Since injecting code directly into binary is complex, we use the following strategy:\n * 1. Create proxy server script\n * 2. Modify API URL to point to local\n * 3. Return a combined patch\n */\nexport function generateWebSearchPatch(): Patch | null {\n // Return a URL replacement patch\n // Use local proxy port 23119 (idle port)\n const originalUrl = \"https://api.factory.ai\";\n const localUrl = \"http://127.0.0.1:23119\";\n\n // Need to pad to same length\n if (localUrl.length > originalUrl.length) {\n console.error(`[websearch] Local URL too long: ${localUrl.length} > ${originalUrl.length}`);\n return null;\n }\n\n const paddedUrl = localUrl.padEnd(originalUrl.length, \" \");\n\n return {\n name: \"webSearch\",\n description: `Replace API URL with local proxy (${localUrl})`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n };\n}\n\n/**\n * Create WebSearch proxy files\n */\nexport async function createWebSearchProxyFiles(\n outputDir: string,\n droidPath: string,\n aliasName: string,\n): Promise<{ proxyScript: string; wrapperScript: string }> {\n // Ensure directory exists\n if (!existsSync(outputDir)) {\n await mkdir(outputDir, { recursive: true });\n }\n\n const proxyScriptPath = join(outputDir, `${aliasName}-search-proxy.js`);\n const wrapperScriptPath = join(outputDir, `${aliasName}-with-search`);\n\n // Write proxy server script\n await writeFile(proxyScriptPath, generateSearchProxyServerCode());\n console.log(`[*] Created search proxy: ${proxyScriptPath}`);\n\n // Write wrapper script\n await writeFile(wrapperScriptPath, generateWrapperScript(droidPath, proxyScriptPath));\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper script: ${wrapperScriptPath}`);\n\n return {\n proxyScript: proxyScriptPath,\n wrapperScript: wrapperScriptPath,\n };\n}\n\n/**\n * Get proxy server code (for export)\n */\nexport function getSearchProxyCode(): string {\n return generateSearchProxyServerCode();\n}\n\n/**\n * Generate Bun preload script\n * This script executes before droid main program, starts search proxy\n */\nfunction generatePreloadScript(): string {\n return `// Droid WebSearch Preload Script\n// Auto-generated by droid-patch --websearch-preload\n// Start search proxy before droid main program\n\nconst http = require('http');\nconst https = require('https');\nconst { execSync } = require('child_process');\n\nconst PORT = process.env.DROID_SEARCH_PORT || 23119;\nconst FACTORY_API = 'https://api.factory.ai';\n\n// Google PSE search\nasync function searchGooglePSE(query, num) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n \n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(num, 10)}\\`;\n const res = await fetch(url);\n const data = await res.json();\n if (data.error) return null;\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || ''\n }));\n } catch (e) {\n return null;\n }\n}\n\n// DuckDuckGo search (use curl for reliability)\nfunction searchDuckDuckGo(query, num) {\n try {\n const url = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const output = execSync(\\`curl -s \"\\${url}\"\\`, { encoding: 'utf8', timeout: 10000 });\n const data = JSON.parse(output);\n const results = [];\n\n if (data.AbstractText && data.AbstractURL) {\n results.push({ title: data.Heading || query, url: data.AbstractURL, content: data.AbstractText });\n }\n\n for (const t of (data.RelatedTopics || [])) {\n if (results.length >= num) break;\n if (t.Text && t.FirstURL) {\n results.push({ title: t.Text.split(' - ')[0], url: t.FirstURL, content: t.Text });\n }\n // Handle subcategories\n if (t.Topics) {\n for (const sub of t.Topics) {\n if (results.length >= num) break;\n if (sub.Text && sub.FirstURL) {\n results.push({ title: sub.Text.split(' - ')[0], url: sub.FirstURL, content: sub.Text });\n }\n }\n }\n }\n return results;\n } catch (e) {\n return [];\n }\n}\n\n// Search function\nasync function search(query, num) {\n // Try Google PSE first\n const googleResults = await searchGooglePSE(query, num);\n if (googleResults && googleResults.length > 0) {\n console.error('[preload-search] Using Google PSE');\n return googleResults;\n }\n\n // Fallback to DuckDuckGo\n console.error('[preload-search] Using DuckDuckGo');\n return searchDuckDuckGo(query, num);\n}\n\n// Check if port is already in use\nfunction isPortInUse(port) {\n try {\n execSync(\\`curl -s http://127.0.0.1:\\${port}/health\\`, { timeout: 1000 });\n return true;\n } catch {\n return false;\n }\n}\n\n// Skip if proxy already running\nif (isPortInUse(PORT)) {\n console.error(\\`[preload] Search proxy already running on port \\${PORT}\\`);\n} else {\n // Start proxy server\n const server = http.createServer(async (req, res) => {\n const url = new URL(req.url, \\`http://\\${req.headers.host}\\`);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // Search endpoint\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', c => body += c);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n console.error(\\`[preload-search] Query: \"\\${query}\"\\`);\n const results = await search(query, numResults || 10);\n console.error(\\`[preload-search] Found \\${results.length} results\\`);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n console.error('[preload-search] Error:', e.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // Proxy other requests to Factory API\n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n const proxyReq = https.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n proxyReq.on('error', (e) => {\n console.error('[preload-proxy] Error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed' }));\n });\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n });\n\n server.listen(PORT, '127.0.0.1', () => {\n console.error(\\`[preload] Search proxy started on http://127.0.0.1:\\${PORT}\\`);\n });\n}\n`;\n}\n\n/**\n * Generate bunfig.toml content\n */\nfunction generateBunfigToml(preloadScriptPath: string): string {\n return `# Droid WebSearch Configuration\n# Auto-generated by droid-patch --websearch-preload\n\npreload = [\"${preloadScriptPath}\"]\n`;\n}\n\n/**\n * Generate preload wrapper script\n * This script cd's to the bunfig.toml directory, then executes droid\n */\nfunction generatePreloadWrapperScript(droidPath: string, bunfigDir: string): string {\n return `#!/bin/bash\n# Droid with WebSearch (Preload)\n# Auto-generated by droid-patch --preload\n\nBUNFIG_DIR=\"${bunfigDir}\"\nDROID_BIN=\"${droidPath}\"\nORIGINAL_DIR=\"$(pwd)\"\n\n# cd to bunfig.toml directory (Bun reads bunfig.toml from cwd)\ncd \"$BUNFIG_DIR\"\n\n# Execute droid, pass all arguments, set working directory to original\nexec \"$DROID_BIN\" --cwd \"$ORIGINAL_DIR\" \"$@\"\n`;\n}\n\n/**\n * Create WebSearch files using Preload method\n *\n * Advantages:\n * - No need to modify binary\n * - Uses Bun's native preload mechanism\n *\n * Files created:\n * - preload script (search proxy)\n * - bunfig.toml (Bun configuration)\n * - wrapper script (directly executable command)\n */\nexport async function createWebSearchPreloadFiles(\n droidDir: string,\n droidPath: string,\n aliasName: string,\n): Promise<{\n preloadScript: string;\n bunfigPath: string;\n wrapperScript: string;\n}> {\n // Ensure directory exists\n if (!existsSync(droidDir)) {\n await mkdir(droidDir, { recursive: true });\n }\n\n const preloadScriptPath = join(droidDir, `${aliasName}-search-preload.js`);\n const bunfigPath = join(droidDir, \"bunfig.toml\");\n const wrapperScriptPath = join(droidDir, aliasName);\n\n // Write preload script\n await writeFile(preloadScriptPath, generatePreloadScript());\n console.log(`[*] Created preload script: ${preloadScriptPath}`);\n\n // Write bunfig.toml\n await writeFile(bunfigPath, generateBunfigToml(preloadScriptPath));\n console.log(`[*] Created bunfig.toml: ${bunfigPath}`);\n\n // Write wrapper script\n await writeFile(wrapperScriptPath, generatePreloadWrapperScript(droidPath, droidDir));\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper: ${wrapperScriptPath}`);\n\n return {\n preloadScript: preloadScriptPath,\n bunfigPath: bunfigPath,\n wrapperScript: wrapperScriptPath,\n };\n}\n\n/**\n * Get preload script code (for export)\n */\nexport function getPreloadScriptCode(): string {\n return generatePreloadScript();\n}\n\n/**\n * Generate unified Fetch Hook Preload script\n * Directly hooks globalThis.fetch, no proxy server needed\n * @internal Reserved for future use - alternative to proxy server approach\n */\nfunction _generateFetchHookPreload(): string {\n return `// Droid WebSearch Fetch Hook\n// Auto-generated by droid-patch --websearch\n// Hook globalThis.fetch to intercept search requests\n\nconst DEBUG = process.env.DROID_SEARCH_DEBUG === '1';\n\nfunction log(...args) {\n if (DEBUG) console.error('[websearch]', ...args);\n}\n\n// === Search Implementation ===\n\nasync function searchGooglePSE(query, numResults) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n\n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(numResults, 10)}\\`;\n const res = await fetch(url);\n const data = await res.json();\n if (data.error) {\n log('Google PSE error:', data.error.message);\n return null;\n }\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n log('Google PSE failed:', e.message);\n return null;\n }\n}\n\nasync function searchDuckDuckGo(query, numResults) {\n const { execSync } = require('child_process');\n\n // Method 1: Try DuckDuckGo HTML lite\n try {\n const curlCmd = \\`curl -s -X POST \"https://lite.duckduckgo.com/lite/\" -H \"Content-Type: application/x-www-form-urlencoded\" -H \"User-Agent: Mozilla/5.0\" -d \"q=\\${encodeURIComponent(query)}\"\\`;\n const html = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n\n if (html && html.length > 1000) {\n const results = parseDDGLiteHTML(html, numResults);\n if (results.length > 0) {\n log('DDG lite:', results.length, 'results');\n return results;\n }\n }\n } catch (e) {\n log('DDG lite failed:', e.message);\n }\n\n // Method 2: Fallback to Instant Answer API\n try {\n const apiUrl = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n content: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n content: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n content: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n log('DDG API:', results.length, 'results');\n return results;\n }\n } catch (e) {\n log('DDG API failed:', e.message);\n }\n\n return [];\n}\n\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n content: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n // Try Google PSE first\n const googleResults = await searchGooglePSE(query, numResults);\n if (googleResults && googleResults.length > 0) {\n log('Using Google PSE');\n return { results: googleResults, source: 'google-pse' };\n }\n\n // Fallback to DuckDuckGo\n log('Using DuckDuckGo');\n const ddgResults = await searchDuckDuckGo(query, numResults);\n return { results: ddgResults, source: 'duckduckgo' };\n}\n\n// === Fetch Hook ===\n\nconst originalFetch = globalThis.fetch;\n\nglobalThis.fetch = async function(input, init) {\n const url = typeof input === 'string' ? input : (input instanceof URL ? input.href : input.url);\n\n // Intercept search requests\n if (url && url.includes('/api/tools/exa/search')) {\n log('Intercepted search request');\n\n try {\n let body = init?.body;\n if (body && typeof body !== 'string') {\n body = await new Response(body).text();\n }\n\n const { query, numResults } = JSON.parse(body || '{}');\n log('Query:', query);\n\n const { results, source } = await search(query, numResults || 10);\n log('Results:', results.length, 'from', source);\n\n return new Response(JSON.stringify({ results }), {\n status: 200,\n headers: { 'Content-Type': 'application/json' }\n });\n } catch (e) {\n log('Search error:', e.message);\n return new Response(JSON.stringify({ error: String(e), results: [] }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' }\n });\n }\n }\n\n // Pass through all other requests\n return originalFetch.apply(this, arguments);\n};\n\n// Also hook Bun.fetch if available\nif (typeof Bun !== 'undefined' && Bun.fetch) {\n const originalBunFetch = Bun.fetch;\n Bun.fetch = globalThis.fetch;\n}\n\nlog('Fetch hook installed');\n`;\n}\n\n/**\n * Generate search proxy server code (runs in background)\n * Since BUN_CONFIG_PRELOAD doesn't work with compiled binaries,\n * use a local proxy server to intercept search requests instead\n *\n * Each droid instance runs its own proxy server.\n * The proxy is killed automatically when droid exits.\n * @param factoryApiUrl - Custom Factory API URL (default: https://api.factory.ai)\n */\nfunction generateSearchProxyServer(factoryApiUrl: string = \"https://api.factory.ai\"): string {\n return `#!/usr/bin/env node\n// Droid WebSearch Proxy Server\n// Auto-generated by droid-patch --websearch\n// This proxy runs as a child process of droid and is killed when droid exits\n\nconst http = require('http');\nconst https = require('https');\nconst { execSync } = require('child_process');\nconst fs = require('fs');\n\nconst DEBUG = process.env.DROID_SEARCH_DEBUG === '1';\nconst PORT = parseInt(process.env.SEARCH_PROXY_PORT || '0'); // 0 = auto-assign\nconst FACTORY_API = '${factoryApiUrl}';\n\nfunction log(...args) {\n if (DEBUG) console.error('[websearch]', ...args);\n}\n\n// === Search Implementation ===\n\n// Smithery Exa MCP - highest priority, requires SMITHERY_API_KEY and SMITHERY_PROFILE\nasync function searchSmitheryExa(query, numResults) {\n const apiKey = process.env.SMITHERY_API_KEY;\n const profile = process.env.SMITHERY_PROFILE;\n if (!apiKey || !profile) return null;\n\n try {\n // Construct URL with authentication\n const serverUrl = \\`https://server.smithery.ai/exa/mcp?api_key=\\${encodeURIComponent(apiKey)}&profile=\\${encodeURIComponent(profile)}\\`;\n log('Smithery Exa request');\n\n // Use MCP protocol to call the search tool via HTTP POST\n const requestBody = JSON.stringify({\n jsonrpc: '2.0',\n id: 1,\n method: 'tools/call',\n params: {\n name: 'web_search_exa',\n arguments: {\n query: query,\n numResults: numResults\n }\n }\n });\n\n const curlCmd = \\`curl -s -X POST \"\\${serverUrl}\" -H \"Content-Type: application/json\" -d '\\${requestBody.replace(/'/g, \"'\\\\\\\\\\\\\\\\''\")}'\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 30000 });\n const response = JSON.parse(jsonStr);\n\n // Parse MCP response\n if (response.result && response.result.content) {\n // MCP returns content as array of text blocks\n const textContent = response.result.content.find(c => c.type === 'text');\n if (textContent && textContent.text) {\n try {\n const searchResults = JSON.parse(textContent.text);\n if (Array.isArray(searchResults) && searchResults.length > 0) {\n return searchResults.slice(0, numResults).map(item => ({\n title: item.title || '',\n url: item.url || '',\n content: item.text || item.snippet || item.highlights?.join(' ') || '',\n publishedDate: item.publishedDate || null,\n author: item.author || null,\n score: item.score || null\n }));\n }\n } catch (parseErr) {\n log('Smithery response parsing failed');\n }\n }\n }\n\n if (response.error) {\n log('Smithery Exa error:', response.error.message || response.error);\n return null;\n }\n } catch (e) {\n log('Smithery Exa failed:', e.message);\n return null;\n }\n return null;\n}\n\nasync function searchGooglePSE(query, numResults) {\n const apiKey = process.env.GOOGLE_PSE_API_KEY;\n const cx = process.env.GOOGLE_PSE_CX;\n if (!apiKey || !cx) return null;\n\n try {\n const url = \\`https://www.googleapis.com/customsearch/v1?key=\\${apiKey}&cx=\\${cx}&q=\\${encodeURIComponent(query)}&num=\\${Math.min(numResults, 10)}\\`;\n log('Google PSE request:', url.replace(apiKey, '***'));\n\n const curlCmd = \\`curl -s \"\\${url}\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.error) {\n log('Google PSE error:', data.error.message);\n return null;\n }\n return (data.items || []).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n } catch (e) {\n log('Google PSE failed:', e.message);\n return null;\n }\n}\n\n// SearXNG - self-hosted meta search engine\nasync function searchSearXNG(query, numResults) {\n const searxngUrl = process.env.SEARXNG_URL;\n if (!searxngUrl) return null;\n\n try {\n const url = \\`\\${searxngUrl}/search?q=\\${encodeURIComponent(query)}&format=json&engines=google,bing,duckduckgo\\`;\n log('SearXNG request:', url);\n\n const curlCmd = \\`curl -s \"\\${url}\" -H \"Accept: application/json\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.results && data.results.length > 0) {\n return data.results.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.url,\n content: item.content || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('SearXNG failed:', e.message);\n }\n return null;\n}\n\n// Serper API - free tier available (2500 queries/month)\nasync function searchSerper(query, numResults) {\n const apiKey = process.env.SERPER_API_KEY;\n if (!apiKey) return null;\n\n try {\n const curlCmd = \\`curl -s \"https://google.serper.dev/search\" -H \"X-API-KEY: \\${apiKey}\" -H \"Content-Type: application/json\" -d '{\"q\":\"\\${query.replace(/\"/g, '\\\\\\\\\"')}\",\"num\":\\${numResults}}'\\`;\n log('Serper request');\n\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.organic && data.organic.length > 0) {\n return data.organic.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.link,\n content: item.snippet || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('Serper failed:', e.message);\n }\n return null;\n}\n\n// Brave Search API - free tier available\nasync function searchBrave(query, numResults) {\n const apiKey = process.env.BRAVE_API_KEY;\n if (!apiKey) return null;\n\n try {\n const url = \\`https://api.search.brave.com/res/v1/web/search?q=\\${encodeURIComponent(query)}&count=\\${numResults}\\`;\n const curlCmd = \\`curl -s \"\\${url}\" -H \"Accept: application/json\" -H \"X-Subscription-Token: \\${apiKey}\"\\`;\n log('Brave request');\n\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n if (data.web && data.web.results && data.web.results.length > 0) {\n return data.web.results.slice(0, numResults).map(item => ({\n title: item.title,\n url: item.url,\n content: item.description || '',\n publishedDate: null,\n author: null,\n score: null\n }));\n }\n } catch (e) {\n log('Brave failed:', e.message);\n }\n return null;\n}\n\n// DuckDuckGo - limited reliability due to bot detection\nasync function searchDuckDuckGo(query, numResults) {\n // DuckDuckGo Instant Answer API (limited results but more reliable)\n try {\n const apiUrl = \\`https://api.duckduckgo.com/?q=\\${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1\\`;\n const curlCmd = \\`curl -s \"\\${apiUrl}\" -H \"User-Agent: Mozilla/5.0\"\\`;\n const jsonStr = execSync(curlCmd, { encoding: 'utf-8', timeout: 15000 });\n const data = JSON.parse(jsonStr);\n\n const results = [];\n\n if (data.Abstract && data.AbstractURL) {\n results.push({\n title: data.Heading || query,\n url: data.AbstractURL,\n content: data.Abstract,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n for (const topic of (data.RelatedTopics || [])) {\n if (results.length >= numResults) break;\n if (topic.Text && topic.FirstURL) {\n results.push({\n title: topic.Text.substring(0, 100),\n url: topic.FirstURL,\n content: topic.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n if (topic.Topics) {\n for (const st of topic.Topics) {\n if (results.length >= numResults) break;\n if (st.Text && st.FirstURL) {\n results.push({\n title: st.Text.substring(0, 100),\n url: st.FirstURL,\n content: st.Text,\n publishedDate: null,\n author: null,\n score: null\n });\n }\n }\n }\n }\n\n if (results.length > 0) {\n log('DDG API:', results.length, 'results');\n return results;\n }\n } catch (e) {\n log('DDG API failed:', e.message);\n }\n\n return [];\n}\n\nfunction parseDDGLiteHTML(html, maxResults) {\n const results = [];\n const linkRegex = /<a[^>]+rel=\"nofollow\"[^>]+href=\"([^\"]+)\"[^>]*>([^<]+)<\\\\/a>/gi;\n const snippetRegex = /<td[^>]*class=\"result-snippet\"[^>]*>([^<]*)<\\\\/td>/gi;\n\n const links = [];\n let match;\n\n while ((match = linkRegex.exec(html)) !== null && links.length < maxResults) {\n let url = match[1];\n if (url.includes('duckduckgo.com') && !url.includes('uddg=')) continue;\n if (url.includes('uddg=')) {\n const uddgMatch = url.match(/uddg=([^&]+)/);\n if (uddgMatch) url = decodeURIComponent(uddgMatch[1]);\n }\n links.push({\n url: url,\n title: decodeHTMLEntities(match[2].trim())\n });\n }\n\n const snippets = [];\n while ((match = snippetRegex.exec(html)) !== null && snippets.length < maxResults) {\n snippets.push(decodeHTMLEntities(match[1].trim()));\n }\n\n for (let i = 0; i < links.length && results.length < maxResults; i++) {\n results.push({\n title: links[i].title,\n url: links[i].url,\n content: snippets[i] || '',\n publishedDate: null,\n author: null,\n score: null\n });\n }\n\n return results;\n}\n\nfunction decodeHTMLEntities(str) {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n}\n\nasync function search(query, numResults = 10) {\n // Priority order:\n // 1. Smithery Exa MCP (best quality if configured)\n // 2. Google PSE (most reliable if configured)\n // 3. Serper (free tier: 2500/month)\n // 4. Brave Search (free tier available)\n // 5. SearXNG (self-hosted)\n // 6. DuckDuckGo (limited due to bot detection)\n\n // 1. Smithery Exa MCP (highest priority)\n const smitheryResults = await searchSmitheryExa(query, numResults);\n if (smitheryResults && smitheryResults.length > 0) {\n log('Using Smithery Exa');\n return { results: smitheryResults, source: 'smithery-exa' };\n }\n\n // 2. Google PSE\n const googleResults = await searchGooglePSE(query, numResults);\n if (googleResults && googleResults.length > 0) {\n log('Using Google PSE');\n return { results: googleResults, source: 'google-pse' };\n }\n\n // 3. Serper\n const serperResults = await searchSerper(query, numResults);\n if (serperResults && serperResults.length > 0) {\n log('Using Serper');\n return { results: serperResults, source: 'serper' };\n }\n\n // 4. Brave Search\n const braveResults = await searchBrave(query, numResults);\n if (braveResults && braveResults.length > 0) {\n log('Using Brave Search');\n return { results: braveResults, source: 'brave' };\n }\n\n // 5. SearXNG\n const searxngResults = await searchSearXNG(query, numResults);\n if (searxngResults && searxngResults.length > 0) {\n log('Using SearXNG');\n return { results: searxngResults, source: 'searxng' };\n }\n\n // 6. DuckDuckGo (last resort, limited results)\n log('Using DuckDuckGo (fallback)');\n const ddgResults = await searchDuckDuckGo(query, numResults);\n return { results: ddgResults, source: 'duckduckgo' };\n}\n\n// === HTTP Proxy Server ===\n\nconst server = http.createServer(async (req, res) => {\n const url = new URL(req.url, \\`http://\\${req.headers.host}\\`);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok', port: server.address()?.port || PORT }));\n return;\n }\n\n // Search endpoint - intercept\n if (url.pathname === '/api/tools/exa/search' && req.method === 'POST') {\n let body = '';\n req.on('data', c => body += c);\n req.on('end', async () => {\n try {\n const { query, numResults } = JSON.parse(body);\n log('Search query:', query);\n const { results, source } = await search(query, numResults || 10);\n log('Results:', results.length, 'from', source);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ results }));\n } catch (e) {\n log('Search error:', e.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: String(e), results: [] }));\n }\n });\n return;\n }\n\n // === Standalone mode (controlled by STANDALONE_MODE env) ===\n // Whitelist approach: only allow core LLM APIs, mock everything else\n if (process.env.STANDALONE_MODE === '1') {\n const pathname = url.pathname;\n\n // Whitelist: Core APIs that should be forwarded to upstream\n const isCoreLLMApi = pathname.startsWith('/api/llm/a/') || pathname.startsWith('/api/llm/o/');\n // /api/tools/exa/search is already handled above\n\n if (!isCoreLLMApi) {\n // Special handling for specific routes\n if (pathname === '/api/sessions/create') {\n log('Mock (dynamic):', pathname);\n const sessionId = \\`local-\\${Date.now()}-\\${Math.random().toString(36).slice(2, 10)}\\`;\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ id: sessionId }));\n return;\n }\n\n if (pathname === '/api/cli/whoami') {\n log('Mock (401):', pathname);\n res.writeHead(401, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Unauthorized', message: 'Local mode - use token fallback' }));\n return;\n }\n\n if (pathname === '/api/tools/get-url-contents') {\n log('Mock (404):', pathname);\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not available', message: 'Use local URL fetch fallback' }));\n return;\n }\n\n // All other non-core APIs: return empty success\n log('Mock (default):', pathname);\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({}));\n return;\n }\n }\n\n // Proxy core LLM requests to upstream API\n log('Proxy:', req.method, url.pathname);\n\n const proxyUrl = new URL(FACTORY_API + url.pathname + url.search);\n // Choose http or https based on target protocol\n const proxyModule = proxyUrl.protocol === 'https:' ? https : http;\n const proxyReq = proxyModule.request(proxyUrl, {\n method: req.method,\n headers: { ...req.headers, host: proxyUrl.host }\n }, proxyRes => {\n res.writeHead(proxyRes.statusCode, proxyRes.headers);\n proxyRes.pipe(res);\n });\n\n proxyReq.on('error', e => {\n log('Proxy error:', e.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Proxy failed: ' + e.message }));\n });\n\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n req.pipe(proxyReq);\n } else {\n proxyReq.end();\n }\n});\n\n// If port is 0, system will automatically assign an available port\nserver.listen(PORT, '127.0.0.1', () => {\n const actualPort = server.address().port;\n const hasGoogle = process.env.GOOGLE_PSE_API_KEY && process.env.GOOGLE_PSE_CX;\n\n // Write port file for parent process to read\n const portFile = process.env.SEARCH_PROXY_PORT_FILE;\n if (portFile) {\n fs.writeFileSync(portFile, String(actualPort));\n }\n\n // Output PORT= line for wrapper script to parse\n console.log('PORT=' + actualPort);\n\n const hasSmithery = process.env.SMITHERY_API_KEY && process.env.SMITHERY_PROFILE;\n log('Search proxy started on http://127.0.0.1:' + actualPort);\n log('Smithery Exa:', hasSmithery ? 'configured (priority 1)' : 'not set');\n log('Google PSE:', hasGoogle ? 'configured' : 'not set');\n log('Serper:', process.env.SERPER_API_KEY ? 'configured' : 'not set');\n log('Brave:', process.env.BRAVE_API_KEY ? 'configured' : 'not set');\n log('SearXNG:', process.env.SEARXNG_URL || 'not set');\n});\n\nprocess.on('SIGTERM', () => { server.close(); process.exit(0); });\nprocess.on('SIGINT', () => { server.close(); process.exit(0); });\n`;\n}\n\n/**\n * Generate unified Wrapper script\n * Each droid instance runs its own proxy:\n * - Uses port 0 to let system auto-assign available port\n * - Proxy runs as child process\n * - Proxy is killed when droid exits\n * - Supports multiple droid instances running simultaneously\n */\n/* eslint-disable no-useless-escape */\nfunction generateUnifiedWrapper(\n droidPath: string,\n proxyScriptPath: string,\n standalone: boolean = false,\n): string {\n const standaloneEnv = standalone ? \"STANDALONE_MODE=1 \" : \"\";\n return `#!/bin/bash\n# Droid with WebSearch\n# Auto-generated by droid-patch --websearch\n# Each instance runs its own proxy on a system-assigned port\n\nPROXY_SCRIPT=\"${proxyScriptPath}\"\nDROID_BIN=\"${droidPath}\"\nPROXY_PID=\"\"\nPORT_FILE=\"/tmp/droid-websearch-\\$\\$.port\"\nSTANDALONE=\"${standalone ? \"1\" : \"0\"}\"\n\n# Cleanup function - kill proxy when droid exits\ncleanup() {\n if [ -n \"\\$PROXY_PID\" ] && kill -0 \"\\$PROXY_PID\" 2>/dev/null; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Stopping proxy (PID: \\$PROXY_PID)\" >&2\n kill \"\\$PROXY_PID\" 2>/dev/null\n wait \"\\$PROXY_PID\" 2>/dev/null\n fi\n rm -f \"\\$PORT_FILE\"\n}\n\n# Set up trap to cleanup on exit\ntrap cleanup EXIT INT TERM\n\n[ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Starting proxy...\" >&2\n[ \"\\$STANDALONE\" = \"1\" ] && [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Standalone mode enabled\" >&2\n\n# Start proxy with port 0 (system will assign available port)\n# Proxy writes actual port to PORT_FILE\nif [ -n \"\\$DROID_SEARCH_DEBUG\" ]; then\n ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE=\"\\$PORT_FILE\" node \"\\$PROXY_SCRIPT\" 2>&1 &\nelse\n ${standaloneEnv}SEARCH_PROXY_PORT=0 SEARCH_PROXY_PORT_FILE=\"\\$PORT_FILE\" node \"\\$PROXY_SCRIPT\" >/dev/null 2>&1 &\nfi\nPROXY_PID=\\$!\n\n# Wait for proxy to start and get actual port (max 5 seconds)\nfor i in {1..50}; do\n # Check if proxy process is still running\n if ! kill -0 \"\\$PROXY_PID\" 2>/dev/null; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Proxy process died\" >&2\n break\n fi\n if [ -f \"\\$PORT_FILE\" ]; then\n ACTUAL_PORT=\\$(cat \"\\$PORT_FILE\" 2>/dev/null)\n if [ -n \"\\$ACTUAL_PORT\" ] && curl -s \"http://127.0.0.1:\\$ACTUAL_PORT/health\" > /dev/null 2>&1; then\n [ -n \"\\$DROID_SEARCH_DEBUG\" ] && echo \"[websearch] Proxy ready on port \\$ACTUAL_PORT (PID: \\$PROXY_PID)\" >&2\n break\n fi\n fi\n sleep 0.1\ndone\n\n# Check if proxy started successfully\nif [ ! -f \"\\$PORT_FILE\" ] || [ -z \"\\$(cat \"\\$PORT_FILE\" 2>/dev/null)\" ]; then\n echo \"[websearch] Failed to start proxy, running without websearch\" >&2\n cleanup\n exec \"\\$DROID_BIN\" \"\\$@\"\nfi\n\nACTUAL_PORT=\\$(cat \"\\$PORT_FILE\")\nrm -f \"\\$PORT_FILE\"\n\n# Run droid with proxy\nexport FACTORY_API_BASE_URL_OVERRIDE=\"http://127.0.0.1:\\$ACTUAL_PORT\"\n\"\\$DROID_BIN\" \"\\$@\"\nDROID_EXIT_CODE=\\$?\n\n# Cleanup will be called by trap\nexit \\$DROID_EXIT_CODE\n`;\n}\n/* eslint-enable no-useless-escape */\n\n/**\n * Create unified WebSearch files\n *\n * Approach: Proxy server mode\n * - wrapper script starts local proxy server\n * - proxy server intercepts search requests, passes through other requests\n * - uses FACTORY_API_BASE_URL_OVERRIDE env var to point to proxy\n * - alias works directly, no extra steps needed\n *\n * @param outputDir - Directory to write files to\n * @param droidPath - Path to droid binary\n * @param aliasName - Alias name for the wrapper\n * @param apiBase - Custom API base URL for proxy to forward requests to\n * @param standalone - Standalone mode: mock non-LLM Factory APIs\n */\nexport async function createWebSearchUnifiedFiles(\n outputDir: string,\n droidPath: string,\n aliasName: string,\n apiBase?: string,\n standalone: boolean = false,\n): Promise<{ wrapperScript: string; preloadScript: string }> {\n if (!existsSync(outputDir)) {\n await mkdir(outputDir, { recursive: true });\n }\n\n const proxyScriptPath = join(outputDir, `${aliasName}-proxy.js`);\n const wrapperScriptPath = join(outputDir, aliasName);\n\n // Write proxy server script with custom API base if provided\n const factoryApiUrl = apiBase || \"https://api.factory.ai\";\n await writeFile(proxyScriptPath, generateSearchProxyServer(factoryApiUrl));\n console.log(`[*] Created proxy script: ${proxyScriptPath}`);\n\n // Write unified wrapper\n await writeFile(\n wrapperScriptPath,\n generateUnifiedWrapper(droidPath, proxyScriptPath, standalone),\n );\n await chmod(wrapperScriptPath, 0o755);\n console.log(`[*] Created wrapper: ${wrapperScriptPath}`);\n\n if (standalone) {\n console.log(`[*] Standalone mode enabled`);\n }\n\n return {\n wrapperScript: wrapperScriptPath,\n preloadScript: proxyScriptPath, // Keep interface compatible\n };\n}\n","import bin from \"tiny-bin\";\nimport { styleText } from \"node:util\";\nimport { existsSync, readFileSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { fileURLToPath } from \"node:url\";\nimport { execSync } from \"node:child_process\";\nimport { patchDroid, type Patch } from \"./patcher.ts\";\nimport {\n createAlias,\n removeAlias,\n listAliases,\n createAliasForWrapper,\n clearAllAliases,\n removeAliasesByFilter,\n type FilterFlag,\n} from \"./alias.ts\";\nimport { createWebSearchUnifiedFiles } from \"./websearch-patch.ts\";\nimport {\n saveAliasMetadata,\n createMetadata,\n loadAliasMetadata,\n listAllMetadata,\n formatPatches,\n} from \"./metadata.ts\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nfunction getVersion(): string {\n try {\n const pkgPath = join(__dirname, \"..\", \"package.json\");\n const pkg = JSON.parse(readFileSync(pkgPath, \"utf-8\"));\n return pkg.version || \"0.0.0\";\n } catch {\n return \"0.0.0\";\n }\n}\n\nconst version = getVersion();\n\nfunction getDroidVersion(droidPath: string): string | undefined {\n try {\n const result = execSync(`\"${droidPath}\" --version`, {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n timeout: 5000,\n }).trim();\n // Parse version from output like \"droid 1.2.3\" or just \"1.2.3\"\n const match = result.match(/(\\d+\\.\\d+\\.\\d+)/);\n return match ? match[1] : result || undefined;\n } catch {\n return undefined;\n }\n}\n\nfunction findDefaultDroidPath(): string {\n const home = homedir();\n\n // Try `which droid` first to find droid in PATH\n try {\n const result = execSync(\"which droid\", {\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (result && existsSync(result)) {\n return result;\n }\n } catch {\n // which command failed, continue with fallback paths\n }\n\n // Common installation paths\n const paths = [\n // Default sh install location\n join(home, \".droid\", \"bin\", \"droid\"),\n // Homebrew on Apple Silicon\n \"/opt/homebrew/bin/droid\",\n // Homebrew on Intel Mac / Linux\n \"/usr/local/bin/droid\",\n // Linux system-wide\n \"/usr/bin/droid\",\n // Current directory\n \"./droid\",\n ];\n\n for (const p of paths) {\n if (existsSync(p)) return p;\n }\n\n // Return default path even if not found (will error later with helpful message)\n return join(home, \".droid\", \"bin\", \"droid\");\n}\n\nbin(\"droid-patch\", \"CLI tool to patch droid binary with various modifications\")\n .package(\"droid-patch\", version)\n .option(\n \"--is-custom\",\n \"Patch isCustom:!0 to isCustom:!1 (enable context compression for custom models)\",\n )\n .option(\n \"--skip-login\",\n \"Inject a fake FACTORY_API_KEY to bypass login requirement (no real key needed)\",\n )\n .option(\n \"--api-base <url>\",\n \"Replace API URL (standalone: binary patch, max 22 chars; with --websearch: proxy forward target, no limit)\",\n )\n .option(\n \"--websearch\",\n \"Enable local WebSearch proxy (each instance runs own proxy, auto-cleanup on exit)\",\n )\n .option(\"--standalone\", \"Standalone mode: mock non-LLM Factory APIs (use with --websearch)\")\n .option(\n \"--reasoning-effort\",\n \"Enable reasoning effort for custom models (set to high, enable UI selector)\",\n )\n .option(\n \"--disable-telemetry\",\n \"Disable telemetry and Sentry error reporting (block data uploads)\",\n )\n .option(\"--dry-run\", \"Verify patches without actually modifying the binary\")\n .option(\"-p, --path <path>\", \"Path to the droid binary\")\n .option(\"-o, --output <dir>\", \"Output directory for patched binary\")\n .option(\"--no-backup\", \"Do not create backup of original binary\")\n .option(\"-v, --verbose\", \"Enable verbose output\")\n .argument(\"[alias]\", \"Alias name for the patched binary\")\n .action(async (options, args) => {\n const alias = args?.[0] as string | undefined;\n const isCustom = options[\"is-custom\"] as boolean;\n const skipLogin = options[\"skip-login\"] as boolean;\n const apiBase = options[\"api-base\"] as string | undefined;\n const websearch = options[\"websearch\"] as boolean;\n const standalone = options[\"standalone\"] as boolean;\n // When --websearch is used with --api-base, forward to custom URL\n // Otherwise forward to official Factory API\n const websearchTarget = websearch ? apiBase || \"https://api.factory.ai\" : undefined;\n const reasoningEffort = options[\"reasoning-effort\"] as boolean;\n const noTelemetry = options[\"disable-telemetry\"] as boolean;\n const dryRun = options[\"dry-run\"] as boolean;\n const path = (options.path as string) || findDefaultDroidPath();\n const outputDir = options.output as string | undefined;\n const backup = options.backup !== false;\n const verbose = options.verbose as boolean;\n\n // If -o is specified with alias, output to that directory with alias name\n const outputPath = outputDir && alias ? join(outputDir, alias) : undefined;\n\n // Handle --websearch only (no binary patching needed)\n // When --websearch is used alone (with optional --standalone), create proxy wrapper without modifying binary\n if (websearch && !isCustom && !skipLogin && !reasoningEffort && !noTelemetry) {\n if (!alias) {\n console.log(styleText(\"red\", \"Error: Alias name required for --websearch\"));\n console.log(styleText(\"gray\", \"Usage: npx droid-patch --websearch <alias>\"));\n process.exit(1);\n }\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid WebSearch Setup\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"white\", `Forward target: ${websearchTarget}`));\n if (standalone) {\n console.log(styleText(\"white\", `Standalone mode: enabled`));\n }\n console.log();\n\n // Create websearch proxy files (proxy script + wrapper)\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const { wrapperScript } = await createWebSearchUnifiedFiles(\n proxyDir,\n path,\n alias,\n websearchTarget,\n standalone,\n );\n\n // Create alias pointing to wrapper\n await createAliasForWrapper(wrapperScript, alias, verbose);\n\n // Save metadata for update command\n const droidVersion = getDroidVersion(path);\n const metadata = createMetadata(\n alias,\n path,\n {\n isCustom: false,\n skipLogin: false,\n apiBase: apiBase || null,\n websearch: true,\n reasoningEffort: false,\n noTelemetry: false,\n standalone: standalone,\n },\n {\n droidPatchVersion: version,\n droidVersion,\n },\n );\n await saveAliasMetadata(metadata);\n\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" WebSearch Ready!\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log();\n console.log(\"Run directly:\");\n console.log(styleText(\"yellow\", ` ${alias}`));\n console.log();\n console.log(styleText(\"cyan\", \"Auto-shutdown:\"));\n console.log(\n styleText(\"gray\", \" Proxy auto-shuts down after 5 min idle (no manual cleanup needed)\"),\n );\n console.log(styleText(\"gray\", \" To disable: export DROID_PROXY_IDLE_TIMEOUT=0\"));\n console.log();\n console.log(\"Search providers (in priority order):\");\n console.log(styleText(\"yellow\", \" 1. Smithery Exa (best quality):\"));\n console.log(styleText(\"gray\", \" export SMITHERY_API_KEY=your_api_key\"));\n console.log(styleText(\"gray\", \" export SMITHERY_PROFILE=your_profile\"));\n console.log(styleText(\"gray\", \" 2. Google PSE:\"));\n console.log(styleText(\"gray\", \" export GOOGLE_PSE_API_KEY=your_api_key\"));\n console.log(styleText(\"gray\", \" export GOOGLE_PSE_CX=your_search_engine_id\"));\n console.log(styleText(\"gray\", \" 3-6. Serper, Brave, SearXNG, DuckDuckGo (fallbacks)\"));\n console.log();\n console.log(\"Debug mode:\");\n console.log(styleText(\"gray\", \" export DROID_SEARCH_DEBUG=1\"));\n return;\n }\n\n if (!isCustom && !skipLogin && !apiBase && !websearch && !reasoningEffort && !noTelemetry) {\n console.log(styleText(\"yellow\", \"No patch flags specified. Available patches:\"));\n console.log(styleText(\"gray\", \" --is-custom Patch isCustom for custom models\"));\n console.log(\n styleText(\"gray\", \" --skip-login Bypass login by injecting a fake API key\"),\n );\n console.log(\n styleText(\n \"gray\",\n \" --api-base Replace API URL (standalone: max 22 chars; with --websearch: no limit)\",\n ),\n );\n console.log(styleText(\"gray\", \" --websearch Enable local WebSearch proxy\"));\n console.log(\n styleText(\"gray\", \" --reasoning-effort Set reasoning effort level for custom models\"),\n );\n console.log(\n styleText(\"gray\", \" --disable-telemetry Disable telemetry and Sentry error reporting\"),\n );\n console.log(\n styleText(\"gray\", \" --standalone Standalone mode: mock non-LLM Factory APIs\"),\n );\n console.log();\n console.log(\"Usage examples:\");\n console.log(styleText(\"cyan\", \" npx droid-patch --is-custom droid-custom\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --skip-login droid-nologin\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --is-custom --skip-login droid-patched\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --websearch droid-search\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --websearch --standalone droid-local\"));\n console.log(styleText(\"cyan\", \" npx droid-patch --disable-telemetry droid-private\"));\n console.log(\n styleText(\n \"cyan\",\n \" npx droid-patch --websearch --api-base=http://127.0.0.1:20002 my-droid\",\n ),\n );\n process.exit(1);\n }\n\n if (!alias && !dryRun) {\n console.log(styleText(\"red\", \"Error: alias name is required\"));\n console.log(\n styleText(\n \"gray\",\n \"Usage: droid-patch [--is-custom] [--skip-login] [-o <dir>] <alias-name>\",\n ),\n );\n process.exit(1);\n }\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid Binary Patcher\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n\n const patches: Patch[] = [];\n if (isCustom) {\n patches.push({\n name: \"isCustom\",\n description: \"Change isCustom:!0 to isCustom:!1\",\n pattern: Buffer.from(\"isCustom:!0\"),\n replacement: Buffer.from(\"isCustom:!1\"),\n });\n }\n\n // Add skip-login patch: replace process.env.FACTORY_API_KEY with a fixed fake key\n // \"process.env.FACTORY_API_KEY\" is 27 chars, we replace with \"fk-droid-patch-skip-00000\" (25 chars + quotes = 27)\n if (skipLogin) {\n patches.push({\n name: \"skipLogin\",\n description: 'Replace process.env.FACTORY_API_KEY with \"fk-droid-patch-skip-00000\"',\n pattern: Buffer.from(\"process.env.FACTORY_API_KEY\"),\n replacement: Buffer.from('\"fk-droid-patch-skip-00000\"'),\n });\n }\n\n // Add api-base patch: replace the Factory API base URL\n // Original: \"https://api.factory.ai\" (22 chars)\n // We need to pad the replacement URL to be exactly 22 chars\n // Note: When --websearch is used, --api-base sets the forward target instead of binary patching\n if (apiBase && !websearch) {\n const originalUrl = \"https://api.factory.ai\";\n const originalLength = originalUrl.length; // 22 chars\n\n // Validate and normalize the URL\n let normalizedUrl = apiBase.replace(/\\/+$/, \"\"); // Remove trailing slashes\n\n if (normalizedUrl.length > originalLength) {\n console.log(\n styleText(\"red\", `Error: API base URL must be ${originalLength} characters or less`),\n );\n console.log(\n styleText(\"gray\", ` Your URL: \"${normalizedUrl}\" (${normalizedUrl.length} chars)`),\n );\n console.log(styleText(\"gray\", ` Maximum: ${originalLength} characters`));\n console.log();\n console.log(styleText(\"yellow\", \"Tip: Use a shorter URL or set up a local redirect.\"));\n console.log(styleText(\"gray\", \" Examples:\"));\n console.log(styleText(\"gray\", \" http://127.0.0.1:3000 (19 chars)\"));\n console.log(styleText(\"gray\", \" http://localhost:80 (19 chars)\"));\n process.exit(1);\n }\n\n // Pad the URL with spaces at the end to match original length\n // Note: trailing spaces in URL are generally ignored\n const paddedUrl = normalizedUrl.padEnd(originalLength, \" \");\n\n patches.push({\n name: \"apiBase\",\n description: `Replace Factory API URL with \"${normalizedUrl}\"`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n });\n }\n\n // Add reasoning-effort patch: set custom models to use \"high\" reasoning\n // Also modify UI conditions to show reasoning selector for custom models\n if (reasoningEffort) {\n // [\"none\"] is 8 chars, [\"high\"] is 8 chars - perfect match!\n patches.push({\n name: \"reasoningEffortSupported\",\n description: 'Change supportedReasoningEfforts:[\"none\"] to [\"high\"]',\n pattern: Buffer.from('supportedReasoningEfforts:[\"none\"]'),\n replacement: Buffer.from('supportedReasoningEfforts:[\"high\"]'),\n });\n\n // \"none\" is 4 chars, \"high\" is 4 chars - perfect match!\n patches.push({\n name: \"reasoningEffortDefault\",\n description: 'Change defaultReasoningEffort:\"none\" to \"high\"',\n pattern: Buffer.from('defaultReasoningEffort:\"none\"'),\n replacement: Buffer.from('defaultReasoningEffort:\"high\"'),\n });\n\n // Change UI condition from length>1 to length>0\n // This allows custom models with single reasoning option to show the selector\n patches.push({\n name: \"reasoningEffortUIShow\",\n description: \"Change supportedReasoningEfforts.length>1 to length>0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length>1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length>0\"),\n });\n\n // Change UI condition from length<=1 to length<=0\n // This enables the reasoning setting in /settings menu for custom models\n patches.push({\n name: \"reasoningEffortUIEnable\",\n description: \"Change supportedReasoningEfforts.length<=1 to length<=0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length<=1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length<=0\"),\n });\n\n // Bypass reasoning effort validation to allow settings.json override\n // This allows \"xhigh\" in settings.json to work even though default is \"high\"\n // Original: if(R&&!B.supportedReasoningEfforts.includes(R)) throw error\n // Changed: if(0&&...) - never throws, any value is accepted\n patches.push({\n name: \"reasoningEffortValidationBypass\",\n description: \"Bypass reasoning effort validation (allows xhigh in settings.json)\",\n pattern: Buffer.from(\"if(R&&!B.supportedReasoningEfforts.includes(R))\"),\n replacement: Buffer.from(\"if(0&&!B.supportedReasoningEfforts.includes(R))\"),\n });\n }\n\n // Add no-telemetry patches: disable telemetry uploads and Sentry error reporting\n // Strategy:\n // 1. Break environment variable names so Sentry is never initialized (Q1() returns false)\n // 2. Invert flushToWeb condition so it returns early without making any fetch request\n if (noTelemetry) {\n // Patch 1: Break Sentry environment variable checks\n // Q1() function checks: VITE_VERCEL_ENV, ENABLE_SENTRY, NEXT_PUBLIC_ENABLE_SENTRY, FACTORY_ENABLE_SENTRY\n // By changing first letter to X, the env vars will never match, so Q1() returns false\n // and Sentry is never initialized\n patches.push({\n name: \"noTelemetrySentryEnv1\",\n description: \"Break ENABLE_SENTRY env var check (E->X)\",\n pattern: Buffer.from(\"ENABLE_SENTRY\"),\n replacement: Buffer.from(\"XNABLE_SENTRY\"),\n });\n\n patches.push({\n name: \"noTelemetrySentryEnv2\",\n description: \"Break VITE_VERCEL_ENV env var check (V->X)\",\n pattern: Buffer.from(\"VITE_VERCEL_ENV\"),\n replacement: Buffer.from(\"XITE_VERCEL_ENV\"),\n });\n\n // Patch 2: Make flushToWeb always return early to prevent ANY fetch request\n // Original: if(this.webEvents.length===0)return; // returns only when empty\n // Changed: if(!0||this.webEvents.length)return; // !0=true, ALWAYS returns\n // Result: Function always exits immediately, no telemetry is ever sent\n patches.push({\n name: \"noTelemetryFlushBlock\",\n description: \"Make flushToWeb always return (!0|| = always true)\",\n pattern: Buffer.from(\"this.webEvents.length===0\"),\n replacement: Buffer.from(\"!0||this.webEvents.length\"),\n });\n }\n\n try {\n const result = await patchDroid({\n inputPath: path,\n outputPath: outputPath,\n patches,\n dryRun,\n backup,\n verbose,\n });\n\n if (dryRun) {\n console.log();\n console.log(styleText(\"blue\", \"═\".repeat(60)));\n console.log(styleText([\"blue\", \"bold\"], \" DRY RUN COMPLETE\"));\n console.log(styleText(\"blue\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"gray\", \"To apply the patches, run without --dry-run:\"));\n console.log(styleText(\"cyan\", ` npx droid-patch --is-custom ${alias || \"<alias-name>\"}`));\n process.exit(0);\n }\n\n // If -o is specified, just output the file without creating alias\n if (outputDir && result.success && result.outputPath) {\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" PATCH SUCCESSFUL\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log();\n console.log(styleText(\"white\", `Patched binary saved to: ${result.outputPath}`));\n process.exit(0);\n }\n\n if (result.success && result.outputPath && alias) {\n console.log();\n\n // If --websearch is also used, create wrapper and point to it\n if (websearch) {\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const { wrapperScript } = await createWebSearchUnifiedFiles(\n proxyDir,\n result.outputPath,\n alias,\n websearchTarget,\n standalone,\n );\n await createAliasForWrapper(wrapperScript, alias, verbose);\n\n console.log();\n console.log(styleText(\"cyan\", \"WebSearch enabled\"));\n console.log(styleText(\"white\", ` Forward target: ${websearchTarget}`));\n if (standalone) {\n console.log(styleText(\"white\", ` Standalone mode: enabled`));\n }\n } else {\n await createAlias(result.outputPath, alias, verbose);\n }\n\n // Save metadata for update command\n const droidVersion = getDroidVersion(path);\n const metadata = createMetadata(\n alias,\n path,\n {\n isCustom: !!isCustom,\n skipLogin: !!skipLogin,\n apiBase: apiBase || null,\n websearch: !!websearch,\n reasoningEffort: !!reasoningEffort,\n noTelemetry: !!noTelemetry,\n standalone: !!standalone,\n },\n {\n droidPatchVersion: version,\n droidVersion,\n },\n );\n await saveAliasMetadata(metadata);\n }\n\n if (result.success) {\n console.log();\n console.log(styleText(\"green\", \"═\".repeat(60)));\n console.log(styleText([\"green\", \"bold\"], \" PATCH SUCCESSFUL\"));\n console.log(styleText(\"green\", \"═\".repeat(60)));\n }\n\n process.exit(result.success ? 0 : 1);\n } catch (error) {\n console.error(styleText(\"red\", `Error: ${(error as Error).message}`));\n if (verbose) console.error((error as Error).stack);\n process.exit(1);\n }\n })\n .command(\"list\", \"List all droid-patch aliases\")\n .action(async () => {\n await listAliases();\n })\n .command(\"remove\", \"Remove alias(es) by name or filter\")\n .argument(\"[alias-or-path]\", \"Alias name or file path to remove\")\n .option(\"--patch-version <version>\", \"Remove aliases created by this droid-patch version\")\n .option(\"--droid-version <version>\", \"Remove aliases for this droid version\")\n .option(\n \"--flag <flag>\",\n \"Remove aliases with this flag (is-custom, skip-login, websearch, api-base, reasoning-effort, disable-telemetry, standalone)\",\n )\n .action(async (options, args) => {\n const target = args?.[0] as string | undefined;\n const patchVersion = options[\"patch-version\"] as string | undefined;\n const droidVersion = options[\"droid-version\"] as string | undefined;\n const flag = options.flag as FilterFlag | undefined;\n\n // If filter options are provided, use filter mode\n if (patchVersion || droidVersion || flag) {\n await removeAliasesByFilter({\n patchVersion,\n droidVersion,\n flags: flag ? [flag] : undefined,\n });\n return;\n }\n\n // If no target and no filter, show error\n if (!target) {\n console.error(\n styleText(\n \"red\",\n \"Error: Provide an alias name or use filter options (--patch-version, --droid-version, --flag)\",\n ),\n );\n process.exit(1);\n }\n\n // Check if it's a file path (contains / or .)\n if (target.includes(\"/\") || existsSync(target)) {\n // It's a file path, delete directly\n const { unlink } = await import(\"node:fs/promises\");\n try {\n await unlink(target);\n console.log(styleText(\"green\", `[*] Removed: ${target}`));\n } catch (error) {\n console.error(styleText(\"red\", `Error: ${(error as Error).message}`));\n process.exit(1);\n }\n } else {\n // It's an alias name\n await removeAlias(target);\n }\n })\n .command(\"version\", \"Print droid-patch version\")\n .action(() => {\n console.log(`droid-patch v${version}`);\n })\n .command(\"clear\", \"Remove all droid-patch aliases and related files\")\n .action(async () => {\n await clearAllAliases();\n })\n .command(\"update\", \"Update aliases with latest droid binary\")\n .argument(\"[alias]\", \"Specific alias to update (optional, updates all if not specified)\")\n .option(\"--dry-run\", \"Preview without making changes\")\n .option(\"-p, --path <path>\", \"Path to new droid binary\")\n .option(\"-v, --verbose\", \"Enable verbose output\")\n .action(async (options, args) => {\n const aliasName = args?.[0] as string | undefined;\n const dryRun = options[\"dry-run\"] as boolean;\n const newBinaryPath = (options.path as string) || findDefaultDroidPath();\n const verbose = options.verbose as boolean;\n\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log(styleText([\"cyan\", \"bold\"], \" Droid-Patch Update\"));\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n console.log();\n\n // Verify the new binary exists\n if (!existsSync(newBinaryPath)) {\n console.log(styleText(\"red\", `Error: Droid binary not found at ${newBinaryPath}`));\n console.log(styleText(\"gray\", \"Use -p to specify a different path\"));\n process.exit(1);\n }\n\n // Get aliases to update\n let metaList: Awaited<ReturnType<typeof loadAliasMetadata>>[];\n if (aliasName) {\n const meta = await loadAliasMetadata(aliasName);\n if (!meta) {\n console.log(styleText(\"red\", `Error: No metadata found for alias \"${aliasName}\"`));\n console.log(\n styleText(\"gray\", \"This alias may have been created before update tracking was added.\"),\n );\n console.log(styleText(\"gray\", \"Remove and recreate the alias to enable update support.\"));\n process.exit(1);\n }\n metaList = [meta];\n } else {\n metaList = await listAllMetadata();\n if (metaList.length === 0) {\n console.log(styleText(\"yellow\", \"No aliases with metadata found.\"));\n console.log(styleText(\"gray\", \"Create aliases with droid-patch to enable update support.\"));\n process.exit(0);\n }\n }\n\n console.log(styleText(\"white\", `Using droid binary: ${newBinaryPath}`));\n console.log(styleText(\"white\", `Found ${metaList.length} alias(es) to update`));\n if (dryRun) {\n console.log(styleText(\"blue\", \"(DRY RUN - no changes will be made)\"));\n }\n console.log();\n\n let successCount = 0;\n let failCount = 0;\n\n for (const meta of metaList) {\n if (!meta) continue;\n\n console.log(styleText(\"cyan\", `─`.repeat(40)));\n console.log(styleText(\"white\", `Updating: ${styleText([\"cyan\", \"bold\"], meta.name)}`));\n console.log(styleText(\"gray\", ` Patches: ${formatPatches(meta.patches)}`));\n\n if (dryRun) {\n console.log(styleText(\"blue\", ` [DRY RUN] Would re-apply patches`));\n successCount++;\n continue;\n }\n\n try {\n // Build patch list based on metadata\n const patches: Patch[] = [];\n\n if (meta.patches.isCustom) {\n patches.push({\n name: \"isCustom\",\n description: \"Change isCustom:!0 to isCustom:!1\",\n pattern: Buffer.from(\"isCustom:!0\"),\n replacement: Buffer.from(\"isCustom:!1\"),\n });\n }\n\n if (meta.patches.skipLogin) {\n patches.push({\n name: \"skipLogin\",\n description: \"Replace process.env.FACTORY_API_KEY with fake key\",\n pattern: Buffer.from(\"process.env.FACTORY_API_KEY\"),\n replacement: Buffer.from('\"fk-droid-patch-skip-00000\"'),\n });\n }\n\n if (meta.patches.apiBase) {\n const originalUrl = \"https://api.factory.ai\";\n const paddedUrl = meta.patches.apiBase.padEnd(originalUrl.length, \" \");\n patches.push({\n name: \"apiBase\",\n description: `Replace Factory API URL with \"${meta.patches.apiBase}\"`,\n pattern: Buffer.from(originalUrl),\n replacement: Buffer.from(paddedUrl),\n });\n }\n\n if (meta.patches.reasoningEffort) {\n patches.push({\n name: \"reasoningEffortSupported\",\n description: 'Change supportedReasoningEfforts:[\"none\"] to [\"high\"]',\n pattern: Buffer.from('supportedReasoningEfforts:[\"none\"]'),\n replacement: Buffer.from('supportedReasoningEfforts:[\"high\"]'),\n });\n patches.push({\n name: \"reasoningEffortDefault\",\n description: 'Change defaultReasoningEffort:\"none\" to \"high\"',\n pattern: Buffer.from('defaultReasoningEffort:\"none\"'),\n replacement: Buffer.from('defaultReasoningEffort:\"high\"'),\n });\n patches.push({\n name: \"reasoningEffortUIShow\",\n description: \"Change supportedReasoningEfforts.length>1 to length>0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length>1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length>0\"),\n });\n patches.push({\n name: \"reasoningEffortUIEnable\",\n description: \"Change supportedReasoningEfforts.length<=1 to length<=0\",\n pattern: Buffer.from(\"supportedReasoningEfforts.length<=1\"),\n replacement: Buffer.from(\"supportedReasoningEfforts.length<=0\"),\n });\n patches.push({\n name: \"reasoningEffortValidationBypass\",\n description: \"Bypass reasoning effort validation (allows xhigh in settings.json)\",\n pattern: Buffer.from(\"if(R&&!B.supportedReasoningEfforts.includes(R))\"),\n replacement: Buffer.from(\"if(0&&!B.supportedReasoningEfforts.includes(R))\"),\n });\n }\n\n if (meta.patches.noTelemetry) {\n patches.push({\n name: \"noTelemetrySentryEnv1\",\n description: \"Break ENABLE_SENTRY env var check (E->X)\",\n pattern: Buffer.from(\"ENABLE_SENTRY\"),\n replacement: Buffer.from(\"XNABLE_SENTRY\"),\n });\n patches.push({\n name: \"noTelemetrySentryEnv2\",\n description: \"Break VITE_VERCEL_ENV env var check (V->X)\",\n pattern: Buffer.from(\"VITE_VERCEL_ENV\"),\n replacement: Buffer.from(\"XITE_VERCEL_ENV\"),\n });\n patches.push({\n name: \"noTelemetryFlushBlock\",\n description: \"Make flushToWeb always return (!0|| = always true)\",\n pattern: Buffer.from(\"this.webEvents.length===0\"),\n replacement: Buffer.from(\"!0||this.webEvents.length\"),\n });\n }\n\n // Determine output path based on whether this is a websearch alias\n const binsDir = join(homedir(), \".droid-patch\", \"bins\");\n const outputPath = join(binsDir, `${meta.name}-patched`);\n\n // Apply patches (only if there are binary patches to apply)\n if (patches.length > 0) {\n const result = await patchDroid({\n inputPath: newBinaryPath,\n outputPath,\n patches,\n dryRun: false,\n backup: false,\n verbose,\n });\n\n if (!result.success) {\n console.log(styleText(\"red\", ` ✗ Failed to apply patches`));\n failCount++;\n continue;\n }\n\n // Re-sign on macOS\n if (process.platform === \"darwin\") {\n try {\n const { execSync } = await import(\"node:child_process\");\n execSync(`codesign --force --deep --sign - \"${outputPath}\"`, {\n stdio: \"pipe\",\n });\n if (verbose) {\n console.log(styleText(\"gray\", ` Re-signed binary`));\n }\n } catch {\n console.log(styleText(\"yellow\", ` [!] Could not re-sign binary`));\n }\n }\n }\n\n // If websearch is enabled, regenerate wrapper files\n // Support both new 'websearch' field and old 'proxy' field for backward compatibility\n const hasWebsearch = meta.patches.websearch || !!meta.patches.proxy;\n if (hasWebsearch) {\n // Determine forward target: apiBase > proxy (legacy) > default\n const forwardTarget =\n meta.patches.apiBase || meta.patches.proxy || \"https://api.factory.ai\";\n const proxyDir = join(homedir(), \".droid-patch\", \"proxy\");\n const targetBinaryPath = patches.length > 0 ? outputPath : newBinaryPath;\n await createWebSearchUnifiedFiles(\n proxyDir,\n targetBinaryPath,\n meta.name,\n forwardTarget,\n meta.patches.standalone || false,\n );\n if (verbose) {\n console.log(styleText(\"gray\", ` Regenerated websearch wrapper`));\n if (meta.patches.standalone) {\n console.log(styleText(\"gray\", ` Standalone mode: enabled`));\n }\n }\n // Migrate old proxy field to new websearch field\n if (meta.patches.proxy && !meta.patches.websearch) {\n meta.patches.websearch = true;\n meta.patches.apiBase = meta.patches.proxy;\n delete meta.patches.proxy;\n }\n }\n\n // Update metadata\n meta.updatedAt = new Date().toISOString();\n meta.originalBinaryPath = newBinaryPath;\n await saveAliasMetadata(meta);\n\n console.log(styleText(\"green\", ` ✓ Updated successfully`));\n successCount++;\n } catch (error) {\n console.log(styleText(\"red\", ` ✗ Error: ${(error as Error).message}`));\n if (verbose) {\n console.error((error as Error).stack);\n }\n failCount++;\n }\n }\n\n console.log();\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n if (dryRun) {\n console.log(styleText([\"blue\", \"bold\"], \" DRY RUN COMPLETE\"));\n console.log(styleText(\"gray\", ` Would update ${successCount} alias(es)`));\n } else if (failCount === 0) {\n console.log(styleText([\"green\", \"bold\"], \" UPDATE COMPLETE\"));\n console.log(styleText(\"gray\", ` Updated ${successCount} alias(es)`));\n } else {\n console.log(styleText([\"yellow\", \"bold\"], \" UPDATE FINISHED WITH ERRORS\"));\n console.log(styleText(\"gray\", ` Success: ${successCount}, Failed: ${failCount}`));\n }\n console.log(styleText(\"cyan\", \"═\".repeat(60)));\n })\n .run()\n .catch((err: Error) => {\n console.error(err);\n process.exit(1);\n });\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "droid-patch",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "CLI tool to patch droid binary with various modifications",
|
|
5
5
|
"homepage": "https://github.com/kingsword09/droid-patch#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"oxlint": "^1.32.0",
|
|
48
48
|
"oxlint-tsgolint": "^0.9.0",
|
|
49
49
|
"tsdown": "^0.17.4",
|
|
50
|
-
"typescript": "^5.
|
|
50
|
+
"typescript": "^5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=22.0.0"
|