hive-rank 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // ../shared/dist/types.js
13
+ var init_types = __esm({
14
+ "../shared/dist/types.js"() {
15
+ "use strict";
16
+ }
17
+ });
18
+
19
+ // ../shared/dist/anonymize.js
20
+ import crypto from "crypto";
21
+ function hashContributorId(uuid) {
22
+ return crypto.createHash("sha256").update(uuid).digest("hex");
23
+ }
24
+ function bucketTimestamp(isoTimestamp) {
25
+ const date = new Date(isoTimestamp);
26
+ if (isNaN(date.getTime())) {
27
+ const match = isoTimestamp.match(/^(\d{4}-\d{2}-\d{2})/);
28
+ if (match)
29
+ return match[1];
30
+ throw new Error(`Invalid timestamp: ${isoTimestamp}`);
31
+ }
32
+ return date.toISOString().slice(0, 10);
33
+ }
34
+ function anonymizeSearchData(contributorId, query, timestamp, results) {
35
+ return {
36
+ contributorHash: hashContributorId(contributorId),
37
+ type: "search",
38
+ query,
39
+ observedDate: bucketTimestamp(timestamp),
40
+ results: results.map((r) => ({
41
+ url: r.url,
42
+ position: r.position,
43
+ title: r.title ?? null,
44
+ snippet: r.snippet ?? null
45
+ }))
46
+ };
47
+ }
48
+ function anonymizeFetchData(contributorId, url, timestamp, title, description, h1, links) {
49
+ return {
50
+ contributorHash: hashContributorId(contributorId),
51
+ type: "fetch",
52
+ url,
53
+ observedDate: bucketTimestamp(timestamp),
54
+ title: title ?? null,
55
+ description: description ?? null,
56
+ h1: h1 ?? null,
57
+ outboundLinks: links ? links.slice(0, MAX_OUTBOUND_LINKS) : null
58
+ };
59
+ }
60
+ var MAX_OUTBOUND_LINKS;
61
+ var init_anonymize = __esm({
62
+ "../shared/dist/anonymize.js"() {
63
+ "use strict";
64
+ MAX_OUTBOUND_LINKS = 100;
65
+ }
66
+ });
67
+
68
+ // ../shared/dist/normalize.js
69
+ var init_normalize = __esm({
70
+ "../shared/dist/normalize.js"() {
71
+ "use strict";
72
+ }
73
+ });
74
+
75
+ // ../shared/dist/validation.js
76
+ var init_validation = __esm({
77
+ "../shared/dist/validation.js"() {
78
+ "use strict";
79
+ }
80
+ });
81
+
82
+ // ../shared/dist/index.js
83
+ var init_dist = __esm({
84
+ "../shared/dist/index.js"() {
85
+ "use strict";
86
+ init_types();
87
+ init_anonymize();
88
+ init_normalize();
89
+ init_validation();
90
+ }
91
+ });
92
+
93
+ // src/hive/client.ts
94
+ var client_exports = {};
95
+ __export(client_exports, {
96
+ contributeToHive: () => contributeToHive
97
+ });
98
+ async function contributeToHive(hiveConfig, toolName, toolInput, parsedResponse) {
99
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
100
+ let contribution;
101
+ if (toolName === "WebSearch") {
102
+ const searchResponse = parsedResponse;
103
+ const query = toolInput.query;
104
+ contribution = anonymizeSearchData(
105
+ hiveConfig.contributorId,
106
+ query,
107
+ timestamp,
108
+ searchResponse.results.map((r, index) => ({
109
+ url: r.url,
110
+ position: index + 1,
111
+ title: r.title ?? null,
112
+ snippet: r.snippet ?? null
113
+ }))
114
+ );
115
+ } else {
116
+ const fetchResponse = parsedResponse;
117
+ const url = toolInput.url;
118
+ contribution = anonymizeFetchData(
119
+ hiveConfig.contributorId,
120
+ url,
121
+ timestamp,
122
+ fetchResponse.title ?? null,
123
+ fetchResponse.description ?? null,
124
+ fetchResponse.h1Tags ?? null,
125
+ fetchResponse.links ?? null
126
+ );
127
+ }
128
+ const controller = new AbortController();
129
+ const timeout = setTimeout(() => controller.abort(), 8e3);
130
+ try {
131
+ const response = await fetch(hiveConfig.endpoint, {
132
+ method: "POST",
133
+ headers: { "Content-Type": "application/json" },
134
+ body: JSON.stringify(contribution),
135
+ signal: controller.signal
136
+ });
137
+ if (!response.ok) {
138
+ let body = "";
139
+ try {
140
+ body = await response.text();
141
+ } catch {
142
+ }
143
+ throw new Error(
144
+ `Hive contribution failed: HTTP ${response.status}${body ? ` \u2014 ${body.slice(0, 200)}` : ""}`
145
+ );
146
+ }
147
+ } finally {
148
+ clearTimeout(timeout);
149
+ }
150
+ }
151
+ var init_client = __esm({
152
+ "src/hive/client.ts"() {
153
+ "use strict";
154
+ init_dist();
155
+ }
156
+ });
157
+
158
+ // hooks/capture-seo-data.ts
159
+ import fs from "fs";
160
+ import path from "path";
161
+ import os from "os";
162
+ import { fileURLToPath } from "url";
163
+ import { dirname } from "path";
164
+ var __filename = fileURLToPath(import.meta.url);
165
+ var __dirname = dirname(__filename);
166
+ var DEBUG_LOG = "/tmp/hive-rank-hook-debug.log";
167
+ function debugLog(message) {
168
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
169
+ fs.appendFileSync(DEBUG_LOG, `[${timestamp}] ${message}
170
+ `);
171
+ }
172
+ var LOCK_DIR = path.join(os.tmpdir(), "hive-rank-hook-locks");
173
+ function acquireLock(toolUseId) {
174
+ try {
175
+ fs.mkdirSync(LOCK_DIR, { recursive: true });
176
+ } catch {
177
+ return true;
178
+ }
179
+ const lockFile = path.join(LOCK_DIR, `${toolUseId}.lock`);
180
+ try {
181
+ const stat = fs.statSync(lockFile);
182
+ if (Date.now() - stat.mtimeMs < 6e4) return false;
183
+ } catch {
184
+ }
185
+ try {
186
+ fs.writeFileSync(lockFile, String(process.pid), { flag: "wx" });
187
+ return true;
188
+ } catch {
189
+ try {
190
+ const stat = fs.statSync(lockFile);
191
+ if (Date.now() - stat.mtimeMs < 6e4) return false;
192
+ fs.writeFileSync(lockFile, String(process.pid));
193
+ return true;
194
+ } catch {
195
+ return true;
196
+ }
197
+ }
198
+ }
199
+ function releaseLock(toolUseId) {
200
+ try {
201
+ fs.unlinkSync(path.join(LOCK_DIR, `${toolUseId}.lock`));
202
+ } catch {
203
+ }
204
+ }
205
+ function cleanStaleLocks() {
206
+ try {
207
+ const files = fs.readdirSync(LOCK_DIR);
208
+ const now = Date.now();
209
+ for (const file of files) {
210
+ if (!file.endsWith(".lock")) continue;
211
+ try {
212
+ const filePath = path.join(LOCK_DIR, file);
213
+ const stat = fs.statSync(filePath);
214
+ if (now - stat.mtimeMs > 6e4) fs.unlinkSync(filePath);
215
+ } catch {
216
+ }
217
+ }
218
+ } catch {
219
+ }
220
+ }
221
+ var DEFAULT_CONFIG = {
222
+ enabled: true,
223
+ endpoint: "https://mcp.hive-rank.com/api/contribute",
224
+ contributorId: null
225
+ };
226
+ function loadConfig() {
227
+ const configPath = path.join(__dirname, "config.json");
228
+ if (fs.existsSync(configPath)) {
229
+ try {
230
+ const userConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
231
+ return { ...DEFAULT_CONFIG, ...userConfig };
232
+ } catch {
233
+ return DEFAULT_CONFIG;
234
+ }
235
+ }
236
+ return DEFAULT_CONFIG;
237
+ }
238
+ function parseWebSearchResponse(response) {
239
+ if (response == null || typeof response === "boolean" || typeof response === "number") {
240
+ return { results: [] };
241
+ }
242
+ const results = [];
243
+ if (typeof response === "object") {
244
+ const structuredResponse = response;
245
+ if (structuredResponse.results && Array.isArray(structuredResponse.results)) {
246
+ for (const resultGroup of structuredResponse.results) {
247
+ if (!resultGroup?.content || !Array.isArray(resultGroup.content)) continue;
248
+ for (const item of resultGroup.content) {
249
+ if (typeof item?.url !== "string") continue;
250
+ if (item.url.startsWith("http://") || item.url.startsWith("https://")) {
251
+ results.push({
252
+ url: item.url,
253
+ title: typeof item.title === "string" ? item.title : void 0
254
+ });
255
+ }
256
+ }
257
+ }
258
+ }
259
+ return { results };
260
+ }
261
+ const responseStr = String(response);
262
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
263
+ let match;
264
+ while ((match = linkPattern.exec(responseStr)) !== null) {
265
+ const [, title, url] = match;
266
+ if (!url.startsWith("http://") && !url.startsWith("https://")) continue;
267
+ const afterMatch = responseStr.slice(match.index + match[0].length);
268
+ const snippetMatch = afterMatch.match(/^\s*\n?([^\n\[]+)/);
269
+ results.push({
270
+ url,
271
+ title: title || void 0,
272
+ snippet: snippetMatch?.[1]?.trim() || void 0
273
+ });
274
+ }
275
+ return { results };
276
+ }
277
+ function parseWebFetchResponse(response) {
278
+ const result = {};
279
+ if (response == null || typeof response === "boolean" || typeof response === "number") {
280
+ return result;
281
+ }
282
+ const responseStr = typeof response === "string" ? response : JSON.stringify(response);
283
+ let titleMatch = responseStr.match(/\*\*(?:Title|Page Title|Page title)\*?\*?:\s*["\']?([^"'\n]+)["\']?/i);
284
+ if (titleMatch) {
285
+ result.title = titleMatch[1].trim();
286
+ } else {
287
+ const lines = responseStr.split("\n");
288
+ for (let i = 0; i < Math.min(lines.length, 10); i++) {
289
+ const line = lines[i].trim();
290
+ if (line.match(/^#+\s+(?:page\s+)?(?:analysis|information|content|metadata)/i)) continue;
291
+ if (line.match(/^#+\s+/)) {
292
+ result.title = line.replace(/^#+\s+/, "").trim();
293
+ break;
294
+ }
295
+ }
296
+ }
297
+ const h1Tags = [];
298
+ const markdownHeadingPattern = /^#{1,2}\s+(.+)$/gm;
299
+ let headingMatch;
300
+ while ((headingMatch = markdownHeadingPattern.exec(responseStr)) !== null) {
301
+ const h1Text = headingMatch[1].trim();
302
+ if (h1Text && !h1Tags.includes(h1Text)) h1Tags.push(h1Text);
303
+ }
304
+ const h1LabelPattern = /\*\*H1\s*(?:heading|tag)?\*?\*?:\s*["\']?([^"'\n]+)["\']?/gi;
305
+ let h1Match;
306
+ while ((h1Match = h1LabelPattern.exec(responseStr)) !== null) {
307
+ const h1Text = h1Match[1].trim();
308
+ if (h1Text && !h1Tags.includes(h1Text)) h1Tags.push(h1Text);
309
+ }
310
+ if (h1Tags.length > 0) result.h1Tags = h1Tags;
311
+ const links = [];
312
+ const urlPattern = /(https?:\/\/[^\s\n\)\]\}]+)/g;
313
+ let urlMatch;
314
+ while ((urlMatch = urlPattern.exec(responseStr)) !== null) {
315
+ let url = urlMatch[1].replace(/[.,;:\)\]\}]*$/, "");
316
+ if (!links.includes(url)) links.push(url);
317
+ }
318
+ if (links.length > 0) result.links = [...new Set(links)];
319
+ let descMatch = responseStr.match(/\*\*(?:meta\s+)?description\*?\*?:\s*["\']?([^"'\n]+)["\']?/i);
320
+ if (!descMatch) {
321
+ descMatch = responseStr.match(/^Description:\s*(.+)$/im);
322
+ }
323
+ if (descMatch && descMatch[1].length > 10) {
324
+ result.description = descMatch[1].trim();
325
+ }
326
+ return result;
327
+ }
328
+ async function main() {
329
+ debugLog("Hook started");
330
+ const stdinPromise = new Promise((resolve) => {
331
+ const chunks = [];
332
+ const timeout = setTimeout(() => resolve(""), 5e3);
333
+ if (process.stdin.isTTY) {
334
+ clearTimeout(timeout);
335
+ resolve("");
336
+ return;
337
+ }
338
+ process.stdin.setEncoding("utf8");
339
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
340
+ process.stdin.on("end", () => {
341
+ clearTimeout(timeout);
342
+ resolve(chunks.join(""));
343
+ });
344
+ process.stdin.on("error", () => {
345
+ clearTimeout(timeout);
346
+ resolve("");
347
+ });
348
+ });
349
+ const inputData = await stdinPromise;
350
+ if (!inputData) {
351
+ debugLog("No input data");
352
+ process.exit(0);
353
+ }
354
+ const config = loadConfig();
355
+ if (!config.enabled || !config.contributorId) {
356
+ debugLog("Hook disabled or no contributorId");
357
+ process.exit(0);
358
+ }
359
+ let hookInput;
360
+ try {
361
+ hookInput = JSON.parse(inputData);
362
+ } catch {
363
+ debugLog("Invalid JSON input");
364
+ process.exit(0);
365
+ }
366
+ const { tool_name, tool_input, tool_response, tool_use_id } = hookInput;
367
+ if (tool_name !== "WebSearch" && tool_name !== "WebFetch") {
368
+ process.exit(0);
369
+ }
370
+ if (tool_use_id && !acquireLock(tool_use_id)) {
371
+ debugLog("Duplicate invocation, skipping");
372
+ process.exit(0);
373
+ }
374
+ cleanStaleLocks();
375
+ if (!tool_response || !tool_input || typeof tool_input !== "object") {
376
+ if (tool_use_id) releaseLock(tool_use_id);
377
+ process.exit(0);
378
+ }
379
+ try {
380
+ let parsedResponse;
381
+ if (tool_name === "WebSearch") {
382
+ parsedResponse = parseWebSearchResponse(tool_response);
383
+ const results = parsedResponse.results;
384
+ debugLog(`Parsed ${results.length} search results`);
385
+ if (results.length === 0) {
386
+ if (tool_use_id) releaseLock(tool_use_id);
387
+ process.exit(0);
388
+ }
389
+ } else {
390
+ parsedResponse = parseWebFetchResponse(tool_response);
391
+ debugLog(`Parsed WebFetch response`);
392
+ }
393
+ const { contributeToHive: contributeToHive2 } = await Promise.resolve().then(() => (init_client(), client_exports));
394
+ await contributeToHive2(
395
+ { enabled: true, endpoint: config.endpoint, contributorId: config.contributorId },
396
+ tool_name,
397
+ tool_input,
398
+ parsedResponse
399
+ );
400
+ debugLog("Contribution sent");
401
+ } catch (error) {
402
+ const errorMsg = error instanceof Error ? error.message : String(error);
403
+ debugLog(`Error: ${errorMsg}`);
404
+ }
405
+ if (tool_use_id) releaseLock(tool_use_id);
406
+ process.exit(0);
407
+ }
408
+ main().catch((error) => {
409
+ console.error("[hive-rank] Fatal error:", error);
410
+ process.exit(1);
411
+ });
412
+ export {
413
+ acquireLock,
414
+ cleanStaleLocks,
415
+ loadConfig,
416
+ parseWebFetchResponse,
417
+ parseWebSearchResponse,
418
+ releaseLock
419
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "enabled": true,
3
+ "endpoint": "https://mcp.hive-rank.com/api/contribute",
4
+ "contributorId": null
5
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "hive-rank",
3
+ "version": "3.0.0",
4
+ "description": "Crowdsourced SEO intelligence for AI agents. Network-powered hooks contribute data to the hive. 6 hive_* tools via remote MCP server.",
5
+ "author": "hive-rank",
6
+ "license": "MIT",
7
+ "homepage": "https://hive-rank.com",
8
+ "keywords": [
9
+ "seo",
10
+ "mcp",
11
+ "claude",
12
+ "ai-agent",
13
+ "rankings",
14
+ "search",
15
+ "serp",
16
+ "model-context-protocol"
17
+ ],
18
+ "type": "module",
19
+ "bin": {
20
+ "hive-rank": "bin/install.js"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "bin",
25
+ "commands"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "scripts": {
31
+ "build": "node scripts/build-hooks.js",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "clean": "rm -rf dist",
35
+ "prepublishOnly": "npm run build"
36
+ },
37
+ "dependencies": {},
38
+ "devDependencies": {
39
+ "@gys-hive/shared": "workspace:*",
40
+ "@types/node": "^20.0.0",
41
+ "esbuild": "^0.24.2",
42
+ "typescript": "^5.4.0",
43
+ "vitest": "^2.0.0"
44
+ }
45
+ }