@deeplake/hivemind 0.7.60 → 0.7.61

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.
@@ -6,18 +6,18 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
9
- "version": "0.7.60"
9
+ "version": "0.7.61"
10
10
  },
11
11
  "plugins": [
12
12
  {
13
13
  "name": "hivemind",
14
14
  "description": "Persistent shared memory powered by Deeplake — captures all session activity and provides cross-session, cross-agent memory search",
15
- "version": "0.7.60",
15
+ "version": "0.7.61",
16
16
  "source": {
17
17
  "source": "git-subdir",
18
18
  "url": "https://github.com/activeloopai/hivemind.git",
19
19
  "path": "claude-code",
20
- "sha": "16a718fb94219d96ad5686ca414504d694643bc9"
20
+ "sha": "edf4a6520595067cd8c0f6064034014e3fc2790e"
21
21
  },
22
22
  "homepage": "https://github.com/activeloopai/hivemind"
23
23
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hivemind",
3
3
  "description": "Cloud-backed persistent memory powered by Deeplake — read, write, and share memory across Claude Code sessions and agents",
4
- "version": "0.7.60",
4
+ "version": "0.7.61",
5
5
  "author": {
6
6
  "name": "Activeloop",
7
7
  "url": "https://deeplake.ai"
@@ -1795,7 +1795,7 @@ function extractLatestVersion(body) {
1795
1795
  return typeof v === "string" && v.length > 0 ? v : null;
1796
1796
  }
1797
1797
  function getInstalledVersion() {
1798
- return "0.7.60".length > 0 ? "0.7.60" : null;
1798
+ return "0.7.61".length > 0 ? "0.7.61" : null;
1799
1799
  }
1800
1800
  function isNewer(latest, current) {
1801
1801
  const parse = (v) => v.replace(/-.*$/, "").split(".").map(Number);
@@ -54,5 +54,5 @@
54
54
  }
55
55
  }
56
56
  },
57
- "version": "0.7.60"
57
+ "version": "0.7.61"
58
58
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hivemind",
3
- "version": "0.7.60",
3
+ "version": "0.7.61",
4
4
  "type": "module",
5
5
  "description": "Hivemind — cloud-backed persistent shared memory for AI agents, powered by DeepLake",
6
6
  "license": "Apache-2.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeplake/hivemind",
3
- "version": "0.7.60",
3
+ "version": "0.7.61",
4
4
  "description": "Cloud-backed persistent shared memory for AI agents powered by Deeplake",
5
5
  "type": "module",
6
6
  "repository": {
@@ -26,6 +26,7 @@
26
26
  "openclaw/openclaw.plugin.json",
27
27
  "openclaw/package.json",
28
28
  ".claude-plugin",
29
+ "scripts",
29
30
  "README.md",
30
31
  "LICENSE"
31
32
  ],
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Local replication of ClawHub's static-analysis scanner against the openclaw
4
+ * plugin bundle. Runs the same per-file regex rules ClawHub uses at publish
5
+ * time so we can see flags BEFORE shipping a release instead of after.
6
+ *
7
+ * Rules are replicated (not imported) from openclaw's skill-scanner — that
8
+ * code lives in our research-reference checkout under ~/al-projects/ext/ and
9
+ * is third-party we don't own. Re-sync these rules if upstream changes them.
10
+ *
11
+ * Reference: openclaw repo, src/security/skill-scanner.ts:147-206
12
+ *
13
+ * Usage:
14
+ * node scripts/audit-openclaw-bundle.mjs # scan openclaw/dist
15
+ * node scripts/audit-openclaw-bundle.mjs <path> # scan a specific dir
16
+ *
17
+ * Exits non-zero if any "critical" or "warn" finding is reported.
18
+ */
19
+
20
+ import { readFileSync, statSync } from "node:fs";
21
+ import { readdir } from "node:fs/promises";
22
+ import { join, extname } from "node:path";
23
+
24
+ // Parse args: positional SCAN_DIR + optional --criticals-only flag.
25
+ //
26
+ // --criticals-only exits non-zero only on `critical` findings, treating
27
+ // `warn` as advisory. Used by the CI release gate so we block on
28
+ // definitively-malicious patterns but tolerate fundamental warns like
29
+ // the worker's "readFileSync + fetch in same file" — irreducible without
30
+ // splitting the worker into multiple shipped files. Local dev still
31
+ // runs with the default strict mode (exits on any finding) so we
32
+ // surface every drift before it ships.
33
+ const rawArgs = process.argv.slice(2);
34
+ const STRICT_MODE = !rawArgs.includes("--criticals-only");
35
+ const SCAN_DIR = rawArgs.find(a => !a.startsWith("--")) ?? "openclaw/dist";
36
+ const SCANNABLE_EXT = new Set([".js", ".mjs", ".cjs"]);
37
+ const MAX_FILE_BYTES = 1024 * 1024; // 1MB; matches upstream default
38
+
39
+ // ---- LINE_RULES (per-line; both pattern AND requiresContext must match) ----
40
+ const LINE_RULES = [
41
+ {
42
+ ruleId: "dangerous-exec",
43
+ severity: "critical",
44
+ message: "Shell command execution detected (child_process)",
45
+ pattern: /\b(exec|execSync|spawn|spawnSync|execFile|execFileSync)\s*\(/,
46
+ requiresContext: /child_process/,
47
+ },
48
+ {
49
+ ruleId: "dynamic-code-execution",
50
+ severity: "critical",
51
+ message: "Dynamic code execution detected",
52
+ pattern: /\beval\s*\(|new\s+Function\s*\(/,
53
+ },
54
+ {
55
+ ruleId: "crypto-mining",
56
+ severity: "critical",
57
+ message: "Possible crypto-mining reference detected",
58
+ pattern: /stratum\+tcp|stratum\+ssl|coinhive|cryptonight|xmrig/i,
59
+ },
60
+ {
61
+ ruleId: "suspicious-network",
62
+ severity: "warn",
63
+ message: "WebSocket connection to non-standard port",
64
+ pattern: /new\s+WebSocket\s*\(\s*["']wss?:\/\/[^"']*:(\d+)/,
65
+ portCheck: true,
66
+ },
67
+ ];
68
+
69
+ const STANDARD_PORTS = new Set([80, 443, 8080, 8443, 3000]);
70
+ const NETWORK_SEND = /\bfetch\s*\(|\bpost\s*\(|\.\s*post\s*\(|http\.request\s*\(/i;
71
+
72
+ // ---- SOURCE_RULES (whole file; both pattern AND requiresContext must match) ----
73
+ const SOURCE_RULES = [
74
+ {
75
+ ruleId: "potential-exfiltration",
76
+ severity: "warn",
77
+ message: "File read combined with network send (possible exfiltration)",
78
+ pattern: /readFileSync|readFile/,
79
+ requiresContext: NETWORK_SEND,
80
+ },
81
+ {
82
+ ruleId: "obfuscated-code-hex",
83
+ severity: "warn",
84
+ message: "Hex-encoded string sequence detected (possible obfuscation)",
85
+ pattern: /(\\x[0-9a-fA-F]{2}){6,}/,
86
+ },
87
+ {
88
+ ruleId: "obfuscated-code-base64",
89
+ severity: "warn",
90
+ message: "Large base64 payload with decode call detected (possible obfuscation)",
91
+ pattern: /(?:atob|Buffer\.from)\s*\(\s*["'][A-Za-z0-9+/=]{200,}["']/,
92
+ },
93
+ {
94
+ ruleId: "env-harvesting",
95
+ severity: "critical",
96
+ message: "Environment variable access combined with network send (possible credential harvesting)",
97
+ pattern: /process\.env/,
98
+ requiresContext: NETWORK_SEND,
99
+ },
100
+ ];
101
+
102
+ function truncate(s, n = 120) { return s.length <= n ? s : s.slice(0, n) + "…"; }
103
+
104
+ function scanFile(path) {
105
+ const stat = statSync(path);
106
+ if (stat.size > MAX_FILE_BYTES) {
107
+ return [{ ruleId: "file-too-large", severity: "info", file: path, line: 0, message: `Skipped (${stat.size} bytes > ${MAX_FILE_BYTES} byte limit)`, evidence: "" }];
108
+ }
109
+ const source = readFileSync(path, "utf-8");
110
+ const lines = source.split("\n");
111
+ const findings = [];
112
+
113
+ for (const rule of LINE_RULES) {
114
+ if (rule.requiresContext && !rule.requiresContext.test(source)) continue;
115
+ for (let i = 0; i < lines.length; i++) {
116
+ const line = lines[i];
117
+ const m = rule.pattern.exec(line);
118
+ if (!m) continue;
119
+ if (rule.portCheck) {
120
+ const port = Number.parseInt(m[1], 10);
121
+ if (STANDARD_PORTS.has(port)) continue;
122
+ }
123
+ findings.push({ ruleId: rule.ruleId, severity: rule.severity, file: path, line: i + 1, message: rule.message, evidence: truncate(line.trim()) });
124
+ break; // one finding per line-rule per file
125
+ }
126
+ }
127
+
128
+ for (const rule of SOURCE_RULES) {
129
+ if (!rule.pattern.test(source)) continue;
130
+ if (rule.requiresContext && !rule.requiresContext.test(source)) continue;
131
+ let matchLine = 0, matchEvidence = "";
132
+ for (let i = 0; i < lines.length; i++) {
133
+ if (rule.pattern.test(lines[i])) { matchLine = i + 1; matchEvidence = lines[i].trim(); break; }
134
+ }
135
+ if (matchLine === 0) { matchLine = 1; matchEvidence = source.slice(0, 120); }
136
+ findings.push({ ruleId: rule.ruleId, severity: rule.severity, file: path, line: matchLine, message: rule.message, evidence: truncate(matchEvidence) });
137
+ }
138
+
139
+ return findings;
140
+ }
141
+
142
+ async function* walk(dir) {
143
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
144
+ const p = join(dir, entry.name);
145
+ if (entry.isDirectory()) yield* walk(p);
146
+ else if (entry.isFile() && SCANNABLE_EXT.has(extname(entry.name).toLowerCase())) yield p;
147
+ }
148
+ }
149
+
150
+ const SEVERITY_RANK = { info: 0, warn: 1, critical: 2 };
151
+ const SEVERITY_ICON = { info: "·", warn: "!", critical: "✗" };
152
+
153
+ const allFindings = [];
154
+ let scannedFiles = 0;
155
+ for await (const file of walk(SCAN_DIR)) {
156
+ scannedFiles++;
157
+ for (const f of scanFile(file)) allFindings.push(f);
158
+ }
159
+
160
+ const counts = { info: 0, warn: 0, critical: 0 };
161
+ for (const f of allFindings) counts[f.severity] = (counts[f.severity] ?? 0) + 1;
162
+
163
+ console.log(`\nScanned ${scannedFiles} file(s) under ${SCAN_DIR}/\n`);
164
+
165
+ if (allFindings.length === 0) {
166
+ console.log("✓ No findings. Bundle is clean against ClawHub's static-analysis rules.\n");
167
+ process.exit(0);
168
+ }
169
+
170
+ allFindings.sort((a, b) => (SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]) || a.file.localeCompare(b.file) || a.line - b.line);
171
+ for (const f of allFindings) {
172
+ console.log(`${SEVERITY_ICON[f.severity]} [${f.severity.toUpperCase()}] ${f.ruleId}`);
173
+ console.log(` ${f.file}:${f.line}`);
174
+ console.log(` ${f.message}`);
175
+ if (f.evidence) console.log(` > ${f.evidence}`);
176
+ console.log();
177
+ }
178
+
179
+ const summary = `${counts.critical} critical, ${counts.warn} warn, ${counts.info} info`;
180
+ console.log(`Summary: ${summary}\n`);
181
+ if (!STRICT_MODE && counts.critical === 0 && counts.warn > 0) {
182
+ console.log(`(--criticals-only: warns are advisory and do NOT block. Re-run without the flag for strict local checks.)\n`);
183
+ }
184
+ const shouldFail = STRICT_MODE
185
+ ? (counts.critical > 0 || counts.warn > 0)
186
+ : counts.critical > 0;
187
+ process.exit(shouldFail ? 1 : 0);
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+ // Ensures the native tree-sitter bindings are loadable on this platform / Node ABI.
3
+ //
4
+ // Why this exists: tree-sitter@0.21.x ships no linux-arm64 prebuild, and
5
+ // tree-sitter-typescript@0.23.x ships a mislabeled (x86-64) one. On linux-arm64
6
+ // both must be compiled from source, and under Node >=22 that compile requires C++20
7
+ // (tree-sitter@0.21's binding.gyp does not request it). tree-sitter is declared as an
8
+ // optionalDependency so this expected arm64 build failure does not abort `npm install`;
9
+ // this script then heals it afterwards.
10
+ //
11
+ // On platforms where the shipped prebuilds work (x64 / darwin / CI) this is a fast
12
+ // no-op and never touches anything. It is intentionally non-fatal: if no toolchain is
13
+ // available it warns and exits 0 rather than breaking the install.
14
+ import { execSync } from 'node:child_process';
15
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
16
+ import { createRequire } from 'node:module';
17
+
18
+ const ROOT = process.cwd();
19
+ const require = createRequire(`${ROOT}/`);
20
+ const PKGS = ['tree-sitter', 'tree-sitter-typescript'];
21
+
22
+ function bindingsLoad() {
23
+ try {
24
+ const Parser = require('tree-sitter');
25
+ const TS = require('tree-sitter-typescript').typescript;
26
+ const parser = new Parser();
27
+ parser.setLanguage(TS);
28
+ parser.parse('const x = 1;');
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ if (process.env.ENSURE_TS_RUNNING) process.exit(0); // recursion guard for the nested npm calls below
36
+ if (bindingsLoad()) process.exit(0); // healthy prebuild / prior build → nothing to do
37
+
38
+ console.error('[ensure-tree-sitter] native bindings not loadable on this platform — building from source...');
39
+
40
+ const pkg = JSON.parse(readFileSync(`${ROOT}/package.json`, 'utf8'));
41
+ const declared = { ...pkg.dependencies, ...pkg.optionalDependencies };
42
+
43
+ const env = { ...process.env, ENSURE_TS_RUNNING: '1' };
44
+ if (process.platform !== 'win32') {
45
+ // Node >=22 V8 headers require C++20; tree-sitter@0.21's binding.gyp doesn't request it.
46
+ env.CXXFLAGS = `${process.env.CXXFLAGS ?? ''} -std=c++20`.trim();
47
+ }
48
+ const run = (cmd) => execSync(cmd, { stdio: 'inherit', env, cwd: ROOT });
49
+
50
+ try {
51
+ // 1. Re-fetch any package npm dropped — an optional dependency whose build failed is
52
+ // removed from node_modules. --ignore-scripts: fetch only, so the compile below is
53
+ // the single source of truth and the project build isn't triggered prematurely.
54
+ const missing = PKGS.filter((n) => !existsSync(`${ROOT}/node_modules/${n}/package.json`));
55
+ if (missing.length) {
56
+ const specs = missing.map((n) => `${n}@${declared[n] ?? 'latest'}`);
57
+ run(`npm install ${specs.join(' ')} --no-save --ignore-scripts`);
58
+ }
59
+
60
+ // 2. Force a from-source compile. These packages install via node-gyp-build, which uses
61
+ // a local prebuild when present and otherwise compiles from source — no network. By
62
+ // removing the (absent or wrong-arch) prebuilds plus any stale build, the rebuild is
63
+ // guaranteed to compile locally, and node-gyp-build loads build/Release ahead of
64
+ // prebuilds, so the correct binary always wins.
65
+ for (const n of PKGS) {
66
+ rmSync(`${ROOT}/node_modules/${n}/prebuilds`, { recursive: true, force: true });
67
+ rmSync(`${ROOT}/node_modules/${n}/build`, { recursive: true, force: true });
68
+ }
69
+ run(`npm rebuild ${PKGS.join(' ')}`);
70
+ } catch (err) {
71
+ console.error('[ensure-tree-sitter] rebuild command failed:', err.message);
72
+ }
73
+
74
+ if (bindingsLoad()) {
75
+ console.error('[ensure-tree-sitter] OK — bindings compiled from source and loadable.');
76
+ process.exit(0);
77
+ }
78
+
79
+ // Strict mode: turn the warning into a hard failure. Opt-in via
80
+ // HIVEMIND_STRICT_POSTINSTALL=1 — set by this repo's own CI workflows so a
81
+ // heal failure surfaces as a red check on the PR instead of getting swallowed
82
+ // and re-emerging downstream as `tsc: Cannot find module 'tree-sitter'`.
83
+ // Default stays non-fatal so end-user consumers of @deeplake/hivemind never
84
+ // get a hard install break — the runtime check at use-time is enough for them.
85
+ const strict = process.env.HIVEMIND_STRICT_POSTINSTALL === '1';
86
+ console.error(
87
+ '[ensure-tree-sitter] WARNING: tree-sitter bindings still unavailable. ' +
88
+ 'Install a C/C++ toolchain and re-run `npm run rebuild:native`.' +
89
+ (strict ? ' (strict mode — failing this install)' : ' (non-fatal)'),
90
+ );
91
+ process.exit(strict ? 1 : 0);
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // Refuse a publish if `npm pack` would include filenames that should
3
+ // never ship to npm — credentials, CI workflows, git internals.
4
+ // Catches a future PR widening package.json's `files` array (or
5
+ // switching to a permissive .npmignore) before any token is touched.
6
+
7
+ import { execFileSync } from 'node:child_process';
8
+
9
+ const FORBIDDEN = [
10
+ /(^|\/)\.npmrc$/,
11
+ /(^|\/)\.env($|\.)/,
12
+ /(^|\/)secrets?(\/|$)/,
13
+ /(^|\/)\.github(\/|$)/,
14
+ /(^|\/)\.git(\/|$)/,
15
+ ];
16
+
17
+ const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], {
18
+ encoding: 'utf8',
19
+ stdio: ['ignore', 'pipe', 'inherit'],
20
+ });
21
+ const entries = JSON.parse(raw)[0].files.map((f) => f.path);
22
+ const hits = entries.filter((p) => FORBIDDEN.some((rx) => rx.test(p)));
23
+
24
+ if (hits.length) {
25
+ console.error('Refusing to publish — forbidden filenames in tarball:');
26
+ for (const h of hits) console.error(' ' + h);
27
+ process.exit(1);
28
+ }
29
+ console.log(`pack-check OK — ${entries.length} files, no forbidden patterns`);
@@ -0,0 +1,15 @@
1
+ export declare const SCALAR_TARGETS: readonly string[];
2
+ export declare const MARKETPLACE_PATH: string;
3
+
4
+ export interface SyncResult {
5
+ writes: number;
6
+ skips: number;
7
+ version: string;
8
+ }
9
+
10
+ export interface SyncOptions {
11
+ root?: string;
12
+ log?: (msg: string) => void;
13
+ }
14
+
15
+ export declare function syncVersions(opts?: SyncOptions): SyncResult;
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // Sync the version field across all manifests from the single source-of-truth
3
+ // in package.json. Runs as a `prebuild` hook so esbuild's version-define
4
+ // inlines the same value into bundles.
5
+ //
6
+ // Idempotent: skips writes when a target already matches.
7
+ // Exits non-zero if any target file is missing or if package.json has no version.
8
+
9
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+
13
+ const SOURCE = "package.json";
14
+
15
+ // Scalar targets: each has a single top-level `version` field tracking package.json.
16
+ export const SCALAR_TARGETS = [
17
+ ".claude-plugin/plugin.json",
18
+ "claude-code/.claude-plugin/plugin.json",
19
+ "openclaw/openclaw.plugin.json",
20
+ "openclaw/package.json",
21
+ "codex/package.json",
22
+ ];
23
+
24
+ // Marketplace target: has BOTH metadata.version AND every plugins[].version.
25
+ export const MARKETPLACE_PATH = ".claude-plugin/marketplace.json";
26
+
27
+ function readJsonAt(root, relPath) {
28
+ const full = resolve(root, relPath);
29
+ if (!existsSync(full)) {
30
+ throw new Error(`sync-versions: missing target ${relPath}`);
31
+ }
32
+ return JSON.parse(readFileSync(full, "utf-8"));
33
+ }
34
+
35
+ function writeJsonAt(root, relPath, obj) {
36
+ writeFileSync(resolve(root, relPath), JSON.stringify(obj, null, 2) + "\n");
37
+ }
38
+
39
+ export function syncVersions({ root = process.cwd(), log = (m) => console.error(m) } = {}) {
40
+ const source = readJsonAt(root, SOURCE);
41
+ const version = source.version;
42
+ if (!version || typeof version !== "string") {
43
+ throw new Error(`sync-versions: ${SOURCE} has no string \`version\` field`);
44
+ }
45
+
46
+ let writes = 0;
47
+ let skips = 0;
48
+
49
+ for (const target of SCALAR_TARGETS) {
50
+ const data = readJsonAt(root, target);
51
+ if (data.version === version) {
52
+ log(`sync-versions: ${target} already at ${version}`);
53
+ skips++;
54
+ continue;
55
+ }
56
+ const old = data.version;
57
+ data.version = version;
58
+ writeJsonAt(root, target, data);
59
+ log(`sync-versions: ${target}: ${old} -> ${version}`);
60
+ writes++;
61
+ }
62
+
63
+ const marketplace = readJsonAt(root, MARKETPLACE_PATH);
64
+ let mpChanged = false;
65
+ if (marketplace.metadata?.version !== version) {
66
+ const old = marketplace.metadata?.version;
67
+ marketplace.metadata = marketplace.metadata || {};
68
+ marketplace.metadata.version = version;
69
+ log(`sync-versions: ${MARKETPLACE_PATH} metadata.version: ${old} -> ${version}`);
70
+ mpChanged = true;
71
+ }
72
+ if (Array.isArray(marketplace.plugins)) {
73
+ for (const plugin of marketplace.plugins) {
74
+ if (plugin.version !== version) {
75
+ const old = plugin.version;
76
+ plugin.version = version;
77
+ log(`sync-versions: ${MARKETPLACE_PATH} plugins[${plugin.name}].version: ${old} -> ${version}`);
78
+ mpChanged = true;
79
+ }
80
+ }
81
+ }
82
+ if (mpChanged) {
83
+ writeJsonAt(root, MARKETPLACE_PATH, marketplace);
84
+ writes++;
85
+ } else {
86
+ log(`sync-versions: ${MARKETPLACE_PATH} already at ${version}`);
87
+ skips++;
88
+ }
89
+
90
+ log(`sync-versions: ${writes} written, ${skips} unchanged`);
91
+ return { writes, skips, version };
92
+ }
93
+
94
+ // Script mode — only runs when invoked directly, not when imported by tests.
95
+ const __entryUrl = process.argv[1] ? fileURLToPath(import.meta.url) === resolve(process.argv[1]) : false;
96
+ if (__entryUrl) {
97
+ try {
98
+ syncVersions();
99
+ } catch (e) {
100
+ console.error(e.message);
101
+ process.exit(1);
102
+ }
103
+ }
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env bash
2
+ # Hivemind install verifier — sanity-checks each of the 9 agent integrations.
3
+ #
4
+ # Run this after `npx -y @deeplake/hivemind install`. It checks file
5
+ # placement, config schema, dry-invokes hook scripts, and exercises the
6
+ # MCP server against its initialize handshake. Doesn't require any agent
7
+ # to actually be running — just verifies our installer's footprint.
8
+ #
9
+ # Exits 0 if everything we installed is healthy; 1 if any agent fails.
10
+ # Skips agents whose marker dir doesn't exist (you don't have them).
11
+ #
12
+ # Usage:
13
+ # bash scripts/verify-install.sh
14
+ # curl -sSL https://raw.githubusercontent.com/activeloopai/hivemind/main/scripts/verify-install.sh | bash
15
+
16
+ set -u
17
+
18
+ PASS=0
19
+ FAIL=0
20
+ SKIP=0
21
+
22
+ green() { printf "\033[0;32m%s\033[0m" "$1"; }
23
+ red() { printf "\033[0;31m%s\033[0m" "$1"; }
24
+ gray() { printf "\033[0;90m%s\033[0m" "$1"; }
25
+ yellow(){ printf "\033[0;33m%s\033[0m" "$1"; }
26
+
27
+ ok() { echo " $(green PASS) $1"; PASS=$((PASS+1)); }
28
+ bad() { echo " $(red FAIL) $1${2:+ — $2}"; FAIL=$((FAIL+1)); }
29
+ skip() { echo " $(gray SKIP) $1${2:+ — $2}"; SKIP=$((SKIP+1)); }
30
+
31
+ section() { echo; echo "$(yellow "▎ $1")"; }
32
+
33
+ require_jq() {
34
+ command -v jq >/dev/null 2>&1 || {
35
+ echo "$(red "fatal:") jq not found on PATH (install jq for this script to run)" >&2
36
+ exit 2
37
+ }
38
+ }
39
+ require_jq
40
+
41
+ # ───────────────────────────────────────────────────────────────────────
42
+ # Claude Code — marketplace plugin
43
+ section "Claude Code"
44
+ if [ -d "$HOME/.claude" ]; then
45
+ if command -v claude >/dev/null 2>&1; then
46
+ if claude plugin list 2>/dev/null | grep -q "hivemind@hivemind"; then
47
+ ok "claude plugin list shows hivemind@hivemind"
48
+ if claude plugin list 2>/dev/null | grep -q "✔ enabled"; then
49
+ ok "plugin enabled"
50
+ else
51
+ bad "plugin not enabled" "run: claude plugin enable hivemind@hivemind"
52
+ fi
53
+ else
54
+ bad "claude plugin list does not show hivemind@hivemind" "rerun: hivemind claude install"
55
+ fi
56
+ else
57
+ bad "claude CLI not found on PATH" "Claude Code is not installed correctly"
58
+ fi
59
+ else
60
+ skip "Claude Code" "no ~/.claude"
61
+ fi
62
+
63
+ # ───────────────────────────────────────────────────────────────────────
64
+ # Codex — ~/.codex/hooks.json + bundle
65
+ section "Codex"
66
+ if [ -d "$HOME/.codex" ]; then
67
+ HOOKS="$HOME/.codex/hooks.json"
68
+ if [ -f "$HOOKS" ]; then
69
+ if jq -e '.hooks.SessionStart' "$HOOKS" >/dev/null 2>&1; then
70
+ ok "$HOOKS has SessionStart"
71
+ else
72
+ bad "$HOOKS missing SessionStart"
73
+ fi
74
+ else
75
+ bad "$HOOKS not found" "rerun: hivemind codex install"
76
+ fi
77
+ if [ -x "$HOME/.codex/hivemind/bundle/session-start.js" ]; then
78
+ ok "session-start.js executable"
79
+ else
80
+ bad "session-start.js missing or not executable"
81
+ fi
82
+ else
83
+ skip "Codex" "no ~/.codex"
84
+ fi
85
+
86
+ # ───────────────────────────────────────────────────────────────────────
87
+ # OpenClaw — ~/.openclaw/extensions/hivemind/
88
+ section "OpenClaw"
89
+ if [ -d "$HOME/.openclaw" ]; then
90
+ EXT="$HOME/.openclaw/extensions/hivemind"
91
+ if [ -f "$EXT/openclaw.plugin.json" ]; then
92
+ ok "openclaw.plugin.json present"
93
+ if jq -e '.contracts.tools' "$EXT/openclaw.plugin.json" >/dev/null 2>&1; then
94
+ ok "manifest declares contracts.tools"
95
+ else
96
+ bad "manifest missing contracts.tools (stale install)"
97
+ fi
98
+ else
99
+ bad "$EXT/openclaw.plugin.json not found" "rerun: hivemind claw install"
100
+ fi
101
+ [ -f "$EXT/dist/index.js" ] && ok "dist/index.js present" || bad "dist/index.js not found"
102
+ else
103
+ skip "OpenClaw" "no ~/.openclaw"
104
+ fi
105
+
106
+ # ───────────────────────────────────────────────────────────────────────
107
+ # Cursor — ~/.cursor/hooks.json (1.7+ schema)
108
+ section "Cursor"
109
+ if [ -d "$HOME/.cursor" ]; then
110
+ HOOKS="$HOME/.cursor/hooks.json"
111
+ if [ -f "$HOOKS" ]; then
112
+ for ev in sessionStart beforeSubmitPrompt postToolUse afterAgentResponse stop sessionEnd; do
113
+ if jq -e ".hooks.$ev[0].command" "$HOOKS" >/dev/null 2>&1; then
114
+ ok "hooks.$ev wired"
115
+ else
116
+ bad "hooks.$ev missing" "rerun: hivemind cursor install"
117
+ fi
118
+ done
119
+ # Dry-invoke session-start.js
120
+ payload='{"hook_event_name":"sessionStart","session_id":"verify","conversation_id":"verify","workspace_roots":["/tmp"]}'
121
+ if echo "$payload" | timeout 5 node "$HOME/.cursor/hivemind/bundle/session-start.js" 2>/dev/null | jq -e '.additional_context' >/dev/null 2>&1; then
122
+ ok "session-start.js produces valid additional_context JSON"
123
+ else
124
+ bad "session-start.js dry-invoke did not return additional_context"
125
+ fi
126
+ else
127
+ bad "$HOOKS not found" "rerun: hivemind cursor install"
128
+ fi
129
+ else
130
+ skip "Cursor" "no ~/.cursor"
131
+ fi
132
+
133
+ # ───────────────────────────────────────────────────────────────────────
134
+ # Hermes Agent — skill drop
135
+ section "Hermes Agent"
136
+ if [ -d "$HOME/.hermes" ]; then
137
+ SKILL="$HOME/.hermes/skills/hivemind-memory/SKILL.md"
138
+ if [ -f "$SKILL" ]; then
139
+ ok "SKILL.md present at $SKILL"
140
+ if grep -q "Hivemind Memory" "$SKILL"; then
141
+ ok "skill content looks right"
142
+ else
143
+ bad "skill content missing 'Hivemind Memory' header"
144
+ fi
145
+ else
146
+ bad "$SKILL not found" "rerun: hivemind hermes install"
147
+ fi
148
+ else
149
+ skip "Hermes Agent" "no ~/.hermes"
150
+ fi
151
+
152
+ # ───────────────────────────────────────────────────────────────────────
153
+ # pi — AGENTS.md + extension
154
+ # No per-agent SKILL.md: pi reads skills from both ~/.pi/agent/skills/ AND
155
+ # ~/.agents/skills/ (the agentskills.io shared dir), so dropping a local
156
+ # skill would collide with the codex installer's shared symlink. AGENTS.md
157
+ # (auto-loaded every turn) plus the registered hivemind tools cover the
158
+ # action surface; see install-pi.ts for the rationale.
159
+ section "pi"
160
+ if [ -d "$HOME/.pi" ]; then
161
+ AGENTS="$HOME/.pi/agent/AGENTS.md"
162
+ if [ -f "$AGENTS" ] && grep -q "BEGIN hivemind-memory" "$AGENTS"; then
163
+ ok "AGENTS.md has BEGIN hivemind-memory marker"
164
+ else
165
+ bad "AGENTS.md missing hivemind block" "rerun: hivemind pi install"
166
+ fi
167
+ EXT="$HOME/.pi/agent/extensions/hivemind.ts"
168
+ if [ -f "$EXT" ]; then
169
+ ok "extension installed at $EXT"
170
+ # Sanity-check that key surfaces are wired in the installed extension.
171
+ if grep -q "registerTool" "$EXT" && grep -q "hivemind_search" "$EXT" && \
172
+ grep -q "hivemind_read" "$EXT" && grep -q "hivemind_index" "$EXT"; then
173
+ ok "extension registers hivemind_search / hivemind_read / hivemind_index"
174
+ else
175
+ bad "extension missing one or more hivemind_* tool registrations"
176
+ fi
177
+ if grep -q 'pi.on("tool_result"' "$EXT" && grep -q 'pi.on("input"' "$EXT"; then
178
+ ok "extension subscribes to input + tool_result for autocapture"
179
+ else
180
+ bad "extension missing input/tool_result subscriptions"
181
+ fi
182
+ else
183
+ bad "$EXT not found" "rerun: hivemind pi install"
184
+ fi
185
+ else
186
+ skip "pi" "no ~/.pi"
187
+ fi
188
+
189
+ # ───────────────────────────────────────────────────────────────────────
190
+ # MCP server (used by Hermes; reused by any future MCP-aware client).
191
+ section "Hivemind MCP server"
192
+ SERVER="$HOME/.hivemind/mcp/server.js"
193
+ if [ -f "$SERVER" ]; then
194
+ ok "server.js installed at $SERVER"
195
+ # Initialize + tools/list handshake. Expect "hivemind_search" in response.
196
+ init='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"verify","version":"1.0"}}}'
197
+ inited='{"jsonrpc":"2.0","method":"notifications/initialized"}'
198
+ list='{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
199
+ resp=$( ( printf '%s\n%s\n%s\n' "$init" "$inited" "$list"; sleep 1 ) | timeout 8 node "$SERVER" 2>/dev/null )
200
+ if echo "$resp" | grep -q '"hivemind_search"'; then
201
+ ok "tools/list returns hivemind_search / hivemind_read / hivemind_index"
202
+ else
203
+ bad "MCP server did not list expected tools" "check ~/.deeplake/credentials.json"
204
+ fi
205
+ else
206
+ skip "MCP server" "no ~/.hivemind/mcp/server.js (no MCP-aware agent installed yet)"
207
+ fi
208
+
209
+ # ───────────────────────────────────────────────────────────────────────
210
+ echo
211
+ echo "$(yellow "▎ Summary")"
212
+ echo " $(green PASS): $PASS"
213
+ echo " $(red FAIL): $FAIL"
214
+ echo " $(gray SKIP): $SKIP (agent not present on this machine)"
215
+ echo
216
+
217
+ if [ "$FAIL" -gt 0 ]; then exit 1; fi
218
+ exit 0