grok-search-rs 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +9 -0
  2. package/package.json +54 -0
  3. package/run.js +267 -0
package/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # grok-search-rs
2
+
3
+ Run the GrokSearch-rs MCP server with npx:
4
+
5
+ ```bash
6
+ npx grok-search-rs
7
+ ```
8
+
9
+ See https://github.com/Episkey-G/GrokSearch-rs for configuration and MCP client examples.
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "grok-search-rs",
3
+ "version": "0.1.2",
4
+ "description": "Rust MCP server for Grok Responses web search, Tavily fetch/map, and Firecrawl fallback",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Episkey-G/GrokSearch-rs.git"
11
+ },
12
+ "author": "Episkey-G",
13
+ "license": "MIT",
14
+ "bugs": {
15
+ "url": "https://github.com/Episkey-G/GrokSearch-rs/issues"
16
+ },
17
+ "homepage": "https://github.com/Episkey-G/GrokSearch-rs#readme",
18
+ "keywords": [
19
+ "mcp",
20
+ "grok",
21
+ "xai",
22
+ "tavily",
23
+ "firecrawl",
24
+ "search",
25
+ "rust",
26
+ "cli"
27
+ ],
28
+ "bin": {
29
+ "grok-search-rs": "run.js"
30
+ },
31
+ "files": [
32
+ "run.js",
33
+ "README.md"
34
+ ],
35
+ "engines": {
36
+ "node": ">=14.14.0"
37
+ },
38
+ "os": [
39
+ "darwin",
40
+ "linux",
41
+ "win32"
42
+ ],
43
+ "cpu": [
44
+ "x64",
45
+ "arm64"
46
+ ],
47
+ "optionalDependencies": {
48
+ "@epsiekygrr_zedxx/grok-search-rs-darwin-universal": "0.1.2",
49
+ "@epsiekygrr_zedxx/grok-search-rs-linux-x64": "0.1.2",
50
+ "@epsiekygrr_zedxx/grok-search-rs-linux-arm64": "0.1.2",
51
+ "@epsiekygrr_zedxx/grok-search-rs-win32-x64": "0.1.2",
52
+ "@epsiekygrr_zedxx/grok-search-rs-win32-arm64": "0.1.2"
53
+ }
54
+ }
package/run.js ADDED
@@ -0,0 +1,267 @@
1
+ #!/usr/bin/env node
2
+ const { spawn } = require("child_process");
3
+ const path = require("path");
4
+ const fs = require("fs");
5
+ const https = require("https");
6
+ const os = require("os");
7
+ const crypto = require("crypto");
8
+
9
+ const PACKAGE_NAME = "grok-search-rs";
10
+ const REPO_OWNER = "Episkey-G";
11
+ const REPO_NAME = "GrokSearch-rs";
12
+ const REQUEST_TIMEOUT = 60000;
13
+ const MAX_REDIRECTS = 10;
14
+ const MAX_RETRIES = 3;
15
+
16
+ function version() {
17
+ return require("./package.json").version;
18
+ }
19
+
20
+ function platformInfo() {
21
+ const platform = process.platform;
22
+ const arch = process.arch;
23
+ if (platform === "darwin" && (arch === "x64" || arch === "arm64")) {
24
+ return {
25
+ packageName: "@epsiekygrr_zedxx/grok-search-rs-darwin-universal",
26
+ assetName: "grok-search-rs_Darwin_universal.tar.gz",
27
+ binaryName: "grok-search-rs",
28
+ };
29
+ }
30
+ if (platform === "linux" && arch === "x64") {
31
+ return {
32
+ packageName: "@epsiekygrr_zedxx/grok-search-rs-linux-x64",
33
+ assetName: "grok-search-rs_Linux_x86_64.tar.gz",
34
+ binaryName: "grok-search-rs",
35
+ };
36
+ }
37
+ if (platform === "linux" && arch === "arm64") {
38
+ return {
39
+ packageName: "@epsiekygrr_zedxx/grok-search-rs-linux-arm64",
40
+ assetName: "grok-search-rs_Linux_aarch64.tar.gz",
41
+ binaryName: "grok-search-rs",
42
+ };
43
+ }
44
+ if (platform === "win32" && arch === "x64") {
45
+ return {
46
+ packageName: "@epsiekygrr_zedxx/grok-search-rs-win32-x64",
47
+ assetName: "grok-search-rs_Windows_x86_64.zip",
48
+ binaryName: "grok-search-rs.exe",
49
+ };
50
+ }
51
+ if (platform === "win32" && arch === "arm64") {
52
+ return {
53
+ packageName: "@epsiekygrr_zedxx/grok-search-rs-win32-arm64",
54
+ assetName: "grok-search-rs_Windows_aarch64.zip",
55
+ binaryName: "grok-search-rs.exe",
56
+ };
57
+ }
58
+ throw new Error(`Unsupported platform: ${platform}/${arch}`);
59
+ }
60
+
61
+ function cacheDir() {
62
+ const home = os.homedir();
63
+ let base;
64
+ if (process.platform === "win32") {
65
+ base = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
66
+ } else if (process.platform === "darwin") {
67
+ base = path.join(home, "Library", "Caches");
68
+ } else {
69
+ base = process.env.XDG_CACHE_HOME || path.join(home, ".cache");
70
+ }
71
+ return path.join(base, PACKAGE_NAME, version());
72
+ }
73
+
74
+ function findOptionalBinary(info) {
75
+ try {
76
+ const pkg = require.resolve(`${info.packageName}/package.json`);
77
+ const bin = path.join(path.dirname(pkg), "bin", info.binaryName);
78
+ if (fs.existsSync(bin)) return bin;
79
+ } catch (_) {}
80
+ return null;
81
+ }
82
+
83
+ function requestBuffer(url, options = {}, redirects = 0) {
84
+ return new Promise((resolve, reject) => {
85
+ if (redirects > MAX_REDIRECTS) return reject(new Error("Too many redirects"));
86
+ const req = https.get(url, options, (res) => {
87
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
88
+ res.resume();
89
+ if (!res.headers.location.startsWith("https://")) {
90
+ return reject(new Error(`Insecure redirect: ${res.headers.location}`));
91
+ }
92
+ return requestBuffer(res.headers.location, options, redirects + 1).then(resolve, reject);
93
+ }
94
+ if (res.statusCode !== 200) {
95
+ res.resume();
96
+ return reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
97
+ }
98
+ const chunks = [];
99
+ res.on("data", (chunk) => chunks.push(chunk));
100
+ res.on("end", () => resolve(Buffer.concat(chunks)));
101
+ });
102
+ req.on("error", reject);
103
+ req.setTimeout(REQUEST_TIMEOUT, () => {
104
+ req.destroy();
105
+ reject(new Error("Request timeout"));
106
+ });
107
+ });
108
+ }
109
+
110
+ async function withRetry(fn) {
111
+ let last;
112
+ for (let i = 0; i < MAX_RETRIES; i++) {
113
+ try {
114
+ return await fn();
115
+ } catch (err) {
116
+ last = err;
117
+ if (String(err.message).includes("404")) throw err;
118
+ if (i < MAX_RETRIES - 1) {
119
+ await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)));
120
+ }
121
+ }
122
+ }
123
+ throw last;
124
+ }
125
+
126
+ async function releaseForVersion() {
127
+ const tag = `v${version()}`;
128
+ const url = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/tags/${tag}`;
129
+ const headers = {
130
+ "User-Agent": PACKAGE_NAME,
131
+ Accept: "application/vnd.github.v3+json",
132
+ ...(process.env.GITHUB_TOKEN ? { Authorization: `token ${process.env.GITHUB_TOKEN}` } : {}),
133
+ };
134
+ const data = await withRetry(() => requestBuffer(url, { headers }));
135
+ return JSON.parse(data.toString());
136
+ }
137
+
138
+ function downloadFile(url, dest, options = {}, redirects = 0) {
139
+ return new Promise((resolve, reject) => {
140
+ if (redirects > MAX_REDIRECTS) return reject(new Error("Too many redirects"));
141
+ const file = fs.createWriteStream(dest);
142
+ const req = https.get(url, options, (res) => {
143
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
144
+ res.resume();
145
+ file.close(() => {
146
+ try { fs.unlinkSync(dest); } catch (_) {}
147
+ if (!res.headers.location.startsWith("https://")) {
148
+ return reject(new Error(`Insecure redirect: ${res.headers.location}`));
149
+ }
150
+ downloadFile(res.headers.location, dest, options, redirects + 1).then(resolve, reject);
151
+ });
152
+ return;
153
+ }
154
+ if (res.statusCode !== 200) {
155
+ res.resume();
156
+ file.close(() => {
157
+ try { fs.unlinkSync(dest); } catch (_) {}
158
+ reject(new Error(`HTTP ${res.statusCode}: ${res.statusMessage}`));
159
+ });
160
+ return;
161
+ }
162
+ res.pipe(file);
163
+ file.on("finish", () => file.close(resolve));
164
+ });
165
+ req.on("error", (err) => file.close(() => reject(err)));
166
+ req.setTimeout(REQUEST_TIMEOUT, () => {
167
+ req.destroy();
168
+ file.close(() => reject(new Error("Download timeout")));
169
+ });
170
+ });
171
+ }
172
+
173
+ function extractArchive(archive, dir, info) {
174
+ return new Promise((resolve, reject) => {
175
+ const child = info.assetName.endsWith(".zip")
176
+ ? spawn("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", `Expand-Archive -LiteralPath '${archive.replace(/'/g, "''")}' -DestinationPath '${dir.replace(/'/g, "''")}' -Force`], { stdio: ["ignore", process.stderr, process.stderr] })
177
+ : spawn("tar", ["-xzf", archive, "-C", dir], { stdio: ["ignore", process.stderr, process.stderr] });
178
+ child.on("error", reject);
179
+ child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`extract exited with code ${code}`)));
180
+ });
181
+ }
182
+
183
+ function acquireLock(lock) {
184
+ try {
185
+ fs.writeFileSync(lock, String(process.pid), { flag: "wx" });
186
+ return true;
187
+ } catch (err) {
188
+ if (err.code !== "EEXIST") throw err;
189
+ return false;
190
+ }
191
+ }
192
+
193
+ async function downloadBinary(info) {
194
+ const dir = cacheDir();
195
+ const bin = path.join(dir, info.binaryName);
196
+ const lock = path.join(dir, ".lock");
197
+ fs.mkdirSync(dir, { recursive: true });
198
+ if (fs.existsSync(bin)) return bin;
199
+
200
+ if (!acquireLock(lock)) {
201
+ console.error("Another grok-search-rs process is installing, waiting...");
202
+ for (let i = 0; i < 60; i++) {
203
+ if (fs.existsSync(bin)) return bin;
204
+ await new Promise((resolve) => setTimeout(resolve, 1000));
205
+ }
206
+ throw new Error("Timeout waiting for install lock");
207
+ }
208
+
209
+ const tempId = crypto.randomBytes(8).toString("hex");
210
+ const archive = path.join(dir, `${tempId}-${info.assetName}`);
211
+ const extractDir = path.join(dir, `${tempId}-extract`);
212
+ try {
213
+ if (fs.existsSync(bin)) return bin;
214
+ console.error(`Downloading ${PACKAGE_NAME} v${version()}...`);
215
+ const release = await releaseForVersion();
216
+ const asset = release.assets.find((item) => item.name === info.assetName);
217
+ if (!asset) {
218
+ throw new Error(`No matching release asset: ${info.assetName}`);
219
+ }
220
+ const headers = {
221
+ "User-Agent": PACKAGE_NAME,
222
+ Accept: "application/octet-stream",
223
+ ...(process.env.GITHUB_TOKEN ? { Authorization: `token ${process.env.GITHUB_TOKEN}` } : {}),
224
+ };
225
+ await withRetry(() => downloadFile(asset.browser_download_url, archive, { headers }));
226
+ fs.mkdirSync(extractDir, { recursive: true });
227
+ await extractArchive(archive, extractDir, info);
228
+ const extracted = path.join(extractDir, info.binaryName);
229
+ if (!fs.existsSync(extracted)) throw new Error(`Binary not found in archive: ${info.binaryName}`);
230
+ fs.renameSync(extracted, bin);
231
+ if (process.platform !== "win32") fs.chmodSync(bin, 0o755);
232
+ console.error(`Installed ${PACKAGE_NAME} to ${bin}`);
233
+ return bin;
234
+ } finally {
235
+ try { fs.unlinkSync(lock); } catch (_) {}
236
+ try { fs.unlinkSync(archive); } catch (_) {}
237
+ try { fs.rmSync(extractDir, { recursive: true, force: true }); } catch (_) {}
238
+ }
239
+ }
240
+
241
+ async function main() {
242
+ const info = platformInfo();
243
+ const optional = findOptionalBinary(info);
244
+ const binary = optional || await downloadBinary(info);
245
+ const child = spawn(binary, process.argv.slice(2), {
246
+ stdio: "inherit",
247
+ env: process.env,
248
+ });
249
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
250
+ process.on(signal, () => {
251
+ if (!child.killed) child.kill(signal);
252
+ });
253
+ }
254
+ child.on("error", (err) => {
255
+ console.error(`Failed to start ${PACKAGE_NAME}: ${err.message}`);
256
+ process.exit(1);
257
+ });
258
+ child.on("exit", (code, signal) => {
259
+ if (signal) process.exit(128 + (os.constants.signals[signal] || 0));
260
+ process.exit(code || 0);
261
+ });
262
+ }
263
+
264
+ main().catch((err) => {
265
+ console.error(err.message || err);
266
+ process.exit(1);
267
+ });