hive-rank 3.3.0 → 3.4.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.
Files changed (3) hide show
  1. package/README.md +35 -1
  2. package/bin/install.js +308 -19
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ Crowdsourced SEO intelligence for AI agents.
4
4
 
5
5
  ## What is this?
6
6
 
7
- Hive Rank aggregates anonymized search data from Claude Code agents into a shared ranking dataset. Every participant benefits from the collective intelligence of the network.
7
+ Hive Rank aggregates anonymized search data from AI coding agents into a shared ranking dataset. Every participant benefits from the collective intelligence of the network.
8
8
 
9
9
  **AI agents are searching for your product right now. Do you know where you rank?**
10
10
 
@@ -14,12 +14,46 @@ Hive Rank aggregates anonymized search data from Claude Code agents into a share
14
14
  npx hive-rank
15
15
  ```
16
16
 
17
+ The installer auto-detects Claude Code, OpenCode, and Codex CLI and configures all detected platforms.
18
+
17
19
  Or add the MCP server directly:
18
20
 
21
+ **Claude Code:**
19
22
  ```bash
20
23
  claude mcp add --transport http hive-rank https://mcp.hive-rank.com/mcp
21
24
  ```
22
25
 
26
+ **OpenCode** — add to `~/.config/opencode/opencode.json`:
27
+ ```json
28
+ {
29
+ "mcp": {
30
+ "hive-rank": {
31
+ "type": "remote",
32
+ "url": "https://mcp.hive-rank.com/mcp",
33
+ "enabled": true
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ **Codex CLI** — add to `~/.codex/config.toml`:
40
+ ```toml
41
+ [features]
42
+ experimental_use_rmcp_client = true
43
+
44
+ [mcp_servers.hive_rank]
45
+ url = "https://mcp.hive-rank.com/mcp"
46
+ ```
47
+
48
+ ## Platform Capabilities
49
+
50
+ | Feature | Claude Code | OpenCode | Codex CLI |
51
+ |---------|------------|----------|-----------|
52
+ | MCP query tools | Yes | Yes | Yes |
53
+ | Data capture (hooks) | Yes | No | No |
54
+ | Slash commands (21) | Yes | No | No |
55
+ | Auto-install | Yes | Yes | Yes |
56
+
23
57
  ## What you get
24
58
 
25
59
  ### 8 MCP Tools
package/bin/install.js CHANGED
@@ -29,6 +29,12 @@ const CLAUDE_DIR = path.join(HOME, '.claude');
29
29
  const COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands', 'hive');
30
30
  const PKG_ROOT = path.resolve(new URL('..', import.meta.url).pathname);
31
31
 
32
+ const OPENCODE_CONFIG_DIR = path.join(HOME, '.config', 'opencode');
33
+ const OPENCODE_CONFIG_FILE = path.join(OPENCODE_CONFIG_DIR, 'opencode.json');
34
+
35
+ const CODEX_DIR = path.join(HOME, '.codex');
36
+ const CODEX_CONFIG_FILE = path.join(CODEX_DIR, 'config.toml');
37
+
32
38
  const MCP_NAME = 'hive-rank';
33
39
  const MCP_URL = 'https://mcp.hive-rank.com/mcp';
34
40
  const SETTINGS_FILE = path.join(CLAUDE_DIR, 'settings.json');
@@ -58,6 +64,118 @@ function writeJsonAtomic(p, obj) {
58
64
  fs.renameSync(tmp, p);
59
65
  }
60
66
 
67
+ // ── TOML helpers (minimal, no deps) ──────────────────────────────────────────
68
+
69
+ function parseToml(text) {
70
+ const result = {};
71
+ let currentSection = null;
72
+
73
+ for (const raw of text.split('\n')) {
74
+ const line = raw.trim();
75
+ if (!line || line.startsWith('#')) continue;
76
+
77
+ // Section header: [foo] or [foo.bar]
78
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/);
79
+ if (sectionMatch) {
80
+ const parts = sectionMatch[1].split('.');
81
+ let target = result;
82
+ for (const part of parts) {
83
+ if (!target[part] || typeof target[part] !== 'object') target[part] = {};
84
+ target = target[part];
85
+ }
86
+ currentSection = parts;
87
+ continue;
88
+ }
89
+
90
+ // Key = value
91
+ const kvMatch = line.match(/^(\w+)\s*=\s*(.+)$/);
92
+ if (kvMatch) {
93
+ const key = kvMatch[1];
94
+ let val = kvMatch[2].trim();
95
+ // Parse value type
96
+ if (val === 'true') val = true;
97
+ else if (val === 'false') val = false;
98
+ else if (/^-?\d+(\.\d+)?$/.test(val)) val = Number(val);
99
+ else if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))
100
+ val = val.slice(1, -1);
101
+
102
+ if (currentSection) {
103
+ let target = result;
104
+ for (const part of currentSection) target = target[part];
105
+ target[key] = val;
106
+ } else {
107
+ result[key] = val;
108
+ }
109
+ }
110
+ }
111
+ return result;
112
+ }
113
+
114
+ function serializeToml(obj, prefix = '') {
115
+ let lines = [];
116
+ const simple = {};
117
+ const sections = {};
118
+
119
+ for (const [k, v] of Object.entries(obj)) {
120
+ if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
121
+ sections[k] = v;
122
+ } else {
123
+ simple[k] = v;
124
+ }
125
+ }
126
+
127
+ // Write simple key-value pairs
128
+ for (const [k, v] of Object.entries(simple)) {
129
+ if (typeof v === 'string') lines.push(`${k} = "${v}"`);
130
+ else lines.push(`${k} = ${v}`);
131
+ }
132
+
133
+ // Write sections
134
+ for (const [k, v] of Object.entries(sections)) {
135
+ const sectionKey = prefix ? `${prefix}.${k}` : k;
136
+ // Check if this section has only simple values (leaf section)
137
+ const hasNestedObjects = Object.values(v).some(val => val !== null && typeof val === 'object' && !Array.isArray(val));
138
+
139
+ if (!hasNestedObjects) {
140
+ if (lines.length > 0) lines.push('');
141
+ lines.push(`[${sectionKey}]`);
142
+ for (const [sk, sv] of Object.entries(v)) {
143
+ if (typeof sv === 'string') lines.push(`${sk} = "${sv}"`);
144
+ else lines.push(`${sk} = ${sv}`);
145
+ }
146
+ } else {
147
+ // Recurse for nested sections, writing any simple values first
148
+ const nestedSimple = {};
149
+ const nestedSections = {};
150
+ for (const [sk, sv] of Object.entries(v)) {
151
+ if (sv !== null && typeof sv === 'object' && !Array.isArray(sv)) {
152
+ nestedSections[sk] = sv;
153
+ } else {
154
+ nestedSimple[sk] = sv;
155
+ }
156
+ }
157
+ if (Object.keys(nestedSimple).length > 0) {
158
+ if (lines.length > 0) lines.push('');
159
+ lines.push(`[${sectionKey}]`);
160
+ for (const [sk, sv] of Object.entries(nestedSimple)) {
161
+ if (typeof sv === 'string') lines.push(`${sk} = "${sv}"`);
162
+ else lines.push(`${sk} = ${sv}`);
163
+ }
164
+ }
165
+ const nested = serializeToml(nestedSections, sectionKey);
166
+ if (nested) lines.push('', nested);
167
+ }
168
+ }
169
+
170
+ return lines.join('\n');
171
+ }
172
+
173
+ function writeTomlAtomic(p, obj) {
174
+ const tmp = p + '.tmp.' + crypto.randomBytes(4).toString('hex');
175
+ fs.writeFileSync(tmp, serializeToml(obj) + '\n', 'utf-8');
176
+ fs.renameSync(tmp, p);
177
+ }
178
+
61
179
  function copyRecursive(src, dest) {
62
180
  if (!fileExists(src)) return;
63
181
  const stat = fs.statSync(src);
@@ -92,6 +210,118 @@ function hasClaudeCli() {
92
210
  }
93
211
  }
94
212
 
213
+ function hasOpenCode() {
214
+ // Check for opencode CLI in PATH
215
+ try {
216
+ const result = spawnSync('opencode', ['--version'], { stdio: 'pipe', timeout: 5000 });
217
+ if (result.status === 0) return true;
218
+ } catch { /* not in PATH */ }
219
+ // Check for config directory existence
220
+ return fileExists(OPENCODE_CONFIG_DIR);
221
+ }
222
+
223
+ function hasCodexCli() {
224
+ try {
225
+ const result = spawnSync('codex', ['--version'], { stdio: 'pipe', timeout: 5000 });
226
+ if (result.status === 0) return true;
227
+ } catch { /* not in PATH */ }
228
+ return fileExists(CODEX_DIR);
229
+ }
230
+
231
+ function registerCodexMcp() {
232
+ fs.mkdirSync(CODEX_DIR, { recursive: true });
233
+
234
+ let config = {};
235
+ if (fileExists(CODEX_CONFIG_FILE)) {
236
+ try {
237
+ config = parseToml(fs.readFileSync(CODEX_CONFIG_FILE, 'utf-8'));
238
+ } catch {
239
+ warn('~/.codex/config.toml exists but could not be parsed. Skipping Codex MCP registration.');
240
+ return 'skipped';
241
+ }
242
+ }
243
+
244
+ // Clean up legacy entries
245
+ if (config.mcp_servers) {
246
+ for (const oldName of ['gys_local', 'hive_seo', 'grow_your_shit', 'hive_rank']) {
247
+ delete config.mcp_servers[oldName];
248
+ }
249
+ }
250
+
251
+ // Ensure feature flag is set
252
+ if (!config.features) config.features = {};
253
+ config.features.experimental_use_rmcp_client = true;
254
+
255
+ // Register MCP server
256
+ if (!config.mcp_servers) config.mcp_servers = {};
257
+ config.mcp_servers.hive_rank = {
258
+ url: MCP_URL,
259
+ };
260
+
261
+ writeTomlAtomic(CODEX_CONFIG_FILE, config);
262
+ return 'direct';
263
+ }
264
+
265
+ function removeCodexMcp() {
266
+ if (!fileExists(CODEX_CONFIG_FILE)) return;
267
+ try {
268
+ const config = parseToml(fs.readFileSync(CODEX_CONFIG_FILE, 'utf-8'));
269
+ let changed = false;
270
+ if (config.mcp_servers) {
271
+ for (const oldName of ['hive_rank', 'gys_local', 'hive_seo', 'grow_your_shit']) {
272
+ if (config.mcp_servers[oldName]) { delete config.mcp_servers[oldName]; changed = true; }
273
+ }
274
+ if (Object.keys(config.mcp_servers).length === 0) delete config.mcp_servers;
275
+ }
276
+ if (changed) writeTomlAtomic(CODEX_CONFIG_FILE, config);
277
+ } catch { /* ignore */ }
278
+ }
279
+
280
+ function registerOpenCodeMcp() {
281
+ fs.mkdirSync(OPENCODE_CONFIG_DIR, { recursive: true });
282
+
283
+ let config = {};
284
+ if (fileExists(OPENCODE_CONFIG_FILE)) {
285
+ try {
286
+ config = readJson(OPENCODE_CONFIG_FILE);
287
+ } catch {
288
+ warn('~/.config/opencode/opencode.json exists but is not valid JSON. Skipping OpenCode MCP registration.');
289
+ return 'skipped';
290
+ }
291
+ }
292
+
293
+ // Clean up old entries
294
+ if (config.mcp) {
295
+ for (const oldName of ['gys-local', 'hive-seo', 'grow-your-shit', MCP_NAME]) {
296
+ delete config.mcp[oldName];
297
+ }
298
+ }
299
+
300
+ if (!config.mcp) config.mcp = {};
301
+ config.mcp[MCP_NAME] = {
302
+ type: 'remote',
303
+ url: MCP_URL,
304
+ enabled: true
305
+ };
306
+
307
+ writeJsonAtomic(OPENCODE_CONFIG_FILE, config);
308
+ return 'direct';
309
+ }
310
+
311
+ function removeOpenCodeMcp() {
312
+ if (!fileExists(OPENCODE_CONFIG_FILE)) return;
313
+ try {
314
+ const config = readJson(OPENCODE_CONFIG_FILE);
315
+ let changed = false;
316
+ if (config.mcp) {
317
+ for (const oldName of [MCP_NAME, 'gys-local', 'hive-seo', 'grow-your-shit']) {
318
+ if (config.mcp[oldName]) { delete config.mcp[oldName]; changed = true; }
319
+ }
320
+ }
321
+ if (changed) writeJsonAtomic(OPENCODE_CONFIG_FILE, config);
322
+ } catch { /* ignore */ }
323
+ }
324
+
95
325
  function registerMcpServer() {
96
326
  if (hasClaudeCli()) {
97
327
  // Remove old entries from all scopes
@@ -269,14 +499,19 @@ if (FLAG_HELP) {
269
499
  What it does:
270
500
  1. Copies dist + commands + docs to ~/.hive-rank/
271
501
  2. Registers hooks in ~/.claude/settings.json
272
- 3. Registers remote MCP server via \`claude mcp add\` (HTTP transport)
502
+ 3. Registers remote MCP server for detected platforms:
503
+ - Claude Code: via \`claude mcp add\` or ~/.claude.json
504
+ - OpenCode: via ~/.config/opencode/opencode.json
505
+ - Codex CLI: via ~/.codex/config.toml
273
506
  4. Installs slash commands to ~/.claude/commands/hive/
274
507
 
275
508
  Config locations:
276
- Hooks: ~/.claude/settings.json
277
- MCP server: ~/.claude.json (via \`claude mcp add --scope user\`)
278
- Commands: ~/.claude/commands/hive/*.md
279
- Privacy: ~/.hive-rank/PRIVACY.md
509
+ Hooks: ~/.claude/settings.json
510
+ MCP (Claude): ~/.claude.json (via \`claude mcp add --scope user\`)
511
+ MCP (OpenCode): ~/.config/opencode/opencode.json
512
+ MCP (Codex): ~/.codex/config.toml
513
+ Commands: ~/.claude/commands/hive/*.md
514
+ Privacy: ~/.hive-rank/PRIVACY.md
280
515
  `);
281
516
  process.exit(0);
282
517
  }
@@ -287,7 +522,9 @@ if (FLAG_UNINSTALL) {
287
522
  if (FLAG_DRY_RUN) {
288
523
  console.log('\n Uninstalling Hive Rank — DRY RUN (no changes will be made)\n');
289
524
  dryLog('Would remove hooks from ' + SETTINGS_FILE);
290
- dryLog('Would remove MCP server');
525
+ dryLog('Would remove Claude Code MCP server');
526
+ if (fileExists(OPENCODE_CONFIG_FILE)) dryLog('Would remove OpenCode MCP server from ' + OPENCODE_CONFIG_FILE);
527
+ if (fileExists(CODEX_CONFIG_FILE)) dryLog('Would remove Codex CLI MCP server from ' + CODEX_CONFIG_FILE);
291
528
  if (fileExists(COMMANDS_DIR)) dryLog('Would remove slash commands: ' + COMMANDS_DIR);
292
529
  if (fileExists(INSTALL_DIR)) dryLog('Would remove ' + INSTALL_DIR + '/');
293
530
  console.log('\n Dry run complete — no changes were made.\n');
@@ -300,7 +537,13 @@ if (FLAG_UNINSTALL) {
300
537
  success('Removed hooks from settings.json');
301
538
 
302
539
  removeMcpServer();
303
- success('Removed MCP server');
540
+ success('Removed Claude Code MCP server');
541
+
542
+ removeOpenCodeMcp();
543
+ success('Removed OpenCode MCP server');
544
+
545
+ removeCodexMcp();
546
+ success('Removed Codex CLI MCP server');
304
547
 
305
548
  // Remove old gys commands too
306
549
  const oldCommandsDir = path.join(CLAUDE_DIR, 'commands', 'gys');
@@ -328,7 +571,7 @@ if (FLAG_UNINSTALL) {
328
571
  }
329
572
  }
330
573
 
331
- console.log('\n Hive Rank has been uninstalled. Restart Claude Code to apply.\n');
574
+ console.log('\n Hive Rank has been uninstalled. Restart your agent to apply.\n');
332
575
  process.exit(0);
333
576
  }
334
577
 
@@ -435,8 +678,12 @@ async function install() {
435
678
  dryLog('Would copy PRIVACY.md and AGENT.md to ~/.hive-rank/');
436
679
  }
437
680
 
438
- // ── Step 3: Register MCP server ──
681
+ // ── Step 3: Register MCP servers ──
682
+
683
+ // Track which platforms were configured (for summary)
684
+ const platforms = { claudeCode: false, openCode: false, codexCli: false };
439
685
 
686
+ // Claude Code
440
687
  if (FLAG_DRY_RUN) {
441
688
  if (hasClaudeCli()) {
442
689
  dryLog(`Would run: claude mcp add --scope user --transport http ${MCP_NAME} ${MCP_URL}`);
@@ -444,14 +691,48 @@ async function install() {
444
691
  dryLog('Would register MCP server in ~/.claude.json');
445
692
  }
446
693
  } else {
447
- log('Registering remote MCP server...');
694
+ log('Registering MCP server for Claude Code...');
448
695
  const mcpMethod = registerMcpServer();
449
696
  if (mcpMethod === 'cli') {
450
- success('MCP server registered via `claude mcp add` (HTTP transport)');
697
+ success('Claude Code: MCP server registered via `claude mcp add`');
698
+ platforms.claudeCode = true;
451
699
  } else if (mcpMethod === 'direct') {
452
- success('MCP server registered in ~/.claude.json');
700
+ success('Claude Code: MCP server registered in ~/.claude.json');
701
+ platforms.claudeCode = true;
453
702
  } else {
454
- warn('MCP server registration skipped — run manually after install');
703
+ warn('Claude Code: MCP registration skipped');
704
+ }
705
+ }
706
+
707
+ // OpenCode
708
+ if (hasOpenCode()) {
709
+ if (FLAG_DRY_RUN) {
710
+ dryLog(`Would register MCP server in ${OPENCODE_CONFIG_FILE}`);
711
+ } else {
712
+ log('Registering MCP server for OpenCode...');
713
+ const ocMethod = registerOpenCodeMcp();
714
+ if (ocMethod === 'direct') {
715
+ success('OpenCode: MCP server registered in ~/.config/opencode/opencode.json');
716
+ platforms.openCode = true;
717
+ } else {
718
+ warn('OpenCode: MCP registration skipped');
719
+ }
720
+ }
721
+ }
722
+
723
+ // Codex CLI
724
+ if (hasCodexCli()) {
725
+ if (FLAG_DRY_RUN) {
726
+ dryLog(`Would register MCP server in ${CODEX_CONFIG_FILE}`);
727
+ } else {
728
+ log('Registering MCP server for Codex CLI...');
729
+ const cxMethod = registerCodexMcp();
730
+ if (cxMethod === 'direct') {
731
+ success('Codex CLI: MCP server registered in ~/.codex/config.toml');
732
+ platforms.codexCli = true;
733
+ } else {
734
+ warn('Codex CLI: MCP registration skipped');
735
+ }
455
736
  }
456
737
  }
457
738
 
@@ -510,12 +791,20 @@ async function install() {
510
791
  node ${path.join(PKG_ROOT, 'bin', 'install.js')}${FLAG_FORCE ? ' --force' : ''}
511
792
  `);
512
793
  } else {
794
+ const platformLines = [];
795
+ platformLines.push(` Claude Code: ${platforms.claudeCode ? '+ MCP server + capture hooks' : '- not detected'}`);
796
+ platformLines.push(` OpenCode: ${platforms.openCode ? '+ MCP server (query tools)' : '- not detected'}`);
797
+ platformLines.push(` Codex CLI: ${platforms.codexCli ? '+ MCP server (query tools)' : '- not detected'}`);
798
+
513
799
  console.log(`
514
800
  ────────────────────────────────────────────────
515
801
  Hive Rank installed successfully!
516
802
  ────────────────────────────────────────────────
517
803
 
518
- Restart Claude Code, then try:
804
+ Platforms:
805
+ ${platformLines.join('\n')}
806
+
807
+ Restart your agent, then try:
519
808
 
520
809
  /hive:help — See all commands
521
810
  /hive:kickstart — Bootstrap your SEO research
@@ -523,11 +812,11 @@ async function install() {
523
812
  /hive:trends — See what's trending
524
813
 
525
814
  Config:
526
- Hooks: ${SETTINGS_FILE}
527
- MCP server: ${MCP_URL} (via HTTP)
528
- Commands: ${COMMANDS_DIR}/
529
- Privacy: ~/.hive-rank/PRIVACY.md
530
- Controls: /hive:privacy, /hive:delete-data
815
+ Hooks: ${SETTINGS_FILE}
816
+ MCP (Claude): ${MCP_URL} (via HTTP)
817
+ ${platforms.openCode ? `MCP (OpenCode): ${OPENCODE_CONFIG_FILE}\n ` : ''}${platforms.codexCli ? `MCP (Codex): ${CODEX_CONFIG_FILE}\n ` : ''}Commands: ${COMMANDS_DIR}/
818
+ Privacy: ~/.hive-rank/PRIVACY.md
819
+ Controls: /hive:privacy, /hive:delete-data
531
820
 
532
821
  To uninstall: node ~/.hive-rank/bin/install.js --uninstall
533
822
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hive-rank",
3
- "version": "3.3.0",
3
+ "version": "3.4.0",
4
4
  "description": "Crowdsourced SEO intelligence for AI agents. Network-powered hooks contribute data to the hive. 8 hive_* tools via remote MCP server.",
5
5
  "author": "hive-rank",
6
6
  "license": "MIT",